1" Language:    OCaml
2" Maintainer:  David Baelde        <firstname.name@ens-lyon.org>
3"              Mike Leary          <leary@nwlink.com>
4"              Markus Mottl        <markus.mottl@gmail.com>
5"              Stefano Zacchiroli  <zack@bononia.it>
6"              Vincent Aravantinos <firstname.name@imag.fr>
7" URL:         http://www.ocaml.info/vim/ftplugin/ocaml.vim
8" Last Change: 2010 Jul 10 - Bugfix, thanks to Pat Rondon
9"              2008 Jul 17 - Bugfix related to fnameescape (VA)
10"              2007 Sep 09 - Added .annot support for ocamlbuild, python not 
11"                            needed anymore (VA)
12"              2006 May 01 - Added .annot support for file.whateverext (SZ)
13"	             2006 Apr 11 - Fixed an initialization bug; fixed ASS abbrev (MM)
14"              2005 Oct 13 - removed GPL; better matchit support (MM, SZ)
15"
16if exists("b:did_ftplugin")
17  finish
18endif
19let b:did_ftplugin=1
20
21" some macro
22if exists('*fnameescape')
23  function! s:Fnameescape(s)
24    return fnameescape(a:s)
25  endfun
26else
27  function! s:Fnameescape(s)
28    return escape(a:s," \t\n*?[{`$\\%#'\"|!<")
29  endfun
30endif
31
32" Error handling -- helps moving where the compiler wants you to go
33let s:cposet=&cpoptions
34set cpo-=C
35setlocal efm=
36      \%EFile\ \"%f\"\\,\ line\ %l\\,\ characters\ %c-%*\\d:,
37      \%EFile\ \"%f\"\\,\ line\ %l\\,\ character\ %c:%m,
38      \%+EReference\ to\ unbound\ regexp\ name\ %m,
39      \%Eocamlyacc:\ e\ -\ line\ %l\ of\ \"%f\"\\,\ %m,
40      \%Wocamlyacc:\ w\ -\ %m,
41      \%-Zmake%.%#,
42      \%C%m,
43      \%D%*\\a[%*\\d]:\ Entering\ directory\ `%f',
44      \%X%*\\a[%*\\d]:\ Leaving\ directory\ `%f',
45      \%D%*\\a:\ Entering\ directory\ `%f',
46      \%X%*\\a:\ Leaving\ directory\ `%f',
47      \%DMaking\ %*\\a\ in\ %f
48
49" Add mappings, unless the user didn't want this.
50if !exists("no_plugin_maps") && !exists("no_ocaml_maps")
51  " (un)commenting
52  if !hasmapto('<Plug>Comment')
53    nmap <buffer> <LocalLeader>c <Plug>LUncomOn
54    vmap <buffer> <LocalLeader>c <Plug>BUncomOn
55    nmap <buffer> <LocalLeader>C <Plug>LUncomOff
56    vmap <buffer> <LocalLeader>C <Plug>BUncomOff
57  endif
58
59  nnoremap <buffer> <Plug>LUncomOn mz0i(* <ESC>$A *)<ESC>`z
60  nnoremap <buffer> <Plug>LUncomOff :s/^(\* \(.*\) \*)/\1/<CR>:noh<CR>
61  vnoremap <buffer> <Plug>BUncomOn <ESC>:'<,'><CR>`<O<ESC>0i(*<ESC>`>o<ESC>0i*)<ESC>`<
62  vnoremap <buffer> <Plug>BUncomOff <ESC>:'<,'><CR>`<dd`>dd`<
63
64  if !hasmapto('<Plug>Abbrev')
65    iabbrev <buffer> ASS (assert (0=1) (* XXX *))
66  endif
67endif
68
69" Let % jump between structure elements (due to Issac Trotts)
70let b:mw = ''
71let b:mw = b:mw . ',\<let\>:\<and\>:\(\<in\>\|;;\)'
72let b:mw = b:mw . ',\<if\>:\<then\>:\<else\>'
73let b:mw = b:mw . ',\<\(for\|while\)\>:\<do\>:\<done\>,'
74let b:mw = b:mw . ',\<\(object\|sig\|struct\|begin\)\>:\<end\>'
75let b:mw = b:mw . ',\<\(match\|try\)\>:\<with\>'
76let b:match_words = b:mw
77
78let b:match_ignorecase=0
79
80" switching between interfaces (.mli) and implementations (.ml)
81if !exists("g:did_ocaml_switch")
82  let g:did_ocaml_switch = 1
83  map <LocalLeader>s :call OCaml_switch(0)<CR>
84  map <LocalLeader>S :call OCaml_switch(1)<CR>
85  fun OCaml_switch(newwin)
86    if (match(bufname(""), "\\.mli$") >= 0)
87      let fname = s:Fnameescape(substitute(bufname(""), "\\.mli$", ".ml", ""))
88      if (a:newwin == 1)
89        exec "new " . fname
90      else
91        exec "arge " . fname
92      endif
93    elseif (match(bufname(""), "\\.ml$") >= 0)
94      let fname = s:Fnameescape(bufname("")) . "i"
95      if (a:newwin == 1)
96        exec "new " . fname
97      else
98        exec "arge " . fname
99      endif
100    endif
101  endfun
102endif
103
104" Folding support
105
106" Get the modeline because folding depends on indentation
107let s:s = line2byte(line('.'))+col('.')-1
108if search('^\s*(\*:o\?caml:')
109  let s:modeline = getline(".")
110else
111  let s:modeline = ""
112endif
113if s:s > 0
114  exe 'goto' s:s
115endif
116
117" Get the indentation params
118let s:m = matchstr(s:modeline,'default\s*=\s*\d\+')
119if s:m != ""
120  let s:idef = matchstr(s:m,'\d\+')
121elseif exists("g:omlet_indent")
122  let s:idef = g:omlet_indent
123else
124  let s:idef = 2
125endif
126let s:m = matchstr(s:modeline,'struct\s*=\s*\d\+')
127if s:m != ""
128  let s:i = matchstr(s:m,'\d\+')
129elseif exists("g:omlet_indent_struct")
130  let s:i = g:omlet_indent_struct
131else
132  let s:i = s:idef
133endif
134
135" Set the folding method
136if exists("g:ocaml_folding")
137  setlocal foldmethod=expr
138  setlocal foldexpr=OMLetFoldLevel(v:lnum)
139endif
140
141" - Only definitions below, executed once -------------------------------------
142
143if exists("*OMLetFoldLevel")
144  finish
145endif
146
147function s:topindent(lnum)
148  let l = a:lnum
149  while l > 0
150    if getline(l) =~ '\s*\%(\<struct\>\|\<sig\>\|\<object\>\)'
151      return indent(l)
152    endif
153    let l = l-1
154  endwhile
155  return -s:i
156endfunction
157
158function OMLetFoldLevel(l)
159
160  " This is for not merging blank lines around folds to them
161  if getline(a:l) !~ '\S'
162    return -1
163  endif
164
165  " We start folds for modules, classes, and every toplevel definition
166  if getline(a:l) =~ '^\s*\%(\<val\>\|\<module\>\|\<class\>\|\<type\>\|\<method\>\|\<initializer\>\|\<inherit\>\|\<exception\>\|\<external\>\)'
167    exe 'return ">' (indent(a:l)/s:i)+1 '"'
168  endif
169
170  " Toplevel let are detected thanks to the indentation
171  if getline(a:l) =~ '^\s*let\>' && indent(a:l) == s:i+s:topindent(a:l)
172    exe 'return ">' (indent(a:l)/s:i)+1 '"'
173  endif
174
175  " We close fold on end which are associated to struct, sig or object.
176  " We use syntax information to do that.
177  if getline(a:l) =~ '^\s*end\>' && synIDattr(synID(a:l, indent(a:l)+1, 0), "name") != "ocamlKeyword"
178    return (indent(a:l)/s:i)+1
179  endif
180
181  " Folds end on ;;
182  if getline(a:l) =~ '^\s*;;'
183    exe 'return "<' (indent(a:l)/s:i)+1 '"'
184  endif
185
186  " Comments around folds aren't merged to them.
187  if synIDattr(synID(a:l, indent(a:l)+1, 0), "name") == "ocamlComment"
188    return -1
189  endif
190
191  return '='
192endfunction
193
194" Vim support for OCaml .annot files
195"
196" Last Change: 2007 Jul 17
197" Maintainer:  Vincent Aravantinos <vincent.aravantinos@gmail.com>
198" License:     public domain
199"
200" Originally inspired by 'ocaml-dtypes.vim' by Stefano Zacchiroli.
201" The source code is quite radically different for we not use python anymore.
202" However this plugin should have the exact same behaviour, that's why the
203" following lines are the quite exact copy of Stefano's original plugin :
204"
205" <<
206" Executing Ocaml_print_type(<mode>) function will display in the Vim bottom
207" line(s) the type of an ocaml value getting it from the corresponding .annot
208" file (if any).  If Vim is in visual mode, <mode> should be "visual" and the
209" selected ocaml value correspond to the highlighted text, otherwise (<mode>
210" can be anything else) it corresponds to the literal found at the current
211" cursor position.
212"
213" Typing '<LocalLeader>t' (LocalLeader defaults to '\', see :h LocalLeader)
214" will cause " Ocaml_print_type function to be invoked with the right 
215" argument depending on the current mode (visual or not).
216" >>
217"
218" If you find something not matching this behaviour, please signal it.
219"
220" Differences are:
221"   - no need for python support
222"     + plus : more portable
223"     + minus: no more lazy parsing, it looks very fast however
224"     
225"   - ocamlbuild support, ie.
226"     + the plugin finds the _build directory and looks for the 
227"       corresponding file inside;
228"     + if the user decides to change the name of the _build directory thanks
229"       to the '-build-dir' option of ocamlbuild, the plugin will manage in
230"       most cases to find it out (most cases = if the source file has a unique
231"       name among your whole project);
232"     + if ocamlbuild is not used, the usual behaviour holds; ie. the .annot
233"       file should be in the same directory as the source file;
234"     + for vim plugin programmers:
235"       the variable 'b:_build_dir' contains the inferred path to the build 
236"       directory, even if this one is not named '_build'.
237"
238" Bonus :
239"   - latin1 accents are handled
240"   - lists are handled, even on multiple lines, you don't need the visual mode
241"     (the cursor must be on the first bracket)
242"   - parenthesized expressions, arrays, and structures (ie. '(...)', '[|...|]',
243"     and '{...}') are handled the same way
244
245  " Copied from Stefano's original plugin :
246  " <<
247  "      .annot ocaml file representation
248  "
249  "      File format (copied verbatim from caml-types.el)
250  "
251  "      file ::= block *
252  "      block ::= position <SP> position <LF> annotation *
253  "      position ::= filename <SP> num <SP> num <SP> num
254  "      annotation ::= keyword open-paren <LF> <SP> <SP> data <LF> close-paren
255  "
256  "      <SP> is a space character (ASCII 0x20)
257  "      <LF> is a line-feed character (ASCII 0x0A)
258  "      num is a sequence of decimal digits
259  "      filename is a string with the lexical conventions of O'Caml
260  "      open-paren is an open parenthesis (ASCII 0x28)
261  "      close-paren is a closed parenthesis (ASCII 0x29)
262  "      data is any sequence of characters where <LF> is always followed by
263  "           at least two space characters.
264  "
265  "      - in each block, the two positions are respectively the start and the
266  "        end of the range described by the block.
267  "      - in a position, the filename is the name of the file, the first num
268  "        is the line number, the second num is the offset of the beginning
269  "        of the line, the third num is the offset of the position itself.
270  "      - the char number within the line is the difference between the third
271  "        and second nums.
272  "
273  "      For the moment, the only possible keyword is \"type\"."
274  " >>
275
276
277" 1. Finding the annotation file even if we use ocamlbuild
278
279    " In:  two strings representing paths
280    " Out: one string representing the common prefix between the two paths
281  function! s:Find_common_path (p1,p2)
282    let temp = a:p2
283    while matchstr(a:p1,temp) == ''
284      let temp = substitute(temp,'/[^/]*$','','')
285    endwhile
286    return temp
287  endfun
288
289    " After call:
290    " - b:annot_file_path : 
291    "                       path to the .annot file corresponding to the
292    "                       source file (dealing with ocamlbuild stuff)
293    " - b:_build_path: 
294    "                       path to the build directory even if this one is
295    "                       not named '_build'
296  function! s:Locate_annotation()
297    if !b:annotation_file_located
298
299      silent exe 'cd' s:Fnameescape(expand('%:p:h'))
300
301      let annot_file_name = s:Fnameescape(expand('%:r')).'.annot'
302
303      " 1st case : the annot file is in the same directory as the buffer (no ocamlbuild)
304      let b:annot_file_path = findfile(annot_file_name,'.')
305      if b:annot_file_path != ''
306        let b:annot_file_path = getcwd().'/'.b:annot_file_path
307        let b:_build_path = ''
308      else
309        " 2nd case : the buffer and the _build directory are in the same directory
310        "      ..
311        "     /  \
312        "    /    \
313        " _build  .ml
314        "
315        let b:_build_path = finddir('_build','.')
316        if b:_build_path != ''
317          let b:_build_path = getcwd().'/'.b:_build_path
318          let b:annot_file_path           = findfile(annot_file_name,'_build')
319          if b:annot_file_path != ''
320            let b:annot_file_path = getcwd().'/'.b:annot_file_path
321          endif
322        else
323          " 3rd case : the _build directory is in a directory higher in the file hierarchy 
324          "            (it can't be deeper by ocamlbuild requirements)
325          "      ..
326          "     /  \
327          "    /    \
328          " _build  ...
329          "           \
330          "            \
331          "           .ml
332          "
333          let b:_build_path = finddir('_build',';')
334          if b:_build_path != ''
335            let project_path                = substitute(b:_build_path,'/_build$','','')
336            let path_relative_to_project    = s:Fnameescape(substitute(expand('%:p:h'),project_path.'/','',''))
337            let b:annot_file_path           = findfile(annot_file_name,project_path.'/_build/'.path_relative_to_project)
338          else
339            let b:annot_file_path = findfile(annot_file_name,'**')
340            "4th case : what if the user decided to change the name of the _build directory ?
341            "           -> we relax the constraints, it should work in most cases
342            if b:annot_file_path != ''
343              " 4a. we suppose the renamed _build directory is in the current directory
344              let b:_build_path = matchstr(b:annot_file_path,'^[^/]*')
345              if b:annot_file_path != ''
346                let b:annot_file_path = getcwd().'/'.b:annot_file_path
347                let b:_build_path     = getcwd().'/'.b:_build_path
348              endif
349            else
350              " 4b. anarchy : the renamed _build directory may be higher in the hierarchy
351              " this will work if the file for which we are looking annotations has a unique name in the whole project
352              " if this is not the case, it may still work, but no warranty here
353              let b:annot_file_path = findfile(annot_file_name,'**;')
354              let project_path      = s:Find_common_path(b:annot_file_path,expand('%:p:h'))
355              let b:_build_path       = matchstr(b:annot_file_path,project_path.'/[^/]*')
356            endif
357          endif
358        endif
359      endif
360
361      if b:annot_file_path == ''
362        throw 'E484: no annotation file found'
363      endif
364
365      silent exe 'cd' '-'
366
367      let b:annotation_file_located = 1
368    endif
369  endfun
370
371  " This in order to locate the .annot file only once
372  let b:annotation_file_located = 0
373
374" 2. Finding the type information in the annotation file
375  
376  " a. The annotation file is opened in vim as a buffer that
377  " should be (almost) invisible to the user.
378
379      " After call:
380      " The current buffer is now the one containing the .annot file.
381      " We manage to keep all this hidden to the user's eye.
382    function! s:Enter_annotation_buffer()
383      let s:current_pos = getpos('.')
384      let s:current_hidden = &l:hidden
385      set hidden
386      let s:current_buf = bufname('%')
387      if bufloaded(b:annot_file_path)
388        silent exe 'keepj keepalt' 'buffer' s:Fnameescape(b:annot_file_path)
389      else
390        silent exe 'keepj keepalt' 'view' s:Fnameescape(b:annot_file_path)
391      endif
392    endfun
393
394      " After call:
395      "   The original buffer has been restored in the exact same state as before.
396    function! s:Exit_annotation_buffer()
397      silent exe 'keepj keepalt' 'buffer' s:Fnameescape(s:current_buf)
398      let &l:hidden = s:current_hidden
399      call setpos('.',s:current_pos)
400    endfun
401
402      " After call:
403      "   The annot file is loaded and assigned to a buffer.
404      "   This also handles the modification date of the .annot file, eg. after a 
405      "   compilation.
406    function! s:Load_annotation()
407      if bufloaded(b:annot_file_path) && b:annot_file_last_mod < getftime(b:annot_file_path)
408        call s:Enter_annotation_buffer()
409        silent exe "bunload"
410        call s:Exit_annotation_buffer()
411      endif
412      if !bufloaded(b:annot_file_path)
413        call s:Enter_annotation_buffer()
414        setlocal nobuflisted
415        setlocal bufhidden=hide
416        setlocal noswapfile
417        setlocal buftype=nowrite
418        call s:Exit_annotation_buffer()
419        let b:annot_file_last_mod = getftime(b:annot_file_path)
420      endif
421    endfun
422  
423  "b. 'search' and 'match' work to find the type information
424   
425      "In:  - lin1,col1: postion of expression first char
426      "     - lin2,col2: postion of expression last char
427      "Out: - the pattern to be looked for to find the block
428      " Must be called in the source buffer (use of line2byte)
429    function! s:Block_pattern(lin1,lin2,col1,col2)
430      let start_num1 = a:lin1
431      let start_num2 = line2byte(a:lin1) - 1
432      let start_num3 = start_num2 + a:col1
433      let path       = '"\(\\"\|[^"]\)\+"'
434      let start_pos  = path.' '.start_num1.' '.start_num2.' '.start_num3
435      let end_num1   = a:lin2
436      let end_num2   = line2byte(a:lin2) - 1
437      let end_num3   = end_num2 + a:col2
438      let end_pos    = path.' '.end_num1.' '.end_num2.' '.end_num3
439      return '^'.start_pos.' '.end_pos."$"
440      " rq: the '^' here is not totally correct regarding the annot file "grammar"
441      " but currently the annotation file respects this, and it's a little bit faster with the '^';
442      " can be removed safely.
443    endfun
444
445      "In: (the cursor position should be at the start of an annotation)
446      "Out: the type information
447      " Must be called in the annotation buffer (use of search)
448    function! s:Match_data()
449      " rq: idem as previously, in the following, the '^' at start of patterns is not necessary
450      keepj while search('^type($','ce',line(".")) == 0
451        keepj if search('^.\{-}($','e') == 0
452          throw "no_annotation"
453        endif
454        keepj if searchpair('(','',')') == 0
455          throw "malformed_annot_file"
456        endif
457      endwhile
458      let begin = line(".") + 1
459      keepj if searchpair('(','',')') == 0
460        throw "malformed_annot_file"
461      endif
462      let end = line(".") - 1
463      return join(getline(begin,end),"\n")
464    endfun
465        
466      "In:  the pattern to look for in order to match the block
467      "Out: the type information (calls s:Match_data)
468      " Should be called in the annotation buffer
469    function! s:Extract_type_data(block_pattern)
470      call s:Enter_annotation_buffer()
471      try
472        if search(a:block_pattern,'e') == 0
473          throw "no_annotation"
474        endif
475        call cursor(line(".") + 1,1)
476        let annotation = s:Match_data()
477      finally
478        call s:Exit_annotation_buffer()
479      endtry
480      return annotation
481    endfun
482  
483  "c. link this stuff with what the user wants
484  " ie. get the expression selected/under the cursor
485    
486    let s:ocaml_word_char = '\w|[�-�]|'''
487
488      "In:  the current mode (eg. "visual", "normal", etc.)
489      "Out: the borders of the expression we are looking for the type
490    function! s:Match_borders(mode)
491      if a:mode == "visual"
492        let cur = getpos(".")
493        normal `<
494        let col1 = col(".")
495        let lin1 = line(".")
496        normal `>
497        let col2 = col(".")
498        let lin2 = line(".")
499        call cursor(cur[1],cur[2])
500        return [lin1,lin2,col1-1,col2]
501      else
502        let cursor_line = line(".")
503        let cursor_col  = col(".")
504        let line = getline('.')
505        if line[cursor_col-1:cursor_col] == '[|'
506          let [lin2,col2] = searchpairpos('\[|','','|\]','n')
507          return [cursor_line,lin2,cursor_col-1,col2+1]
508        elseif     line[cursor_col-1] == '['
509          let [lin2,col2] = searchpairpos('\[','','\]','n')
510          return [cursor_line,lin2,cursor_col-1,col2]
511        elseif line[cursor_col-1] == '('
512          let [lin2,col2] = searchpairpos('(','',')','n')
513          return [cursor_line,lin2,cursor_col-1,col2]
514        elseif line[cursor_col-1] == '{'
515          let [lin2,col2] = searchpairpos('{','','}','n')
516          return [cursor_line,lin2,cursor_col-1,col2]
517        else
518          let [lin1,col1] = searchpos('\v%('.s:ocaml_word_char.'|\.)*','ncb')
519          let [lin2,col2] = searchpos('\v%('.s:ocaml_word_char.'|\.)*','nce')
520          if col1 == 0 || col2 == 0
521            throw "no_expression"
522          endif
523          return [cursor_line,cursor_line,col1-1,col2]
524        endif
525      endif
526    endfun
527
528      "In:  the current mode (eg. "visual", "normal", etc.)
529      "Out: the type information (calls s:Extract_type_data)
530    function! s:Get_type(mode)
531      let [lin1,lin2,col1,col2] = s:Match_borders(a:mode)
532      return s:Extract_type_data(s:Block_pattern(lin1,lin2,col1,col2))
533    endfun
534  
535  "d. main
536      "In:         the current mode (eg. "visual", "normal", etc.)
537      "After call: the type information is displayed
538    if !exists("*Ocaml_get_type")
539      function Ocaml_get_type(mode)
540        call s:Locate_annotation()
541        call s:Load_annotation()
542        return s:Get_type(a:mode)
543      endfun
544    endif
545
546    if !exists("*Ocaml_get_type_or_not")
547      function Ocaml_get_type_or_not(mode)
548        let t=reltime()
549        try
550          return Ocaml_get_type(a:mode)
551        catch
552          return ""
553        endtry
554      endfun
555    endif
556
557    if !exists("*Ocaml_print_type")
558      function Ocaml_print_type(mode)
559        if expand("%:e") == "mli"
560          echohl ErrorMsg | echo "No annotations for interface (.mli) files" | echohl None
561          return
562        endif
563        try
564          echo Ocaml_get_type(a:mode)
565        catch /E484:/
566          echohl ErrorMsg | echo "No type annotations (.annot) file found" | echohl None
567        catch /no_expression/
568          echohl ErrorMsg | echo "No expression found under the cursor" | echohl None
569        catch /no_annotation/
570          echohl ErrorMsg | echo "No type annotation found for the given text" | echohl None
571        catch /malformed_annot_file/
572          echohl ErrorMsg | echo "Malformed .annot file" | echohl None
573        endtry
574      endfun
575    endif
576
577" Maps
578  map  <silent> <LocalLeader>t :call Ocaml_print_type("normal")<CR>
579  vmap <silent> <LocalLeader>t :<C-U>call Ocaml_print_type("visual")<CR>`<
580
581let &cpoptions=s:cposet
582unlet s:cposet
583
584" vim:sw=2 fdm=indent
585