321 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			VimL
		
	
	
	
	
	
			
		
		
	
	
			321 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			VimL
		
	
	
	
	
	
| 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
 |