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