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