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