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