blob: c9639efed3267d5f08dde54f68b7ec721e480005 [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>
5" Last Change: 2016 Oct 01
6" 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]\>'
60 \ . '\|\<end\%(\|for\|group\|def\|fig\|char\|logochar\|glyph\|graph\)\>'
61 \ . '\|[)\]}]'
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 Moolenaar2ec618c2016-10-01 14:47:05 +0200121" Find the first non-comment non-blank line before the current line. Skip also
122" verbatimtex/btex... etex blocks.
123function! s:PrevNonBlankNonComment(lnum)
124 let l:lnum = prevnonblank(a:lnum - 1)
125 while getline(l:lnum) =~# '^\s*%' ||
126 \ synIDattr(synID(a:lnum, 1, 1), "name") =~# '^mpTeXinsert$\|^tex\|^Delimiter'
127 let l:lnum = prevnonblank(l:lnum - 1)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000128 endwhile
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200129 return l:lnum
Bram Moolenaar071d4272004-06-13 20:20:40 +0000130endfunction
131
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200132" Returns true if the last tag appearing in the line is an open tag; returns
133" false otherwise.
134function! s:LastTagIsOpen(line)
135 let o = s:LastValidMatchEnd(a:line, g:mp_open_tag, 0)
136 if o == - 1 | return v:false | endif
137 return s:LastValidMatchEnd(a:line, g:mp_close_tag, o) < 0
138endfunction
139
140" A simple, efficient and quite effective heuristics is used to test whether
141" a line should cause the next line to be indented: count the "opening tags"
142" (if, for, def, ...) in the line, count the "closing tags" (endif, endfor,
143" ...) in the line, and compute the difference. We call the result the
144" "weight" of the line. If the weight is positive, then the next line should
145" most likely be indented. Note that `else` and `elseif` are both opening and
146" closing tags, so they "cancel out" in almost all cases, the only exception
147" being a leading `else[if]`, which is counted as an opening tag, but not as
148" a closing tag (so that, for instance, a line containing a single `else:`
149" will have weight equal to one, not zero). We do not treat a trailing
150" `else[if]` in any special way, because lines ending with an open tag are
151" dealt with separately before this function is called (see
152" GetMetaPostIndentIntern()).
153"
154" Example:
155"
156" forsuffixes $=a,b: if x.$ = y.$ : draw else: fill fi
157" % This line will be indented because |{forsuffixes,if,else}| > |{else,fi}| (3 > 2)
158" endfor
159
160function! s:Weight(line)
161 let [o, i] = [0, s:ValidMatchEnd(a:line, g:mp_open_tag, 0)]
162 while i > 0
163 let o += 1
164 let i = s:ValidMatchEnd(a:line, g:mp_open_tag, i)
Bram Moolenaar071d4272004-06-13 20:20:40 +0000165 endwhile
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200166 let [c, i] = [0, matchend(a:line, '^\s*\<else\%[if]\>')] " Skip a leading else[if]
167 let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
168 while i > 0
169 let c += 1
170 let i = s:ValidMatchEnd(a:line, g:mp_close_tag, i)
171 endwhile
172 return o - c
173endfunction
174
175" Similar to matchend(), but skips strings and comments.
176" line: a String
177function! s:ValidMatchEnd(line, pat, start)
178 let i = matchend(a:line, a:pat, a:start)
179 while i > 0 && s:CommentOrString(a:line, i)
180 let i = matchend(a:line, a:pat, i)
181 endwhile
182 return i
183endfunction
184
185" Like s:ValidMatchEnd(), but returns the end position of the last (i.e.,
186" rightmost) match.
187function! s:LastValidMatchEnd(line, pat, start)
188 let last_found = -1
189 let i = matchend(a:line, a:pat, a:start)
190 while i > 0
191 if !s:CommentOrString(a:line, i)
192 let last_found = i
193 endif
194 let i = matchend(a:line, a:pat, i)
195 endwhile
196 return last_found
197endfunction
198
199function! s:DecreaseIndentOnClosingTag(curr_indent)
200 let cur_text = getline(v:lnum)
201 if cur_text =~# '^\s*\%('.g:mp_close_tag.'\)'
202 return max([a:curr_indent - shiftwidth(), 0])
Bram Moolenaar071d4272004-06-13 20:20:40 +0000203 endif
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200204 return a:curr_indent
Bram Moolenaar071d4272004-06-13 20:20:40 +0000205endfunction
206" }}}
207
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200208" Main function {{{
Bram Moolenaar071d4272004-06-13 20:20:40 +0000209"
Bram Moolenaar2ec618c2016-10-01 14:47:05 +0200210" Note: Every rule of indentation in MetaPost is very subjective. We might get
211" creative, but things get murky very soon (there are too many corner cases).
212" So, we provide a means for the user to decide what to do when this script
213" doesn't get it. We use a simple idea: use '%>', '%<' and '%=' to explicitly
214" control indentation. The '<' and '>' symbols may be repeated many times
215" (e.g., '%>>' will cause the next line to be indented twice).
216"
217" By using '%>...', '%<...' and '%=', the indentation the user wants is
218" preserved by commands like gg=G, even if it does not follow the rules of
219" this script.
220"
221" Example:
222"
223" shiftwidth=4
224" def foo =
225" makepen(subpath(T-n,t) of r %>
226" shifted .5down %>
227" --subpath(t,T) of r shifted .5up -- cycle) %<<
228" withcolor black
229" enddef
230"
231" The default indentation of the previous example would be:
232"
233" def foo =
234" makepen(subpath(T-n,t) of r
235" shifted .5down
236" --subpath(t,T) of r shifted .5up -- cycle)
237" withcolor black
238" enddef
239"
240" Personally, I prefer the latter, but anyway...
241function! GetMetaPostIndentIntern()
242
243 " This is the reference line relative to which the current line is indented
244 " (but see below).
245 let lnum = s:PrevNonBlankNonComment(v:lnum)
246
247 " At the start of the file use zero indent.
248 if lnum == 0
249 return 0
250 endif
251
252 let prev_text = getline(lnum)
253
254 " User-defined overrides take precedence over anything else.
255 " See above for an example.
256 let j = match(prev_text, '%[<>=]')
257 if j > 0
258 let i = strlen(matchstr(prev_text, '%>\+', j)) - 1
259 if i > 0
260 return indent(lnum) + i * shiftwidth()
261 endif
262
263 let i = strlen(matchstr(prev_text, '%<\+', j)) - 1
264 if i > 0
265 return max([indent(lnum) - i * shiftwidth(), 0])
266 endif
267
268 if match(prev_text, '%=', j)
269 return indent(lnum)
270 endif
271 endif
272
273 " If the reference line ends with an open tag, indent.
274 "
275 " Example:
276 "
277 " if c:
278 " 0
279 " else:
280 " 1
281 " fi if c2: % Note that this line has weight equal to zero.
282 " ... % This line will be indented
283 if s:LastTagIsOpen(prev_text)
284 return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
285 endif
286
287 " Lines with a positive weight are unbalanced and should likely be indented.
288 "
289 " Example:
290 "
291 " def f = enddef for i = 1 upto 5: if x[i] > 0: 1 else: 2 fi
292 " ... % This line will be indented (because of the unterminated `for`)
293 if s:Weight(prev_text) > 0
294 return s:DecreaseIndentOnClosingTag(indent(lnum) + shiftwidth())
295 endif
296
297 " Unterminated statements cause indentation to kick in.
298 "
299 " Example:
300 "
301 " draw unitsquare
302 " withcolor black; % This line is indented because of `draw`.
303 " x := a + b + c
304 " + d + e; % This line is indented because of `:=`.
305 "
306 let i = s:LastValidMatchEnd(prev_text, g:mp_statement, 0)
307 if i >= 0 " Does the line contain a statement?
308 if s:ValidMatchEnd(prev_text, ';', i) < 0 " Is the statement unterminated?
309 return indent(lnum) + shiftwidth()
310 else
311 return s:DecreaseIndentOnClosingTag(indent(lnum))
312 endif
313 endif
314
315 " Deal with the special case of a statement spanning multiple lines. If the
316 " current reference line L ends with a semicolon, search backwards for
317 " another semicolon or a statement keyword. If the latter is found first,
318 " its line is used as the reference line for indenting the current line
319 " instead of L.
320 "
321 " Example:
322 "
323 " if cond:
324 " draw if a: z0 else: z1 fi
325 " shifted S
326 " scaled T; % L
327 "
328 " for i = 1 upto 3: % <-- Current line: this gets the same indent as `draw ...`
329 "
330 " NOTE: we get here if and only if L does not contain a statement (among
331 " those listed in g:mp_statement).
332 if s:ValidMatchEnd(prev_text, ';'.s:eol, 0) >= 0 " L ends with a semicolon
333 let stm_lnum = s:PrevNonBlankNonComment(lnum)
334 while stm_lnum > 0
335 let prev_text = getline(stm_lnum)
336 let sc_pos = s:LastValidMatchEnd(prev_text, ';', 0)
337 let stm_pos = s:ValidMatchEnd(prev_text, g:mp_statement, sc_pos)
338 if stm_pos > sc_pos
339 let lnum = stm_lnum
340 break
341 elseif sc_pos > stm_pos
342 break
343 endif
344 let stm_lnum = s:PrevNonBlankNonComment(stm_lnum)
345 endwhile
346 endif
347
348 return s:DecreaseIndentOnClosingTag(indent(lnum))
349endfunction
350" }}}
Bram Moolenaar071d4272004-06-13 20:20:40 +0000351
Bram Moolenaar8e52a592012-05-18 21:49:28 +0200352let &cpo = s:keepcpo
353unlet s:keepcpo
354
Bram Moolenaar071d4272004-06-13 20:20:40 +0000355" vim:sw=2:fdm=marker