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