codekingpro/portable-devtools
114k
1vim9script2 3# Vim indent file.4# Language: Hare5# Maintainer: Amelia Clarke <selene@perilune.dev>6# Last Change: 2026 Jan 247# Upstream: https://git.sr.ht/~sircmpwn/hare.vim8 9if exists('b:did_indent')10 finish11endif12b:did_indent = 113 14# L0 -> Don't unindent lines that look like C labels.15# :0 -> Don't indent `case` in match and switch expressions. This only affects16# lines containing `:` (that isn't part of `::`).17# +0 -> Don't indent continuation lines.18# (s -> Indent one level inside parens.19# u0 -> Don't indent additional levels inside nested parens.20# U1 -> Don't treat `(` any differently if it started a line.21# m1 -> Indent lines starting with `)` the same as the matching `(`.22# j1 -> Indent blocks one level inside parens.23# J1 -> Indent structs and unions correctly.24# *0 -> Don't search for unclosed C-style block comments.25# #1 -> Don't unindent lines starting with `#`.26setlocal cinoptions=L0,:0,+0,(s,u0,U1,m1,j1,J1,*0,#127setlocal cinscopedecls=28setlocal indentexpr=GetHareIndent()29setlocal indentkeys=0{,0},0),0],!^F,o,O,e,0=case30setlocal nolisp31b:undo_indent = 'setl cino< cinsd< inde< indk< lisp<'32 33# Calculates the indentation for the current line, using the value computed by34# cindent and manually fixing the cases where it behaves incorrectly.35def GetHareIndent(): number36 # Get the preceding lines of context and the value computed by cindent.37 const line = getline(v:lnum)38 const [plnum, pline] = PrevNonBlank(v:lnum - 1)39 const [pplnum, ppline] = PrevNonBlank(plnum - 1)40 const pindent = indent(plnum)41 const ppindent = indent(pplnum)42 const cindent = cindent(v:lnum) / shiftwidth() * shiftwidth()43 44 # If this line is a comment, don't try to align it with a comment at the end45 # of the previous line.46 if line =~ '^\s*//' && getline(plnum) =~ '\s*//.*$'47 return -148 endif49 50 # Indent `case`.51 if line =~ '^\s*case\>'52 # If the previous line was also a `case`, use the same indent.53 if pline =~ '^\s*case\>'54 return pindent55 endif56 57 # If the previous line started the block, use the same indent.58 if pline =~ '{$'59 if pline =~ '\v<%(match|switch)>[^(]*\('60 return pindent61 endif62 return pindent - GetValue('hare_indent_match_switch', 1, 1, 2)63 * shiftwidth()64 endif65 66 # If the current line contains a `:` that is not part of `::`, use the67 # computed cindent.68 if line =~ '\v%(%(::)*)@>:'69 return cindent70 endif71 72 # Unindent after a multi-line `case`.73 if pline =~ '=>$'74 return pindent - shiftwidth() * GetValue('hare_indent_case', 2)75 endif76 77 # If the previous line closed a set of parens, search for the previous78 # `case` within the same block and use the same indent. This fixes issues79 # with `case` not being correctly unindented after a function call80 # continuation line:81 #82 # case let err: fs::error =>83 # fmt::fatalf("Unable to open {}: {}",84 # os::args[1], fs::strerror(err));85 # case // <-- cindent tries to unindent by only one shiftwidth86 if pline =~ ');$'87 const case = PrevMatchInBlock('^\s*case\>', plnum - 1)88 if case > 089 return indent(case)90 endif91 endif92 93 # If cindent would indent the same or more than the previous line, unindent.94 if cindent >= pindent95 return pindent - shiftwidth()96 endif97 98 # Otherwise, use the computed cindent.99 return cindent100 endif101 102 # Indent after `case`.103 if line !~ '^\s*}'104 # If the previous `case` started and ended on the same line, indent.105 if pline =~ '^\s*case\>.*;$'106 return pindent + shiftwidth()107 endif108 109 # Indent after a single-line `case`.110 if pline =~ '^\s*case\>.*=>$'111 return pindent + shiftwidth()112 endif113 114 # Indent inside a multi-line `case`.115 if pline =~ '^\s*case\>' && pline !~ '=>'116 return pindent + shiftwidth() * GetValue('hare_indent_case', 2)117 endif118 119 # Indent after a multi-line `case`.120 if pline =~ '=>$'121 return pindent - shiftwidth() * (GetValue('hare_indent_case', 2) - 1)122 endif123 124 # Don't unindent while inside a `case` body.125 if ppline =~ '=>$' && pline =~ ';$'126 return pindent127 endif128 129 # Don't unindent if the previous line ended a block. This fixes a very130 # peculiar edge case where cindent would try to unindent after a block, but131 # only if it is the first expression within a `case` body:132 #133 # case =>134 # if (foo) {135 # bar();136 # };137 # | <-- cindent tries to unindent by one shiftwidth138 if pline =~ '};$' && cindent < pindent139 return pindent140 endif141 142 # If the previous line closed a set of parens, and cindent would try to143 # unindent more than one level, search for the previous `case` within the144 # same block. If that line didn't contain a `:` (excluding `::`), indent one145 # level more. This fixes an issue where cindent would unindent too far when146 # there was no `:` after a `case`:147 #148 # case foo =>149 # bar(baz,150 # quux);151 # | <-- cindent tries to unindent by two shiftwidths152 if pline =~ ').*;$' && cindent < pindent - shiftwidth()153 const case = PrevMatchInBlock('^\s*case\>', plnum - 1)154 if case > 0 && GetTrimmedLine(case) !~ '\v%(%(::)*)@>:'155 return indent(case) + shiftwidth()156 endif157 endif158 endif159 160 # If the previous line ended with `=`, indent.161 if pline =~ '=$'162 return pindent + shiftwidth()163 endif164 165 # If the previous line opened an array literal, indent.166 if pline =~ '[$'167 return pindent + shiftwidth()168 endif169 170 # If the previous line started a binding expression, indent.171 if pline =~ '\v<%(const|def|let|type)$'172 return pindent + shiftwidth()173 endif174 175 # Indent continuation lines.176 if !TrailingParen(pline)177 # If this line closed an array and cindent would indent the same amount as178 # the previous line, unindent.179 if line =~ '^\s*]' && cindent == pindent180 return cindent - shiftwidth()181 endif182 183 # If the previous line closed an array literal, use the same indent. This184 # fixes an issue where cindent would try to indent an additional level after185 # an array literal containing indexing or slicing expressions, but only186 # inside a block:187 #188 # export fn main() void = {189 # const foo = [190 # bar[..4],191 # baz[..],192 # quux[1..],193 # ];194 # | <-- cindent tries to indent by one shiftwidth195 if pline =~ '^\s*];$' && cindent > pindent196 return pindent197 endif198 199 # Don't indent any further if the previous line closed an enum, struct, or200 # union.201 if pline =~ '^\s*},$' && cindent > pindent202 return pindent203 endif204 205 # If the previous line started a binding expression, and the first binding206 # was on the same line, indent.207 if pline =~ '\v<%(const|def|let|type)>.{-}\=.*,$'208 return pindent + shiftwidth()209 endif210 211 # Use the original indentation after a single continuation line.212 if pline =~ '[,;]$' && ppline =~ '=$'213 return ppindent214 endif215 216 # Don't unindent within a binding expression.217 if pline =~ ',$' && ppline =~ '\v<%(const|def|let|type)$'218 return pindent219 endif220 endif221 222 # If the previous line had an unclosed `if` or `for` condition, indent twice.223 if pline =~ '\v<%(if|for)>'224 const cond = match(pline, '\v%(if|for)>[^(]*\zs\(')225 if cond != -1 && TrailingParen(pline, cond)226 return pindent + shiftwidth() * 2227 endif228 endif229 230 # Optionally indent unclosed `match` and `switch` conditions an extra level.231 if pline =~ '\v<%(match|switch)>'232 const cond = match(pline, '\v<%(match|switch)>[^(]*\zs\(')233 if cond != -1 && TrailingParen(pline, cond)234 return pindent + shiftwidth()235 * GetValue('hare_indent_match_switch', 1, 1, 2)236 endif237 endif238 239 # Otherwise, use the computed cindent.240 return cindent241enddef242 243# Returns a line, with any comments or whitespace trimmed from the end.244def GetTrimmedLine(lnum: number): string245 var line = getline(lnum)246 247 # Use syntax highlighting attributes when possible.248 if has('syntax_items')249 # If the last character is inside a comment, do a binary search to find the250 # beginning of the comment.251 const len = strlen(line)252 if synIDattr(synID(lnum, len, true), 'name') =~ 'Comment\|Todo'253 var min = 1254 var max = len255 while min < max256 const col = (min + max) / 2257 if synIDattr(synID(lnum, col, true), 'name') =~ 'Comment\|Todo'258 max = col259 else260 min = col + 1261 endif262 endwhile263 line = strpart(line, 0, min - 1)264 endif265 return substitute(line, '\s*$', '', '')266 endif267 268 # Otherwise, use a regex as a fallback.269 return substitute(line, '\s*//.*$', '', '')270enddef271 272# Returns the value of a configuration variable, clamped within the given range.273def GetValue(274 name: string,275 default: number,276 min: number = 0,277 max: number = default,278): number279 const n = get(b:, name, get(g:, name, default))280 return min([max, max([n, min])])281enddef282 283# Returns the line number of the previous match for a pattern within the same284# block. Returns 0 if nothing was found.285def PrevMatchInBlock(286 pattern: string,287 lnum: number,288 maxlines: number = 20,289): number290 var block = 0291 for n in range(lnum, lnum - maxlines, -1)292 if n < 1293 break294 endif295 296 const line = GetTrimmedLine(n)297 if line =~ '{$'298 block -= 1299 if block < 0300 break301 endif302 endif303 304 if line =~ pattern && block == 0305 return n306 endif307 308 if line =~ '^\s*}'309 block += 1310 endif311 endfor312 return 0313enddef314 315# Returns the line number and contents of the previous non-blank line, with any316# comments trimmed.317def PrevNonBlank(lnum: number): tuple<number, string>318 var plnum = prevnonblank(lnum)319 var pline = GetTrimmedLine(plnum)320 while plnum > 1 && pline !~ '[^[:blank:]]'321 plnum = prevnonblank(plnum - 1)322 pline = GetTrimmedLine(plnum)323 endwhile324 return (plnum, pline)325enddef326 327# Returns whether a line contains at least one unclosed `(`.328# XXX: Can still be fooled by parens inside rune and string literals.329def TrailingParen(line: string, start: number = 0): bool330 var total = 0331 for n in strpart(line, start)->filter((_, n) => n =~ '[()]')->reverse()332 if n == ')'333 total += 1334 else335 total -= 1336 if total < 0337 return true338 endif339 endif340 endfor341 return false342enddef343 344# vim: et sts=2 sw=2 ts=8 tw=80345 