0
0
mirror of https://github.com/vim/vim.git synced 2025-09-26 04:04:07 -04:00
Files
vim/runtime/indent/hare.vim
Amelia Clarke 6d68508e62 runtime(hare): update for Hare 0.25.2
closes: #18222

Signed-off-by: Amelia Clarke <selene@perilune.dev>
Signed-off-by: Christian Brabandt <cb@256bit.org>
2025-09-08 15:30:41 -04:00

341 lines
10 KiB
VimL

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