123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321 |
- 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
|