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
|