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