mp.vim 9.7 KB

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