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