blob: 07873e13a16aaa98d0bee7774e6e5721a558e6a1 [file] [log] [blame]
Bram Moolenaar7dd54322022-08-26 18:01:12 +01001vim9script
2
3# MetaPost indent file
4# Language: MetaPost
5# Maintainer: Nicola Vitacolonna <nvitacolonna@gmail.com>
6# Former Maintainers: Eugene Minkovskii <emin@mccme.ru>
7# Latest Revision: 2022 Aug 12
Bram Moolenaar071d4272004-06-13 20:20:40 +00008
Bram Moolenaar071d4272004-06-13 20:20:40 +00009if exists("b:did_indent")
10 finish
11endif
Bram Moolenaar071d4272004-06-13 20:20:40 +000012
Bram Moolenaar7dd54322022-08-26 18:01:12 +010013b:did_indent = 1
14
15setlocal indentexpr=g:MetaPostIndent()
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020016setlocal indentkeys+==end,=else,=fi,=fill,0),0]
Bram Moolenaar7dd54322022-08-26 18:01:12 +010017setlocal nolisp
18setlocal nosmartindent
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020019
Bram Moolenaar7dd54322022-08-26 18:01:12 +010020b:undo_indent = "setl indentexpr< indentkeys< lisp< smartindent<"
Bram Moolenaar071d4272004-06-13 20:20:40 +000021
Bram Moolenaar7dd54322022-08-26 18:01:12 +010022# Regexps {{{
23# Expressions starting indented blocks
24const MP_OPEN_TAG = [
25 '\<if\>',
26 '\<else\%[if]\>',
27 '\<for\%(\|ever\|suffixes\)\>',
28 '\<begingroup\>',
29 '\<\%(\|var\|primary\|secondary\|tertiary\)def\>',
30 '^\s*\<begin\%(fig\|graph\|glyph\|char\|logochar\)\>',
31 '[([{]',
32 ]->extend(get(g:, "mp_open_tag", []))->join('\|')
Bram Moolenaar071d4272004-06-13 20:20:40 +000033
Bram Moolenaar7dd54322022-08-26 18:01:12 +010034# Expressions ending indented blocks
35const MP_CLOSE_TAG = [
36 '\<fi\>',
37 '\<else\%[if]\>',
38 '\<end\%(\|for\|group\|def\|fig\|char\|glyph\|graph\)\>',
39 '[)\]}]'
40 ]->extend(get(g:, "mp_close_tag", []))->join('\|')
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020041
Bram Moolenaar7dd54322022-08-26 18:01:12 +010042# Statements that may span multiple lines and are ended by a semicolon. To
43# keep this list short, statements that are unlikely to be very long or are
44# not very common (e.g., keywords like `interim` or `showtoken`) are not
45# included.
46#
47# The regex for assignments and equations (the last branch) is tricky, because
48# it must not match things like `for i :=`, `if a=b`, `def...=`, etc... It is
49# not perfect, but it works reasonably well.
50const MP_STATEMENT = [
51 '\<\%(\|un\|cut\)draw\%(dot\)\=\>',
52 '\<\%(\|un\)fill\%[draw]\>',
53 '\<draw\%(dbl\)\=arrow\>',
54 '\<clip\>',
55 '\<addto\>',
56 '\<save\>',
57 '\<setbounds\>',
58 '\<message\>',
59 '\<errmessage\>',
60 '\<errhelp\>',
61 '\<fontmapline\>',
62 '\<pickup\>',
63 '\<show\>',
64 '\<special\>',
65 '\<write\>',
66 '\%(^\|;\)\%([^;=]*\%(' .. MP_OPEN_TAG .. '\)\)\@!.\{-}:\==',
67 ]->join('\|')
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020068
Bram Moolenaar7dd54322022-08-26 18:01:12 +010069# A line ends with zero or more spaces, possibly followed by a comment.
70const EOL = '\s*\%($\|%\)'
71# }}}
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020072
Bram Moolenaar7dd54322022-08-26 18:01:12 +010073# Auxiliary functions {{{
74# Returns true if (0-based) position immediately preceding `pos` in `line` is
75# inside a string or a comment; returns false otherwise.
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020076
Bram Moolenaar7dd54322022-08-26 18:01:12 +010077# This is the function that is called more often when indenting, so it is
78# critical that it is efficient. The method we use is significantly faster
79# than using syntax attributes, and more general (it does not require
80# syntax_items). It is also faster than using a single regex matching an even
81# number of quotes. It helps that MetaPost strings cannot span more than one
82# line and cannot contain escaped quotes.
83def IsCommentOrString(line: string, pos: number): bool
84 var in_string = 0
85 var q = stridx(line, '"')
86 var c = stridx(line, '%')
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020087
Bram Moolenaar7dd54322022-08-26 18:01:12 +010088 while q >= 0 && q < pos
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020089 if c >= 0 && c < q
Bram Moolenaar7dd54322022-08-26 18:01:12 +010090 if in_string # Find next percent symbol
91 c = stridx(line, '%', q + 1)
92 else # Inside comment
93 return true
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020094 endif
95 endif
Bram Moolenaar7dd54322022-08-26 18:01:12 +010096 in_string = 1 - in_string
97 q = stridx(line, '"', q + 1) # Find next quote
Bram Moolenaar071d4272004-06-13 20:20:40 +000098 endwhile
Bram Moolenaar071d4272004-06-13 20:20:40 +000099
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100100 return in_string || (c >= 0 && c <= pos)
101enddef
102
103# Find the first non-comment non-blank line before the given line.
104def PrevNonBlankNonComment(lnum: number): number
105 var nr = prevnonblank(lnum - 1)
106 while getline(nr) =~# '^\s*%'
107 nr = prevnonblank(nr - 1)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000108 endwhile
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100109 return nr
110enddef
Bram Moolenaar071d4272004-06-13 20:20:40 +0000111
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100112# Returns true if the last tag appearing in the line is an open tag; returns
113# false otherwise.
114def LastTagIsOpen(line: string): bool
115 var o = LastValidMatchEnd(line, MP_OPEN_TAG, 0)
116 if o == - 1
117 return false
118 endif
119 return LastValidMatchEnd(line, MP_CLOSE_TAG, o) < 0
120enddef
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200121
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100122# A simple, efficient and quite effective heuristics is used to test whether
123# a line should cause the next line to be indented: count the "opening tags"
124# (if, for, def, ...) in the line, count the "closing tags" (endif, endfor,
125# ...) in the line, and compute the difference. We call the result the
126# "weight" of the line. If the weight is positive, then the next line should
127# most likely be indented. Note that `else` and `elseif` are both opening and
128# closing tags, so they "cancel out" in almost all cases, the only exception
129# being a leading `else[if]`, which is counted as an opening tag, but not as
130# a closing tag (so that, for instance, a line containing a single `else:`
131# will have weight equal to one, not zero). We do not treat a trailing
132# `else[if]` in any special way, because lines ending with an open tag are
133# dealt with separately before this function is called (see MetaPostIndent()).
134#
135# Example:
136#
137# forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi
138# % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2)
139# endfor
140def Weight(line: string): number
141 var o = 0
142 var i = ValidMatchEnd(line, MP_OPEN_TAG, 0)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200143 while i > 0
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100144 o += 1
145 i = ValidMatchEnd(line, MP_OPEN_TAG, i)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000146 endwhile
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100147 var c = 0
148 i = matchend(line, '^\s*\<else\%[if]\>') # Skip a leading else[if]
149 i = ValidMatchEnd(line, MP_CLOSE_TAG, i)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200150 while i > 0
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100151 c += 1
152 i = ValidMatchEnd(line, MP_CLOSE_TAG, i)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200153 endwhile
154 return o - c
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100155enddef
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200156
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100157# Similar to matchend(), but skips strings and comments.
158# line: a String
159def ValidMatchEnd(line: string, pat: string, start: number): number
160 var i = matchend(line, pat, start)
161 while i > 0 && IsCommentOrString(line, i)
162 i = matchend(line, pat, i)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200163 endwhile
164 return i
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100165enddef
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200166
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100167# Like s:ValidMatchEnd(), but returns the end position of the last (i.e.,
168# rightmost) match.
169def LastValidMatchEnd(line: string, pat: string, start: number): number
170 var last_found = -1
171 var i = matchend(line, pat, start)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200172 while i > 0
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100173 if !IsCommentOrString(line, i)
174 last_found = i
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200175 endif
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100176 i = matchend(line, pat, i)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200177 endwhile
178 return last_found
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100179enddef
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200180
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100181def DecreaseIndentOnClosingTag(curr_indent: number): number
182 var cur_text = getline(v:lnum)
183 if cur_text =~# '^\s*\%(' .. MP_CLOSE_TAG .. '\)'
184 return max([curr_indent - shiftwidth(), 0])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000185 endif
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100186 return curr_indent
187enddef
188# }}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000189
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100190# Main function {{{
191def g:MetaPostIndent(): number
192 # Do not touch indentation inside verbatimtex/btex.. etex blocks.
Bram Moolenaardc083282016-10-11 08:57:33 +0200193 if synIDattr(synID(v:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter'
194 return -1
195 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200196
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100197 # At the start of a MetaPost block inside ConTeXt, do not touch indentation
198 if synIDattr(synID(prevnonblank(v:lnum - 1), 1, 1), "name") == "contextBlockDelim"
199 return -1
200 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200201
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100202 var lnum = PrevNonBlankNonComment(v:lnum)
203
204 # At the start of the file use zero indent.
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200205 if lnum == 0
206 return 0
207 endif
208
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100209 var prev_text = getline(lnum)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200210
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100211 # Every rule of indentation in MetaPost is very subjective. We might get
212 # creative, but things get murky very soon (there are too many corner
213 # cases). So, we provide a means for the user to decide what to do when this
214 # script doesn't get it. We use a simple idea: use '%>', '%<', '%=', and
215 # '%!', to explicitly control indentation. The '<' and '>' symbols may be
216 # repeated many times (e.g., '%>>' will cause the next line to be indented
217 # twice).
218 #
219 # User-defined overrides take precedence over anything else.
220 var j = match(prev_text, '%[<>=!]')
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200221 if j > 0
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100222 var i = strlen(matchstr(prev_text, '%>\+', j)) - 1
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200223 if i > 0
224 return indent(lnum) + i * shiftwidth()
225 endif
226
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100227 i = strlen(matchstr(prev_text, '%<\+', j)) - 1
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200228 if i > 0
229 return max([indent(lnum) - i * shiftwidth(), 0])
230 endif
231
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100232 if match(prev_text, '%=', j) > -1
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200233 return indent(lnum)
234 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200235
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100236 if match(prev_text, '%!', j) > -1
237 return -1
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200238 endif
239 endif
240
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100241 # If the reference line ends with an open tag, indent.
242 #
243 # Example:
244 #
245 # if c:
246 # 0
247 # else:
248 # 1
249 # fi if c2: % Note that this line has weight equal to zero.
250 # ... % This line will be indented
251 if LastTagIsOpen(prev_text)
252 return DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
253 endif
254
255 # Lines with a positive weight are unbalanced and should likely be indented.
256 #
257 # Example:
258 #
259 # def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi
260 # ... % This line will be indented (because of the unterminated `for`)
261 if Weight(prev_text) > 0
262 return DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
263 endif
264
265 # Unterminated statements cause indentation to kick in.
266 #
267 # Example:
268 #
269 # draw unitsquare
270 # withcolor black; % This line is indented because of `draw`.
271 # x := a + b + c
272 # + d + e; % This line is indented because of `:=`.
273 #
274 var i = LastValidMatchEnd(prev_text, MP_STATEMENT, 0)
275 if i >= 0 # Does the line contain a statement?
276 if ValidMatchEnd(prev_text, ';', i) < 0 # Is the statement unterminated?
277 return indent(lnum) + shiftwidth()
278 else
279 return DecreaseIndentOnClosingTag(indent(lnum))
280 endif
281 endif
282
283 # Deal with the special case of a statement spanning multiple lines. If the
284 # current reference line L ends with a semicolon, search backwards for
285 # another semicolon or a statement keyword. If the latter is found first,
286 # its line is used as the reference line for indenting the current line
287 # instead of L.
288 #
289 # Example:
290 #
291 # if cond:
292 # draw if a: z0 else: z1 fi
293 # shifted S
294 # scaled T; % L
295 #
296 # for i = 1 upto 3: % <-- Current line: this gets the same indent as `draw ...`
297 #
298 # NOTE: we get here only if L does not contain a statement (among those
299 # listed in g:MP_STATEMENT).
300 if ValidMatchEnd(prev_text, ';' .. EOL, 0) >= 0 # L ends with a semicolon
301 var stm_lnum = PrevNonBlankNonComment(lnum)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200302 while stm_lnum > 0
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100303 prev_text = getline(stm_lnum)
304 var sc_pos = LastValidMatchEnd(prev_text, ';', 0)
305 var stm_pos = ValidMatchEnd(prev_text, MP_STATEMENT, sc_pos)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200306 if stm_pos > sc_pos
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100307 lnum = stm_lnum
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200308 break
309 elseif sc_pos > stm_pos
310 break
311 endif
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100312 stm_lnum = PrevNonBlankNonComment(stm_lnum)
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200313 endwhile
314 endif
315
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100316 return DecreaseIndentOnClosingTag(indent(lnum))
317enddef
318# }}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000319
Bram Moolenaar7dd54322022-08-26 18:01:12 +0100320# vim: sw=2 fdm=marker