neovim

Neovim text editor
git clone https://git.dasho.dev/neovim.git
Log | Files | Refs | README

mp.vim (11321B)


      1 " MetaPost indent file
      2 " Language:           MetaPost
      3 " Maintainer:         Nicola Vitacolonna <nvitacolonna@gmail.com>
      4 " Former Maintainers: Eugene Minkovskii <emin@mccme.ru>
      5 " Last Change:        2016 Oct 2, 4:13pm
      6 " Version: 0.2
      7 
      8 if exists("b:did_indent")
      9  finish
     10 endif
     11 let b:did_indent = 1
     12 
     13 setlocal indentexpr=GetMetaPostIndent()
     14 setlocal indentkeys+==end,=else,=fi,=fill,0),0]
     15 
     16 let b:undo_indent = "setl indentkeys< indentexpr<"
     17 
     18 " Only define the function once.
     19 if exists("*GetMetaPostIndent")
     20  finish
     21 endif
     22 let s:keepcpo= &cpo
     23 set cpo&vim
     24 
     25 function GetMetaPostIndent()
     26  let ignorecase_save = &ignorecase
     27  try
     28    let &ignorecase = 0
     29    return GetMetaPostIndentIntern()
     30  finally
     31    let &ignorecase = ignorecase_save
     32  endtry
     33 endfunc
     34 
     35 " Regexps {{{
     36 " Note: the next three variables are made global so that a user may add
     37 " further keywords.
     38 "
     39 " Example:
     40 "
     41 "    Put these in ~/.vim/after/indent/mp.vim
     42 "
     43 "    let g:mp_open_tag .= '\|\<begintest\>'
     44 "    let g:mp_close_tag .= '\|\<endtest\>'
     45 
     46 " Expressions starting indented blocks
     47 let g:mp_open_tag = ''
     48      \ . '\<if\>'
     49      \ . '\|\<else\%[if]\>'
     50      \ . '\|\<for\%(\|ever\|suffixes\)\>'
     51      \ . '\|\<begingroup\>'
     52      \ . '\|\<\%(\|var\|primary\|secondary\|tertiary\)def\>'
     53      \ . '\|^\s*\<begin\%(fig\|graph\|glyph\|char\|logochar\)\>'
     54      \ . '\|[([{]'
     55 
     56 " Expressions ending indented blocks
     57 let g:mp_close_tag = ''
     58      \ . '\<fi\>'
     59      \ . '\|\<else\%[if]\>'
     60      \ . '\|\<end\%(\|for\|group\|def\|fig\|char\|glyph\|graph\)\>'
     61      \ . '\|[)\]}]'
     62 
     63 " Statements that may span multiple lines and are ended by a semicolon. To
     64 " keep this list short, statements that are unlikely to be very long or are
     65 " not very common (e.g., keywords like `interim` or `showtoken`) are not
     66 " included.
     67 "
     68 " The regex for assignments and equations (the last branch) is tricky, because
     69 " it must not match things like `for i :=`, `if a=b`, `def...=`, etc... It is
     70 " not perfect, but it works reasonably well.
     71 let g:mp_statement = ''
     72      \ . '\<\%(\|un\|cut\)draw\>'
     73      \ . '\|\<\%(\|un\)fill\%[draw]\>'
     74      \ . '\|\<draw\%(dbl\)\=arrow\>'
     75      \ . '\|\<clip\>'
     76      \ . '\|\<addto\>'
     77      \ . '\|\<save\>'
     78      \ . '\|\<setbounds\>'
     79      \ . '\|\<message\>'
     80      \ . '\|\<errmessage\>'
     81      \ . '\|\<errhelp\>'
     82      \ . '\|\<fontmapline\>'
     83      \ . '\|\<pickup\>'
     84      \ . '\|\<show\>'
     85      \ . '\|\<special\>'
     86      \ . '\|\<write\>'
     87      \ . '\|\%(^\|;\)\%([^;=]*\%('.g:mp_open_tag.'\)\)\@!.\{-}:\=='
     88 
     89 " A line ends with zero or more spaces, possibly followed by a comment.
     90 let s:eol = '\s*\%($\|%\)'
     91 " }}}
     92 
     93 " Auxiliary functions {{{
     94 " Returns 1 if (0-based) position immediately preceding `pos` in `line` is
     95 " inside a string or a comment; returns 0 otherwise.
     96 
     97 " This is the function that is called more often when indenting, so it is
     98 " critical that it is efficient. The method we use is significantly faster
     99 " than using syntax attributes, and more general (it does not require
    100 " syntax_items). It is also faster than using a single regex matching an even
    101 " number of quotes. It helps that MetaPost strings cannot span more than one
    102 " line and cannot contain escaped quotes.
    103 function! s:CommentOrString(line, pos)
    104  let in_string = 0
    105  let q = stridx(a:line, '"')
    106  let c = stridx(a:line, '%')
    107  while q >= 0 && q < a:pos
    108    if c >= 0 && c < q
    109      if in_string " Find next percent symbol
    110        let c = stridx(a:line, '%', q + 1)
    111      else " Inside comment
    112        return 1
    113      endif
    114    endif
    115    let in_string = 1 - in_string
    116    let q = stridx(a:line, '"', q + 1) " Find next quote
    117  endwhile
    118  return in_string || (c >= 0 && c <= a:pos)
    119 endfunction
    120 
    121 " Find the first non-comment non-blank line before the current line.
    122 function! s:PrevNonBlankNonComment(lnum)
    123  let l:lnum = prevnonblank(a:lnum - 1)
    124  while getline(l:lnum) =~# '^\s*%'
    125    let l:lnum = prevnonblank(l:lnum - 1)
    126  endwhile
    127  return l:lnum
    128 endfunction
    129 
    130 " Returns true if the last tag appearing in the line is an open tag; returns
    131 " false otherwise.
    132 function! s:LastTagIsOpen(line)
    133  let o = s:LastValidMatchEnd(a:line, g:mp_open_tag, 0)
    134  if o == - 1 | return v:false | endif
    135  return s:LastValidMatchEnd(a:line, g:mp_close_tag, o) < 0
    136 endfunction
    137 
    138 " A simple, efficient and quite effective heuristics is used to test whether
    139 " a line should cause the next line to be indented: count the "opening tags"
    140 " (if, for, def, ...) in the line, count the "closing tags" (endif, endfor,
    141 " ...) in the line, and compute the difference. We call the result the
    142 " "weight" of the line. If the weight is positive, then the next line should
    143 " most likely be indented. Note that `else` and `elseif` are both opening and
    144 " closing tags, so they "cancel out" in almost all cases, the only exception
    145 " being a leading `else[if]`, which is counted as an opening tag, but not as
    146 " a closing tag (so that, for instance, a line containing a single `else:`
    147 " will have weight equal to one, not zero). We do not treat a trailing
    148 " `else[if]` in any special way, because lines ending with an open tag are
    149 " dealt with separately before this function is called (see
    150 " GetMetaPostIndentIntern()).
    151 "
    152 " Example:
    153 "
    154 "     forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi
    155 "       % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2)
    156 "     endfor
    157 
    158 function! s:Weight(line)
    159  let [o, i] = [0, s:ValidMatchEnd(a:line, g:mp_open_tag, 0)]
    160  while i > 0
    161    let o += 1
    162    let i = s:ValidMatchEnd(a:line, g:mp_open_tag, i)
    163  endwhile
    164  let [c, i] = [0, matchend(a:line, '^\s*\<else\%[if]\>')] " Skip a leading else[if]
    165  let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
    166  while i > 0
    167    let c += 1
    168    let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
    169  endwhile
    170  return o - c
    171 endfunction
    172 
    173 " Similar to matchend(), but skips strings and comments.
    174 " line: a String
    175 function! s:ValidMatchEnd(line, pat, start)
    176  let i = matchend(a:line, a:pat, a:start)
    177  while i > 0 && s:CommentOrString(a:line, i)
    178    let i = matchend(a:line, a:pat, i)
    179  endwhile
    180  return i
    181 endfunction
    182 
    183 " Like s:ValidMatchEnd(), but returns the end position of the last (i.e.,
    184 " rightmost) match.
    185 function! s:LastValidMatchEnd(line, pat, start)
    186  let last_found = -1
    187  let i = matchend(a:line, a:pat, a:start)
    188  while i > 0
    189    if !s:CommentOrString(a:line, i)
    190      let last_found = i
    191    endif
    192    let i = matchend(a:line, a:pat, i)
    193  endwhile
    194  return last_found
    195 endfunction
    196 
    197 function! s:DecreaseIndentOnClosingTag(curr_indent)
    198  let cur_text = getline(v:lnum)
    199  if cur_text =~# '^\s*\%('.g:mp_close_tag.'\)'
    200    return max([a:curr_indent - shiftwidth(), 0])
    201  endif
    202  return a:curr_indent
    203 endfunction
    204 " }}}
    205 
    206 " Main function {{{
    207 "
    208 " Note: Every rule of indentation in MetaPost is very subjective. We might get
    209 " creative, but things get murky very soon (there are too many corner cases).
    210 " So, we provide a means for the user to decide what to do when this script
    211 " doesn't get it. We use a simple idea: use '%>', '%<' and '%=' to explicitly
    212 " control indentation. The '<' and '>' symbols may be repeated many times
    213 " (e.g., '%>>' will cause the next line to be indented twice).
    214 "
    215 " By using '%>...', '%<...' and '%=', the indentation the user wants is
    216 " preserved by commands like gg=G, even if it does not follow the rules of
    217 " this script.
    218 "
    219 " Example:
    220 "
    221 "    def foo =
    222 "        makepen(
    223 "            subpath(T-n,t) of r  %>
    224 "                shifted .5down   %>
    225 "                    --subpath(t,T) of r shifted .5up -- cycle   %<<<
    226 "        )
    227 "        withcolor black
    228 "    enddef
    229 "
    230 " The default indentation of the previous example would be:
    231 "
    232 "    def foo =
    233 "        makepen(
    234 "            subpath(T-n,t) of r
    235 "            shifted .5down
    236 "            --subpath(t,T) of r shifted .5up -- cycle
    237 "        )
    238 "        withcolor black
    239 "    enddef
    240 "
    241 " Personally, I prefer the latter, but anyway...
    242 function! GetMetaPostIndentIntern()
    243  " Do not touch indentation inside verbatimtex/btex.. etex blocks.
    244  if synIDattr(synID(v:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter'
    245    return -1
    246  endif
    247 
    248  " This is the reference line relative to which the current line is indented
    249  " (but see below).
    250  let lnum = s:PrevNonBlankNonComment(v:lnum)
    251 
    252  " At the start of the file use zero indent.
    253  if lnum == 0
    254    return 0
    255  endif
    256 
    257  let prev_text = getline(lnum)
    258 
    259  " User-defined overrides take precedence over anything else.
    260  " See above for an example.
    261  let j = match(prev_text, '%[<>=]')
    262  if j > 0
    263    let i = strlen(matchstr(prev_text, '%>\+', j)) - 1
    264    if i > 0
    265      return indent(lnum) + i * shiftwidth()
    266    endif
    267 
    268    let i = strlen(matchstr(prev_text, '%<\+', j)) - 1
    269    if i > 0
    270      return max([indent(lnum) - i * shiftwidth(), 0])
    271    endif
    272 
    273    if match(prev_text, '%=', j)
    274      return indent(lnum)
    275    endif
    276  endif
    277 
    278  " If the reference line ends with an open tag, indent.
    279  "
    280  " Example:
    281  "
    282  " if c:
    283  "     0
    284  " else:
    285  "     1
    286  " fi if c2: % Note that this line has weight equal to zero.
    287  "     ...   % This line will be indented
    288  if s:LastTagIsOpen(prev_text)
    289    return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
    290  endif
    291 
    292  " Lines with a positive weight are unbalanced and should likely be indented.
    293  "
    294  " Example:
    295  "
    296  " def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi
    297  "     ... % This line will be indented (because of the unterminated `for`)
    298  if s:Weight(prev_text) > 0
    299    return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
    300  endif
    301 
    302  " Unterminated statements cause indentation to kick in.
    303  "
    304  " Example:
    305  "
    306  " draw unitsquare
    307  "     withcolor black; % This line is indented because of `draw`.
    308  " x := a + b + c
    309  "     + d + e;         % This line is indented because of `:=`.
    310  "
    311  let i = s:LastValidMatchEnd(prev_text, g:mp_statement, 0)
    312  if i >= 0 " Does the line contain a statement?
    313    if s:ValidMatchEnd(prev_text, ';', i) < 0 " Is the statement unterminated?
    314      return indent(lnum) + shiftwidth()
    315    else
    316      return s:DecreaseIndentOnClosingTag(indent(lnum))
    317    endif
    318  endif
    319 
    320  " Deal with the special case of a statement spanning multiple lines. If the
    321  " current reference line L ends with a semicolon, search backwards for
    322  " another semicolon or a statement keyword. If the latter is found first,
    323  " its line is used as the reference line for indenting the current line
    324  " instead of L.
    325  "
    326  "  Example:
    327  "
    328  "  if cond:
    329  "    draw if a: z0 else: z1 fi
    330  "        shifted S
    331  "        scaled T;      % L
    332  "
    333  "    for i = 1 upto 3:  % <-- Current line: this gets the same indent as `draw ...`
    334  "
    335  " NOTE: we get here only if L does not contain a statement (among those
    336  " listed in g:mp_statement).
    337  if s:ValidMatchEnd(prev_text, ';'.s:eol, 0) >= 0 " L ends with a semicolon
    338    let stm_lnum = s:PrevNonBlankNonComment(lnum)
    339    while stm_lnum > 0
    340      let prev_text = getline(stm_lnum)
    341      let sc_pos = s:LastValidMatchEnd(prev_text, ';', 0)
    342      let stm_pos = s:ValidMatchEnd(prev_text, g:mp_statement, sc_pos)
    343      if stm_pos > sc_pos
    344        let lnum = stm_lnum
    345        break
    346      elseif sc_pos > stm_pos
    347        break
    348      endif
    349      let stm_lnum = s:PrevNonBlankNonComment(stm_lnum)
    350    endwhile
    351  endif
    352 
    353  return s:DecreaseIndentOnClosingTag(indent(lnum))
    354 endfunction
    355 " }}}
    356 
    357 let &cpo = s:keepcpo
    358 unlet s:keepcpo
    359 
    360 " vim:sw=2:fdm=marker