File: hare.vim

package info (click to toggle)
vim 2%3A9.1.2103-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 93,456 kB
  • sloc: ansic: 433,730; cpp: 6,399; makefile: 4,597; sh: 2,397; java: 2,312; xml: 2,099; python: 1,595; perl: 1,419; awk: 730; lisp: 501; cs: 458; objc: 369; sed: 8; csh: 6; haskell: 1
file content (340 lines) | stat: -rw-r--r-- 10,435 bytes parent folder | download
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
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
vim9script

# Vim indent file.
# Language:    Hare
# Maintainer:  Amelia Clarke <selene@perilune.dev>
# Last Change: 2025 Sep 06
# Upstream:    https://git.sr.ht/~sircmpwn/hare.vim

if exists('b:did_indent')
  finish
endif
b:did_indent = 1

# L0 -> Don't unindent lines that look like C labels.
# :0 -> Don't indent `case` in match and switch expressions. This only affects
#       lines containing `:` (that isn't part of `::`).
# +0 -> Don't indent continuation lines.
# (s -> Indent one level inside parens.
# u0 -> Don't indent additional levels inside nested parens.
# U1 -> Don't treat `(` any differently if it is at the start of a line.
# m1 -> Indent lines starting with `)` the same as the matching `(`.
# j1 -> Indent blocks one level inside parens.
# J1 -> Indent structs and unions correctly.
# *0 -> Don't search for unclosed C-style block comments.
# #1 -> Don't unindent lines starting with `#`.
setlocal cinoptions=L0,:0,+0,(s,u0,U1,m1,j1,J1,*0,#1
setlocal cinscopedecls=
setlocal indentexpr=GetHareIndent()
setlocal indentkeys=0{,0},0),0],!^F,o,O,e,0=case
setlocal nolisp
b:undo_indent = 'setl cino< cinsd< inde< indk< lisp<'

# Calculates the indentation for the current line, using the value computed by
# cindent and manually fixing the cases where it behaves incorrectly.
def GetHareIndent(): number
  # Get the preceding lines of context and the value computed by cindent.
  const line = getline(v:lnum)
  const [plnum, pline] = PrevNonBlank(v:lnum - 1)
  const [pplnum, ppline] = PrevNonBlank(plnum - 1)
  const pindent = indent(plnum)
  const ppindent = indent(pplnum)
  const cindent = cindent(v:lnum) / shiftwidth() * shiftwidth()

  # If this line is a comment, don't try to align it with a comment at the end
  # of the previous line.
  if line =~ '^\s*//' && getline(plnum) =~ '\s*//.*$'
    return -1
  endif

  # Indent `case`.
  if line =~ '^\s*case\>'
    # If the previous line was also a `case`, use the same indent.
    if pline =~ '^\s*case\>'
      return pindent
    endif

    # If the previous line started the block, use the same indent.
    if pline =~ '{$'
      return pindent
    endif

    # If the current line contains a `:` that is not part of `::`, use the
    # computed cindent.
    if line =~ '\v%(%(::)*)@>:'
      return cindent
    endif

    # Unindent after a multi-line `case`.
    if pline =~ '=>$'
      return pindent - shiftwidth() * GetValue('hare_indent_case', 2)
    endif

    # If the previous line closed a set of parens, search for the previous
    # `case` within the same block and use the same indent. This fixes issues
    # with `case` not being correctly unindented after a function call
    # continuation line:
    #
    #   case let err: fs::error =>
    #           fmt::fatalf("Unable to open {}: {}",
    #                   os::args[1], fs::strerror(err));
    #           case // <-- cindent tries to unindent by only one shiftwidth
    if pline =~ ');$'
      const case = PrevMatchInBlock('^\s*case\>', plnum - 1)
      if case > 0
        return indent(case)
      endif
    endif

    # If cindent would indent the same or more than the previous line, unindent.
    if cindent >= pindent
      return pindent - shiftwidth()
    endif

    # Otherwise, use the computed cindent.
    return cindent
  endif

  # Indent after `case`.
  if line !~ '^\s*}'
    # If the previous `case` started and ended on the same line, indent.
    if pline =~ '^\s*case\>.*;$'
      return pindent + shiftwidth()
    endif

    # Indent after a single-line `case`.
    if pline =~ '^\s*case\>.*=>$'
      return pindent + shiftwidth()
    endif

    # Indent inside a multi-line `case`.
    if pline =~ '^\s*case\>' && pline !~ '=>'
      return pindent + shiftwidth() * GetValue('hare_indent_case', 2)
    endif

    # Indent after a multi-line `case`.
    if pline =~ '=>$'
      return pindent - shiftwidth() * (GetValue('hare_indent_case', 2) - 1)
    endif

    # Don't unindent while inside a `case` body.
    if ppline =~ '=>$' && pline =~ ';$'
      return pindent
    endif

    # Don't unindent if the previous line ended a block. This fixes a very
    # peculiar edge case where cindent would try to unindent after a block, but
    # only if it is the first expression within a `case` body:
    #
    #   case =>
    #           if (foo) {
    #                   bar();
    #           };
    #   | <-- cindent tries to unindent by one shiftwidth
    if pline =~ '};$' && cindent < pindent
      return pindent
    endif

    # If the previous line closed a set of parens, and cindent would try to
    # unindent more than one level, search for the previous `case` within the
    # same block. If that line didn't contain a `:` (excluding `::`), indent one
    # level more. This fixes an issue where cindent would unindent too far when
    # there was no `:` after a `case`:
    #
    #   case foo =>
    #           bar(baz,
    #                   quux);
    #   | <-- cindent tries to unindent by two shiftwidths
    if pline =~ ').*;$' && cindent < pindent - shiftwidth()
      const case = PrevMatchInBlock('^\s*case\>', plnum - 1)
      if case > 0 && GetTrimmedLine(case) !~ '\v%(%(::)*)@>:'
        return indent(case) + shiftwidth()
      endif
    endif
  endif

  # If the previous line ended with `=`, indent.
  if pline =~ '=$'
    return pindent + shiftwidth()
  endif

  # If the previous line opened an array literal, indent.
  if pline =~ '[$'
    return pindent + shiftwidth()
  endif

  # If the previous line started a binding expression, indent.
  if pline =~ '\v<%(const|def|let|type)$'
    return pindent + shiftwidth()
  endif

  # Indent continuation lines.
  if !TrailingParen(pline)
    # If this line closed an array and cindent would indent the same amount as
    # the previous line, unindent.
    if line =~ '^\s*]' && cindent == pindent
      return cindent - shiftwidth()
    endif

    # If the previous line closed an array literal, use the same indent. This
    # fixes an issue where cindent would try to indent an additional level after
    # an array literal containing indexing or slicing expressions, but only
    # inside a block:
    #
    #   export fn main() void = {
    #           const foo = [
    #                   bar[..4],
    #                   baz[..],
    #                   quux[1..],
    #           ];
    #                   | <-- cindent tries to indent by one shiftwidth
    if pline =~ '^\s*];$' && cindent > pindent
      return pindent
    endif

    # Don't indent any further if the previous line closed an enum, struct, or
    # union.
    if pline =~ '^\s*},$' && cindent > pindent
      return pindent
    endif

    # If the previous line started a binding expression, and the first binding
    # was on the same line, indent.
    if pline =~ '\v<%(const|def|let|type)>.{-}\=.*,$'
      return pindent + shiftwidth()
    endif

    # Use the original indentation after a single continuation line.
    if pline =~ '[,;]$' && ppline =~ '=$'
      return ppindent
    endif

    # Don't unindent within a binding expression.
    if pline =~ ',$' && ppline =~ '\v<%(const|def|let|type)$'
      return pindent
    endif
  endif

  # If the previous line had an unclosed `if` or `for` condition, indent twice.
  if pline =~ '\v<%(if|for)>'
    const cond = match(pline, '\v%(if|for)>[^(]*\zs\(')
    if cond != -1 && TrailingParen(pline, cond)
      return pindent + shiftwidth() * 2
    endif
  endif

  # Optionally indent unclosed `match` and `switch` conditions an extra level.
  if pline =~ '\v<%(match|switch)>'
    const cond = match(pline, '\v<%(match|switch)>[^(]*\zs\(')
    if cond != -1 && TrailingParen(pline, cond)
      return pindent + shiftwidth()
        * GetValue('hare_indent_match_switch', 1, 1, 2)
    endif
  endif

  # Otherwise, use the computed cindent.
  return cindent
enddef

# Returns a line, with any comments or whitespace trimmed from the end.
def GetTrimmedLine(lnum: number): string
  var line = getline(lnum)

  # Use syntax highlighting attributes when possible.
  if has('syntax_items')
    # If the last character is inside a comment, do a binary search to find the
    # beginning of the comment.
    const len = strlen(line)
    if synIDattr(synID(lnum, len, true), 'name') =~ 'Comment\|Todo'
      var min = 1
      var max = len
      while min < max
        const col = (min + max) / 2
        if synIDattr(synID(lnum, col, true), 'name') =~ 'Comment\|Todo'
          max = col
        else
          min = col + 1
        endif
      endwhile
      line = strpart(line, 0, min - 1)
    endif
    return substitute(line, '\s*$', '', '')
  endif

  # Otherwise, use a regex as a fallback.
  return substitute(line, '\s*//.*$', '', '')
enddef

# Returns the value of a configuration variable, clamped within the given range.
def GetValue(
  name: string,
  default: number,
  min: number = 0,
  max: number = default,
): number
  const n = get(b:, name, get(g:, name, default))
  return min([max, max([n, min])])
enddef

# Returns the line number of the previous match for a pattern within the same
# block. Returns 0 if nothing was found.
def PrevMatchInBlock(
  pattern: string,
  lnum: number,
  maxlines: number = 20,
): number
  var block = 0
  for n in range(lnum, lnum - maxlines, -1)
    if n < 1
      break
    endif

    const line = GetTrimmedLine(n)
    if line =~ '{$'
      block -= 1
      if block < 0
        break
      endif
    endif

    if line =~ pattern && block == 0
      return n
    endif

    if line =~ '^\s*}'
      block += 1
    endif
  endfor
  return 0
enddef

# Returns the line number and contents of the previous non-blank line, with any
# comments trimmed.
def PrevNonBlank(lnum: number): tuple<number, string>
  var plnum = prevnonblank(lnum)
  var pline = GetTrimmedLine(plnum)
  while plnum > 1 && pline !~ '[^[:blank:]]'
    plnum = prevnonblank(plnum - 1)
    pline = GetTrimmedLine(plnum)
  endwhile
  return (plnum, pline)
enddef

# Returns whether a line contains at least one unclosed `(`.
# XXX: Can still be fooled by parens inside rune and string literals.
def TrailingParen(line: string, start: number = 0): bool
  var total = 0
  for n in strpart(line, start)->filter((_, n) => n =~ '[()]')->reverse()
    if n == ')'
      total += 1
    else
      total -= 1
      if total < 0
        return true
      endif
    endif
  endfor
  return false
enddef

# vim: et sts=2 sw=2 ts=8 tw=80