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