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