diff options
Diffstat (limited to 'indent/mp.vim')
| -rw-r--r-- | indent/mp.vim | 364 | 
1 files changed, 364 insertions, 0 deletions
| diff --git a/indent/mp.vim b/indent/mp.vim new file mode 100644 index 00000000..36c3f716 --- /dev/null +++ b/indent/mp.vim @@ -0,0 +1,364 @@ +if !exists('g:polyglot_disabled') || index(g:polyglot_disabled, 'vim') == -1 +   +" MetaPost indent file +" Language:           MetaPost +" Maintainer:         Nicola Vitacolonna <nvitacolonna@gmail.com> +" Former Maintainers: Eugene Minkovskii <emin@mccme.ru> +" Last Change:        2016 Oct 2, 4:13pm +" Version: 0.2 + +if exists("b:did_indent") +  finish +endif +let b:did_indent = 1 + +setlocal indentexpr=GetMetaPostIndent() +setlocal indentkeys+==end,=else,=fi,=fill,0),0] + +let b:undo_indent = "setl indentkeys< indentexpr<" + +" Only define the function once. +if exists("*GetMetaPostIndent") +  finish +endif +let s:keepcpo= &cpo +set cpo&vim + +function GetMetaPostIndent() +  let ignorecase_save = &ignorecase +  try +    let &ignorecase = 0 +    return GetMetaPostIndentIntern() +  finally +    let &ignorecase = ignorecase_save +  endtry +endfunc + +" Regexps {{{ +" Note: the next three variables are made global so that a user may add +" further keywords. +" +" Example: +" +"    Put these in ~/.vim/after/indent/mp.vim +" +"    let g:mp_open_tag .= '\|\<begintest\>' +"    let g:mp_close_tag .= '\|\<endtest\>' + +" Expressions starting indented blocks +let g:mp_open_tag = '' +      \ . '\<if\>' +      \ . '\|\<else\%[if]\>' +      \ . '\|\<for\%(\|ever\|suffixes\)\>' +      \ . '\|\<begingroup\>' +      \ . '\|\<\%(\|var\|primary\|secondary\|tertiary\)def\>' +      \ . '\|^\s*\<begin\%(fig\|graph\|glyph\|char\|logochar\)\>' +      \ . '\|[([{]' + +" Expressions ending indented blocks +let g:mp_close_tag = '' +      \ . '\<fi\>' +      \ . '\|\<else\%[if]\>' +      \ . '\|\<end\%(\|for\|group\|def\|fig\|char\|glyph\|graph\)\>' +      \ . '\|[)\]}]' + +" Statements that may span multiple lines and are ended by a semicolon. To +" keep this list short, statements that are unlikely to be very long or are +" not very common (e.g., keywords like `interim` or `showtoken`) are not +" included. +" +" The regex for assignments and equations (the last branch) is tricky, because +" it must not match things like `for i :=`, `if a=b`, `def...=`, etc... It is +" not perfect, but it works reasonably well. +let g:mp_statement = '' +      \ . '\<\%(\|un\|cut\)draw\>' +      \ . '\|\<\%(\|un\)fill\%[draw]\>' +      \ . '\|\<draw\%(dbl\)\=arrow\>' +      \ . '\|\<clip\>' +      \ . '\|\<addto\>' +      \ . '\|\<save\>' +      \ . '\|\<setbounds\>' +      \ . '\|\<message\>' +      \ . '\|\<errmessage\>' +      \ . '\|\<errhelp\>' +      \ . '\|\<fontmapline\>' +      \ . '\|\<pickup\>' +      \ . '\|\<show\>' +      \ . '\|\<special\>' +      \ . '\|\<write\>' +      \ . '\|\%(^\|;\)\%([^;=]*\%('.g:mp_open_tag.'\)\)\@!.\{-}:\==' + +" A line ends with zero or more spaces, possibly followed by a comment. +let s:eol = '\s*\%($\|%\)' +" }}} + +" Auxiliary functions {{{ +" Returns 1 if (0-based) position immediately preceding `pos` in `line` is +" inside a string or a comment; returns 0 otherwise. + +" This is the function that is called more often when indenting, so it is +" critical that it is efficient. The method we use is significantly faster +" than using syntax attributes, and more general (it does not require +" syntax_items). It is also faster than using a single regex matching an even +" number of quotes. It helps that MetaPost strings cannot span more than one +" line and cannot contain escaped quotes. +function! s:CommentOrString(line, pos) +  let in_string = 0 +  let q = stridx(a:line, '"') +  let c = stridx(a:line, '%') +  while q >= 0 && q < a:pos +    if c >= 0 && c < q +      if in_string " Find next percent symbol +        let c = stridx(a:line, '%', q + 1) +      else " Inside comment +        return 1 +      endif +    endif +    let in_string = 1 - in_string +    let q = stridx(a:line, '"', q + 1) " Find next quote +  endwhile +  return in_string || (c >= 0 && c <= a:pos) +endfunction + +" Find the first non-comment non-blank line before the current line. +function! s:PrevNonBlankNonComment(lnum) +  let l:lnum = prevnonblank(a:lnum - 1) +  while getline(l:lnum) =~# '^\s*%' +    let l:lnum = prevnonblank(l:lnum - 1) +  endwhile +  return l:lnum +endfunction + +" Returns true if the last tag appearing in the line is an open tag; returns +" false otherwise. +function! s:LastTagIsOpen(line) +  let o = s:LastValidMatchEnd(a:line, g:mp_open_tag, 0) +  if o == - 1 | return v:false | endif +  return s:LastValidMatchEnd(a:line, g:mp_close_tag, o) < 0 +endfunction + +" A simple, efficient and quite effective heuristics is used to test whether +" a line should cause the next line to be indented: count the "opening tags" +" (if, for, def, ...) in the line, count the "closing tags" (endif, endfor, +" ...) in the line, and compute the difference. We call the result the +" "weight" of the line. If the weight is positive, then the next line should +" most likely be indented. Note that `else` and `elseif` are both opening and +" closing tags, so they "cancel out" in almost all cases, the only exception +" being a leading `else[if]`, which is counted as an opening tag, but not as +" a closing tag (so that, for instance, a line containing a single `else:` +" will have weight equal to one, not zero). We do not treat a trailing +" `else[if]` in any special way, because lines ending with an open tag are +" dealt with separately before this function is called (see +" GetMetaPostIndentIntern()). +" +" Example: +" +"     forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi +"       % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2) +"     endfor + +function! s:Weight(line) +  let [o, i] = [0, s:ValidMatchEnd(a:line, g:mp_open_tag, 0)] +  while i > 0 +    let o += 1 +    let i = s:ValidMatchEnd(a:line, g:mp_open_tag, i) +  endwhile +  let [c, i] = [0, matchend(a:line, '^\s*\<else\%[if]\>')] " Skip a leading else[if] +  let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i) +  while i > 0 +    let c += 1 +    let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i) +  endwhile +  return o - c +endfunction + +" Similar to matchend(), but skips strings and comments. +" line: a String +function! s:ValidMatchEnd(line, pat, start) +  let i = matchend(a:line, a:pat, a:start) +  while i > 0 && s:CommentOrString(a:line, i) +    let i = matchend(a:line, a:pat, i) +  endwhile +  return i +endfunction + +" Like s:ValidMatchEnd(), but returns the end position of the last (i.e., +" rightmost) match. +function! s:LastValidMatchEnd(line, pat, start) +  let last_found = -1 +  let i = matchend(a:line, a:pat, a:start) +  while i > 0 +    if !s:CommentOrString(a:line, i) +      let last_found = i +    endif +    let i = matchend(a:line, a:pat, i) +  endwhile +  return last_found +endfunction + +function! s:DecreaseIndentOnClosingTag(curr_indent) +  let cur_text = getline(v:lnum) +  if cur_text =~# '^\s*\%('.g:mp_close_tag.'\)' +    return max([a:curr_indent - shiftwidth(), 0]) +  endif +  return a:curr_indent +endfunction +" }}} + +" Main function {{{ +" +" Note: Every rule of indentation in MetaPost is very subjective. We might get +" creative, but things get murky very soon (there are too many corner cases). +" So, we provide a means for the user to decide what to do when this script +" doesn't get it. We use a simple idea: use '%>', '%<' and '%=' to explicitly +" control indentation. The '<' and '>' symbols may be repeated many times +" (e.g., '%>>' will cause the next line to be indented twice). +" +" By using '%>...', '%<...' and '%=', the indentation the user wants is +" preserved by commands like gg=G, even if it does not follow the rules of +" this script. +" +" Example: +" +"    def foo = +"        makepen( +"            subpath(T-n,t) of r  %> +"                shifted .5down   %> +"                    --subpath(t,T) of r shifted .5up -- cycle   %<<< +"        ) +"        withcolor black +"    enddef +" +" The default indentation of the previous example would be: +" +"    def foo = +"        makepen( +"            subpath(T-n,t) of r +"            shifted .5down +"            --subpath(t,T) of r shifted .5up -- cycle +"        ) +"        withcolor black +"    enddef +" +" Personally, I prefer the latter, but anyway... +function! GetMetaPostIndentIntern() +  " Do not touch indentation inside verbatimtex/btex.. etex blocks. +  if synIDattr(synID(v:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter' +    return -1 +  endif + +  " This is the reference line relative to which the current line is indented +  " (but see below). +  let lnum = s:PrevNonBlankNonComment(v:lnum) + +  " At the start of the file use zero indent. +  if lnum == 0 +    return 0 +  endif + +  let prev_text = getline(lnum) + +  " User-defined overrides take precedence over anything else. +  " See above for an example. +  let j = match(prev_text, '%[<>=]') +  if j > 0 +    let i = strlen(matchstr(prev_text, '%>\+', j)) - 1 +    if i > 0 +      return indent(lnum) + i * shiftwidth() +    endif + +    let i = strlen(matchstr(prev_text, '%<\+', j)) - 1 +    if i > 0 +      return max([indent(lnum) - i * shiftwidth(), 0]) +    endif + +    if match(prev_text, '%=', j) +      return indent(lnum) +    endif +  endif + +  " If the reference line ends with an open tag, indent. +  " +  " Example: +  " +  " if c: +  "     0 +  " else: +  "     1 +  " fi if c2: % Note that this line has weight equal to zero. +  "     ...   % This line will be indented +  if s:LastTagIsOpen(prev_text) +    return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth()) +  endif + +  " Lines with a positive weight are unbalanced and should likely be indented. +  " +  " Example: +  " +  " def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi +  "     ... % This line will be indented (because of the unterminated `for`) +  if s:Weight(prev_text) > 0 +    return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth()) +  endif + +  " Unterminated statements cause indentation to kick in. +  " +  " Example: +  " +  " draw unitsquare +  "     withcolor black; % This line is indented because of `draw`. +  " x := a + b + c +  "     + d + e;         % This line is indented because of `:=`. +  " +  let i = s:LastValidMatchEnd(prev_text, g:mp_statement, 0) +  if i >= 0 " Does the line contain a statement? +    if s:ValidMatchEnd(prev_text, ';', i) < 0 " Is the statement unterminated? +      return indent(lnum) + shiftwidth() +    else +      return s:DecreaseIndentOnClosingTag(indent(lnum)) +    endif +  endif + +  " Deal with the special case of a statement spanning multiple lines. If the +  " current reference line L ends with a semicolon, search backwards for +  " another semicolon or a statement keyword. If the latter is found first, +  " its line is used as the reference line for indenting the current line +  " instead of L. +  " +  "  Example: +  " +  "  if cond: +  "    draw if a: z0 else: z1 fi +  "        shifted S +  "        scaled T;      % L +  " +  "    for i = 1 upto 3:  % <-- Current line: this gets the same indent as `draw ...` +  " +  " NOTE: we get here only if L does not contain a statement (among those +  " listed in g:mp_statement). +  if s:ValidMatchEnd(prev_text, ';'.s:eol, 0) >= 0 " L ends with a semicolon +    let stm_lnum = s:PrevNonBlankNonComment(lnum) +    while stm_lnum > 0 +      let prev_text = getline(stm_lnum) +      let sc_pos = s:LastValidMatchEnd(prev_text, ';', 0) +      let stm_pos = s:ValidMatchEnd(prev_text, g:mp_statement, sc_pos) +      if stm_pos > sc_pos +        let lnum = stm_lnum +        break +      elseif sc_pos > stm_pos +        break +      endif +      let stm_lnum = s:PrevNonBlankNonComment(stm_lnum) +    endwhile +  endif + +  return s:DecreaseIndentOnClosingTag(indent(lnum)) +endfunction +" }}} + +let &cpo = s:keepcpo +unlet s:keepcpo + +" vim:sw=2:fdm=marker + +endif | 
