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