summaryrefslogtreecommitdiffstats
path: root/runtime/autoload/racket.vim
blob: 50c24b0b06ab0739a18e707771ff89ffb295a1bb (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
" Maintainer: D. Ben Knoble <ben.knoble+github@gmail.com>
" URL: https://github.com/benknoble/vim-racket
" Last Change: 2023 Sep 22
vim9script

def MakePatternFromLiterals(xs: list<string>): string
  return printf('\V%s', xs->mapnew((_, v) => escape(v, '\'))->join('\|'))
enddef

const openers = ['(', '[', '{']
const closers = {'(': ')', '[': ']', '{': '}'}
const brackets_pattern: string = closers->items()->flattennew()->MakePatternFromLiterals()

# transliterated from a modified copy of src/indent.c

export def Indent(): number
  if InHerestring(v:lnum)
    return -1
  endif
  # Indent from first column to avoid odd results from nested forms.
  cursor(v:lnum, 1)
  const bracket = FindBracket()
  if bracket == null_dict || !bracket.found
    return -1
  endif

  # assert_report(printf('{lnum: %d, str: %s, found: %s, line: %d, column: %d}',
  #   v:lnum, getline(bracket.line)[bracket.column - 1], bracket.found, bracket.line, bracket.column))
  # N.B. Column =/= Line Index; Columns start at 1
  const amount: number = bracket.column
  const line = getline(bracket.line)

  const lw = Lispword(line[bracket.column :])
  if !IsForFold(lw) # skip: see comments about for/fold special case below
    # "Extra trick"
    var current = prevnonblank(v:lnum - 1)
    while current > bracket.line
      cursor(current, 1)
      if getline(current) !~# '^\s*;' && synID(current, 1, 0)->synIDattr('name') !~? 'string' && FindBracket() == bracket
        return indent(current)
      endif
      current = prevnonblank(current - 1)
    endwhile
    cursor(v:lnum, 1)
  endif

  if index(openers, line[bracket.column - 1]) >= 0 && !empty(lw)
    # Special case for/fold &co. The iterator clause (2nd form) is indented
    # under the accumulator clause (1st form). Everything else is standard.
    const start_of_first_form = match(line[bracket.column :], MakePatternFromLiterals(openers))
    # assert_report(printf('{line: %s}', line))
    # assert_report(printf('{start: %s}', start_of_first_form >= 0 ? line[bracket.column + start_of_first_form :] : '<NULL>'))
    if IsForFold(lw) && IsSecondForm(bracket.line, bracket.column, v:lnum) && start_of_first_form >= 0
      return amount + start_of_first_form
    else
      # Lispword, but not for/fold second form (or first form couldn't be
      # found): indent like define or lambda.
      # 2 extra indent, but subtract 1 for columns starting at 1.
      # Current vim9 doesn't constant fold "x + 2 - 1", so write "x + 1"
      return amount + 1
    endif
  else
    # assert_report(printf('{line: %s}', line[bracket.column :]))
    return amount + IndentForContinuation(bracket.line, bracket.column, line[bracket.column :])
  endif
enddef

def InHerestring(start: number): bool
  return synID(start, col([start, '$']) - 1, 0)->synIDattr('name') =~? 'herestring'
enddef

def FindBracket(): dict<any>
  const paren = FindMatch('(', ')')
  const square = FindMatch('\[', ']')
  const curly = FindMatch('{', '}')
  return null_dict
    ->MatchMax(paren)
    ->MatchMax(square)
    ->MatchMax(curly)
enddef

def Lispword(line: string): string
  # assume keyword on same line as opener
  const word: string = matchstr(line, '^\s*\k\+\>')->trim()
  # assert_report(printf('line: %s; word: %s', line, word))
  # assert_report(&l:lispwords->split(',')->index(word) >= 0 ? 't' : 'f')
  return &l:lispwords->split(',')->index(word) >= 0 ? word : ''
enddef

# line contains everything on line_nr after column
def IndentForContinuation(line_nr: number, column: number, line: string): number
  const end = len(line)
  var indent = match(line, '[^[:space:]]')
  # first word is a string or some other literal (or maybe a form); assume that
  # the current line is outside such a thing
  if indent < end && ['"', '#']->index(line[indent]) >= 0
    return indent
  endif
  if indent < end && ["'", '`']->index(line[indent]) >= 0
    # could be a form or a word. Advance one and see.
    ++indent
  endif
  if indent < end && ['(', '[', '{']->index(line[indent]) >= 0
    # there's a form; assume outside, but need to skip it to see if any others
    cursor(line_nr, column + indent + 1)
    # assert_report(getline(line_nr)[column + indent :])
    normal! %
    const [_, matched_line, matched_col, _, _] = getcursorcharpos()
    if line_nr != matched_line || matched_col == column + indent + 1
      return indent
    endif
    indent = matched_col - column
  endif
  var in_delim: bool
  var quoted: bool
  while indent < end && (line[indent] !~# '\s' || in_delim || quoted)
    if line[indent] == '\' && !in_delim
      quoted = true
    else
      quoted = false
    endif
    if line[indent] == '|' && !quoted
      in_delim = !in_delim
    endif
    ++indent
  endwhile
  # not handling newlines in first words
  if quoted || in_delim
    return 0
  endif
  # no other word on this line
  if indent == end
    return 0
  endif
  # find beginning of next word
  indent += match(line[indent :], '[^[:space:]]')
  return indent
enddef

def FindMatch(start: string, end: string): dict<any>
  # TODO too slow…
  # could try replicating C? might have false positives. Or make "100"
  # configurable number: for amounts of indent bodies, we're still fast enough…
  const [linenr, column] = searchpairpos(start, '', end, 'bnzW',
    () =>
      synID(line('.'), col('.'), 0)->synIDattr('name') =~? 'char\|string\|comment',
    line('.') > 100 ? line('.') - 100 : 0)
  if linenr > 0 && column > 0
    return {found: true, line: linenr, column: column}
  else
    return {found: false, line: linenr, column: column}
  endif
enddef

def MatchMax(left: dict<any>, right: dict<any>): dict<any>
  if left == null_dict || !left.found
    return right
  endif
  if right == null_dict || !right.found
    return left
  endif
  # left and right non-null, both found
  return PosLT(left, right) ? right : left
enddef

def PosLT(left: dict<any>, right: dict<any>): bool
  return left.line != right.line
     \ ? left.line < right.line
     \ : (left.column != right.column && left.column < right.column)
enddef

def IsForFold(word: string): bool
  return ['for/fold', 'for/foldr', 'for*/fold', 'for*/foldr']->index(word) >= 0
enddef

def IsSecondForm(blnum: number, bcol: number, vlnum: number): bool
  var forms_seen: number # "top-level" (inside for/fold) counter only
  var [lnum, col] = [blnum, bcol + 1]
  cursor(lnum, col)
  var stack: list<string> = []

  while lnum <= vlnum
    const found = search(brackets_pattern, '', vlnum, 0, () =>
      synID(line('.'), col('.'), 0)->synIDattr('name') =~? 'char\|string\|comment')
    if found <= 0
      break
    endif
    const pos = getcursorcharpos()
    lnum = pos[1]
    col = pos[2]
    var current_char = getline(lnum)[col - 1]
    # assert_report(printf('search: %d, %d: %s', lnum, col, current_char))
    # assert_report(printf('forms seen post-search: %d', forms_seen))
    if index(openers, current_char) >= 0
      insert(stack, current_char)
    elseif !empty(stack) && current_char ==# closers[stack[0]]
      stack = stack[1 :]
      if empty(stack)
        ++forms_seen
      endif
    else
      # parse failure of some kind: not an opener or not the correct closer
      return false
    endif
    # assert_report(printf('forms seen pre-check: %d', forms_seen))
    if forms_seen > 2
      return false
    endif
  endwhile

  # assert_report(printf('forms seen pre-return: %d', forms_seen))
  return forms_seen == 2 || (forms_seen == 1 && !empty(stack))
enddef