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