runtime(hare): update for Hare 0.25.2

Commit: 
https://github.com/vim/vim/commit/6d68508e6239930b2ed889a55acd647506ed8dfb
Author: Amelia Clarke <[email protected]>
Date:   Mon Sep 8 15:30:41 2025 -0400

    runtime(hare): update for Hare 0.25.2
    
    closes: https://github.com/vim/vim/issues/18222
    
    Signed-off-by: Amelia Clarke <[email protected]>
    Signed-off-by: Christian Brabandt <[email protected]>

diff --git a/runtime/autoload/dist/ft.vim b/runtime/autoload/dist/ft.vim
index 4d39689cc..b0609727e 100644
--- a/runtime/autoload/dist/ft.vim
+++ b/runtime/autoload/dist/ft.vim
@@ -3,7 +3,7 @@ vim9script
 # Vim functions for file type detection
 #
 # Maintainer:          The Vim Project <https://github.com/vim/vim>
-# Last Change:         2025 Sep 04
+# Last Change:         2025 Sep 08
 # Former Maintainer:   Bram Moolenaar <[email protected]>
 
 # These functions are moved here from runtime/filetype.vim to make startup
@@ -441,29 +441,29 @@ export def FTfs()
   endif
 enddef
 
-# Recursively search for Hare source files in a directory and any
-# subdirectories, up to a given depth.
+# Recursively searches for Hare source files within a directory, up to a given
+# depth.
 def IsHareModule(dir: string, depth: number): bool
-  if depth <= 0
-    return !empty(glob(dir .. '/*.ha'))
+  if depth < 1
+    return false
+  elseif depth == 1
+    return !glob(dir .. '/*.ha')->empty()
   endif
 
-  return reduce(sort(glob(dir .. '/*', true, true),
-    (a, b) => isdirectory(a) - isdirectory(b)),
-    (acc, n) => acc
+  # Check all files in the directory before recursing into subdirectories.
+  return glob(dir .. '/*', true, true)
+    ->sort((a, b) => isdirectory(a) - isdirectory(b))
+    ->reduce((acc, n) => acc
       || n =~ '\.ha$'
-      || isdirectory(n)
-      && IsHareModule(n, depth - 1),
+      || isdirectory(n) && IsHareModule(n, depth - 1),
     false)
 enddef
 
-# Determine if a README file exists within a Hare module and should be given 
the
-# Haredoc filetype.
+# Determines whether a README file is inside a Hare module and should receive
+# the 'haredoc' filetype.
 export def FTharedoc()
-  if exists('g:filetype_haredoc')
-    if IsHareModule('<afile>:h', get(g:, 'haredoc_search_depth', 1))
-      setf haredoc
-    endif
+  if IsHareModule('<afile>:h', get(g:, 'filetype_haredoc', 1))
+    setf haredoc
   endif
 enddef
 
diff --git a/runtime/autoload/hare.vim b/runtime/autoload/hare.vim
index c4581fccf..479b0f681 100644
--- a/runtime/autoload/hare.vim
+++ b/runtime/autoload/hare.vim
@@ -1,26 +1,82 @@
-" Vim autoload file.
-" Language:     Hare
-" Maintainer:   Amelia Clarke <[email protected]>
-" Last Updated: 2024-05-10
-" Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
-
-" Attempt to find the directory for a given Hare module.
-function hare#FindModule(str)
-  let path = substitute(trim(a:str, ':', 2), '::', '/', 'g')
-  let dir = finddir(path)
-  while !empty(path) && empty(dir)
-    let path = substitute(path, '/\?\h\w*$', '', '')
-    let dir = finddir(path)
+vim9script
+
+# Helper functions for Hare.
+# Language:     Hare
+# Maintainer:   Amelia Clarke <[email protected]>
+# Last Updated: 2025 Sep 06
+# Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
+
+# Returns the value of HAREPATH, if it exists. Otherwise, returns a safe
+# default.
+export def GetPath(): string
+  var path: list<string>
+  if !empty($HAREPATH)
+    path = split($HAREPATH, ':')
+  else
+    path = ParsePath()
+    if empty(path)
+      return '/usr/src/hare/stdlib,/usr/src/hare/third-party'
+    endif
+  endif
+  return mapnew(path, (_, n) => escape(n, ' ,;'))->join(',')
+enddef
+
+# Converts a module identifier into a path.
+export def IncludeExpr(): string
+  var path = trim(v:fname, ':', 2)->substitute('::', '/', 'g')
+
+  # If the module cannot be found, it might be a member instead. Try removing
+  # the final component until a directory is found.
+  while !finddir(path)
+    const head = fnamemodify(path, ':h')
+    if head == '.'
+      break
+    endif
+    path = head
   endwhile
-  return dir
-endfunction
 
-" Return the value of HAREPATH if it exists. Otherwise use a reasonable 
default.
-function hare#GetPath()
-  if empty($HAREPATH)
-    return '/usr/src/hare/stdlib,/usr/src/hare/third-party'
+  return path
+enddef
+
+# Modifies quickfix or location list entries to refer to the correct paths 
after
+# running :make or :lmake, respectively.
+export def QuickFixPaths()
+  var GetList: func
+  var SetList: func
+
+  if expand('<amatch>') =~ '^l'
+    GetList = function('getloclist', [0])
+    SetList = function('setloclist', [0])
+  else
+    GetList = function('getqflist')
+    SetList = function('setqflist')
   endif
-  return substitute($HAREPATH, ':', ',', 'g')
-endfunction
 
-" vim: et sts=2 sw=2 ts=8
+  final list = GetList({ items: 0 })
+  for n in list.items
+    if !empty(n.module)
+      n.filename = findfile(n.module)
+    endif
+  endfor
+  SetList([], 'r', list)
+enddef
+
+# Attempts to parse the directories in $HAREPATH from the output of `hare
+# version -v`. Otherwise, returns an empty list.
+def ParsePath(): list<string>
+  if !executable('hare')
+    return []
+  endif
+
+  silent const lines = systemlist('hare version -v')
+  const min = match(lines, '^HAREPATH') + 1
+  if min == 0
+    return []
+  endif
+
+  const max = match(lines, '^\S', min)
+  return (max < 0 ? slice(lines, min) : slice(lines, min, max))
+    ->mapnew((_, n) => matchstr(n, '^\s*\zs.*'))
+enddef
+
+# vim: et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/compiler/hare.vim b/runtime/compiler/hare.vim
index 33edb3a28..88f36a9e2 100644
--- a/runtime/compiler/hare.vim
+++ b/runtime/compiler/hare.vim
@@ -1,29 +1,35 @@
-" Vim compiler file.
-" Compiler:    Hare
-" Maintainer:  Amelia Clarke <[email protected]>
-" Last Change: 2024-05-23
-" Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
 
-if exists('current_compiler')
+# Vim compiler file.
+# Compiler:    Hare
+# Maintainer:  Amelia Clarke <[email protected]>
+# Last Change: 2025 Sep 06
+# Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+
+if exists('g:current_compiler')
   finish
 endif
-let current_compiler = 'hare'
-
-let s:cpo_save = &cpo
-set cpo&vim
 
 if filereadable('Makefile') || filereadable('makefile')
   CompilerSet makeprg=make
 else
-  CompilerSet makeprg=hare\ build
+  const makeprg = 'hare build '
+    .. get(b:, 'hare_makeprg_params', get(g:, 'hare_makeprg_params', '-q'))
+  execute 'CompilerSet makeprg=' .. escape(makeprg, ' "\|')
 endif
 
 CompilerSet errorformat=
-  \%f:%l:%c:\ syntax\ error:\ %m,
-  \%f:%l:%c:\ error:\ %m,
+  \%o:%l:%v:\ syntax\ error:\ %m,
+  \%o:%l:%v:\ error:\ %m,
+  \Error:\ %m,
   \%-G%.%#
 
-let &cpo = s:cpo_save
-unlet s:cpo_save
+augroup HareQuickFix
+  autocmd!
+  autocmd QuickFixCmdPost make hare#QuickFixPaths()
+  autocmd QuickFixCmdPost lmake hare#QuickFixPaths()
+augroup END
+
+g:current_compiler = 'hare'
 
-" vim: et sts=2 sw=2 ts=8
+# vim: et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/doc/ft_hare.txt b/runtime/doc/ft_hare.txt
index 937c5e096..cf0b1f935 100644
--- a/runtime/doc/ft_hare.txt
+++ b/runtime/doc/ft_hare.txt
@@ -1,77 +1,134 @@
 *ft_hare.txt*  Support for the Hare programming language
 
 ==============================================================================
-CONTENTS                                                               *hare*
+CONTENTS                                                       *hare* 
*hare.vim*
 
-1. Introduction                                                          
|hare-intro|
-2. Filetype plugin                                              |hare-plugin|
-3. Settings                                                   |hare-settings|
+1. Introduction                                                       
|ft-hare-intro|
+2. Filetype plugin                                           |ft-hare-plugin|
+3. Haredoc filetype                                       |ft-haredoc-plugin|
+4. Indentation settings                                              
|ft-hare-indent|
+5. Compiler support                                           |compiler-hare|
 
 ==============================================================================
-INTRODUCTION                                                     *hare-intro*
+INTRODUCTION                                                  *ft-hare-intro*
 
-This plugin provides syntax highlighting, indentation, and other functionality
-for the Hare programming language. Support is also provided for README files
-inside Hare modules, but this must be enabled by setting |g:filetype_haredoc|.
+This plugin provides syntax highlighting, indentation, and other supporting
+functionality for the Hare programming language.
 
-==============================================================================
-FILETYPE PLUGIN                                                         
*hare-plugin*
 
-This plugin automatically sets the value of 'path' to include the contents of
-the HAREPATH environment variable, allowing commands such as |gf| to directly
-open standard library or third-party modules. If HAREPATH is not set, it
-defaults to the recommended paths for most Unix-like filesystems, namely
-/usr/src/hare/stdlib and /usr/src/hare/third-party.
+FILETYPE PLUGIN                                                      
*ft-hare-plugin*
 
-==============================================================================
-SETTINGS                                                      *hare-settings*
+This plugin has a few different variables that can be defined inside your
+|vimrc| to tweak its behavior.
 
-This plugin provides a small number of variables that you can define in your
-vimrc to configure its behavior.
+Additionally, support is provided for folding `{ }` blocks. To enable folding,
+add the following to a file inside your |after-directory| (e.g.
+~/.vim/after/ftplugin/hare.vim): >
 
-                                                         *g:filetype_haredoc*
-This plugin is able to automatically detect Hare modules and set the "haredoc"
-filetype for any README files. As the recursive directory search used as a
-heuristic has a minor performance impact, this feature is disabled by default
-and must be specifically opted into: >
-       let g:filetype_haredoc = 1
-<
-See |g:haredoc_search_depth| for ways to tweak the searching behavior.
+       setlocal foldmethod=syntax
+
+Because block-based folding tends to create many small folds, consider setting
+a few related options, such as 'foldminlines' and 'foldnestmax'.
 
                                                    *g:hare_recommended_style*
-The following options are set by default, in accordance with the official Hare
+The following options are set by default, in accordance with Hare's official
 style guide: >
+
        setlocal noexpandtab
        setlocal shiftwidth=0
        setlocal softtabstop=0
        setlocal tabstop=8
        setlocal textwidth=80
-<
-To disable this behavior: >
+
+To disable this behavior, add the following to your |vimrc|: >
+
        let g:hare_recommended_style = 0
+<
+                                                    *g:hare_symbol_operators*
+By default, symbolic operators do not receive any special highlighting (with
+`!`, `?`, and `::` being the only exceptions). To enable syntax highlighting
+for most other operators, add the following to your |vimrc|: >
+
+       let g:hare_symbol_operators = 1
 <
                                                          *g:hare_space_error*
-By default, trailing whitespace and tabs preceded by space characters are
-highlighted as errors. This is automatically turned off when in insert mode.
-To disable this highlighting completely: >
+By default, trailing whitespace and spaces followed by <Tab> characters will
+be highlighted as errors. This is automatically disabled in Insert mode. To
+turn off this highlighting completely, add the following to your |vimrc|: >
+
        let g:hare_space_error = 0
-<
-                                                     *g:haredoc_search_depth*
-By default, when |g:filetype_haredoc| is enabled, only the current directory
-and its immediate subdirectories are searched for Hare files. The maximum
-search depth may be adjusted with: >
-       let g:haredoc_search_depth = 2
-<
+
+
+HAREDOC FILETYPE                                          *ft-haredoc-plugin*
+
+This plugin will automatically detect README files inside Hare modules, using
+a recursive directory search, and give them the "haredoc" filetype. Because
+this is such a common filename, this plugin only searches for Hare source
+files within the same directory by default.
+
+                                                         *g:filetype_haredoc*
+The |g:filetype_haredoc| variable can be used to tweak the depth of this
+search, or bypass the detection of Hare documentation files altogether:
+
        Value           Effect~
-       0               Only search the current directory.
-       1               Search the current directory and immediate
-                       subdirectories.
-       2               Search the current directory and two levels of
-                       subdirectories.
+       0               No automatic detection
+       1               Search current directory only (this is the default)
+       2               Search one level of subdirectories
+       3               Search two levels of subdirectories
+
+The search depth may be any positive integer, but values higher than `2` are
+unlikely to provide a tangible benefit in most situations.
+
+
+INDENTATION SETTINGS                                         *ft-hare-indent*
+
+Unlike most other settings for this plugin, the indentation settings may also
+be set per-buffer, overriding any global configuration that exists. To do
+this, simply prefix the variable with |b:| instead of |g:|.
+
+                                                 *g:hare_indent_match_switch*
+By default, continuation lines for "match" and "switch" conditions are
+indented only one level: >hare
+
+       const file = match (os::create(path, 0o644,
+               flag::WRONLY | flag::TRUNC)) {
+       case let file: io::file =>
+               yield file;
+       // ...
+
+If you instead prefer indenting them two levels, to more closely resemble "if"
+and "for" conditions, add the following line to your |vimrc|: >
+
+       let g:hare_indent_match_switch = 2
+<
+                                                         *g:hare_indent_case*
+By default, continuation lines for cases in "match" and "switch" expressions
+are indented two levels, to visually distinguish them from the body of the
+case: >hare
+
+       case ltok::I8, ltok::I16, ltok::I32,
+                       ltok::I64, ltok::INT =>
+               // ...
+
+If you prefer a different amount of indentation, you can adjust it using
+|g:hare_indent_case|. Valid values include `0`, `1`, and `2`.
+
+
+COMPILER SUPPORT                                              *compiler-hare*
+
+If this plugin detects a Makefile in the current directory, it will assume you
+wish to use `make` for your build system, and will leave 'makeprg' untouched.
+Otherwise, `hare build` will be used.
+
+                                                      *g:hare_makeprg_params*
+When `hare build` is used, additional compiler options may be appended to
+'makeprg' with the |g:hare_makeprg_params| variable. It may also be set on a
+per-buffer basis (using |b:| instead of |g:|), overriding any global
+configuration that exists. For example: >
+
+       let b:hare_makeprg_params = '-lc -t o'
 
-The maximum search depth can be set to any integer, but using values higher
-than 2 is not recommended, and will likely provide no tangible benefit in most
-situations.
+The global default is "-q", to suppress writing to stdout while building.
 
 ==============================================================================
- vim:tw=78:ts=8:noet:ft=help:norl:
+ vim:ft=help:noet:ts=8:tw=78:norl:
diff --git a/runtime/doc/tags b/runtime/doc/tags
index 489cb3989..c78a0d934 100644
--- a/runtime/doc/tags
+++ b/runtime/doc/tags
@@ -6718,6 +6718,7 @@ compiler-dotnet   quickfix.txt    /*compiler-dotnet*
 compiler-gcc   quickfix.txt    /*compiler-gcc*
 compiler-gnat  ft_ada.txt      /*compiler-gnat*
 compiler-groff quickfix.txt    /*compiler-groff*
+compiler-hare  ft_hare.txt     /*compiler-hare*
 compiler-hpada ft_ada.txt      /*compiler-hpada*
 compiler-javac quickfix.txt    /*compiler-javac*
 compiler-make  quickfix.txt    /*compiler-make*
@@ -7508,6 +7509,10 @@ ft-gprof-plugin  filetype.txt    /*ft-gprof-plugin*
 ft-groff-syntax        syntax.txt      /*ft-groff-syntax*
 ft-gsp-syntax  syntax.txt      /*ft-gsp-syntax*
 ft-hare        filetype.txt    /*ft-hare*
+ft-hare-indent ft_hare.txt     /*ft-hare-indent*
+ft-hare-intro  ft_hare.txt     /*ft-hare-intro*
+ft-hare-plugin ft_hare.txt     /*ft-hare-plugin*
+ft-haredoc-plugin      ft_hare.txt     /*ft-haredoc-plugin*
 ft-haskell-syntax      syntax.txt      /*ft-haskell-syntax*
 ft-help-omni   helphelp.txt    /*ft-help-omni*
 ft-html-indent indent.txt      /*ft-html-indent*
@@ -7759,9 +7764,12 @@ g:gnat.Set_Project_File()        ft_ada.txt      
/*g:gnat.Set_Project_File()*
 g:gnat.Tags()  ft_ada.txt      /*g:gnat.Tags()*
 g:gnat.Tags_Command    ft_ada.txt      /*g:gnat.Tags_Command*
 g:gzip_exec    pi_gzip.txt     /*g:gzip_exec*
+g:hare_indent_case     ft_hare.txt     /*g:hare_indent_case*
+g:hare_indent_match_switch     ft_hare.txt     /*g:hare_indent_match_switch*
+g:hare_makeprg_params  ft_hare.txt     /*g:hare_makeprg_params*
 g:hare_recommended_style       ft_hare.txt     /*g:hare_recommended_style*
 g:hare_space_error     ft_hare.txt     /*g:hare_space_error*
-g:haredoc_search_depth ft_hare.txt     /*g:haredoc_search_depth*
+g:hare_symbol_operators        ft_hare.txt     /*g:hare_symbol_operators*
 g:help_example_languages       helphelp.txt    /*g:help_example_languages*
 g:html_charset_override        syntax.txt      /*g:html_charset_override*
 g:html_diff_one_file   syntax.txt      /*g:html_diff_one_file*
@@ -8252,9 +8260,7 @@ haiku-vimdir      os_haiku.txt    /*haiku-vimdir*
 hangul hangulin.txt    /*hangul*
 hangulin.txt   hangulin.txt    /*hangulin.txt*
 hare   ft_hare.txt     /*hare*
-hare-intro     ft_hare.txt     /*hare-intro*
-hare-plugin    ft_hare.txt     /*hare-plugin*
-hare-settings  ft_hare.txt     /*hare-settings*
+hare.vim       ft_hare.txt     /*hare.vim*
 has()  builtin.txt     /*has()*
 has-patch      builtin.txt     /*has-patch*
 has-python     if_pyth.txt     /*has-python*
diff --git a/runtime/ftplugin/hare.vim b/runtime/ftplugin/hare.vim
index 6c61c818d..eca1a7881 100644
--- a/runtime/ftplugin/hare.vim
+++ b/runtime/ftplugin/hare.vim
@@ -1,61 +1,52 @@
-" Vim filetype plugin.
-" Language:     Hare
-" Maintainer:   Amelia Clarke <[email protected]>
-" Last Updated: 2024 Oct 04
-" Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
+
+# Vim filetype plugin.
+# Language:     Hare
+# Maintainer:   Amelia Clarke <[email protected]>
+# Last Updated: 2025 Sep 06
+# Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
 
 if exists('b:did_ftplugin')
   finish
 endif
-let b:did_ftplugin = 1
+b:did_ftplugin = 1
 
-let s:cpo_save = &cpo
-set cpo&vim
+# Use the Hare compiler.
+compiler hare
+b:undo_ftplugin = 'compiler make'
 
-" Formatting settings.
+# Formatting settings.
 setlocal comments=://
 setlocal commentstring=//\ %s
-setlocal formatlistpat=^\ \?-\ 
+setlocal formatlistpat=^\s*-\ 
 setlocal formatoptions+=croqnlj/ formatoptions-=t
+b:undo_ftplugin ..= ' | setl cms< com< flp< fo<'
 
-" Search for Hare modules.
-setlocal include=^\s*use\>
-setlocal includeexpr=hare#FindModule(v:fname)
+# Locate Hare modules.
+&l:include = ' ^\s*use\s+%(\h\w*\s*\=)?'
+setlocal includeexpr=hare#IncludeExpr()
 setlocal isfname+=:
+&l:path = ',,' .. hare#GetPath()
 setlocal suffixesadd=.ha
+b:undo_ftplugin ..= ' | setl inc< inex< isf< pa< sua<'
 
-" Add HAREPATH to the default search paths.
-setlocal path-=/usr/include,,
-let &l:path .= ',' .. hare#GetPath() .. ',,'
-
-let b:undo_ftplugin = 'setl cms< com< flp< fo< inc< inex< isf< pa< sua< mp<'
-
-" Follow the Hare style guide by default.
+# Follow the official style guide by default.
 if get(g:, 'hare_recommended_style', 1)
   setlocal noexpandtab
   setlocal shiftwidth=0
   setlocal softtabstop=0
   setlocal tabstop=8
   setlocal textwidth=80
-  let b:undo_ftplugin .= ' et< sts< sw< ts< tw<'
+  b:undo_ftplugin ..= ' | setl et< sts< sw< ts< tw<'
 endif
 
-augroup hare.vim
-  autocmd!
-
-  " Highlight whitespace errors by default.
-  if get(g:, 'hare_space_error', 1)
+# Highlight incorrect whitespace outside of insert mode.
+if get(g:, 'hare_space_error', 1)
+  augroup HareSpaceError
+    autocmd!
     autocmd InsertEnter * hi link hareSpaceError NONE
     autocmd InsertLeave * hi link hareSpaceError Error
-  endif
-augroup END
-
-if !exists('current_compiler')
-  let b:undo_ftplugin .= "| compiler make"
-  compiler hare
+  augroup END
 endif
 
-let &cpo = s:cpo_save
-unlet s:cpo_save
-
-" vim: et sts=2 sw=2 ts=8
+# vim: et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/ftplugin/haredoc.vim b/runtime/ftplugin/haredoc.vim
index 69030b47b..ca66b0663 100644
--- a/runtime/ftplugin/haredoc.vim
+++ b/runtime/ftplugin/haredoc.vim
@@ -1,44 +1,51 @@
-" Vim filetype plugin.
-" Language:     Haredoc (Hare documentation format)
-" Maintainer:   Amelia Clarke <[email protected]>
-" Last Updated: 2024-05-02
-" Upstream:     https://git.sr.ht/~selene/hare.vim
+vim9script
+
+# Vim filetype plugin.
+# Language:     Haredoc (Hare documentation format)
+# Maintainer:   Amelia Clarke <[email protected]>
+# Last Updated: 2025 Sep 06
+# Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
 
 if exists('b:did_ftplugin')
   finish
 endif
-let b:did_ftplugin = 1
+b:did_ftplugin = 1
 
-let s:cpo_save = &cpo
-set cpo&vim
+# Use the Hare compiler.
+compiler hare
+b:undo_ftplugin = 'compiler make'
 
-" Formatting settings.
+# Formatting settings.
 setlocal comments=:\   
-setlocal formatlistpat=^\ \?-\ 
+setlocal commentstring=\       %s
+setlocal formatlistpat=^-\ 
 setlocal formatoptions+=tnlj formatoptions-=c formatoptions-=q
+b:undo_ftplugin ..= ' | setl cms< com< flp< fo<'
 
-" Search for Hare modules.
-setlocal includeexpr=hare#FindModule(v:fname)
+# Locate Hare modules.
+setlocal includeexpr=hare#IncludeExpr()
 setlocal isfname+=:
+&l:path = ',,' .. hare#GetPath()
 setlocal suffixesadd=.ha
+b:undo_ftplugin ..= ' | setl inex< isf< pa< sua<'
 
-" Add HAREPATH to the default search paths.
-setlocal path-=/usr/include,,
-let &l:path .= ',' .. hare#GetPath() .. ',,'
-
-let b:undo_ftplugin = 'setl com< flp< fo< inex< isf< pa< sua<'
-
-" Follow the Hare style guide by default.
+# Follow the official style guide by default.
 if get(g:, 'hare_recommended_style', 1)
   setlocal noexpandtab
   setlocal shiftwidth=0
   setlocal softtabstop=0
   setlocal tabstop=8
   setlocal textwidth=80
-  let b:undo_ftplugin .= ' et< sts< sw< ts< tw<'
+  b:undo_ftplugin ..= ' | setl et< sts< sw< ts< tw<'
 endif
 
-let &cpo = s:cpo_save
-unlet s:cpo_save
+# Highlight incorrect whitespace outside of insert mode.
+if get(g:, 'hare_space_error', 1)
+  augroup HaredocSpaceError
+    autocmd!
+    autocmd InsertEnter * hi link haredocSpaceError NONE
+    autocmd InsertLeave * hi link haredocSpaceError Error
+  augroup END
+endif
 
-" vim: et sts=2 sw=2 ts=8
+# vim: et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/indent/hare.vim b/runtime/indent/hare.vim
index 1b51d1e80..84496348a 100644
--- a/runtime/indent/hare.vim
+++ b/runtime/indent/hare.vim
@@ -1,146 +1,340 @@
-" Vim indent file
-" Language:    Hare
-" Maintainer:  Amelia Clarke <[email protected]>
-" Last Change: 2024-04-14
-" Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
+
+# Vim indent file.
+# Language:    Hare
+# Maintainer:  Amelia Clarke <[email protected]>
+# Last Change: 2025 Sep 06
+# Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
 
 if exists('b:did_indent')
   finish
 endif
-let b:did_indent = 1
-
-let s:cpo_save = &cpo
-set cpo&vim
-
-" L0 -> don't deindent labels
-" (s -> use one indent after a trailing (
-" m1 -> if ) starts a line, indent it the same as its matching (
-" ks -> add an extra indent to extra lines in an if expression or for 
expression
-" j1 -> indent code inside {} one level when in parentheses
-" J1 -> see j1
-" *0 -> don't search for unclosed block comments
-" #1 -> don't deindent lines that begin with #
-setlocal cinoptions=L0,(s,m1,ks,j1,J1,*0,#1
-
-" Controls which keys reindent the current line.
-" 0{     -> { at beginning of line
-" 0}     -> } at beginning of line
-" 0)     -> ) at beginning of line
-" 0]     -> ] at beginning of line
-" !^F    -> <C-f> (not inserted)
-" o      -> <CR> or `o` command
-" O      -> `O` command
-" e      -> else
-" 0=case -> case
+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<'
 
-setlocal cinwords=if,else,for,switch,match
+# 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()
 
-setlocal indentexpr=GetHareIndent()
+  # 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
 
-let b:undo_indent = 'setl cino< cinw< inde< indk<'
+  # 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 exists('*GetHareIndent()')
-  finish
-endif
+    # If the previous line started the block, use the same indent.
+    if pline =~ '{$'
+      return pindent
+    endif
 
-function! FloorCindent(lnum)
-  return cindent(a:lnum) / shiftwidth() * shiftwidth()
-endfunction
+    # If the current line contains a `:` that is not part of `::`, use the
+    # computed cindent.
+    if line =~ ' %(%(::)*)@>:'
+      return cindent
+    endif
 
-function! GetHareIndent()
-  let line = getline(v:lnum)
-  let prevlnum = prevnonblank(v:lnum - 1)
-  let prevline = getline(prevlnum)
-  let prevprevline = getline(prevnonblank(prevlnum - 1))
+    # Unindent after a multi-line `case`.
+    if pline =~ '=>$'
+      return pindent - shiftwidth() * GetValue('hare_indent_case', 2)
+    endif
 
-  " This is all very hacky and imperfect, but it's tough to do much better when
-  " working with regex-based indenting rules.
+    # 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 the previous line ended with =, indent by one shiftwidth.
-  if prevline =~# ' \=\s*(//.*)?$'
-    return indent(prevlnum) + shiftwidth()
+    # 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
 
-  " If the previous line ended in a semicolon and the line before that ended
-  " with =, deindent by one shiftwidth.
-  if prevline =~# ' ;\s*(//.*)?$' && prevprevline =~# ' \=\s*(//.*)?$'
-    return indent(prevlnum) - shiftwidth()
+  # 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) !~ ' %(%(::)*)@>:'
+        return indent(case) + shiftwidth()
+      endif
+    endif
   endif
 
-  " TODO: The following edge-case is still indented incorrectly:
-  " case =>
-  "         if (foo) {
-  "                 bar;
-  "         };
-  " | // cursor is incorrectly deindented by one shiftwidth.
-  "
-  " This only happens if the {} block is the first statement in the case body.
-  " If `case` is typed, the case will also be incorrectly deindented by one
-  " shiftwidth. Are you having fun yet?
-
-  " Deindent cases.
-  if line =~# ' ^\s*case'
-    " If the previous line was also a case, don't do any special indenting.
-    if prevline =~# ' ^\s*case'
-      return indent(prevlnum)
-    end
-
-    " If the previous line was a multiline case, deindent by one shiftwidth.
-    if prevline =~# ' \=\>\s*(//.*)?$'
-      return indent(prevlnum) - shiftwidth()
-    endif
-
-    " If the previous line started a block, deindent by one shiftwidth.
-    " This handles the first case in a switch/match block.
-    if prevline =~# ' \{\s*(//.*)?$'
-      return FloorCindent(v:lnum) - shiftwidth()
-    end
-
-    " If the previous line ended in a semicolon and the line before that wasn't
-    " a case, deindent by one shiftwidth.
-    if prevline =~# ' ;\s*(//.*)?$' && prevprevline !~# ' \=\>\s*(//.*)?$'
-      return FloorCindent(v:lnum) - shiftwidth()
-    end
-
-    let l:indent = FloorCindent(v:lnum)
-
-    " If a normal cindent would indent the same amount as the previous line,
-    " deindent by one shiftwidth. This fixes some issues with `case let` 
blocks.
-    if l:indent == indent(prevlnum)
-      return l:indent - shiftwidth()
-    endif
-
-    " Otherwise, do a normal cindent.
-    return l:indent
+  # If the previous line ended with `=`, indent.
+  if pline =~ '=$'
+    return pindent + shiftwidth()
   endif
 
-  " Don't indent an extra shiftwidth for cases which span multiple lines.
-  if prevline =~# ' \=\>\s*(//.*)?$' && prevline !~# ' ^\s*case\W'
-    return indent(prevlnum)
+  # If the previous line opened an array literal, indent.
+  if pline =~ '[$'
+    return pindent + shiftwidth()
   endif
 
-  " Indent the body of a case.
-  " If the previous line ended in a semicolon and the line before that was a
-  " case, don't do any special indenting.
-  if prevline =~# ' ;\s*(//.*)?$' && prevprevline =~# ' \=\>\s*(//.*)?$'
-        \ && line !~# ' ^\s*}'
-    return indent(prevlnum)
+  # If the previous line started a binding expression, indent.
+  if pline =~ ' <%(const|def|let|type)$'
+    return pindent + shiftwidth()
   endif
 
-  let l:indent = FloorCindent(v:lnum)
+  # 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
 
-  " If the previous line was a case and a normal cindent wouldn't indent, 
indent
-  " an extra shiftwidth.
-  if prevline =~# ' \=\>\s*(//.*)?$' && l:indent == indent(prevlnum)
-    return l:indent + shiftwidth()
+    # 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 =~ ' <%(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 =~ ' <%(const|def|let|type)$'
+      return pindent
+    endif
   endif
 
-  " If everything above is false, do a normal cindent.
-  return l:indent
-endfunction
+  # If the previous line had an unclosed `if` or `for` condition, indent twice.
+  if pline =~ ' <%(if|for)>'
+    const cond = match(pline, ' %(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 =~ ' <%(match|switch)>'
+    const cond = match(pline, ' <%(match|switch)>[^(]*\zs\(')
+    if cond != -1 && TrailingParen(pline, cond)
+      return pindent + shiftwidth()
+        * GetValue('hare_indent_match_switch', 1, 1, 2)
+    endif
+  endif
 
-let &cpo = s:cpo_save
-unlet s:cpo_save
+  # 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 sw=2 sts=2 ts=8
+# vim: et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/syntax/hare.vim b/runtime/syntax/hare.vim
index 4c7ae9248..992b7b905 100644
--- a/runtime/syntax/hare.vim
+++ b/runtime/syntax/hare.vim
@@ -1,157 +1,268 @@
-" Vim syntax file.
-" Language:    Hare
-" Maintainer:  Amelia Clarke <[email protected]>
-" Last Change: 2024-05-10
-" Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
+
+# Vim syntax file.
+# Language:    Hare
+# Maintainer:  Amelia Clarke <[email protected]>
+# Last Change: 2025 Sep 06
+# Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
 
 if exists('b:current_syntax')
   finish
 endif
-syn include @haredoc syntax/haredoc.vim
-let b:current_syntax = 'hare'
 
-" Syntax {{{1
+# Syntax {{{1
 syn case match
 syn iskeyword @,48-57,@-@,_
 
-" Keywords {{{2
-syn keyword hareConditional else if match switch
-syn keyword hareDefine def
-syn keyword hareInclude use
-syn keyword hareKeyword break continue return yield
-syn keyword hareKeyword case
-syn keyword hareKeyword const let
-syn keyword hareKeyword defer
-syn keyword hareKeyword export static
-syn keyword hareKeyword fn
-syn keyword hareOperator as is
-syn keyword hareRepeat for
-syn keyword hareTypedef type
-
-" Attributes.
-syn keyword hareAttribute @fini @init @test
-syn keyword hareAttribute @offset @packed
-syn keyword hareAttribute @symbol
-syn keyword hareAttribute @threadlocal
+# Reserved keywords.
+syn cluster hareReserved 
contains=hareBoolean,hareBuiltin,hareConditional,hareConstant,hareDefine,hareInclude,hareKeyword,hareLabel,hareOperator,hareRepeat,hareStorageClass,hareStructure,hareType,hareTypedef
 
-" Builtins.
-syn keyword hareBuiltin abort assert
-syn keyword hareBuiltin align len offset
-syn keyword hareBuiltin alloc free
-syn keyword hareBuiltin append delete insert
-syn keyword hareBuiltin vaarg vaend vastart
-
-" Types {{{2
+# Types {{{2
+syn cluster hareType 
contains=hareErrorFlag,harePointer,hareSlice,hareStorageClass,hareStructure,hareTaggedUnion,hareType
 syn keyword hareType bool
 syn keyword hareType done
 syn keyword hareType f32 f64
 syn keyword hareType i8 i16 i32 i64 int
 syn keyword hareType never
+syn keyword hareType nomem
 syn keyword hareType opaque
 syn keyword hareType rune str
-syn keyword hareType u8 u16 u32 u64 uint
-syn keyword hareType uintptr
-syn keyword hareType valist
+syn keyword hareType u8 u16 u32 u64 uint uintptr
 syn keyword hareType void
 
-" Other types.
-syn keyword hareStorageClass nullable
+# C ABI.
+syn keyword hareType valist
+
+# Slice and array types.
+syn region hareSlice matchgroup=hareSlice start='\[' end=']' contained 
containedin=hareBuiltinTypeCall,hareTaggedUnion contains=TOP 
nextgroup=@hareType skipempty skipwhite
+syn match hareSlice '\[[*_]]' contains=hareSliceBounds nextgroup=@hareType 
skipempty skipwhite
+syn match hareSliceBounds '[*_]' contained display
+
+# Other types.
+syn keyword hareStorageClass nullable nextgroup=harePointer skipempty skipwhite
 syn keyword hareStructure enum struct union
 
-" Literals {{{2
-syn keyword hareBoolean false true
+# Declarations {{{2
+syn keyword hareDefine def
+syn keyword hareInclude use
+syn keyword hareKeyword const nextgroup=@hareType skipempty skipwhite
+syn keyword hareKeyword export static
+syn keyword hareKeyword fn nextgroup=@hareFunction skipempty skipwhite
+syn keyword hareKeyword let
+syn keyword hareTypedef type nextgroup=hareTypeIdentifier skipempty skipwhite
+
+# Function declarations.
+syn cluster hareFunction contains=hareFunction,hareFuncParams
+syn match hareFunction ' <\h\w*%(::\h\w*)*>' contained 
contains=@hareIdentifier nextgroup=hareFuncParams skipempty skipwhite
+syn region hareFuncParams matchgroup=hareFuncParams start='(' end=')' 
contained contains=TOP nextgroup=@hareType skipempty skipwhite
+
+# Type declarations.
+# FIXME: Does not yet account for type declarations with multiple bindings.
+syn match hareTypeIdentifier ' <\h\w*%(::\h\w*)*>' contained 
contains=hareIdentifier nextgroup=hareTypeEquals skipempty skipwhite transparent
+syn match hareTypeEquals '=' contained nextgroup=@hareType skipempty skipwhite 
transparent
+
+# Identifiers.
+syn match hareIdentifier ' <\h\w*%(::\h\w*)*>' contains=@hareIdentifier 
nextgroup=@harePostfix skipempty skipwhite
+syn cluster hareIdentifier contains=hareDelimiter,hareName
+syn match hareName '\<\h\w*\>' contained contains=@hareReserved transparent
+
+# Attributes {{{3
+syn keyword hareAttribute @init @fini @test
+syn keyword hareAttribute @offset nextgroup=hareAttrParens skipempty skipwhite
+syn keyword hareAttribute @packed
+syn keyword hareAttribute @symbol nextgroup=hareAttrParens skipempty skipwhite
+syn keyword hareAttribute @threadlocal
+
+# Match the parens after attributes.
+syn region hareAttrParens matchgroup=hareAttrParens start='(' end=')' 
contained contains=TOP
+
+# Expressions {{{2
+syn keyword hareConditional else
+syn keyword hareConditional if nextgroup=hareCondParens skipempty skipwhite
+syn keyword hareConditional match switch nextgroup=@hareCondition skipempty 
skipwhite
+syn keyword hareKeyword break continue return yield
+syn keyword hareKeyword defer
+syn keyword hareLabel case nextgroup=@hareType skipempty skipwhite
+syn keyword hareOperator as is nextgroup=@hareType skipempty skipwhite
+syn keyword hareRepeat for nextgroup=@hareCondition skipempty skipwhite
+
+# Match the parens in conditionals and for-loops.
+syn cluster hareCondition contains=hareCondLabel,hareCondParens
+syn match hareCondLabel ':\h\w*\>' contained contains=hareUserLabel 
nextgroup=hareCondParens skipempty skipwhite transparent
+syn region hareCondParens matchgroup=hareCondParens start='(' end=')' 
contained contains=TOP
+
+# Builtins {{{3
+syn keyword hareBuiltin abort assert nextgroup=hareBuiltinCall skipempty 
skipwhite
+syn keyword hareBuiltin align nextgroup=hareBuiltinTypeCall skipempty skipwhite
+syn keyword hareBuiltin alloc free nextgroup=hareBuiltinCall skipempty 
skipwhite
+syn keyword hareBuiltin append insert delete nextgroup=hareBuiltinCall 
skipempty skipwhite
+syn keyword hareBuiltin len offset nextgroup=hareBuiltinCall skipempty 
skipwhite
+
+# C ABI.
+syn keyword hareBuiltin vastart vaarg vaend nextgroup=hareBuiltinCall 
skipempty skipwhite
+
+# Highlight `size` as a builtin only if it is followed by an open paren.
+syn match hareType '\<size\>'
+syn match hareBuiltin '\<size\ze(' nextgroup=hareBuiltinTypeCall
+
+# Match the parens in builtin expressions.
+syn region hareBuiltinCall matchgroup=hareBuiltinCall start='(' end=')' 
contained contains=TOP nextgroup=@harePostfix skipempty skipwhite
+syn region hareBuiltinTypeCall matchgroup=hareBuiltinTypeCall start='(' 
end=')' contained contains=TOP nextgroup=@harePostfix skipempty skipwhite
+
+# Operators {{{3
+syn match hareSymbolOperator '\.\{2,3}'
+syn match hareSymbolOperator '[!<=>]=\?'
+syn match hareSymbolOperator '=>'
+
+# Additive and multiplicative arithmetic.
+syn match hareSymbolOperator '[-+*/%]=\?'
+
+# Bit-shifting arithmetic.
+syn match hareSymbolOperator '\%(<<\|>>\)=\?'
+
+# Bitwise arithmetic.
+syn match hareSymbolOperator '[&^|]=\?'
+syn match hareSymbolOperator '\~'
+
+# Logical arithmetic.
+syn match hareSymbolOperator '\%(&&\|^^\|||\)=\?'
+
+# Highlight `!`, `*`, and `|` correctly in types.
+syn match hareErrorFlag '!' contained 
containedin=hareBuiltinTypeCall,hareTaggedUnion nextgroup=@hareType skipempty 
skipwhite
+syn match harePointer '*' contained 
containedin=hareBuiltinTypeCall,hareTaggedUnion nextgroup=@hareType skipempty 
skipwhite
+syn match hareTaggedUnionBar '|' contained containedin=hareTaggedUnion
+
+# Postfix expressions {{{3
+# TODO: Match postfix expressions after literals.
+syn cluster harePostfix 
contains=hareCast,hareErrorCheck,hareFieldAccess,hareFuncCall,hareIndex
+
+# Casts and type hints.
+syn match hareCast ':' nextgroup=@hareType skipempty skipwhite
+
+# Error handling.
+syn match hareErrorCheck '!=\@!' contained nextgroup=@harePostfix skipempty 
skipwhite
+syn match hareErrorCheck '?' nextgroup=@harePostfix skipempty skipwhite
+
+# Field access.
+syn match hareFieldAccess '\.\w\+\>' contained contains=hareName,hareNumber 
nextgroup=@harePostfix skipempty skipwhite
+
+# Function calls.
+syn region hareFuncCall matchgroup=hareFuncCall start='(' end=')' contained 
contains=TOP nextgroup=@harePostfix skipempty skipwhite
+
+# Indexing and slicing.
+syn region hareIndex matchgroup=hareIndex start='\[' end=']' contained 
contains=TOP nextgroup=@harePostfix skipempty skipwhite
+
+# Nested expressions.
+syn region hareParens matchgroup=hareParens start='(' end=')' contains=TOP 
nextgroup=@harePostfix skipempty skipwhite
+
+# Tagged union and tuple types.
+syn region hareTaggedUnion matchgroup=hareTaggedUnion start='(' end=')' 
contained containedin=hareBuiltinTypeCall,hareTaggedUnion contains=TOP
+
+# Literals {{{3
+syn keyword hareBoolean true false
 syn keyword hareConstant null
 
-" Integer literals.
-syn match hareNumber ' 
<%(0|[1-9]%(_?\d)*)%([Ee]\+?\d+)?%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber ' <0b[01]%(_?[01])*%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber ' <0o\o%(_?\o)*%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber ' <0x\x%(_?\x)*%([iu]%(8|16|32|64)?|z)?>' display
+# Integers.
+syn match hareNumber ' 
<%(0|[1-9]%(_?\d)*)%([Ee]\+?\d+)?%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber ' <0b[01]%(_?[01])*%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber ' <0o\o%(_?\o)*%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber ' <0x\x%(_?\x)*%([iu]%(8|16|32|64)?|z)?>'
 
-" Floating-point literals.
-syn match hareFloat ' 
<%(0|[1-9]%(_?\d)*)\.\d%(_?\d)*%([Ee][+-]?\d+)?%(f32|f64)?>' display
-syn match hareFloat ' <%(0|[1-9]%(_?\d)*)%([Ee][+-]?\d+)?%(f32|f64)>' display
-syn match hareFloat ' <0x\x%(_?\x)*%(\.\x%(_?\x)*)?[Pp][+-]?\d+%(f32|f64)?>' 
display
+# Floats.
+syn match hareFloat ' 
<%(0|[1-9]%(_?\d)*)\.\d%(_?\d)*%([Ee][+-]?\d+)?%(f32|f64)?>'
+syn match hareFloat ' <%(0|[1-9]%(_?\d)*)%([Ee][+-]?\d+)?%(f32|f64)>'
+syn match hareFloat ' <%(0|[1-9]%(_?\d)*)[Ee]-\d+>'
+syn match hareFloat ' <0x\x%(_?\x)*%(\.\x%(_?\x)*)?[Pp][+-]?\d+%(f32|f64)?>'
 
-" Rune and string literals.
+# Rune and string literals.
 syn region hareRune start="'" skip="\'" end="'" contains=hareEscape
 syn region hareString start='"' skip='\"' end='"' 
contains=hareEscape,hareFormat
 syn region hareString start='`' end='`' contains=hareFormat
 
-" Escape sequences.
+# Escape sequences.
 syn match hareEscape '\[0abfnrtv\'"]' contained
 syn match hareEscape ' \%(x\x{2}|u\x{4}|U\x{8})' contained display
 
-" Format sequences.
-syn match hareFormat ' \{\d*%(:%(\.?\d+|[ 
+\-=Xbefgox]|F[.2ESUs]|_%(.|\%([0abfnrtv\'"]|x\x{2}|u\x{4}|U\x{8})))*)?}' 
contained contains=hareEscape display
+# Format sequences.
+syn match hareFormat ' \{\d*%(:%(\.?\d+|[- 
+=Xbefgox]|F[.2ESUs]|_%(\_.|\%([0abfnrtv\'"]|x\x{2}|u\x{4}|U\x{8})))*)?}' 
contained contains=hareEscape
 syn match hareFormat '{\d*%\d*}' contained display
-syn match hareFormat '{{\|}}' contained display
+syn match hareFormat '{{\|}}' contained
 
-" Miscellaneous {{{2
+# Miscellaneous {{{2
 
-" Comments.
-syn region hareComment start='//' end='$' contains=hareTodo,@haredoc,@Spell 
display
-syn keyword hareTodo FIXME TODO XXX contained
-
-" Identifiers.
-syn match hareDelimiter '::' display
-syn match hareName '\<\h\w*\>' nextgroup=@harePostfix skipempty skipwhite 
transparent
-
-" Labels.
-syn match hareLabel ':\h\w*\>' display
+# Annotations.
+syn region hareAnnotation start='#\[' end=']' contains=hareAnnotationIdentifier
+syn match hareAnnotationIdentifier ' <\h\w*%(::\h\w*)*>' contained 
contains=@hareIdentifier nextgroup=hareAnnotationParens skipempty skipwhite 
transparent
+syn region hareAnnotationParens matchgroup=hareAnnotationParens start='(' 
end=')' contained contains=TOP
 
-" Match `size` as a type unless it is followed by an open paren.
-syn match hareType '\<size\>' display
-syn match hareBuiltin '\<size\ze(' display
+# Blocks.
+syn region hareBlock matchgroup=hareBlock start='{' end='}' contains=TOP fold 
nextgroup=@harePostfix skipempty skipwhite
 
-" Postfix expressions.
-syn cluster harePostfix contains=hareErrorTest,hareField,hareIndex,hareParens
-syn match hareErrorTest '!=\@!' contained nextgroup=@harePostfix skipempty 
skipwhite
-syn match hareErrorTest '?' nextgroup=@harePostfix skipempty skipwhite
-syn match hareField '\.\w*\>'hs=s+1 contained contains=hareNumber 
nextgroup=@harePostfix skipempty skipwhite
-syn region hareIndex start='\[' end=']' contained nextgroup=@harePostfix 
skipempty skipwhite transparent
-syn region hareParens start='(' end=')' nextgroup=@harePostfix skipempty 
skipwhite transparent
+# Comments.
+syn region hareComment start='//' end='$' contains=@hareComment keepend
+syn cluster hareComment contains=hareCommentCode,hareCommentRef,hareTodo,@Spell
+syn region hareCommentCode start='     \zs' end='$' contained 
contains=@NoSpell display
+syn match hareCommentRef ' \[\[\h\w*%(::\h\w*)*%(::)?]]' contained 
contains=@NoSpell display
+syn keyword hareTodo FIXME TODO XXX contained
 
-" Whitespace errors.
-syn match hareSpaceError '^ \+\ze      ' display
-syn match hareSpaceError excludenl '\s\+$' containedin=ALL display
+# Delimiters.
+syn match hareDelimiter '::'
 
-" Folding {{{3
-syn region hareBlock start='{' end='}' fold transparent
+# Labels.
+syn match hareUserLabel ':\h\w*\>' contains=hareName
 
-" Default highlighting {{{1
+# Default highlighting {{{1
+hi def link hareAnnotation PreProc
+hi def link hareAnnotationParens hareAnnotation
 hi def link hareAttribute PreProc
 hi def link hareBoolean Boolean
 hi def link hareBuiltin Operator
 hi def link hareComment Comment
+hi def link hareCommentCode hareComment
+hi def link hareCommentRef SpecialComment
 hi def link hareConditional Conditional
 hi def link hareConstant Constant
 hi def link hareDefine Define
 hi def link hareDelimiter Delimiter
-hi def link hareErrorTest Special
+hi def link hareErrorFlag hareStorageClass
+hi def link hareErrorCheck Special
 hi def link hareEscape SpecialChar
 hi def link hareFloat Float
 hi def link hareFormat SpecialChar
+hi def link hareFunction Function
 hi def link hareInclude Include
 hi def link hareKeyword Keyword
-hi def link hareLabel Special
+hi def link hareLabel Label
 hi def link hareNumber Number
 hi def link hareOperator Operator
+hi def link harePointer hareStorageClass
 hi def link hareRepeat Repeat
 hi def link hareRune Character
+hi def link hareSliceBounds harePointer
 hi def link hareStorageClass StorageClass
 hi def link hareString String
 hi def link hareStructure Structure
 hi def link hareTodo Todo
 hi def link hareType Type
 hi def link hareTypedef Typedef
+hi def link hareUserLabel Identifier
 
-" Highlight embedded haredoc references.
-hi! def link haredocRefValid SpecialComment
+# Optionally highlight symbolic operators.
+if get(g:, 'hare_symbol_operators')
+  hi! def link hareSymbolOperator hareOperator
+else
+  hi! def link hareSymbolOperator NONE
+endif
 
-" Highlight whitespace errors by default.
+# Highlight incorrect whitespace by default.
+syn match hareSpaceError '\s\+$' containedin=ALL display
+syn match hareSpaceError ' \+\ze       ' display
 if get(g:, 'hare_space_error', 1)
-  hi def link hareSpaceError Error
+  hi! def link hareSpaceError Error
+else
+  hi! def link hareSpaceError NONE
 endif
 
-" vim: et sts=2 sw=2 ts=8
+b:current_syntax = 'hare'
+
+# vim: fdm=marker et sts=2 sw=2 ts=8 tw=80
diff --git a/runtime/syntax/haredoc.vim b/runtime/syntax/haredoc.vim
index 09c99c1d5..adf15bc3d 100644
--- a/runtime/syntax/haredoc.vim
+++ b/runtime/syntax/haredoc.vim
@@ -1,32 +1,43 @@
-" Vim syntax file.
-" Language:    Haredoc (Hare documentation format)
-" Maintainer:  Amelia Clarke <[email protected]>
-" Last Change: 2024-05-10
-" Upstream:    https://git.sr.ht/~selene/hare.vim
+vim9script
+
+# Vim syntax file.
+# Language:    Haredoc (Hare documentation format)
+# Maintainer:  Amelia Clarke <[email protected]>
+# Last Change: 2025 Aug 14
+# Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
 
 if exists('b:current_syntax')
   finish
 endif
-let b:current_syntax = 'haredoc'
 
-" Syntax {{{1
+# Syntax {{{1
 syn case match
 syn iskeyword @,48-57,_
 
-" Code samples.
-syn region haredocCodeSample excludenl start=' \zs' end='$' contains=@NoSpell 
display
+# Embedded code samples.
+syn region haredocCode start=' \zs' end='$' contains=@NoSpell display
 
-" References to other declarations and modules.
-syn region haredocRef start='\[\[' end=']]' contains=haredocRefValid,@NoSpell 
display keepend oneline
-syn match haredocRefValid ' \[\[\h\w*%(::\h\w*)*%(::)?]]' contained 
contains=@NoSpell display
+# References to other declarations and modules.
+syn match haredocRef ' \[\[\h\w*%(::\h\w*)*%(::)?]]' contains=@NoSpell display
 
-" Miscellaneous.
+# Miscellaneous.
 syn keyword haredocTodo FIXME TODO XXX
 
-" Default highlighting {{{1
-hi def link haredocCodeSample Comment
-hi def link haredocRef Error
-hi def link haredocRefValid Special
+# Default highlighting {{{1
+hi def link haredocCode Comment
+hi def link haredocRef Special
 hi def link haredocTodo Todo
 
-" vim: et sts=2 sw=2 ts=8
+# Highlight incorrect whitespace by default.
+syn match haredocSpaceError '\s\+$' containedin=ALL display
+syn match haredocSpaceError '^ \zs \+\ze       ' containedin=ALL display
+syn match haredocSpaceError '[^ ]\zs \+\ze     ' containedin=ALL display
+if get(g:, 'hare_space_error', 1)
+  hi! def link haredocSpaceError Error
+else
+  hi! def link haredocSpaceError NONE
+endif
+
+b:current_syntax = 'haredoc'
+
+# vim: fdm=marker et sts=2 sw=2 ts=8 tw=80
diff --git a/src/testdir/test_filetype.vim b/src/testdir/test_filetype.vim
index d91d1bafe..8452739b3 100644
--- a/src/testdir/test_filetype.vim
+++ b/src/testdir/test_filetype.vim
@@ -1708,6 +1708,7 @@ endfunc
 
 func Test_haredoc_file()
   filetype on
+
   call assert_true(mkdir('foo/bar', 'pR'))
 
   call writefile([], 'README', 'D')
@@ -1715,28 +1716,37 @@ func Test_haredoc_file()
   call assert_notequal('haredoc', &filetype)
   bwipe!
 
-  let g:filetype_haredoc = 1
+  let g:filetype_haredoc = 3
+  call writefile([], 'foo/bar/bar.ha', 'D')
+  split README
+  call assert_equal('haredoc', &filetype)
+  bwipe!
+
+  let g:filetype_haredoc = 2
   split README
   call assert_notequal('haredoc', &filetype)
   bwipe!
 
-  call writefile([], 'foo/quux.ha')
+  call writefile([], 'foo/foo.ha', 'D')
   split README
   call assert_equal('haredoc', &filetype)
   bwipe!
-  call delete('foo/quux.ha')
 
-  call writefile([], 'foo/bar/baz.ha', 'D')
+  let g:filetype_haredoc = 1
   split README
   call assert_notequal('haredoc', &filetype)
   bwipe!
 
-  let g:haredoc_search_depth = 2
+  call writefile([], 'main.ha', 'D')
   split README
   call assert_equal('haredoc', &filetype)
   bwipe!
+
+  let g:filetype_haredoc = 0
+  split README
+  call assert_notequal('haredoc', &filetype)
+  bwipe!
   unlet g:filetype_haredoc
-  unlet g:haredoc_search_depth
 
   filetype off
 endfunc

-- 
-- 
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php

--- 
You received this message because you are subscribed to the Google Groups 
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to [email protected].
To view this discussion visit 
https://groups.google.com/d/msgid/vim_dev/E1uvhnT-00C7kO-PS%40256bit.org.

Raspunde prin e-mail lui