| vim9script |
| |
| # Language: Vim script |
| # Maintainer: github user lacygoill |
| # Last Change: 2025 Apr 13 |
| # |
| # Includes changes from The Vim Project: |
| # - 2024 Feb 09: Fix indent after literal Dict (A. Radev via #13966) |
| # - 2024 Nov 08: Fix indent after :silent! function (D. Kearns via #16009) |
| # - 2024 Dec 26: Fix indent for enums (Jim Zhou via #16293) |
| |
| # NOTE: Whenever you change the code, make sure the tests are still passing: |
| # |
| # $ cd runtime/indent/ |
| # $ make clean; make test || vimdiff testdir/vim.{ok,fail} |
| |
| # Config {{{1 |
| |
| const TIMEOUT: number = get(g:, 'vim_indent', {}) |
| ->get('searchpair_timeout', 100) |
| |
| def IndentMoreInBracketBlock(): number # {{{2 |
| if get(g:, 'vim_indent', {}) |
| ->get('more_in_bracket_block', false) |
| return shiftwidth() |
| else |
| return 0 |
| endif |
| enddef |
| |
| def IndentMoreLineContinuation(): number # {{{2 |
| var n: any = get(g:, 'vim_indent', {}) |
| # We inspect `g:vim_indent_cont` to stay backward compatible. |
| ->get('line_continuation', get(g:, 'vim_indent_cont', shiftwidth() * 3)) |
| |
| if n->typename() == 'string' |
| return n->eval() |
| else |
| return n |
| endif |
| enddef |
| # }}}2 |
| |
| # Init {{{1 |
| var patterns: list<string> |
| # Tokens {{{2 |
| # BAR_SEPARATION {{{3 |
| |
| const BAR_SEPARATION: string = '[^|\\]\@1<=|' |
| |
| # OPENING_BRACKET {{{3 |
| |
| const OPENING_BRACKET: string = '[[{(]' |
| |
| # CLOSING_BRACKET {{{3 |
| |
| const CLOSING_BRACKET: string = '[]})]' |
| |
| # NON_BRACKET {{{3 |
| |
| const NON_BRACKET: string = '[^[\]{}()]' |
| |
| # LIST_OR_DICT_CLOSING_BRACKET {{{3 |
| |
| const LIST_OR_DICT_CLOSING_BRACKET: string = '[]}]' |
| |
| # LIST_OR_DICT_OPENING_BRACKET {{{3 |
| |
| const LIST_OR_DICT_OPENING_BRACKET: string = '[[{]' |
| |
| # CHARACTER_UNDER_CURSOR {{{3 |
| |
| const CHARACTER_UNDER_CURSOR: string = '\%.c.' |
| |
| # INLINE_COMMENT {{{3 |
| |
| # TODO: It is not required for an inline comment to be surrounded by whitespace. |
| # But it might help against false positives. |
| # To be more reliable, we should inspect the syntax, and only require whitespace |
| # before the `#` comment leader. But that might be too costly (because of |
| # `synstack()`). |
| const INLINE_COMMENT: string = '\s[#"]\%(\s\|[{}]\{3}\)' |
| |
| # INLINE_VIM9_COMMENT {{{3 |
| |
| const INLINE_VIM9_COMMENT: string = '\s#' |
| |
| # COMMENT {{{3 |
| |
| # TODO: Technically, `"\s` is wrong. |
| # |
| # First, whitespace is not required. |
| # Second, in Vim9, a string might appear at the start of the line. |
| # To be sure, we should also inspect the syntax. |
| # We can't use `INLINE_COMMENT` here. {{{ |
| # |
| # const COMMENT: string = $'^\s*{INLINE_COMMENT}' |
| # ^------------^ |
| # ✘ |
| # |
| # Because `INLINE_COMMENT` asserts the presence of a whitespace before the |
| # comment leader. This assertion is not satisfied for a comment starting at the |
| # start of the line. |
| #}}} |
| const COMMENT: string = '^\s*\%(#\|"\\\=\s\).*$' |
| |
| # DICT_KEY {{{3 |
| |
| const DICT_KEY: string = '^\s*\%(' |
| .. '\%(\w\|-\)\+' |
| .. '\|' |
| .. '"[^"]*"' |
| .. '\|' |
| .. "'[^']*'" |
| .. '\|' |
| .. '\[[^]]\+\]' |
| .. '\)' |
| .. ':\%(\s\|$\)' |
| |
| # END_OF_COMMAND {{{3 |
| |
| const END_OF_COMMAND: string = $'\s*\%($\|||\@!\|{INLINE_COMMENT}\)' |
| |
| # END_OF_LINE {{{3 |
| |
| const END_OF_LINE: string = $'\s*\%($\|{INLINE_COMMENT}\)' |
| |
| # END_OF_VIM9_LINE {{{3 |
| |
| const END_OF_VIM9_LINE: string = $'\s*\%($\|{INLINE_VIM9_COMMENT}\)' |
| |
| # OPERATOR {{{3 |
| |
| const OPERATOR: string = '\%(^\|\s\)\%([-+*/%]\|\.\.\|||\|&&\|??\|?\|<<\|>>\|\%([=!]=\|[<>]=\=\|[=!]\~\|is\|isnot\)[?#]\=\)\%(\s\|$\)\@=\%(\s*[|<]\)\@!' |
| # assignment operators |
| .. '\|' .. '\s\%([-+*/%]\|\.\.\)\==\%(\s\|$\)\@=' |
| # support `:` when used inside conditional operator `?:` |
| .. '\|' .. '\%(\s\|^\):\%(\s\|$\)' |
| |
| # HEREDOC_OPERATOR {{{3 |
| |
| const HEREDOC_OPERATOR: string = '\s=<<\s\@=\%(\s\+\%(trim\|eval\)\)\{,2}' |
| |
| # PATTERN_DELIMITER {{{3 |
| |
| # A better regex would be: |
| # |
| # [^-+*/%.:# \t[:alnum:]\"|]\@=.\|->\@!\%(=\s\)\@!\|[+*/%]\%(=\s\)\@! |
| # |
| # But sometimes, it can be too costly and cause `E363` to be given. |
| const PATTERN_DELIMITER: string = '[-+*/%]\%(=\s\)\@!' |
| # }}}2 |
| # Syntaxes {{{2 |
| # BLOCKS {{{3 |
| |
| const BLOCKS: list<list<string>> = [ |
| ['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], |
| ['for', 'endfor\='], |
| ['wh\%[ile]', 'endw\%[hile]'], |
| ['try', 'cat\%[ch]', 'fina\|finally\=', 'endt\%[ry]'], |
| ['def', 'enddef'], |
| ['fu\%[nction](\@!', 'endf\%[unction]'], |
| ['class', 'endclass'], |
| ['interface', 'endinterface'], |
| ['enum', 'endenum'], |
| ['aug\%[roup]\%(\s\+[eE][nN][dD]\)\@!\s\+\S\+', 'aug\%[roup]\s\+[eE][nN][dD]'], |
| ] |
| |
| # MODIFIERS {{{3 |
| |
| # some keywords can be prefixed by modifiers (e.g. `def` can be prefixed by `export`) |
| const MODIFIERS: dict<string> = { |
| def: ['export', 'static'], |
| class: ['export', 'abstract', 'export abstract'], |
| interface: ['export'], |
| enum: ['export'], |
| } |
| # ... |
| # class: ['export', 'abstract', 'export abstract'], |
| # ... |
| # → |
| # ... |
| # class: '\%(export\|abstract\|export\s\+abstract\)\s\+', |
| # ... |
| ->map((_, mods: list<string>): string => |
| '\%(' .. mods |
| ->join('\|') |
| ->substitute('\s\+', '\\s\\+', 'g') |
| .. '\)' .. '\s\+') |
| |
| # HIGHER_ORDER_COMMAND {{{3 |
| |
| patterns =<< trim eval END |
| argdo\>!\= |
| bufdo\>!\= |
| cdo\>!\= |
| folddoc\%[losed]\> |
| foldd\%[oopen]\> |
| ldo\=\>!\= |
| tabdo\=\> |
| windo\> |
| au\%[tocmd]\>!\=.* |
| com\%[mand]\>!\=.* |
| g\%[lobal]!\={PATTERN_DELIMITER}.* |
| v\%[global]!\={PATTERN_DELIMITER}.* |
| END |
| |
| const HIGHER_ORDER_COMMAND: string = $'\%(^\|{BAR_SEPARATION}\)\s*\<\%({patterns->join('\|')}\)\%(\s\|$\)\@=' |
| |
| # START_MIDDLE_END {{{3 |
| |
| # Let's derive this constant from `BLOCKS`: |
| # |
| # [['if', 'el\%[se]', 'elseif\=', 'en\%[dif]'], |
| # ['for', 'endfor\='], |
| # ..., |
| # [...]] |
| # → |
| # { |
| # 'for': ['for', '', 'endfor\='], |
| # 'endfor': ['for', '', 'endfor\='], |
| # 'if': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], |
| # 'else': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], |
| # 'elseif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], |
| # 'endif': ['if', 'el\%[se]\|elseif\=', 'en\%[dif]'], |
| # ... |
| # } |
| var START_MIDDLE_END: dict<list<string>> |
| |
| def Unshorten(kwd: string): string |
| return BlockStartKeyword(kwd) |
| enddef |
| |
| def BlockStartKeyword(line: string): string |
| var kwd: string = line->matchstr('\l\+') |
| return fullcommand(kwd, false) |
| enddef |
| |
| { |
| for kwds: list<string> in BLOCKS |
| var [start: string, middle: string, end: string] = [kwds[0], '', kwds[-1]] |
| if MODIFIERS->has_key(start->Unshorten()) |
| start = $'\%({MODIFIERS[start]}\)\={start}' |
| endif |
| if kwds->len() > 2 |
| middle = kwds[1 : -2]->join('\|') |
| endif |
| for kwd: string in kwds |
| START_MIDDLE_END->extend({[kwd->Unshorten()]: [start, middle, end]}) |
| endfor |
| endfor |
| } |
| |
| START_MIDDLE_END = START_MIDDLE_END |
| ->map((_, kwds: list<string>) => |
| kwds->map((_, kwd: string) => kwd == '' |
| ? '' |
| : $'\%(^\|{BAR_SEPARATION}\|\<sil\%[ent]\|{HIGHER_ORDER_COMMAND}\)\s*' |
| .. $'\<\%({kwd}\)\>\%(\s\|$\|!\)\@=\%(\s*{OPERATOR}\)\@!')) |
| |
| lockvar! START_MIDDLE_END |
| |
| # ENDS_BLOCK {{{3 |
| |
| const ENDS_BLOCK: string = '^\s*\%(' |
| .. BLOCKS |
| ->copy() |
| ->map((_, kwds: list<string>): string => kwds[-1]) |
| ->join('\|') |
| .. '\|' .. CLOSING_BRACKET |
| .. $'\){END_OF_COMMAND}' |
| |
| # ENDS_BLOCK_OR_CLAUSE {{{3 |
| |
| patterns = BLOCKS |
| ->copy() |
| ->map((_, kwds: list<string>) => kwds[1 :]) |
| ->flattennew() |
| # `catch` and `elseif` need to be handled as special cases |
| ->filter((_, pat: string): bool => pat->Unshorten() !~ '^\%(catch\|elseif\)\>') |
| |
| const ENDS_BLOCK_OR_CLAUSE: string = '^\s*\%(' .. patterns->join('\|') .. $'\){END_OF_COMMAND}' |
| .. $'\|^\s*cat\%[ch]\%(\s\+\({PATTERN_DELIMITER}\).*\1\)\={END_OF_COMMAND}' |
| .. $'\|^\s*elseif\=\>\%(\s\|$\)\@=\%(\s*{OPERATOR}\)\@!' |
| |
| # STARTS_NAMED_BLOCK {{{3 |
| |
| patterns = [] |
| { |
| for kwds: list<string> in BLOCKS |
| for kwd: string in kwds[0 : -2] |
| if MODIFIERS->has_key(kwd->Unshorten()) |
| patterns += [$'\%({MODIFIERS[kwd]}\)\={kwd}'] |
| else |
| patterns += [kwd] |
| endif |
| endfor |
| endfor |
| } |
| |
| const STARTS_NAMED_BLOCK: string = $'^\s*\%(sil\%[ent]!\=\s\+\)\=\%({patterns->join('\|')}\)\>\%(\s\|$\|!\)\@=' |
| |
| # STARTS_CURLY_BLOCK {{{3 |
| |
| # TODO: `{` alone on a line is not necessarily the start of a block. |
| # It could be a dictionary if the previous line ends with a binary/ternary |
| # operator. This can cause an issue whenever we use `STARTS_CURLY_BLOCK` or |
| # `LINE_CONTINUATION_AT_EOL`. |
| const STARTS_CURLY_BLOCK: string = '\%(' |
| .. '^\s*{' |
| .. '\|' .. '^.*\zs\s=>\s\+{' |
| .. '\|' .. $'^\%(\s*\|.*{BAR_SEPARATION}\s*\)\%(com\%[mand]\|au\%[tocmd]\).*\zs\s{{' |
| .. '\)' .. END_OF_COMMAND |
| |
| # STARTS_FUNCTION {{{3 |
| |
| const STARTS_FUNCTION: string = $'^\s*\%({MODIFIERS.def}\)\=def\>!\=\s\@=' |
| |
| # ENDS_FUNCTION {{{3 |
| |
| const ENDS_FUNCTION: string = $'^\s*enddef\>{END_OF_COMMAND}' |
| |
| # ASSIGNS_HEREDOC {{{3 |
| |
| const ASSIGNS_HEREDOC: string = $'^\%({COMMENT}\)\@!.*\%({HEREDOC_OPERATOR}\)\s\+\zs[A-Z]\+{END_OF_LINE}' |
| |
| # PLUS_MINUS_COMMAND {{{3 |
| |
| # In legacy, the `:+` and `:-` commands are not required to be preceded by a colon. |
| # As a result, when `+` or `-` is alone on a line, there is ambiguity. |
| # It might be an operator or a command. |
| # To not break the indentation in legacy scripts, we might need to consider such |
| # lines as commands. |
| const PLUS_MINUS_COMMAND: string = '^\s*[+-]\s*$' |
| |
| # TRICKY_COMMANDS {{{3 |
| |
| # Some commands are tricky because they accept an argument which can be |
| # conflated with an operator. Examples: |
| # |
| # argdelete * |
| # cd - |
| # normal! == |
| # nunmap <buffer> ( |
| # |
| # TODO: Other commands might accept operators as argument. Handle them too. |
| patterns =<< trim eval END |
| {'\'}<argd\%[elete]\s\+\*\s*$ |
| \<[lt]\=cd!\=\s\+-\s*$ |
| \<norm\%[al]!\=\s*\S\+$ |
| \%(\<sil\%[ent]!\=\s\+\)\=\<[nvxsoilct]\=\%(nore\|un\)map!\=\s |
| {PLUS_MINUS_COMMAND} |
| END |
| |
| const TRICKY_COMMANDS: string = patterns->join('\|') |
| # }}}2 |
| # EOL {{{2 |
| # OPENING_BRACKET_AT_EOL {{{3 |
| |
| const OPENING_BRACKET_AT_EOL: string = OPENING_BRACKET .. END_OF_VIM9_LINE |
| |
| # CLOSING_BRACKET_AT_EOL {{{3 |
| |
| const CLOSING_BRACKET_AT_EOL: string = CLOSING_BRACKET .. END_OF_VIM9_LINE |
| |
| # COMMA_AT_EOL {{{3 |
| |
| const COMMA_AT_EOL: string = $',{END_OF_VIM9_LINE}' |
| |
| # COMMA_OR_DICT_KEY_AT_EOL {{{3 |
| |
| const COMMA_OR_DICT_KEY_AT_EOL: string = $'\%(,\|{DICT_KEY}\){END_OF_VIM9_LINE}' |
| |
| # LAMBDA_ARROW_AT_EOL {{{3 |
| |
| const LAMBDA_ARROW_AT_EOL: string = $'\s=>{END_OF_VIM9_LINE}' |
| |
| # LINE_CONTINUATION_AT_EOL {{{3 |
| |
| const LINE_CONTINUATION_AT_EOL: string = '\%(' |
| .. ',' |
| .. '\|' .. OPERATOR |
| .. '\|' .. '\s=>' |
| .. '\|' .. '[^=]\zs[[(]' |
| .. '\|' .. DICT_KEY |
| # `{` is ambiguous. |
| # It can be the start of a dictionary or a block. |
| # We only want to match the former. |
| .. '\|' .. $'^\%({STARTS_CURLY_BLOCK}\)\@!.*\zs{{' |
| .. '\)\s*\%(\s#[^{].*\)\=$' |
| # }}}2 |
| # SOL {{{2 |
| # BACKSLASH_AT_SOL {{{3 |
| |
| const BACKSLASH_AT_SOL: string = '^\s*\%(\\\|[#"]\\ \)' |
| |
| # CLOSING_BRACKET_AT_SOL {{{3 |
| |
| const CLOSING_BRACKET_AT_SOL: string = $'^\s*{CLOSING_BRACKET}' |
| |
| # LINE_CONTINUATION_AT_SOL {{{3 |
| |
| const LINE_CONTINUATION_AT_SOL: string = '^\s*\%(' |
| .. '\\' |
| .. '\|' .. '[#"]\\ ' |
| .. '\|' .. OPERATOR |
| .. '\|' .. '->\s*\h' |
| .. '\|' .. '\.\h' # dict member |
| .. '\|' .. '|' |
| # TODO: `}` at the start of a line is not necessarily a line continuation. |
| # Could be the end of a block. |
| .. '\|' .. CLOSING_BRACKET |
| .. '\)' |
| |
| # RANGE_AT_SOL {{{3 |
| |
| const RANGE_AT_SOL: string = '^\s*:\S' |
| # }}}1 |
| # Interface {{{1 |
| export def Expr(lnum = v:lnum): number # {{{2 |
| # line which is indented |
| var line_A: dict<any> = {text: getline(lnum), lnum: lnum} |
| # line above, on which we'll base the indent of line A |
| var line_B: dict<any> |
| |
| if line_A->AtStartOf('HereDoc') |
| line_A->CacheHeredoc() |
| elseif line_A.lnum->IsInside('HereDoc') |
| return line_A.text->HereDocIndent() |
| elseif line_A.lnum->IsRightBelow('HereDoc') |
| var ind: number = b:vimindent.startindent |
| unlet! b:vimindent |
| return ind |
| endif |
| |
| # Don't move this block after the function header one. |
| # Otherwise, we might clear the cache too early if the line following the |
| # header is a comment. |
| if line_A.text =~ COMMENT |
| return CommentIndent() |
| endif |
| |
| line_B = PrevCodeLine(line_A.lnum) |
| if line_A.text =~ BACKSLASH_AT_SOL |
| if line_B.text =~ BACKSLASH_AT_SOL |
| return Indent(line_B.lnum) |
| else |
| return Indent(line_B.lnum) + IndentMoreLineContinuation() |
| endif |
| endif |
| |
| if line_A->AtStartOf('FuncHeader') |
| && !IsInInterface() |
| line_A.lnum->CacheFuncHeader() |
| elseif line_A.lnum->IsInside('FuncHeader') |
| return b:vimindent.startindent + 2 * shiftwidth() |
| elseif line_A.lnum->IsRightBelow('FuncHeader') |
| var startindent: number = b:vimindent.startindent |
| unlet! b:vimindent |
| if line_A.text =~ ENDS_FUNCTION |
| return startindent |
| else |
| return startindent + shiftwidth() |
| endif |
| endif |
| |
| var past_bracket_block: dict<any> |
| if exists('b:vimindent') |
| && b:vimindent->has_key('is_BracketBlock') |
| past_bracket_block = RemovePastBracketBlock(line_A) |
| endif |
| if line_A->AtStartOf('BracketBlock') |
| line_A->CacheBracketBlock() |
| endif |
| if line_A.lnum->IsInside('BracketBlock') |
| var is_in_curly_block: bool = IsInCurlyBlock() |
| for block: dict<any> in b:vimindent.block_stack |
| if line_A.lnum <= block.startlnum |
| continue |
| endif |
| if !block->has_key('startindent') |
| block.startindent = Indent(block.startlnum) |
| endif |
| if !is_in_curly_block |
| return BracketBlockIndent(line_A, block) |
| endif |
| endfor |
| endif |
| if line_A.text->ContinuesBelowBracketBlock(line_B, past_bracket_block) |
| && line_A.text !~ CLOSING_BRACKET_AT_SOL |
| return past_bracket_block.startindent |
| + (past_bracket_block.startline =~ STARTS_NAMED_BLOCK ? 2 * shiftwidth() : 0) |
| endif |
| |
| # Problem: If we press `==` on the line right below the start of a multiline |
| # lambda (split after its arrow `=>`), the indent is not correct. |
| # Solution: Indent relative to the line above. |
| if line_B->EndsWithLambdaArrow() |
| return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() |
| endif |
| # FIXME: Similar issue here: |
| # |
| # var x = [] |
| # ->filter((_, _) => |
| # true) |
| # ->items() |
| # |
| # Press `==` on last line. |
| # Expected: The `->items()` line is indented like `->filter(...)`. |
| # Actual: It's indented like `true)`. |
| # Is it worth fixing? `=ip` gives the correct indentation, because then the |
| # cache is used. |
| |
| # Don't move this block before the heredoc one.{{{ |
| # |
| # A heredoc might be assigned on the very first line. |
| # And if it is, we need to cache some info. |
| #}}} |
| # Don't move it before the function header and bracket block ones either.{{{ |
| # |
| # You could, because these blocks of code deal with construct which can only |
| # appear in a Vim9 script. And in a Vim9 script, the first line is |
| # `vim9script`. Or maybe some legacy code/comment (see `:help vim9-mix`). |
| # But you can't find a Vim9 function header or Vim9 bracket block on the |
| # first line. |
| # |
| # Anyway, even if you could, don't. First, it would be inconsistent. |
| # Second, it could give unexpected results while we're trying to fix some |
| # failing test. |
| #}}} |
| if line_A.lnum == 1 |
| return 0 |
| endif |
| |
| # Don't do that: |
| # if line_A.text !~ '\S' |
| # return -1 |
| # endif |
| # It would prevent a line from being automatically indented when using the |
| # normal command `o`. |
| # TODO: Can we write a test for this? |
| |
| if line_B.text =~ STARTS_CURLY_BLOCK |
| return Indent(line_B.lnum) + shiftwidth() + IndentMoreInBracketBlock() |
| |
| elseif line_A.text =~ CLOSING_BRACKET_AT_SOL |
| var start: number = MatchingOpenBracket(line_A) |
| if start <= 0 |
| return -1 |
| endif |
| return Indent(start) + IndentMoreInBracketBlock() |
| |
| elseif line_A.text =~ ENDS_BLOCK_OR_CLAUSE |
| && !line_B->EndsWithLineContinuation() |
| var kwd: string = BlockStartKeyword(line_A.text) |
| if !START_MIDDLE_END->has_key(kwd) |
| return -1 |
| endif |
| |
| # If the cursor is after the match for the end pattern, we won't find |
| # the start of the block. Let's make sure that doesn't happen. |
| cursor(line_A.lnum, 1) |
| |
| var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] |
| var block_start: number = SearchPairStart(start, middle, end) |
| if block_start > 0 |
| return Indent(block_start) |
| else |
| return -1 |
| endif |
| endif |
| |
| var base_ind: number |
| if line_A->IsFirstLineOfCommand(line_B) |
| line_A.isfirst = true |
| line_B = line_B->FirstLinePreviousCommand() |
| base_ind = Indent(line_B.lnum) |
| |
| if line_B->EndsWithCurlyBlock() |
| && !line_A->IsInThisBlock(line_B.lnum) |
| return base_ind |
| endif |
| |
| else |
| line_A.isfirst = false |
| base_ind = Indent(line_B.lnum) |
| |
| var line_C: dict<any> = PrevCodeLine(line_B.lnum) |
| if !line_B->IsFirstLineOfCommand(line_C) || line_C.lnum <= 0 |
| return base_ind |
| endif |
| endif |
| |
| var ind: number = base_ind + Offset(line_A, line_B) |
| return [ind, 0]->max() |
| enddef |
| |
| def g:GetVimIndent(): number # {{{2 |
| # for backward compatibility |
| return Expr() |
| enddef |
| # }}}1 |
| # Core {{{1 |
| def Offset( # {{{2 |
| # we indent this line ... |
| line_A: dict<any>, |
| # ... relatively to this line |
| line_B: dict<any>, |
| ): number |
| |
| if line_B->AtStartOf('FuncHeader') |
| && IsInInterface() |
| return 0 |
| |
| # increase indentation inside a block |
| elseif line_B.text =~ STARTS_NAMED_BLOCK |
| || line_B->EndsWithCurlyBlock() |
| # But don't indent if the line starting the block also closes it. |
| if line_B->AlsoClosesBlock() |
| return 0 |
| # Indent twice for a line continuation in the block header itself, so that |
| # we can easily distinguish the end of the block header from the start of |
| # the block body. |
| elseif (line_B->EndsWithLineContinuation() |
| && !line_A.isfirst) |
| || (line_A.text =~ LINE_CONTINUATION_AT_SOL |
| && line_A.text !~ PLUS_MINUS_COMMAND) |
| || line_A.text->Is_IN_KeywordForLoop(line_B.text) |
| return 2 * shiftwidth() |
| else |
| return shiftwidth() |
| endif |
| |
| # increase indentation of a line if it's the continuation of a command which |
| # started on a previous line |
| elseif !line_A.isfirst |
| && (line_B->EndsWithLineContinuation() |
| || line_A.text =~ LINE_CONTINUATION_AT_SOL) |
| && !(line_B->EndsWithComma() && line_A.lnum->IsInside('EnumBlock')) |
| return shiftwidth() |
| endif |
| |
| return 0 |
| enddef |
| |
| def HereDocIndent(line_A: string): number # {{{2 |
| # at the end of a heredoc |
| if line_A =~ $'^\s*{b:vimindent.endmarker}$' |
| # `END` must be at the very start of the line if the heredoc is not trimmed |
| if !b:vimindent.is_trimmed |
| # We can't invalidate the cache just yet. |
| # The indent of `END` is meaningless; it's always 0. The next line |
| # will need to be indented relative to the start of the heredoc. It |
| # must know where it starts; it needs the cache. |
| return 0 |
| else |
| var ind: number = b:vimindent.startindent |
| # invalidate the cache so that it's not used for the next heredoc |
| unlet! b:vimindent |
| return ind |
| endif |
| endif |
| |
| # In a non-trimmed heredoc, all of leading whitespace is semantic. |
| # Leave it alone. |
| if !b:vimindent.is_trimmed |
| # But do save the indent of the assignment line. |
| if !b:vimindent->has_key('startindent') |
| b:vimindent.startindent = b:vimindent.startlnum->Indent() |
| endif |
| return -1 |
| endif |
| |
| # In a trimmed heredoc, *some* of the leading whitespace is semantic. |
| # We want to preserve it, so we can't just indent relative to the assignment |
| # line. That's because we're dealing with data, not with code. |
| # Instead, we need to compute by how much the indent of the assignment line |
| # was increased or decreased. Then, we need to apply that same change to |
| # every line inside the body. |
| var offset: number |
| if !b:vimindent->has_key('offset') |
| var old_startindent: number = b:vimindent.startindent |
| var new_startindent: number = b:vimindent.startlnum->Indent() |
| offset = new_startindent - old_startindent |
| |
| # If all the non-empty lines in the body have a higher indentation relative |
| # to the assignment, there is no need to indent them more. |
| # But if at least one of them does have the same indentation level (or a |
| # lower one), then we want to indent it further (and the whole block with it). |
| # This way, we can clearly distinguish the heredoc block from the rest of |
| # the code. |
| var end: number = search($'^\s*{b:vimindent.endmarker}$', 'nW') |
| var should_indent_more: bool = range(v:lnum, end - 1) |
| ->indexof((_, lnum: number): bool => Indent(lnum) <= old_startindent && getline(lnum) != '') >= 0 |
| if should_indent_more |
| offset += shiftwidth() |
| endif |
| |
| b:vimindent.offset = offset |
| b:vimindent.startindent = new_startindent |
| endif |
| |
| return [0, Indent(v:lnum) + b:vimindent.offset]->max() |
| enddef |
| |
| def CommentIndent(): number # {{{2 |
| var line_B: dict<any> |
| line_B.lnum = prevnonblank(v:lnum - 1) |
| line_B.text = getline(line_B.lnum) |
| if line_B.text =~ COMMENT |
| return Indent(line_B.lnum) |
| endif |
| |
| var next: number = NextCodeLine() |
| if next == 0 |
| return 0 |
| endif |
| var vimindent_save: dict<any> = get(b:, 'vimindent', {})->deepcopy() |
| var ind: number = next->Expr() |
| # The previous `Expr()` might have set or deleted `b:vimindent`. |
| # This could cause issues (e.g. when indenting 2 commented lines above a |
| # heredoc). Let's make sure the state of the variable is not altered. |
| if vimindent_save->empty() |
| unlet! b:vimindent |
| else |
| b:vimindent = vimindent_save |
| endif |
| if getline(next) =~ ENDS_BLOCK |
| return ind + shiftwidth() |
| else |
| return ind |
| endif |
| enddef |
| |
| def BracketBlockIndent(line_A: dict<any>, block: dict<any>): number # {{{2 |
| var ind: number = block.startindent |
| |
| if line_A.text =~ CLOSING_BRACKET_AT_SOL |
| if b:vimindent.is_on_named_block_line |
| ind += 2 * shiftwidth() |
| endif |
| return ind + IndentMoreInBracketBlock() |
| endif |
| |
| var startline: dict<any> = { |
| text: block.startline, |
| lnum: block.startlnum |
| } |
| if startline->EndsWithComma() |
| || startline->EndsWithLambdaArrow() |
| || (startline->EndsWithOpeningBracket() |
| # TODO: Is that reliable? |
| && block.startline !~ |
| $'^\s*{NON_BRACKET}\+{LIST_OR_DICT_CLOSING_BRACKET},\s\+{LIST_OR_DICT_OPENING_BRACKET}') |
| ind += shiftwidth() + IndentMoreInBracketBlock() |
| endif |
| |
| if b:vimindent.is_on_named_block_line |
| ind += shiftwidth() |
| endif |
| |
| if block.is_dict |
| && line_A.text !~ DICT_KEY |
| ind += shiftwidth() |
| endif |
| |
| return ind |
| enddef |
| |
| def CacheHeredoc(line_A: dict<any>) # {{{2 |
| var endmarker: string = line_A.text->matchstr(ASSIGNS_HEREDOC) |
| var endlnum: number = search($'^\s*{endmarker}$', 'nW') |
| var is_trimmed: bool = line_A.text =~ $'.*\s\%(trim\%(\s\+eval\)\=\)\s\+[A-Z]\+{END_OF_LINE}' |
| b:vimindent = { |
| is_HereDoc: true, |
| startlnum: line_A.lnum, |
| endlnum: endlnum, |
| endmarker: endmarker, |
| is_trimmed: is_trimmed, |
| } |
| if is_trimmed |
| b:vimindent.startindent = Indent(line_A.lnum) |
| endif |
| RegisterCacheInvalidation() |
| enddef |
| |
| def CacheFuncHeader(startlnum: number) # {{{2 |
| var pos: list<number> = getcurpos() |
| cursor(startlnum, 1) |
| if search('(', 'W', startlnum) <= 0 |
| return |
| endif |
| var endlnum: number = SearchPair('(', '', ')', 'nW') |
| setpos('.', pos) |
| if endlnum == startlnum |
| return |
| endif |
| |
| b:vimindent = { |
| is_FuncHeader: true, |
| startindent: startlnum->Indent(), |
| endlnum: endlnum, |
| } |
| RegisterCacheInvalidation() |
| enddef |
| |
| def CacheBracketBlock(line_A: dict<any>) # {{{2 |
| var pos: list<number> = getcurpos() |
| var opening: string = line_A.text->matchstr(CHARACTER_UNDER_CURSOR) |
| var closing: string = {'[': ']', '{': '}', '(': ')'}[opening] |
| var endlnum: number = SearchPair(opening, '', closing, 'nW') |
| setpos('.', pos) |
| if endlnum <= line_A.lnum |
| return |
| endif |
| |
| if !exists('b:vimindent') |
| b:vimindent = { |
| is_BracketBlock: true, |
| is_on_named_block_line: line_A.text =~ STARTS_NAMED_BLOCK, |
| block_stack: [], |
| } |
| endif |
| |
| var is_dict: bool |
| var is_curly_block: bool |
| if opening == '{' |
| if line_A.text =~ STARTS_CURLY_BLOCK |
| [is_dict, is_curly_block] = [false, true] |
| else |
| [is_dict, is_curly_block] = [true, false] |
| endif |
| endif |
| b:vimindent.block_stack->insert({ |
| is_dict: is_dict, |
| is_curly_block: is_curly_block, |
| startline: line_A.text, |
| startlnum: line_A.lnum, |
| endlnum: endlnum, |
| }) |
| |
| RegisterCacheInvalidation() |
| enddef |
| |
| def RegisterCacheInvalidation() # {{{2 |
| # invalidate the cache so that it's not used for the next `=` normal command |
| autocmd_add([{ |
| cmd: 'unlet! b:vimindent', |
| event: 'ModeChanged', |
| group: '__VimIndent__', |
| once: true, |
| pattern: '*:n', |
| replace: true, |
| }]) |
| enddef |
| |
| def RemovePastBracketBlock(line_A: dict<any>): dict<any> # {{{2 |
| var stack: list<dict<any>> = b:vimindent.block_stack |
| |
| var removed: dict<any> |
| if line_A.lnum > stack[0].endlnum |
| removed = stack[0] |
| endif |
| |
| stack->filter((_, block: dict<any>): bool => line_A.lnum <= block.endlnum) |
| if stack->empty() |
| unlet! b:vimindent |
| endif |
| return removed |
| enddef |
| # }}}1 |
| # Util {{{1 |
| # Get {{{2 |
| def Indent(lnum: number): number # {{{3 |
| if lnum <= 0 |
| # Don't return `-1`. It could cause `Expr()` to return a non-multiple of `'shiftwidth'`.{{{ |
| # |
| # It would be OK if we were always returning `Indent()` directly. But |
| # we don't. Most of the time, we include it in some computation |
| # like `Indent(...) + shiftwidth()`. If `'shiftwidth'` is `4`, and |
| # `Indent()` returns `-1`, `Expr()` will end up returning `3`. |
| #}}} |
| return 0 |
| endif |
| return indent(lnum) |
| enddef |
| |
| def MatchingOpenBracket(line: dict<any>): number # {{{3 |
| var end: string = line.text->matchstr(CLOSING_BRACKET) |
| var start: string = {']': '[', '}': '{', ')': '('}[end] |
| cursor(line.lnum, 1) |
| return SearchPairStart(start, '', end) |
| enddef |
| |
| def FirstLinePreviousCommand(line: dict<any>): dict<any> # {{{3 |
| var line_B: dict<any> = line |
| |
| while line_B.lnum > 1 |
| var code_line_above: dict<any> = PrevCodeLine(line_B.lnum) |
| |
| if line_B.text =~ CLOSING_BRACKET_AT_SOL |
| var n: number = MatchingOpenBracket(line_B) |
| |
| if n <= 0 |
| break |
| endif |
| |
| line_B.lnum = n |
| line_B.text = getline(line_B.lnum) |
| continue |
| |
| elseif line_B->IsFirstLineOfCommand(code_line_above) |
| break |
| endif |
| |
| line_B = code_line_above |
| endwhile |
| |
| return line_B |
| enddef |
| |
| def PrevCodeLine(lnum: number): dict<any> # {{{3 |
| var line: string = getline(lnum) |
| if line =~ '^\s*[A-Z]\+$' |
| var endmarker: string = line->matchstr('[A-Z]\+') |
| var pos: list<number> = getcurpos() |
| cursor(lnum, 1) |
| var n: number = search(ASSIGNS_HEREDOC, 'bnW') |
| setpos('.', pos) |
| if n > 0 |
| line = getline(n) |
| if line =~ $'{HEREDOC_OPERATOR}\s\+{endmarker}' |
| return {lnum: n, text: line} |
| endif |
| endif |
| endif |
| |
| var n: number = prevnonblank(lnum - 1) |
| line = getline(n) |
| while line =~ COMMENT && n > 1 |
| n = prevnonblank(n - 1) |
| line = getline(n) |
| endwhile |
| # If we get back to the first line, we return 1 no matter what; even if it's a |
| # commented line. That should not cause an issue though. We just want to |
| # avoid a commented line above which there is a line of code which is more |
| # relevant. There is nothing above the first line. |
| return {lnum: n, text: line} |
| enddef |
| |
| def NextCodeLine(): number # {{{3 |
| var last: number = line('$') |
| if v:lnum == last |
| return 0 |
| endif |
| |
| var lnum: number = v:lnum + 1 |
| while lnum <= last |
| var line: string = getline(lnum) |
| if line != '' && line !~ COMMENT |
| return lnum |
| endif |
| ++lnum |
| endwhile |
| return 0 |
| enddef |
| |
| def SearchPair( # {{{3 |
| start: string, |
| middle: string, |
| end: string, |
| flags: string, |
| stopline = 0, |
| ): number |
| |
| var s: string = start |
| var e: string = end |
| if start == '[' || start == ']' |
| s = s->escape('[]') |
| endif |
| if end == '[' || end == ']' |
| e = e->escape('[]') |
| endif |
| # VIM_INDENT_TEST_TRACE_START |
| return searchpair('\C' .. s, (middle == '' ? '' : '\C' .. middle), '\C' .. e, |
| flags, (): bool => InCommentOrString(), stopline, TIMEOUT) |
| # VIM_INDENT_TEST_TRACE_END dist#vimindent#SearchPair |
| enddef |
| |
| def SearchPairStart( # {{{3 |
| start: string, |
| middle: string, |
| end: string, |
| ): number |
| return SearchPair(start, middle, end, 'bnW') |
| enddef |
| |
| def SearchPairEnd( # {{{3 |
| start: string, |
| middle: string, |
| end: string, |
| stopline = 0, |
| ): number |
| return SearchPair(start, middle, end, 'nW', stopline) |
| enddef |
| # }}}2 |
| # Test {{{2 |
| def AtStartOf(line_A: dict<any>, syntax: string): bool # {{{3 |
| if syntax == 'BracketBlock' |
| return AtStartOfBracketBlock(line_A) |
| endif |
| |
| var pat: string = { |
| HereDoc: ASSIGNS_HEREDOC, |
| FuncHeader: STARTS_FUNCTION |
| }[syntax] |
| return line_A.text =~ pat |
| && (!exists('b:vimindent') || !b:vimindent->has_key('is_HereDoc')) |
| enddef |
| |
| def AtStartOfBracketBlock(line_A: dict<any>): bool # {{{3 |
| # We ignore bracket blocks while we're indenting a function header |
| # because it makes the logic simpler. It might mean that we don't |
| # indent correctly a multiline bracket block inside a function header, |
| # but that's a corner case for which it doesn't seem worth making the |
| # code more complex. |
| if exists('b:vimindent') |
| && !b:vimindent->has_key('is_BracketBlock') |
| return false |
| endif |
| |
| var pos: list<number> = getcurpos() |
| cursor(line_A.lnum, [line_A.lnum, '$']->col()) |
| |
| if SearchPair(OPENING_BRACKET, '', CLOSING_BRACKET, 'bcW', line_A.lnum) <= 0 |
| setpos('.', pos) |
| return false |
| endif |
| # Don't restore the cursor position. |
| # It needs to be on a bracket for `CacheBracketBlock()` to work as intended. |
| |
| return line_A->EndsWithOpeningBracket() |
| || line_A->EndsWithCommaOrDictKey() |
| || line_A->EndsWithLambdaArrow() |
| enddef |
| |
| def ContinuesBelowBracketBlock( # {{{3 |
| line_A: string, |
| line_B: dict<any>, |
| block: dict<any> |
| ): bool |
| |
| return !block->empty() |
| && (line_A =~ LINE_CONTINUATION_AT_SOL |
| || line_B->EndsWithLineContinuation()) |
| enddef |
| |
| def IsInside(lnum: number, syntax: string): bool # {{{3 |
| if syntax == 'EnumBlock' |
| var cur_pos = getpos('.') |
| cursor(lnum, 1) |
| var enum_pos = search('^\C\s*\%(export\s\)\=\s*enum\s\+\S\+', 'bnW') |
| var endenum_pos = search('^\C\s*endenum\>', 'bnW') |
| setpos('.', cur_pos) |
| |
| if enum_pos == 0 && endenum_pos == 0 |
| return false |
| endif |
| if (enum_pos > 0 && (endenum_pos == 0 || enum_pos > endenum_pos)) |
| return true |
| endif |
| return false |
| endif |
| |
| if !exists('b:vimindent') |
| || !b:vimindent->has_key($'is_{syntax}') |
| return false |
| endif |
| |
| if syntax == 'BracketBlock' |
| if !b:vimindent->has_key('block_stack') |
| || b:vimindent.block_stack->empty() |
| return false |
| endif |
| return lnum <= b:vimindent.block_stack[0].endlnum |
| endif |
| |
| return lnum <= b:vimindent.endlnum |
| enddef |
| |
| def IsRightBelow(lnum: number, syntax: string): bool # {{{3 |
| return exists('b:vimindent') |
| && b:vimindent->has_key($'is_{syntax}') |
| && lnum > b:vimindent.endlnum |
| enddef |
| |
| def IsInCurlyBlock(): bool # {{{3 |
| return b:vimindent.block_stack |
| ->indexof((_, block: dict<any>): bool => block.is_curly_block) >= 0 |
| enddef |
| |
| def IsInThisBlock(line_A: dict<any>, lnum: number): bool # {{{3 |
| var pos: list<number> = getcurpos() |
| cursor(lnum, [lnum, '$']->col()) |
| var end: number = SearchPairEnd('{', '', '}') |
| setpos('.', pos) |
| |
| return line_A.lnum <= end |
| enddef |
| |
| def IsInInterface(): bool # {{{3 |
| return SearchPair('interface', '', 'endinterface', 'nW') > 0 |
| enddef |
| |
| def IsFirstLineOfCommand(line_1: dict<any>, line_2: dict<any>): bool # {{{3 |
| if line_1.text->Is_IN_KeywordForLoop(line_2.text) |
| return false |
| endif |
| |
| if line_1.text =~ RANGE_AT_SOL |
| || line_1.text =~ PLUS_MINUS_COMMAND |
| return true |
| endif |
| |
| if line_2.text =~ DICT_KEY |
| && !line_1->IsInThisBlock(line_2.lnum) |
| return true |
| endif |
| |
| var line_1_is_good: bool = line_1.text !~ COMMENT |
| && line_1.text !~ DICT_KEY |
| && line_1.text !~ LINE_CONTINUATION_AT_SOL |
| |
| var line_2_is_good: bool = !line_2->EndsWithLineContinuation() |
| |
| return line_1_is_good && line_2_is_good |
| enddef |
| |
| def Is_IN_KeywordForLoop(line_1: string, line_2: string): bool # {{{3 |
| return line_2 =~ '^\s*for\s' |
| && line_1 =~ '^\s*in\s' |
| enddef |
| |
| def InCommentOrString(): bool # {{{3 |
| return synstack('.', col('.')) |
| ->indexof((_, id: number): bool => synIDattr(id, 'name') =~ '\ccomment\|string\|heredoc') >= 0 |
| enddef |
| |
| def AlsoClosesBlock(line_B: dict<any>): bool # {{{3 |
| # We know that `line_B` opens a block. |
| # Let's see if it also closes that block. |
| var kwd: string = BlockStartKeyword(line_B.text) |
| if !START_MIDDLE_END->has_key(kwd) |
| return false |
| endif |
| |
| var [start: string, middle: string, end: string] = START_MIDDLE_END[kwd] |
| var pos: list<number> = getcurpos() |
| cursor(line_B.lnum, 1) |
| var block_end: number = SearchPairEnd(start, middle, end, line_B.lnum) |
| setpos('.', pos) |
| |
| return block_end > 0 |
| enddef |
| |
| def EndsWithComma(line: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line, COMMA_AT_EOL) |
| enddef |
| |
| def EndsWithCommaOrDictKey(line_A: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line_A, COMMA_OR_DICT_KEY_AT_EOL) |
| enddef |
| |
| def EndsWithCurlyBlock(line_B: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line_B, STARTS_CURLY_BLOCK) |
| enddef |
| |
| def EndsWithLambdaArrow(line_A: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line_A, LAMBDA_ARROW_AT_EOL) |
| enddef |
| |
| def EndsWithLineContinuation(line_B: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line_B, LINE_CONTINUATION_AT_EOL) |
| enddef |
| |
| def EndsWithOpeningBracket(line: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line, OPENING_BRACKET_AT_EOL) |
| enddef |
| |
| def EndsWithClosingBracket(line: dict<any>): bool # {{{3 |
| return NonCommentedMatch(line, CLOSING_BRACKET_AT_EOL) |
| enddef |
| |
| def NonCommentedMatch(line: dict<any>, pat: string): bool # {{{3 |
| # Could happen if there is no code above us, and we're not on the 1st line. |
| # In that case, `PrevCodeLine()` returns `{lnum: 0, line: ''}`. |
| if line.lnum == 0 |
| return false |
| endif |
| |
| # Technically, that's wrong. A line might start with a range and end with a |
| # line continuation symbol. But it's unlikely. And it's useful to assume the |
| # opposite because it prevents us from conflating a mark with an operator or |
| # the start of a list: |
| # |
| # not a comparison operator |
| # v |
| # :'< mark < |
| # :'< mark [ |
| # ^ |
| # not the start of a list |
| if line.text =~ RANGE_AT_SOL |
| return false |
| endif |
| |
| # that's not an arithmetic operator |
| # v |
| # catch /pattern / |
| # |
| # When `/` is used as a pattern delimiter, it's always present twice. |
| # And usually, the first occurrence is in the middle of a sequence of |
| # non-whitespace characters. If we can find such a `/`, we assume that the |
| # trailing `/` is not an operator. |
| # Warning: Here, don't use a too complex pattern.{{{ |
| # |
| # In particular, avoid backreferences. |
| # For example, this would be too costly: |
| # |
| # if line.text =~ $'\%(\S*\({PATTERN_DELIMITER}\)\S\+\|\S\+\({PATTERN_DELIMITER}\)\S*\)' |
| # .. $'\s\+\1{END_OF_COMMAND}' |
| # |
| # Sometimes, it could even give `E363`. |
| #}}} |
| var delim: string = line.text |
| ->matchstr($'\s\+\zs{PATTERN_DELIMITER}\ze{END_OF_COMMAND}') |
| if !delim->empty() |
| delim = $'\V{delim}\m' |
| if line.text =~ $'\%(\S*{delim}\S\+\|\S\+{delim}\S*\)\s\+{delim}{END_OF_COMMAND}' |
| return false |
| endif |
| endif |
| # TODO: We might still miss some corner cases:{{{ |
| # |
| # conflated with arithmetic division |
| # v |
| # substitute/pat / rep / |
| # echo |
| # ^--^ |
| # ✘ |
| # |
| # A better way to handle all these corner cases, would be to inspect the top |
| # of the syntax stack: |
| # |
| # :echo synID('.', col('.'), v:false)->synIDattr('name') |
| # |
| # Unfortunately, the legacy syntax plugin is not accurate enough. |
| # For example, it doesn't highlight a slash as an operator. |
| # }}} |
| |
| # `%` at the end of a line is tricky. |
| # It might be the modulo operator or the current file (e.g. `edit %`). |
| # Let's assume it's the latter. |
| if line.text =~ $'%{END_OF_COMMAND}' |
| return false |
| endif |
| |
| if line.text =~ TRICKY_COMMANDS |
| return false |
| endif |
| |
| var pos: list<number> = getcurpos() |
| cursor(line.lnum, 1) |
| # VIM_INDENT_TEST_TRACE_START |
| var match_lnum: number = search(pat, 'cnW', line.lnum, TIMEOUT, (): bool => InCommentOrString()) |
| # VIM_INDENT_TEST_TRACE_END dist#vimindent#NonCommentedMatch |
| setpos('.', pos) |
| return match_lnum > 0 |
| enddef |
| # }}}1 |
| # vim:sw=4 |