From 025a3e2baf18605dd319042457c31b64cc552c9c Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Jun 2026 09:42:27 +0800 Subject: [PATCH 1/5] vim-patch:9.2.0479: [security]: runtime(tar): command injection in tar plugin Problem: [security]: runtime(tar): command injection in tar plugin (Christopher Lusk) Solution: Use the correct shellescape(args, 1) form for a :! command Github Advisory: https://github.com/vim/vim/security/advisories/GHSA-2fpv-9ff7-xg5w https://github.com/vim/vim/commit/3fb5e58fbc63d86a3e65f1a141b0d67af2aa38a1 Co-authored-by: Christian Brabandt --- runtime/autoload/tar.vim | 1 + test/old/testdir/test_plugin_tar.vim | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim index c492e1fe4d..c0e9bf2f88 100644 --- a/runtime/autoload/tar.vim +++ b/runtime/autoload/tar.vim @@ -24,6 +24,7 @@ " 2026 Apr 09 by Vim Project: fix bug with dotted filename (#19930) " 2026 Apr 15 by Vim Project: fix more path traversal issues (#19981) " 2026 Apr 16 by Vim Project: use g:tar_secure in tar#Extract() +" 2026 May 14 by Vim Project: use correct shellescape() call in Vimuntar() " " Contains many ideas from Michael Toren's " diff --git a/test/old/testdir/test_plugin_tar.vim b/test/old/testdir/test_plugin_tar.vim index 2152fe701e..706ef0d89b 100644 --- a/test/old/testdir/test_plugin_tar.vim +++ b/test/old/testdir/test_plugin_tar.vim @@ -321,3 +321,23 @@ func Test_extract_with_dotted_filename() call delete('X.txt') bw! endfunc + +func Test_extract_command_injection() + throw 'SKipped: Nvim does not support :Vimuntar' + CheckExecutable gunzip + CheckExecutable touch + var tgz = eval('0z1F8B08087795056A000364756D6D792E74617200EDCE2B12C2300004D01C254' .. + '7480269CE534080A8495BD1DBF3996106C3A08A7ACFACD8157B59A7690BFB4A0FC3707C666E357D' .. + 'E65BC8B5A47CC8A5D61A522EA5B510D3CEBF5ED679197B8CE17CEDB7F9D4C76FBB5F3D000000000' .. + '000000000FCD11D32415E2C00280000') + var dirname = tempname() + + mkdir(dirname, 'R') + var tar = dirname .. "/';%$(touch pwned)'.tgz" + writefile(tgz, tar) + new + exe "e " .. fnameescape(tar) + exe ":Vimuntar " .. dirname + assert_false(filereadable(dirname .. "/pwned")) + bw! +endfunc From 147872452ec4f7190e8f3a3e6255b8f1ca9bbf60 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Jun 2026 09:39:45 +0800 Subject: [PATCH 2/5] vim-patch:9.2.0673: tests: Test_recover_corrupted_swap_file() cannot handle symlinks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: tests: Test_recover_corrupted_swap_file() cannot handle symlinks Solution: Use writefile(readblob()) instead (Vladimír Marek) Test_recover_corrupted_swap_file1() copies prebuilt corrupt swap samples before recovering them. In an out-of-source-tree build those sample files may be symlinks into the source tree. filecopy() preserves symlinks, so the copied target may remain a symlink. Recovery opens swap files with O_NOFOLLOW, so that copied symlink cannot be opened. Read the sample as a blob and write it back so the recovery target is a real swap file. closes: vim/vim#20561 https://github.com/vim/vim/commit/88cbd003125840ed20f455653e28b91e3237f380 Co-authored-by: Vladimír Marek --- test/old/testdir/test_recover.vim | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/old/testdir/test_recover.vim b/test/old/testdir/test_recover.vim index c33427572e..2a5c453406 100644 --- a/test/old/testdir/test_recover.vim +++ b/test/old/testdir/test_recover.vim @@ -508,7 +508,9 @@ func Test_recover_corrupted_swap_file1() new let sample = 'samples/recover-crash1.swp' let target = '.Xpoc1.swp' " Xpoc1.swp (non-hidden) doesn't work in Nvim - call filecopy(sample, target) + " In an out-of-source-tree build the sample may be a symlink, this copies the + " data into a real file. + call writefile(readblob(sample), target, 'D') try sil recover! Xpoc1 catch /^Vim\%((\S\+)\)\=:E1364:/ @@ -522,7 +524,9 @@ func Test_recover_corrupted_swap_file1() new let sample = 'samples/recover-crash2.swp' let target = '.Xpoc2.swp' " Xpoc1.swp (non-hidden) doesn't work in Nvim - call filecopy(sample, target) + " In an out-of-source-tree build the sample may be a symlink, this copies the + " data into a real file. + call writefile(readblob(sample), target, 'D') try sil recover! Xpoc2 catch /^Vim\%((\S\+)\)\=:E1364:/ From 3e851d407e2383403d612b6fce52b2024a1313be Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Jun 2026 09:41:43 +0800 Subject: [PATCH 3/5] vim-patch:b0ce576: runtime(tar): fix lz4 extraction on non-Linux systems MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Patch 9.2.0306 fixed malformed lz4 extraction commands by using "tar -I lz4" on Linux and leaving non-Linux tar implementations to auto-detect lz4 input. That still fails on systems where tar does not support either -I lz4 or automatic lz4 decompression, such as Solaris /usr/bin/tar. Keep the existing Linux path using GNU tar's "-I lz4" support. For non-Linux systems, use lz4 explicitly to decompress the archive to stdout and feed the resulting tar stream to the configured tar extraction command. This is the same style tar.vim already used for lz4 archives before patch 9.2.0306. Follow-up for vim/vim#19925 closes: vim/vim#20555 https://github.com/vim/vim/commit/b0ce576fbc7af4a33226a8d076a01aadd024a23e Co-authored-by: Vladimír Marek --- runtime/autoload/tar.vim | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/runtime/autoload/tar.vim b/runtime/autoload/tar.vim index c0e9bf2f88..1c23fd9bb6 100644 --- a/runtime/autoload/tar.vim +++ b/runtime/autoload/tar.vim @@ -25,6 +25,7 @@ " 2026 Apr 15 by Vim Project: fix more path traversal issues (#19981) " 2026 Apr 16 by Vim Project: use g:tar_secure in tar#Extract() " 2026 May 14 by Vim Project: use correct shellescape() call in Vimuntar() +" 2026 Jun 16 by Vim Project: fix lz4 extraction on non-linux systemd (#20555) " " Contains many ideas from Michael Toren's " @@ -128,7 +129,7 @@ let g:tar_leading_pat='\m^\%([.]\{,2\}/\)\+' fun! s:Msg(func, severity, msg) redraw! if a:severity =~? 'error' - echohl Error + echohl Error else echohl WarningMsg endif @@ -732,8 +733,10 @@ fun! tar#Extract() elseif tarball =~# "\.tlz4$" if has("linux") let extractcmd= substitute(extractcmd,"-","-I lz4 -","") + call system(extractcmd." ".shellescape(tarball)." ".g:tar_secure.shellescape(fname)) + else + call system("lz4 --decompress --stdout -- ".shellescape(tarball)." | ".extractcmd." - ".g:tar_secure.shellescape(fname)) endif - call system(extractcmd." ".shellescape(tarball)." ".g:tar_secure.shellescape(fname)) if v:shell_error != 0 call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else @@ -743,8 +746,10 @@ fun! tar#Extract() elseif tarball =~# "\.tar\.lz4$" if has("linux") let extractcmd= substitute(extractcmd,"-","-I lz4 -","") + call system(extractcmd." ".shellescape(tarball)." ".g:tar_secure.shellescape(fname)) + else + call system("lz4 --decompress --stdout -- ".shellescape(tarball)." | ".extractcmd." - ".g:tar_secure.shellescape(fname)) endif - call system(extractcmd." ".shellescape(tarball)." ".g:tar_secure.shellescape(fname)) if v:shell_error != 0 call s:Msg('tar#Extract', 'error', $"{extractcmd} {tarball} {fname}: failed!") else From dc1393d9c1b96c3c1573da1b4e128dab53496435 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Jun 2026 09:44:30 +0800 Subject: [PATCH 4/5] vim-patch:9.2.0675: tests: Test_cd_from_non_existing_dir() fails on Solaris MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: tests: Test_cd_from_non_existing_dir() fails on Solaris Solution: Skip the test on Solaris (Vladimír Marek). Test_cd_from_non_existing_dir() depends on deleting the current working directory. Solaris does not allow that, so skip the test there. closes: vim/vim#20563 https://github.com/vim/vim/commit/b464c36bf976d8f5c7d42371704e487c65f905f4 Co-authored-by: Vladimír Marek --- test/old/testdir/test_cd.vim | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/old/testdir/test_cd.vim b/test/old/testdir/test_cd.vim index 89c8a12bf2..21cb31e0e5 100644 --- a/test/old/testdir/test_cd.vim +++ b/test/old/testdir/test_cd.vim @@ -215,6 +215,9 @@ func Test_lcd_win_execute() endfunc func Test_cd_from_non_existing_dir() + if has('sun') + throw 'Skipped: Solaris does not allow deleting the current working directory' + endif CheckNotMSWindows let saveddir = getcwd() From cb33b05ba35c8c16e54a62a73f800cdd2ee02e64 Mon Sep 17 00:00:00 2001 From: zeertzjq Date: Fri, 19 Jun 2026 09:44:55 +0800 Subject: [PATCH 5/5] vim-patch:8181c6e: runtime(luau): runtime support is incomplete Problem: runtime(luau): runtime support is incomplete. Solution: Add Luau syntax, indent, filetype plugin and indent tests. closes: vim/vim#20544 https://github.com/vim/vim/commit/8181c6e31387c0467fcd6394d1b7da07f38e737c Co-authored-by: Lopy <70210066+lopi-py@users.noreply.github.com> --- runtime/doc/filetype.txt | 6 ++ runtime/ftplugin/luau.vim | 191 ++++++++++++++++++++++++++++++++++++-- runtime/indent/luau.vim | 155 +++++++++++++++++++++++++++++-- runtime/syntax/luau.vim | 188 ++++++++++++++++++++++++++++++++++++- 4 files changed, 521 insertions(+), 19 deletions(-) diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt index b1bb6b7a23..f64c4983c3 100644 --- a/runtime/doc/filetype.txt +++ b/runtime/doc/filetype.txt @@ -839,6 +839,12 @@ For example, to use Lua 5.1, set the variables like this: > let g:lua_version = 5 let g:lua_subversion = 1 < +LUAU *ft-luau-plugin* *g:luau_folding* + +You can enable folding of Luau block constructs using |fold-expr| by: > + + let g:luau_folding = 1 +< MAIL *ft-mail-plugin* Options: diff --git a/runtime/ftplugin/luau.vim b/runtime/ftplugin/luau.vim index 458d0b05a9..9b413e6778 100644 --- a/runtime/ftplugin/luau.vim +++ b/runtime/ftplugin/luau.vim @@ -1,14 +1,193 @@ " Vim filetype plugin file -" Language: Luau -" Maintainer: None yet -" Last Change: 2023 Apr 30 +" Language: Luau +" Maintainer: Lopy (@lopi-py) +" Last Change: 2026 Jun 16 if exists("b:did_ftplugin") finish endif +let b:did_ftplugin = 1 -" Luau is a superset of Lua -runtime! ftplugin/lua.vim +let s:cpo_save = &cpo +set cpo&vim +setlocal comments=:---,:-- +setlocal commentstring=--\ %s +setlocal formatoptions-=t formatoptions+=croql -" vim: nowrap sw=2 sts=2 ts=8 +let &l:define = '\<\%(function\|local\%(\s\+function\)\=\|const\%(\s\+function\)\=\|type\%(\s\+function\)\=\|export\s\+type\%(\s\+function\)\=\|class\|declare\s\+\%(function\|class\|extern\s\+type\)\)\>' +let &l:include = '\.\{-}\' .. + \ '\|^\s*\%(' .. s:attr .. '\)*class\>\s\+\h' .. + \ '\|^\s*\%(' .. s:attr .. '\)*declare\s\+class\>\s\+\h' .. + \ '\|^\s*\%(' .. s:attr .. '\)*declare\s\+extern\s\+type\>.\{-}\' .. s:line_end + +if exists("loaded_matchit") && !exists("b:match_words") + let b:match_ignorecase = 0 + let s:match_tail = ':\\|^\s*\%(else\|elseif\)\>:' .. s:end_pat .. + \ ',^\s*repeat\>:\,' .. + \ '\%(--\)\=\[\(=*\)\[:\]\1\]' + let s:match_words = '\<\%(do\|function\)\>\|' .. s:end_block .. s:match_tail + let s:match_words_no_function = '\\|' .. s:end_block .. s:match_tail + let b:match_words = "LuauMatchWords()" + let b:match_skip = 'LuauMatchSkip()' + let b:undo_ftplugin ..= " | unlet! b:match_words b:match_ignorecase b:match_skip" +endif + +if (has("gui_win32") || has("gui_gtk")) && !exists("b:browsefilter") + let b:browsefilter = "Luau Source Files (*.luau)\t*.luau\n" + if has("win32") + let b:browsefilter ..= "All Files (*.*)\t*\n" + else + let b:browsefilter ..= "All Files (*)\t*\n" + endif + let b:undo_ftplugin ..= " | unlet! b:browsefilter" +endif + +if has("folding") && get(g:, "luau_folding", 0) + setlocal foldmethod=expr + setlocal foldexpr=s:LuauFold() + let b:luau_lasttick = -1 + let b:undo_ftplugin ..= " | setl foldexpr< foldmethod< | unlet! b:luau_lasttick b:luau_foldlists" +endif + +" the rest of the file needs to be sourced only once per Vim session +if exists("s:loaded_luau") || &cp + let &cpo = s:cpo_save + unlet s:cpo_save + finish +endif +let s:loaded_luau = 1 + +function s:LuauInclude(fname) abort + let fname = a:fname + if fname =~# '^@' + return fname + endif + for path in [fname, fname .. ".luau", fname .. ".lua", fname .. "/init.luau", fname .. "/init.lua"] + if filereadable(path) + return path + endif + endfor + return fname +endfunction + +function s:IsStringOrComment(lnum, col) abort + let name = synIDattr(synID(a:lnum, a:col, 1), "name") + return name =~# '^luau\%(Comment\|.*String\)' +endfunction + +function s:LineCommentStart(lnum) abort + let line = getline(a:lnum) + let midx = stridx(line, '--') + while midx != -1 + if !s:IsStringOrComment(a:lnum, midx + 1) + return midx + endif + let midx = stridx(line, '--', midx + 2) + endwhile + return -1 +endfunction + +function s:InDeclareClass(lnum) abort + let save_cursor = getcurpos() + call cursor(a:lnum - 1, 1) + let lnum = search('^\s*\%(end\>\|declare\s\+class\>\)', 'bcnW') + call setpos('.', save_cursor) + return lnum > 0 && getline(lnum) =~# '^\s*declare\s\+class\>' +endfunction + +function LuauMatchSkip() abort + let lnum = line(".") + let col = col(".") + let comment = s:LineCommentStart(lnum) + return s:IsStringOrComment(lnum, col) + \ || (comment != -1 && col > comment) + \ || (expand("") ==# "function" && s:InDeclareClass(lnum)) +endfunction + +function LuauMatchWords() abort + if exists("s:match_words_no_function") + \ && expand("") ==# "function" && s:InDeclareClass(line(".")) + return s:match_words_no_function + endif + return get(s:, "match_words", "") +endfunction + +let s:fold_guard = '^\s*\%(@\|\%(do\|if\|repeat\|for\|while\|public' .. + \ '\|function\|return\|local\|const\|type\|export\|class' .. + \ '\|declare\|end\|until\)\>\)' + +let s:patterns = [ + \ ['^\s*do\>' .. s:line_end, s:end_pat, 'block'], + \ ['^\s*if\>.\{-}\' .. s:line_end, s:end_pat, 'block'], + \ ['^\s*repeat\>' .. s:line_end, '^\s*until\>.*', 'block'], + \ ['^\s*for\>.\{-}\' .. s:line_end, s:end_pat, 'block'], + \ ['^\s*while\>.\{-}\' .. s:line_end, s:end_pat, 'block'], + \ ['^\s*\%(' .. s:attr .. '\)*\%(public\s\+\)\=function\>.*', s:end_pat, 'function'], + \ ['^\s*return\s\+function\>.*', s:end_pat, 'function'], + \ ['^\s*\%(' .. s:attr .. '\)*local\s\+function\>.*', s:end_pat, 'function'], + \ ['^\s*\%(' .. s:attr .. '\)*const\s\+function\>.*', s:end_pat, 'function'], + \ ['^\s*\%(' .. s:attr .. '\)*\%(export\s\+\)\=type\s\+function\>.*', s:end_pat, 'function'], + \ ['^\s*class\>\s\+\h.*', s:end_pat, 'class'], + \ ['^\s*declare\s\+class\>\s\+\h.*', s:end_pat, 'declare_class'], + \ ['^\s*declare\s\+extern\s\+type\>.\{-}\' .. s:line_end, s:end_pat, 'block'], + \ ] + +function s:LuauFold() abort + if b:luau_lasttick == b:changedtick + return b:luau_foldlists[v:lnum - 1] + endif + let b:luau_lasttick = b:changedtick + + let b:luau_foldlists = [] + let foldlist = [] + let buf = getline(1, "$") + for line in buf + let open = 0 + let end = 0 + if line !~# s:fold_guard + call add(b:luau_foldlists, "" .. len(foldlist)) + continue + endif + for t in s:patterns + let tagopen = t[0] + let tagend = t[1] + if line =~# tagopen + if t[2] ==# 'function' && len(foldlist) > 0 && foldlist[-1][2] ==# 'declare_class' + continue + endif + call add(foldlist, t) + let open = 1 + break + elseif line =~# tagend + if len(foldlist) > 0 && line =~# foldlist[-1][1] + call remove(foldlist, -1) + let end = 1 + else + let foldlist = [] + endif + break + endif + endfor + let prefix = "" + if open == 1 | let prefix = ">" | endif + if end == 1 | let prefix = "<" | endif + call add(b:luau_foldlists, prefix .. (len(foldlist) + end)) + endfor + + return b:luau_foldlists[v:lnum - 1] +endfunction + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: nowrap sw=2 sts=2 ts=8 noet: diff --git a/runtime/indent/luau.vim b/runtime/indent/luau.vim index 69893f7399..a7422acf36 100644 --- a/runtime/indent/luau.vim +++ b/runtime/indent/luau.vim @@ -1,14 +1,153 @@ -" Vim filetype indent file -" Language: Luau -" Maintainer: None yet -" Last Change: 2023 Apr 30 +" Vim indent file +" Language: Luau +" Maintainer: Lopy (@lopi-py) +" Last Change: 2026 Jun 17 -" Only load this indent file when no other was loaded. +" only load this indent file when no other was loaded if exists("b:did_indent") - finish + finish +endif +let b:did_indent = 1 + +let s:cpo_save = &cpo +set cpo&vim + +setlocal indentexpr=GetLuauIndent() + +" make Vim call GetLuauIndent() when it finds a closing keyword or delimiter +setlocal indentkeys+=0=end,0=until,0=else,0=elseif,0=},0=),0=] + +setlocal autoindent + +let b:undo_indent = "setlocal autoindent< indentexpr< indentkeys<" + +" only define the function once +if exists("*GetLuauIndent") + let &cpo = s:cpo_save + unlet s:cpo_save + finish endif -" Luau is a superset of Lua -runtime! indent/lua.vim +function GetLuauIndent() abort + let ignorecase_save = &ignorecase + try + let &ignorecase = 0 + return s:GetLuauIndentIntern() + finally + let &ignorecase = ignorecase_save + endtry +endfunction +function s:InDeclareClass(lnum) abort + let save_cursor = getcurpos() + call cursor(a:lnum - 1, 1) + let lnum = search('^\s*\%(end\>\|declare\s\+class\>\)', 'bcnW') + call setpos('.', save_cursor) + return lnum > 0 && getline(lnum) =~# '^\s*declare\s\+class\>' +endfunction +function s:IsStringOrComment(lnum, col) abort + let name = synIDattr(synID(a:lnum, a:col, 1), "name") + return name =~# '^luau\%(Comment\|.*String\)' +endfunction + +function s:LineCommentStart(lnum) abort + let line = getline(a:lnum) + let midx = stridx(line, '--') + while midx != -1 + if !s:IsStringOrComment(a:lnum, midx + 1) + return midx + endif + let midx = stridx(line, '--', midx + 2) + endwhile + return -1 +endfunction + +function s:IsCode(lnum, col) abort + let comment = s:LineCommentStart(a:lnum) + return (comment == -1 || a:col <= comment) && !s:IsStringOrComment(a:lnum, a:col) +endfunction + +function s:HasBlockCloser(lnum) abort + let line = getline(a:lnum) + let midx = match(line, '\<\%(end\|until\)\>') + while midx != -1 + if s:IsCode(a:lnum, midx + 1) + return 1 + endif + let midx = match(line, '\<\%(end\|until\)\>', midx + 1) + endwhile + return 0 +endfunction + +function s:GetLuauIndentIntern() abort + " find a non-blank line above the current line + let prevlnum = prevnonblank(v:lnum - 1) + + " hit the start of the file, use zero indent + if prevlnum == 0 + return 0 + endif + + " add a 'shiftwidth' after lines that start a block + let ind = indent(prevlnum) + let prevline = getline(prevlnum) + let attr = '@\%(\h\w*\|\[[^]]*\]\)\s*' + let stmt = 'if\>\|for\>\|while\>\|repeat\>\|else\>\|elseif\>\|do\>\|then\>' + let class = 'class\>\s\+\h\|declare\s\+class\>\s\+\h' + let func = '\%(\%(public\s\+\)\=function\|local\s\+function\|const\s\+function\|type\s\+function\|return\s\+function\)' + let declare_func = '^\s*\%(' .. attr .. '\)*declare\s\+function\>' + let midx = -1 + if prevline =~# '^\s*\%(@\|\%(if\|for\|while\|repeat\|else\|elseif\|do\|then\|class\)\>\|declare\s\+class\>\)' + let midx = match(prevline, '^\s*\%(' .. attr .. '\)*\%(' .. stmt .. '\|' .. class .. '\)') + endif + if midx == -1 + if prevline =~# '^\s*\%(' .. attr .. '\)*declare\s\+extern\s\+type\>' + let midx = match(prevline, '\\s*\%(--.*\)\=$') + endif + if midx == -1 + let midx = match(prevline, '\%({\|(\|\[\)\s*\%(--\%([^[].*\)\?\)\?$') + if midx == -1 && stridx(prevline, 'function') != -1 && prevline !~# declare_func + let midx = match(prevline, '\<' .. func .. '\>\s*\%(\k\|[.:]\)\{-}\s*\%(<[^>]*>\s*\)\=(') + endif + endif + endif + + if midx == -1 && prevline =~ '^\s*)\s*\%(:.\+\)\=\s*\%(--.*\)\=$' + let save_cursor = getcurpos() + call cursor(prevlnum, match(prevline, ')') + 1) + let [par_lnum, par_col] = searchpairpos('(', '', ')', 'bnW') + call setpos('.', save_cursor) + if par_lnum > 0 + let funline = getline(par_lnum) + let funidx = match(funline, '\<' .. func .. '\>') + if funidx != -1 && funidx < par_col && funline !~# declare_func + let midx = match(prevline, ')') + endif + endif + endif + + if midx != -1 + " add 'shiftwidth' if this is not in a comment or string and the block + " does not close on the same line + if s:IsCode(prevlnum, midx + 1) && !s:HasBlockCloser(prevlnum) + \ && !(prevline =~# '^\s*\%(public\s\+\)\=function\>' && s:InDeclareClass(prevlnum)) + let ind = ind + shiftwidth() + endif + endif + + " subtract a 'shiftwidth' on end, else, elseif, until, '}', ')' and ']' + let midx = match(getline(v:lnum), '^\s*\%(end\>\|else\>\|elseif\>\|until\>\|}\|)\|\]\)') + if midx != -1 + if s:IsCode(v:lnum, midx + 1) + let ind = ind - shiftwidth() + endif + endif + + return ind +endfunction + +let &cpo = s:cpo_save +unlet s:cpo_save + +" vim: nowrap sw=2 sts=2 ts=8 noet: diff --git a/runtime/syntax/luau.vim b/runtime/syntax/luau.vim index 59eccac100..e5b3490ee4 100644 --- a/runtime/syntax/luau.vim +++ b/runtime/syntax/luau.vim @@ -1,15 +1,193 @@ " Vim syntax file -" Language: Luau -" Maintainer: None yet -" Last Change: 2023 Apr 30 +" Language: Luau +" Maintainer: Lopy (@lopi-py) +" Last Change: 2026 Jun 17 +" quit when a syntax file was already loaded if exists("b:current_syntax") finish endif -" Luau is a superset of lua -runtime! syntax/lua.vim +let s:cpo_save = &cpo +set cpo&vim + +syn case match +syn sync minlines=300 + +" comments +syn keyword luauTodo contained TODO FIXME +syn match luauDirective contained "--!\%(strict\|nonstrict\|nocheck\|nolint\%(Global\)\=\|native\|optimize\)\>.*" + +" strings +syn match luauSpecial contained #\\[[:digit:]]\{1,3}\|\\x[[:xdigit:]]\{2}\|\\u{[[:xdigit:]]\+}\|\\z\s*\|\\\n\|\\[^xu[:digit:]\r\n]# +syn region luauString2 matchgroup=luauStringDelimiter start="\[\z(=*\)\[" end="\]\z1\]" contains=@Spell +syn region luauString matchgroup=luauStringDelimiter start=+'+ end=+'+ skip=+\\\\\|\\'+ contains=luauSpecial,@Spell +syn region luauString matchgroup=luauStringDelimiter start=+"+ end=+"+ skip=+\\\\\|\\"+ contains=luauSpecial,@Spell +syn region luauInterpString matchgroup=luauStringDelimiter start=+`+ end=+`+ skip=+\\`+ contains=luauSpecial,luauInterp,@Spell +syn region luauInterp contained transparent matchgroup=luauInterpDelimiter start=+\\\@" +syn match luauNumber "\<0_*[bB][01_]*[01][01_]*\>" +syn match luauNumber "\<\d[[:digit:]_]*\%(\.[[:digit:]_]*\)\=\%([eE][-+]\=[[:digit:]_]*\d[[:digit:]_]*\)\=\>" +syn match luauNumber "\.\d[[:digit:]_]*\%([eE][-+]\=[[:digit:]_]*\d[[:digit:]_]*\)\=\>" + +" keywords +syn keyword luauStatement return local break end +syn keyword luauStatement do nextgroup=luauStatement skipwhite +syn keyword luauStatement contained continue +syn match luauStatement "^\s*\zscontinue\>\ze\s*\%(;\|end\>\|else\>\|elseif\>\|until\>\|--.*\|$\)" +syn match luauStatement ";\s*\zscontinue\>\ze\s*\%(;\|end\>\|else\>\|elseif\>\|until\>\|--.*\|$\)" +syn keyword luauFunction function nextgroup=luauFunctionName,luauGenericParams skipwhite +syn match luauStatement "^\s*\%(@\%(\h\w*\|\[[^]]*\]\)\s*\)*\zsconst\>\ze\s\+\%(function\>\|\h\)" +syn match luauStatement "^\s*\%(@\%(\h\w*\|\[[^]]*\]\)\s*\)*\zsdeclare\>\ze\s\+\%(function\>\|class\>\s\+\h\|extern\s\+type\>\|\h\w*\s*:\)" nextgroup=luauStatement,luauDeclareName,luauFunction skipwhite +syn match luauStatement contained "\\ze\s\+\h" nextgroup=luauTypedef skipwhite +syn match luauDeclareName contained "\h\w*\ze\s*:" nextgroup=luauTypeColon skipwhite +syn match luauStatement "\\ze\s\+type\>" +syn match luauStatement contained "\\ze\s\+type\>" +syn match luauStatement "\\ze\s\+\h" nextgroup=luauTypeName skipwhite +syn match luauStatement "\\ze\s*\%(--.*\)\=$" +syn match luauModifier "\\ze\s\+\%(function\|\h\)" +syn match luauTypeKeyword "\\ze\s\+\h" nextgroup=luauTypeFunction,luauTypedef skipwhite +syn keyword luauTypeFunction contained function nextgroup=luauFunctionName skipwhite +syn match luauFunctionName contained transparent "\h\w*\%([.:]\h\w*\)*" nextgroup=luauGenericParams skipwhite +syn keyword luauCond if elseif +syn keyword luauCond then else nextgroup=luauStatement skipwhite +syn keyword luauRepeat while for until in +syn keyword luauRepeat repeat nextgroup=luauStatement skipwhite +syn keyword luauOperator and or not + +" operators and punctuation +syn match luauSymbolOperator "[#+*/%^=<>~?-]\|\.\{3}\|\.\.=\=" +syn match luauSymbolOperator "->" nextgroup=luauSymbolOperator,luauConstant,luauTableType,luauTypePack,luauTypeName skipwhite +syn match luauSymbolOperator "[|&]" nextgroup=luauConstant,luauTableType,luauTypePack,luauTypeName skipwhite +syn match luauSymbolOperator "::" + +" tables +syn region luauTableBlock transparent matchgroup=luauTable start="{" end="}" contains=TOP,luauStatement + +syn match luauComment "--.*$" contains=luauTodo,luauDirective,@Spell +syn region luauComment matchgroup=luauCommentDelimiter start="--\[\z(=*\)\[" end="\]\z1\]" contains=luauTodo,@Spell + +" the first line may start with #! +syn match luauComment "\%^#!.*" + +" attributes +syn match luauAttribute "@\h\w*" +syn region luauAttributeBlock matchgroup=luauAttributeDelimiter start="@\[" end="\]" contains=luauAttributeName,luauAttributeTable,luauString,luauString2,luauInterpString,luauNumber,luauConstant +syn region luauAttributeTable contained transparent matchgroup=luauTable start="{" end="}" contains=luauAttributeTable,luauString,luauString2,luauInterpString,luauNumber,luauConstant,luauSymbolOperator +syn keyword luauAttributeName contained checked native deprecated + +" constants and types +syn keyword luauConstant nil true false +syn match luauConstant "\.\.\." +syn keyword luauSelf self +syn match luauSelf contained "(\s*\zsself\>\ze\s*:" +syn cluster luauTypeCommon contains=luauGenericParams,luauString,luauString2,luauInterpString,luauNumber,luauConstant,luauSymbolOperator +syn cluster luauTypeExpr contains=luauFunctionTypeParams,luauTableType,luauTypeParam,luauType,luauTypeKeyword,@luauTypeCommon +syn region luauGenericParams contained transparent matchgroup=luauSymbolOperator start="<" end=">" contains=@luauTypeExpr +syn region luauTypedefGenericParams contained transparent matchgroup=luauSymbolOperator start="<" end=">" contains=@luauTypeExpr nextgroup=luauTypeAliasAssign skipwhite +syn region luauExplicitTypeArgs transparent matchgroup=luauSymbolOperator start="<<" end=">>" contains=@luauTypeExpr +syn match luauTypeParam contained "\h\w*\%(\.\.\.\)\=" +syn match luauTypedef "\h\w*" contained nextgroup=luauTypedefGenericParams,luauTypeAliasAssign skipwhite +syn match luauTypeAliasAssign contained "=" nextgroup=luauSymbolOperator,luauConstant,luauTableType,luauTypeName skipwhite +syn region luauTableType contained transparent matchgroup=luauTable start="{" end="}" contains=luauFunctionTypeParams,luauTableType,luauSelf,luauTypeIndexer,luauTableElementType,luauTypeColon,luauTypeKeyword,@luauTypeCommon +syn match luauTypeIndexer "^\s*\[\s*" nextgroup=luauType skipwhite +syn match luauTypeIndexer contained transparent "\[\s*" nextgroup=luauType skipwhite +syn match luauType contained "\h\w*\%(\.\h\w*\)*\ze\s*\]\s*:" nextgroup=luauGenericParams skipwhite +syn match luauTableElementType contained "\h\w*\%(\.\h\w*\)*\ze\s*[\|$\)" nextgroup=luauGenericParams skipwhite +syn region luauFunctionTypeParams transparent start="(\ze\%([^()]*\))\s*->" end=")\ze\s*->" contains=luauFunctionTypeParam,luauTypeColon,luauSelf,@luauTypeCommon +syn match luauFunctionTypeParam contained "\h\w*\%(\.\h\w*\)*\%(\.\.\.\)\=\ze\s*?\=\s*[,)|&]" nextgroup=luauGenericParams skipwhite +syn match luauFunctionReturnStart transparent ")\s*:\s*" nextgroup=luauSymbolOperator,luauFunctionTypeParams,luauConstant,luauTableType,luauTypePack,luauTypeName skipwhite +syn region luauTypePack contained transparent start="(\%([^()]*)\s*->\)\@!" end=")" contains=luauFunctionTypeParams,luauTableType,luauTypeName,@luauTypeCommon +syn match luauTypeName contained "\h\w*\%(\.\h\w*\)*\%(\.\.\.\)\=" nextgroup=luauGenericParams skipwhite +syn match luauTypeKeyword "\<\%(read\|write\)\>\ze\s*\%(\[\|\h\+\s*:\)" nextgroup=luauTypeIndexer skipwhite + +" metamethods +syn keyword luauMetaMethod __index __newindex __mode __namecall __call __iter __len +syn keyword luauMetaMethod __eq __add __sub __mul __div __idiv __mod __pow __unm +syn keyword luauMetaMethod __lt __le __concat __type __metatable __tostring + +" methods +syn match luauMethodColon transparent ":\ze\s*\h\w*\s*\%(<<.\{-}>>\s*\|<[^>]*>\s*\)\=\%((\|{\|'\|\"\|\[\)" nextgroup=luauFunc skipwhite +syn match luauFunc contained "\h\w*\ze\s*\%(<<.\{-}>>\s*\|<[^>]*>\s*\)\=\%((\|{\|'\|\"\|\[\)" + +" global functions and values +syn keyword luauFunc assert error gcinfo getfenv getmetatable +syn keyword luauFunc ipairs loadstring newproxy next pairs pcall print +syn keyword luauFunc rawequal rawget rawlen rawset require select setfenv +syn keyword luauFunc setmetatable tonumber tostring unpack xpcall +syn keyword luauGlobal _G +syn keyword luauBuiltinConstant _VERSION +syn match luauFunc "\\ze\s*\%(<<.\{-}>>\s*\)\=(" +syn match luauFunc "\\ze\s*\%(<<.\{-}>>\s*\)\=(" + +" standard library members +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauBuiltinConstant /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauFunc /\/ +syn match luauBuiltinConstant /\/ +syn match luauFunc /\/ +syn match luauBuiltinConstant /\/ +syn match luauFunc /\/ + +" define the default highlighting +hi def link luauStatement Statement +hi def link luauRepeat Repeat +hi def link luauString String +hi def link luauString2 String +hi def link luauInterpString String +hi def link luauStringDelimiter luauString +hi def link luauInterpDelimiter Special +hi def link luauNumber Number +hi def link luauOperator Operator +hi def link luauSymbolOperator luauOperator +hi def link luauConstant Constant +hi def link luauCond Conditional +hi def link luauFunction Function +hi def link luauMetaMethod Function +hi def link luauTable Structure +hi def link luauComment Comment +hi def link luauCommentDelimiter luauComment +hi def link luauDirective PreProc +hi def link luauTodo Todo +hi def link luauSpecial SpecialChar +hi def link luauFunc Identifier +hi def link luauGlobal Identifier +hi def link luauBuiltinConstant Constant +hi def link luauSelf Identifier +hi def link luauModifier StorageClass +hi def link luauType Type +hi def link luauTypeAfterColon luauType +hi def link luauTableElementType luauType +hi def link luauTypeAliasAssign luauSymbolOperator +hi def link luauFunctionTypeParam luauType +hi def link luauTypeName luauType +hi def link luauTypedef Typedef +hi def link luauTypeParam Type +hi def link luauTypeKeyword Keyword +hi def link luauTypeFunction luauFunction +hi def link luauDeclareName Identifier +hi def link luauAttribute PreProc +hi def link luauAttributeName PreProc +hi def link luauAttributeDelimiter PreProc let b:current_syntax = "luau" +let &cpo = s:cpo_save +unlet s:cpo_save + " vim: nowrap sw=2 sts=2 ts=8 noet: