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