1" MetaPost indent file 2" Language: MetaPost 3" Maintainer: Nicola Vitacolonna <[email protected]> 4" Former Maintainers: Eugene Minkovskii <[email protected]> 5" Last Change: 2016 Oct 2, 4:13pm 6" Version: 0.2 7 8if exists("b:did_indent") 9 finish 10endif 11let b:did_indent = 1 12 13setlocal indentexpr=GetMetaPostIndent() 14setlocal indentkeys+==end,=else,=fi,=fill,0),0] 15 16let b:undo_indent = "setl indentkeys< indentexpr<" 17 18" Only define the function once. 19if exists("*GetMetaPostIndent") 20 finish 21endif 22let s:keepcpo= &cpo 23set cpo&vim 24 25function GetMetaPostIndent() 26 let ignorecase_save = &ignorecase 27 try 28 let &ignorecase = 0 29 return GetMetaPostIndentIntern() 30 finally 31 let &ignorecase = ignorecase_save 32 endtry 33endfunc 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 47let 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 57let 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. 71let 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. 90let 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. 103function! 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) 119endfunction 120 121" Find the first non-comment non-blank line before the current line. 122function! 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 128endfunction 129 130" Returns true if the last tag appearing in the line is an open tag; returns 131" false otherwise. 132function! 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 136endfunction 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 158function! 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 171endfunction 172 173" Similar to matchend(), but skips strings and comments. 174" line: a String 175function! 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 181endfunction 182 183" Like s:ValidMatchEnd(), but returns the end position of the last (i.e., 184" rightmost) match. 185function! 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 195endfunction 196 197function! 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 203endfunction 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... 242function! 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)) 354endfunction 355" }}} 356 357let &cpo = s:keepcpo 358unlet s:keepcpo 359 360" vim:sw=2:fdm=marker 361