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