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