diff options
56 files changed, 3031 insertions, 579 deletions
diff --git a/.github/scripts/reviewers_add.js b/.github/scripts/reviewers_add.js index 08b4e85b74..73e1ece516 100644 --- a/.github/scripts/reviewers_add.js +++ b/.github/scripts/reviewers_add.js @@ -57,7 +57,6 @@ module.exports = async ({ github, context }) => { if (labels.includes("lsp")) { reviewers.add("MariaSolOs"); - reviewers.add("mfussenegger"); } if (labels.includes("netrw")) { diff --git a/cmake.deps/deps.txt b/cmake.deps/deps.txt index c42790ed5d..f7827fc47a 100644 --- a/cmake.deps/deps.txt +++ b/cmake.deps/deps.txt @@ -1,8 +1,8 @@ LIBUV_URL https://github.com/libuv/libuv/archive/v1.49.2.tar.gz LIBUV_SHA256 388ffcf3370d4cf7c4b3a3205504eea06c4be5f9e80d2ab32d19f8235accc1cf -LUAJIT_URL https://github.com/LuaJIT/LuaJIT/archive/fe71d0fb54ceadfb5b5f3b6baf29e486d97f6059.tar.gz -LUAJIT_SHA256 92325f209b21aaf0a67b099bc73cf9bbac5789a9749bdc3898d4a990abb4f36e +LUAJIT_URL https://github.com/LuaJIT/LuaJIT/archive/19878ec05c239ccaf5f3d17af27670a963e25b8b.tar.gz +LUAJIT_SHA256 e91acbe181cf6ffa3ef15870b8e620131002240ba24c5c779fd0131db021517f LUA_URL https://www.lua.org/ftp/lua-5.1.5.tar.gz LUA_SHA256 2640fc56a795f29d28ef15e13c34a47e223960b0240e8cb0a82d9b0738695333 diff --git a/runtime/autoload/gzip.vim b/runtime/autoload/gzip.vim index 26b1cda034..a6fbe2c336 100644 --- a/runtime/autoload/gzip.vim +++ b/runtime/autoload/gzip.vim @@ -1,6 +1,6 @@ " Vim autoload file for editing compressed files. " Maintainer: The Vim Project <https://github.com/vim/vim> -" Last Change: 2023 Aug 10 +" Last Change: 2024 Nov 25 " Former Maintainer: Bram Moolenaar <Bram@vim.org> " These functions are used by the gzip plugin. @@ -148,6 +148,9 @@ fun gzip#read(cmd) else let fname = escape(expand("%:r"), " \t\n*?[{`$\\%#'\"|!<") endif + if filereadable(undofile(expand("%"))) + exe "sil rundo " . fnameescape(undofile(expand("%"))) + endif if &verbose >= 8 execute "doau BufReadPost " . fname else diff --git a/runtime/autoload/spotbugs.vim b/runtime/autoload/spotbugs.vim new file mode 100644 index 0000000000..9161395794 --- /dev/null +++ b/runtime/autoload/spotbugs.vim @@ -0,0 +1,250 @@ +" Default pre- and post-compiler actions for SpotBugs +" Maintainers: @konfekt and @zzzyxwvut +" Last Change: 2024 Nov 27 + +let s:save_cpo = &cpo +set cpo&vim + +if v:version > 900 + + function! spotbugs#DeleteClassFiles() abort + if !exists('b:spotbugs_class_files') + return + endif + + for pathname in b:spotbugs_class_files + let classname = pathname =~# "^'.\\+\\.class'$" + \ ? eval(pathname) + \ : pathname + + if classname =~# '\.class$' && filereadable(classname) + " Since v9.0.0795. + let octad = readblob(classname, 0, 8) + + " Test the magic number and the major version number (45 for v1.0). + " Since v9.0.2027. + if len(octad) == 8 && octad[0 : 3] == 0zcafe.babe && + \ or((octad[6] << 8), octad[7]) >= 45 + echomsg printf('Deleting %s: %d', classname, delete(classname)) + endif + endif + endfor + + let b:spotbugs_class_files = [] + endfunction + +else + + function! s:DeleteClassFilesWithNewLineCodes(classname) abort + " The distribution of "0a"s in class file versions 2560 and 2570: + " + " 0zca.fe.ba.be.00.00.0a.00 0zca.fe.ba.be.00.00.0a.0a + " 0zca.fe.ba.be.00.0a.0a.00 0zca.fe.ba.be.00.0a.0a.0a + " 0zca.fe.ba.be.0a.00.0a.00 0zca.fe.ba.be.0a.00.0a.0a + " 0zca.fe.ba.be.0a.0a.0a.00 0zca.fe.ba.be.0a.0a.0a.0a + let numbers = [0, 0, 0, 0, 0, 0, 0, 0] + let offset = 0 + let lines = readfile(a:classname, 'b', 4) + + " Track NL byte counts to handle files of less than 8 bytes. + let nl_cnt = len(lines) + " Track non-NL byte counts for "0zca.fe.ba.be.0a.0a.0a.0a". + let non_nl_cnt = 0 + + for line in lines + for idx in range(strlen(line)) + " Remap NLs to Nuls. + let numbers[offset] = (line[idx] == "\n") ? 0 : char2nr(line[idx]) % 256 + let non_nl_cnt += 1 + let offset += 1 + + if offset > 7 + break + endif + endfor + + let nl_cnt -= 1 + + if offset > 7 || (nl_cnt < 1 && non_nl_cnt > 4) + break + endif + + " Reclaim NLs. + let numbers[offset] = 10 + let offset += 1 + + if offset > 7 + break + endif + endfor + + " Test the magic number and the major version number (45 for v1.0). + if offset > 7 && numbers[0] == 0xca && numbers[1] == 0xfe && + \ numbers[2] == 0xba && numbers[3] == 0xbe && + \ (numbers[6] * 256 + numbers[7]) >= 45 + echomsg printf('Deleting %s: %d', a:classname, delete(a:classname)) + endif + endfunction + + function! spotbugs#DeleteClassFiles() abort + if !exists('b:spotbugs_class_files') + return + endif + + let encoding = &encoding + + try + set encoding=latin1 + + for pathname in b:spotbugs_class_files + let classname = pathname =~# "^'.\\+\\.class'$" + \ ? eval(pathname) + \ : pathname + + if classname =~# '\.class$' && filereadable(classname) + let line = get(readfile(classname, 'b', 1), 0, '') + let length = strlen(line) + + " Test the magic number and the major version number (45 for v1.0). + if length > 3 && line[0 : 3] == "\xca\xfe\xba\xbe" + if length > 7 && ((line[6] == "\n" ? 0 : char2nr(line[6]) % 256) * 256 + + \ (line[7] == "\n" ? 0 : char2nr(line[7]) % 256)) >= 45 + echomsg printf('Deleting %s: %d', classname, delete(classname)) + else + call s:DeleteClassFilesWithNewLineCodes(classname) + endif + endif + endif + endfor + finally + let &encoding = encoding + endtry + + let b:spotbugs_class_files = [] + endfunction + +endif + +function! spotbugs#DefaultPostCompilerAction() abort + " Since v7.4.191. + make %:S +endfunction + +" Look for "spotbugs#compiler" in "ftplugin/java.vim". +let s:compiler = exists('spotbugs#compiler') ? spotbugs#compiler : '' +let s:readable = filereadable($VIMRUNTIME . '/compiler/' . s:compiler . '.vim') + +if s:readable && s:compiler ==# 'maven' && executable('mvn') + + function! spotbugs#DefaultPreCompilerAction() abort + call spotbugs#DeleteClassFiles() + compiler maven + make compile + endfunction + + function! spotbugs#DefaultPreCompilerTestAction() abort + call spotbugs#DeleteClassFiles() + compiler maven + make test-compile + endfunction + + function! spotbugs#DefaultProperties() abort + return { + \ 'PreCompilerAction': + \ function('spotbugs#DefaultPreCompilerAction'), + \ 'PreCompilerTestAction': + \ function('spotbugs#DefaultPreCompilerTestAction'), + \ 'PostCompilerAction': + \ function('spotbugs#DefaultPostCompilerAction'), + \ 'sourceDirPath': 'src/main/java', + \ 'classDirPath': 'target/classes', + \ 'testSourceDirPath': 'src/test/java', + \ 'testClassDirPath': 'target/test-classes', + \ } + endfunction + + unlet s:readable s:compiler +elseif s:readable && s:compiler ==# 'ant' && executable('ant') + + function! spotbugs#DefaultPreCompilerAction() abort + call spotbugs#DeleteClassFiles() + compiler ant + make compile + endfunction + + function! spotbugs#DefaultPreCompilerTestAction() abort + call spotbugs#DeleteClassFiles() + compiler ant + make compile-test + endfunction + + function! spotbugs#DefaultProperties() abort + return { + \ 'PreCompilerAction': + \ function('spotbugs#DefaultPreCompilerAction'), + \ 'PreCompilerTestAction': + \ function('spotbugs#DefaultPreCompilerTestAction'), + \ 'PostCompilerAction': + \ function('spotbugs#DefaultPostCompilerAction'), + \ 'sourceDirPath': 'src', + \ 'classDirPath': 'build/classes', + \ 'testSourceDirPath': 'test', + \ 'testClassDirPath': 'build/test/classes', + \ } + endfunction + + unlet s:readable s:compiler +elseif s:readable && s:compiler ==# 'javac' && executable('javac') + + function! spotbugs#DefaultPreCompilerAction() abort + call spotbugs#DeleteClassFiles() + compiler javac + + if get(b:, 'javac_makeprg_params', get(g:, 'javac_makeprg_params', '')) =~ '\s@\S' + " Read options and filenames from @options [@sources ...]. + make + else + " Let Javac figure out what files to compile. + execute 'make ' . join(map(filter(copy(v:argv), + \ "v:val =~# '\\.java\\=$'"), + \ 'shellescape(v:val)'), ' ') + endif + endfunction + + function! spotbugs#DefaultPreCompilerTestAction() abort + call spotbugs#DefaultPreCompilerAction() + endfunction + + function! spotbugs#DefaultProperties() abort + return { + \ 'PreCompilerAction': + \ function('spotbugs#DefaultPreCompilerAction'), + \ 'PreCompilerTestAction': + \ function('spotbugs#DefaultPreCompilerTestAction'), + \ 'PostCompilerAction': + \ function('spotbugs#DefaultPostCompilerAction'), + \ } + endfunction + + unlet s:readable s:compiler +else + + function! spotbugs#DefaultPreCompilerAction() abort + echomsg printf('Not supported: "%s"', s:compiler) + endfunction + + function! spotbugs#DefaultPreCompilerTestAction() abort + call spotbugs#DefaultPreCompilerAction() + endfunction + + function! spotbugs#DefaultProperties() abort + return {} + endfunction + + unlet s:readable +endif + +let &cpo = s:save_cpo +unlet s:save_cpo + +" vim: set foldmethod=syntax shiftwidth=2 expandtab: diff --git a/runtime/compiler/eslint.vim b/runtime/compiler/eslint.vim index db7a665991..0414817900 100644 --- a/runtime/compiler/eslint.vim +++ b/runtime/compiler/eslint.vim @@ -1,13 +1,12 @@ " Vim compiler file " Compiler: ESLint for JavaScript " Maintainer: Romain Lafourcade <romainlafourcade@gmail.com> -" Last Change: 2020 August 20 -" 2024 Apr 03 by The Vim Project (removed :CompilerSet definition) +" Last Change: 2024 Nov 30 if exists("current_compiler") finish endif let current_compiler = "eslint" -CompilerSet makeprg=npx\ eslint\ --format\ compact -CompilerSet errorformat=%f:\ line\ %l\\,\ col\ %c\\,\ %m,%-G%.%# +CompilerSet makeprg=npx\ eslint\ --format\ stylish +CompilerSet errorformat=%-P%f,\%\\s%#%l:%c\ %#\ %trror\ \ %m,\%\\s%#%l:%c\ %#\ %tarning\ \ %m,\%-Q,\%-G%.%#, diff --git a/runtime/compiler/javac.vim b/runtime/compiler/javac.vim index 9bd4cdf270..53cd772ed8 100644 --- a/runtime/compiler/javac.vim +++ b/runtime/compiler/javac.vim @@ -1,7 +1,7 @@ " Vim compiler file " Compiler: Java Development Kit Compiler " Maintainer: Doug Kearns <dougkearns@gmail.com> -" Last Change: 2024 Jun 14 +" Last Change: 2024 Nov 19 (enable local javac_makeprg_params) if exists("current_compiler") finish @@ -11,11 +11,7 @@ let current_compiler = "javac" let s:cpo_save = &cpo set cpo&vim -if exists("g:javac_makeprg_params") - execute $'CompilerSet makeprg=javac\ {escape(g:javac_makeprg_params, ' \|"')}' -else - CompilerSet makeprg=javac -endif +execute $'CompilerSet makeprg=javac\ {escape(get(b:, 'javac_makeprg_params', get(g:, 'javac_makeprg_params', '')), ' \|"')}' CompilerSet errorformat=%E%f:%l:\ error:\ %m, \%W%f:%l:\ warning:\ %m, diff --git a/runtime/compiler/maven.vim b/runtime/compiler/maven.vim index ef8d8a6fb2..72e74e301d 100644 --- a/runtime/compiler/maven.vim +++ b/runtime/compiler/maven.vim @@ -14,7 +14,7 @@ if exists("current_compiler") endif let current_compiler = "maven" -CompilerSet makeprg=mvn\ --batch-mode +execute $'CompilerSet makeprg=mvn\ --batch-mode\ {escape(get(b:, 'maven_makeprg_params', get(g:, 'maven_makeprg_params', '')), ' \|"')}' " Error message for POM CompilerSet errorformat=[FATAL]\ Non-parseable\ POM\ %f:\ %m%\\s%\\+@%.%#line\ %l\\,\ column\ %c%.%#, diff --git a/runtime/compiler/pytest.vim b/runtime/compiler/pytest.vim new file mode 100644 index 0000000000..7fc189932c --- /dev/null +++ b/runtime/compiler/pytest.vim @@ -0,0 +1,103 @@ +" Vim compiler file +" Compiler: Pytest (Python testing framework) +" Maintainer: @Konfekt and @mgedmin +" Last Change: 2024 Nov 28 + +if exists("current_compiler") | finish | endif +let current_compiler = "pytest" + +let s:cpo_save = &cpo +set cpo&vim + +" CompilerSet makeprg=pytest +if has('unix') + execute $'CompilerSet makeprg=/usr/bin/env\ PYTHONWARNINGS=ignore\ pytest\ {escape(get(b:, 'pytest_makeprg_params', get(g:, 'pytest_makeprg_params', '--tb=short --quiet')), ' \|"')}' +elseif has('win32') + execute $'CompilerSet makeprg=set\ PYTHONWARNINGS=ignore\ &&\ pytest\ {escape(get(b:, 'pytest_makeprg_params', get(g:, 'pytest_makeprg_params', '--tb=short --quiet')), ' \|"')}' +else + CompilerSet makeprg=pytest\ --tb=short\ --quiet + execute $'CompilerSet makeprg=pytest\ {escape(get(b:, 'pytest_makeprg_params', get(g:, 'pytest_makeprg_params', '--tb=short --quiet')), ' \|"')}' +endif + +" Pytest syntax errors {{{2 + +" Reset error format so that sourcing .vimrc again and again doesn't grow it +" without bounds +setlocal errorformat& + +" For the record, the default errorformat is this: +" +" %*[^"]"%f"%*\D%l: %m +" "%f"%*\D%l: %m +" %-G%f:%l: (Each undeclared identifier is reported only once +" %-G%f:%l: for each function it appears in.) +" %-GIn file included from %f:%l:%c: +" %-GIn file included from %f:%l:%c\, +" %-GIn file included from %f:%l:%c +" %-GIn file included from %f:%l +" %-G%*[ ]from %f:%l:%c +" %-G%*[ ]from %f:%l: +" %-G%*[ ]from %f:%l\, +" %-G%*[ ]from %f:%l +" %f:%l:%c:%m +" %f(%l):%m +" %f:%l:%m +" "%f"\, line %l%*\D%c%*[^ ] %m +" %D%*\a[%*\d]: Entering directory %*[`']%f' +" %X%*\a[%*\d]: Leaving directory %*[`']%f' +" %D%*\a: Entering directory %*[`']%f' +" %X%*\a: Leaving directory %*[`']%f' +" %DMaking %*\a in %f +" %f|%l| %m +" +" and sometimes it misfires, so let's fix it up a bit +" (TBH I don't even know what compiler produces filename(lineno) so why even +" have it?) +setlocal errorformat-=%f(%l):%m + +" Sometimes Vim gets confused about ISO-8601 timestamps and thinks they're +" filenames; this is a big hammer that ignores anything filename-like on lines +" that start with at least two spaces, possibly preceded by a number and +" optional punctuation +setlocal errorformat^=%+G%\\d%#%.%\\=\ \ %.%# + +" Similar, but when the entire line starts with a date +setlocal errorformat^=%+G\\d\\d\\d\\d-\\d\\d-\\d\\d\ \\d\\d:\\d\\d%.%# + +" make: *** [Makefile:14: target] Error 1 +setlocal errorformat^=%+Gmake:\ ***\ %.%# + +" FAILED tests.py::test_with_params[YYYY-MM-DD:HH:MM:SS] - Exception: bla bla +setlocal errorformat^=%+GFAILED\ %.%# + +" AssertionError: assert ...YYYY-MM-DD:HH:MM:SS... +setlocal errorformat^=%+GAssertionError:\ %.%# + +" --- /path/to/file:before YYYY-MM-DD HH:MM:SS.ssssss +setlocal errorformat^=---%f:%m + +" +++ /path/to/file:before YYYY-MM-DD HH:MM:SS.ssssss +setlocal errorformat^=+++%f:%m + +" Sometimes pytest prepends an 'E' marker at the beginning of a traceback line +setlocal errorformat+=E\ %#File\ \"%f\"\\,\ line\ %l%.%# + +" Python tracebacks (unittest + doctest output) {{{2 + +" This collapses the entire traceback into just the last file+lineno, +" which is convenient when you want to jump to the line that failed (and not +" the top-level entry point), but it makes it impossible to see the full +" traceback, which sucks. +""setlocal errorformat+= +"" \File\ \"%f\"\\,\ line\ %l%.%#, +"" \%C\ %.%#, +"" \%-A\ \ File\ \"unittest%.py\"\\,\ line\ %.%#, +"" \%-A\ \ File\ \"%f\"\\,\ line\ 0%.%#, +"" \%A\ \ File\ \"%f\"\\,\ line\ %l%.%#, +"" \%Z%[%^\ ]%\\@=%m +setlocal errorformat+=File\ \"%f\"\\,\ line\ %l\\,%#%m + +exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"') + +let &cpo = s:cpo_save +unlet s:cpo_save diff --git a/runtime/compiler/spotbugs.vim b/runtime/compiler/spotbugs.vim new file mode 100644 index 0000000000..72a5084976 --- /dev/null +++ b/runtime/compiler/spotbugs.vim @@ -0,0 +1,189 @@ +" Vim compiler file +" Compiler: Spotbugs (Java static checker; needs javac compiled classes) +" Maintainer: @konfekt and @zzzyxwvut +" Last Change: 2024 Nov 27 + +if exists('g:current_compiler') || bufname() !~# '\.java\=$' || wordcount().chars < 9 + finish +endif + +let s:cpo_save = &cpo +set cpo&vim + +" Unfortunately Spotbugs does not output absolute paths, so you need to +" pass the directory of the files being checked as `-sourcepath` parameter. +" The regex, auxpath and glob try to include all dependent classes of the +" current buffer. See https://github.com/spotbugs/spotbugs/issues/856 + +" FIXME: When "search()" is used with the "e" flag, it makes no _further_ +" progress after claiming an EOL match (i.e. "\_" or "\n", but not "$"). +" XXX: Omit anonymous class declarations +let s:keywords = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\|package\)\%(\s\|$\)' +let s:type_names = '\C\<\%(\.\@1<!class\|@\=interface\|enum\|record\)\s*\(\K\k*\)\>' +" Capture ";" for counting a class file directory (see s:package_dir_heads below) +let s:package_names = '\C\<package\s*\(\K\%(\k*\.\=\)\+;\)' +let s:package = '' + +if has('syntax') && exists('g:syntax_on') && exists('b:current_syntax') && + \ b:current_syntax == 'java' && hlexists('javaClassDecl') + + function! s:GetDeclaredTypeNames() abort + if bufname() =~# '\<\%(module\|package\)-info\.java\=$' + return [expand('%:t:r')] + endif + defer execute('silent! normal! g``') + call cursor(1, 1) + let type_names = [] + let lnum = search(s:keywords, 'eW') + while lnum > 0 + let name_attr = synIDattr(synID(lnum, (col('.') - 1), 0), 'name') + if name_attr ==# 'javaClassDecl' + let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:type_names) + if !empty(tokens) | call add(type_names, tokens[1]) | endif + elseif name_attr ==# 'javaExternal' + let tokens = matchlist(getline(lnum)..getline(lnum + 1), s:package_names) + if !empty(tokens) | let s:package = tokens[1] | endif + endif + let lnum = search(s:keywords, 'eW') + endwhile + return type_names + endfunction + +else + function! s:GetDeclaredTypeNames() abort + if bufname() =~# '\<\%(module\|package\)-info\.java\=$' + return [expand('%:t:r')] + endif + " Undo the unsetting of &hls, see below + if &hls + defer execute('set hls') + endif + " Possibly restore the current values for registers '"' and "y", see below + defer call('setreg', ['"', getreg('"'), getregtype('"')]) + defer call('setreg', ['y', getreg('y'), getregtype('y')]) + defer execute('silent bwipeout') + " Copy buffer contents for modification + silent %y y + new + " Apply ":help scratch-buffer" effects and match "$" in Java (generated) + " type names (see s:type_names) + setlocal iskeyword+=$ buftype=nofile bufhidden=hide noswapfile nohls + 0put y + " Discard text blocks and strings + silent keeppatterns %s/\\\@<!"""\_.\{-}\\\@<!"""\|\\"//ge + silent keeppatterns %s/".*"//ge + " Discard comments + silent keeppatterns %s/\/\/.\+$//ge + silent keeppatterns %s/\/\*\_.\{-}\*\///ge + call cursor(1, 1) + let type_names = [] + let lnum = search(s:keywords, 'eW') + while lnum > 0 + let line = getline(lnum) + if line =~# '\<package\>' + let tokens = matchlist(line..getline(lnum + 1), s:package_names) + if !empty(tokens) | let s:package = tokens[1] | endif + else + let tokens = matchlist(line..getline(lnum + 1), s:type_names) + if !empty(tokens) | call add(type_names, tokens[1]) | endif + endif + let lnum = search(s:keywords, 'eW') + endwhile + return type_names + endfunction +endif + +if has('win32') + + function! s:GlobClassFiles(src_type_name) abort + return glob(a:src_type_name..'$*.class', 1, 1) + endfunction + +else + function! s:GlobClassFiles(src_type_name) abort + return glob(a:src_type_name..'\$*.class', 1, 1) + endfunction +endif + +if exists('g:spotbugs_properties') && + \ (has_key(g:spotbugs_properties, 'sourceDirPath') && + \ has_key(g:spotbugs_properties, 'classDirPath')) || + \ (has_key(g:spotbugs_properties, 'testSourceDirPath') && + \ has_key(g:spotbugs_properties, 'testClassDirPath')) + +function! s:FindClassFiles(src_type_name) abort + let class_files = [] + " Match pairwise the components of source and class pathnames + for [src_dir, bin_dir] in filter([ + \ [get(g:spotbugs_properties, 'sourceDirPath', ''), + \ get(g:spotbugs_properties, 'classDirPath', '')], + \ [get(g:spotbugs_properties, 'testSourceDirPath', ''), + \ get(g:spotbugs_properties, 'testClassDirPath', '')]], + \ '!(empty(v:val[0]) || empty(v:val[1]))') + " Since only the rightmost "src" is sought, while there can be any number of + " such filenames, no "fnamemodify(a:src_type_name, ':p:s?src?bin?')" is used + let tail_idx = strridx(a:src_type_name, src_dir) + " No such directory or no such inner type (i.e. without "$") + if tail_idx < 0 | continue | endif + " Substitute "bin_dir" for the rightmost "src_dir" + let candidate_type_name = strpart(a:src_type_name, 0, tail_idx).. + \ bin_dir.. + \ strpart(a:src_type_name, (tail_idx + strlen(src_dir))) + for candidate in insert(s:GlobClassFiles(candidate_type_name), + \ candidate_type_name..'.class') + if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif + endfor + if !empty(class_files) | break | endif + endfor + return class_files +endfunction + +else +function! s:FindClassFiles(src_type_name) abort + let class_files = [] + for candidate in insert(s:GlobClassFiles(a:src_type_name), + \ a:src_type_name..'.class') + if filereadable(candidate) | call add(class_files, shellescape(candidate)) | endif + endfor + return class_files +endfunction +endif + +function! s:CollectClassFiles() abort + " Get a platform-independent pathname prefix, cf. "expand('%:p:h')..'/'" + let pathname = expand('%:p') + let tail_idx = strridx(pathname, expand('%:t')) + let src_pathname = strpart(pathname, 0, tail_idx) + let all_class_files = [] + " Get all type names in the current buffer and let the filename globbing + " discover inner type names from arbitrary type names + for type_name in s:GetDeclaredTypeNames() + call extend(all_class_files, s:FindClassFiles(src_pathname..type_name)) + endfor + return all_class_files +endfunction + +" Expose class files for removal etc. +let b:spotbugs_class_files = s:CollectClassFiles() +let s:package_dir_heads = repeat(':h', (1 + strlen(substitute(s:package, '[^.;]', '', 'g')))) +let g:current_compiler = 'spotbugs' +" CompilerSet makeprg=spotbugs +let &l:makeprg = 'spotbugs'..(has('win32') ? '.bat' : '')..' '.. + \ get(b:, 'spotbugs_makeprg_params', get(g:, 'spotbugs_makeprg_params', '-workHard -experimental')).. + \ ' -textui -emacs -auxclasspath %:p'..s:package_dir_heads..':S -sourcepath %:p'..s:package_dir_heads..':S '.. + \ join(b:spotbugs_class_files, ' ') +" Emacs expects doubled line numbers +setlocal errorformat=%f:%l:%*[0-9]\ %m,%f:-%*[0-9]:-%*[0-9]\ %m + +" " This compiler is meant to be used for a single buffer only +" exe 'CompilerSet makeprg='..escape(&l:makeprg, ' \|"') +" exe 'CompilerSet errorformat='..escape(&l:errorformat, ' \|"') + +delfunction s:CollectClassFiles +delfunction s:FindClassFiles +delfunction s:GlobClassFiles +delfunction s:GetDeclaredTypeNames +let &cpo = s:cpo_save +unlet s:package_dir_heads s:package s:package_names s:type_names s:keywords s:cpo_save + +" vim: set foldmethod=syntax shiftwidth=2 expandtab: diff --git a/runtime/doc/diagnostic.txt b/runtime/doc/diagnostic.txt index 9ccc3102b6..eaa3681caa 100644 --- a/runtime/doc/diagnostic.txt +++ b/runtime/doc/diagnostic.txt @@ -163,6 +163,33 @@ show a sign for the highest severity diagnostic on a given line: >lua } < + *diagnostic-loclist-example* +Whenever the |location-list| is opened, the following `show` handler will show +the most recent diagnostics: >lua + + vim.diagnostic.handlers.loclist = { + show = function(_, _, _, opts) + -- Generally don't want it to open on every update + opts.loclist.open = opts.loclist.open or false + local winid = vim.api.nvim_get_current_win() + vim.diagnostic.setloclist(opts.loclist) + vim.api.nvim_set_current_win(winid) + end + } +< + +The handler accepts the same options as |vim.diagnostic.setloclist()| and can be +configured using |vim.diagnostic.config()|: >lua + + -- Open the location list on every diagnostic change (warnings/errors only). + vim.diagnostic.config({ + loclist = { + open = true, + severity = { min = vim.diagnostic.severity.WARN }, + } + }) +< + ============================================================================== HIGHLIGHTS *diagnostic-highlights* diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 350edc068f..f7157df0f2 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -204,6 +204,7 @@ won't run if your server doesn't support them. - `'textDocument/diagnostic'` - `'textDocument/documentHighlight'` - `'textDocument/documentSymbol'` +- `'textDocument/foldingRange'` - `'textDocument/formatting'` - `'textDocument/hover'` - `'textDocument/implementation'` @@ -697,6 +698,35 @@ commands *vim.lsp.commands* The second argument is the `ctx` of |lsp-handler| +foldclose({kind}, {winid}) *vim.lsp.foldclose()* + Close all {kind} of folds in the the window with {winid}. + + To automatically fold imports when opening a file, you can use an autocmd: >lua + vim.api.nvim_create_autocmd('LspNotify', { + callback = function(args) + if args.data.method == 'textDocument/didOpen' then + vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf)) + end + end, + }) +< + + Parameters: ~ + • {kind} (`lsp.FoldingRangeKind`) Kind to close, one of "comment", + "imports" or "region". + • {winid} (`integer?`) Defaults to the current window. + +foldexpr({lnum}) *vim.lsp.foldexpr()* + Provides an interface between the built-in client and a `foldexpr` + function. + + Parameters: ~ + • {lnum} (`integer`) line number + +foldtext() *vim.lsp.foldtext()* + Provides a `foldtext` function that shows the `collapsedText` retrieved, + defaults to the first folded line if `collapsedText` is not provided. + formatexpr({opts}) *vim.lsp.formatexpr()* Provides an interface between the built-in client and a `formatexpr` function. @@ -975,7 +1005,7 @@ Lua module: vim.lsp.client *lsp-client* • {dynamic_capabilities} (`lsp.DynamicCapabilities`) • {request} (`fun(self: vim.lsp.Client, method: string, params: table?, handler: lsp.Handler?, bufnr: integer?): boolean, integer?`) See |Client:request()|. - • {request_sync} (`fun(self: vim.lsp.Client, method: string, params: table, timeout_ms: integer?, bufnr: integer): {err: lsp.ResponseError?, result:any}?, string?`) + • {request_sync} (`fun(self: vim.lsp.Client, method: string, params: table, timeout_ms: integer?, bufnr: integer?): {err: lsp.ResponseError?, result:any}?, string?`) See |Client:request_sync()|. • {notify} (`fun(self: vim.lsp.Client, method: string, params: table?): boolean`) See |Client:notify()|. @@ -1179,7 +1209,7 @@ Client:request({method}, {params}, {handler}, {bufnr}) • {method} (`string`) LSP method name. • {params} (`table?`) LSP request params. • {handler} (`lsp.Handler?`) Response |lsp-handler| for this method. - • {bufnr} (`integer?`) Buffer handle. 0 for current (default). + • {bufnr} (`integer?`) (default: 0) Buffer handle, or 0 for current. Return (multiple): ~ (`boolean`) status indicates whether the request was successful. If it @@ -1201,7 +1231,8 @@ Client:request_sync({method}, {params}, {timeout_ms}, {bufnr}) • {params} (`table`) LSP request params. • {timeout_ms} (`integer?`) Maximum time in milliseconds to wait for a result. Defaults to 1000 - • {bufnr} (`integer`) Buffer handle (0 for current). + • {bufnr} (`integer?`) (default: 0) Buffer handle, or 0 for + current. Return (multiple): ~ (`{err: lsp.ResponseError?, result:any}?`) `result` and `err` from the @@ -1385,7 +1416,7 @@ format({opts}) *vim.lsp.buf.format()* predicate are included. Example: >lua -- Never request typescript-language-server for formatting vim.lsp.buf.format { - filter = function(client) return client.name ~= "tsserver" end + filter = function(client) return client.name ~= "ts_ls" end } < • {async}? (`boolean`, default: false) If true the method @@ -1448,7 +1479,7 @@ references({context}, {opts}) *vim.lsp.buf.references()* window. Parameters: ~ - • {context} (`table?`) Context for the request + • {context} (`lsp.ReferenceContext?`) Context for the request • {opts} (`vim.lsp.ListOpts?`) See |vim.lsp.ListOpts|. See also: ~ diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 4d4a51872a..c3dddaf888 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -2485,22 +2485,24 @@ vim.validate({name}, {value}, {validator}, {optional}, {message}) ============================================================================== Lua module: vim.loader *vim.loader* -vim.loader.disable() *vim.loader.disable()* +vim.loader.enable({enable}) *vim.loader.enable()* WARNING: This feature is experimental/unstable. - Disables the experimental Lua module loader: - • removes the loaders - • adds the default Nvim loader - -vim.loader.enable() *vim.loader.enable()* - WARNING: This feature is experimental/unstable. + Enables or disables the experimental Lua module loader: - Enables the experimental Lua module loader: - • overrides loadfile + Enable (`enable=true`): + • overrides |loadfile()| • adds the Lua loader using the byte-compilation cache • adds the libs loader • removes the default Nvim loader + Disable (`enable=false`): + • removes the loaders + • adds the default Nvim loader + + Parameters: ~ + • {enable} (`boolean?`) true/nil to enable, false to disable + vim.loader.find({modname}, {opts}) *vim.loader.find()* WARNING: This feature is experimental/unstable. @@ -2924,6 +2926,17 @@ vim.keymap.set({mode}, {lhs}, {rhs}, {opts}) *vim.keymap.set()* ============================================================================== Lua module: vim.fs *vim.fs* + + *vim.fs.exists()* +Use |uv.fs_stat()| to check a file's type, and whether it exists. + +Example: >lua + if vim.uv.fs_stat(file) then + vim.print("file exists") + end +< + + vim.fs.basename({file}) *vim.fs.basename()* Return the basename of the given path diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 58ab7ef44c..ad3f2c0a6a 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -11,13 +11,17 @@ For changes in the previous release, see |news-0.10|. Type |gO| to see the table of contents. ============================================================================== -BREAKING CHANGES IN HEAD *news-breaking-dev* +BREAKING CHANGES IN HEAD OR EXPERIMENTAL *news-breaking-dev* ====== Remove this section before release. ====== The following changes to UNRELEASED features were made during the development cycle (Nvim HEAD, the "master" branch). +EXPERIMENTS + +• Removed `vim.loader.disable()`. Use `vim.loader.enable(false)` instead. + OPTIONS • 'jumpoptions' flag "unload" has been renamed to "clean". @@ -226,6 +230,8 @@ LSP • |vim.lsp.buf.hover()| now highlights hover ranges using the |hl-LspReferenceTarget| highlight group. • Functions in |vim.lsp.Client| can now be called as methods. +• Implemented LSP folding: |vim.lsp.foldexpr()| + https://microsoft.github.io/language-server-protocol/specification/#textDocument_foldingRange LUA @@ -280,6 +286,8 @@ TUI :lua =vim.api.nvim_get_chan_info(vim.api.nvim_list_uis()[1].chan) • |log| messages written by the builtin UI client (TUI, |--remote-ui|) are now prefixed with "ui" instead of "?". +• The TUI will re-query the terminal's background color when a theme update + notification is received and Nvim will update 'background' accordingly. UI diff --git a/runtime/doc/options.txt b/runtime/doc/options.txt index ea56633c77..c5f21c64a2 100644 --- a/runtime/doc/options.txt +++ b/runtime/doc/options.txt @@ -5917,6 +5917,7 @@ A jump table for the options with a short description can be found at |Q_op|. All fields except the {item} are optional. A single percent sign can be given as "%%". + *stl-%!* When the option starts with "%!" then it is used as an expression, evaluated and the result is used as the option value. Example: >vim set statusline=%!MyStatusLine() diff --git a/runtime/doc/quickfix.txt b/runtime/doc/quickfix.txt index b3399b2766..a291c0277d 100644 --- a/runtime/doc/quickfix.txt +++ b/runtime/doc/quickfix.txt @@ -1317,9 +1317,117 @@ g:compiler_gcc_ignore_unmatched_lines JAVAC *compiler-javac* Commonly used compiler options can be added to 'makeprg' by setting the -g:javac_makeprg_params variable. For example: > +b/g:javac_makeprg_params variable. For example: > + let g:javac_makeprg_params = "-Xlint:all -encoding utf-8" -< + +MAVEN *compiler-maven* + +Commonly used compiler options can be added to 'makeprg' by setting the +b/g:maven_makeprg_params variable. For example: > + + let g:maven_makeprg_params = "-DskipTests -U -X" + +SPOTBUGS *compiler-spotbugs* + +SpotBugs is a static analysis tool that can be used to find bugs in Java. +It scans the Java bytecode of all classes in the currently open buffer. +(Therefore, `:compiler! spotbugs` is not supported.) + +Commonly used compiler options can be added to 'makeprg' by setting the +"b:" or "g:spotbugs_makeprg_params" variable. For example: > + + let b:spotbugs_makeprg_params = "-longBugCodes -effort:max -low" + +The global default is "-workHard -experimental". + +By default, the class files are searched in the directory where the source +files are placed. However, typical Java projects use distinct directories +for source files and class files. To make both known to SpotBugs, assign +their paths (distinct and relative to their common root directory) to the +following properties (using the example of a common Maven project): > + + let g:spotbugs_properties = { + \ 'sourceDirPath': 'src/main/java', + \ 'classDirPath': 'target/classes', + \ 'testSourceDirPath': 'src/test/java', + \ 'testClassDirPath': 'target/test-classes', + \ } + +Note that values for the path keys describe only for SpotBugs where to look +for files; refer to the documentation for particular compiler plugins for more +information. + +The default pre- and post-compiler actions are provided for Ant, Maven, and +Javac compiler plugins and can be selected by assigning the name of a compiler +plugin to the "compiler" key: > + + let g:spotbugs_properties = { + \ 'compiler': 'maven', + \ } + +This single setting is essentially equivalent to all the settings below, with +the exception made for the "PreCompilerAction" and "PreCompilerTestAction" +values: their listed |Funcref|s will obtain no-op implementations whereas the +implicit Funcrefs of the "compiler" key will obtain the requested defaults if +available. > + + let g:spotbugs_properties = { + \ 'PreCompilerAction': + \ function('spotbugs#DefaultPreCompilerAction'), + \ 'PreCompilerTestAction': + \ function('spotbugs#DefaultPreCompilerTestAction'), + \ 'PostCompilerAction': + \ function('spotbugs#DefaultPostCompilerAction'), + \ 'sourceDirPath': 'src/main/java', + \ 'classDirPath': 'target/classes', + \ 'testSourceDirPath': 'src/test/java', + \ 'testClassDirPath': 'target/test-classes', + \ } + +With default actions, the compiler of choice will attempt to rebuild the class +files for the buffer (and possibly for the whole project) as soon as a Java +syntax file is loaded; then, `spotbugs` will attempt to analyze the quality of +the compilation unit of the buffer. + +When default actions are not suited to a desired workflow, consider writing +arbitrary functions yourself and matching their |Funcref|s to the supported +keys: "PreCompilerAction", "PreCompilerTestAction", and "PostCompilerAction". + +The next example re-implements the default pre-compiler actions for a Maven +project and requests other default Maven settings with the "compiler" entry: > + + function! MavenPreCompilerAction() abort + call spotbugs#DeleteClassFiles() + compiler maven + make compile + endfunction + + function! MavenPreCompilerTestAction() abort + call spotbugs#DeleteClassFiles() + compiler maven + make test-compile + endfunction + + let g:spotbugs_properties = { + \ 'compiler': 'maven', + \ 'PreCompilerAction': + \ function('MavenPreCompilerAction'), + \ 'PreCompilerTestAction': + \ function('MavenPreCompilerTestAction'), + \ } + +Note that all entered custom settings will take precedence over the matching +default settings in "g:spotbugs_properties". + +The "g:spotbugs_properties" variable is consulted by the Java filetype plugin +(|ft-java-plugin|) to arrange for the described automation, and, therefore, it +must be defined before |FileType| events can take place for the buffers loaded +with Java source files. It could, for example, be set in a project-local +|vimrc| loaded by [0]. + +[0] https://github.com/MarcWeber/vim-addon-local-vimrc/ + GNU MAKE *compiler-make* Since the default make program is "make", the compiler plugin for make, @@ -1409,6 +1517,13 @@ Useful values for the 'makeprg' options therefore are: setlocal makeprg=./alltests.py " Run a testsuite setlocal makeprg=python\ %:S " Run a single testcase +PYTEST COMPILER *compiler-pytest* +Commonly used compiler options can be added to 'makeprg' by setting the +b/g:pytest_makeprg_params variable. For example: > + + let b:pytest_makeprg_params = "--verbose --no-summary --disable-warnings" + +The global default is "--tb=short --quiet"; Python warnings are suppressed. TEX COMPILER *compiler-tex* diff --git a/runtime/ftplugin/java.vim b/runtime/ftplugin/java.vim index 55b358374f..6e12fe2fe5 100644 --- a/runtime/ftplugin/java.vim +++ b/runtime/ftplugin/java.vim @@ -3,7 +3,7 @@ " Maintainer: Aliaksei Budavei <0x000c70 AT gmail DOT com> " Former Maintainer: Dan Sharp " Repository: https://github.com/zzzyxwvut/java-vim.git -" Last Change: 2024 Sep 26 +" Last Change: 2024 Nov 24 " 2024 Jan 14 by Vim Project (browsefilter) " 2024 May 23 by Riley Bruins <ribru17@gmail.com> ('commentstring') @@ -90,10 +90,127 @@ if (has("gui_win32") || has("gui_gtk")) && !exists("b:browsefilter") endif endif +" The support for pre- and post-compiler actions for SpotBugs. +if exists("g:spotbugs_properties") && has_key(g:spotbugs_properties, 'compiler') + try + let spotbugs#compiler = g:spotbugs_properties.compiler + let g:spotbugs_properties = extend( + \ spotbugs#DefaultProperties(), + \ g:spotbugs_properties, + \ 'force') + catch + echomsg v:errmsg + finally + call remove(g:spotbugs_properties, 'compiler') + endtry +endif + +if exists("g:spotbugs_properties") && + \ filereadable($VIMRUNTIME . '/compiler/spotbugs.vim') + let s:request = 0 + + if has_key(g:spotbugs_properties, 'PreCompilerAction') + let s:dispatcher = 'call g:spotbugs_properties.PreCompilerAction() | ' + let s:request += 1 + endif + + if has_key(g:spotbugs_properties, 'PreCompilerTestAction') + let s:dispatcher = 'call g:spotbugs_properties.PreCompilerTestAction() | ' + let s:request += 2 + endif + + if has_key(g:spotbugs_properties, 'PostCompilerAction') + let s:request += 4 + endif + + if (s:request == 3 || s:request == 7) && + \ has_key(g:spotbugs_properties, 'sourceDirPath') && + \ has_key(g:spotbugs_properties, 'testSourceDirPath') + function! s:DispatchAction(path_action_pairs) abort + let name = expand('%:p') + + for [path, Action] in a:path_action_pairs + if name =~# (path . '.\{-}\.java\=$') + call Action() + break + endif + endfor + endfunction + + let s:dispatcher = printf('call s:DispatchAction(%s) | ', + \ string([[g:spotbugs_properties.sourceDirPath, + \ g:spotbugs_properties.PreCompilerAction], + \ [g:spotbugs_properties.testSourceDirPath, + \ g:spotbugs_properties.PreCompilerTestAction]])) + endif + + if s:request + if exists("b:spotbugs_syntax_once") + let s:actions = [{'event': 'BufWritePost'}] + else + " XXX: Handle multiple FileType events when vimrc contains more + " than one filetype setting for the language, e.g.: + " :filetype plugin indent on + " :autocmd BufRead,BufNewFile *.java setlocal filetype=java ... + " XXX: DO NOT ADD b:spotbugs_syntax_once TO b:undo_ftplugin ! + let b:spotbugs_syntax_once = 1 + let s:actions = [{ + \ 'event': 'Syntax', + \ 'once': 1, + \ }, { + \ 'event': 'BufWritePost', + \ }] + endif + + for s:idx in range(len(s:actions)) + if s:request == 7 || s:request == 6 || s:request == 5 + let s:actions[s:idx].cmd = s:dispatcher . 'compiler spotbugs | ' . + \ 'call g:spotbugs_properties.PostCompilerAction()' + elseif s:request == 4 + let s:actions[s:idx].cmd = 'compiler spotbugs | ' . + \ 'call g:spotbugs_properties.PostCompilerAction()' + elseif s:request == 3 || s:request == 2 || s:request == 1 + let s:actions[s:idx].cmd = s:dispatcher . 'compiler spotbugs' + else + let s:actions[s:idx].cmd = '' + endif + endfor + + if !exists("#java_spotbugs") + augroup java_spotbugs + augroup END + endif + + " The events are defined in s:actions. + silent! autocmd! java_spotbugs BufWritePost <buffer> + silent! autocmd! java_spotbugs Syntax <buffer> + + for s:action in s:actions + execute printf('autocmd java_spotbugs %s <buffer> %s', + \ s:action.event, + \ s:action.cmd . (has_key(s:action, 'once') + \ ? printf(' | autocmd! java_spotbugs %s <buffer>', + \ s:action.event) + \ : '')) + endfor + + unlet! s:action s:actions s:idx s:dispatcher + endif + + unlet s:request +endif + +function! JavaFileTypeCleanUp() abort + setlocal suffixes< suffixesadd< formatoptions< comments< commentstring< path< includeexpr< + unlet! b:browsefilter + + " The concatenated removals may be misparsed as a BufWritePost autocmd. + silent! autocmd! java_spotbugs BufWritePost <buffer> + silent! autocmd! java_spotbugs Syntax <buffer> +endfunction + " Undo the stuff we changed. -let b:undo_ftplugin = "setlocal suffixes< suffixesadd<" . - \ " formatoptions< comments< commentstring< path< includeexpr<" . - \ " | unlet! b:browsefilter" +let b:undo_ftplugin = 'call JavaFileTypeCleanUp() | delfunction JavaFileTypeCleanUp' " See ":help vim9-mix". if !has("vim9script") @@ -114,6 +231,19 @@ if exists("s:zip_func_upgradable") setlocal suffixesadd< endif +if exists("*s:DispatchAction") + def! s:DispatchAction(path_action_pairs: list<list<any>>) + const name: string = expand('%:p') + + for [path: string, Action: func: any] in path_action_pairs + if name =~# (path .. '.\{-}\.java\=$') + Action() + break + endif + endfor + enddef +endif + " Restore the saved compatibility options. let &cpo = s:save_cpo unlet s:save_cpo diff --git a/runtime/ftplugin/python.vim b/runtime/ftplugin/python.vim index c000296726..6f20468896 100644 --- a/runtime/ftplugin/python.vim +++ b/runtime/ftplugin/python.vim @@ -3,8 +3,9 @@ " Maintainer: Tom Picton <tom@tompicton.com> " Previous Maintainer: James Sully <sullyj3@gmail.com> " Previous Maintainer: Johannes Zellner <johannes@zellner.org> -" Last Change: 2024/05/13 -" https://github.com/tpict/vim-ftplugin-python +" Repository: https://github.com/tpict/vim-ftplugin-python +" Last Change: 2024/05/13 +" 2024 Nov 30 use pytest compiler (#16130) if exists("b:did_ftplugin") | finish | endif let b:did_ftplugin = 1 @@ -134,6 +135,11 @@ elseif executable('python') setlocal keywordprg=python\ -m\ pydoc endif +if expand('%:t') =~# '\v^test_.*\.py$|_test\.py$' && executable('pytest') + compiler pytest + let &l:makeprg .= ' %:S' +endif + " Script for filetype switching to undo the local stuff we may have changed let b:undo_ftplugin = 'setlocal cinkeys<' \ . '|setlocal comments<' @@ -148,6 +154,7 @@ let b:undo_ftplugin = 'setlocal cinkeys<' \ . '|setlocal softtabstop<' \ . '|setlocal suffixesadd<' \ . '|setlocal tabstop<' + \ . '|setlocal makeprg<' \ . '|silent! nunmap <buffer> [M' \ . '|silent! nunmap <buffer> [[' \ . '|silent! nunmap <buffer> []' diff --git a/runtime/lua/vim/_defaults.lua b/runtime/lua/vim/_defaults.lua index 2687f34302..6583cf48b3 100644 --- a/runtime/lua/vim/_defaults.lua +++ b/runtime/lua/vim/_defaults.lua @@ -546,8 +546,9 @@ do --- --- @param option string Option name --- @param value any Option value - local function setoption(option, value) - if vim.api.nvim_get_option_info2(option, {}).was_set then + --- @param force boolean? Always set the value, even if already set + local function setoption(option, value, force) + if not force and vim.api.nvim_get_option_info2(option, {}).was_set then -- Don't do anything if option is already set return end @@ -563,7 +564,7 @@ do once = true, nested = true, callback = function() - setoption(option, value) + setoption(option, value, force) end, }) end @@ -645,11 +646,15 @@ do return nil, nil, nil end - local timer = assert(vim.uv.new_timer()) - + -- This autocommand updates the value of 'background' anytime we receive + -- an OSC 11 response from the terminal emulator. If the user has set + -- 'background' explictly then we will delete this autocommand, + -- effectively disabling automatic background setting. + local force = false local id = vim.api.nvim_create_autocmd('TermResponse', { group = group, nested = true, + desc = "Update the value of 'background' automatically based on the terminal emulator's background color", callback = function(args) local resp = args.data ---@type string local r, g, b = parseosc11(resp) @@ -661,27 +666,33 @@ do if rr and gg and bb then local luminance = (0.299 * rr) + (0.587 * gg) + (0.114 * bb) local bg = luminance < 0.5 and 'dark' or 'light' - setoption('background', bg) + setoption('background', bg, force) + + -- On the first query response, don't force setting the option in + -- case the user has already set it manually. If they have, then + -- this autocommand will be deleted. If they haven't, then we do + -- want to force setting the option to override the value set by + -- this autocommand. + if not force then + force = true + end end + end + end, + }) - return true + vim.api.nvim_create_autocmd('VimEnter', { + group = group, + nested = true, + once = true, + callback = function() + if vim.api.nvim_get_option_info2('background', {}).was_set then + vim.api.nvim_del_autocmd(id) end end, }) io.stdout:write('\027]11;?\007') - - timer:start(1000, 0, function() - -- Delete the autocommand if no response was received - vim.schedule(function() - -- Suppress error if autocommand has already been deleted - pcall(vim.api.nvim_del_autocmd, id) - end) - - if not timer:is_closing() then - timer:close() - end - end) end --- If the TUI (term_has_truecolor) was able to determine that the host diff --git a/runtime/lua/vim/_meta/options.lua b/runtime/lua/vim/_meta/options.lua index c635d9bd3b..247b464a70 100644 --- a/runtime/lua/vim/_meta/options.lua +++ b/runtime/lua/vim/_meta/options.lua @@ -6311,6 +6311,7 @@ vim.wo.stc = vim.wo.statuscolumn --- All fields except the {item} are optional. A single percent sign can --- be given as "%%". --- +--- *stl-%!* --- When the option starts with "%!" then it is used as an expression, --- evaluated and the result is used as the option value. Example: --- diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index aa566973b6..5d771c30e9 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -962,7 +962,9 @@ local extension = { purs = 'purescript', arr = 'pyret', pxd = 'pyrex', + pxi = 'pyrex', pyx = 'pyrex', + ['pyx+'] = 'pyrex', pyw = 'python', py = 'python', pyi = 'python', diff --git a/runtime/lua/vim/fs.lua b/runtime/lua/vim/fs.lua index d91eeaf02f..2f007d97c3 100644 --- a/runtime/lua/vim/fs.lua +++ b/runtime/lua/vim/fs.lua @@ -1,3 +1,15 @@ +--- @brief <pre>help +--- *vim.fs.exists()* +--- Use |uv.fs_stat()| to check a file's type, and whether it exists. +--- +--- Example: +--- +--- >lua +--- if vim.uv.fs_stat(file) then +--- vim.print("file exists") +--- end +--- < + local uv = vim.uv local M = {} diff --git a/runtime/lua/vim/loader.lua b/runtime/lua/vim/loader.lua index 0cce0ab21d..71d0188128 100644 --- a/runtime/lua/vim/loader.lua +++ b/runtime/lua/vim/loader.lua @@ -399,50 +399,51 @@ function M.reset(path) end end ---- Enables the experimental Lua module loader: ---- * overrides loadfile +--- Enables or disables the experimental Lua module loader: +--- +--- Enable (`enable=true`): +--- * overrides |loadfile()| --- * adds the Lua loader using the byte-compilation cache --- * adds the libs loader --- * removes the default Nvim loader --- ---- @since 0 -function M.enable() - if M.enabled then - return - end - M.enabled = true - vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p') - _G.loadfile = loadfile_cached - -- add Lua loader - table.insert(loaders, 2, loader_cached) - -- add libs loader - table.insert(loaders, 3, loader_lib_cached) - -- remove Nvim loader - for l, loader in ipairs(loaders) do - if loader == vim._load_package then - table.remove(loaders, l) - break - end - end -end - ---- Disables the experimental Lua module loader: +--- Disable (`enable=false`): --- * removes the loaders --- * adds the default Nvim loader --- --- @since 0 -function M.disable() - if not M.enabled then +--- +--- @param enable? (boolean) true/nil to enable, false to disable +function M.enable(enable) + enable = enable == nil and true or enable + if enable == M.enabled then return end - M.enabled = false - _G.loadfile = _loadfile - for l, loader in ipairs(loaders) do - if loader == loader_cached or loader == loader_lib_cached then - table.remove(loaders, l) + M.enabled = enable + + if enable then + vim.fn.mkdir(vim.fn.fnamemodify(M.path, ':p'), 'p') + _G.loadfile = loadfile_cached + -- add Lua loader + table.insert(loaders, 2, loader_cached) + -- add libs loader + table.insert(loaders, 3, loader_lib_cached) + -- remove Nvim loader + for l, loader in ipairs(loaders) do + if loader == vim._load_package then + table.remove(loaders, l) + break + end + end + else + _G.loadfile = _loadfile + for l, loader in ipairs(loaders) do + if loader == loader_cached or loader == loader_lib_cached then + table.remove(loaders, l) + end end + table.insert(loaders, 2, vim._load_package) end - table.insert(loaders, 2, vim._load_package) end --- Tracks the time spent in a function diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6d29c9e4df..a3791e15c3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -3,6 +3,7 @@ local validate = vim.validate local lsp = vim._defer_require('vim.lsp', { _changetracking = ..., --- @module 'vim.lsp._changetracking' + _folding_range = ..., --- @module 'vim.lsp._folding_range' _snippet_grammar = ..., --- @module 'vim.lsp._snippet_grammar' _tagfunc = ..., --- @module 'vim.lsp._tagfunc' _watchfiles = ..., --- @module 'vim.lsp._watchfiles' @@ -57,6 +58,7 @@ lsp._request_name_to_capability = { [ms.textDocument_documentHighlight] = { 'documentHighlightProvider' }, [ms.textDocument_documentLink] = { 'documentLinkProvider' }, [ms.textDocument_documentSymbol] = { 'documentSymbolProvider' }, + [ms.textDocument_foldingRange] = { 'foldingRangeProvider' }, [ms.textDocument_formatting] = { 'documentFormattingProvider' }, [ms.textDocument_hover] = { 'hoverProvider' }, [ms.textDocument_implementation] = { 'implementationProvider' }, @@ -1094,6 +1096,38 @@ function lsp.tagfunc(pattern, flags) return vim.lsp._tagfunc(pattern, flags) end +--- Provides an interface between the built-in client and a `foldexpr` function. +---@param lnum integer line number +function lsp.foldexpr(lnum) + return vim.lsp._folding_range.foldexpr(lnum) +end + +--- Close all {kind} of folds in the the window with {winid}. +--- +--- To automatically fold imports when opening a file, you can use an autocmd: +--- +--- ```lua +--- vim.api.nvim_create_autocmd('LspNotify', { +--- callback = function(args) +--- if args.data.method == 'textDocument/didOpen' then +--- vim.lsp.foldclose('imports', vim.fn.bufwinid(args.buf)) +--- end +--- end, +--- }) +--- ``` +--- +---@param kind lsp.FoldingRangeKind Kind to close, one of "comment", "imports" or "region". +---@param winid? integer Defaults to the current window. +function lsp.foldclose(kind, winid) + return vim.lsp._folding_range.foldclose(kind, winid) +end + +--- Provides a `foldtext` function that shows the `collapsedText` retrieved, +--- defaults to the first folded line if `collapsedText` is not provided. +function lsp.foldtext() + return vim.lsp._folding_range.foldtext() +end + ---Checks whether a client is stopped. --- ---@param client_id (integer) diff --git a/runtime/lua/vim/lsp/_folding_range.lua b/runtime/lua/vim/lsp/_folding_range.lua new file mode 100644 index 0000000000..6a445017a3 --- /dev/null +++ b/runtime/lua/vim/lsp/_folding_range.lua @@ -0,0 +1,371 @@ +local util = require('vim.lsp.util') +local log = require('vim.lsp.log') +local ms = require('vim.lsp.protocol').Methods +local api = vim.api + +local M = {} + +---@class (private) vim.lsp.folding_range.BufState +--- +---@field version? integer +--- +--- Never use this directly, `renew()` the cached foldinfo +--- then use on demand via `row_*` fields. +--- +--- Index In the form of client_id -> ranges +---@field client_ranges table<integer, lsp.FoldingRange[]?> +--- +--- Index in the form of row -> [foldlevel, mark] +---@field row_level table<integer, [integer, ">" | "<"?]?> +--- +--- Index in the form of start_row -> kinds +---@field row_kinds table<integer, table<lsp.FoldingRangeKind, true?>?>> +--- +--- Index in the form of start_row -> collapsed_text +---@field row_text table<integer, string?> + +---@type table<integer, vim.lsp.folding_range.BufState?> +local bufstates = {} + +--- Renew the cached foldinfo in the buffer. +---@param bufnr integer +local function renew(bufnr) + local bufstate = assert(bufstates[bufnr]) + + ---@type table<integer, [integer, ">" | "<"?]?> + local row_level = {} + ---@type table<integer, table<lsp.FoldingRangeKind, true?>?>> + local row_kinds = {} + ---@type table<integer, string?> + local row_text = {} + + for _, ranges in pairs(bufstate.client_ranges) do + for _, range in ipairs(ranges) do + local start_row = range.startLine + local end_row = range.endLine + -- Adding folds within a single line is not supported by Nvim. + if start_row ~= end_row then + row_text[start_row] = range.collapsedText + + local kind = range.kind + if kind then + local kinds = row_kinds[start_row] or {} + kinds[kind] = true + row_kinds[start_row] = kinds + end + + for row = start_row, end_row do + local level = row_level[row] or { 0 } + level[1] = level[1] + 1 + row_level[row] = level + end + row_level[start_row][2] = '>' + row_level[end_row][2] = '<' + end + end + end + + bufstate.row_level = row_level + bufstate.row_kinds = row_kinds + bufstate.row_text = row_text +end + +--- Renew the cached foldinfo then force `foldexpr()` to be re-evaluated, +--- without opening folds. +---@param bufnr integer +local function foldupdate(bufnr) + renew(bufnr) + for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do + local wininfo = vim.fn.getwininfo(winid)[1] + if wininfo and wininfo.tabnr == vim.fn.tabpagenr() then + if vim.wo[winid].foldmethod == 'expr' then + vim._foldupdate(winid, 0, api.nvim_buf_line_count(bufnr)) + end + end + end +end + +--- Whether `foldupdate()` is scheduled for the buffer with `bufnr`. +--- +--- Index in the form of bufnr -> true? +---@type table<integer, true?> +local scheduled_foldupdate = {} + +--- Schedule `foldupdate()` after leaving insert mode. +---@param bufnr integer +local function schedule_foldupdate(bufnr) + if not scheduled_foldupdate[bufnr] then + scheduled_foldupdate[bufnr] = true + api.nvim_create_autocmd('InsertLeave', { + buffer = bufnr, + once = true, + callback = function() + foldupdate(bufnr) + scheduled_foldupdate[bufnr] = nil + end, + }) + end +end + +---@param results table<integer,{err: lsp.ResponseError?, result: lsp.FoldingRange[]?}> +---@type lsp.MultiHandler +local function multi_handler(results, ctx) + local bufnr = assert(ctx.bufnr) + -- Handling responses from outdated buffer only causes performance overhead. + if util.buf_versions[bufnr] ~= ctx.version then + return + end + + local bufstate = assert(bufstates[bufnr]) + for client_id, result in pairs(results) do + if result.err then + log.error(result.err) + else + bufstate.client_ranges[client_id] = result.result + end + end + bufstate.version = ctx.version + + if api.nvim_get_mode().mode:match('^i') then + -- `foldUpdate()` is guarded in insert mode. + schedule_foldupdate(bufnr) + else + foldupdate(bufnr) + end +end + +---@param result lsp.FoldingRange[]? +---@type lsp.Handler +local function handler(err, result, ctx) + multi_handler({ [ctx.client_id] = { err = err, result = result } }, ctx) +end + +--- Request `textDocument/foldingRange` from the server. +--- `foldupdate()` is scheduled once after the request is completed. +---@param bufnr integer +---@param client? vim.lsp.Client The client whose server supports `foldingRange`. +local function request(bufnr, client) + ---@type lsp.FoldingRangeParams + local params = { textDocument = util.make_text_document_params(bufnr) } + + if client then + client:request(ms.textDocument_foldingRange, params, handler, bufnr) + return + end + + if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then + return + end + + vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, multi_handler) +end + +-- NOTE: +-- `bufstate` and event hooks are interdependent: +-- * `bufstate` needs event hooks for correctness. +-- * event hooks require the previous `bufstate` for updates. +-- Since they are manually created and destroyed, +-- we ensure their lifecycles are always synchronized. +-- +-- TODO(ofseed): +-- 1. Implement clearing `bufstate` and event hooks +-- when no clients in the buffer support the corresponding method. +-- 2. Then generalize this state management to other LSP modules. +local augroup_setup = api.nvim_create_augroup('vim_lsp_folding_range/setup', {}) + +--- Initialize `bufstate` and event hooks, then request folding ranges. +--- Manage their lifecycle within this function. +---@param bufnr integer +---@return vim.lsp.folding_range.BufState? +local function setup(bufnr) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + + -- Register the new `bufstate`. + bufstates[bufnr] = { + client_ranges = {}, + row_level = {}, + row_kinds = {}, + row_text = {}, + } + + -- Event hooks from `buf_attach` can't be removed externally. + -- Hooks and `bufstate` share the same lifecycle; + -- they should self-destroy if `bufstate == nil`. + api.nvim_buf_attach(bufnr, false, { + -- `on_detach` also runs on buffer reload (`:e`). + -- Ensure `bufstate` and hooks are cleared to avoid duplication or leftover states. + on_detach = function() + bufstates[bufnr] = nil + api.nvim_clear_autocmds({ buffer = bufnr, group = augroup_setup }) + end, + -- Reset `bufstate` and request folding ranges. + on_reload = function() + bufstates[bufnr] = { + client_ranges = {}, + row_level = {}, + row_kinds = {}, + row_text = {}, + } + request(bufnr) + end, + --- Sync changed rows with their previous foldlevels before applying new ones. + on_bytes = function(_, _, _, start_row, _, _, old_row, _, _, new_row, _, _) + if bufstates[bufnr] == nil then + return true + end + local row_level = bufstates[bufnr].row_level + if next(row_level) == nil then + return + end + local row = new_row - old_row + if row > 0 then + vim._list_insert(row_level, start_row, start_row + math.abs(row) - 1, { -1 }) + -- If the previous row ends a fold, + -- Nvim treats the first row after consecutive `-1`s as a new fold start, + -- which is not the desired behavior. + local prev_level = row_level[start_row - 1] + if prev_level and prev_level[2] == '<' then + row_level[start_row] = { prev_level[1] - 1 } + end + elseif row < 0 then + vim._list_remove(row_level, start_row, start_row + math.abs(row) - 1) + end + end, + }) + api.nvim_create_autocmd('LspDetach', { + group = augroup_setup, + buffer = bufnr, + callback = function(args) + if not api.nvim_buf_is_loaded(bufnr) then + return + end + + ---@type integer + local client_id = args.data.client_id + bufstates[bufnr].client_ranges[client_id] = nil + + ---@type vim.lsp.Client[] + local clients = vim + .iter(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) + ---@param client vim.lsp.Client + :filter(function(client) + return client.id ~= client_id + end) + :totable() + if #clients == 0 then + bufstates[bufnr] = { + client_ranges = {}, + row_level = {}, + row_kinds = {}, + row_text = {}, + } + end + + foldupdate(bufnr) + end, + }) + api.nvim_create_autocmd('LspAttach', { + group = augroup_setup, + buffer = bufnr, + callback = function(args) + local client = assert(vim.lsp.get_client_by_id(args.data.client_id)) + request(bufnr, client) + end, + }) + api.nvim_create_autocmd('LspNotify', { + group = augroup_setup, + buffer = bufnr, + callback = function(args) + local client = assert(vim.lsp.get_client_by_id(args.data.client_id)) + if + client:supports_method(ms.textDocument_foldingRange, bufnr) + and ( + args.data.method == ms.textDocument_didChange + or args.data.method == ms.textDocument_didOpen + ) + then + request(bufnr, client) + end + end, + }) + + request(bufnr) + + return bufstates[bufnr] +end + +---@param kind lsp.FoldingRangeKind +---@param winid integer +local function foldclose(kind, winid) + vim._with({ win = winid }, function() + local bufnr = api.nvim_win_get_buf(winid) + local row_kinds = bufstates[bufnr].row_kinds + -- Reverse traverse to ensure that the smallest ranges are closed first. + for row = api.nvim_buf_line_count(bufnr) - 1, 0, -1 do + local kinds = row_kinds[row] + if kinds and kinds[kind] then + vim.cmd(row + 1 .. 'foldclose') + end + end + end) +end + +---@param kind lsp.FoldingRangeKind +---@param winid? integer +function M.foldclose(kind, winid) + vim.validate('kind', kind, 'string') + vim.validate('winid', winid, 'number', true) + + winid = winid or api.nvim_get_current_win() + local bufnr = api.nvim_win_get_buf(winid) + local bufstate = bufstates[bufnr] + if not bufstate then + return + end + + if bufstate.version == util.buf_versions[bufnr] then + foldclose(kind, winid) + return + end + -- Schedule `foldclose()` if the buffer is not up-to-date. + + if not next(vim.lsp.get_clients({ bufnr = bufnr, method = ms.textDocument_foldingRange })) then + return + end + ---@type lsp.FoldingRangeParams + local params = { textDocument = util.make_text_document_params(bufnr) } + vim.lsp.buf_request_all(bufnr, ms.textDocument_foldingRange, params, function(...) + multi_handler(...) + foldclose(kind, winid) + end) +end + +---@return string +function M.foldtext() + local bufnr = api.nvim_get_current_buf() + local lnum = vim.v.foldstart + local row = lnum - 1 + local bufstate = bufstates[bufnr] + if bufstate and bufstate.row_text[row] then + return bufstate.row_text[row] + end + return vim.fn.getline(lnum) +end + +---@param lnum? integer +---@return string level +function M.foldexpr(lnum) + local bufnr = api.nvim_get_current_buf() + local bufstate = bufstates[bufnr] or setup(bufnr) + if not bufstate then + return '0' + end + + local row = (lnum or vim.v.lnum) - 1 + local level = bufstate.row_level[row] + return level and (level[2] or '') .. (level[1] or '0') or '0' +end + +return M diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index aca6bf27f4..10479807a2 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -487,7 +487,7 @@ end --- ```lua --- -- Never request typescript-language-server for formatting --- vim.lsp.buf.format { ---- filter = function(client) return client.name ~= "tsserver" end +--- filter = function(client) return client.name ~= "ts_ls" end --- } --- ``` --- @field filter? fun(client: vim.lsp.Client): boolean? @@ -736,7 +736,7 @@ end --- Lists all the references to the symbol under the cursor in the quickfix window. --- ----@param context (table|nil) Context for the request +---@param context lsp.ReferenceContext? Context for the request ---@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references ---@param opts? vim.lsp.ListOpts function M.references(context, opts) diff --git a/runtime/lua/vim/lsp/client.lua b/runtime/lua/vim/lsp/client.lua index 7c2b7192f5..a14b6ccda6 100644 --- a/runtime/lua/vim/lsp/client.lua +++ b/runtime/lua/vim/lsp/client.lua @@ -639,7 +639,7 @@ end --- @param method string LSP method name. --- @param params? table LSP request params. --- @param handler? lsp.Handler Response |lsp-handler| for this method. ---- @param bufnr? integer Buffer handle. 0 for current (default). +--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current. --- @return boolean status indicates whether the request was successful. --- If it is `false`, then it will always be `false` (the client has shutdown). --- @return integer? request_id Can be used with |Client:cancel_request()|. @@ -718,7 +718,7 @@ end --- @param params table LSP request params. --- @param timeout_ms integer? Maximum time in milliseconds to wait for --- a result. Defaults to 1000 ---- @param bufnr integer Buffer handle (0 for current). +--- @param bufnr? integer (default: 0) Buffer handle, or 0 for current. --- @return {err: lsp.ResponseError?, result:any}? `result` and `err` from the |lsp-handler|. --- `nil` is the request was unsuccessful --- @return string? err On timeout, cancel or error, where `err` is a diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 3d29dad90a..cfd47d8f7c 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -440,6 +440,13 @@ function protocol.make_client_capabilities() properties = { 'command' }, }, }, + foldingRange = { + dynamicRegistration = false, + lineFoldingOnly = true, + foldingRange = { + collapsedText = true, + }, + }, formatting = { dynamicRegistration = true, }, diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 3de76f93a6..ced8aa5745 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1894,7 +1894,7 @@ function M.make_position_params(window, position_encoding) local buf = api.nvim_win_get_buf(window) if position_encoding == nil then vim.notify_once( - 'warning: position_encoding is required, using the offset_encoding from the first client', + 'position_encoding param is required in vim.lsp.util.make_position_params. Defaulting to position encoding of the first client.', vim.log.levels.WARN ) position_encoding = M._get_offset_encoding(buf) @@ -1950,7 +1950,7 @@ function M.make_range_params(window, position_encoding) local buf = api.nvim_win_get_buf(window or 0) if position_encoding == nil then vim.notify_once( - 'warning: position_encoding is required, using the offset_encoding from the first client', + 'position_encoding param is required in vim.lsp.util.make_range_params. Defaulting to position encoding of the first client.', vim.log.levels.WARN ) position_encoding = M._get_offset_encoding(buf) @@ -1979,7 +1979,7 @@ function M.make_given_range_params(start_pos, end_pos, bufnr, position_encoding) bufnr = bufnr or api.nvim_get_current_buf() if position_encoding == nil then vim.notify_once( - 'warning: position_encoding is required, using the offset_encoding from the first client', + 'position_encoding param is required in vim.lsp.util.make_given_range_params. Defaulting to position encoding of the first client.', vim.log.levels.WARN ) position_encoding = M._get_offset_encoding(bufnr) diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 4f2373b182..2e8edea22a 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -737,6 +737,51 @@ function vim.list_slice(list, start, finish) return new_list end +--- Efficiently insert items into the middle of a list. +--- +--- Calling table.insert() in a loop will re-index the tail of the table on +--- every iteration, instead this function will re-index the table exactly +--- once. +--- +--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 +--- +---@param t any[] +---@param first integer +---@param last integer +---@param v any +function vim._list_insert(t, first, last, v) + local n = #t + + -- Shift table forward + for i = n - first, 0, -1 do + t[last + 1 + i] = t[first + i] + end + + -- Fill in new values + for i = first, last do + t[i] = v + end +end + +--- Efficiently remove items from middle of a list. +--- +--- Calling table.remove() in a loop will re-index the tail of the table on +--- every iteration, instead this function will re-index the table exactly +--- once. +--- +--- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 +--- +---@param t any[] +---@param first integer +---@param last integer +function vim._list_remove(t, first, last) + local n = #t + for i = 0, n - first do + t[first + i] = t[last + 1 + i] + t[last + 1 + i] = nil + end +end + --- Trim whitespace (Lua pattern "%s") from both sides of a string. --- ---@see |lua-patterns| diff --git a/runtime/lua/vim/text.lua b/runtime/lua/vim/text.lua index d45c8021c6..f910ab3a1d 100644 --- a/runtime/lua/vim/text.lua +++ b/runtime/lua/vim/text.lua @@ -2,6 +2,18 @@ local M = {} +local alphabet = '0123456789ABCDEF' +local atoi = {} ---@type table<string, integer> +local itoa = {} ---@type table<integer, string> +do + for i = 1, #alphabet do + local char = alphabet:sub(i, i) + itoa[i - 1] = char + atoi[char] = i - 1 + atoi[char:lower()] = i - 1 + end +end + --- Hex encode a string. --- --- @param str string String to encode @@ -9,7 +21,9 @@ local M = {} function M.hexencode(str) local enc = {} ---@type string[] for i = 1, #str do - enc[i] = string.format('%02X', str:byte(i, i + 1)) + local byte = str:byte(i) + enc[2 * i - 1] = itoa[math.floor(byte / 16)] + enc[2 * i] = itoa[byte % 16] end return table.concat(enc) end @@ -26,8 +40,12 @@ function M.hexdecode(enc) local str = {} ---@type string[] for i = 1, #enc, 2 do - local n = assert(tonumber(enc:sub(i, i + 1), 16)) - str[#str + 1] = string.char(n) + local u = atoi[enc:sub(i, i)] + local l = atoi[enc:sub(i + 1, i + 1)] + if not u or not l then + return nil, 'string must contain only hex characters' + end + str[(i + 1) / 2] = string.char(u * 16 + l) end return table.concat(str), nil end diff --git a/runtime/lua/vim/treesitter/_fold.lua b/runtime/lua/vim/treesitter/_fold.lua index 7237d2e7d4..0cb5b497c7 100644 --- a/runtime/lua/vim/treesitter/_fold.lua +++ b/runtime/lua/vim/treesitter/_fold.lua @@ -30,65 +30,20 @@ function FoldInfo.new() }, FoldInfo) end ---- Efficiently remove items from middle of a list a list. ---- ---- Calling table.remove() in a loop will re-index the tail of the table on ---- every iteration, instead this function will re-index the table exactly ---- once. ---- ---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 ---- ----@param t any[] ----@param first integer ----@param last integer -local function list_remove(t, first, last) - local n = #t - for i = 0, n - first do - t[first + i] = t[last + 1 + i] - t[last + 1 + i] = nil - end -end - ---@package ---@param srow integer ---@param erow integer 0-indexed, exclusive function FoldInfo:remove_range(srow, erow) - list_remove(self.levels, srow + 1, erow) - list_remove(self.levels0, srow + 1, erow) -end - ---- Efficiently insert items into the middle of a list. ---- ---- Calling table.insert() in a loop will re-index the tail of the table on ---- every iteration, instead this function will re-index the table exactly ---- once. ---- ---- Based on https://stackoverflow.com/questions/12394841/safely-remove-items-from-an-array-table-while-iterating/53038524#53038524 ---- ----@param t any[] ----@param first integer ----@param last integer ----@param v any -local function list_insert(t, first, last, v) - local n = #t - - -- Shift table forward - for i = n - first, 0, -1 do - t[last + 1 + i] = t[first + i] - end - - -- Fill in new values - for i = first, last do - t[i] = v - end + vim._list_remove(self.levels, srow + 1, erow) + vim._list_remove(self.levels0, srow + 1, erow) end ---@package ---@param srow integer ---@param erow integer 0-indexed, exclusive function FoldInfo:add_range(srow, erow) - list_insert(self.levels, srow + 1, erow, -1) - list_insert(self.levels0, srow + 1, erow, -1) + vim._list_insert(self.levels, srow + 1, erow, -1) + vim._list_insert(self.levels0, srow + 1, erow, -1) end ---@param range Range2 diff --git a/runtime/syntax/po.vim b/runtime/syntax/po.vim index 08d6baec27..6da27f639d 100644 --- a/runtime/syntax/po.vim +++ b/runtime/syntax/po.vim @@ -1,10 +1,11 @@ " Vim syntax file " Language: po (gettext) " Maintainer: Dwayne Bailey <dwayne@translate.org.za> -" Last Change: 2015 Jun 07 +" Last Change: 2024 Nov 28 " Contributors: Dwayne Bailey (Most advanced syntax highlighting) " Leonardo Fontenelle (Spell checking) " Nam SungHyun <namsh@kldp.org> (Original maintainer) +" Eisuke Kawashima (add format-flags: #16132) " quit when a syntax file was already loaded if exists("b:current_syntax") @@ -32,9 +33,9 @@ syn region poMsgCTxt matchgroup=poStatementMsgCTxt start=+^msgctxt "+rs=e-1 syn region poMsgID matchgroup=poStatementMsgid start=+^msgid "+rs=e-1 matchgroup=poStringID end=+^msgstr\(\|\[[\]0\[]\]\) "+me=s-1 contains=poStringID,poStatementMsgidplural,poStatementMsgid syn region poMsgSTR matchgroup=poStatementMsgstr start=+^msgstr\(\|\[[\]0\[]\]\) "+rs=e-1 matchgroup=poStringSTR end=+\n\n+me=s-1 contains=poStringSTR,poStatementMsgstr syn region poStringCTxt start=+"+ skip=+\\\\\|\\"+ end=+"+ -syn region poStringID start=+"+ skip=+\\\\\|\\"+ end=+"+ contained +syn region poStringID start=+"+ skip=+\\\\\|\\"+ end=+"+ contained \ contains=poSpecial,poFormat,poCommentKDE,poPluralKDE,poKDEdesktopFile,poHtml,poAcceleratorId,poHtmlNot,poVariable -syn region poStringSTR start=+"+ skip=+\\\\\|\\"+ end=+"+ contained +syn region poStringSTR start=+"+ skip=+\\\\\|\\"+ end=+"+ contained \ contains=@Spell,poSpecial,poFormat,poHeaderItem,poCommentKDEError,poHeaderUndefined,poPluralKDEError,poMsguniqError,poKDEdesktopFile,poHtml,poAcceleratorStr,poHtmlNot,poVariable " Header and Copyright @@ -45,13 +46,43 @@ syn match poCopyrightUnset "SOME DESCRIPTIVE TITLE\|FIRST AUTHOR <EMAIL@ADDR " Translation comment block including: translator comment, automatic comments, flags and locations syn match poComment "^#.*$" syn keyword poFlagFuzzy fuzzy contained + +syn match poFlagFormat /\<\%(no-\)\?awk-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?boost-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?c++-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?c-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?csharp-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?elisp-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?gcc-internal-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?gfc-internal-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?java-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?java-printf-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?javascript-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?kde-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?librep-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?lisp-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?lua-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?objc-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?object-pascal-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?perl-brace-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?perl-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?php-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?python-brace-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?python-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?qt-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?qt-plural-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?ruby-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?scheme-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?sh-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?smalltalk-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?tcl-format\>/ contained +syn match poFlagFormat /\<\%(no-\)\?ycp-format\>/ contained + syn match poCommentTranslator "^# .*$" contains=poCopyrightUnset -syn match poCommentAutomatic "^#\..*$" +syn match poCommentAutomatic "^#\..*$" syn match poCommentSources "^#:.*$" -syn match poCommentFlags "^#,.*$" contains=poFlagFuzzy -syn match poDiffOld '\(^#| "[^{]*+}\|{+[^}]*+}\|{+[^}]*\|"$\)' contained -syn match poDiffNew '\(^#| "[^{]*-}\|{-[^}]*-}\|{-[^}]*\|"$\)' contained -syn match poCommentDiff "^#|.*$" contains=poDiffOld,poDiffNew +syn match poCommentFlags "^#,.*$" contains=poFlagFuzzy,poFlagFormat +syn match poCommentPrevious "^#|.*$" " Translations (also includes header fields as they appear in a translation msgstr) syn region poCommentKDE start=+"_: +ms=s+1 end="\\n" end="\"\n^msgstr"me=s-1 contained @@ -66,13 +97,13 @@ syn match poFormat "%%" contained syn region poMsguniqError matchgroup=poMsguniqErrorMarkers start="#-#-#-#-#" end='#\("\n"\|\)-\("\n"\|\)#\("\n"\|\)-\("\n"\|\)#\("\n"\|\)-\("\n"\|\)#\("\n"\|\)-\("\n"\|\)#\("\n"\|\)\\n' contained " Obsolete messages -syn match poObsolete "^#\~.*$" +syn match poObsolete "^#\~.*$" " KDE Name= handling syn match poKDEdesktopFile "\"\(Name\|Comment\|GenericName\|Description\|Keywords\|About\)="ms=s+1,me=e-1 " Accelerator keys - this messes up if the preceding or following char is a multibyte unicode char -syn match poAcceleratorId contained "[^&_~][&_~]\(\a\|\d\)[^:]"ms=s+1,me=e-1 +syn match poAcceleratorId contained "[^&_~][&_~]\(\a\|\d\)[^:]"ms=s+1,me=e-1 syn match poAcceleratorStr contained "[^&_~][&_~]\(\a\|\d\)[^:]"ms=s+1,me=e-1 contains=@Spell " Variables simple @@ -86,11 +117,10 @@ hi def link poComment Comment hi def link poCommentAutomatic Comment hi def link poCommentTranslator Comment hi def link poCommentFlags Special -hi def link poCommentDiff Comment +hi def link poCommentPrevious Comment hi def link poCopyrightUnset Todo hi def link poFlagFuzzy Todo -hi def link poDiffOld Todo -hi def link poDiffNew Special +hi def link poFlagFormat Todo hi def link poObsolete Comment hi def link poStatementMsgid Statement diff --git a/scripts/gen_vimdoc.lua b/scripts/gen_vimdoc.lua index 1125021bdc..3f870c561f 100755 --- a/scripts/gen_vimdoc.lua +++ b/scripts/gen_vimdoc.lua @@ -274,6 +274,7 @@ local config = { 'diagnostic.lua', 'codelens.lua', 'completion.lua', + 'folding_range.lua', 'inlay_hint.lua', 'tagfunc.lua', 'semantic_tokens.lua', diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index e2b1036d95..cffdcf0415 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -556,7 +556,7 @@ foreach(sfile ${NVIM_SOURCES} set(PREPROC_OUTPUT -w -E -o ${gf_i}) endif() - set(depends "${HEADER_GENERATOR}" "${sfile}" "${LUA_GEN_DEPS}") + set(depends "${HEADER_GENERATOR}" "${sfile}" "${LUA_GEN_DEPS}" "${GENERATOR_C_GRAMMAR}") if("${f}" STREQUAL "version.c") # Ensure auto/versiondef_git.h exists after "make clean". list(APPEND depends update_version_stamp "${NVIM_VERSION_GIT_H}" "${NVIM_VERSION_DEF_H}") diff --git a/src/nvim/drawscreen.c b/src/nvim/drawscreen.c index 19a8093a16..835fdcf7d0 100644 --- a/src/nvim/drawscreen.c +++ b/src/nvim/drawscreen.c @@ -396,7 +396,7 @@ void check_screensize(void) { // Limit Rows and Columns to avoid an overflow in Rows * Columns. // need room for one window and command line - Rows = MIN(MAX(Rows, min_rows()), 1000); + Rows = MIN(MAX(Rows, min_rows_for_all_tabpages()), 1000); Columns = MIN(MAX(Columns, MIN_COLUMNS), 10000); } @@ -2072,7 +2072,7 @@ static void win_update(win_T *wp) // match in fixed position might need redraw // if lines were inserted or deleted || (wp->w_match_head != NULL - && buf->b_mod_xlines != 0))))) + && buf->b_mod_set && buf->b_mod_xlines != 0))))) || lnum == wp->w_cursorline || lnum == wp->w_last_cursorline) { if (lnum == mod_top) { @@ -2291,7 +2291,8 @@ static void win_update(win_T *wp) // - 'number' is set and below inserted/deleted lines, or // - 'relativenumber' is set and cursor moved vertically, // the text doesn't need to be redrawn, but the number column does. - if ((wp->w_p_nu && mod_top != 0 && lnum >= mod_bot && buf->b_mod_xlines != 0) + if ((wp->w_p_nu && mod_top != 0 && lnum >= mod_bot + && buf->b_mod_set && buf->b_mod_xlines != 0) || (wp->w_p_rnu && wp->w_last_cursor_lnum_rnu != wp->w_cursor.lnum)) { foldinfo_T info = wp->w_p_cul && lnum == wp->w_cursor.lnum ? cursorline_fi : fold_info(wp, lnum); diff --git a/src/nvim/generators/c_grammar.lua b/src/nvim/generators/c_grammar.lua index a0a9c86789..9b1c284c1e 100644 --- a/src/nvim/generators/c_grammar.lua +++ b/src/nvim/generators/c_grammar.lua @@ -1,10 +1,37 @@ -local lpeg = vim.lpeg - -- lpeg grammar for building api metadata from a set of header files. It -- ignores comments and preprocessor commands and parses a very small subset -- of C prototypes with a limited set of types + +--- @class nvim.c_grammar.Proto +--- @field [1] 'proto' +--- @field pos integer +--- @field endpos integer +--- @field fast boolean +--- @field name string +--- @field return_type string +--- @field parameters [string, string][] +--- @field static true? +--- @field inline true? + +--- @class nvim.c_grammar.Preproc +--- @field [1] 'preproc' +--- @field content string + +--- @class nvim.c_grammar.Empty +--- @field [1] 'empty' + +--- @alias nvim.c_grammar.result +--- | nvim.c_grammar.Proto +--- | nvim.c_grammar.Preproc +--- | nvim.c_grammar.Empty + +--- @class nvim.c_grammar +--- @field match fun(self, input: string): nvim.c_grammar.result[] + +local lpeg = vim.lpeg + local P, R, S, V = lpeg.P, lpeg.R, lpeg.S, lpeg.V -local C, Ct, Cc, Cg = lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg +local C, Ct, Cc, Cg, Cp = lpeg.C, lpeg.Ct, lpeg.Cc, lpeg.Cg, lpeg.Cp --- @param pat vim.lpeg.Pattern local function rep(pat) @@ -21,125 +48,248 @@ local function opt(pat) return pat ^ -1 end -local any = P(1) -- (consume one character) +local any = P(1) local letter = R('az', 'AZ') + S('_$') local num = R('09') local alpha = letter + num local nl = P('\r\n') + P('\n') -local not_nl = any - nl local space = S(' \t') +local str = P('"') * rep((P('\\') * any) + (1 - P('"'))) * P('"') +local char = P("'") * (any - P("'")) * P("'") local ws = space + nl -local fill = rep(ws) -local c_comment = P('//') * rep(not_nl) -local cdoc_comment = P('///') * opt(Ct(Cg(rep(space) * rep(not_nl), 'comment'))) -local c_preproc = P('#') * rep(not_nl) -local dllexport = P('DLLEXPORT') * rep1(ws) +local wb = #-alpha -- word boundary +local id = letter * rep(alpha) + +local comment_inline = P('/*') * rep(1 - P('*/')) * P('*/') +local comment = P('//') * rep(1 - nl) * nl +local preproc = Ct(Cc('preproc') * P('#') * Cg(rep(1 - nl) * nl, 'content')) + +local fill = rep(ws + comment_inline + comment + preproc) + +--- @param s string +--- @return vim.lpeg.Pattern +local function word(s) + return fill * P(s) * wb * fill +end --- @param x vim.lpeg.Pattern local function comma1(x) return x * rep(fill * P(',') * fill * x) end --- Define the grammar - -local basic_id = letter * rep(alpha) - -local str = P('"') * rep(any - P('"')) * P('"') +--- @param v string +local function Pf(v) + return fill * P(v) * fill +end --- @param x vim.lpeg.Pattern local function paren(x) return P('(') * fill * x * fill * P(')') end +local cdoc_comment = P('///') * opt(Ct(Cg(rep(space) * rep(1 - nl), 'comment'))) + +local braces = P({ + 'S', + A = comment_inline + comment + preproc + str + char + (any - S('{}')), + S = P('{') * rep(V('A')) * rep(V('S') + V('A')) * P('}'), +}) + -- stylua: ignore start local typed_container = P({ 'S', - ID = V('S') + basic_id, S = ( (P('Union') * paren(comma1(V('ID')))) - + (P('ArrayOf') * paren(basic_id * opt(P(',') * fill * rep1(num)))) - + (P('DictOf') * paren(basic_id)) + + (P('ArrayOf') * paren(id * opt(P(',') * fill * rep1(num)))) + + (P('DictOf') * paren(id)) + (P('LuaRefOf') * paren( - paren(comma1((V('ID') + str) * rep1(ws) * opt(P('*')) * basic_id)) + paren(comma1((V('ID') + str) * rep1(ws) * opt(P('*')) * id)) * opt(P(',') * fill * opt(P('*')) * V('ID')) )) - + (P('Dict') * paren(basic_id))), + + (P('Dict') * paren(id))), + ID = V('S') + id, }) -- stylua: ignore end -local c_id = typed_container + basic_id -local c_void = P('void') +local ptr_mod = word('restrict') + word('__restrict') + word('const') +local opt_ptr = rep(Pf('*') * opt(ptr_mod)) + +--- @param name string +--- @param var string +--- @return vim.lpeg.Pattern +local function attr(name, var) + return Cg((P(name) * Cc(true)), var) +end + +--- @param name string +--- @param var string +--- @return vim.lpeg.Pattern +local function attr_num(name, var) + return Cg((P(name) * paren(C(rep1(num)))), var) +end + +local fattr = ( + attr_num('FUNC_API_SINCE', 'since') + + attr_num('FUNC_API_DEPRECATED_SINCE', 'deprecated_since') + + attr('FUNC_API_FAST', 'fast') + + attr('FUNC_API_RET_ALLOC', 'ret_alloc') + + attr('FUNC_API_NOEXPORT', 'noexport') + + attr('FUNC_API_REMOTE_ONLY', 'remote_only') + + attr('FUNC_API_LUA_ONLY', 'lua_only') + + attr('FUNC_API_TEXTLOCK_ALLOW_CMDWIN', 'textlock_allow_cmdwin') + + attr('FUNC_API_TEXTLOCK', 'textlock') + + attr('FUNC_API_REMOTE_IMPL', 'remote_impl') + + attr('FUNC_API_COMPOSITOR_IMPL', 'compositor_impl') + + attr('FUNC_API_CLIENT_IMPL', 'client_impl') + + attr('FUNC_API_CLIENT_IGNORE', 'client_ignore') + + (P('FUNC_') * rep(alpha) * opt(fill * paren(rep(1 - P(')') * any)))) +) + +local void = P('void') * wb -local c_param_type = ( - ((P('Error') * fill * P('*') * fill) * Cc('error')) - + ((P('Arena') * fill * P('*') * fill) * Cc('arena')) - + ((P('lua_State') * fill * P('*') * fill) * Cc('lstate')) - + C(opt(P('const ')) * c_id * rep1(ws) * rep1(P('*'))) - + (C(c_id) * rep1(ws)) +local api_param_type = ( + (word('Error') * opt_ptr * Cc('error')) + + (word('Arena') * opt_ptr * Cc('arena')) + + (word('lua_State') * opt_ptr * Cc('lstate')) ) -local c_type = (C(c_void) * (ws ^ 1)) + c_param_type -local c_param = Ct(c_param_type * C(c_id)) -local c_param_list = c_param * (fill * (P(',') * fill * c_param) ^ 0) -local c_params = Ct(c_void + c_param_list) +local ctype = C( + opt(word('const')) + * ( + typed_container + -- 'unsigned' is a type modifier, and a type itself + + (word('unsigned char') + word('unsigned')) + + (word('struct') * fill * id) + + id + ) + * opt(word('const')) + * opt_ptr +) -local impl_line = (any - P('}')) * opt(rep(not_nl)) * nl +local return_type = (C(void) * fill) + ctype -local ignore_line = rep1(not_nl) * nl +-- stylua: ignore start +local params = Ct( + (void * #P(')')) + + comma1(Ct( + (api_param_type + ctype) + * fill + * C(id) + * rep(Pf('[') * rep(alpha) * Pf(']')) + * rep(fill * fattr) + )) + * opt(Pf(',') * P('...')) +) +-- stylua: ignore end +local ignore_line = rep1(1 - nl) * nl local empty_line = Ct(Cc('empty') * nl * nl) -local c_proto = Ct( - Cc('proto') - * opt(dllexport) - * opt(Cg(P('static') * fill * Cc(true), 'static')) - * Cg(c_type, 'return_type') - * Cg(c_id, 'name') - * fill - * (P('(') * fill * Cg(c_params, 'parameters') * fill * P(')')) - * Cg(Cc(false), 'fast') - * (fill * Cg((P('FUNC_API_SINCE(') * C(rep1(num))) * P(')'), 'since') ^ -1) - * (fill * Cg((P('FUNC_API_DEPRECATED_SINCE(') * C(rep1(num))) * P(')'), 'deprecated_since') ^ -1) - * (fill * Cg((P('FUNC_API_FAST') * Cc(true)), 'fast') ^ -1) - * (fill * Cg((P('FUNC_API_RET_ALLOC') * Cc(true)), 'ret_alloc') ^ -1) - * (fill * Cg((P('FUNC_API_NOEXPORT') * Cc(true)), 'noexport') ^ -1) - * (fill * Cg((P('FUNC_API_REMOTE_ONLY') * Cc(true)), 'remote_only') ^ -1) - * (fill * Cg((P('FUNC_API_LUA_ONLY') * Cc(true)), 'lua_only') ^ -1) - * (fill * (Cg(P('FUNC_API_TEXTLOCK_ALLOW_CMDWIN') * Cc(true), 'textlock_allow_cmdwin') + Cg( - P('FUNC_API_TEXTLOCK') * Cc(true), - 'textlock' - )) ^ -1) - * (fill * Cg((P('FUNC_API_REMOTE_IMPL') * Cc(true)), 'remote_impl') ^ -1) - * (fill * Cg((P('FUNC_API_COMPOSITOR_IMPL') * Cc(true)), 'compositor_impl') ^ -1) - * (fill * Cg((P('FUNC_API_CLIENT_IMPL') * Cc(true)), 'client_impl') ^ -1) - * (fill * Cg((P('FUNC_API_CLIENT_IGNORE') * Cc(true)), 'client_ignore') ^ -1) - * fill - * (P(';') + (P('{') * nl + (impl_line ^ 0) * P('}'))) +local proto_name = opt_ptr * fill * id + +-- __inline is used in MSVC +local decl_mod = ( + Cg(word('static') * Cc(true), 'static') + + Cg((word('inline') + word('__inline')) * Cc(true), 'inline') ) -local dict_key = P('DictKey(') * Cg(rep1(any - P(')')), 'dict_key') * P(')') -local keyset_field = - Ct(Cg(c_id, 'type') * ws * Cg(c_id, 'name') * fill * (dict_key ^ -1) * fill * P(';') * fill) -local c_keyset = Ct( - P('typedef') - * ws - * P('struct') +local proto = Ct( + Cg(Cp(), 'pos') + * Cc('proto') + * -#P('typedef') + * #alpha + * opt(P('DLLEXPORT') * rep1(ws)) + * rep(decl_mod) + * Cg(return_type, 'return_type') * fill - * P('{') + * Cg(proto_name, 'name') * fill - * Cg(Ct(keyset_field ^ 1), 'fields') - * P('}') - * fill - * P('Dict') + * paren(Cg(params, 'parameters')) + * Cg(Cc(false), 'fast') + * rep(fill * fattr) + * Cg(Cp(), 'endpos') + * (fill * (S(';') + braces)) +) + +local keyset_field = Ct( + Cg(ctype, 'type') * fill - * P('(') - * Cg(c_id, 'keyset_name') + * Cg(id, 'name') * fill - * P(')') - * P(';') + * opt(P('DictKey') * paren(Cg(rep1(1 - P(')')), 'dict_key'))) + * Pf(';') ) -local grammar = Ct( - rep1(empty_line + c_proto + cdoc_comment + c_comment + c_preproc + ws + c_keyset + ignore_line) +local keyset = Ct( + P('typedef') + * word('struct') + * Pf('{') + * Cg(Ct(rep1(keyset_field)), 'fields') + * Pf('}') + * P('Dict') + * paren(Cg(id, 'keyset_name')) + * Pf(';') ) -return { grammar = grammar, typed_container = typed_container } + +local grammar = + Ct(rep1(empty_line + proto + cdoc_comment + comment + preproc + ws + keyset + ignore_line)) + +if arg[1] == '--test' then + for i, t in ipairs({ + 'void multiqueue_put_event(MultiQueue *self, Event event) {} ', + 'void *xmalloc(size_t size) {} ', + { + 'struct tm *os_localtime_r(const time_t *restrict clock,', + ' struct tm *restrict result) FUNC_ATTR_NONNULL_ALL {}', + }, + { + '_Bool', + '# 163 "src/nvim/event/multiqueue.c"', + ' multiqueue_empty(MultiQueue *self)', + '{}', + }, + 'const char *find_option_end(const char *arg, OptIndex *opt_idxp) {}', + 'bool semsg(const char *const fmt, ...) {}', + 'int32_t utf_ptr2CharInfo_impl(uint8_t const *p, uintptr_t const len) {}', + 'void ex_argdedupe(exarg_T *eap FUNC_ATTR_UNUSED) {}', + 'static TermKeySym register_c0(TermKey *tk, TermKeySym sym, unsigned char ctrl, const char *name) {}', + 'unsigned get_bkc_flags(buf_T *buf) {}', + 'char *xstpcpy(char *restrict dst, const char *restrict src) {}', + 'bool try_leave(const TryState *const tstate, Error *const err) {}', + 'void api_set_error(ErrorType errType) {}', + + -- Do not consume leading preproc statements + { + '#line 1 "D:/a/neovim/neovim/src\\nvim/mark.h"', + 'static __inline int mark_global_index(const char name)', + ' FUNC_ATTR_CONST', + '{}', + }, + { + '', + '#line 1 "D:/a/neovim/neovim/src\\nvim/mark.h"', + 'static __inline int mark_global_index(const char name)', + '{}', + }, + { + 'size_t xstrlcpy(char *__restrict dst, const char *__restrict src, size_t dsize)', + ' FUNC_ATTR_NONNULL_ALL', + ' {}', + }, + }) do + if type(t) == 'table' then + t = table.concat(t, '\n') .. '\n' + end + t = t:gsub(' +', ' ') + local r = grammar:match(t) + if not r then + print('Test ' .. i .. ' failed') + print(' |' .. table.concat(vim.split(t, '\n'), '\n |')) + end + end +end + +return { + grammar = grammar --[[@as nvim.c_grammar]], + typed_container = typed_container, +} diff --git a/src/nvim/generators/gen_api_ui_events.lua b/src/nvim/generators/gen_api_ui_events.lua index 30a83330eb..a3bb76cb91 100644 --- a/src/nvim/generators/gen_api_ui_events.lua +++ b/src/nvim/generators/gen_api_ui_events.lua @@ -98,7 +98,7 @@ local function call_ui_event_method(output, ev) end events = vim.tbl_filter(function(ev) - return ev[1] ~= 'empty' + return ev[1] ~= 'empty' and ev[1] ~= 'preproc' end, events) for i = 1, #events do diff --git a/src/nvim/generators/gen_declarations.lua b/src/nvim/generators/gen_declarations.lua index 2ec0e9ab68..6e1ea92572 100644 --- a/src/nvim/generators/gen_declarations.lua +++ b/src/nvim/generators/gen_declarations.lua @@ -1,136 +1,105 @@ -local fname = arg[1] -local static_fname = arg[2] -local non_static_fname = arg[3] -local preproc_fname = arg[4] -local static_basename = arg[5] - -local lpeg = vim.lpeg - -local fold = function(func, ...) - local result = nil - for _, v in ipairs({ ... }) do - if result == nil then - result = v - else - result = func(result, v) +local grammar = require('generators.c_grammar').grammar + +--- @param fname string +--- @return string? +local function read_file(fname) + local f = io.open(fname, 'r') + if not f then + return + end + local contents = f:read('*a') + f:close() + return contents +end + +--- @param fname string +--- @param contents string[] +local function write_file(fname, contents) + local contents_s = table.concat(contents, '\n') .. '\n' + local fcontents = read_file(fname) + if fcontents == contents_s then + return + end + local f = assert(io.open(fname, 'w')) + f:write(contents_s) + f:close() +end + +--- @param fname string +--- @param non_static_fname string +--- @return string? non_static +local function add_iwyu_non_static(fname, non_static_fname) + if fname:find('.*/src/nvim/.*%.c$') then + -- Add an IWYU pragma comment if the corresponding .h file exists. + local header_fname = fname:sub(1, -3) .. '.h' + local header_f = io.open(header_fname, 'r') + if header_f then + header_f:close() + return (header_fname:gsub('.*/src/nvim/', 'nvim/')) end + elseif non_static_fname:find('/include/api/private/dispatch_wrappers%.h%.generated%.h$') then + return 'nvim/api/private/dispatch.h' + elseif non_static_fname:find('/include/ui_events_call%.h%.generated%.h$') then + return 'nvim/ui.h' + elseif non_static_fname:find('/include/ui_events_client%.h%.generated%.h$') then + return 'nvim/ui_client.h' + elseif non_static_fname:find('/include/ui_events_remote%.h%.generated%.h$') then + return 'nvim/api/ui.h' end - return result end -local folder = function(func) - return function(...) - return fold(func, ...) +--- @param d string +local function process_decl(d) + -- Comments are really handled by preprocessor, so the following is not + -- needed + d = d:gsub('/%*.-%*/', '') + d = d:gsub('//.-\n', '\n') + d = d:gsub('# .-\n', '') + d = d:gsub('\n', ' ') + d = d:gsub('%s+', ' ') + d = d:gsub(' ?%( ?', '(') + d = d:gsub(' ?, ?', ', ') + d = d:gsub(' ?(%*+) ?', ' %1') + d = d:gsub(' ?(FUNC_ATTR_)', ' %1') + d = d:gsub(' $', '') + d = d:gsub('^ ', '') + return d .. ';' +end + +--- @param fname string +--- @param text string +--- @return string[] static +--- @return string[] non_static +--- @return boolean any_static +local function gen_declarations(fname, text) + local non_static = {} --- @type string[] + local static = {} --- @type string[] + + local neededfile = fname:match('[^/]+$') + local curfile = nil + local any_static = false + for _, node in ipairs(grammar:match(text)) do + if node[1] == 'preproc' then + curfile = node.content:match('^%a* %d+ "[^"]-/?([^"/]+)"') or curfile + elseif node[1] == 'proto' and curfile == neededfile then + local node_text = text:sub(node.pos, node.endpos - 1) + local declaration = process_decl(node_text) + + if node.static then + if not any_static and declaration:find('FUNC_ATTR_') then + any_static = true + end + static[#static + 1] = declaration + else + non_static[#non_static + 1] = 'DLLEXPORT ' .. declaration + end + end end -end -local lit = lpeg.P -local set = function(...) - return lpeg.S(fold(function(a, b) - return a .. b - end, ...)) -end -local any_character = lpeg.P(1) -local rng = function(s, e) - return lpeg.R(s .. e) -end -local concat = folder(function(a, b) - return a * b -end) -local branch = folder(function(a, b) - return a + b -end) -local one_or_more = function(v) - return v ^ 1 -end -local two_or_more = function(v) - return v ^ 2 -end -local any_amount = function(v) - return v ^ 0 -end -local one_or_no = function(v) - return v ^ -1 -end -local look_behind = lpeg.B -local look_ahead = function(v) - return #v -end -local neg_look_ahead = function(v) - return -v -end -local neg_look_behind = function(v) - return -look_behind(v) + return static, non_static, any_static end -local w = branch(rng('a', 'z'), rng('A', 'Z'), lit('_')) -local aw = branch(w, rng('0', '9')) -local s = set(' ', '\n', '\t') -local raw_word = concat(w, any_amount(aw)) -local right_word = concat(raw_word, neg_look_ahead(aw)) -local word = branch( - concat( - branch(lit('ArrayOf('), lit('DictOf('), lit('Dict(')), -- typed container macro - one_or_more(any_character - lit(')')), - lit(')') - ), - concat(neg_look_behind(aw), right_word) -) -local inline_comment = - concat(lit('/*'), any_amount(concat(neg_look_ahead(lit('*/')), any_character)), lit('*/')) -local spaces = any_amount(branch( - s, - -- Comments are really handled by preprocessor, so the following is not needed - inline_comment, - concat(lit('//'), any_amount(concat(neg_look_ahead(lit('\n')), any_character)), lit('\n')), - -- Linemarker inserted by preprocessor - concat(lit('# '), any_amount(concat(neg_look_ahead(lit('\n')), any_character)), lit('\n')) -)) -local typ_part = concat(word, any_amount(concat(spaces, lit('*'))), spaces) - -local typ_id = two_or_more(typ_part) -local arg = typ_id -- argument name is swallowed by typ -local pattern = concat( - any_amount(branch(set(' ', '\t'), inline_comment)), - typ_id, -- return type with function name - spaces, - lit('('), - spaces, - one_or_no(branch( -- function arguments - concat( - arg, -- first argument, does not require comma - any_amount(concat( -- following arguments, start with a comma - spaces, - lit(','), - spaces, - arg, - any_amount(concat(lit('['), spaces, any_amount(aw), spaces, lit(']'))) - )), - one_or_no(concat(spaces, lit(','), spaces, lit('...'))) - ), - lit('void') -- also accepts just void - )), - spaces, - lit(')'), - any_amount(concat( -- optional attributes - spaces, - lit('FUNC_'), - any_amount(aw), - one_or_no(concat( -- attribute argument - spaces, - lit('('), - any_amount(concat(neg_look_ahead(lit(')')), any_character)), - lit(')') - )) - )), - look_ahead(concat( -- definition must be followed by "{" - spaces, - lit('{') - )) -) - -if fname == '--help' then - print([[ +local usage = [[ Usage: gen_declarations.lua definitions.c static.h non-static.h definitions.i @@ -141,204 +110,77 @@ non-static.h. File `definitions.i' should contain an already preprocessed version of definitions.c and it is the only one which is actually parsed, definitions.c is needed only to determine functions from which file out of all functions found in definitions.i are needed and to generate an IWYU comment. - -Additionally uses the following environment variables: - - NVIM_GEN_DECLARATIONS_LINE_NUMBERS: - If set to 1 then all generated declarations receive a comment with file - name and line number after the declaration. This may be useful for - debugging gen_declarations script, but not much beyond that with - configured development environment (i.e. with with clang/etc). - - WARNING: setting this to 1 will cause extensive rebuilds: declarations - generator script will not regenerate non-static.h file if its - contents did not change, but including line numbers will make - contents actually change. - - With contents changed timestamp of the file is regenerated even - when no real changes were made (e.g. a few lines were added to - a function which is not at the bottom of the file). - - With changed timestamp build system will assume that header - changed, triggering rebuilds of all C files which depend on the - "changed" header. -]]) - os.exit() -end - -local preproc_f = io.open(preproc_fname) -local text = preproc_f:read('*all') -preproc_f:close() - -local non_static = [[ -#define DEFINE_FUNC_ATTRIBUTES -#include "nvim/func_attr.h" -#undef DEFINE_FUNC_ATTRIBUTES -#ifndef DLLEXPORT -# ifdef MSWIN -# define DLLEXPORT __declspec(dllexport) -# else -# define DLLEXPORT -# endif -#endif -]] - -local static = [[ -#define DEFINE_FUNC_ATTRIBUTES -#include "nvim/func_attr.h" -#undef DEFINE_FUNC_ATTRIBUTES ]] -local non_static_footer = [[ -#include "nvim/func_attr.h" -]] +local function main() + local fname = arg[1] + local static_fname = arg[2] + local non_static_fname = arg[3] + local preproc_fname = arg[4] + local static_basename = arg[5] -local static_footer = [[ -#define DEFINE_EMPTY_ATTRIBUTES -#include "nvim/func_attr.h" // IWYU pragma: export -]] - -if fname:find('.*/src/nvim/.*%.c$') then - -- Add an IWYU pragma comment if the corresponding .h file exists. - local header_fname = fname:sub(1, -3) .. '.h' - local header_f = io.open(header_fname, 'r') - if header_f ~= nil then - header_f:close() - non_static = ([[ -// IWYU pragma: private, include "%s" -]]):format(header_fname:gsub('.*/src/nvim/', 'nvim/')) .. non_static + if fname == '--help' or #arg < 5 then + print(usage) + os.exit() end -elseif fname:find('.*/src/nvim/.*%.h$') then - static = ([[ -// IWYU pragma: private, include "%s" -]]):format(fname:gsub('.*/src/nvim/', 'nvim/')) .. static -elseif non_static_fname:find('/include/api/private/dispatch_wrappers%.h%.generated%.h$') then - non_static = [[ -// IWYU pragma: private, include "nvim/api/private/dispatch.h" -]] .. non_static -elseif non_static_fname:find('/include/ui_events_call%.h%.generated%.h$') then - non_static = [[ -// IWYU pragma: private, include "nvim/ui.h" -]] .. non_static -elseif non_static_fname:find('/include/ui_events_client%.h%.generated%.h$') then - non_static = [[ -// IWYU pragma: private, include "nvim/ui_client.h" -]] .. non_static -elseif non_static_fname:find('/include/ui_events_remote%.h%.generated%.h$') then - non_static = [[ -// IWYU pragma: private, include "nvim/api/ui.h" -]] .. non_static -end -local filepattern = '^#%a* (%d+) "([^"]-)/?([^"/]+)"' + local text = assert(read_file(preproc_fname)) -local init = 1 -local curfile = nil -local neededfile = fname:match('[^/]+$') -local declline = 0 -local declendpos = 0 -local curdir = nil -local is_needed_file = false -local init_is_nl = true -local any_static = false -while init ~= nil do - if init_is_nl and text:sub(init, init) == '#' then - local line, dir, file = text:match(filepattern, init) - if file ~= nil then - curfile = file - is_needed_file = (curfile == neededfile) - declline = tonumber(line) - 1 - curdir = dir:gsub('.*/src/nvim/', '') - else - declline = declline - 1 - end - elseif init < declendpos then -- luacheck: ignore 542 - -- Skipping over declaration - elseif is_needed_file then - s = init - local e = pattern:match(text, init) - if e ~= nil then - local declaration = text:sub(s, e - 1) - -- Comments are really handled by preprocessor, so the following is not - -- needed - declaration = declaration:gsub('/%*.-%*/', '') - declaration = declaration:gsub('//.-\n', '\n') - - declaration = declaration:gsub('# .-\n', '') + local static_decls, non_static_decls, any_static = gen_declarations(fname, text) - declaration = declaration:gsub('\n', ' ') - declaration = declaration:gsub('%s+', ' ') - declaration = declaration:gsub(' ?%( ?', '(') - -- declaration = declaration:gsub(' ?%) ?', ')') - declaration = declaration:gsub(' ?, ?', ', ') - declaration = declaration:gsub(' ?(%*+) ?', ' %1') - declaration = declaration:gsub(' ?(FUNC_ATTR_)', ' %1') - declaration = declaration:gsub(' $', '') - declaration = declaration:gsub('^ ', '') - declaration = declaration .. ';' - - if os.getenv('NVIM_GEN_DECLARATIONS_LINE_NUMBERS') == '1' then - declaration = declaration .. (' // %s/%s:%u'):format(curdir, curfile, declline) - end - declaration = declaration .. '\n' - if declaration:sub(1, 6) == 'static' then - if declaration:find('FUNC_ATTR_') then - any_static = true - end - static = static .. declaration - else - declaration = 'DLLEXPORT ' .. declaration - non_static = non_static .. declaration - end - declendpos = e - end - end - init = text:find('[\n;}]', init) - if init == nil then - break - end - init_is_nl = text:sub(init, init) == '\n' - init = init + 1 - if init_is_nl and is_needed_file then - declline = declline + 1 + local static = {} --- @type string[] + if fname:find('.*/src/nvim/.*%.h$') then + static[#static + 1] = ('// IWYU pragma: private, include "%s"'):format( + fname:gsub('.*/src/nvim/', 'nvim/') + ) end -end - -non_static = non_static .. non_static_footer -static = static .. static_footer - -local F -F = io.open(static_fname, 'w') -F:write(static) -F:close() - -if any_static then - F = io.open(fname, 'r') - local orig_text = F:read('*a') - local pat = '\n#%s?include%s+"' .. static_basename .. '"\n' - local pat_comment = '\n#%s?include%s+"' .. static_basename .. '"%s*//' - if not string.find(orig_text, pat) and not string.find(orig_text, pat_comment) then - error('fail: missing include for ' .. static_basename .. ' in ' .. fname) + vim.list_extend(static, { + '#define DEFINE_FUNC_ATTRIBUTES', + '#include "nvim/func_attr.h"', + '#undef DEFINE_FUNC_ATTRIBUTES', + }) + vim.list_extend(static, static_decls) + vim.list_extend(static, { + '#define DEFINE_EMPTY_ATTRIBUTES', + '#include "nvim/func_attr.h" // IWYU pragma: export', + '', + }) + + write_file(static_fname, static) + + if any_static then + local orig_text = assert(read_file(fname)) + local pat = '\n#%s?include%s+"' .. static_basename .. '"\n' + local pat_comment = '\n#%s?include%s+"' .. static_basename .. '"%s*//' + if not orig_text:find(pat) and not orig_text:find(pat_comment) then + error(('fail: missing include for %s in %s'):format(static_basename, fname)) + end end - F:close() -end -if non_static_fname == 'SKIP' then - return -- only want static declarations -end - --- Before generating the non-static headers, check if the current file (if --- exists) is different from the new one. If they are the same, we won't touch --- the current version to avoid triggering an unnecessary rebuilds of modules --- that depend on this one -F = io.open(non_static_fname, 'r') -if F ~= nil then - if F:read('*a') == non_static then - os.exit(0) + if non_static_fname ~= 'SKIP' then + local non_static = {} --- @type string[] + local iwyu_non_static = add_iwyu_non_static(fname, non_static_fname) + if iwyu_non_static then + non_static[#non_static + 1] = ('// IWYU pragma: private, include "%s"'):format( + iwyu_non_static + ) + end + vim.list_extend(non_static, { + '#define DEFINE_FUNC_ATTRIBUTES', + '#include "nvim/func_attr.h"', + '#undef DEFINE_FUNC_ATTRIBUTES', + '#ifndef DLLEXPORT', + '# ifdef MSWIN', + '# define DLLEXPORT __declspec(dllexport)', + '# else', + '# define DLLEXPORT', + '# endif', + '#endif', + }) + vim.list_extend(non_static, non_static_decls) + non_static[#non_static + 1] = '#include "nvim/func_attr.h"' + write_file(non_static_fname, non_static) end - F:close() end -F = io.open(non_static_fname, 'w') -F:write(non_static) -F:close() +return main() diff --git a/src/nvim/option.c b/src/nvim/option.c index 27b80c0ac8..6da9635479 100644 --- a/src/nvim/option.c +++ b/src/nvim/option.c @@ -2764,10 +2764,11 @@ static const char *check_num_option_bounds(OptIndex opt_idx, OptInt *newval, cha switch (opt_idx) { case kOptLines: - if (*newval < min_rows() && full_screen) { - vim_snprintf(errbuf, errbuflen, _("E593: Need at least %d lines"), min_rows()); + if (*newval < min_rows_for_all_tabpages() && full_screen) { + vim_snprintf(errbuf, errbuflen, _("E593: Need at least %d lines"), + min_rows_for_all_tabpages()); errmsg = errbuf; - *newval = min_rows(); + *newval = min_rows_for_all_tabpages(); } // True max size is defined by check_screensize(). *newval = MIN(*newval, INT_MAX); diff --git a/src/nvim/options.lua b/src/nvim/options.lua index bcb05b107b..84c90e44a7 100644 --- a/src/nvim/options.lua +++ b/src/nvim/options.lua @@ -8407,6 +8407,7 @@ return { All fields except the {item} are optional. A single percent sign can be given as "%%". + *stl-%!* When the option starts with "%!" then it is used as an expression, evaluated and the result is used as the option value. Example: >vim set statusline=%!MyStatusLine() diff --git a/src/nvim/optionstr.c b/src/nvim/optionstr.c index b47517b1a2..918443db9f 100644 --- a/src/nvim/optionstr.c +++ b/src/nvim/optionstr.c @@ -2191,7 +2191,11 @@ static const char *did_set_statustabline_rulerformat(optset_T *args, bool rulerf if (wid && *s == '(' && (errmsg = check_stl_option(p_ruf)) == NULL) { ru_wid = wid; } else { - errmsg = check_stl_option(p_ruf); + // Validate the flags in 'rulerformat' only if it doesn't point to + // a custom function ("%!" flag). + if ((*varp)[1] != '!') { + errmsg = check_stl_option(p_ruf); + } } } else if (rulerformat || s[0] != '%' || s[1] != '!') { // check 'statusline', 'winbar', 'tabline' or 'statuscolumn' diff --git a/src/nvim/tui/input.c b/src/nvim/tui/input.c index 98dd7b4b45..744d306c06 100644 --- a/src/nvim/tui/input.c +++ b/src/nvim/tui/input.c @@ -160,12 +160,16 @@ void tinput_init(TermInput *input, Loop *loop) // initialize a timer handle for handling ESC with libtermkey uv_timer_init(&loop->uv, &input->timer_handle); input->timer_handle.data = input; + + uv_timer_init(&loop->uv, &input->bg_query_timer); + input->bg_query_timer.data = input; } void tinput_destroy(TermInput *input) { map_destroy(int, &kitty_key_map); uv_close((uv_handle_t *)&input->timer_handle, NULL); + uv_close((uv_handle_t *)&input->bg_query_timer, NULL); rstream_may_close(&input->read_stream); termkey_destroy(input->tk); } @@ -179,6 +183,7 @@ void tinput_stop(TermInput *input) { rstream_stop(&input->read_stream); uv_timer_stop(&input->timer_handle); + uv_timer_stop(&input->bg_query_timer); } static void tinput_done_event(void **argv) @@ -474,6 +479,13 @@ static void tinput_timer_cb(uv_timer_t *handle) tinput_flush(input); } +static void bg_query_timer_cb(uv_timer_t *handle) + FUNC_ATTR_NONNULL_ALL +{ + TermInput *input = handle->data; + tui_query_bg_color(input->tui_data); +} + /// Handle focus events. /// /// If the upcoming sequence of bytes in the input stream matches the termcode @@ -660,6 +672,33 @@ static void handle_unknown_csi(TermInput *input, const TermKeyKey *key) } } break; + case 'n': + // Device Status Report (DSR) + if (nparams == 2) { + int args[2]; + for (size_t i = 0; i < ARRAY_SIZE(args); i++) { + if (termkey_interpret_csi_param(params[i], &args[i], NULL, NULL) != TERMKEY_RES_KEY) { + return; + } + } + + if (args[0] == 997) { + // Theme update notification + // https://github.com/contour-terminal/contour/blob/master/docs/vt-extensions/color-palette-update-notifications.md + // The second argument tells us whether the OS theme is set to light + // mode or dark mode, but all we care about is the background color of + // the terminal emulator. We query for that with OSC 11 and the response + // is handled by the autocommand created in _defaults.lua. The terminal + // may send us multiple notifications all at once so we use a timer to + // coalesce the queries. + if (uv_timer_get_due_in(&input->bg_query_timer) > 0) { + return; + } + + uv_timer_start(&input->bg_query_timer, bg_query_timer_cb, 100, 0); + } + } + break; default: break; } diff --git a/src/nvim/tui/input.h b/src/nvim/tui/input.h index 4c2baf908e..f6aaff30de 100644 --- a/src/nvim/tui/input.h +++ b/src/nvim/tui/input.h @@ -32,6 +32,7 @@ typedef struct { TermKey *tk; TermKey_Terminfo_Getstr_Hook *tk_ti_hook_fn; ///< libtermkey terminfo hook uv_timer_t timer_handle; + uv_timer_t bg_query_timer; ///< timer used to batch background color queries Loop *loop; RStream read_stream; TUIData *tui_data; diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 6e0012096b..603db5b891 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -241,16 +241,19 @@ void tui_handle_term_mode(TUIData *tui, TermMode mode, TermModeState state) tui->unibi_ext.sync = (int)unibi_add_ext_str(tui->ut, "Sync", "\x1b[?2026%?%p1%{1}%-%tl%eh%;"); break; - case kTermModeResizeEvents: - signal_watcher_stop(&tui->winch_handle); - tui_set_term_mode(tui, mode, true); - break; case kTermModeGraphemeClusters: if (!is_set) { tui_set_term_mode(tui, mode, true); tui->did_set_grapheme_cluster_mode = true; } break; + case kTermModeThemeUpdates: + tui_set_term_mode(tui, mode, true); + break; + case kTermModeResizeEvents: + signal_watcher_stop(&tui->winch_handle); + tui_set_term_mode(tui, mode, true); + break; } } } @@ -320,6 +323,18 @@ static void tui_reset_key_encoding(TUIData *tui) } } +/// Write the OSC 11 sequence to the terminal emulator to query the current +/// background color. +/// +/// The response will be handled by the TermResponse autocommand created in +/// _defaults.lua. +void tui_query_bg_color(TUIData *tui) + FUNC_ATTR_NONNULL_ALL +{ + out(tui, S_LEN("\x1b]11;?\x07")); + flush_buf(tui); +} + /// Enable the alternate screen and emit other control sequences to start the TUI. /// /// This is also called when the TUI is resumed after being suspended. We reinitialize all state @@ -438,14 +453,13 @@ static void terminfo_start(TUIData *tui) // Enable bracketed paste unibi_out_ext(tui, tui->unibi_ext.enable_bracketed_paste); - // Query support for mode 2026 (Synchronized Output). Some terminals also - // support an older DCS sequence for synchronized output, but we will only use - // mode 2026. + // Query support for private DEC modes that Nvim can take advantage of. // Some terminals (such as Terminal.app) do not support DECRQM, so skip the query. if (!nsterm) { tui_request_term_mode(tui, kTermModeSynchronizedOutput); - tui_request_term_mode(tui, kTermModeResizeEvents); tui_request_term_mode(tui, kTermModeGraphemeClusters); + tui_request_term_mode(tui, kTermModeThemeUpdates); + tui_request_term_mode(tui, kTermModeResizeEvents); } // Don't use DECRQSS in screen or tmux, as they behave strangely when receiving it. @@ -493,6 +507,10 @@ static void terminfo_start(TUIData *tui) /// Disable the alternate screen and prepare for the TUI to close. static void terminfo_stop(TUIData *tui) { + // Disable theme update notifications. We do this first to avoid getting any + // more notifications after we reset the cursor and any color palette changes. + tui_set_term_mode(tui, kTermModeThemeUpdates, false); + // Destroy output stuff tui_mode_change(tui, NULL_STRING, SHAPE_IDX_N); tui_mouse_off(tui); @@ -509,6 +527,7 @@ static void terminfo_stop(TUIData *tui) if (tui->did_set_grapheme_cluster_mode) { tui_set_term_mode(tui, kTermModeGraphemeClusters, false); } + // May restore old title before exiting alternate screen. tui_set_title(tui, NULL_STRING); if (ui_client_exit_status == 0) { diff --git a/src/nvim/tui/tui_defs.h b/src/nvim/tui/tui_defs.h index bd99d6b0ad..5d6f027bf7 100644 --- a/src/nvim/tui/tui_defs.h +++ b/src/nvim/tui/tui_defs.h @@ -5,6 +5,7 @@ typedef struct TUIData TUIData; typedef enum { kTermModeSynchronizedOutput = 2026, kTermModeGraphemeClusters = 2027, + kTermModeThemeUpdates = 2031, kTermModeResizeEvents = 2048, } TermMode; diff --git a/src/nvim/window.c b/src/nvim/window.c index ac4c5a8e4a..d92b2ab601 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -6189,7 +6189,7 @@ const char *did_set_winminheight(optset_T *args FUNC_ATTR_UNUSED) // loop until there is a 'winminheight' that is possible while (p_wmh > 0) { const int room = Rows - (int)p_ch; - const int needed = min_rows(); + const int needed = min_rows_for_all_tabpages(); if (room >= needed) { break; } @@ -7072,6 +7072,22 @@ int min_rows(void) return MIN_LINES; } + int total = frame_minheight(curtab->tp_topframe, NULL); + total += tabline_height() + global_stl_height(); + if (p_ch > 0) { + total += 1; // count the room for the command line + } + return total; +} + +/// Return the minimal number of rows that is needed on the screen to display +/// the current number of windows for all tab pages. +int min_rows_for_all_tabpages(void) +{ + if (firstwin == NULL) { // not initialized yet + return MIN_LINES; + } + int total = 0; FOR_ALL_TABS(tp) { int n = frame_minheight(tp->tp_topframe, NULL); diff --git a/test/benchmark/text_spec.lua b/test/benchmark/text_spec.lua new file mode 100644 index 0000000000..9cfeaf765b --- /dev/null +++ b/test/benchmark/text_spec.lua @@ -0,0 +1,52 @@ +describe('vim.text', function() + --- @param t number[] + local function mean(t) + assert(#t > 0) + local sum = 0 + for _, v in ipairs(t) do + sum = sum + v + end + return sum / #t + end + + --- @param t number[] + local function median(t) + local len = #t + if len % 2 == 0 then + return t[len / 2] + end + return t[(len + 1) / 2] + end + + --- @param f fun(t: number[]): table<number, number|string|table> + local function measure(f, input, N) + local stats = {} ---@type number[] + for _ = 1, N do + local tic = vim.uv.hrtime() + f(input) + local toc = vim.uv.hrtime() + stats[#stats + 1] = (toc - tic) / 1000000 + end + table.sort(stats) + print( + string.format( + '\nN: %d, Min: %0.6f ms, Max: %0.6f ms, Median: %0.6f ms, Mean: %0.6f ms', + N, + math.min(unpack(stats)), + math.max(unpack(stats)), + median(stats), + mean(stats) + ) + ) + end + + local input, output = string.rep('😂', 2 ^ 16), string.rep('F09F9882', 2 ^ 16) + + it('hexencode', function() + measure(vim.text.hexencode, input, 100) + end) + + it('hexdecode', function() + measure(vim.text.hexdecode, output, 100) + end) +end) diff --git a/test/functional/legacy/cmdline_spec.lua b/test/functional/legacy/cmdline_spec.lua index bf146e1322..3addcb957c 100644 --- a/test/functional/legacy/cmdline_spec.lua +++ b/test/functional/legacy/cmdline_spec.lua @@ -216,6 +216,22 @@ describe('cmdline', function() longish | ]] end) + + -- oldtest: Test_rulerformat_function() + it("'rulerformat' can use %!", function() + local screen = Screen.new(40, 2) + exec([[ + func TestRulerFn() + return '10,20%=30%%' + endfunc + ]]) + api.nvim_set_option_value('ruler', true, {}) + api.nvim_set_option_value('rulerformat', '%!TestRulerFn()', {}) + screen:expect([[ + ^ | + 10,20 30% | + ]]) + end) end) describe('cmdwin', function() diff --git a/test/functional/lua/loader_spec.lua b/test/functional/lua/loader_spec.lua index 8508f2aa14..20d3a940b2 100644 --- a/test/functional/lua/loader_spec.lua +++ b/test/functional/lua/loader_spec.lua @@ -10,7 +10,17 @@ local eq = t.eq describe('vim.loader', function() before_each(clear) - it('can work in compatibility with --luamod-dev #27413', function() + it('can be disabled', function() + exec_lua(function() + local orig_loader = _G.loadfile + vim.loader.enable() + assert(orig_loader ~= _G.loadfile) + vim.loader.enable(false) + assert(orig_loader == _G.loadfile) + end) + end) + + it('works with --luamod-dev #27413', function() clear({ args = { '--luamod-dev' } }) exec_lua(function() vim.loader.enable() @@ -31,7 +41,7 @@ describe('vim.loader', function() end) end) - it('handles changing files (#23027)', function() + it('handles changing files #23027', function() exec_lua(function() vim.loader.enable() end) @@ -63,7 +73,7 @@ describe('vim.loader', function() ) end) - it('handles % signs in modpath (#24491)', function() + it('handles % signs in modpath #24491', function() exec_lua [[ vim.loader.enable() ]] @@ -82,7 +92,7 @@ describe('vim.loader', function() eq(2, exec_lua('return loadfile(...)()', tmp2)) end) - it('correct indent on error message (#29809)', function() + it('indents error message #29809', function() local errmsg = exec_lua [[ vim.loader.enable() local _, errmsg = pcall(require, 'non_existent_module') diff --git a/test/functional/lua/text_spec.lua b/test/functional/lua/text_spec.lua index be471bfd62..dd08a6ec04 100644 --- a/test/functional/lua/text_spec.lua +++ b/test/functional/lua/text_spec.lua @@ -26,5 +26,21 @@ describe('vim.text', function() eq(output, vim.text.hexencode(input)) eq(input, vim.text.hexdecode(output)) end) + + it('errors on invalid input', function() + -- Odd number of hex characters + do + local res, err = vim.text.hexdecode('ABC') + eq(nil, res) + eq('string must have an even number of hex characters', err) + end + + -- Non-hexadecimal input + do + local res, err = vim.text.hexdecode('nothex') + eq(nil, res) + eq('string must contain only hex characters', err) + end + end) end) end) diff --git a/test/functional/plugin/lsp/folding_range_spec.lua b/test/functional/plugin/lsp/folding_range_spec.lua new file mode 100644 index 0000000000..7e68a598d2 --- /dev/null +++ b/test/functional/plugin/lsp/folding_range_spec.lua @@ -0,0 +1,647 @@ +local t = require('test.testutil') +local n = require('test.functional.testnvim')() +local Screen = require('test.functional.ui.screen') +local t_lsp = require('test.functional.plugin.lsp.testutil') + +local eq = t.eq +local tempname = t.tmpname + +local clear_notrace = t_lsp.clear_notrace +local create_server_definition = t_lsp.create_server_definition + +local api = n.api +local exec_lua = n.exec_lua +local insert = n.insert +local command = n.command +local feed = n.feed + +describe('vim.lsp.folding_range', function() + local text = [[// foldLevel() {{{2 +/// @return fold level at line number "lnum" in the current window. +static int foldLevel(linenr_T lnum) +{ + // While updating the folds lines between invalid_top and invalid_bot have + // an undefined fold level. Otherwise update the folds first. + if (invalid_top == 0) { + checkupdate(curwin); + } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { + return prev_lnum_lvl; + } else if (lnum >= invalid_top && lnum <= invalid_bot) { + return -1; + } + + // Return quickly when there is no folding at all in this window. + if (!hasAnyFolding(curwin)) { + return 0; + } + + return foldLevelWin(curwin, lnum); +}]] + + local result = { + { + endLine = 19, + kind = 'region', + startCharacter = 1, + startLine = 3, + }, + { + endCharacter = 2, + endLine = 7, + kind = 'region', + startCharacter = 25, + startLine = 6, + }, + { + endCharacter = 2, + endLine = 9, + kind = 'region', + startCharacter = 55, + startLine = 8, + }, + { + endCharacter = 2, + endLine = 11, + kind = 'region', + startCharacter = 58, + startLine = 10, + }, + { + endCharacter = 2, + endLine = 16, + kind = 'region', + startCharacter = 31, + startLine = 15, + }, + { + endCharacter = 68, + endLine = 1, + kind = 'comment', + startCharacter = 2, + startLine = 0, + }, + { + endCharacter = 64, + endLine = 5, + kind = 'comment', + startCharacter = 4, + startLine = 4, + }, + } + + local bufnr ---@type integer + local client_id ---@type integer + + clear_notrace() + before_each(function() + clear_notrace() + + exec_lua(create_server_definition) + bufnr = n.api.nvim_get_current_buf() + client_id = exec_lua(function() + _G.server = _G._create_server({ + capabilities = { + foldingRangeProvider = true, + }, + handlers = { + ['textDocument/foldingRange'] = function(_, _, callback) + callback(nil, result) + end, + }, + }) + + vim.api.nvim_win_set_buf(0, bufnr) + + return vim.lsp.start({ name = 'dummy', cmd = _G.server.cmd }) + end) + command('set foldmethod=expr foldcolumn=1 foldlevel=999') + insert(text) + end) + after_each(function() + api.nvim_exec_autocmds('VimLeavePre', { modeline = false }) + end) + + describe('setup()', function() + ---@type integer + local bufnr_set_expr + ---@type integer + local bufnr_never_set_expr + + local function buf_autocmd_num(bufnr_to_check) + return exec_lua(function() + return #vim.api.nvim_get_autocmds({ buffer = bufnr_to_check, event = 'LspNotify' }) + end) + end + + before_each(function() + command([[setlocal foldexpr=v:lua.vim.lsp.foldexpr()]]) + exec_lua(function() + bufnr_set_expr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_set_current_buf(bufnr_set_expr) + end) + insert(text) + command('write ' .. tempname(false)) + command([[setlocal foldexpr=v:lua.vim.lsp.foldexpr()]]) + exec_lua(function() + bufnr_never_set_expr = vim.api.nvim_create_buf(true, false) + vim.api.nvim_set_current_buf(bufnr_never_set_expr) + end) + insert(text) + api.nvim_win_set_buf(0, bufnr_set_expr) + end) + + it('only create event hooks where foldexpr has been set', function() + eq(1, buf_autocmd_num(bufnr)) + eq(1, buf_autocmd_num(bufnr_set_expr)) + eq(0, buf_autocmd_num(bufnr_never_set_expr)) + end) + + it('does not create duplicate event hooks after reloaded', function() + command('edit') + eq(1, buf_autocmd_num(bufnr_set_expr)) + end) + + it('cleans up event hooks when buffer is unloaded', function() + command('bdelete') + eq(0, buf_autocmd_num(bufnr_set_expr)) + end) + end) + + describe('expr()', function() + --- @type test.functional.ui.screen + local screen + before_each(function() + screen = Screen.new(80, 45) + screen:set_default_attr_ids({ + [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue }, + [2] = { bold = true, foreground = Screen.colors.Blue1 }, + [3] = { bold = true, reverse = true }, + [4] = { reverse = true }, + }) + command([[set foldexpr=v:lua.vim.lsp.foldexpr()]]) + command([[split]]) + end) + + it('can compute fold levels', function() + ---@type table<integer, string> + local foldlevels = {} + for i = 1, 21 do + foldlevels[i] = exec_lua('return vim.lsp.foldexpr(' .. i .. ')') + end + eq({ + [1] = '>1', + [2] = '<1', + [3] = '0', + [4] = '>1', + [5] = '>2', + [6] = '<2', + [7] = '>2', + [8] = '<2', + [9] = '>2', + [10] = '<2', + [11] = '>2', + [12] = '<2', + [13] = '1', + [14] = '1', + [15] = '1', + [16] = '>2', + [17] = '<2', + [18] = '1', + [19] = '1', + [20] = '<1', + [21] = '0', + }, foldlevels) + end) + + it('updates folds in all windows', function() + screen:expect({ + grid = [[ +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:[No Name] [+] }| +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }} | +{4:[No Name] [+] }| + | + ]], + }) + end) + + it('persists wherever foldexpr is set', function() + command([[setlocal foldexpr=]]) + feed('<C-w><C-w>zx') + screen:expect({ + grid = [[ +{1: }// foldLevel() {{{2 | +{1: }/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1: }{ | +{1: } // While updating the folds lines between invalid_top and invalid_bot have | +{1: } // an undefined fold level. Otherwise update the folds first. | +{1: } if (invalid_top == 0) { | +{1: } checkupdate(curwin); | +{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1: } return prev_lnum_lvl; | +{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1: } return -1; | +{1: } } | +{1: } | +{1: } // Return quickly when there is no folding at all in this window. | +{1: } if (!hasAnyFolding(curwin)) { | +{1: } return 0; | +{1: } } | +{1: } | +{1: } return foldLevelWin(curwin, lnum); | +{1: }} | +{4:[No Name] [+] }| +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:[No Name] [+] }| + | + ]], + }) + end) + + it('synchronizes changed rows with their previous foldlevels', function() + command('1,2d') + screen:expect({ + grid = [[ +{1: }^static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }} | +{2:~ }|*2 +{3:[No Name] [+] }| +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }} | +{2:~ }|*2 +{4:[No Name] [+] }| + | +]], + }) + end) + + it('clears folds when sole client detaches', function() + exec_lua(function() + vim.lsp.buf_detach_client(bufnr, client_id) + end) + screen:expect({ + grid = [[ +{1: }// foldLevel() {{{2 | +{1: }/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1: }{ | +{1: } // While updating the folds lines between invalid_top and invalid_bot have | +{1: } // an undefined fold level. Otherwise update the folds first. | +{1: } if (invalid_top == 0) { | +{1: } checkupdate(curwin); | +{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1: } return prev_lnum_lvl; | +{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1: } return -1; | +{1: } } | +{1: } | +{1: } // Return quickly when there is no folding at all in this window. | +{1: } if (!hasAnyFolding(curwin)) { | +{1: } return 0; | +{1: } } | +{1: } | +{1: } return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:[No Name] [+] }| +{1: }// foldLevel() {{{2 | +{1: }/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1: }{ | +{1: } // While updating the folds lines between invalid_top and invalid_bot have | +{1: } // an undefined fold level. Otherwise update the folds first. | +{1: } if (invalid_top == 0) { | +{1: } checkupdate(curwin); | +{1: } } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1: } return prev_lnum_lvl; | +{1: } } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1: } return -1; | +{1: } } | +{1: } | +{1: } // Return quickly when there is no folding at all in this window. | +{1: } if (!hasAnyFolding(curwin)) { | +{1: } return 0; | +{1: } } | +{1: } | +{1: } return foldLevelWin(curwin, lnum); | +{1: }} | +{4:[No Name] [+] }| + | + ]], + }) + end) + + it('remains valid after the client re-attaches.', function() + exec_lua(function() + vim.lsp.buf_detach_client(bufnr, client_id) + vim.lsp.buf_attach_client(bufnr, client_id) + end) + screen:expect({ + grid = [[ +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:[No Name] [+] }| +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }} | +{4:[No Name] [+] }| + | + ]], + }) + end) + end) + + describe('foldtext()', function() + --- @type test.functional.ui.screen + local screen + before_each(function() + screen = Screen.new(80, 23) + screen:set_default_attr_ids({ + [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue }, + [2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey }, + [3] = { bold = true, foreground = Screen.colors.Blue1 }, + [4] = { bold = true, reverse = true }, + [5] = { reverse = true }, + }) + command( + [[set foldexpr=v:lua.vim.lsp.foldexpr() foldtext=v:lua.vim.lsp.foldtext() foldlevel=1]] + ) + end) + + it('shows the first folded line if `collapsedText` does not exist', function() + screen:expect({ + grid = [[ +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:+}{2: // While updating the folds lines between invalid_top and invalid_bot have···}| +{1:+}{2: if (invalid_top == 0) {······················································}| +{1:+}{2: } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {························}| +{1:+}{2: } else if (lnum >= invalid_top && lnum <= invalid_bot) {·····················}| +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:+}{2: if (!hasAnyFolding(curwin)) {················································}| +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:~ }|*6 + | + ]], + }) + end) + end) + + describe('foldclose()', function() + --- @type test.functional.ui.screen + local screen + before_each(function() + screen = Screen.new(80, 23) + screen:set_default_attr_ids({ + [1] = { background = Screen.colors.Grey, foreground = Screen.colors.DarkBlue }, + [2] = { foreground = Screen.colors.DarkBlue, background = Screen.colors.LightGrey }, + [3] = { bold = true, foreground = Screen.colors.Blue1 }, + [4] = { bold = true, reverse = true }, + [5] = { reverse = true }, + }) + command([[set foldexpr=v:lua.vim.lsp.foldexpr()]]) + end) + + it('closes all folds of one kind immediately', function() + exec_lua(function() + vim.lsp.foldclose('comment') + end) + screen:expect({ + grid = [[ +{1:+}{2:+-- 2 lines: foldLevel()······················································}| +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:+}{2:+--- 2 lines: While updating the folds lines between invalid_top and invalid_b}| +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:~ }|*3 + | + ]], + }) + end) + + it('closes the smallest fold first', function() + exec_lua(function() + vim.lsp.foldclose('region') + end) + screen:expect({ + grid = [[ +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:+}{2:+-- 17 lines: {································································}| +{1: }^} | +{3:~ }|*17 + | + ]], + }) + command('4foldopen') + screen:expect({ + grid = [[ +{1:-}// foldLevel() {{{2 | +{1:│}/// @return fold level at line number "lnum" in the current window. | +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:-} // While updating the folds lines between invalid_top and invalid_bot have | +{1:2} // an undefined fold level. Otherwise update the folds first. | +{1:+}{2:+--- 2 lines: if (invalid_top == 0) {·········································}| +{1:+}{2:+--- 2 lines: } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) {···········}| +{1:+}{2:+--- 2 lines: } else if (lnum >= invalid_top && lnum <= invalid_bot) {········}| +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:+}{2:+--- 2 lines: if (!hasAnyFolding(curwin)) {···································}| +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:~ }|*5 + | + ]], + }) + end) + + it('is defered when the buffer is not up-to-date', function() + exec_lua(function() + vim.lsp.foldclose('comment') + vim.lsp.util.buf_versions[bufnr] = 0 + end) + screen:expect({ + grid = [[ +{1:+}{2:+-- 2 lines: foldLevel()······················································}| +{1: }static int foldLevel(linenr_T lnum) | +{1:-}{ | +{1:+}{2:+--- 2 lines: While updating the folds lines between invalid_top and invalid_b}| +{1:-} if (invalid_top == 0) { | +{1:2} checkupdate(curwin); | +{1:-} } else if (lnum == prev_lnum && prev_lnum_lvl >= 0) { | +{1:2} return prev_lnum_lvl; | +{1:-} } else if (lnum >= invalid_top && lnum <= invalid_bot) { | +{1:2} return -1; | +{1:│} } | +{1:│} | +{1:│} // Return quickly when there is no folding at all in this window. | +{1:-} if (!hasAnyFolding(curwin)) { | +{1:2} return 0; | +{1:│} } | +{1:│} | +{1:│} return foldLevelWin(curwin, lnum); | +{1: }^} | +{3:~ }|*3 + | + ]], + }) + end) + end) +end) diff --git a/test/old/testdir/test_cmdline.vim b/test/old/testdir/test_cmdline.vim index 6fa3ee5250..2f34ecb414 100644 --- a/test/old/testdir/test_cmdline.vim +++ b/test/old/testdir/test_cmdline.vim @@ -4036,6 +4036,27 @@ func Test_rulerformat_position() call StopVimInTerminal(buf) endfunc +" Test for using "%!" in 'rulerformat' to use a function +func Test_rulerformat_function() + CheckScreendump + + let lines =<< trim END + func TestRulerFn() + return '10,20%=30%%' + endfunc + END + call writefile(lines, 'Xrulerformat_function', 'D') + + let buf = RunVimInTerminal('-S Xrulerformat_function', #{rows: 2, cols: 40}) + call term_sendkeys(buf, ":set ruler rulerformat=%!TestRulerFn()\<CR>") + call term_sendkeys(buf, ":redraw!\<CR>") + call term_wait(buf) + call VerifyScreenDump(buf, 'Test_rulerformat_function', {}) + + " clean up + call StopVimInTerminal(buf) +endfunc + func Test_getcompletion_usercmd() command! -nargs=* -complete=command TestCompletion echo <q-args> diff --git a/test/old/testdir/test_compiler.vim b/test/old/testdir/test_compiler.vim index 07b57b76d9..3ad6b365de 100644 --- a/test/old/testdir/test_compiler.vim +++ b/test/old/testdir/test_compiler.vim @@ -65,10 +65,10 @@ func Test_compiler_completion() call assert_match('^"compiler ' .. clist .. '$', @:) call feedkeys(":compiler p\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_match('"compiler pandoc pbx perl\( p[a-z_]\+\)\+ pylint pyunit', @:) + call assert_match('"compiler pandoc pbx perl\( p[a-z_]\+\)\+ pyunit', @:) call feedkeys(":compiler! p\<C-A>\<C-B>\"\<CR>", 'tx') - call assert_match('"compiler! pandoc pbx perl\( p[a-z_]\+\)\+ pylint pyunit', @:) + call assert_match('"compiler! pandoc pbx perl\( p[a-z_]\+\)\+ pyunit', @:) endfunc func Test_compiler_error() @@ -78,3 +78,197 @@ func Test_compiler_error() call assert_fails('compiler! doesnotexist', 'E666:') unlet! g:current_compiler endfunc + +func s:SpotBugsParseFilterMakePrg(dirname, makeprg) + let result = {} + let result.sourcepath = '' + let result.classfiles = [] + + " Get the argument after the rightmost occurrence of "-sourcepath". + let offset = strridx(a:makeprg, '-sourcepath') + if offset < 0 + return result + endif + let offset += 1 + strlen('-sourcepath') + let result.sourcepath = matchstr(strpart(a:makeprg, offset), '.\{-}\ze[ \t]') + + " Get the class file arguments, dropping the pathname prefix. + let offset = stridx(a:makeprg, a:dirname, offset) + if offset < 0 + return result + endif + + while offset > -1 + let candidate = matchstr(a:makeprg, '[^ \t]\{-}\.class\>', offset) + if empty(candidate) + break + endif + call add(result.classfiles, candidate) + let offset = stridx(a:makeprg, a:dirname, (1 + strlen(candidate) + offset)) + endwhile + + call sort(result.classfiles) + return result +endfunc + +func Test_compiler_spotbugs_makeprg() + let save_shellslash = &shellslash + set shellslash + + call assert_true(mkdir('Xspotbugs/src/tests/α/β/γ/δ', 'pR')) + call assert_true(mkdir('Xspotbugs/tests/α/β/γ/δ', 'pR')) + + let lines =<< trim END + // EOL comment. /* + abstract class + 𐌂1 /* Multiline comment. */ { + /* Multiline comment. */ // EOL comment. /* + static final String COMMENT_A_LIKE = "/*"; + { new Object() {/* Try globbing. */}; } + static { interface 𐌉𐌉1 {} } + static class 𐌂11 { interface 𐌉𐌉2 {} } + } + /* Multiline comment. */ // EOL comment. /* + final class 𐌂2 { + public static void main(String... aa) { + record 𐌓() {} + enum 𐌄 {} + } + } // class + END + + " THE EXPECTED RESULTS. + let results = {} + let results['Xspotbugs/src/tests/𐌂1.java'] = { + \ 'sourcepath': '%:p:h:S', + \ 'classfiles': sort([ + \ 'Xspotbugs/tests/𐌂1$1.class', + \ 'Xspotbugs/tests/𐌂1$1𐌉𐌉1.class', + \ 'Xspotbugs/tests/𐌂1$𐌂11$𐌉𐌉2.class', + \ 'Xspotbugs/tests/𐌂1$𐌂11.class', + \ 'Xspotbugs/tests/𐌂1.class', + \ 'Xspotbugs/tests/𐌂2$1𐌄.class', + \ 'Xspotbugs/tests/𐌂2$1𐌓.class', + \ 'Xspotbugs/tests/𐌂2.class']), + \ } + " No class file for an empty source file even with "-Xpkginfo:always". + let results['Xspotbugs/src/tests/package-info.java'] = { + \ 'sourcepath': '', + \ 'classfiles': [], + \ } + let results['Xspotbugs/src/tests/α/𐌂1.java'] = { + \ 'sourcepath': '%:p:h:h:S', + \ 'classfiles': sort([ + \ 'Xspotbugs/tests/α/𐌂1$1.class', + \ 'Xspotbugs/tests/α/𐌂1$1𐌉𐌉1.class', + \ 'Xspotbugs/tests/α/𐌂1$𐌂11$𐌉𐌉2.class', + \ 'Xspotbugs/tests/α/𐌂1$𐌂11.class', + \ 'Xspotbugs/tests/α/𐌂1.class', + \ 'Xspotbugs/tests/α/𐌂2$1𐌄.class', + \ 'Xspotbugs/tests/α/𐌂2$1𐌓.class', + \ 'Xspotbugs/tests/α/𐌂2.class']), + \ } + let results['Xspotbugs/src/tests/α/package-info.java'] = { + \ 'sourcepath': '%:p:h:S', + \ 'classfiles': ['Xspotbugs/tests/α/package-info.class'], + \ } + let results['Xspotbugs/src/tests/α/β/𐌂1.java'] = { + \ 'sourcepath': '%:p:h:h:h:S', + \ 'classfiles': sort([ + \ 'Xspotbugs/tests/α/β/𐌂1$1.class', + \ 'Xspotbugs/tests/α/β/𐌂1$1𐌉𐌉1.class', + \ 'Xspotbugs/tests/α/β/𐌂1$𐌂11$𐌉𐌉2.class', + \ 'Xspotbugs/tests/α/β/𐌂1$𐌂11.class', + \ 'Xspotbugs/tests/α/β/𐌂1.class', + \ 'Xspotbugs/tests/α/β/𐌂2$1𐌄.class', + \ 'Xspotbugs/tests/α/β/𐌂2$1𐌓.class', + \ 'Xspotbugs/tests/α/β/𐌂2.class']), + \ } + let results['Xspotbugs/src/tests/α/β/package-info.java'] = { + \ 'sourcepath': '%:p:h:S', + \ 'classfiles': ['Xspotbugs/tests/α/β/package-info.class'], + \ } + let results['Xspotbugs/src/tests/α/β/γ/𐌂1.java'] = { + \ 'sourcepath': '%:p:h:h:h:h:S', + \ 'classfiles': sort([ + \ 'Xspotbugs/tests/α/β/γ/𐌂1$1.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂1$1𐌉𐌉1.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂1$𐌂11$𐌉𐌉2.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂1$𐌂11.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂1.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂2$1𐌄.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂2$1𐌓.class', + \ 'Xspotbugs/tests/α/β/γ/𐌂2.class']), + \ } + let results['Xspotbugs/src/tests/α/β/γ/package-info.java'] = { + \ 'sourcepath': '%:p:h:S', + \ 'classfiles': ['Xspotbugs/tests/α/β/γ/package-info.class'], + \ } + let results['Xspotbugs/src/tests/α/β/γ/δ/𐌂1.java'] = { + \ 'sourcepath': '%:p:h:h:h:h:h:S', + \ 'classfiles': sort([ + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$1.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$1𐌉𐌉1.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$𐌂11$𐌉𐌉2.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1$𐌂11.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂1.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂2$1𐌄.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂2$1𐌓.class', + \ 'Xspotbugs/tests/α/β/γ/δ/𐌂2.class']), + \ } + let results['Xspotbugs/src/tests/α/β/γ/δ/package-info.java'] = { + \ 'sourcepath': '%:p:h:S', + \ 'classfiles': ['Xspotbugs/tests/α/β/γ/δ/package-info.class'], + \ } + + " MAKE CLASS FILES DISCOVERABLE! + let g:spotbugs_properties = { + \ 'sourceDirPath': 'src/tests', + \ 'classDirPath': 'tests', + \ } + + call assert_true(has_key(s:SpotBugsParseFilterMakePrg('Xspotbugs', ''), 'sourcepath')) + call assert_true(has_key(s:SpotBugsParseFilterMakePrg('Xspotbugs', ''), 'classfiles')) + + " Write 45 mock-up class files for 10 source files. + for [class_dir, src_dir, package] in [ + \ ['Xspotbugs/tests/', 'Xspotbugs/src/tests/', ''], + \ ['Xspotbugs/tests/α/', 'Xspotbugs/src/tests/α/', 'package α;'], + \ ['Xspotbugs/tests/α/β/', 'Xspotbugs/src/tests/α/β/', 'package α.β;'], + \ ['Xspotbugs/tests/α/β/γ/', 'Xspotbugs/src/tests/α/β/γ/', 'package α.β.γ;'], + \ ['Xspotbugs/tests/α/β/γ/δ/', 'Xspotbugs/src/tests/α/β/γ/δ/', 'package α.β.γ.δ;']] + for class_file in ['𐌂1$1.class', '𐌂1$1𐌉𐌉1.class', '𐌂1$𐌂11$𐌉𐌉2.class', + \ '𐌂1$𐌂11.class', '𐌂1.class', '𐌂2$1𐌄.class', '𐌂2$1𐌓.class', '𐌂2.class'] + call writefile(0zcafe.babe.0000.0041, class_dir .. class_file) + endfor + call writefile(0zcafe.babe.0000.0041, class_dir .. 'package-info.class') + + " Write Java source files. + let type_file = src_dir .. '𐌂1.java' + call writefile(insert(copy(lines), package), type_file) + let package_file = src_dir .. 'package-info.java' + call writefile([package], src_dir .. 'package-info.java') + + for s in ['on', 'off'] + execute 'syntax ' .. s + + execute 'edit ' .. type_file + compiler spotbugs + let result = s:SpotBugsParseFilterMakePrg('Xspotbugs', &l:makeprg) + call assert_equal(results[type_file].sourcepath, result.sourcepath) + call assert_equal(results[type_file].classfiles, result.classfiles) + bwipeout + + execute 'edit ' .. package_file + compiler spotbugs + let result = s:SpotBugsParseFilterMakePrg('Xspotbugs', &l:makeprg) + call assert_equal(results[package_file].sourcepath, result.sourcepath) + call assert_equal(results[package_file].classfiles, result.classfiles) + bwipeout + endfor + endfor + + let &shellslash = save_shellslash +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index 58064ea412..06ac59de20 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -610,7 +610,7 @@ func s:GetFilenameChecks() abort \ 'purescript': ['file.purs'], \ 'pymanifest': ['MANIFEST.in'], \ 'pyret': ['file.arr'], - \ 'pyrex': ['file.pyx', 'file.pxd'], + \ 'pyrex': ['file.pyx', 'file.pxd', 'file.pxi', 'file.pyx+'], \ 'python': ['file.py', 'file.pyw', '.pythonstartup', '.pythonrc', '.python_history', '.jline-jython.history', 'file.ptl', 'file.pyi', 'SConstruct'], \ 'ql': ['file.ql', 'file.qll'], \ 'qml': ['file.qml', 'file.qbs'], diff --git a/test/old/testdir/test_options.vim b/test/old/testdir/test_options.vim index b6bdb1be52..c948846819 100644 --- a/test/old/testdir/test_options.vim +++ b/test/old/testdir/test_options.vim @@ -2252,16 +2252,57 @@ func Test_opt_default() call assert_equal('vt', &formatoptions) set formatoptions&vim call assert_equal('tcq', &formatoptions) + + call assert_equal('ucs-bom,utf-8,default,latin1', &fencs) + set fencs=latin1 + set fencs& + call assert_equal('ucs-bom,utf-8,default,latin1', &fencs) + set fencs=latin1 + set all& + call assert_equal('ucs-bom,utf-8,default,latin1', &fencs) endfunc " Test for the 'cmdheight' option -func Test_cmdheight() +func Test_opt_cmdheight() %bw! let ht = &lines set cmdheight=9999 call assert_equal(1, winheight(0)) call assert_equal(ht - 1, &cmdheight) set cmdheight& + + " The status line should be taken into account. + set laststatus=2 + set cmdheight=9999 + call assert_equal(ht - 2, &cmdheight) + set cmdheight& laststatus=1 " Accommodate Nvim default + + " The tabline should be taken into account only non-GUI. + set showtabline=2 + set cmdheight=9999 + if has('gui_running') + call assert_equal(ht - 1, &cmdheight) + else + call assert_equal(ht - 2, &cmdheight) + endif + set cmdheight& showtabline& + + " The 'winminheight' should be taken into account. + set winheight=3 winminheight=3 + split + set cmdheight=9999 + call assert_equal(ht - 8, &cmdheight) + %bw! + set cmdheight& winminheight& winheight& + + " Only the windows in the current tabpage are taken into account. + set winheight=3 winminheight=3 showtabline=0 + split + tabnew + set cmdheight=9999 + call assert_equal(ht - 3, &cmdheight) + %bw! + set cmdheight& winminheight& winheight& showtabline& endfunc " To specify a control character as an option value, '^' can be used diff --git a/test/old/testdir/test_window_cmd.vim b/test/old/testdir/test_window_cmd.vim index 8048fa6ff8..e173aa1e73 100644 --- a/test/old/testdir/test_window_cmd.vim +++ b/test/old/testdir/test_window_cmd.vim @@ -55,6 +55,27 @@ func Test_window_cmd_cmdwin_with_vsp() set ls&vim endfunc +func Test_cmdheight_not_changed() + throw 'Skipped: N/A' + set cmdheight=2 + set winminheight=0 + augroup Maximize + autocmd WinEnter * wincmd _ + augroup END + split + tabnew + tabfirst + call assert_equal(2, &cmdheight) + + tabonly! + only + set winminheight& cmdheight& + augroup Maximize + au! + augroup END + augroup! Maximize +endfunc + " Test for jumping to windows func Test_window_jump() new |