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