summaryrefslogtreecommitdiffstats
path: root/runtime/indent/mp.vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/indent/mp.vim')
-rw-r--r--runtime/indent/mp.vim320
1 files changed, 320 insertions, 0 deletions
diff --git a/runtime/indent/mp.vim b/runtime/indent/mp.vim
new file mode 100644
index 0000000..07873e1
--- /dev/null
+++ b/runtime/indent/mp.vim
@@ -0,0 +1,320 @@
+vim9script
+
+# MetaPost indent file
+# Language: MetaPost
+# Maintainer: Nicola Vitacolonna <nvitacolonna@gmail.com>
+# Former Maintainers: Eugene Minkovskii <emin@mccme.ru>
+# Latest Revision: 2022 Aug 12
+
+if exists("b:did_indent")
+ finish
+endif
+
+b:did_indent = 1
+
+setlocal indentexpr=g:MetaPostIndent()
+setlocal indentkeys+==end,=else,=fi,=fill,0),0]
+setlocal nolisp
+setlocal nosmartindent
+
+b:undo_indent = "setl indentexpr< indentkeys< lisp< smartindent<"
+
+# Regexps {{{
+# Expressions starting indented blocks
+const MP_OPEN_TAG = [
+ '\<if\>',
+ '\<else\%[if]\>',
+ '\<for\%(\|ever\|suffixes\)\>',
+ '\<begingroup\>',
+ '\<\%(\|var\|primary\|secondary\|tertiary\)def\>',
+ '^\s*\<begin\%(fig\|graph\|glyph\|char\|logochar\)\>',
+ '[([{]',
+ ]->extend(get(g:, "mp_open_tag", []))->join('\|')
+
+# Expressions ending indented blocks
+const MP_CLOSE_TAG = [
+ '\<fi\>',
+ '\<else\%[if]\>',
+ '\<end\%(\|for\|group\|def\|fig\|char\|glyph\|graph\)\>',
+ '[)\]}]'
+ ]->extend(get(g:, "mp_close_tag", []))->join('\|')
+
+# Statements that may span multiple lines and are ended by a semicolon. To
+# keep this list short, statements that are unlikely to be very long or are
+# not very common (e.g., keywords like `interim` or `showtoken`) are not
+# included.
+#
+# The regex for assignments and equations (the last branch) is tricky, because
+# it must not match things like `for i :=`, `if a=b`, `def...=`, etc... It is
+# not perfect, but it works reasonably well.
+const MP_STATEMENT = [
+ '\<\%(\|un\|cut\)draw\%(dot\)\=\>',
+ '\<\%(\|un\)fill\%[draw]\>',
+ '\<draw\%(dbl\)\=arrow\>',
+ '\<clip\>',
+ '\<addto\>',
+ '\<save\>',
+ '\<setbounds\>',
+ '\<message\>',
+ '\<errmessage\>',
+ '\<errhelp\>',
+ '\<fontmapline\>',
+ '\<pickup\>',
+ '\<show\>',
+ '\<special\>',
+ '\<write\>',
+ '\%(^\|;\)\%([^;=]*\%(' .. MP_OPEN_TAG .. '\)\)\@!.\{-}:\==',
+ ]->join('\|')
+
+# A line ends with zero or more spaces, possibly followed by a comment.
+const EOL = '\s*\%($\|%\)'
+# }}}
+
+# Auxiliary functions {{{
+# Returns true if (0-based) position immediately preceding `pos` in `line` is
+# inside a string or a comment; returns false otherwise.
+
+# This is the function that is called more often when indenting, so it is
+# critical that it is efficient. The method we use is significantly faster
+# than using syntax attributes, and more general (it does not require
+# syntax_items). It is also faster than using a single regex matching an even
+# number of quotes. It helps that MetaPost strings cannot span more than one
+# line and cannot contain escaped quotes.
+def IsCommentOrString(line: string, pos: number): bool
+ var in_string = 0
+ var q = stridx(line, '"')
+ var c = stridx(line, '%')
+
+ while q >= 0 && q < pos
+ if c >= 0 && c < q
+ if in_string # Find next percent symbol
+ c = stridx(line, '%', q + 1)
+ else # Inside comment
+ return true
+ endif
+ endif
+ in_string = 1 - in_string
+ q = stridx(line, '"', q + 1) # Find next quote
+ endwhile
+
+ return in_string || (c >= 0 && c <= pos)
+enddef
+
+# Find the first non-comment non-blank line before the given line.
+def PrevNonBlankNonComment(lnum: number): number
+ var nr = prevnonblank(lnum - 1)
+ while getline(nr) =~# '^\s*%'
+ nr = prevnonblank(nr - 1)
+ endwhile
+ return nr
+enddef
+
+# Returns true if the last tag appearing in the line is an open tag; returns
+# false otherwise.
+def LastTagIsOpen(line: string): bool
+ var o = LastValidMatchEnd(line, MP_OPEN_TAG, 0)
+ if o == - 1
+ return false
+ endif
+ return LastValidMatchEnd(line, MP_CLOSE_TAG, o) < 0
+enddef
+
+# A simple, efficient and quite effective heuristics is used to test whether
+# a line should cause the next line to be indented: count the "opening tags"
+# (if, for, def, ...) in the line, count the "closing tags" (endif, endfor,
+# ...) in the line, and compute the difference. We call the result the
+# "weight" of the line. If the weight is positive, then the next line should
+# most likely be indented. Note that `else` and `elseif` are both opening and
+# closing tags, so they "cancel out" in almost all cases, the only exception
+# being a leading `else[if]`, which is counted as an opening tag, but not as
+# a closing tag (so that, for instance, a line containing a single `else:`
+# will have weight equal to one, not zero). We do not treat a trailing
+# `else[if]` in any special way, because lines ending with an open tag are
+# dealt with separately before this function is called (see MetaPostIndent()).
+#
+# Example:
+#
+# forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi
+# % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2)
+# endfor
+def Weight(line: string): number
+ var o = 0
+ var i = ValidMatchEnd(line, MP_OPEN_TAG, 0)
+ while i > 0
+ o += 1
+ i = ValidMatchEnd(line, MP_OPEN_TAG, i)
+ endwhile
+ var c = 0
+ i = matchend(line, '^\s*\<else\%[if]\>') # Skip a leading else[if]
+ i = ValidMatchEnd(line, MP_CLOSE_TAG, i)
+ while i > 0
+ c += 1
+ i = ValidMatchEnd(line, MP_CLOSE_TAG, i)
+ endwhile
+ return o - c
+enddef
+
+# Similar to matchend(), but skips strings and comments.
+# line: a String
+def ValidMatchEnd(line: string, pat: string, start: number): number
+ var i = matchend(line, pat, start)
+ while i > 0 && IsCommentOrString(line, i)
+ i = matchend(line, pat, i)
+ endwhile
+ return i
+enddef
+
+# Like s:ValidMatchEnd(), but returns the end position of the last (i.e.,
+# rightmost) match.
+def LastValidMatchEnd(line: string, pat: string, start: number): number
+ var last_found = -1
+ var i = matchend(line, pat, start)
+ while i > 0
+ if !IsCommentOrString(line, i)
+ last_found = i
+ endif
+ i = matchend(line, pat, i)
+ endwhile
+ return last_found
+enddef
+
+def DecreaseIndentOnClosingTag(curr_indent: number): number
+ var cur_text = getline(v:lnum)
+ if cur_text =~# '^\s*\%(' .. MP_CLOSE_TAG .. '\)'
+ return max([curr_indent - shiftwidth(), 0])
+ endif
+ return curr_indent
+enddef
+# }}}
+
+# Main function {{{
+def g:MetaPostIndent(): number
+ # Do not touch indentation inside verbatimtex/btex.. etex blocks.
+ if synIDattr(synID(v:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter'
+ return -1
+ endif
+
+ # At the start of a MetaPost block inside ConTeXt, do not touch indentation
+ if synIDattr(synID(prevnonblank(v:lnum - 1), 1, 1), "name") == "contextBlockDelim"
+ return -1
+ endif
+
+ var lnum = PrevNonBlankNonComment(v:lnum)
+
+ # At the start of the file use zero indent.
+ if lnum == 0
+ return 0
+ endif
+
+ var prev_text = getline(lnum)
+
+ # Every rule of indentation in MetaPost is very subjective. We might get
+ # creative, but things get murky very soon (there are too many corner
+ # cases). So, we provide a means for the user to decide what to do when this
+ # script doesn't get it. We use a simple idea: use '%>', '%<', '%=', and
+ # '%!', to explicitly control indentation. The '<' and '>' symbols may be
+ # repeated many times (e.g., '%>>' will cause the next line to be indented
+ # twice).
+ #
+ # User-defined overrides take precedence over anything else.
+ var j = match(prev_text, '%[<>=!]')
+ if j > 0
+ var i = strlen(matchstr(prev_text, '%>\+', j)) - 1
+ if i > 0
+ return indent(lnum) + i * shiftwidth()
+ endif
+
+ i = strlen(matchstr(prev_text, '%<\+', j)) - 1
+ if i > 0
+ return max([indent(lnum) - i * shiftwidth(), 0])
+ endif
+
+ if match(prev_text, '%=', j) > -1
+ return indent(lnum)
+ endif
+
+ if match(prev_text, '%!', j) > -1
+ return -1
+ endif
+ endif
+
+ # If the reference line ends with an open tag, indent.
+ #
+ # Example:
+ #
+ # if c:
+ # 0
+ # else:
+ # 1
+ # fi if c2: % Note that this line has weight equal to zero.
+ # ... % This line will be indented
+ if LastTagIsOpen(prev_text)
+ return DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
+ endif
+
+ # Lines with a positive weight are unbalanced and should likely be indented.
+ #
+ # Example:
+ #
+ # def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi
+ # ... % This line will be indented (because of the unterminated `for`)
+ if Weight(prev_text) > 0
+ return DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
+ endif
+
+ # Unterminated statements cause indentation to kick in.
+ #
+ # Example:
+ #
+ # draw unitsquare
+ # withcolor black; % This line is indented because of `draw`.
+ # x := a + b + c
+ # + d + e; % This line is indented because of `:=`.
+ #
+ var i = LastValidMatchEnd(prev_text, MP_STATEMENT, 0)
+ if i >= 0 # Does the line contain a statement?
+ if ValidMatchEnd(prev_text, ';', i) < 0 # Is the statement unterminated?
+ return indent(lnum) + shiftwidth()
+ else
+ return DecreaseIndentOnClosingTag(indent(lnum))
+ endif
+ endif
+
+ # Deal with the special case of a statement spanning multiple lines. If the
+ # current reference line L ends with a semicolon, search backwards for
+ # another semicolon or a statement keyword. If the latter is found first,
+ # its line is used as the reference line for indenting the current line
+ # instead of L.
+ #
+ # Example:
+ #
+ # if cond:
+ # draw if a: z0 else: z1 fi
+ # shifted S
+ # scaled T; % L
+ #
+ # for i = 1 upto 3: % <-- Current line: this gets the same indent as `draw ...`
+ #
+ # NOTE: we get here only if L does not contain a statement (among those
+ # listed in g:MP_STATEMENT).
+ if ValidMatchEnd(prev_text, ';' .. EOL, 0) >= 0 # L ends with a semicolon
+ var stm_lnum = PrevNonBlankNonComment(lnum)
+ while stm_lnum > 0
+ prev_text = getline(stm_lnum)
+ var sc_pos = LastValidMatchEnd(prev_text, ';', 0)
+ var stm_pos = ValidMatchEnd(prev_text, MP_STATEMENT, sc_pos)
+ if stm_pos > sc_pos
+ lnum = stm_lnum
+ break
+ elseif sc_pos > stm_pos
+ break
+ endif
+ stm_lnum = PrevNonBlankNonComment(stm_lnum)
+ endwhile
+ endif
+
+ return DecreaseIndentOnClosingTag(indent(lnum))
+enddef
+# }}}
+
+# vim: sw=2 fdm=marker