D. Ben Knoble | 5eb9cb5 | 2023-12-16 08:24:15 -0500 | [diff] [blame] | 1 | " Maintainer: D. Ben Knoble <ben.knoble+github@gmail.com> |
| 2 | " URL: https://github.com/benknoble/vim-racket |
D. Ben Knoble | 8e013b1 | 2024-11-13 19:45:38 +0100 | [diff] [blame] | 3 | " Last Change: 2024 Nov 12 |
D. Ben Knoble | 5eb9cb5 | 2023-12-16 08:24:15 -0500 | [diff] [blame] | 4 | vim9script |
| 5 | |
| 6 | def MakePatternFromLiterals(xs: list<string>): string |
| 7 | return printf('\V%s', xs->mapnew((_, v) => escape(v, '\'))->join('\|')) |
| 8 | enddef |
| 9 | |
| 10 | const openers = ['(', '[', '{'] |
| 11 | const closers = {'(': ')', '[': ']', '{': '}'} |
| 12 | const brackets_pattern: string = closers->items()->flattennew()->MakePatternFromLiterals() |
| 13 | |
| 14 | # transliterated from a modified copy of src/indent.c |
| 15 | |
| 16 | export def Indent(): number |
| 17 | if InHerestring(v:lnum) |
| 18 | return -1 |
| 19 | endif |
| 20 | # Indent from first column to avoid odd results from nested forms. |
| 21 | cursor(v:lnum, 1) |
| 22 | const bracket = FindBracket() |
| 23 | if bracket == null_dict || !bracket.found |
| 24 | return -1 |
| 25 | endif |
| 26 | |
| 27 | # assert_report(printf('{lnum: %d, str: %s, found: %s, line: %d, column: %d}', |
| 28 | # v:lnum, getline(bracket.line)[bracket.column - 1], bracket.found, bracket.line, bracket.column)) |
| 29 | # N.B. Column =/= Line Index; Columns start at 1 |
| 30 | const amount: number = bracket.column |
| 31 | const line = getline(bracket.line) |
| 32 | |
| 33 | const lw = Lispword(line[bracket.column :]) |
| 34 | if !IsForFold(lw) # skip: see comments about for/fold special case below |
| 35 | # "Extra trick" |
| 36 | var current = prevnonblank(v:lnum - 1) |
| 37 | while current > bracket.line |
| 38 | cursor(current, 1) |
| 39 | if getline(current) !~# '^\s*;' && synID(current, 1, 0)->synIDattr('name') !~? 'string' && FindBracket() == bracket |
| 40 | return indent(current) |
| 41 | endif |
| 42 | current = prevnonblank(current - 1) |
| 43 | endwhile |
| 44 | cursor(v:lnum, 1) |
| 45 | endif |
| 46 | |
| 47 | if index(openers, line[bracket.column - 1]) >= 0 && !empty(lw) |
| 48 | # Special case for/fold &co. The iterator clause (2nd form) is indented |
| 49 | # under the accumulator clause (1st form). Everything else is standard. |
| 50 | const start_of_first_form = match(line[bracket.column :], MakePatternFromLiterals(openers)) |
| 51 | # assert_report(printf('{line: %s}', line)) |
| 52 | # assert_report(printf('{start: %s}', start_of_first_form >= 0 ? line[bracket.column + start_of_first_form :] : '<NULL>')) |
| 53 | if IsForFold(lw) && IsSecondForm(bracket.line, bracket.column, v:lnum) && start_of_first_form >= 0 |
| 54 | return amount + start_of_first_form |
| 55 | else |
| 56 | # Lispword, but not for/fold second form (or first form couldn't be |
| 57 | # found): indent like define or lambda. |
| 58 | # 2 extra indent, but subtract 1 for columns starting at 1. |
| 59 | # Current vim9 doesn't constant fold "x + 2 - 1", so write "x + 1" |
| 60 | return amount + 1 |
| 61 | endif |
| 62 | else |
| 63 | # assert_report(printf('{line: %s}', line[bracket.column :])) |
| 64 | return amount + IndentForContinuation(bracket.line, bracket.column, line[bracket.column :]) |
| 65 | endif |
| 66 | enddef |
| 67 | |
| 68 | def InHerestring(start: number): bool |
| 69 | return synID(start, col([start, '$']) - 1, 0)->synIDattr('name') =~? 'herestring' |
| 70 | enddef |
| 71 | |
| 72 | def FindBracket(): dict<any> |
| 73 | const paren = FindMatch('(', ')') |
| 74 | const square = FindMatch('\[', ']') |
| 75 | const curly = FindMatch('{', '}') |
| 76 | return null_dict |
| 77 | ->MatchMax(paren) |
| 78 | ->MatchMax(square) |
| 79 | ->MatchMax(curly) |
| 80 | enddef |
| 81 | |
| 82 | def Lispword(line: string): string |
| 83 | # assume keyword on same line as opener |
| 84 | const word: string = matchstr(line, '^\s*\k\+\>')->trim() |
| 85 | # assert_report(printf('line: %s; word: %s', line, word)) |
| 86 | # assert_report(&l:lispwords->split(',')->index(word) >= 0 ? 't' : 'f') |
| 87 | return &l:lispwords->split(',')->index(word) >= 0 ? word : '' |
| 88 | enddef |
| 89 | |
| 90 | # line contains everything on line_nr after column |
| 91 | def IndentForContinuation(line_nr: number, column: number, line: string): number |
| 92 | const end = len(line) |
| 93 | var indent = match(line, '[^[:space:]]') |
| 94 | # first word is a string or some other literal (or maybe a form); assume that |
| 95 | # the current line is outside such a thing |
| 96 | if indent < end && ['"', '#']->index(line[indent]) >= 0 |
| 97 | return indent |
| 98 | endif |
| 99 | if indent < end && ["'", '`']->index(line[indent]) >= 0 |
| 100 | # could be a form or a word. Advance one and see. |
| 101 | ++indent |
| 102 | endif |
| 103 | if indent < end && ['(', '[', '{']->index(line[indent]) >= 0 |
| 104 | # there's a form; assume outside, but need to skip it to see if any others |
| 105 | cursor(line_nr, column + indent + 1) |
| 106 | # assert_report(getline(line_nr)[column + indent :]) |
| 107 | normal! % |
| 108 | const [_, matched_line, matched_col, _, _] = getcursorcharpos() |
| 109 | if line_nr != matched_line || matched_col == column + indent + 1 |
| 110 | return indent |
| 111 | endif |
| 112 | indent = matched_col - column |
| 113 | endif |
| 114 | var in_delim: bool |
| 115 | var quoted: bool |
| 116 | while indent < end && (line[indent] !~# '\s' || in_delim || quoted) |
| 117 | if line[indent] == '\' && !in_delim |
| 118 | quoted = true |
| 119 | else |
| 120 | quoted = false |
| 121 | endif |
| 122 | if line[indent] == '|' && !quoted |
| 123 | in_delim = !in_delim |
| 124 | endif |
| 125 | ++indent |
| 126 | endwhile |
| 127 | # not handling newlines in first words |
| 128 | if quoted || in_delim |
| 129 | return 0 |
| 130 | endif |
| 131 | # no other word on this line |
| 132 | if indent == end |
| 133 | return 0 |
| 134 | endif |
| 135 | # find beginning of next word |
| 136 | indent += match(line[indent :], '[^[:space:]]') |
| 137 | return indent |
| 138 | enddef |
| 139 | |
| 140 | def FindMatch(start: string, end: string): dict<any> |
| 141 | # TODO too slow… |
| 142 | # could try replicating C? might have false positives. Or make "100" |
| 143 | # configurable number: for amounts of indent bodies, we're still fast enough… |
| 144 | const [linenr, column] = searchpairpos(start, '', end, 'bnzW', |
| 145 | () => |
| 146 | synID(line('.'), col('.'), 0)->synIDattr('name') =~? 'char\|string\|comment', |
| 147 | line('.') > 100 ? line('.') - 100 : 0) |
| 148 | if linenr > 0 && column > 0 |
| 149 | return {found: true, line: linenr, column: column} |
| 150 | else |
| 151 | return {found: false, line: linenr, column: column} |
| 152 | endif |
| 153 | enddef |
| 154 | |
| 155 | def MatchMax(left: dict<any>, right: dict<any>): dict<any> |
| 156 | if left == null_dict || !left.found |
| 157 | return right |
| 158 | endif |
| 159 | if right == null_dict || !right.found |
| 160 | return left |
| 161 | endif |
| 162 | # left and right non-null, both found |
| 163 | return PosLT(left, right) ? right : left |
| 164 | enddef |
| 165 | |
| 166 | def PosLT(left: dict<any>, right: dict<any>): bool |
| 167 | return left.line != right.line |
| 168 | \ ? left.line < right.line |
| 169 | \ : (left.column != right.column && left.column < right.column) |
| 170 | enddef |
| 171 | |
| 172 | def IsForFold(word: string): bool |
D. Ben Knoble | 8e013b1 | 2024-11-13 19:45:38 +0100 | [diff] [blame] | 173 | return ['for/fold', 'for/foldr', 'for/lists', 'for*/fold', 'for*/foldr', 'for*/lists']->index(word) >= 0 |
D. Ben Knoble | 5eb9cb5 | 2023-12-16 08:24:15 -0500 | [diff] [blame] | 174 | enddef |
| 175 | |
| 176 | def IsSecondForm(blnum: number, bcol: number, vlnum: number): bool |
| 177 | var forms_seen: number # "top-level" (inside for/fold) counter only |
| 178 | var [lnum, col] = [blnum, bcol + 1] |
| 179 | cursor(lnum, col) |
| 180 | var stack: list<string> = [] |
| 181 | |
| 182 | while lnum <= vlnum |
| 183 | const found = search(brackets_pattern, '', vlnum, 0, () => |
| 184 | synID(line('.'), col('.'), 0)->synIDattr('name') =~? 'char\|string\|comment') |
| 185 | if found <= 0 |
| 186 | break |
| 187 | endif |
| 188 | const pos = getcursorcharpos() |
| 189 | lnum = pos[1] |
| 190 | col = pos[2] |
| 191 | var current_char = getline(lnum)[col - 1] |
| 192 | # assert_report(printf('search: %d, %d: %s', lnum, col, current_char)) |
| 193 | # assert_report(printf('forms seen post-search: %d', forms_seen)) |
| 194 | if index(openers, current_char) >= 0 |
| 195 | insert(stack, current_char) |
| 196 | elseif !empty(stack) && current_char ==# closers[stack[0]] |
| 197 | stack = stack[1 :] |
| 198 | if empty(stack) |
| 199 | ++forms_seen |
| 200 | endif |
| 201 | else |
| 202 | # parse failure of some kind: not an opener or not the correct closer |
| 203 | return false |
| 204 | endif |
| 205 | # assert_report(printf('forms seen pre-check: %d', forms_seen)) |
| 206 | if forms_seen > 2 |
| 207 | return false |
| 208 | endif |
| 209 | endwhile |
| 210 | |
| 211 | # assert_report(printf('forms seen pre-return: %d', forms_seen)) |
D. Ben Knoble | 8e013b1 | 2024-11-13 19:45:38 +0100 | [diff] [blame] | 212 | return (forms_seen == 2 && empty(stack)) || (forms_seen == 1 && !empty(stack)) |
D. Ben Knoble | 5eb9cb5 | 2023-12-16 08:24:15 -0500 | [diff] [blame] | 213 | enddef |