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