xref: /vim-8.2.3635/runtime/indent/mp.vim (revision dc083288)
1" MetaPost indent file
2" Language:           MetaPost
3" Maintainer:         Nicola Vitacolonna <[email protected]>
4" Former Maintainers: Eugene Minkovskii <[email protected]>
5" Last Change:        2016 Oct 2, 4:13pm
6" Version: 0.2
7
8if exists("b:did_indent")
9  finish
10endif
11let b:did_indent = 1
12
13setlocal indentexpr=GetMetaPostIndent()
14setlocal indentkeys+==end,=else,=fi,=fill,0),0]
15
16let b:undo_indent = "setl indentkeys< indentexpr<"
17
18" Only define the function once.
19if exists("*GetMetaPostIndent")
20  finish
21endif
22let s:keepcpo= &cpo
23set cpo&vim
24
25function 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\|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
117  endwhile
118  return in_string || (c >= 0 && c <= a:pos)
119endfunction
120
121" Find the first non-comment non-blank line before the current line.
122function! s:PrevNonBlankNonComment(lnum)
123  let l:lnum = prevnonblank(a:lnum - 1)
124  while getline(l:lnum) =~# '^\s*%'
125    let l:lnum = prevnonblank(l:lnum - 1)
126  endwhile
127  return l:lnum
128endfunction
129
130" 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)
163  endwhile
164  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])
201  endif
202  return a:curr_indent
203endfunction
204" }}}
205
206" Main function {{{
207"
208" 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"
221"    def foo =
222"        makepen(
223"            subpath(T-n,t) of r  %>
224"                shifted .5down   %>
225"                    --subpath(t,T) of r shifted .5up -- cycle   %<<<
226"        )
227"        withcolor black
228"    enddef
229"
230" The default indentation of the previous example would be:
231"
232"    def foo =
233"        makepen(
234"            subpath(T-n,t) of r
235"            shifted .5down
236"            --subpath(t,T) of r shifted .5up -- cycle
237"        )
238"        withcolor black
239"    enddef
240"
241" Personally, I prefer the latter, but anyway...
242function! GetMetaPostIndentIntern()
243  " 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
247
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  "
335  " NOTE: we get here only if L does not contain a statement (among those
336  " listed in g:mp_statement).
337  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" }}}
356
357let &cpo = s:keepcpo
358unlet s:keepcpo
359
360" vim:sw=2:fdm=marker
361