mirror of
https://github.com/vim/vim.git
synced 2025-09-26 04:04:07 -04:00
closes: #18222 Signed-off-by: Amelia Clarke <selene@perilune.dev> Signed-off-by: Christian Brabandt <cb@256bit.org>
341 lines
10 KiB
VimL
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
|