blob: 86fa2539c12209c821f8c036151d009673e2e034 [file] [log] [blame]
Bram Moolenaar071d4272004-06-13 20:20:40 +00001" MetaPost indent file
Bram Moolenaar2ec618c2016-10-01 14:47:05 +02002" Language: MetaPost
3" Maintainer: Nicola Vitacolonna <nvitacolonna@gmail.com>
4" Former Maintainers: Eugene Minkovskii <emin@mccme.ru>
Bram Moolenaardc083282016-10-11 08:57:33 +02005" Last Change: 2016 Oct 2, 4:13pm
Bram Moolenaar2ec618c2016-10-01 14:47:05 +02006" Version: 0.2
Bram Moolenaar071d4272004-06-13 20:20:40 +00007
Bram Moolenaar071d4272004-06-13 20:20:40 +00008if exists("b:did_indent")
9 finish
10endif
11let b:did_indent = 1
12
13setlocal indentexpr=GetMetaPostIndent()
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020014setlocal indentkeys+==end,=else,=fi,=fill,0),0]
15
16let b:undo_indent = "setl indentkeys< indentexpr<"
Bram Moolenaar071d4272004-06-13 20:20:40 +000017
18" Only define the function once.
19if exists("*GetMetaPostIndent")
20 finish
21endif
Bram Moolenaar8e52a592012-05-18 21:49:28 +020022let s:keepcpo= &cpo
23set cpo&vim
Bram Moolenaar071d4272004-06-13 20:20:40 +000024
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020025function GetMetaPostIndent()
26 let ignorecase_save = &ignorecase
27 try
28 let &ignorecase = 0
29 return GetMetaPostIndentIntern()
30 finally
31 let &ignorecase = ignorecase_save
32 endtry
33endfunc
34
35" Regexps {{{
36" Note: the next three variables are made global so that a user may add
37" further keywords.
38"
39" Example:
40"
41" Put these in ~/.vim/after/indent/mp.vim
42"
43" let g:mp_open_tag .= '\|\<begintest\>'
44" let g:mp_close_tag .= '\|\<endtest\>'
45
46" Expressions starting indented blocks
47let g:mp_open_tag = ''
48 \ . '\<if\>'
49 \ . '\|\<else\%[if]\>'
50 \ . '\|\<for\%(\|ever\|suffixes\)\>'
51 \ . '\|\<begingroup\>'
52 \ . '\|\<\%(\|var\|primary\|secondary\|tertiary\)def\>'
53 \ . '\|^\s*\<begin\%(fig\|graph\|glyph\|char\|logochar\)\>'
54 \ . '\|[([{]'
55
56" Expressions ending indented blocks
57let g:mp_close_tag = ''
58 \ . '\<fi\>'
59 \ . '\|\<else\%[if]\>'
Bram Moolenaardc083282016-10-11 08:57:33 +020060 \ . '\|\<end\%(\|for\|group\|def\|fig\|char\|glyph\|graph\)\>'
Bram Moolenaar2ec618c2016-10-01 14:47:05 +020061 \ . '\|[)\]}]'
62
63" Statements that may span multiple lines and are ended by a semicolon. To
64" keep this list short, statements that are unlikely to be very long or are
65" not very common (e.g., keywords like `interim` or `showtoken`) are not
66" included.
67"
68" The regex for assignments and equations (the last branch) is tricky, because
69" it must not match things like `for i :=`, `if a=b`, `def...=`, etc... It is
70" not perfect, but it works reasonably well.
71let g:mp_statement = ''
72 \ . '\<\%(\|un\|cut\)draw\>'
73 \ . '\|\<\%(\|un\)fill\%[draw]\>'
74 \ . '\|\<draw\%(dbl\)\=arrow\>'
75 \ . '\|\<clip\>'
76 \ . '\|\<addto\>'
77 \ . '\|\<save\>'
78 \ . '\|\<setbounds\>'
79 \ . '\|\<message\>'
80 \ . '\|\<errmessage\>'
81 \ . '\|\<errhelp\>'
82 \ . '\|\<fontmapline\>'
83 \ . '\|\<pickup\>'
84 \ . '\|\<show\>'
85 \ . '\|\<special\>'
86 \ . '\|\<write\>'
87 \ . '\|\%(^\|;\)\%([^;=]*\%('.g:mp_open_tag.'\)\)\@!.\{-}:\=='
88
89" A line ends with zero or more spaces, possibly followed by a comment.
90let s:eol = '\s*\%($\|%\)'
91" }}}
92
93" Auxiliary functions {{{
94" Returns 1 if (0-based) position immediately preceding `pos` in `line` is
95" inside a string or a comment; returns 0 otherwise.
96
97" This is the function that is called more often when indenting, so it is
98" critical that it is efficient. The method we use is significantly faster
99" than using syntax attributes, and more general (it does not require
100" syntax_items). It is also faster than using a single regex matching an even
101" number of quotes. It helps that MetaPost strings cannot span more than one
102" line and cannot contain escaped quotes.
103function! s:CommentOrString(line, pos)
104 let in_string = 0
105 let q = stridx(a:line, '"')
106 let c = stridx(a:line, '%')
107 while q >= 0 && q < a:pos
108 if c >= 0 && c < q
109 if in_string " Find next percent symbol
110 let c = stridx(a:line, '%', q + 1)
111 else " Inside comment
112 return 1
113 endif
114 endif
115 let in_string = 1 - in_string
116 let q = stridx(a:line, '"', q + 1) " Find next quote
Bram Moolenaar071d4272004-06-13 20:20:40 +0000117 endwhile
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200118 return in_string || (c >= 0 && c <= a:pos)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000119endfunction
120
Bram Moolenaardc083282016-10-11 08:57:33 +0200121" Find the first non-comment non-blank line before the current line.
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200122function! s:PrevNonBlankNonComment(lnum)
123 let l:lnum = prevnonblank(a:lnum - 1)
Bram Moolenaardc083282016-10-11 08:57:33 +0200124 while getline(l:lnum) =~# '^\s*%'
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200125 let l:lnum = prevnonblank(l:lnum - 1)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000126 endwhile
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200127 return l:lnum
Bram Moolenaar071d4272004-06-13 20:20:40 +0000128endfunction
129
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200130" Returns true if the last tag appearing in the line is an open tag; returns
131" false otherwise.
132function! s:LastTagIsOpen(line)
133 let o = s:LastValidMatchEnd(a:line, g:mp_open_tag, 0)
134 if o == - 1 | return v:false | endif
135 return s:LastValidMatchEnd(a:line, g:mp_close_tag, o) < 0
136endfunction
137
138" A simple, efficient and quite effective heuristics is used to test whether
139" a line should cause the next line to be indented: count the "opening tags"
140" (if, for, def, ...) in the line, count the "closing tags" (endif, endfor,
141" ...) in the line, and compute the difference. We call the result the
142" "weight" of the line. If the weight is positive, then the next line should
143" most likely be indented. Note that `else` and `elseif` are both opening and
144" closing tags, so they "cancel out" in almost all cases, the only exception
145" being a leading `else[if]`, which is counted as an opening tag, but not as
146" a closing tag (so that, for instance, a line containing a single `else:`
147" will have weight equal to one, not zero). We do not treat a trailing
148" `else[if]` in any special way, because lines ending with an open tag are
149" dealt with separately before this function is called (see
150" GetMetaPostIndentIntern()).
151"
152" Example:
153"
154" forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi
155" % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2)
156" endfor
157
158function! s:Weight(line)
159 let [o, i] = [0, s:ValidMatchEnd(a:line, g:mp_open_tag, 0)]
160 while i > 0
161 let o += 1
162 let i = s:ValidMatchEnd(a:line, g:mp_open_tag, i)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000163 endwhile
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200164 let [c, i] = [0, matchend(a:line, '^\s*\<else\%[if]\>')] " Skip a leading else[if]
165 let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
166 while i > 0
167 let c += 1
168 let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
169 endwhile
170 return o - c
171endfunction
172
173" Similar to matchend(), but skips strings and comments.
174" line: a String
175function! s:ValidMatchEnd(line, pat, start)
176 let i = matchend(a:line, a:pat, a:start)
177 while i > 0 && s:CommentOrString(a:line, i)
178 let i = matchend(a:line, a:pat, i)
179 endwhile
180 return i
181endfunction
182
183" Like s:ValidMatchEnd(), but returns the end position of the last (i.e.,
184" rightmost) match.
185function! s:LastValidMatchEnd(line, pat, start)
186 let last_found = -1
187 let i = matchend(a:line, a:pat, a:start)
188 while i > 0
189 if !s:CommentOrString(a:line, i)
190 let last_found = i
191 endif
192 let i = matchend(a:line, a:pat, i)
193 endwhile
194 return last_found
195endfunction
196
197function! s:DecreaseIndentOnClosingTag(curr_indent)
198 let cur_text = getline(v:lnum)
199 if cur_text =~# '^\s*\%('.g:mp_close_tag.'\)'
200 return max([a:curr_indent - shiftwidth(), 0])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000201 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200202 return a:curr_indent
Bram Moolenaar071d4272004-06-13 20:20:40 +0000203endfunction
204" }}}
205
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200206" Main function {{{
Bram Moolenaar071d4272004-06-13 20:20:40 +0000207"
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200208" Note: Every rule of indentation in MetaPost is very subjective. We might get
209" creative, but things get murky very soon (there are too many corner cases).
210" So, we provide a means for the user to decide what to do when this script
211" doesn't get it. We use a simple idea: use '%>', '%<' and '%=' to explicitly
212" control indentation. The '<' and '>' symbols may be repeated many times
213" (e.g., '%>>' will cause the next line to be indented twice).
214"
215" By using '%>...', '%<...' and '%=', the indentation the user wants is
216" preserved by commands like gg=G, even if it does not follow the rules of
217" this script.
218"
219" Example:
220"
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200221" def foo =
Bram Moolenaardc083282016-10-11 08:57:33 +0200222" makepen(
223" subpath(T-n,t) of r %>
224" shifted .5down %>
225" --subpath(t,T) of r shifted .5up -- cycle %<<<
226" )
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200227" withcolor black
228" enddef
229"
230" The default indentation of the previous example would be:
231"
232" def foo =
Bram Moolenaardc083282016-10-11 08:57:33 +0200233" makepen(
234" subpath(T-n,t) of r
235" shifted .5down
236" --subpath(t,T) of r shifted .5up -- cycle
237" )
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200238" withcolor black
239" enddef
240"
241" Personally, I prefer the latter, but anyway...
242function! GetMetaPostIndentIntern()
Bram Moolenaardc083282016-10-11 08:57:33 +0200243 " Do not touch indentation inside verbatimtex/btex.. etex blocks.
244 if synIDattr(synID(v:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter'
245 return -1
246 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200247
248 " This is the reference line relative to which the current line is indented
249 " (but see below).
250 let lnum = s:PrevNonBlankNonComment(v:lnum)
251
252 " At the start of the file use zero indent.
253 if lnum == 0
254 return 0
255 endif
256
257 let prev_text = getline(lnum)
258
259 " User-defined overrides take precedence over anything else.
260 " See above for an example.
261 let j = match(prev_text, '%[<>=]')
262 if j > 0
263 let i = strlen(matchstr(prev_text, '%>\+', j)) - 1
264 if i > 0
265 return indent(lnum) + i * shiftwidth()
266 endif
267
268 let i = strlen(matchstr(prev_text, '%<\+', j)) - 1
269 if i > 0
270 return max([indent(lnum) - i * shiftwidth(), 0])
271 endif
272
273 if match(prev_text, '%=', j)
274 return indent(lnum)
275 endif
276 endif
277
278 " If the reference line ends with an open tag, indent.
279 "
280 " Example:
281 "
282 " if c:
283 " 0
284 " else:
285 " 1
286 " fi if c2: % Note that this line has weight equal to zero.
287 " ... % This line will be indented
288 if s:LastTagIsOpen(prev_text)
289 return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
290 endif
291
292 " Lines with a positive weight are unbalanced and should likely be indented.
293 "
294 " Example:
295 "
296 " def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi
297 " ... % This line will be indented (because of the unterminated `for`)
298 if s:Weight(prev_text) > 0
299 return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
300 endif
301
302 " Unterminated statements cause indentation to kick in.
303 "
304 " Example:
305 "
306 " draw unitsquare
307 " withcolor black; % This line is indented because of `draw`.
308 " x := a + b + c
309 " + d + e; % This line is indented because of `:=`.
310 "
311 let i = s:LastValidMatchEnd(prev_text, g:mp_statement, 0)
312 if i >= 0 " Does the line contain a statement?
313 if s:ValidMatchEnd(prev_text, ';', i) < 0 " Is the statement unterminated?
314 return indent(lnum) + shiftwidth()
315 else
316 return s:DecreaseIndentOnClosingTag(indent(lnum))
317 endif
318 endif
319
320 " Deal with the special case of a statement spanning multiple lines. If the
321 " current reference line L ends with a semicolon, search backwards for
322 " another semicolon or a statement keyword. If the latter is found first,
323 " its line is used as the reference line for indenting the current line
324 " instead of L.
325 "
326 " Example:
327 "
328 " if cond:
329 " draw if a: z0 else: z1 fi
330 " shifted S
331 " scaled T; % L
332 "
333 " for i = 1 upto 3: % <-- Current line: this gets the same indent as `draw ...`
334 "
Bram Moolenaardc083282016-10-11 08:57:33 +0200335 " NOTE: we get here only if L does not contain a statement (among those
336 " listed in g:mp_statement).
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200337 if s:ValidMatchEnd(prev_text, ';'.s:eol, 0) >= 0 " L ends with a semicolon
338 let stm_lnum = s:PrevNonBlankNonComment(lnum)
339 while stm_lnum > 0
340 let prev_text = getline(stm_lnum)
341 let sc_pos = s:LastValidMatchEnd(prev_text, ';', 0)
342 let stm_pos = s:ValidMatchEnd(prev_text, g:mp_statement, sc_pos)
343 if stm_pos > sc_pos
344 let lnum = stm_lnum
345 break
346 elseif sc_pos > stm_pos
347 break
348 endif
349 let stm_lnum = s:PrevNonBlankNonComment(stm_lnum)
350 endwhile
351 endif
352
353 return s:DecreaseIndentOnClosingTag(indent(lnum))
354endfunction
355" }}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000356
Bram Moolenaar8e52a592012-05-18 21:49:28 +0200357let &cpo = s:keepcpo
358unlet s:keepcpo
359
Bram Moolenaar071d4272004-06-13 20:20:40 +0000360" vim:sw=2:fdm=marker