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