mp.vim 11 KB

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