]> git.ipfire.org Git - thirdparty/vim.git/commitdiff
runtime(hare): update for Hare 0.25.2
authorAmelia Clarke <selene@perilune.dev>
Mon, 8 Sep 2025 19:30:41 +0000 (15:30 -0400)
committerChristian Brabandt <cb@256bit.org>
Mon, 8 Sep 2025 19:30:41 +0000 (15:30 -0400)
closes: #18222

Signed-off-by: Amelia Clarke <selene@perilune.dev>
Signed-off-by: Christian Brabandt <cb@256bit.org>
runtime/autoload/dist/ft.vim
runtime/autoload/hare.vim
runtime/compiler/hare.vim
runtime/doc/ft_hare.txt
runtime/doc/tags
runtime/ftplugin/hare.vim
runtime/ftplugin/haredoc.vim
runtime/indent/hare.vim
runtime/syntax/hare.vim
runtime/syntax/haredoc.vim
src/testdir/test_filetype.vim

index 4d39689cc0cc7a04ebcb74ad865e5ab1f406693e..b0609727e0898a18f1ff8236cbdc5648feaa238e 100644 (file)
@@ -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 <Bram@vim.org>
 
 # 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
 
index c4581fccf9af8402ed4e60dba57812895f8a007c..479b0f6812d86f3407c66b6b445ac2dc7418fd71 100644 (file)
@@ -1,26 +1,82 @@
-" Vim autoload file.
-" Language:     Hare
-" Maintainer:   Amelia Clarke <selene@perilune.dev>
-" 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 <selene@perilune.dev>
+# 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
index 33edb3a281353647ef09e2c9fca8b51e8d59cb2e..88f36a9e2093bf78b2256b4f1db36cd581510fba 100644 (file)
@@ -1,29 +1,35 @@
-" Vim compiler file.
-" Compiler:    Hare
-" Maintainer:  Amelia Clarke <selene@perilune.dev>
-" 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 <selene@perilune.dev>
+# 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
index 937c5e09610e593d1732c63833480f7b5297f61a..cf0b1f93578d3e58416066d9b540b35639639a2f 100644 (file)
 *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:
index 489cb3989304e53fa9f0342a38177b072d46e469..c78a0d934fc5cf3f2e991d083e4569dfcb29a082 100644 (file)
@@ -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*
index 6c61c818d19a34ff6871bd39200a96682970c3ee..eca1a78817fa991b2cd358559a7eae18cd92bef2 100644 (file)
@@ -1,61 +1,52 @@
-" Vim filetype plugin.
-" Language:     Hare
-" Maintainer:   Amelia Clarke <selene@perilune.dev>
-" Last Updated: 2024 Oct 04
-" Upstream:     https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
+
+# Vim filetype plugin.
+# Language:     Hare
+# Maintainer:   Amelia Clarke <selene@perilune.dev>
+# 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 = '\v^\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
index 69030b47bae963862b8f812ecb88b73a45215f78..ca66b066388c6c5dbd0f645d4d10d842975d4cf1 100644 (file)
@@ -1,44 +1,51 @@
-" Vim filetype plugin.
-" Language:     Haredoc (Hare documentation format)
-" Maintainer:   Amelia Clarke <selene@perilune.dev>
-" 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 <selene@perilune.dev>
+# 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
index 1b51d1e80a99cea419004b5253105434963935cd..84496348a1cdc11a767264cd42c827c977ebf2f3 100644 (file)
-" Vim indent file
-" Language:    Hare
-" Maintainer:  Amelia Clarke <selene@perilune.dev>
-" Last Change: 2024-04-14
-" Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+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
-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 =~ '\v%(%(::)*)@>:'
+      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 =~# '\v\=\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 =~# '\v;\s*(//.*)?$' && prevprevline =~# '\v\=\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) !~ '\v%(%(::)*)@>:'
+        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 =~# '\v^\s*case'
-    " If the previous line was also a case, don't do any special indenting.
-    if prevline =~# '\v^\s*case'
-      return indent(prevlnum)
-    end
-
-    " If the previous line was a multiline case, deindent by one shiftwidth.
-    if prevline =~# '\v\=\>\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 =~# '\v\{\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 =~# '\v;\s*(//.*)?$' && prevprevline !~# '\v\=\>\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 =~# '\v\=\>\s*(//.*)?$' && prevline !~# '\v^\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 =~# '\v;\s*(//.*)?$' && prevprevline =~# '\v\=\>\s*(//.*)?$'
-        \ && line !~# '\v^\s*}'
-    return indent(prevlnum)
+  # If the previous line started a binding expression, indent.
+  if pline =~ '\v<%(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 =~# '\v\=\>\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 =~ '\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 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 =~ '\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
 
-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
index 4c7ae924869b82b895bf9a4cf6748f5363a81447..992b7b90590a4a47ca18072d0833d8de63e8c602 100644 (file)
-" Vim syntax file.
-" Language:    Hare
-" Maintainer:  Amelia Clarke <selene@perilune.dev>
-" Last Change: 2024-05-10
-" Upstream:    https://git.sr.ht/~sircmpwn/hare.vim
+vim9script
+
+# Vim syntax 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: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 '\v<\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 '\v<\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 '\v<\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 '\v<%(0|[1-9]%(_?\d)*)%([Ee]\+?\d+)?%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber '\v<0b[01]%(_?[01])*%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber '\v<0o\o%(_?\o)*%([iu]%(8|16|32|64)?|z)?>' display
-syn match hareNumber '\v<0x\x%(_?\x)*%([iu]%(8|16|32|64)?|z)?>' display
+# Integers.
+syn match hareNumber '\v<%(0|[1-9]%(_?\d)*)%([Ee]\+?\d+)?%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber '\v<0b[01]%(_?[01])*%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber '\v<0o\o%(_?\o)*%([iu]%(8|16|32|64)?|z)?>'
+syn match hareNumber '\v<0x\x%(_?\x)*%([iu]%(8|16|32|64)?|z)?>'
 
-" Floating-point literals.
-syn match hareFloat '\v<%(0|[1-9]%(_?\d)*)\.\d%(_?\d)*%([Ee][+-]?\d+)?%(f32|f64)?>' display
-syn match hareFloat '\v<%(0|[1-9]%(_?\d)*)%([Ee][+-]?\d+)?%(f32|f64)>' display
-syn match hareFloat '\v<0x\x%(_?\x)*%(\.\x%(_?\x)*)?[Pp][+-]?\d+%(f32|f64)?>' display
+# Floats.
+syn match hareFloat '\v<%(0|[1-9]%(_?\d)*)\.\d%(_?\d)*%([Ee][+-]?\d+)?%(f32|f64)?>'
+syn match hareFloat '\v<%(0|[1-9]%(_?\d)*)%([Ee][+-]?\d+)?%(f32|f64)>'
+syn match hareFloat '\v<%(0|[1-9]%(_?\d)*)[Ee]-\d+>'
+syn match hareFloat '\v<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 '\v\\%(x\x{2}|u\x{4}|U\x{8})' contained display
 
-" Format sequences.
-syn match hareFormat '\v\{\d*%(:%(\.?\d+|[ +\-=Xbefgox]|F[.2ESUs]|_%(.|\\%([0abfnrtv\\'"]|x\x{2}|u\x{4}|U\x{8})))*)?}' contained contains=hareEscape display
+# Format sequences.
+syn match hareFormat '\v\{\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 '\v<\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='\t\zs' end='$' contained contains=@NoSpell display
+syn match hareCommentRef '\v\[\[\h\w*%(::\h\w*)*%(::)?]]' contained contains=@NoSpell display
+syn keyword hareTodo FIXME TODO XXX contained
 
-" Whitespace errors.
-syn match hareSpaceError '^ \+\ze\t' 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\t' 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
index 09c99c1d56428a4c07948bd397df075eca667800..adf15bc3df2eada23fe6bac2e1037c2ddc4e6041 100644 (file)
@@ -1,32 +1,43 @@
-" Vim syntax file.
-" Language:    Haredoc (Hare documentation format)
-" Maintainer:  Amelia Clarke <selene@perilune.dev>
-" 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 <selene@perilune.dev>
+# 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='\t\zs' end='$' contains=@NoSpell display
+# Embedded code samples.
+syn region haredocCode start='\t\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 '\v\[\[\h\w*%(::\h\w*)*%(::)?]]' contained contains=@NoSpell display
+# References to other declarations and modules.
+syn match haredocRef '\v\[\[\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\t' containedin=ALL display
+syn match haredocSpaceError '[^ ]\zs \+\ze\t' 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
index d91d1bafe0f921a2f46beb8864cee6d0b3f30ca8..8452739b3e0eb96093859d4701ed21a81ecbfb0b 100644 (file)
@@ -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