30547
|
1 vim9script
|
|
2
|
|
3 # Language: Vim script
|
|
4 # Maintainer: github user lacygoill
|
32721
|
5 # Last Change: 2023 Jun 29
|
30547
|
6
|
30634
|
7 # NOTE: Whenever you change the code, make sure the tests are still passing:
|
|
8 #
|
|
9 # $ cd runtime/indent/
|
31885
|
10 # $ make clean; make test || vimdiff testdir/vim.{ok,fail}
|
30634
|
11
|
30547
|
12 # Config {{{1
|
|
13
|
|
14 const TIMEOUT: number = get(g:, 'vim_indent', {})
|
|
15 ->get('searchpair_timeout', 100)
|
|
16
|
|
17 def IndentMoreInBracketBlock(): number # {{{2
|
|
18 if get(g:, 'vim_indent', {})
|
|
19 ->get('more_in_bracket_block', false)
|
|
20 return shiftwidth()
|
|
21 else
|
|
22 return 0
|
|
23 endif
|
|
24 enddef
|
|
25
|
|
26 def IndentMoreLineContinuation(): number # {{{2
|
|
27 var n: any = get(g:, 'vim_indent', {})
|
|
28 # We inspect `g:vim_indent_cont` to stay backward compatible.
|
|
29 ->get('line_continuation', get(g:, 'vim_indent_cont', shiftwidth() * 3))
|
|
30
|
|
31 if n->typename() == 'string'
|
|
32 return n->eval()
|
|
33 else
|
|
34 return n
|
|
35 endif
|
|
36 enddef
|
|
37 # }}}2
|
|
38
|
|
39 # Init {{{1
|
|
40 var patterns: list<string>
|
|
41 # Tokens {{{2
|
|
42 # BAR_SEPARATION {{{3
|
|
43
|
|
44 const BAR_SEPARATION: string = '[^|\\]\@1<=|'
|
|
45
|
|
46 # OPENING_BRACKET {{{3
|
|
47
|
|
48 const OPENING_BRACKET: string = '[[{(]'
|
|
49
|
|
50 # CLOSING_BRACKET {{{3
|
|
51
|
|
52 const CLOSING_BRACKET: string = '[]})]'
|
|
53
|
|
54 # NON_BRACKET {{{3
|
|
55
|
|
56 const NON_BRACKET: string = '[^[\]{}()]'
|
|
57
|
|
58 # LIST_OR_DICT_CLOSING_BRACKET {{{3
|
|
59
|
|
60 const LIST_OR_DICT_CLOSING_BRACKET: string = '[]}]'
|
|
61
|
|
62 # LIST_OR_DICT_OPENING_BRACKET {{{3
|
|
63
|
|
64 const LIST_OR_DICT_OPENING_BRACKET: string = '[[{]'
|
|
65
|
|
66 # CHARACTER_UNDER_CURSOR {{{3
|
|
67
|
|
68 const CHARACTER_UNDER_CURSOR: string = '\%.c.'
|
|
69
|
|
70 # INLINE_COMMENT {{{3
|
|
71
|
|
72 # TODO: It is not required for an inline comment to be surrounded by whitespace.
|
|
73 # But it might help against false positives.
|
|
74 # To be more reliable, we should inspect the syntax, and only require whitespace
|
|
75 # before the `#` comment leader. But that might be too costly (because of
|
|
76 # `synstack()`).
|
|
77 const INLINE_COMMENT: string = '\s[#"]\%(\s\|[{}]\{3}\)'
|
|
78
|
|
79 # INLINE_VIM9_COMMENT {{{3
|
|
80
|
|
81 const INLINE_VIM9_COMMENT: string = '\s#'
|
|
82
|
|
83 # COMMENT {{{3
|
|
84
|
|
85 # TODO: Technically, `"\s` is wrong.
|
|
86 #
|
|
87 # First, whitespace is not required.
|
|
88 # Second, in Vim9, a string might appear at the start of the line.
|
|
89 # To be sure, we should also inspect the syntax.
|
|
90 # We can't use `INLINE_COMMENT` here. {{{
|
|
91 #
|
|
92 # const COMMENT: string = $'^\s*{INLINE_COMMENT}'
|
|
93 # ^------------^
|
|
94 # ✘
|
|
95 #
|
|
96 # Because `INLINE_COMMENT` asserts the presence of a whitespace before the
|
|
97 # comment leader. This assertion is not satisfied for a comment starting at the
|
|
98 # start of the line.
|
|
99 #}}}
|
|
100 const COMMENT: string = '^\s*\%(#\|"\\\=\s\).*$'
|
|
101
|
|
102 # DICT_KEY {{{3
|
|
103
|
|
104 const DICT_KEY: string = '^\s*\%('
|
|
105 .. '\%(\w\|-\)\+'
|
|
106 .. '\|'
|
|
107 .. '"[^"]*"'
|
|
108 .. '\|'
|
|
109 .. "'[^']*'"
|
|
110 .. '\|'
|
|
111 .. '\[[^]]\+\]'
|
|
112 .. '\)'
|
|
113 .. ':\%(\s\|$\)'
|
|
114
|
|
115 # END_OF_COMMAND {{{3
|
|
116
|
|
117 const END_OF_COMMAND: string = $'\s*\%($\|||\@!\|{INLINE_COMMENT}\)'
|
|
118
|
|
119 # END_OF_LINE {{{3
|
|
120
|
|
121 const END_OF_LINE: string = $'\s*\%($\|{INLINE_COMMENT}\)'
|
|
122
|
|
123 # END_OF_VIM9_LINE {{{3
|
|
124
|
|
125 const END_OF_VIM9_LINE: string = $'\s*\%($\|{INLINE_VIM9_COMMENT}\)'
|
|
126
|
|
127 # OPERATOR {{{3
|
|
128
|
|
129 const OPERATOR: string = '\%(^\|\s\)\%([-+*/%]\|\.\.\|||\|&&\|??\|?\|<<\|>>\|\%([=!]=\|[<>]=\=\|[=!]\~\|is\|isnot\)[?#]\=\)\%(\s\|$\)\@=\%(\s*[|<]\)\@!'
|
|
130 # assignment operators
|
|
131 .. '\|' .. '\s\%([-+*/%]\|\.\.\)\==\%(\s\|$\)\@='
|
|
132 # support `:` when used inside conditional operator `?:`
|
|
133 .. '\|' .. '\%(\s\|^\):\%(\s\|$\)'
|
|
134
|
|
135 # HEREDOC_OPERATOR {{{3
|
|
136
|
|
137 const HEREDOC_OPERATOR: string = '\s=<<\s\@=\%(\s\+\%(trim\|eval\)\)\{,2}'
|
|
138
|
|
139 # PATTERN_DELIMITER {{{3
|
|
140
|
|
141 # A better regex would be:
|
|
142 #
|
|
143 # [^-+*/%.:# \t[:alnum:]\"|]\@=.\|->\@!\%(=\s\)\@!\|[+*/%]\%(=\s\)\@!
|
|
144 #
|
|
145 # But sometimes, it can be too costly and cause `E363` to be given.
|
|
146 const PATTERN_DELIMITER: string = '[-+*/%]\%(=\s\)\@!'
|
|
147 # }}}2
|
|
148 # Syntaxes {{{2
|
31885
|
149 # BLOCKS {{{3
|
30547
|
150
|
31885
|
151 const BLOCKS: list<list<string>> = [
|
|
152 ['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'],
|
|
153 ['for', 'endfor\='],
|
|
154 ['wh\%[ile]', 'endw\%[hile]'],
|
|
155 ['try', 'cat\%[ch]', 'fina\|finally\=', 'endt\%[ry]'],
|
|
156 ['def', 'enddef'],
|
|
157 ['fu\%[nction](\@!', 'endf\%[unction]'],
|
|
158 ['class', 'endclass'],
|
|
159 ['interface', 'endinterface'],
|
|
160 ['enum', 'endenum'],
|
|
161 ['aug\%[roup]\%(\s\+[eE][nN][dD]\)\@!\s\+\S\+', 'aug\%[roup]\s\+[eE][nN][dD]'],
|
|
162 ]
|
|
163
|
|
164 # MODIFIERS {{{3
|
30547
|
165
|
31885
|
166 # some keywords can be prefixed by modifiers (e.g. `def` can be prefixed by `export`)
|
|
167 const MODIFIERS: dict<string> = {
|
|
168 def: ['export', 'static'],
|
|
169 class: ['export', 'abstract', 'export abstract'],
|
|
170 interface: ['export'],
|
|
171 }
|
|
172 # ...
|
|
173 # class: ['export', 'abstract', 'export abstract'],
|
|
174 # ...
|
|
175 # →
|
|
176 # ...
|
|
177 # class: '\%(export\|abstract\|export\s\+abstract\)\s\+',
|
|
178 # ...
|
|
179 ->map((_, mods: list<string>): string =>
|
|
180 '\%(' .. mods
|
|
181 ->join('\|')
|
|
182 ->substitute('\s\+', '\\s\\+', 'g')
|
|
183 .. '\)' .. '\s\+')
|
30547
|
184
|
|
185 # HIGHER_ORDER_COMMAND {{{3
|
|
186
|
|
187 patterns =<< trim eval END
|
|
188 argdo\>!\=
|
|
189 bufdo\>!\=
|
|
190 cdo\>!\=
|
|
191 folddoc\%[losed]\>
|
|
192 foldd\%[oopen]\>
|
|
193 ldo\=\>!\=
|
|
194 tabdo\=\>
|
|
195 windo\>
|
32721
|
196 au\%[tocmd]\>!\=.*
|
|
197 com\%[mand]\>!\=.*
|
30547
|
198 g\%[lobal]!\={PATTERN_DELIMITER}.*
|
|
199 v\%[global]!\={PATTERN_DELIMITER}.*
|
|
200 END
|
|
201
|
32721
|
202 const HIGHER_ORDER_COMMAND: string = $'\%(^\|{BAR_SEPARATION}\)\s*\<\%({patterns->join('\|')}\)\%(\s\|$\)\@='
|
31885
|
203
|
|
204 # START_MIDDLE_END {{{3
|
30547
|
205
|
31885
|
206 # Let's derive this constant from `BLOCKS`:
|
|
207 #
|
|
208 # [['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'],
|
|
209 # ['for', 'endfor\='],
|
|
210 # ...,
|
|
211 # [...]]
|
|
212 # →
|
|
213 # {
|
|
214 # 'for': ['for', '', 'endfor\='],
|
|
215 # 'endfor': ['for', '', 'endfor\='],
|
|
216 # 'if': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'],
|
|
217 # 'else': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'],
|
|
218 # 'elseif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'],
|
|
219 # 'endif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'],
|
|
220 # ...
|
|
221 # }
|
|
222 var START_MIDDLE_END: dict<list<string>>
|
30547
|
223
|
31885
|
224 def Unshorten(kwd: string): string
|
|
225 return BlockStartKeyword(kwd)
|
|
226 enddef
|
30547
|
227
|
31885
|
228 def BlockStartKeyword(line: string): string
|
|
229 var kwd: string = line->matchstr('\l\+')
|
|
230 return fullcommand(kwd, false)
|
|
231 enddef
|
30547
|
232
|
31885
|
233 {
|
|
234 for kwds: list<string> in BLOCKS
|
|
235 var [start: string, middle: string, end: string] = [kwds[0], '', kwds[-1]]
|
|
236 if MODIFIERS->has_key(start->Unshorten())
|
|
237 start = $'\%({MODIFIERS[start]}\)\={start}'
|
|
238 endif
|
|
239 if kwds->len() > 2
|
|
240 middle = kwds[1 : -2]->join('\|')
|
|
241 endif
|
|
242 for kwd: string in kwds
|
|
243 START_MIDDLE_END->extend({[kwd->Unshorten()]: [start, middle, end]})
|
|
244 endfor
|
|
245 endfor
|
|
246 }
|
|
247
|
|
248 START_MIDDLE_END = START_MIDDLE_END
|
|
249 ->map((_, kwds: list<string>) =>
|
|
250 kwds->map((_, kwd: string) => kwd == ''
|
|
251 ? ''
|
|
252 : $'\%(^\|{BAR_SEPARATION}\|\<sil\%[ent]\|{HIGHER_ORDER_COMMAND}\)\s*'
|
32721
|
253 .. $'\<\%({kwd}\)\>\%(\s\|$\|!\)\@=\%(\s*{OPERATOR}\)\@!'))
|
31885
|
254
|
|
255 lockvar! START_MIDDLE_END
|
30547
|
256
|
|
257 # ENDS_BLOCK {{{3
|
|
258
|
|
259 const ENDS_BLOCK: string = '^\s*\%('
|
31885
|
260 .. BLOCKS
|
|
261 ->copy()
|
|
262 ->map((_, kwds: list<string>): string => kwds[-1])
|
|
263 ->join('\|')
|
30547
|
264 .. '\|' .. CLOSING_BRACKET
|
|
265 .. $'\){END_OF_COMMAND}'
|
|
266
|
|
267 # ENDS_BLOCK_OR_CLAUSE {{{3
|
|
268
|
31885
|
269 patterns = BLOCKS
|
|
270 ->copy()
|
|
271 ->map((_, kwds: list<string>) => kwds[1 :])
|
|
272 ->flattennew()
|
|
273 # `catch` and `elseif` need to be handled as special cases
|
|
274 ->filter((_, pat: string): bool => pat->Unshorten() !~ '^\%(catch\|elseif\)\>')
|
30547
|
275
|
|
276 const ENDS_BLOCK_OR_CLAUSE: string = '^\s*\%(' .. patterns->join('\|') .. $'\){END_OF_COMMAND}'
|
|
277 .. $'\|^\s*cat\%[ch]\%(\s\+\({PATTERN_DELIMITER}\).*\1\)\={END_OF_COMMAND}'
|
32721
|
278 .. $'\|^\s*elseif\=\>\%(\s\|$\)\@=\%(\s*{OPERATOR}\)\@!'
|
30547
|
279
|
31885
|
280 # STARTS_NAMED_BLOCK {{{3
|
|
281
|
|
282 patterns = []
|
|
283 {
|
|
284 for kwds: list<string> in BLOCKS
|
|
285 for kwd: string in kwds[0 : -2]
|
|
286 if MODIFIERS->has_key(kwd->Unshorten())
|
|
287 patterns += [$'\%({MODIFIERS[kwd]}\)\={kwd}']
|
|
288 else
|
|
289 patterns += [kwd]
|
|
290 endif
|
|
291 endfor
|
|
292 endfor
|
|
293 }
|
|
294
|
32721
|
295 const STARTS_NAMED_BLOCK: string = $'^\s*\%(sil\%[ent]\s\+\)\=\%({patterns->join('\|')}\)\>\%(\s\|$\|!\)\@='
|
31885
|
296
|
30547
|
297 # STARTS_CURLY_BLOCK {{{3
|
|
298
|
|
299 # TODO: `{` alone on a line is not necessarily the start of a block.
|
|
300 # It could be a dictionary if the previous line ends with a binary/ternary
|
|
301 # operator. This can cause an issue whenever we use `STARTS_CURLY_BLOCK` or
|
|
302 # `LINE_CONTINUATION_AT_EOL`.
|
|
303 const STARTS_CURLY_BLOCK: string = '\%('
|
|
304 .. '^\s*{'
|
|
305 .. '\|' .. '^.*\zs\s=>\s\+{'
|
|
306 .. '\|' .. $'^\%(\s*\|.*{BAR_SEPARATION}\s*\)\%(com\%[mand]\|au\%[tocmd]\).*\zs\s{{'
|
|
307 .. '\)' .. END_OF_COMMAND
|
|
308
|
|
309 # STARTS_FUNCTION {{{3
|
|
310
|
32721
|
311 const STARTS_FUNCTION: string = $'^\s*\%({MODIFIERS.def}\)\=def\>!\=\s\@='
|
30547
|
312
|
|
313 # ENDS_FUNCTION {{{3
|
|
314
|
31885
|
315 const ENDS_FUNCTION: string = $'^\s*enddef\>{END_OF_COMMAND}'
|
|
316
|
|
317 # ASSIGNS_HEREDOC {{{3
|
|
318
|
|
319 const ASSIGNS_HEREDOC: string = $'^\%({COMMENT}\)\@!.*\%({HEREDOC_OPERATOR}\)\s\+\zs[A-Z]\+{END_OF_LINE}'
|
|
320
|
|
321 # PLUS_MINUS_COMMAND {{{3
|
30547
|
322
|
31885
|
323 # In legacy, the `:+` and `:-` commands are not required to be preceded by a colon.
|
|
324 # As a result, when `+` or `-` is alone on a line, there is ambiguity.
|
|
325 # It might be an operator or a command.
|
|
326 # To not break the indentation in legacy scripts, we might need to consider such
|
|
327 # lines as commands.
|
|
328 const PLUS_MINUS_COMMAND: string = '^\s*[+-]\s*$'
|
|
329
|
|
330 # TRICKY_COMMANDS {{{3
|
30547
|
331
|
31885
|
332 # Some commands are tricky because they accept an argument which can be
|
|
333 # conflated with an operator. Examples:
|
|
334 #
|
|
335 # argdelete *
|
|
336 # cd -
|
|
337 # normal! ==
|
|
338 # nunmap <buffer> (
|
|
339 #
|
|
340 # TODO: Other commands might accept operators as argument. Handle them too.
|
|
341 patterns =<< trim eval END
|
|
342 {'\'}<argd\%[elete]\s\+\*\s*$
|
|
343 \<[lt]\=cd!\=\s\+-\s*$
|
|
344 \<norm\%[al]!\=\s*\S\+$
|
|
345 \%(\<sil\%[ent]!\=\s\+\)\=\<[nvxsoilct]\=\%(nore\|un\)map!\=\s
|
|
346 {PLUS_MINUS_COMMAND}
|
|
347 END
|
|
348
|
|
349 const TRICKY_COMMANDS: string = patterns->join('\|')
|
30547
|
350 # }}}2
|
|
351 # EOL {{{2
|
|
352 # OPENING_BRACKET_AT_EOL {{{3
|
|
353
|
30634
|
354 const OPENING_BRACKET_AT_EOL: string = OPENING_BRACKET .. END_OF_VIM9_LINE
|
30547
|
355
|
31885
|
356 # CLOSING_BRACKET_AT_EOL {{{3
|
|
357
|
|
358 const CLOSING_BRACKET_AT_EOL: string = CLOSING_BRACKET .. END_OF_VIM9_LINE
|
|
359
|
30547
|
360 # COMMA_AT_EOL {{{3
|
|
361
|
|
362 const COMMA_AT_EOL: string = $',{END_OF_VIM9_LINE}'
|
|
363
|
|
364 # COMMA_OR_DICT_KEY_AT_EOL {{{3
|
|
365
|
|
366 const COMMA_OR_DICT_KEY_AT_EOL: string = $'\%(,\|{DICT_KEY}\){END_OF_VIM9_LINE}'
|
|
367
|
|
368 # LAMBDA_ARROW_AT_EOL {{{3
|
|
369
|
|
370 const LAMBDA_ARROW_AT_EOL: string = $'\s=>{END_OF_VIM9_LINE}'
|
|
371
|
|
372 # LINE_CONTINUATION_AT_EOL {{{3
|
|
373
|
|
374 const LINE_CONTINUATION_AT_EOL: string = '\%('
|
|
375 .. ','
|
|
376 .. '\|' .. OPERATOR
|
|
377 .. '\|' .. '\s=>'
|
|
378 .. '\|' .. '[^=]\zs[[(]'
|
|
379 .. '\|' .. DICT_KEY
|
|
380 # `{` is ambiguous.
|
|
381 # It can be the start of a dictionary or a block.
|
|
382 # We only want to match the former.
|
|
383 .. '\|' .. $'^\%({STARTS_CURLY_BLOCK}\)\@!.*\zs{{'
|
|
384 .. '\)\s*\%(\s#.*\)\=$'
|
|
385 # }}}2
|
|
386 # SOL {{{2
|
|
387 # BACKSLASH_AT_SOL {{{3
|
|
388
|
|
389 const BACKSLASH_AT_SOL: string = '^\s*\%(\\\|[#"]\\ \)'
|
|
390
|
|
391 # CLOSING_BRACKET_AT_SOL {{{3
|
|
392
|
|
393 const CLOSING_BRACKET_AT_SOL: string = $'^\s*{CLOSING_BRACKET}'
|
|
394
|
|
395 # LINE_CONTINUATION_AT_SOL {{{3
|
|
396
|
|
397 const LINE_CONTINUATION_AT_SOL: string = '^\s*\%('
|
|
398 .. '\\'
|
|
399 .. '\|' .. '[#"]\\ '
|
|
400 .. '\|' .. OPERATOR
|
|
401 .. '\|' .. '->\s*\h'
|
|
402 .. '\|' .. '\.\h' # dict member
|
|
403 .. '\|' .. '|'
|
|
404 # TODO: `}` at the start of a line is not necessarily a line continuation.
|
|
405 # Could be the end of a block.
|
|
406 .. '\|' .. CLOSING_BRACKET
|
|
407 .. '\)'
|
|
408
|
|
409 # RANGE_AT_SOL {{{3
|
|
410
|
|
411 const RANGE_AT_SOL: string = '^\s*:\S'
|
|
412 # }}}1
|
|
413 # Interface {{{1
|
30875
|
414 export def Expr(lnum = v:lnum): number # {{{2
|
30547
|
415 # line which is indented
|
|
416 var line_A: dict<any> = {text: getline(lnum), lnum: lnum}
|
|
417 # line above, on which we'll base the indent of line A
|
|
418 var line_B: dict<any>
|
|
419
|
|
420 if line_A->AtStartOf('HereDoc')
|
|
421 line_A->CacheHeredoc()
|
|
422 elseif line_A.lnum->IsInside('HereDoc')
|
|
423 return line_A.text->HereDocIndent()
|
|
424 elseif line_A.lnum->IsRightBelow('HereDoc')
|
|
425 var ind: number = b:vimindent.startindent
|
|
426 unlet! b:vimindent
|
|
427 return ind
|
|
428 endif
|
|
429
|
|
430 # Don't move this block after the function header one.
|
|
431 # Otherwise, we might clear the cache too early if the line following the
|
|
432 # header is a comment.
|
|
433 if line_A.text =~ COMMENT
|
|
434 return CommentIndent()
|
|
435 endif
|
|
436
|
|
437 line_B = PrevCodeLine(line_A.lnum)
|
|
438 if line_A.text =~ BACKSLASH_AT_SOL
|
|
439 if line_B.text =~ BACKSLASH_AT_SOL
|
|
440 return Indent(line_B.lnum)
|
|
441 else
|
|
442 return Indent(line_B.lnum) + IndentMoreLineContinuation()
|
|
443 endif
|
|
444 endif
|
|
445
|
|
446 if line_A->AtStartOf('FuncHeader')
|
31885
|
447 && !IsInInterface()
|
30547
|
448 line_A.lnum->CacheFuncHeader()
|
|
449 elseif line_A.lnum->IsInside('FuncHeader')
|
|
450 return b:vimindent.startindent + 2 * shiftwidth()
|
|
451 elseif line_A.lnum->IsRightBelow('FuncHeader')
|
|
452 var startindent: number = b:vimindent.startindent
|
|
453 unlet! b:vimindent
|
|
454 if line_A.text =~ ENDS_FUNCTION
|
|
455 return startindent
|
|
456 else
|
|
457 return startindent + shiftwidth()
|
|
458 endif
|
|
459 endif
|
|
460
|
|
461 var past_bracket_block: dict<any>
|
|
462 if exists('b:vimindent')
|
|
463 && b:vimindent->has_key('is_BracketBlock')
|
|
464 past_bracket_block = RemovePastBracketBlock(line_A)
|
|
465 endif
|
|
466 if line_A->AtStartOf('BracketBlock')
|
|
467 line_A->CacheBracketBlock()
|
|
468 endif
|
|
469 if line_A.lnum->IsInside('BracketBlock')
|
30875
|
470 var is_in_curly_block: bool = IsInCurlyBlock()
|
30547
|
471 for block: dict<any> in b:vimindent.block_stack
|
30634
|
472 if line_A.lnum <= block.startlnum
|
|
473 continue
|
|
474 endif
|
|
475 if !block->has_key('startindent')
|
|
476 block.startindent = Indent(block.startlnum)
|
|
477 endif
|
30875
|
478 if !is_in_curly_block
|
30547
|
479 return BracketBlockIndent(line_A, block)
|
|
480 endif
|
|
481 endfor
|
|
482 endif
|
|
483 if line_A.text->ContinuesBelowBracketBlock(line_B, past_bracket_block)
|
|
484 && line_A.text !~ CLOSING_BRACKET_AT_SOL
|
|
485 return past_bracket_block.startindent
|
31885
|
486 + (past_bracket_block.startline =~ STARTS_NAMED_BLOCK ? 2 * shiftwidth() : 0)
|
30547
|
487 endif
|
|
488
|
|
489 # Problem: If we press `==` on the line right below the start of a multiline
|
|
490 # lambda (split after its arrow `=>`), the indent is not correct.
|
|
491 # Solution: Indent relative to the line above.
|
|
492 if line_B->EndsWithLambdaArrow()
|
|
493 return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock()
|
|
494 endif
|
31885
|
495 # FIXME: Similar issue here:
|
|
496 #
|
|
497 # var x = []
|
|
498 # ->filter((_, _) =>
|
|
499 # true)
|
|
500 # ->items()
|
|
501 #
|
|
502 # Press `==` on last line.
|
|
503 # Expected: The `->items()` line is indented like `->filter(...)`.
|
|
504 # Actual: It's indented like `true)`.
|
|
505 # Is it worth fixing? `=ip` gives the correct indentation, because then the
|
|
506 # cache is used.
|
30547
|
507
|
|
508 # Don't move this block before the heredoc one.{{{
|
|
509 #
|
|
510 # A heredoc might be assigned on the very first line.
|
|
511 # And if it is, we need to cache some info.
|
|
512 #}}}
|
|
513 # Don't move it before the function header and bracket block ones either.{{{
|
|
514 #
|
|
515 # You could, because these blocks of code deal with construct which can only
|
|
516 # appear in a Vim9 script. And in a Vim9 script, the first line is
|
|
517 # `vim9script`. Or maybe some legacy code/comment (see `:help vim9-mix`).
|
|
518 # But you can't find a Vim9 function header or Vim9 bracket block on the
|
|
519 # first line.
|
|
520 #
|
|
521 # Anyway, even if you could, don't. First, it would be inconsistent.
|
|
522 # Second, it could give unexpected results while we're trying to fix some
|
|
523 # failing test.
|
|
524 #}}}
|
|
525 if line_A.lnum == 1
|
|
526 return 0
|
|
527 endif
|
|
528
|
|
529 # Don't do that:
|
|
530 # if line_A.text !~ '\S'
|
|
531 # return -1
|
|
532 # endif
|
|
533 # It would prevent a line from being automatically indented when using the
|
|
534 # normal command `o`.
|
|
535 # TODO: Can we write a test for this?
|
|
536
|
|
537 if line_B.text =~ STARTS_CURLY_BLOCK
|
|
538 return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock()
|
|
539
|
|
540 elseif line_A.text =~ CLOSING_BRACKET_AT_SOL
|
|
541 var start: number = MatchingOpenBracket(line_A)
|
|
542 if start <= 0
|
|
543 return -1
|
|
544 endif
|
|
545 return Indent(start) + IndentMoreInBracketBlock()
|
|
546
|
|
547 elseif line_A.text =~ ENDS_BLOCK_OR_CLAUSE
|
|
548 && !line_B->EndsWithLineContinuation()
|
|
549 var kwd: string = BlockStartKeyword(line_A.text)
|
|
550 if !START_MIDDLE_END->has_key(kwd)
|
|
551 return -1
|
|
552 endif
|
|
553
|
|
554 # If the cursor is after the match for the end pattern, we won't find
|
|
555 # the start of the block. Let's make sure that doesn't happen.
|
|
556 cursor(line_A.lnum, 1)
|
|
557
|
|
558 var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd]
|
30634
|
559 var block_start: number = SearchPairStart(start, middle, end)
|
30547
|
560 if block_start > 0
|
|
561 return Indent(block_start)
|
|
562 else
|
|
563 return -1
|
|
564 endif
|
|
565 endif
|
|
566
|
|
567 var base_ind: number
|
|
568 if line_A->IsFirstLineOfCommand(line_B)
|
|
569 line_A.isfirst = true
|
|
570 line_B = line_B->FirstLinePreviousCommand()
|
|
571 base_ind = Indent(line_B.lnum)
|
|
572
|
|
573 if line_B->EndsWithCurlyBlock()
|
|
574 && !line_A->IsInThisBlock(line_B.lnum)
|
|
575 return base_ind
|
|
576 endif
|
|
577
|
|
578 else
|
|
579 line_A.isfirst = false
|
|
580 base_ind = Indent(line_B.lnum)
|
|
581
|
|
582 var line_C: dict<any> = PrevCodeLine(line_B.lnum)
|
|
583 if !line_B->IsFirstLineOfCommand(line_C) || line_C.lnum <= 0
|
|
584 return base_ind
|
|
585 endif
|
|
586 endif
|
|
587
|
|
588 var ind: number = base_ind + Offset(line_A, line_B)
|
|
589 return [ind, 0]->max()
|
|
590 enddef
|
|
591
|
|
592 def g:GetVimIndent(): number # {{{2
|
|
593 # for backward compatibility
|
30875
|
594 return Expr()
|
30547
|
595 enddef
|
|
596 # }}}1
|
|
597 # Core {{{1
|
|
598 def Offset( # {{{2
|
|
599 # we indent this line ...
|
|
600 line_A: dict<any>,
|
|
601 # ... relatively to this line
|
|
602 line_B: dict<any>,
|
|
603 ): number
|
|
604
|
31885
|
605 if line_B->AtStartOf('FuncHeader')
|
|
606 && IsInInterface()
|
|
607 return 0
|
|
608
|
30547
|
609 # increase indentation inside a block
|
31885
|
610 elseif line_B.text =~ STARTS_NAMED_BLOCK
|
|
611 || line_B->EndsWithCurlyBlock()
|
30547
|
612 # But don't indent if the line starting the block also closes it.
|
|
613 if line_B->AlsoClosesBlock()
|
|
614 return 0
|
|
615 # Indent twice for a line continuation in the block header itself, so that
|
|
616 # we can easily distinguish the end of the block header from the start of
|
|
617 # the block body.
|
30634
|
618 elseif (line_B->EndsWithLineContinuation()
|
|
619 && !line_A.isfirst)
|
|
620 || (line_A.text =~ LINE_CONTINUATION_AT_SOL
|
|
621 && line_A.text !~ PLUS_MINUS_COMMAND)
|
30547
|
622 || line_A.text->Is_IN_KeywordForLoop(line_B.text)
|
|
623 return 2 * shiftwidth()
|
|
624 else
|
|
625 return shiftwidth()
|
|
626 endif
|
|
627
|
|
628 # increase indentation of a line if it's the continuation of a command which
|
|
629 # started on a previous line
|
|
630 elseif !line_A.isfirst
|
|
631 && (line_B->EndsWithLineContinuation()
|
|
632 || line_A.text =~ LINE_CONTINUATION_AT_SOL)
|
|
633 return shiftwidth()
|
|
634 endif
|
|
635
|
|
636 return 0
|
|
637 enddef
|
|
638
|
|
639 def HereDocIndent(line_A: string): number # {{{2
|
|
640 # at the end of a heredoc
|
|
641 if line_A =~ $'^\s*{b:vimindent.endmarker}$'
|
|
642 # `END` must be at the very start of the line if the heredoc is not trimmed
|
|
643 if !b:vimindent.is_trimmed
|
|
644 # We can't invalidate the cache just yet.
|
|
645 # The indent of `END` is meaningless; it's always 0. The next line
|
|
646 # will need to be indented relative to the start of the heredoc. It
|
|
647 # must know where it starts; it needs the cache.
|
|
648 return 0
|
|
649 else
|
|
650 var ind: number = b:vimindent.startindent
|
|
651 # invalidate the cache so that it's not used for the next heredoc
|
|
652 unlet! b:vimindent
|
|
653 return ind
|
|
654 endif
|
|
655 endif
|
|
656
|
|
657 # In a non-trimmed heredoc, all of leading whitespace is semantic.
|
|
658 # Leave it alone.
|
|
659 if !b:vimindent.is_trimmed
|
|
660 # But do save the indent of the assignment line.
|
|
661 if !b:vimindent->has_key('startindent')
|
|
662 b:vimindent.startindent = b:vimindent.startlnum->Indent()
|
|
663 endif
|
|
664 return -1
|
|
665 endif
|
|
666
|
|
667 # In a trimmed heredoc, *some* of the leading whitespace is semantic.
|
|
668 # We want to preserve it, so we can't just indent relative to the assignment
|
|
669 # line. That's because we're dealing with data, not with code.
|
|
670 # Instead, we need to compute by how much the indent of the assignment line
|
|
671 # was increased or decreased. Then, we need to apply that same change to
|
|
672 # every line inside the body.
|
|
673 var offset: number
|
|
674 if !b:vimindent->has_key('offset')
|
|
675 var old_startindent: number = b:vimindent.startindent
|
|
676 var new_startindent: number = b:vimindent.startlnum->Indent()
|
|
677 offset = new_startindent - old_startindent
|
|
678
|
|
679 # If all the non-empty lines in the body have a higher indentation relative
|
|
680 # to the assignment, there is no need to indent them more.
|
|
681 # But if at least one of them does have the same indentation level (or a
|
|
682 # lower one), then we want to indent it further (and the whole block with it).
|
|
683 # This way, we can clearly distinguish the heredoc block from the rest of
|
|
684 # the code.
|
|
685 var end: number = search($'^\s*{b:vimindent.endmarker}$', 'nW')
|
|
686 var should_indent_more: bool = range(v:lnum, end - 1)
|
|
687 ->indexof((_, lnum: number): bool => Indent(lnum) <= old_startindent && getline(lnum) != '') >= 0
|
|
688 if should_indent_more
|
|
689 offset += shiftwidth()
|
|
690 endif
|
|
691
|
|
692 b:vimindent.offset = offset
|
|
693 b:vimindent.startindent = new_startindent
|
|
694 endif
|
|
695
|
|
696 return [0, Indent(v:lnum) + b:vimindent.offset]->max()
|
|
697 enddef
|
|
698
|
|
699 def CommentIndent(): number # {{{2
|
|
700 var line_B: dict<any>
|
|
701 line_B.lnum = prevnonblank(v:lnum - 1)
|
|
702 line_B.text = getline(line_B.lnum)
|
|
703 if line_B.text =~ COMMENT
|
|
704 return Indent(line_B.lnum)
|
|
705 endif
|
|
706
|
|
707 var next: number = NextCodeLine()
|
|
708 if next == 0
|
|
709 return 0
|
|
710 endif
|
|
711 var vimindent_save: dict<any> = get(b:, 'vimindent', {})->deepcopy()
|
|
712 var ind: number = next->Expr()
|
|
713 # The previous `Expr()` might have set or deleted `b:vimindent`.
|
|
714 # This could cause issues (e.g. when indenting 2 commented lines above a
|
|
715 # heredoc). Let's make sure the state of the variable is not altered.
|
|
716 if vimindent_save->empty()
|
|
717 unlet! b:vimindent
|
|
718 else
|
|
719 b:vimindent = vimindent_save
|
|
720 endif
|
|
721 if getline(next) =~ ENDS_BLOCK
|
|
722 return ind + shiftwidth()
|
|
723 else
|
|
724 return ind
|
|
725 endif
|
|
726 enddef
|
|
727
|
|
728 def BracketBlockIndent(line_A: dict<any>, block: dict<any>): number # {{{2
|
|
729 var ind: number = block.startindent
|
|
730
|
|
731 if line_A.text =~ CLOSING_BRACKET_AT_SOL
|
|
732 if b:vimindent.is_on_named_block_line
|
|
733 ind += 2 * shiftwidth()
|
|
734 endif
|
|
735 return ind + IndentMoreInBracketBlock()
|
|
736 endif
|
|
737
|
|
738 var startline: dict<any> = {
|
|
739 text: block.startline,
|
|
740 lnum: block.startlnum
|
|
741 }
|
|
742 if startline->EndsWithComma()
|
|
743 || startline->EndsWithLambdaArrow()
|
|
744 || (startline->EndsWithOpeningBracket()
|
|
745 # TODO: Is that reliable?
|
|
746 && block.startline !~
|
|
747 $'^\s*{NON_BRACKET}\+{LIST_OR_DICT_CLOSING_BRACKET},\s\+{LIST_OR_DICT_OPENING_BRACKET}')
|
|
748 ind += shiftwidth() + IndentMoreInBracketBlock()
|
|
749 endif
|
|
750
|
|
751 if b:vimindent.is_on_named_block_line
|
|
752 ind += shiftwidth()
|
|
753 endif
|
|
754
|
|
755 if block.is_dict
|
|
756 && line_A.text !~ DICT_KEY
|
|
757 ind += shiftwidth()
|
|
758 endif
|
|
759
|
|
760 return ind
|
|
761 enddef
|
|
762
|
|
763 def CacheHeredoc(line_A: dict<any>) # {{{2
|
|
764 var endmarker: string = line_A.text->matchstr(ASSIGNS_HEREDOC)
|
|
765 var endlnum: number = search($'^\s*{endmarker}$', 'nW')
|
|
766 var is_trimmed: bool = line_A.text =~ $'.*\s\%(trim\%(\s\+eval\)\=\)\s\+[A-Z]\+{END_OF_LINE}'
|
|
767 b:vimindent = {
|
|
768 is_HereDoc: true,
|
|
769 startlnum: line_A.lnum,
|
|
770 endlnum: endlnum,
|
|
771 endmarker: endmarker,
|
|
772 is_trimmed: is_trimmed,
|
|
773 }
|
|
774 if is_trimmed
|
|
775 b:vimindent.startindent = Indent(line_A.lnum)
|
|
776 endif
|
|
777 RegisterCacheInvalidation()
|
|
778 enddef
|
|
779
|
|
780 def CacheFuncHeader(startlnum: number) # {{{2
|
|
781 var pos: list<number> = getcurpos()
|
|
782 cursor(startlnum, 1)
|
|
783 if search('(', 'W', startlnum) <= 0
|
|
784 return
|
|
785 endif
|
|
786 var endlnum: number = SearchPair('(', '', ')', 'nW')
|
|
787 setpos('.', pos)
|
|
788 if endlnum == startlnum
|
|
789 return
|
|
790 endif
|
|
791
|
|
792 b:vimindent = {
|
|
793 is_FuncHeader: true,
|
|
794 startindent: startlnum->Indent(),
|
|
795 endlnum: endlnum,
|
|
796 }
|
|
797 RegisterCacheInvalidation()
|
|
798 enddef
|
|
799
|
|
800 def CacheBracketBlock(line_A: dict<any>) # {{{2
|
|
801 var pos: list<number> = getcurpos()
|
|
802 var opening: string = line_A.text->matchstr(CHARACTER_UNDER_CURSOR)
|
|
803 var closing: string = {'[': ']', '{': '}', '(': ')'}[opening]
|
|
804 var endlnum: number = SearchPair(opening, '', closing, 'nW')
|
|
805 setpos('.', pos)
|
|
806 if endlnum <= line_A.lnum
|
|
807 return
|
|
808 endif
|
|
809
|
|
810 if !exists('b:vimindent')
|
|
811 b:vimindent = {
|
|
812 is_BracketBlock: true,
|
|
813 is_on_named_block_line: line_A.text =~ STARTS_NAMED_BLOCK,
|
|
814 block_stack: [],
|
|
815 }
|
|
816 endif
|
|
817
|
|
818 var is_dict: bool
|
|
819 var is_curly_block: bool
|
|
820 if opening == '{'
|
|
821 if line_A.text =~ STARTS_CURLY_BLOCK
|
|
822 [is_dict, is_curly_block] = [false, true]
|
|
823 else
|
|
824 [is_dict, is_curly_block] = [true, false]
|
|
825 endif
|
|
826 endif
|
|
827 b:vimindent.block_stack->insert({
|
|
828 is_dict: is_dict,
|
|
829 is_curly_block: is_curly_block,
|
|
830 startline: line_A.text,
|
|
831 startlnum: line_A.lnum,
|
|
832 endlnum: endlnum,
|
|
833 })
|
|
834
|
|
835 RegisterCacheInvalidation()
|
|
836 enddef
|
|
837
|
|
838 def RegisterCacheInvalidation() # {{{2
|
|
839 # invalidate the cache so that it's not used for the next `=` normal command
|
|
840 autocmd_add([{
|
|
841 cmd: 'unlet! b:vimindent',
|
|
842 event: 'ModeChanged',
|
|
843 group: '__VimIndent__',
|
|
844 once: true,
|
|
845 pattern: '*:n',
|
|
846 replace: true,
|
|
847 }])
|
|
848 enddef
|
|
849
|
|
850 def RemovePastBracketBlock(line_A: dict<any>): dict<any> # {{{2
|
|
851 var stack: list<dict<any>> = b:vimindent.block_stack
|
|
852
|
|
853 var removed: dict<any>
|
|
854 if line_A.lnum > stack[0].endlnum
|
|
855 removed = stack[0]
|
|
856 endif
|
|
857
|
|
858 stack->filter((_, block: dict<any>): bool => line_A.lnum <= block.endlnum)
|
|
859 if stack->empty()
|
|
860 unlet! b:vimindent
|
|
861 endif
|
|
862 return removed
|
|
863 enddef
|
|
864 # }}}1
|
|
865 # Util {{{1
|
|
866 # Get {{{2
|
|
867 def Indent(lnum: number): number # {{{3
|
|
868 if lnum <= 0
|
|
869 # Don't return `-1`. It could cause `Expr()` to return a non-multiple of `'shiftwidth'`.{{{
|
|
870 #
|
|
871 # It would be OK if we were always returning `Indent()` directly. But
|
|
872 # we don't. Most of the time, we include it in some computation
|
|
873 # like `Indent(...) + shiftwidth()`. If `'shiftwidth'` is `4`, and
|
|
874 # `Indent()` returns `-1`, `Expr()` will end up returning `3`.
|
|
875 #}}}
|
|
876 return 0
|
|
877 endif
|
|
878 return indent(lnum)
|
|
879 enddef
|
|
880
|
|
881 def MatchingOpenBracket(line: dict<any>): number # {{{3
|
|
882 var end: string = line.text->matchstr(CLOSING_BRACKET)
|
|
883 var start: string = {']': '[', '}': '{', ')': '('}[end]
|
|
884 cursor(line.lnum, 1)
|
|
885 return SearchPairStart(start, '', end)
|
|
886 enddef
|
|
887
|
|
888 def FirstLinePreviousCommand(line: dict<any>): dict<any> # {{{3
|
|
889 var line_B: dict<any> = line
|
|
890
|
|
891 while line_B.lnum > 1
|
|
892 var code_line_above: dict<any> = PrevCodeLine(line_B.lnum)
|
|
893
|
|
894 if line_B.text =~ CLOSING_BRACKET_AT_SOL
|
|
895 var n: number = MatchingOpenBracket(line_B)
|
|
896
|
|
897 if n <= 0
|
|
898 break
|
|
899 endif
|
|
900
|
|
901 line_B.lnum = n
|
|
902 line_B.text = getline(line_B.lnum)
|
|
903 continue
|
|
904
|
|
905 elseif line_B->IsFirstLineOfCommand(code_line_above)
|
|
906 break
|
|
907 endif
|
|
908
|
|
909 line_B = code_line_above
|
|
910 endwhile
|
|
911
|
|
912 return line_B
|
|
913 enddef
|
|
914
|
|
915 def PrevCodeLine(lnum: number): dict<any> # {{{3
|
|
916 var line: string = getline(lnum)
|
|
917 if line =~ '^\s*[A-Z]\+$'
|
|
918 var endmarker: string = line->matchstr('[A-Z]\+')
|
|
919 var pos: list<number> = getcurpos()
|
|
920 cursor(lnum, 1)
|
|
921 var n: number = search(ASSIGNS_HEREDOC, 'bnW')
|
|
922 setpos('.', pos)
|
|
923 if n > 0
|
|
924 line = getline(n)
|
|
925 if line =~ $'{HEREDOC_OPERATOR}\s\+{endmarker}'
|
|
926 return {lnum: n, text: line}
|
|
927 endif
|
|
928 endif
|
|
929 endif
|
|
930
|
|
931 var n: number = prevnonblank(lnum - 1)
|
|
932 line = getline(n)
|
|
933 while line =~ COMMENT && n > 1
|
|
934 n = prevnonblank(n - 1)
|
|
935 line = getline(n)
|
|
936 endwhile
|
|
937 # If we get back to the first line, we return 1 no matter what; even if it's a
|
|
938 # commented line. That should not cause an issue though. We just want to
|
|
939 # avoid a commented line above which there is a line of code which is more
|
|
940 # relevant. There is nothing above the first line.
|
|
941 return {lnum: n, text: line}
|
|
942 enddef
|
|
943
|
|
944 def NextCodeLine(): number # {{{3
|
|
945 var last: number = line('$')
|
|
946 if v:lnum == last
|
|
947 return 0
|
|
948 endif
|
|
949
|
|
950 var lnum: number = v:lnum + 1
|
|
951 while lnum <= last
|
|
952 var line: string = getline(lnum)
|
|
953 if line != '' && line !~ COMMENT
|
|
954 return lnum
|
|
955 endif
|
|
956 ++lnum
|
|
957 endwhile
|
|
958 return 0
|
|
959 enddef
|
|
960
|
|
961 def SearchPair( # {{{3
|
|
962 start: string,
|
|
963 middle: string,
|
|
964 end: string,
|
|
965 flags: string,
|
|
966 stopline = 0,
|
|
967 ): number
|
|
968
|
|
969 var s: string = start
|
|
970 var e: string = end
|
|
971 if start == '[' || start == ']'
|
|
972 s = s->escape('[]')
|
|
973 endif
|
|
974 if end == '[' || end == ']'
|
|
975 e = e->escape('[]')
|
|
976 endif
|
31885
|
977 return searchpair('\C' .. s, (middle == '' ? '' : '\C' .. middle), '\C' .. e,
|
|
978 flags, (): bool => InCommentOrString(), stopline, TIMEOUT)
|
30547
|
979 enddef
|
|
980
|
|
981 def SearchPairStart( # {{{3
|
|
982 start: string,
|
|
983 middle: string,
|
|
984 end: string,
|
|
985 ): number
|
|
986 return SearchPair(start, middle, end, 'bnW')
|
|
987 enddef
|
|
988
|
|
989 def SearchPairEnd( # {{{3
|
|
990 start: string,
|
|
991 middle: string,
|
|
992 end: string,
|
|
993 stopline = 0,
|
|
994 ): number
|
|
995 return SearchPair(start, middle, end, 'nW', stopline)
|
|
996 enddef
|
|
997 # }}}2
|
|
998 # Test {{{2
|
|
999 def AtStartOf(line_A: dict<any>, syntax: string): bool # {{{3
|
|
1000 if syntax == 'BracketBlock'
|
|
1001 return AtStartOfBracketBlock(line_A)
|
|
1002 endif
|
|
1003
|
|
1004 var pat: string = {
|
|
1005 HereDoc: ASSIGNS_HEREDOC,
|
|
1006 FuncHeader: STARTS_FUNCTION
|
|
1007 }[syntax]
|
|
1008 return line_A.text =~ pat
|
|
1009 && (!exists('b:vimindent') || !b:vimindent->has_key('is_HereDoc'))
|
|
1010 enddef
|
|
1011
|
|
1012 def AtStartOfBracketBlock(line_A: dict<any>): bool # {{{3
|
|
1013 # We ignore bracket blocks while we're indenting a function header
|
|
1014 # because it makes the logic simpler. It might mean that we don't
|
|
1015 # indent correctly a multiline bracket block inside a function header,
|
|
1016 # but that's a corner case for which it doesn't seem worth making the
|
|
1017 # code more complex.
|
|
1018 if exists('b:vimindent')
|
|
1019 && !b:vimindent->has_key('is_BracketBlock')
|
|
1020 return false
|
|
1021 endif
|
|
1022
|
|
1023 var pos: list<number> = getcurpos()
|
|
1024 cursor(line_A.lnum, [line_A.lnum, '$']->col())
|
|
1025
|
|
1026 if SearchPair(OPENING_BRACKET, '', CLOSING_BRACKET, 'bcW', line_A.lnum) <= 0
|
|
1027 setpos('.', pos)
|
|
1028 return false
|
|
1029 endif
|
|
1030 # Don't restore the cursor position.
|
|
1031 # It needs to be on a bracket for `CacheBracketBlock()` to work as intended.
|
|
1032
|
|
1033 return line_A->EndsWithOpeningBracket()
|
|
1034 || line_A->EndsWithCommaOrDictKey()
|
|
1035 || line_A->EndsWithLambdaArrow()
|
|
1036 enddef
|
|
1037
|
|
1038 def ContinuesBelowBracketBlock( # {{{3
|
|
1039 line_A: string,
|
|
1040 line_B: dict<any>,
|
|
1041 block: dict<any>
|
|
1042 ): bool
|
|
1043
|
|
1044 return !block->empty()
|
|
1045 && (line_A =~ LINE_CONTINUATION_AT_SOL
|
|
1046 || line_B->EndsWithLineContinuation())
|
|
1047 enddef
|
|
1048
|
|
1049 def IsInside(lnum: number, syntax: string): bool # {{{3
|
|
1050 if !exists('b:vimindent')
|
|
1051 || !b:vimindent->has_key($'is_{syntax}')
|
|
1052 return false
|
|
1053 endif
|
|
1054
|
|
1055 if syntax == 'BracketBlock'
|
|
1056 if !b:vimindent->has_key('block_stack')
|
|
1057 || b:vimindent.block_stack->empty()
|
|
1058 return false
|
|
1059 endif
|
|
1060 return lnum <= b:vimindent.block_stack[0].endlnum
|
|
1061 endif
|
|
1062
|
|
1063 return lnum <= b:vimindent.endlnum
|
|
1064 enddef
|
|
1065
|
|
1066 def IsRightBelow(lnum: number, syntax: string): bool # {{{3
|
|
1067 return exists('b:vimindent')
|
|
1068 && b:vimindent->has_key($'is_{syntax}')
|
|
1069 && lnum > b:vimindent.endlnum
|
|
1070 enddef
|
|
1071
|
30875
|
1072 def IsInCurlyBlock(): bool # {{{3
|
|
1073 return b:vimindent.block_stack
|
|
1074 ->indexof((_, block: dict<any>): bool => block.is_curly_block) >= 0
|
|
1075 enddef
|
|
1076
|
30547
|
1077 def IsInThisBlock(line_A: dict<any>, lnum: number): bool # {{{3
|
|
1078 var pos: list<number> = getcurpos()
|
|
1079 cursor(lnum, [lnum, '$']->col())
|
|
1080 var end: number = SearchPairEnd('{', '', '}')
|
|
1081 setpos('.', pos)
|
|
1082
|
|
1083 return line_A.lnum <= end
|
|
1084 enddef
|
|
1085
|
31885
|
1086 def IsInInterface(): bool # {{{3
|
|
1087 return SearchPair('interface', '', 'endinterface', 'nW') > 0
|
|
1088 enddef
|
|
1089
|
30547
|
1090 def IsFirstLineOfCommand(line_1: dict<any>, line_2: dict<any>): bool # {{{3
|
|
1091 if line_1.text->Is_IN_KeywordForLoop(line_2.text)
|
|
1092 return false
|
|
1093 endif
|
|
1094
|
|
1095 if line_1.text =~ RANGE_AT_SOL
|
|
1096 || line_1.text =~ PLUS_MINUS_COMMAND
|
|
1097 return true
|
|
1098 endif
|
|
1099
|
|
1100 if line_2.text =~ DICT_KEY
|
|
1101 && !line_1->IsInThisBlock(line_2.lnum)
|
|
1102 return true
|
|
1103 endif
|
|
1104
|
|
1105 var line_1_is_good: bool = line_1.text !~ COMMENT
|
|
1106 && line_1.text !~ DICT_KEY
|
|
1107 && line_1.text !~ LINE_CONTINUATION_AT_SOL
|
|
1108
|
|
1109 var line_2_is_good: bool = !line_2->EndsWithLineContinuation()
|
|
1110
|
|
1111 return line_1_is_good && line_2_is_good
|
|
1112 enddef
|
|
1113
|
|
1114 def Is_IN_KeywordForLoop(line_1: string, line_2: string): bool # {{{3
|
|
1115 return line_2 =~ '^\s*for\s'
|
|
1116 && line_1 =~ '^\s*in\s'
|
|
1117 enddef
|
|
1118
|
|
1119 def InCommentOrString(): bool # {{{3
|
32061
|
1120 return synstack('.', col('.'))
|
|
1121 ->indexof((_, id: number): bool => synIDattr(id, 'name') =~ '\ccomment\|string\|heredoc') >= 0
|
30547
|
1122 enddef
|
|
1123
|
|
1124 def AlsoClosesBlock(line_B: dict<any>): bool # {{{3
|
|
1125 # We know that `line_B` opens a block.
|
|
1126 # Let's see if it also closes that block.
|
|
1127 var kwd: string = BlockStartKeyword(line_B.text)
|
|
1128 if !START_MIDDLE_END->has_key(kwd)
|
|
1129 return false
|
|
1130 endif
|
|
1131
|
|
1132 var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd]
|
|
1133 var pos: list<number> = getcurpos()
|
|
1134 cursor(line_B.lnum, 1)
|
|
1135 var block_end: number = SearchPairEnd(start, middle, end, line_B.lnum)
|
|
1136 setpos('.', pos)
|
|
1137
|
|
1138 return block_end > 0
|
|
1139 enddef
|
|
1140
|
|
1141 def EndsWithComma(line: dict<any>): bool # {{{3
|
|
1142 return NonCommentedMatch(line, COMMA_AT_EOL)
|
|
1143 enddef
|
|
1144
|
|
1145 def EndsWithCommaOrDictKey(line_A: dict<any>): bool # {{{3
|
|
1146 return NonCommentedMatch(line_A, COMMA_OR_DICT_KEY_AT_EOL)
|
|
1147 enddef
|
|
1148
|
|
1149 def EndsWithCurlyBlock(line_B: dict<any>): bool # {{{3
|
|
1150 return NonCommentedMatch(line_B, STARTS_CURLY_BLOCK)
|
|
1151 enddef
|
|
1152
|
|
1153 def EndsWithLambdaArrow(line_A: dict<any>): bool # {{{3
|
|
1154 return NonCommentedMatch(line_A, LAMBDA_ARROW_AT_EOL)
|
|
1155 enddef
|
|
1156
|
|
1157 def EndsWithLineContinuation(line_B: dict<any>): bool # {{{3
|
|
1158 return NonCommentedMatch(line_B, LINE_CONTINUATION_AT_EOL)
|
|
1159 enddef
|
|
1160
|
|
1161 def EndsWithOpeningBracket(line: dict<any>): bool # {{{3
|
|
1162 return NonCommentedMatch(line, OPENING_BRACKET_AT_EOL)
|
|
1163 enddef
|
|
1164
|
31885
|
1165 def EndsWithClosingBracket(line: dict<any>): bool # {{{3
|
|
1166 return NonCommentedMatch(line, CLOSING_BRACKET_AT_EOL)
|
|
1167 enddef
|
|
1168
|
30547
|
1169 def NonCommentedMatch(line: dict<any>, pat: string): bool # {{{3
|
|
1170 # Could happen if there is no code above us, and we're not on the 1st line.
|
|
1171 # In that case, `PrevCodeLine()` returns `{lnum: 0, line: ''}`.
|
|
1172 if line.lnum == 0
|
|
1173 return false
|
|
1174 endif
|
|
1175
|
|
1176 # Technically, that's wrong. A line might start with a range and end with a
|
|
1177 # line continuation symbol. But it's unlikely. And it's useful to assume the
|
|
1178 # opposite because it prevents us from conflating a mark with an operator or
|
|
1179 # the start of a list:
|
|
1180 #
|
|
1181 # not a comparison operator
|
|
1182 # v
|
|
1183 # :'< mark <
|
|
1184 # :'< mark [
|
|
1185 # ^
|
|
1186 # not the start of a list
|
|
1187 if line.text =~ RANGE_AT_SOL
|
|
1188 return false
|
|
1189 endif
|
|
1190
|
|
1191 # that's not an arithmetic operator
|
|
1192 # v
|
|
1193 # catch /pattern /
|
|
1194 #
|
|
1195 # When `/` is used as a pattern delimiter, it's always present twice.
|
|
1196 # And usually, the first occurrence is in the middle of a sequence of
|
|
1197 # non-whitespace characters. If we can find such a `/`, we assume that the
|
|
1198 # trailing `/` is not an operator.
|
|
1199 # Warning: Here, don't use a too complex pattern.{{{
|
|
1200 #
|
|
1201 # In particular, avoid backreferences.
|
|
1202 # For example, this would be too costly:
|
|
1203 #
|
|
1204 # if line.text =~ $'\%(\S*\({PATTERN_DELIMITER}\)\S\+\|\S\+\({PATTERN_DELIMITER}\)\S*\)'
|
|
1205 # .. $'\s\+\1{END_OF_COMMAND}'
|
|
1206 #
|
|
1207 # Sometimes, it could even give `E363`.
|
|
1208 #}}}
|
|
1209 var delim: string = line.text
|
|
1210 ->matchstr($'\s\+\zs{PATTERN_DELIMITER}\ze{END_OF_COMMAND}')
|
|
1211 if !delim->empty()
|
|
1212 delim = $'\V{delim}\m'
|
|
1213 if line.text =~ $'\%(\S*{delim}\S\+\|\S\+{delim}\S*\)\s\+{delim}{END_OF_COMMAND}'
|
|
1214 return false
|
|
1215 endif
|
|
1216 endif
|
|
1217 # TODO: We might still miss some corner cases:{{{
|
|
1218 #
|
|
1219 # conflated with arithmetic division
|
|
1220 # v
|
|
1221 # substitute/pat / rep /
|
|
1222 # echo
|
|
1223 # ^--^
|
|
1224 # ✘
|
|
1225 #
|
|
1226 # A better way to handle all these corner cases, would be to inspect the top
|
|
1227 # of the syntax stack:
|
|
1228 #
|
|
1229 # :echo synID('.', col('.'), v:false)->synIDattr('name')
|
|
1230 #
|
|
1231 # Unfortunately, the legacy syntax plugin is not accurate enough.
|
|
1232 # For example, it doesn't highlight a slash as an operator.
|
|
1233 # }}}
|
|
1234
|
|
1235 # `%` at the end of a line is tricky.
|
|
1236 # It might be the modulo operator or the current file (e.g. `edit %`).
|
|
1237 # Let's assume it's the latter.
|
|
1238 if line.text =~ $'%{END_OF_COMMAND}'
|
|
1239 return false
|
|
1240 endif
|
|
1241
|
31885
|
1242 if line.text =~ TRICKY_COMMANDS
|
30547
|
1243 return false
|
|
1244 endif
|
|
1245
|
|
1246 var pos: list<number> = getcurpos()
|
|
1247 cursor(line.lnum, 1)
|
|
1248 var match_lnum: number = search(pat, 'cnW', line.lnum, TIMEOUT, (): bool => InCommentOrString())
|
|
1249 setpos('.', pos)
|
|
1250 return match_lnum > 0
|
|
1251 enddef
|
|
1252 # }}}1
|
|
1253 # vim:sw=4
|