diff options
106 files changed, 2652 insertions, 838 deletions
diff --git a/.builds/openbsd.yml b/.builds/openbsd.yml index ed2962998c..5fa6556066 100644 --- a/.builds/openbsd.yml +++ b/.builds/openbsd.yml @@ -1,17 +1,17 @@ # sourcehut CI: https://builds.sr.ht/~jmk/neovim -image: openbsd/6.5 +image: openbsd/6.7 packages: - autoconf-2.69p2 - automake-1.15.1 - cmake -- gettext-0.19.8.1p3 -- gettext-tools-0.19.8.1 +- gettext-runtime-0.20.1p1 +- gettext-tools-0.20.1p3 - gmake - libtool -- ninja-1.8.2p0 -- unzip-6.0p11 +- ninja-1.10.0 +- unzip-6.0p13 sources: - https://github.com/neovim/neovim diff --git a/.github/ISSUE_TEMPLATE/lsp_bug_report.md b/.github/ISSUE_TEMPLATE/lsp_bug_report.md new file mode 100644 index 0000000000..0e5155c7ac --- /dev/null +++ b/.github/ISSUE_TEMPLATE/lsp_bug_report.md @@ -0,0 +1,46 @@ +--- +name: Language server client bug report +about: Report a built-in lsp problem in Nvim +title: '' +labels: bug, lsp + +--- + +<!-- Before reporting: search existing issues and check the FAQ. --> + +- `nvim --version`: +- language server name/version: +- Operating system/version: + +<details> +<summary>nvim -c ":checkhealth nvim nvim_lsp"</summary> + +<!-- Paste the results from `nvim -c ":checkhealth nvim nvim_lsp"` here. --> + +</details> + +<details> +<summary>lsp.log</summary> + +<!-- +Please paste the lsp log before and after the problem. + +You can set log level like this. +`:lua vim.lsp.set_log_level("debug")` + +You can find the location of the log with the following command. +`:lua print(vim.lsp.get_log_path())` +--> + +</details> + +### Steps to reproduce using `nvim -u NORC` + +``` +nvim -u NORC +``` + +### Actual behaviour + +### Expected behaviour + diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index 0ca41d5dfd..6c9e06d59d 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -12,7 +12,7 @@ check_type_size("size_t" SIZEOF_SIZE_T) check_type_size("long long" SIZEOF_LONG_LONG) check_type_size("void *" SIZEOF_VOID_PTR) -if (CMAKE_HOST_SYSTEM_VERSION MATCHES ".*-Microsoft") +if (CMAKE_HOST_SYSTEM_VERSION MATCHES ".*-(Microsoft|microsoft-standard)") # Windows Subsystem for Linux set(HAVE_WSL 1) endif() diff --git a/runtime/autoload/health/provider.vim b/runtime/autoload/health/provider.vim index 6d481e9f49..4975dc66b8 100644 --- a/runtime/autoload/health/provider.vim +++ b/runtime/autoload/health/provider.vim @@ -581,7 +581,7 @@ function! s:check_ruby() abort endif call health#report_info('Host: '. host) - let latest_gem_cmd = has('win32') ? 'cmd /c gem list -ra ^^neovim$' : 'gem list -ra ^neovim$' + let latest_gem_cmd = has('win32') ? 'cmd /c gem list -ra "^^neovim$"' : 'gem list -ra ^neovim$' let latest_gem = s:system(split(latest_gem_cmd)) if s:shell_error || empty(latest_gem) call health#report_error('Failed to run: '. latest_gem_cmd, diff --git a/runtime/autoload/provider.vim b/runtime/autoload/provider.vim index dc24e801d0..803c1a0b1c 100644 --- a/runtime/autoload/provider.vim +++ b/runtime/autoload/provider.vim @@ -3,8 +3,11 @@ " Start the provider and perform a 'poll' request " " Returns a valid channel on success -function! provider#Poll(argv, orig_name, log_env) abort +function! provider#Poll(argv, orig_name, log_env, ...) abort let job = {'rpc': v:true, 'stderr_buffered': v:true} + if a:0 + let job = extend(job, a:1) + endif try let channel_id = jobstart(a:argv, job) if channel_id > 0 && rpcrequest(channel_id, 'poll') ==# 'ok' diff --git a/runtime/autoload/provider/pythonx.vim b/runtime/autoload/provider/pythonx.vim index 23e7ff8f64..e89d519790 100644 --- a/runtime/autoload/provider/pythonx.vim +++ b/runtime/autoload/provider/pythonx.vim @@ -19,7 +19,7 @@ function! provider#pythonx#Require(host) abort call add(args, plugin.path) endfor - return provider#Poll(args, a:host.orig_name, '$NVIM_PYTHON_LOG_FILE') + return provider#Poll(args, a:host.orig_name, '$NVIM_PYTHON_LOG_FILE', {'overlapped': v:true}) endfunction function! s:get_python_executable_from_host_var(major_version) abort @@ -29,7 +29,7 @@ endfunction function! s:get_python_candidates(major_version) abort return { \ 2: ['python2', 'python2.7', 'python2.6', 'python'], - \ 3: ['python3', 'python3.8', 'python3.7', 'python3.6', 'python3.5', + \ 3: ['python3', 'python3.9', 'python3.8', 'python3.7', 'python3.6', 'python3.5', \ 'python3.4', 'python3.3', 'python'] \ }[a:major_version] endfunction diff --git a/runtime/autoload/remote/define.vim b/runtime/autoload/remote/define.vim index 2688a62a82..2aec96e365 100644 --- a/runtime/autoload/remote/define.vim +++ b/runtime/autoload/remote/define.vim @@ -24,7 +24,7 @@ function! remote#define#CommandOnHost(host, method, sync, name, opts) endif if has_key(a:opts, 'nargs') - call add(forward_args, ' <args>') + call add(forward_args, ' " . <q-args> . "') endif exe s:GetCommandPrefix(a:name, a:opts) diff --git a/runtime/doc/autocmd.txt b/runtime/doc/autocmd.txt index 64ca7b6a45..f1753b75cc 100644 --- a/runtime/doc/autocmd.txt +++ b/runtime/doc/autocmd.txt @@ -844,6 +844,7 @@ TextYankPost Just after a |yank| or |deleting| command, but not regcontents regname regtype + visual The `inclusive` flag combined with the |'[| and |']| marks can be used to calculate the precise region of the operation. diff --git a/runtime/doc/editing.txt b/runtime/doc/editing.txt index 23a65f16e4..ac398ec494 100644 --- a/runtime/doc/editing.txt +++ b/runtime/doc/editing.txt @@ -1265,7 +1265,7 @@ exist, the next-higher scope in the hierarchy applies. other tabs and windows is not changed. *:tcd-* -:tcd[!] - Change to the previous current directory (before the +:tc[d][!] - Change to the previous current directory (before the previous ":tcd {path}" command). *:tch* *:tchdir* @@ -1280,7 +1280,7 @@ exist, the next-higher scope in the hierarchy applies. :lch[dir][!] Same as |:lcd|. *:lcd-* -:lcd[!] - Change to the previous current directory (before the +:lc[d][!] - Change to the previous current directory (before the previous ":lcd {path}" command). *:pw* *:pwd* *E187* diff --git a/runtime/doc/eval.txt b/runtime/doc/eval.txt index 1992c34102..92f703a8a3 100644 --- a/runtime/doc/eval.txt +++ b/runtime/doc/eval.txt @@ -1423,6 +1423,10 @@ PREDEFINED VIM VARIABLES *vim-variable* *v:var* *v:* *E963* Some variables can be set by the user, but the type cannot be changed. + *v:argv* *argv-variable* +v:argv The command line arguments Vim was invoked with. This is a + list of strings. The first item is the Vim command. + *v:beval_col* *beval_col-variable* v:beval_col The number of the column, over which the mouse pointer is. This is the byte index in the |v:beval_lnum| line. @@ -1587,6 +1591,8 @@ v:event Dictionary of event data for the current |autocommand|. Valid operation. regtype Type of register as returned by |getregtype()|. + visual Selection is visual (as opposed to, + e.g., via motion). completed_item Current selected complete item on |CompleteChanged|, Is `{}` when no complete item selected. @@ -2600,6 +2606,7 @@ argv([{nr} [, {winid}]) the whole |arglist| is returned. The {winid} argument specifies the window ID, see |argc()|. + For the Vim command line arguments see |v:argv|. assert_beeps({cmd}) *assert_beeps()* Run {cmd} and add an error message to |v:errors| if it does @@ -5499,6 +5506,11 @@ jobstart({cmd}[, {opts}]) *jobstart()* stdout data. |on_stderr|: (function) Callback invoked when the job emits stderr data. + overlapped: (boolean) Set FILE_FLAG_OVERLAPPED for the + standard input/output passed to the child process. + Normally you do not need to set this. + (Only available on MS-Windows, On other + platforms, this option is silently ignored.) pty: (boolean) Connect the job to a new pseudo terminal, and its streams to the master file descriptor. Then `on_stderr` is ignored, @@ -5828,6 +5840,7 @@ maparg({name} [, {mode} [, {abbr} [, {dict}]]]) *maparg()* "rhs" The {rhs} of the mapping as typed. "silent" 1 for a |:map-silent| mapping, else 0. "noremap" 1 if the {rhs} of the mapping is not remappable. + "script" 1 if mapping was defined with <script>. "expr" 1 for an expression mapping (|:map-<expr>|). "buffer" 1 for a buffer local mapping (|:map-local|). "mode" Modes for which the mapping is defined. In diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 9460e600e3..3a93390210 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -164,21 +164,21 @@ name: > LSP HIGHLIGHT *lsp-highlight* *hl-LspDiagnosticsError* -LspDiagnosticsError used for "Error" diagnostic virtual text +LspDiagnosticsError used for "Error" diagnostic virtual text *hl-LspDiagnosticsErrorSign* -LspDiagnosticsErrorSign used for "Error" diagnostic signs in sign column +LspDiagnosticsErrorSign used for "Error" diagnostic signs in sign column *hl-LspDiagnosticsWarning* -LspDiagnosticsWarning used for "Warning" diagnostic virtual text +LspDiagnosticsWarning used for "Warning" diagnostic virtual text *hl-LspDiagnosticsWarningSign* -LspDiagnosticsWarningSign used for "Warning" diagnostic signs in sign column +LspDiagnosticsWarningSign used for "Warning" diagnostic signs in sign column *hl-LspDiagnosticsInformation* -LspDiagnosticInformation used for "Information" diagnostic virtual text +LspDiagnosticsInformation used for "Information" diagnostic virtual text *hl-LspDiagnosticsInformationSign* -LspDiagnosticInformationSign used for "Information" signs in sign column +LspDiagnosticsInformationSign used for "Information" signs in sign column *hl-LspDiagnosticsHint* -LspDiagnosticHint used for "Hint" diagnostic virtual text +LspDiagnosticsHint used for "Hint" diagnostic virtual text *hl-LspDiagnosticsHintSign* -LspDiagnosticHintSign used for "Hint" diagnostic signs in sign column +LspDiagnosticsHintSign used for "Hint" diagnostic signs in sign column *hl-LspReferenceText* LspReferenceText used for highlighting "text" references *hl-LspReferenceRead* @@ -1042,15 +1042,10 @@ get_current_line_to_cursor() get_severity_highlight_name({severity}) TODO: Documentation - *vim.lsp.util.highlight_range()* -highlight_range({bufnr}, {ns}, {hiname}, {start}, {finish}) - TODO: Documentation - - *vim.lsp.util.highlight_region()* -highlight_region({ft}, {start}, {finish}) +jump_to_location({location}) *vim.lsp.util.jump_to_location()* TODO: Documentation -jump_to_location({location}) *vim.lsp.util.jump_to_location()* +preview_location({location}) *vim.lsp.util.preview_location()* TODO: Documentation locations_to_items({locations}) *vim.lsp.util.locations_to_items()* diff --git a/runtime/doc/lua.txt b/runtime/doc/lua.txt index 2b83c35c90..5a49d36503 100644 --- a/runtime/doc/lua.txt +++ b/runtime/doc/lua.txt @@ -705,6 +705,10 @@ the highlight via > au TextYankPost * silent! lua require'vim.highlight'.on_yank("IncSearch", 500) < +If you want to exclude visual selections from highlighting on yank, use +> +au TextYankPost * silent! lua return (not vim.v.event.visual) and require'vim.highlight'.on_yank() +< vim.highlight.on_yank([{higroup}, {timeout}, {event}]) *vim.highlight.on_yank()* @@ -713,6 +717,16 @@ vim.highlight.on_yank([{higroup}, {timeout}, {event}]) in milliseconds ({timeout}, default `500`), and the event structure that is fired ({event}, default `vim.v.event`). + +vim.highlight.range({bufnr}, {ns}, {higroup}, {start}, {finish}, {rtype}, {inclusive}) + *vim.highlight.range()* + Highlights the range between {start} and {finish} (tuples of {line,col}) + in buffer {bufnr} with the highlight group {higroup} using the namespace + {ns}. Optional arguments are the type of range (characterwise, linewise, + or blockwise, see |setreg|; default to characterwise) and whether the + range is inclusive (default false). + + ------------------------------------------------------------------------------ VIM.REGEX *lua-regex* @@ -827,6 +841,62 @@ vim.schedule({callback}) *vim.schedule()* Schedules {callback} to be invoked soon by the main event-loop. Useful to avoid |textlock| or other temporary restrictions. + +vim.defer_fn({fn}, {timeout}) *vim.defer_fn* + Defers calling {fn} until {timeout} ms passes. Use to do a one-shot timer + that calls {fn}. + + Parameters: ~ + {fn} Callback to call once {timeout} expires + {timeout} Time in ms to wait before calling {fn} + + Returns: ~ + |vim.loop|.new_timer() object + +vim.wait({time}, {callback} [, {interval}]) *vim.wait()* + Wait for {time} in milliseconds until {callback} returns `true`. + + Executes {callback} immediately and at approximately {interval} + milliseconds (default 200). Nvim still processes other events during + this time. + + + Returns: ~ + If {callback} returns `true` during the {time}: + `true, nil` + + If {callback} never returns `true` during the {time}: + `false, -1` + + If {callback} is interrupted during the {time}: + `false, -2` + + If {callback} errors, the error is raised. + + Examples: > + + --- + -- Wait for 100 ms, allowing other events to process + vim.wait(100, function() end) + + --- + -- Wait for 100 ms or until global variable set. + vim.wait(100, function() return vim.g.waiting_for_var end) + + --- + -- Wait for 1 second or until global variable set, checking every ~500 ms + vim.wait(1000, function() return vim.g.waiting_for_var end, 500) + + --- + -- Schedule a function to set a value in 100ms + vim.defer_fn(function() vim.g.timer_result = true end, 100) + + -- Would wait ten seconds if results blocked. Actually only waits 100 ms + if vim.wait(10000, function() return vim.g.timer_result end) then + print('Only waiting a little bit of time!') + end +< + vim.fn.{func}({...}) *vim.fn* Invokes |vim-function| or |user-function| {func} with arguments {...}. To call autoload functions, use the syntax: > diff --git a/runtime/filetype.vim b/runtime/filetype.vim index 0b5003dc44..383a45b9d3 100644 --- a/runtime/filetype.vim +++ b/runtime/filetype.vim @@ -543,6 +543,9 @@ au BufNewFile,BufRead */etc/elinks.conf,*/.elinks/elinks.conf setf elinks " ERicsson LANGuage; Yaws is erlang too au BufNewFile,BufRead *.erl,*.hrl,*.yaws setf erlang +" Elm +au BufNewFile,BufRead *.elm setf elm + " Elm Filter Rules file au BufNewFile,BufRead filter-rules setf elmfilt @@ -880,11 +883,12 @@ au BufNewFile,BufRead *.ll setf lifelines " Lilo: Linux loader au BufNewFile,BufRead lilo.conf setf lilo -" Lisp (*.el = ELisp, *.cl = Common Lisp, *.jl = librep Lisp) +" Lisp (*.el = ELisp, *.cl = Common Lisp) +" *.jl was removed, it's also used for Julia, better skip than guess wrong. if has("fname_case") - au BufNewFile,BufRead *.lsp,*.lisp,*.el,*.cl,*.jl,*.L,.emacs,.sawfishrc setf lisp + au BufNewFile,BufRead *.lsp,*.lisp,*.el,*.cl,*.L,.emacs,.sawfishrc setf lisp else - au BufNewFile,BufRead *.lsp,*.lisp,*.el,*.cl,*.jl,.emacs,.sawfishrc setf lisp + au BufNewFile,BufRead *.lsp,*.lisp,*.el,*.cl,.emacs,.sawfishrc setf lisp endif " SBCL implementation of Common Lisp diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index 5c98c626a4..69c3c8a4dc 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -2,12 +2,34 @@ local api = vim.api local highlight = {} +--- Highlight range between two positions +--- +--@param bufnr number of buffer to apply highlighting to +--@param ns namespace to add highlight to +--@param higroup highlight group to use for highlighting +--@param rtype type of range (:help setreg, default charwise) +--@param inclusive boolean indicating whether the range is end-inclusive (default false) +function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive) + rtype = rtype or 'v' + inclusive = inclusive or false + + -- sanity check + if start[2] < 0 or finish[2] < start[2] then return end + + local region = vim.region(bufnr, start, finish, rtype, inclusive) + for linenr, cols in pairs(region) do + api.nvim_buf_add_highlight(bufnr, ns, higroup, linenr, cols[1], cols[2]) + end + +end + --- Highlight the yanked region --- +--- --- use from init.vim via --- au TextYankPost * lua require'vim.highlight'.on_yank() --- customize highlight group and timeout via --- au TextYankPost * lua require'vim.highlight'.on_yank("IncSearch", 500) +--- -- @param higroup highlight group for yanked region -- @param timeout time in ms before highlight is cleared -- @param event event structure @@ -27,10 +49,7 @@ function highlight.on_yank(higroup, timeout, event) pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]} pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]} - local region = vim.region(bufnr, pos1, pos2, event.regtype, event.inclusive) - for linenr, cols in pairs(region) do - api.nvim_buf_add_highlight(bufnr, yank_ns, higroup, linenr, cols[1], cols[2]) - end + highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive) vim.defer_fn( function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end, diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 61da2130c8..e759511347 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -46,31 +46,6 @@ local function is_dir(filename) return stat and stat.type == 'directory' or false end --- TODO Use vim.wait when that is available, but provide an alternative for now. -local wait = vim.wait or function(timeout_ms, condition, interval) - validate { - timeout_ms = { timeout_ms, 'n' }; - condition = { condition, 'f' }; - interval = { interval, 'n', true }; - } - assert(timeout_ms > 0, "timeout_ms must be > 0") - local _ = log.debug() and log.debug("wait.fallback", timeout_ms) - interval = interval or 200 - local interval_cmd = "sleep "..interval.."m" - local timeout = timeout_ms + uv.now() - -- TODO is there a better way to sync this? - while true do - uv.update_time() - if condition() then - return 0 - end - if uv.now() >= timeout then - return -1 - end - nvim_command(interval_cmd) - -- vim.loop.sleep(10) - end -end local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" } local valid_encodings = { @@ -122,19 +97,19 @@ local function validate_encoding(encoding) end function lsp._cmd_parts(input) - local cmd, cmd_args - if vim.tbl_islist(input) then - cmd = input[1] - cmd_args = {} - -- Don't mutate our input. - for i, v in ipairs(input) do - assert(type(v) == 'string', "input arguments must be strings") - if i > 1 then - table.insert(cmd_args, v) - end + vim.validate{cmd={ + input, + function() return vim.tbl_islist(input) end, + "list"}} + + local cmd = input[1] + local cmd_args = {} + -- Don't mutate our input. + for i, v in ipairs(input) do + vim.validate{["cmd argument"]={v, "s"}} + if i > 1 then + table.insert(cmd_args, v) end - else - error("cmd type must be list.") end return cmd, cmd_args end @@ -524,7 +499,7 @@ function lsp.start_client(config) function client.request(method, params, callback, bufnr) if not callback then callback = resolve_callback(method) - or error("not found: request callback for client "..client.name) + or error(string.format("not found: %q request callback for client %q.", method, client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) -- TODO keep these checks or just let it go anyway? @@ -532,6 +507,8 @@ function lsp.start_client(config) or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') + or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration') + or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') then @@ -609,9 +586,7 @@ do old_utf16_size) local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) - if old_byte_size == 0 then - return - end + -- Don't do anything if there are no clients attached. if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then return @@ -810,8 +785,8 @@ function lsp._vim_exit_handler() for _, client in pairs(active_clients) do client.stop() end - local wait_result = wait(500, function() return tbl_isempty(active_clients) end, 50) - if wait_result ~= 0 then + + if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then for _, client in pairs(active_clients) do client.stop(true) end @@ -889,12 +864,14 @@ function lsp.buf_request_sync(bufnr, method, params, timeout_ms) for _ in pairs(client_request_ids) do expected_result_count = expected_result_count + 1 end - local wait_result = wait(timeout_ms or 100, function() + + local wait_result, reason = vim.wait(timeout_ms or 100, function() return result_count >= expected_result_count end, 10) - if wait_result ~= 0 then + + if not wait_result then cancel() - return nil, wait_result_reason[wait_result] + return nil, wait_result_reason[reason] end return request_results end diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 37e9f1e5c1..4b14f0132d 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -72,6 +72,17 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) return end + + -- Unloaded buffers should not handle diagnostics. + -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. + -- This should trigger another publish of the diagnostics. + -- + -- In particular, this stops a ton of spam when first starting a server for current + -- unloaded buffers. + if not api.nvim_buf_is_loaded(bufnr) then + return + end + util.buf_clear_diagnostics(bufnr) -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic @@ -242,12 +253,12 @@ end -- Add boilerplate error validation and logging for all of these. for k, fn in pairs(M) do - M[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + M[k] = function(err, method, params, client_id, bufnr) + log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr }) if err then error(tostring(err)) end - return fn(err, method, params, client_id) + return fn(err, method, params, client_id, bufnr) end end diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 78aabf08ce..696ce43a59 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -24,7 +24,7 @@ do local function path_join(...) return table.concat(vim.tbl_flatten{...}, path_sep) end - local logfilename = path_join(vim.fn.stdpath('data'), 'vim-lsp.log') + local logfilename = path_join(vim.fn.stdpath('data'), 'lsp.log') --- Return the log filename. function log.get_filename() diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 877d11411b..4fded1961d 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -633,8 +633,7 @@ function protocol.make_client_capabilities() dynamicRegistration = false; completionItem = { - -- TODO(tjdevries): Is it possible to implement this in plain lua? - snippetSupport = false; + snippetSupport = true; commitCharactersSupport = false; preselectSupport = false; deprecatedSupport = false; @@ -898,7 +897,8 @@ function protocol.resolve_capabilities(server_capabilities) text_document_will_save = ifnil(textDocumentSync.willSave, false); text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false); text_document_save = ifnil(textDocumentSync.save, false); - text_document_save_include_text = ifnil(textDocumentSync.save and textDocumentSync.save.includeText, false); + text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table' + and textDocumentSync.save.includeText, false); } else return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) @@ -924,6 +924,28 @@ function protocol.resolve_capabilities(server_capabilities) error("The server sent invalid codeActionProvider") end + if server_capabilities.declarationProvider == nil then + general_properties.declaration = false + elseif type(server_capabilities.declarationProvider) == 'boolean' then + general_properties.declaration = server_capabilities.declarationProvider + elseif type(server_capabilities.declarationProvider) == 'table' then + -- TODO: support more detailed declarationProvider options. + general_properties.declaration = false + else + error("The server sent invalid declarationProvider") + end + + if server_capabilities.typeDefinitionProvider == nil then + general_properties.type_definition = false + elseif type(server_capabilities.typeDefinitionProvider) == 'boolean' then + general_properties.type_definition = server_capabilities.typeDefinitionProvider + elseif type(server_capabilities.typeDefinitionProvider) == 'table' then + -- TODO: support more detailed typeDefinitionProvider options. + general_properties.type_definition = false + else + error("The server sent invalid typeDefinitionProvider") + end + if server_capabilities.implementationProvider == nil then general_properties.implementation = false elseif type(server_capabilities.implementationProvider) == 'boolean' then diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 5c6d183ac1..4c3c4fa6cb 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -3,6 +3,7 @@ local vim = vim local validate = vim.validate local api = vim.api local list_extend = vim.list_extend +local highlight = require 'vim.highlight' local M = {} @@ -91,20 +92,26 @@ local function sort_by_key(fn) end end local edit_sort_key = sort_by_key(function(e) - return {e.A[1], e.A[2], e.i} + return {e.A[1], e.A[2], -e.i} end) -local function get_line_byte_from_line_character(bufnr, lnum, cnum) - -- Skip check when the byte and character position is the same - if cnum > 0 then - local lines = api.nvim_buf_get_lines(bufnr, lnum, lnum+1, false) - +--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position +-- Returns a zero-indexed column, since set_lines() does the conversion to +-- 1-indexed +local function get_line_byte_from_position(bufnr, position) + -- LSP's line and characters are 0-indexed + -- Vim's line and columns are 1-indexed + local col = position.character + -- When on the first character, we can ignore the difference between byte and + -- character + if col > 0 then + local line = position.line + local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false) if #lines > 0 then - return vim.str_byteindex(lines[1], cnum) + return vim.str_byteindex(lines[1], col) end end - - return cnum + return col end function M.apply_text_edits(text_edits, bufnr) @@ -117,15 +124,9 @@ function M.apply_text_edits(text_edits, bufnr) for i, e in ipairs(text_edits) do -- adjust start and end column for UTF-16 encoding of non-ASCII characters local start_row = e.range.start.line - local start_col = get_line_byte_from_line_character( - bufnr, - start_row, - e.range.start.character) + local start_col = get_line_byte_from_position(bufnr, e.range.start) local end_row = e.range["end"].line - local end_col = get_line_byte_from_line_character( - bufnr, - end_row, - e.range["end"].character) + local end_col = get_line_byte_from_position(bufnr, e.range['end']) start_line = math.min(e.range.start.line, start_line) finish_line = math.max(e.range["end"].line, finish_line) -- TODO(ashkan) sanity check ranges for overlap. @@ -199,6 +200,66 @@ function M.get_current_line_to_cursor() return line:sub(pos[2]+1) end +local function parse_snippet_rec(input, inner) + local res = "" + + local close, closeend = nil, nil + if inner then + close, closeend = input:find("}", 1, true) + while close ~= nil and input:sub(close-1,close-1) == "\\" do + close, closeend = input:find("}", closeend+1, true) + end + end + + local didx = input:find('$', 1, true) + if didx == nil and close == nil then + return input, "" + elseif close ~=nil and (didx == nil or close < didx) then + -- No inner placeholders + return input:sub(0, close-1), input:sub(closeend+1) + end + + res = res .. input:sub(0, didx-1) + input = input:sub(didx+1) + + local tabstop, tabstopend = input:find('^%d+') + local placeholder, placeholderend = input:find('^{%d+:') + local choice, choiceend = input:find('^{%d+|') + + if tabstop then + input = input:sub(tabstopend+1) + elseif choice then + input = input:sub(choiceend+1) + close, closeend = input:find("|}", 1, true) + + res = res .. input:sub(0, close-1) + input = input:sub(closeend+1) + elseif placeholder then + -- TODO: add support for variables + input = input:sub(placeholderend+1) + + -- placeholders and variables are recursive + while input ~= "" do + local r, tail = parse_snippet_rec(input, true) + r = r:gsub("\\}", "}") + + res = res .. r + input = tail + end + else + res = res .. "$" + end + + return res, input +end + +-- Parse completion entries, consuming snippet tokens +function M.parse_snippet(input) + local res, _ = parse_snippet_rec(input, false) + + return res +end + -- Sort by CompletionItem.sortText -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function sort_completion_items(items) @@ -213,14 +274,22 @@ end -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function get_completion_word(item) if item.textEdit ~= nil and item.textEdit.newText ~= nil then - return item.textEdit.newText + if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then + return item.textEdit.newText + else + return M.parse_snippet(item.textEdit.newText) + end elseif item.insertText ~= nil then - return item.insertText + if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then + return item.insertText + else + return M.parse_snippet(item.insertText) + end end return item.label end --- Some lanuguage servers return complementary candidates whose prefixes do not match are also returned. +-- Some language servers return complementary candidates whose prefixes do not match are also returned. -- So we exclude completion candidates whose prefix does not match. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) @@ -474,13 +543,33 @@ function M.jump_to_location(location) api.nvim_buf_set_option(0, 'buflisted', true) local range = location.range or location.targetSelectionRange local row = range.start.line - local col = range.start.character - local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] - col = vim.str_byteindex(line, col) + local col = get_line_byte_from_position(0, range.start) api.nvim_win_set_cursor(0, {row + 1, col}) return true end +--- Preview a location in a floating windows +--- +--- behavior depends on type of location: +--- - for Location, range is shown (e.g., function definition) +--- - for LocationLink, targetRange is shown (e.g., body of function definition) +--- +--@param location a single Location or LocationLink +--@return bufnr,winnr buffer and window number of floating window or nil +function M.preview_location(location) + -- location may be LocationLink or Location (more useful for the former) + local uri = location.targetUri or location.uri + if uri == nil then return end + local bufnr = vim.uri_to_bufnr(uri) + if not api.nvim_buf_is_loaded(bufnr) then + vim.fn.bufload(bufnr) + end + local range = location.targetRange or location.range + local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false) + local filetype = api.nvim_buf_get_option(bufnr, 'filetype') + return M.open_floating_preview(contents, filetype) +end + local function find_window_by_var(name, value) for _, win in ipairs(api.nvim_list_wins()) do if npcall(api.nvim_win_get_var, win, name) == value then @@ -525,13 +614,53 @@ function M.focusable_preview(unique_name, fn) end) end --- Convert markdown into syntax highlighted regions by stripping the code --- blocks and converting them into highlighted code. --- This will by default insert a blank line separator after those code block --- regions to improve readability. +--- Trim empty lines from input and pad left and right with spaces +--- +--@param contents table of lines to trim and pad +--@param opts dictionary with optional fields +-- - pad_left amount of columns to pad contents at left (default 1) +-- - pad_right amount of columns to pad contents at right (default 1) +--@return contents table of trimmed and padded lines +function M._trim_and_pad(contents, opts) + validate { + contents = { contents, 't' }; + opts = { opts, 't', true }; + } + opts = opts or {} + local left_padding = (" "):rep(opts.pad_left or 1) + local right_padding = (" "):rep(opts.pad_right or 1) + contents = M.trim_empty_lines(contents) + for i, line in ipairs(contents) do + contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding) + end + return contents +end + + + +--- Convert markdown into syntax highlighted regions by stripping the code +--- blocks and converting them into highlighted code. +--- This will by default insert a blank line separator after those code block +--- regions to improve readability. +--- The result is shown in a floating preview +--- TODO: refactor to separate stripping/converting and make use of open_floating_preview +--- +--@param contents table of lines to show in window +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +-- - pad_left amount of columns to pad contents at left +-- - pad_right amount of columns to pad contents at right +-- - separator insert separator after code block +--@return width,height size of float function M.fancy_floating_markdown(contents, opts) - local pad_left = opts and opts.pad_left - local pad_right = opts and opts.pad_right + validate { + contents = { contents, 't' }; + opts = { opts, 't', true }; + } + opts = opts or {} + local stripped = {} local highlights = {} do @@ -565,31 +694,27 @@ function M.fancy_floating_markdown(contents, opts) end end end - local width = 0 - for i, v in ipairs(stripped) do - v = v:gsub("\r", "") - if pad_left then v = (" "):rep(pad_left)..v end - if pad_right then v = v..(" "):rep(pad_right) end - stripped[i] = v - width = math.max(width, #v) - end - if opts and opts.max_width then - width = math.min(opts.max_width, width) - end - -- TODO(ashkan): decide how to make this customizable. - local insert_separator = true + -- Clean up and add padding + stripped = M._trim_and_pad(stripped, opts) + + -- Compute size of float needed to show (wrapped) lines + opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) + local width, height = M._make_floating_popup_size(stripped, opts) + + -- Insert blank line separator after code block + local insert_separator = opts.separator or true if insert_separator then for i, h in ipairs(highlights) do h.start = h.start + i - 1 h.finish = h.finish + i - 1 if h.finish + 1 <= #stripped then table.insert(stripped, h.finish + 1, string.rep("─", width)) + height = height + 1 end end end -- Make the floating window. - local height = #stripped local bufnr = api.nvim_create_buf(false, true) local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts)) vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped) @@ -601,7 +726,7 @@ function M.fancy_floating_markdown(contents, opts) vim.cmd("ownsyntax markdown") local idx = 1 - local function highlight_region(ft, start, finish) + local function apply_syntax_to_region(ft, start, finish) if ft == '' then return end local name = ft..idx idx = idx + 1 @@ -617,8 +742,8 @@ function M.fancy_floating_markdown(contents, opts) -- make sure that regions between code blocks are definitely markdown. -- local ph = {start = 0; finish = 1;} for _, h in ipairs(highlights) do - -- highlight_region('markdown', ph.finish, h.start) - highlight_region(h.ft, h.start, h.finish) + -- apply_syntax_to_region('markdown', ph.finish, h.start) + apply_syntax_to_region(h.ft, h.start, h.finish) -- ph = h end @@ -630,33 +755,81 @@ function M.close_preview_autocmd(events, winnr) api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") end -function M.open_floating_preview(contents, filetype, opts) +--- Compute size of float needed to show contents (with optional wrapping) +--- +--@param contents table of lines to show in window +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +--@return width,height size of float +function M._make_floating_popup_size(contents, opts) validate { contents = { contents, 't' }; - filetype = { filetype, 's', true }; opts = { opts, 't', true }; } opts = opts or {} - -- Trim empty lines from the end. - contents = M.trim_empty_lines(contents) - local width = opts.width - local height = opts.height or #contents + local height = opts.height + local line_widths = {} + if not width then width = 0 for i, line in ipairs(contents) do - -- Clean up the input and add left pad. - line = " "..line:gsub("\r", "") -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - local line_width = vim.fn.strdisplaywidth(line) - width = math.max(line_width, width) - contents[i] = line + line_widths[i] = vim.fn.strdisplaywidth(line) + width = math.max(line_widths[i], width) end - -- Add right padding of 1 each. - width = width + 1 end + if not height then + height = #contents + local wrap_at = opts.wrap_at + if wrap_at and width > wrap_at then + height = 0 + if vim.tbl_isempty(line_widths) then + for _, line in ipairs(contents) do + local line_width = vim.fn.strdisplaywidth(line) + height = height + math.ceil(line_width/wrap_at) + end + else + for i = 1, #contents do + height = height + math.ceil(line_widths[i]/wrap_at) + end + end + end + end + + return width, height +end + +--- Show contents in a floating window +--- +--@param contents table of lines to show in window +--@param filetype string of filetype to set for opened buffer +--@param opts dictionary with optional fields +-- - height of floating window +-- - width of floating window +-- - wrap_at character to wrap at for computing height +-- - pad_left amount of columns to pad contents at left +-- - pad_right amount of columns to pad contents at right +--@return bufnr,winnr buffer and window number of floating window or nil +function M.open_floating_preview(contents, filetype, opts) + validate { + contents = { contents, 't' }; + filetype = { filetype, 's', true }; + opts = { opts, 't', true }; + } + opts = opts or {} + + -- Clean up input: trim empty lines from the end, pad + contents = M._trim_and_pad(contents, opts) + + -- Compute size of float needed to show (wrapped) lines + opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0)) + local width, height = M._make_floating_popup_size(contents, opts) + local floating_bufnr = api.nvim_create_buf(false, true) if filetype then api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) @@ -668,23 +841,10 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden"}, floating_winnr) + M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr) return floating_bufnr, floating_winnr end -local function highlight_range(bufnr, ns, hiname, start, finish) - if start[1] == finish[1] then - -- TODO care about encoding here since this is in byte index? - api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2]) - else - api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1) - for line = start[1] + 1, finish[1] - 1 do - api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1) - end - api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2]) - end -end - do local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics") local reference_ns = api.nvim_create_namespace("vim_lsp_references") @@ -818,8 +978,7 @@ do [protocol.DiagnosticSeverity.Hint]='Hint', } - -- TODO care about encoding here since this is in byte index? - highlight_range(bufnr, diagnostic_ns, + highlight.range(bufnr, diagnostic_ns, underline_highlight_name..hlmap[diagnostic.severity], {start.line, start.character}, {finish.line, finish.character} @@ -843,7 +1002,7 @@ do [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite"; } local kind = reference["kind"] or protocol.DocumentHighlightKind.Text - highlight_range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) + highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos) end end diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 2135bfc837..384d22cb89 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -79,7 +79,7 @@ function vim.gsplit(s, sep, plain) end return function() - if done then + if done or (s == '' and sep == '') then return end if sep == '' then diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index d3b78a7f73..c502e45bd0 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -162,16 +162,17 @@ function Query:match_preds(match, pattern, bufnr) -- Here we only want to return if a predicate DOES NOT match, and -- continue on the other case. This way unknown predicates will not be considered, -- which allows some testing and easier user extensibility (#12173). + -- Also, tree-sitter strips the leading # from predicates for us. if pred[1] == "eq?" then local node = match[pred[2]] local node_text = get_node_text(node, bufnr) local str if type(pred[3]) == "string" then - -- (eq? @aa "foo") + -- (#eq? @aa "foo") str = pred[3] else - -- (eq? @aa @bb) + -- (#eq? @aa @bb) str = get_node_text(match[pred[3]], bufnr) end diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index e28cc9e20f..9c3535c676 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -65,9 +65,11 @@ local function uri_from_fname(path) return table.concat(uri_parts) end +local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*)://.*' + local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) - local scheme = fname:match("^([a-z]+)://.*") + local scheme = fname:match(URI_SCHEME_PATTERN) if scheme then return fname else @@ -76,6 +78,10 @@ local function uri_from_bufnr(bufnr) end local function uri_to_fname(uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) + if scheme ~= 'file' then + return uri + end uri = uri_decode(uri) -- TODO improve this. if is_windows_file_uri(uri) then @@ -89,7 +95,7 @@ end -- Return or create a buffer for a uri. local function uri_to_bufnr(uri) - local scheme = assert(uri:match("^([a-z]+)://.*"), 'Uri must contain a scheme: ' .. uri) + local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme == 'file' then return vim.fn.bufadd(uri_to_fname(uri)) else diff --git a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim index aa2b69ad97..28dc3256c7 100644 --- a/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim +++ b/runtime/pack/dist/opt/termdebug/plugin/termdebug.vim @@ -645,7 +645,7 @@ func s:InstallCommands() command Gdb call win_gotoid(s:gdbwin) command Program call win_gotoid(s:ptywin) command Source call s:GotoSourcewinOrCreateIt() - " command Winbar call s:InstallWinbar() + command Winbar call s:InstallWinbar() " TODO: can the K mapping be restored? nnoremap K :Evaluate<CR> @@ -655,6 +655,19 @@ endfunc " let s:winbar_winids = [] +" Install the window toolbar in the current window. +func s:InstallWinbar() + " if has('menu') && &mouse != '' + " nnoremenu WinBar.Step :Step<CR> + " nnoremenu WinBar.Next :Over<CR> + " nnoremenu WinBar.Finish :Finish<CR> + " nnoremenu WinBar.Cont :Continue<CR> + " nnoremenu WinBar.Stop :Stop<CR> + " nnoremenu WinBar.Eval :Evaluate<CR> + " call add(s:winbar_winids, win_getid(winnr())) + " endif +endfunc + " Delete installed debugger commands in the current window. func s:DeleteCommands() delcommand Break @@ -670,7 +683,7 @@ func s:DeleteCommands() delcommand Gdb delcommand Program delcommand Source - " delcommand Winbar + delcommand Winbar nunmap K @@ -940,7 +953,7 @@ func s:GotoSourcewinOrCreateIt() if !win_gotoid(s:sourcewin) new let s:sourcewin = win_getid(winnr()) - " call s:InstallWinbar() + call s:InstallWinbar() endif endfunc @@ -971,7 +984,7 @@ func s:HandleCursor(msg) " TODO: find existing window exe 'split ' . fnameescape(fname) let s:sourcewin = win_getid(winnr()) - " call s:InstallWinbar() + call s:InstallWinbar() else exe 'edit ' . fnameescape(fname) endif diff --git a/scripts/update-ts-runtime.sh b/scripts/update-ts-runtime.sh new file mode 100755 index 0000000000..1a947e0ac9 --- /dev/null +++ b/scripts/update-ts-runtime.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# +# This script will update the treesitter runtime to the provided commit. +# Usage : +# $0 <tree-sitter commit sha> +set -e + +ts_source_dir="/tmp/tree-sitter" +ts_url="https://github.com/tree-sitter/tree-sitter.git" + +base_dir="$(cd "$(dirname $(dirname $0))" && pwd)" +ts_dest_dir="$base_dir/src/tree_sitter/" +ts_current_commit="$ts_dest_dir/treesitter_commit_hash.txt" + +echo "Updating treesitter runtime from $(cat "$ts_current_commit") to $1..." + +if [ ! -d "$ts_source_dir" ]; then + echo "Cloning treesitter..." + git clone "$ts_url" "$ts_source_dir" +else + echo "Found a non-empty $ts_source_dir directory..." + git -C "$ts_source_dir" fetch +fi + +echo "Checking out $1..." +git -C "$ts_source_dir" checkout $1 + +echo "Removing old files..." +find "$ts_dest_dir" -not -name "LICENSE" -not -name "README.md" -not -type d -delete + +echo "Copying files..." +cp -t "$ts_dest_dir" -r "$ts_source_dir/lib/src"/* +cp -t "$ts_dest_dir" "$ts_source_dir/lib/include/tree_sitter"/* + +echo "$1" > "$ts_current_commit" + +make +TEST_FILE="$base_dir/test/functional/lua/treesitter_spec.lua" make test + diff --git a/scripts/vim-patch.sh b/scripts/vim-patch.sh index a8b622d5c4..9c4349abca 100755 --- a/scripts/vim-patch.sh +++ b/scripts/vim-patch.sh @@ -7,7 +7,7 @@ set -p # Ensure that the user has a bash that supports -A if [[ "${BASH_VERSINFO[0]}" -lt 4 ]]; then - echo "This script requires bash version 3 or later (you have ${BASH_VERSION})." >&2 + >&2 echo "error: script requires bash 4+ (you have ${BASH_VERSION})." exit 1 fi diff --git a/src/nvim/api/private/helpers.c b/src/nvim/api/private/helpers.c index a458762cc6..f194b6b474 100644 --- a/src/nvim/api/private/helpers.c +++ b/src/nvim/api/private/helpers.c @@ -1075,8 +1075,8 @@ bool object_to_vim(Object obj, typval_T *tv, Error *err) break; case kObjectTypeBoolean: - tv->v_type = VAR_SPECIAL; - tv->vval.v_special = obj.data.boolean? kSpecialVarTrue: kSpecialVarFalse; + tv->v_type = VAR_BOOL; + tv->vval.v_bool = obj.data.boolean? kBoolVarTrue: kBoolVarFalse; break; case kObjectTypeBuffer: diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 087ab37296..40cef87cf0 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1037,7 +1037,7 @@ void nvim_set_current_win(Window window, Error *err) /// /// @param listed Sets 'buflisted' /// @param scratch Creates a "throwaway" |scratch-buffer| for temporary work -/// (always 'nomodified') +/// (always 'nomodified'). Also sets 'nomodeline' on the buffer. /// @param[out] err Error details, if any /// @return Buffer handle, or 0 on error /// @@ -1067,9 +1067,10 @@ Buffer nvim_create_buf(Boolean listed, Boolean scratch, Error *err) if (scratch) { aco_save_T aco; aucmd_prepbuf(&aco, buf); - set_option_value("bh", 0L, "hide", OPT_LOCAL); - set_option_value("bt", 0L, "nofile", OPT_LOCAL); - set_option_value("swf", 0L, NULL, OPT_LOCAL); + set_option_value("bufhidden", 0L, "hide", OPT_LOCAL); + set_option_value("buftype", 0L, "nofile", OPT_LOCAL); + set_option_value("swapfile", 0L, NULL, OPT_LOCAL); + set_option_value("modeline", 0L, NULL, OPT_LOCAL); // 'nomodeline' aucmd_restbuf(&aco); } return buf->b_fnum; diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index 7c8f93163a..3ce39feda5 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -186,14 +186,17 @@ int open_buffer( } } - /* - * if there is no memfile at all, exit - * This is OK, since there are no changes to lose. - */ + // If there is no memfile at all, exit. + // This is OK, since there are no changes to lose. if (curbuf == NULL) { EMSG(_("E82: Cannot allocate any buffer, exiting...")); + + // Don't try to do any saving, with "curbuf" NULL almost nothing + // will work. + v_dying = 2; getout(2); } + EMSG(_("E83: Cannot allocate buffer, using other one...")); enter_buffer(curbuf); if (old_tw != curbuf->b_p_tw) { diff --git a/src/nvim/channel.c b/src/nvim/channel.c index 5eb29a7290..37cbfb968b 100644 --- a/src/nvim/channel.c +++ b/src/nvim/channel.c @@ -301,7 +301,8 @@ static void close_cb(Stream *stream, void *data) /// @returns [allocated] channel Channel *channel_job_start(char **argv, CallbackReader on_stdout, CallbackReader on_stderr, Callback on_exit, - bool pty, bool rpc, bool detach, const char *cwd, + bool pty, bool rpc, bool overlapped, bool detach, + const char *cwd, uint16_t pty_width, uint16_t pty_height, char *term_name, char **env, varnumber_T *status_out) { @@ -342,6 +343,7 @@ Channel *channel_job_start(char **argv, CallbackReader on_stdout, proc->detach = detach; proc->cwd = cwd; proc->env = env; + proc->overlapped = overlapped; char *cmd = xstrdup(proc->argv[0]); bool has_out, has_err; diff --git a/src/nvim/eval.c b/src/nvim/eval.c index 4acee7b453..66cd0e09c6 100644 --- a/src/nvim/eval.c +++ b/src/nvim/eval.c @@ -213,8 +213,8 @@ static struct vimvar { VV(VV_ERRORS, "errors", VAR_LIST, 0), VV(VV_MSGPACK_TYPES, "msgpack_types", VAR_DICT, VV_RO), VV(VV_EVENT, "event", VAR_DICT, VV_RO), - VV(VV_FALSE, "false", VAR_SPECIAL, VV_RO), - VV(VV_TRUE, "true", VAR_SPECIAL, VV_RO), + VV(VV_FALSE, "false", VAR_BOOL, VV_RO), + VV(VV_TRUE, "true", VAR_BOOL, VV_RO), VV(VV_NULL, "null", VAR_SPECIAL, VV_RO), VV(VV__NULL_LIST, "_null_list", VAR_LIST, VV_RO), VV(VV__NULL_DICT, "_null_dict", VAR_DICT, VV_RO), @@ -230,12 +230,14 @@ static struct vimvar { VV(VV_ECHOSPACE, "echospace", VAR_NUMBER, VV_RO), VV(VV_EXITING, "exiting", VAR_NUMBER, VV_RO), VV(VV_LUA, "lua", VAR_PARTIAL, VV_RO), + VV(VV_ARGV, "argv", VAR_LIST, VV_RO), }; #undef VV // shorthand #define vv_type vv_di.di_tv.v_type #define vv_nr vv_di.di_tv.vval.v_number +#define vv_bool vv_di.di_tv.vval.v_bool #define vv_special vv_di.di_tv.vval.v_special #define vv_float vv_di.di_tv.vval.v_float #define vv_str vv_di.di_tv.vval.v_string @@ -387,8 +389,8 @@ void eval_init(void) set_vim_var_nr(VV_TYPE_FLOAT, VAR_TYPE_FLOAT); set_vim_var_nr(VV_TYPE_BOOL, VAR_TYPE_BOOL); - set_vim_var_special(VV_FALSE, kSpecialVarFalse); - set_vim_var_special(VV_TRUE, kSpecialVarTrue); + set_vim_var_bool(VV_FALSE, kBoolVarFalse); + set_vim_var_bool(VV_TRUE, kBoolVarTrue); set_vim_var_special(VV_NULL, kSpecialVarNull); set_vim_var_special(VV_EXITING, kSpecialVarNull); @@ -443,7 +445,7 @@ void eval_clear(void) // unreferenced lists and dicts (void)garbage_collect(false); - // functions + // functions not garbage collected free_all_functions(); } @@ -872,17 +874,19 @@ char_u *eval_to_string(char_u *arg, char_u **nextcmd, int convert) char_u *eval_to_string_safe(char_u *arg, char_u **nextcmd, int use_sandbox) { char_u *retval; - void *save_funccalp; + funccal_entry_T funccal_entry; - save_funccalp = save_funccal(); - if (use_sandbox) - ++sandbox; - ++textlock; - retval = eval_to_string(arg, nextcmd, FALSE); - if (use_sandbox) - --sandbox; - --textlock; - restore_funccal(save_funccalp); + save_funccal(&funccal_entry); + if (use_sandbox) { + sandbox++; + } + textlock++; + retval = eval_to_string(arg, nextcmd, false); + if (use_sandbox) { + sandbox--; + } + textlock--; + restore_funccal(); return retval; } @@ -1834,12 +1838,15 @@ static char_u *ex_let_one(char_u *arg, typval_T *const tv, int opt_type; long numval; char *stringval = NULL; + const char *s = NULL; const char c1 = *p; *p = NUL; varnumber_T n = tv_get_number(tv); - const char *s = tv_get_string_chk(tv); // != NULL if number or string. + if (tv->v_type != VAR_BOOL && tv->v_type != VAR_SPECIAL) { + s = tv_get_string_chk(tv); // != NULL if number or string. + } if (s != NULL && op != NULL && *op != '=') { opt_type = get_option_value(arg, &numval, (char_u **)&stringval, opt_flags); @@ -1865,7 +1872,8 @@ static char_u *ex_let_one(char_u *arg, typval_T *const tv, } } } - if (s != NULL) { + if (s != NULL || tv->v_type == VAR_BOOL + || tv->v_type == VAR_SPECIAL) { set_option_value((const char *)arg, n, s, opt_flags); arg_end = (char_u *)p; } @@ -4196,6 +4204,7 @@ eval_index( } return FAIL; } + case VAR_BOOL: case VAR_SPECIAL: { if (verbose) { EMSG(_("E909: Cannot index a special variable")); @@ -4417,6 +4426,7 @@ eval_index( *rettv = var1; break; } + case VAR_BOOL: case VAR_SPECIAL: case VAR_FUNC: case VAR_FLOAT: @@ -5025,7 +5035,7 @@ bool garbage_collect(bool testing) // 3. Check if any funccal can be freed now. // This may call us back recursively. - did_free = did_free || free_unref_funccal(copyID, testing); + did_free = free_unref_funccal(copyID, testing) || did_free; } else if (p_verbose > 0) { verb_msg(_( "Not enough memory to set references, garbage collection aborted!")); @@ -5270,6 +5280,7 @@ bool set_ref_in_item(typval_T *tv, int copyID, ht_stack_T **ht_stack, abort = set_ref_in_func(tv->vval.v_string, NULL, copyID); break; case VAR_UNKNOWN: + case VAR_BOOL: case VAR_SPECIAL: case VAR_FLOAT: case VAR_NUMBER: @@ -5740,11 +5751,11 @@ int assert_bool(typval_T *argvars, bool is_true) if ((argvars[0].v_type != VAR_NUMBER || (tv_get_number_chk(&argvars[0], &error) == 0) == is_true || error) - && (argvars[0].v_type != VAR_SPECIAL - || (argvars[0].vval.v_special - != (SpecialVarValue) (is_true - ? kSpecialVarTrue - : kSpecialVarFalse)))) { + && (argvars[0].v_type != VAR_BOOL + || (argvars[0].vval.v_bool + != (BoolVarValue)(is_true + ? kBoolVarTrue + : kBoolVarFalse)))) { prepare_assert_error(&ga); fill_assert_error(&ga, &argvars[1], (char_u *)(is_true ? "True" : "False"), @@ -6117,7 +6128,7 @@ void common_function(typval_T *argvars, typval_T *rettv, if (tv_list_len(list) == 0) { arg_idx = 0; } else if (tv_list_len(list) > MAX_FUNC_ARGS) { - emsg_funcname((char *)e_toomanyarg, name); + emsg_funcname((char *)e_toomanyarg, s); xfree(name); goto theend; } @@ -6751,6 +6762,7 @@ void mapblock_fill_dict(dict_T *const dict, } tv_dict_add_allocated_str(dict, S_LEN("lhs"), lhs); tv_dict_add_nr(dict, S_LEN("noremap"), noremap_value); + tv_dict_add_nr(dict, S_LEN("script"), mp->m_noremap == REMAP_SCRIPT ? 1 : 0); tv_dict_add_nr(dict, S_LEN("expr"), mp->m_expr ? 1 : 0); tv_dict_add_nr(dict, S_LEN("silent"), mp->m_silent ? 1 : 0); tv_dict_add_nr(dict, S_LEN("sid"), (varnumber_T)mp->m_script_ctx.sc_sid); @@ -8045,6 +8057,17 @@ void set_vim_var_nr(const VimVarIndex idx, const varnumber_T val) vimvars[idx].vv_nr = val; } +/// Set boolean v: {true, false} to the given value +/// +/// @param[in] idx Index of variable to set. +/// @param[in] val Value to set to. +void set_vim_var_bool(const VimVarIndex idx, const BoolVarValue val) +{ + tv_clear(&vimvars[idx].vv_tv); + vimvars[idx].vv_type = VAR_BOOL; + vimvars[idx].vv_bool = val; +} + /// Set special v: variable to the given value /// /// @param[in] idx Index of variable to set. @@ -8108,6 +8131,23 @@ void set_vim_var_dict(const VimVarIndex idx, dict_T *const val) } } +/// Set the v:argv list. +void set_argv_var(char **argv, int argc) +{ + list_T *l = tv_list_alloc(argc); + int i; + + if (l == NULL) { + getout(1); + } + tv_list_set_lock(l, VAR_FIXED); + for (i = 0; i < argc; i++) { + tv_list_append_string(l, (const char *const)argv[i], -1); + TV_LIST_ITEM_TV(tv_list_last(l))->v_lock = VAR_FIXED; + } + set_vim_var_list(VV_ARGV, l); +} + /* * Set v:register if needed. */ @@ -9122,6 +9162,7 @@ int var_item_copy(const vimconv_T *const conv, case VAR_FLOAT: case VAR_FUNC: case VAR_PARTIAL: + case VAR_BOOL: case VAR_SPECIAL: tv_copy(from, to); break; @@ -9748,9 +9789,11 @@ const void *var_shada_iter(const void *const iter, const char **const name, void var_set_global(const char *const name, typval_T vartv) { - funccall_T *const saved_funccal = (funccall_T *)save_funccal(); + funccal_entry_T funccall_entry; + + save_funccal(&funccall_entry); set_var(name, strlen(name), &vartv, false); - restore_funccal(saved_funccal); + restore_funccal(); } int store_session_globals(FILE *fd) @@ -10306,8 +10349,10 @@ typval_T eval_call_provider(char *provider, char *method, list_T *arguments) .autocmd_fname = autocmd_fname, .autocmd_match = autocmd_match, .autocmd_bufnr = autocmd_bufnr, - .funccalp = save_funccal() + .funccalp = (void *)get_current_funccal() }; + funccal_entry_T funccal_entry; + save_funccal(&funccal_entry); provider_call_nesting++; typval_T argvars[3] = { @@ -10334,7 +10379,7 @@ typval_T eval_call_provider(char *provider, char *method, list_T *arguments) tv_list_unref(arguments); // Restore caller scope information - restore_funccal(provider_caller_scope.funccalp); + restore_funccal(); provider_caller_scope = saved_provider_caller_scope; provider_call_nesting--; assert(provider_call_nesting >= 0); diff --git a/src/nvim/eval.h b/src/nvim/eval.h index ebc0eb0b1a..0b4cbb3b4d 100644 --- a/src/nvim/eval.h +++ b/src/nvim/eval.h @@ -159,6 +159,7 @@ typedef enum { VV_ECHOSPACE, VV_EXITING, VV_LUA, + VV_ARGV, } VimVarIndex; /// All recognized msgpack types diff --git a/src/nvim/eval/decode.c b/src/nvim/eval/decode.c index 42999ddd62..daba304f00 100644 --- a/src/nvim/eval/decode.c +++ b/src/nvim/eval/decode.c @@ -795,9 +795,9 @@ json_decode_string_cycle_start: } p += 3; POP(((typval_T) { - .v_type = VAR_SPECIAL, + .v_type = VAR_BOOL, .v_lock = VAR_UNLOCKED, - .vval = { .v_special = kSpecialVarTrue }, + .vval = { .v_bool = kBoolVarTrue }, }), false); break; } @@ -808,9 +808,9 @@ json_decode_string_cycle_start: } p += 4; POP(((typval_T) { - .v_type = VAR_SPECIAL, + .v_type = VAR_BOOL, .v_lock = VAR_UNLOCKED, - .vval = { .v_special = kSpecialVarFalse }, + .vval = { .v_bool = kBoolVarFalse }, }), false); break; } @@ -954,10 +954,10 @@ int msgpack_to_vim(const msgpack_object mobj, typval_T *const rettv) } case MSGPACK_OBJECT_BOOLEAN: { *rettv = (typval_T) { - .v_type = VAR_SPECIAL, + .v_type = VAR_BOOL, .v_lock = VAR_UNLOCKED, .vval = { - .v_special = mobj.via.boolean ? kSpecialVarTrue : kSpecialVarFalse + .v_bool = mobj.via.boolean ? kBoolVarTrue : kBoolVarFalse }, }; break; diff --git a/src/nvim/eval/encode.c b/src/nvim/eval/encode.c index 138f638eb2..137f099df6 100644 --- a/src/nvim/eval/encode.c +++ b/src/nvim/eval/encode.c @@ -34,10 +34,13 @@ #define utf_ptr2len(b) ((size_t)utf_ptr2len((char_u *)b)) #define utf_char2len(b) ((size_t)utf_char2len(b)) +const char *const encode_bool_var_names[] = { + [kBoolVarTrue] = "true", + [kBoolVarFalse] = "false", +}; + const char *const encode_special_var_names[] = { [kSpecialVarNull] = "null", - [kSpecialVarTrue] = "true", - [kSpecialVarFalse] = "false", }; #ifdef INCLUDE_GENERATED_DECLARATIONS diff --git a/src/nvim/eval/encode.h b/src/nvim/eval/encode.h index ccea245ab3..596bb49ae0 100644 --- a/src/nvim/eval/encode.h +++ b/src/nvim/eval/encode.h @@ -55,6 +55,7 @@ static inline ListReaderState encode_init_lrstate(const list_T *const list) } /// Array mapping values from SpecialVarValue enum to names +extern const char *const encode_bool_var_names[]; extern const char *const encode_special_var_names[]; /// First codepoint in high surrogates block diff --git a/src/nvim/eval/executor.c b/src/nvim/eval/executor.c index 8cd21f8d62..da05ecda43 100644 --- a/src/nvim/eval/executor.c +++ b/src/nvim/eval/executor.c @@ -28,11 +28,13 @@ int eexe_mod_op(typval_T *const tv1, const typval_T *const tv2, FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NO_SANITIZE_UNDEFINED { // Can't do anything with a Funcref, a Dict or special value on the right. - if (tv2->v_type != VAR_FUNC && tv2->v_type != VAR_DICT) { + if (tv2->v_type != VAR_FUNC && tv2->v_type != VAR_DICT + && tv2->v_type != VAR_BOOL && tv2->v_type != VAR_SPECIAL) { switch (tv1->v_type) { case VAR_DICT: case VAR_FUNC: case VAR_PARTIAL: + case VAR_BOOL: case VAR_SPECIAL: { break; } diff --git a/src/nvim/eval/funcs.c b/src/nvim/eval/funcs.c index 79a52d9779..1071e75c06 100644 --- a/src/nvim/eval/funcs.c +++ b/src/nvim/eval/funcs.c @@ -175,8 +175,8 @@ static int non_zero_arg(typval_T *argvars) { return ((argvars[0].v_type == VAR_NUMBER && argvars[0].vval.v_number != 0) - || (argvars[0].v_type == VAR_SPECIAL - && argvars[0].vval.v_special == kSpecialVarTrue) + || (argvars[0].v_type == VAR_BOOL + && argvars[0].vval.v_bool == kBoolVarTrue) || (argvars[0].v_type == VAR_STRING && argvars[0].vval.v_string != NULL && *argvars[0].vval.v_string != NUL)); @@ -1758,21 +1758,23 @@ static void f_empty(typval_T *argvars, typval_T *rettv, FunPtr fptr) n = (tv_dict_len(argvars[0].vval.v_dict) == 0); break; } - case VAR_SPECIAL: { - // Using switch to get warning if SpecialVarValue receives more values. - switch (argvars[0].vval.v_special) { - case kSpecialVarTrue: { + case VAR_BOOL: { + switch (argvars[0].vval.v_bool) { + case kBoolVarTrue: { n = false; break; } - case kSpecialVarFalse: - case kSpecialVarNull: { + case kBoolVarFalse: { n = true; break; } } break; } + case VAR_SPECIAL: { + n = argvars[0].vval.v_special == kSpecialVarNull; + break; + } case VAR_UNKNOWN: { internal_error("f_empty(UNKNOWN)"); break; @@ -4860,6 +4862,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) bool rpc = false; bool pty = false; bool clear_env = false; + bool overlapped = false; CallbackReader on_stdout = CALLBACK_READER_INIT, on_stderr = CALLBACK_READER_INIT; Callback on_exit = CALLBACK_NONE; @@ -4871,12 +4874,23 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) rpc = tv_dict_get_number(job_opts, "rpc") != 0; pty = tv_dict_get_number(job_opts, "pty") != 0; clear_env = tv_dict_get_number(job_opts, "clear_env") != 0; + overlapped = tv_dict_get_number(job_opts, "overlapped") != 0; + if (pty && rpc) { EMSG2(_(e_invarg2), "job cannot have both 'pty' and 'rpc' options set"); shell_free_argv(argv); return; } +#ifdef WIN32 + if (pty && overlapped) { + EMSG2(_(e_invarg2), + "job cannot have both 'pty' and 'overlapped' options set"); + shell_free_argv(argv); + return; + } +#endif + char *new_cwd = tv_dict_get_string(job_opts, "cwd", false); if (new_cwd && strlen(new_cwd) > 0) { cwd = new_cwd; @@ -4943,7 +4957,7 @@ static void f_jobstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) } Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, pty, - rpc, detach, cwd, width, height, + rpc, overlapped, detach, cwd, width, height, term_name, env, &rettv->vval.v_number); if (chan) { channel_create_event(chan, NULL); @@ -5189,6 +5203,7 @@ static void f_len(typval_T *argvars, typval_T *rettv, FunPtr fptr) break; } case VAR_UNKNOWN: + case VAR_BOOL: case VAR_SPECIAL: case VAR_FLOAT: case VAR_PARTIAL: @@ -7243,7 +7258,7 @@ static void f_rpcrequest(typval_T *argvars, typval_T *rettv, FunPtr fptr) uint8_t *save_sourcing_name, *save_autocmd_fname, *save_autocmd_match; linenr_T save_sourcing_lnum; int save_autocmd_bufnr; - void *save_funccalp; + funccal_entry_T funccal_entry; if (l_provider_call_nesting) { // If this is called from a provider function, restore the scope @@ -7254,7 +7269,7 @@ static void f_rpcrequest(typval_T *argvars, typval_T *rettv, FunPtr fptr) save_autocmd_fname = autocmd_fname; save_autocmd_match = autocmd_match; save_autocmd_bufnr = autocmd_bufnr; - save_funccalp = save_funccal(); + save_funccal(&funccal_entry); current_sctx = provider_caller_scope.script_ctx; sourcing_name = provider_caller_scope.sourcing_name; @@ -7262,7 +7277,7 @@ static void f_rpcrequest(typval_T *argvars, typval_T *rettv, FunPtr fptr) autocmd_fname = provider_caller_scope.autocmd_fname; autocmd_match = provider_caller_scope.autocmd_match; autocmd_bufnr = provider_caller_scope.autocmd_bufnr; - restore_funccal(provider_caller_scope.funccalp); + set_current_funccal((funccall_T *)(provider_caller_scope.funccalp)); } @@ -7280,7 +7295,7 @@ static void f_rpcrequest(typval_T *argvars, typval_T *rettv, FunPtr fptr) autocmd_fname = save_autocmd_fname; autocmd_match = save_autocmd_match; autocmd_bufnr = save_autocmd_bufnr; - restore_funccal(save_funccalp); + restore_funccal(); } if (ERROR_SET(&err)) { @@ -7369,8 +7384,8 @@ static void f_rpcstart(typval_T *argvars, typval_T *rettv, FunPtr fptr) Channel *chan = channel_job_start(argv, CALLBACK_READER_INIT, CALLBACK_READER_INIT, CALLBACK_NONE, - false, true, false, NULL, 0, 0, NULL, NULL, - &rettv->vval.v_number); + false, true, false, false, NULL, 0, 0, + NULL, NULL, &rettv->vval.v_number); if (chan) { channel_create_event(chan, NULL); } @@ -10458,7 +10473,7 @@ static void f_termopen(typval_T *argvars, typval_T *rettv, FunPtr fptr) uint16_t term_width = MAX(0, curwin->w_width_inner - win_col_off(curwin)); Channel *chan = channel_job_start(argv, on_stdout, on_stderr, on_exit, - true, false, false, cwd, + true, false, false, false, cwd, term_width, curwin->w_height_inner, xstrdup("xterm-256color"), NULL, &rettv->vval.v_number); @@ -10808,20 +10823,8 @@ static void f_type(typval_T *argvars, typval_T *rettv, FunPtr fptr) case VAR_LIST: n = VAR_TYPE_LIST; break; case VAR_DICT: n = VAR_TYPE_DICT; break; case VAR_FLOAT: n = VAR_TYPE_FLOAT; break; - case VAR_SPECIAL: { - switch (argvars[0].vval.v_special) { - case kSpecialVarTrue: - case kSpecialVarFalse: { - n = VAR_TYPE_BOOL; - break; - } - case kSpecialVarNull: { - n = 7; - break; - } - } - break; - } + case VAR_BOOL: n = VAR_TYPE_BOOL; break; + case VAR_SPECIAL:n = VAR_TYPE_SPECIAL; break; case VAR_UNKNOWN: { internal_error("f_type(UNKNOWN)"); break; diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index 35130f6f40..0daaf6c878 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -1693,21 +1693,21 @@ int tv_dict_add_float(dict_T *const d, const char *const key, return OK; } -/// Add a special entry to dictionary +/// Add a boolean entry to dictionary /// /// @param[out] d Dictionary to add entry to. /// @param[in] key Key to add. /// @param[in] key_len Key length. -/// @param[in] val SpecialVarValue to add. +/// @param[in] val BoolVarValue to add. /// /// @return OK in case of success, FAIL when key already exists. -int tv_dict_add_special(dict_T *const d, const char *const key, - const size_t key_len, SpecialVarValue val) +int tv_dict_add_bool(dict_T *const d, const char *const key, + const size_t key_len, BoolVarValue val) { dictitem_T *const item = tv_dict_item_alloc_len(key, key_len); - item->di_tv.v_type = VAR_SPECIAL; - item->di_tv.vval.v_special = val; + item->di_tv.v_type = VAR_BOOL; + item->di_tv.vval.v_bool = val; if (tv_dict_add(d, item) == FAIL) { tv_dict_item_free(item); return FAIL; @@ -2013,12 +2013,15 @@ void tv_dict_alloc_ret(typval_T *const ret_tv) #define TYPVAL_ENCODE_CONV_NIL(tv) \ do { \ - tv->vval.v_special = kSpecialVarFalse; \ + tv->vval.v_special = kSpecialVarNull; \ tv->v_lock = VAR_UNLOCKED; \ } while (0) #define TYPVAL_ENCODE_CONV_BOOL(tv, num) \ - TYPVAL_ENCODE_CONV_NIL(tv) + do { \ + tv->vval.v_bool = kBoolVarFalse; \ + tv->v_lock = VAR_UNLOCKED; \ + } while (0) #define TYPVAL_ENCODE_CONV_NUMBER(tv, num) \ do { \ @@ -2293,6 +2296,7 @@ void tv_free(typval_T *tv) tv_dict_unref(tv->vval.v_dict); break; } + case VAR_BOOL: case VAR_SPECIAL: case VAR_NUMBER: case VAR_FLOAT: @@ -2324,6 +2328,7 @@ void tv_copy(const typval_T *const from, typval_T *const to) switch (from->v_type) { case VAR_NUMBER: case VAR_FLOAT: + case VAR_BOOL: case VAR_SPECIAL: { break; } @@ -2425,6 +2430,7 @@ void tv_item_lock(typval_T *const tv, const int deep, const bool lock) case VAR_STRING: case VAR_FUNC: case VAR_PARTIAL: + case VAR_BOOL: case VAR_SPECIAL: { break; } @@ -2588,6 +2594,9 @@ bool tv_equal(typval_T *const tv1, typval_T *const tv2, const bool ic, const char *s2 = tv_get_string_buf(tv2, buf2); return mb_strcmp_ic((bool)ic, s1, s2) == 0; } + case VAR_BOOL: { + return tv1->vval.v_bool == tv2->vval.v_bool; + } case VAR_SPECIAL: { return tv1->vval.v_special == tv2->vval.v_special; } @@ -2638,6 +2647,10 @@ bool tv_check_str_or_nr(const typval_T *const tv) EMSG(_("E728: Expected a Number or a String, Dictionary found")); return false; } + case VAR_BOOL: { + EMSG(_("E5299: Expected a Number or a String, Boolean found")); + return false; + } case VAR_SPECIAL: { EMSG(_("E5300: Expected a Number or a String")); return false; @@ -2677,6 +2690,7 @@ bool tv_check_num(const typval_T *const tv) { switch (tv->v_type) { case VAR_NUMBER: + case VAR_BOOL: case VAR_SPECIAL: case VAR_STRING: { return true; @@ -2721,6 +2735,7 @@ bool tv_check_str(const typval_T *const tv) { switch (tv->v_type) { case VAR_NUMBER: + case VAR_BOOL: case VAR_SPECIAL: case VAR_STRING: { return true; @@ -2791,8 +2806,11 @@ varnumber_T tv_get_number_chk(const typval_T *const tv, bool *const ret_error) } return n; } + case VAR_BOOL: { + return tv->vval.v_bool == kBoolVarTrue ? 1 : 0; + } case VAR_SPECIAL: { - return tv->vval.v_special == kSpecialVarTrue ? 1 : 0; + return 0; } case VAR_UNKNOWN: { emsgf(_(e_intern2), "tv_get_number(UNKNOWN)"); @@ -2860,6 +2878,10 @@ float_T tv_get_float(const typval_T *const tv) EMSG(_("E894: Using a Dictionary as a Float")); break; } + case VAR_BOOL: { + EMSG(_("E362: Using a boolean value as a Float")); + break; + } case VAR_SPECIAL: { EMSG(_("E907: Using a special value as a Float")); break; @@ -2897,6 +2919,10 @@ const char *tv_get_string_buf_chk(const typval_T *const tv, char *const buf) } return ""; } + case VAR_BOOL: { + STRCPY(buf, encode_bool_var_names[tv->vval.v_bool]); + return buf; + } case VAR_SPECIAL: { STRCPY(buf, encode_special_var_names[tv->vval.v_special]); return buf; diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index 4390db1b71..343dd205ff 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -91,10 +91,14 @@ typedef struct dict_watcher { bool busy; // prevent recursion if the dict is changed in the callback } DictWatcher; +/// Bool variable values +typedef enum { + kBoolVarFalse, ///< v:false + kBoolVarTrue, ///< v:true +} BoolVarValue; + /// Special variable values typedef enum { - kSpecialVarFalse, ///< v:false - kSpecialVarTrue, ///< v:true kSpecialVarNull, ///< v:null } SpecialVarValue; @@ -114,6 +118,7 @@ typedef enum { VAR_LIST, ///< List, .v_list is used. VAR_DICT, ///< Dictionary, .v_dict is used. VAR_FLOAT, ///< Floating-point value, .v_float is used. + VAR_BOOL, ///< true, false VAR_SPECIAL, ///< Special value (true, false, null), .v_special ///< is used. VAR_PARTIAL, ///< Partial, .v_partial is used. @@ -125,6 +130,7 @@ typedef struct { VarLockStatus v_lock; ///< Variable lock status. union typval_vval_union { varnumber_T v_number; ///< Number, for VAR_NUMBER. + BoolVarValue v_bool; ///< Bool value, for VAR_BOOL SpecialVarValue v_special; ///< Special value, for VAR_SPECIAL. float_T v_float; ///< Floating-point number, for VAR_FLOAT. char_u *v_string; ///< String, for VAR_STRING and VAR_FUNC, can be NULL. diff --git a/src/nvim/eval/typval_encode.c.h b/src/nvim/eval/typval_encode.c.h index af21a6fbe3..0aa64b1d5f 100644 --- a/src/nvim/eval/typval_encode.c.h +++ b/src/nvim/eval/typval_encode.c.h @@ -379,17 +379,22 @@ static int _TYPVAL_ENCODE_CONVERT_ONE_VALUE( TYPVAL_ENCODE_CONV_REAL_LIST_AFTER_START(tv, _mp_last(*mpstack)); break; } + case VAR_BOOL: { + switch (tv->vval.v_bool) { + case kBoolVarTrue: + case kBoolVarFalse: { + TYPVAL_ENCODE_CONV_BOOL(tv, tv->vval.v_bool == kBoolVarTrue); + break; + } + } + break; + } case VAR_SPECIAL: { switch (tv->vval.v_special) { case kSpecialVarNull: { TYPVAL_ENCODE_CONV_NIL(tv); // -V1037 break; } - case kSpecialVarTrue: - case kSpecialVarFalse: { - TYPVAL_ENCODE_CONV_BOOL(tv, tv->vval.v_special == kSpecialVarTrue); - break; - } } break; } @@ -607,7 +612,7 @@ _convert_one_value_regular_dict: {} kMPConvDict); TYPVAL_ENCODE_CONV_DICT_START(tv, tv->vval.v_dict, tv->vval.v_dict->dv_hashtab.ht_used); - assert(saved_copyID != copyID && saved_copyID != copyID - 1); + assert(saved_copyID != copyID); _mp_push(*mpstack, ((MPConvStackVal) { .tv = tv, .type = kMPConvDict, diff --git a/src/nvim/eval/userfunc.c b/src/nvim/eval/userfunc.c index ae8557a8bc..4d658498c1 100644 --- a/src/nvim/eval/userfunc.c +++ b/src/nvim/eval/userfunc.c @@ -43,11 +43,11 @@ hashtab_T func_hashtab; static garray_T funcargs = GA_EMPTY_INIT_VALUE; // pointer to funccal for currently active function -funccall_T *current_funccal = NULL; +static funccall_T *current_funccal = NULL; // Pointer to list of previously used funccal, still around because some // item in it is still being used. -funccall_T *previous_funccal = NULL; +static funccall_T *previous_funccal = NULL; static char *e_funcexts = N_( "E122: Function %s already exists, add ! to replace it"); @@ -170,6 +170,7 @@ int get_lambda_tv(char_u **arg, typval_T *rettv, bool evaluate) garray_T newargs = GA_EMPTY_INIT_VALUE; garray_T *pnewargs; ufunc_T *fp = NULL; + partial_T *pt = NULL; int varargs; int ret; char_u *start = skipwhite(*arg + 1); @@ -219,7 +220,6 @@ int get_lambda_tv(char_u **arg, typval_T *rettv, bool evaluate) int len, flags = 0; char_u *p; char_u name[20]; - partial_T *pt; garray_T newlines; lambda_no++; @@ -274,6 +274,7 @@ int get_lambda_tv(char_u **arg, typval_T *rettv, bool evaluate) errret: ga_clear_strings(&newargs); xfree(fp); + xfree(pt); eval_lavars_used = old_eval_lavars; return FAIL; } @@ -541,14 +542,8 @@ static void add_nr_var(dict_T *dp, dictitem_T *v, char *name, varnumber_T nr) v->di_tv.vval.v_number = nr; } -/* - * Free "fc" and what it contains. - */ -static void -free_funccal( - funccall_T *fc, - int free_val // a: vars were allocated -) +// Free "fc" +static void free_funccal(funccall_T *fc) { for (int i = 0; i < fc->fc_funcs.ga_len; i++) { ufunc_T *fp = ((ufunc_T **)(fc->fc_funcs.ga_data))[i]; @@ -563,56 +558,89 @@ free_funccal( } ga_clear(&fc->fc_funcs); - // The a: variables typevals may not have been allocated, only free the - // allocated variables. - vars_clear_ext(&fc->l_avars.dv_hashtab, free_val); + func_ptr_unref(fc->func); + xfree(fc); +} +// Free "fc" and what it contains. +// Can be called only when "fc" is kept beyond the period of it called, +// i.e. after cleanup_function_call(fc). +static void free_funccal_contents(funccall_T *fc) +{ // Free all l: variables. vars_clear(&fc->l_vars.dv_hashtab); - // Free the a:000 variables if they were allocated. - if (free_val) { - TV_LIST_ITER(&fc->l_varlist, li, { - tv_clear(TV_LIST_ITEM_TV(li)); - }); - } + // Free all a: variables. + vars_clear(&fc->l_avars.dv_hashtab); - func_ptr_unref(fc->func); - xfree(fc); + // Free the a:000 variables. + TV_LIST_ITER(&fc->l_varlist, li, { + tv_clear(TV_LIST_ITEM_TV(li)); + }); + + free_funccal(fc); } /// Handle the last part of returning from a function: free the local hashtable. /// Unless it is still in use by a closure. static void cleanup_function_call(funccall_T *fc) { + bool may_free_fc = fc->fc_refcount <= 0; + bool free_fc = true; + current_funccal = fc->caller; - // If the a:000 list and the l: and a: dicts are not referenced and there - // is no closure using it, we can free the funccall_T and what's in it. - if (!fc_referenced(fc)) { - free_funccal(fc, false); + // Free all l: variables if not referred. + if (may_free_fc && fc->l_vars.dv_refcount == DO_NOT_FREE_CNT) { + vars_clear(&fc->l_vars.dv_hashtab); } else { - static int made_copy = 0; + free_fc = false; + } - // "fc" is still in use. This can happen when returning "a:000", - // assigning "l:" to a global variable or defining a closure. - // Link "fc" in the list for garbage collection later. - fc->caller = previous_funccal; - previous_funccal = fc; + // If the a:000 list and the l: and a: dicts are not referenced and + // there is no closure using it, we can free the funccall_T and what's + // in it. + if (may_free_fc && fc->l_avars.dv_refcount == DO_NOT_FREE_CNT) { + vars_clear_ext(&fc->l_avars.dv_hashtab, false); + } else { + free_fc = false; // Make a copy of the a: variables, since we didn't do that above. TV_DICT_ITER(&fc->l_avars, di, { tv_copy(&di->di_tv, &di->di_tv); }); + } + + if (may_free_fc && fc->l_varlist.lv_refcount // NOLINT(runtime/deprecated) + == DO_NOT_FREE_CNT) { + fc->l_varlist.lv_first = NULL; // NOLINT(runtime/deprecated) + + } else { + free_fc = false; // Make a copy of the a:000 items, since we didn't do that above. TV_LIST_ITER(&fc->l_varlist, li, { tv_copy(TV_LIST_ITEM_TV(li), TV_LIST_ITEM_TV(li)); }); + } - if (++made_copy == 10000) { - // We have made a lot of copies. This can happen when - // repetitively calling a function that creates a reference to + if (free_fc) { + free_funccal(fc); + } else { + static int made_copy = 0; + + // "fc" is still in use. This can happen when returning "a:000", + // assigning "l:" to a global variable or defining a closure. + // Link "fc" in the list for garbage collection later. + fc->caller = previous_funccal; + previous_funccal = fc; + + if (want_garbage_collect) { + // If garbage collector is ready, clear count. + made_copy = 0; + } else if (++made_copy >= (int)((4096 * 1024) / sizeof(*fc))) { + // We have made a lot of copies, worth 4 Mbyte. This can happen + // when repetitively calling a function that creates a reference to // itself somehow. Call the garbage collector soon to avoid using // too much memory. made_copy = 0; @@ -639,7 +667,7 @@ static void funccal_unref(funccall_T *fc, ufunc_T *fp, bool force) for (pfc = &previous_funccal; *pfc != NULL; pfc = &(*pfc)->caller) { if (fc == *pfc) { *pfc = fc->caller; - free_funccal(fc, true); + free_funccal_contents(fc); return; } } @@ -766,7 +794,7 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, // check for CTRL-C hit line_breakcheck(); // prepare the funccall_T structure - fc = xmalloc(sizeof(funccall_T)); + fc = xcalloc(1, sizeof(funccall_T)); fc->caller = current_funccal; current_funccal = fc; fc->func = fp; @@ -881,9 +909,11 @@ void call_user_func(ufunc_T *fp, int argcount, typval_T *argvars, } if (ai >= 0 && ai < MAX_FUNC_ARGS) { - tv_list_append(&fc->l_varlist, &fc->l_listitems[ai]); - *TV_LIST_ITEM_TV(&fc->l_listitems[ai]) = argvars[i]; - TV_LIST_ITEM_TV(&fc->l_listitems[ai])->v_lock = VAR_FIXED; + listitem_T *li = &fc->l_listitems[ai]; + + *TV_LIST_ITEM_TV(li) = argvars[i]; + TV_LIST_ITEM_TV(li)->v_lock = VAR_FIXED; + tv_list_append(&fc->l_varlist, li); } } @@ -1106,21 +1136,26 @@ static bool func_name_refcount(char_u *name) return isdigit(*name) || *name == '<'; } -/* - * Save the current function call pointer, and set it to NULL. - * Used when executing autocommands and for ":source". - */ -void *save_funccal(void) -{ - funccall_T *fc = current_funccal; +static funccal_entry_T *funccal_stack = NULL; +// Save the current function call pointer, and set it to NULL. +// Used when executing autocommands and for ":source". +void save_funccal(funccal_entry_T *entry) +{ + entry->top_funccal = current_funccal; + entry->next = funccal_stack; + funccal_stack = entry; current_funccal = NULL; - return (void *)fc; } -void restore_funccal(void *vfc) +void restore_funccal(void) { - current_funccal = (funccall_T *)vfc; + if (funccal_stack == NULL) { + IEMSG("INTERNAL: restore_funccal()"); + } else { + current_funccal = funccal_stack->top_funccal; + funccal_stack = funccal_stack->next; + } } funccall_T *get_current_funccal(void) @@ -1128,6 +1163,11 @@ funccall_T *get_current_funccal(void) return current_funccal; } +void set_current_funccal(funccall_T *fc) +{ + current_funccal = fc; +} + #if defined(EXITFREE) void free_all_functions(void) { @@ -1137,10 +1177,13 @@ void free_all_functions(void) uint64_t todo = 1; uint64_t used; - // Clean up the call stack. + // Clean up the current_funccal chain and the funccal stack. while (current_funccal != NULL) { tv_clear(current_funccal->rettv); cleanup_function_call(current_funccal); + if (current_funccal == NULL && funccal_stack != NULL) { + restore_funccal(); + } } // First clear what the functions contain. Since this may lower the @@ -3121,7 +3164,7 @@ bool free_unref_funccal(int copyID, int testing) if (can_free_funccal(*pfc, copyID)) { funccall_T *fc = *pfc; *pfc = fc->caller; - free_funccal(fc, true); + free_funccal_contents(fc); did_free = true; did_free_funccal = true; } else { @@ -3314,9 +3357,18 @@ bool set_ref_in_call_stack(int copyID) bool abort = false; for (funccall_T *fc = current_funccal; fc != NULL; fc = fc->caller) { - abort = abort || set_ref_in_ht(&fc->l_vars.dv_hashtab, copyID, NULL); - abort = abort || set_ref_in_ht(&fc->l_avars.dv_hashtab, copyID, NULL); + abort = abort || set_ref_in_funccal(fc, copyID); } + + // Also go through the funccal_stack. + for (funccal_entry_T *entry = funccal_stack; entry != NULL; + entry = entry->next) { + for (funccall_T *fc = entry->top_funccal; !abort && fc != NULL; + fc = fc->caller) { + abort = abort || set_ref_in_funccal(fc, copyID); + } + } + return abort; } diff --git a/src/nvim/eval/userfunc.h b/src/nvim/eval/userfunc.h index ad8e071548..e8ad0bf1da 100644 --- a/src/nvim/eval/userfunc.h +++ b/src/nvim/eval/userfunc.h @@ -11,6 +11,12 @@ typedef struct { dictitem_T *fd_di; ///< Dictionary item used. } funcdict_T; +typedef struct funccal_entry funccal_entry_T; +struct funccal_entry { + void *top_funccal; + funccal_entry_T *next; +}; + /// errors for when calling a function typedef enum { ERROR_UNKNOWN = 0, diff --git a/src/nvim/event/libuv_process.c b/src/nvim/event/libuv_process.c index 37b9f73ba4..13517d3df1 100644 --- a/src/nvim/event/libuv_process.c +++ b/src/nvim/event/libuv_process.c @@ -52,7 +52,7 @@ int libuv_process_spawn(LibuvProcess *uvproc) if (!proc->in.closed) { uvproc->uvstdio[0].flags = UV_CREATE_PIPE | UV_READABLE_PIPE; #ifdef WIN32 - uvproc->uvstdio[0].flags |= UV_OVERLAPPED_PIPE; + uvproc->uvstdio[0].flags |= proc->overlapped ? UV_OVERLAPPED_PIPE : 0; #endif uvproc->uvstdio[0].data.stream = STRUCT_CAST(uv_stream_t, &proc->in.uv.pipe); @@ -61,8 +61,9 @@ int libuv_process_spawn(LibuvProcess *uvproc) if (!proc->out.closed) { uvproc->uvstdio[1].flags = UV_CREATE_PIPE | UV_WRITABLE_PIPE; #ifdef WIN32 - // pipe must be readable for IOCP to work. - uvproc->uvstdio[1].flags |= UV_READABLE_PIPE | UV_OVERLAPPED_PIPE; + // pipe must be readable for IOCP to work on Windows. + uvproc->uvstdio[1].flags |= proc->overlapped ? + (UV_READABLE_PIPE | UV_OVERLAPPED_PIPE) : 0; #endif uvproc->uvstdio[1].data.stream = STRUCT_CAST(uv_stream_t, &proc->out.uv.pipe); diff --git a/src/nvim/event/process.h b/src/nvim/event/process.h index b677b80bfe..84e81238e9 100644 --- a/src/nvim/event/process.h +++ b/src/nvim/event/process.h @@ -27,7 +27,7 @@ struct process { Stream in, out, err; process_exit_cb cb; internal_process_cb internal_exit_cb, internal_close_cb; - bool closed, detach; + bool closed, detach, overlapped; MultiQueue *events; }; diff --git a/src/nvim/event/stream.c b/src/nvim/event/stream.c index 0e87f7c6c1..1e9e530a42 100644 --- a/src/nvim/event/stream.c +++ b/src/nvim/event/stream.c @@ -11,6 +11,9 @@ #include "nvim/rbuffer.h" #include "nvim/macros.h" #include "nvim/event/stream.h" +#ifdef WIN32 +# include "nvim/os/os_win_console.h" +#endif #ifdef INCLUDE_GENERATED_DECLARATIONS # include "event/stream.c.generated.h" @@ -62,6 +65,11 @@ void stream_init(Loop *loop, Stream *stream, int fd, uv_stream_t *uvstream) if (type == UV_TTY) { uv_tty_init(&loop->uv, &stream->uv.tty, fd, 0); uv_tty_set_mode(&stream->uv.tty, UV_TTY_MODE_RAW); + DWORD dwMode; + if (GetConsoleMode(stream->uv.tty.handle, &dwMode)) { + dwMode |= ENABLE_VIRTUAL_TERMINAL_INPUT; + SetConsoleMode(stream->uv.tty.handle, dwMode); + } stream->uvstream = STRUCT_CAST(uv_stream_t, &stream->uv.tty); } else { #endif diff --git a/src/nvim/ex_cmds2.c b/src/nvim/ex_cmds2.c index 9f4055af8d..7f4b01e306 100644 --- a/src/nvim/ex_cmds2.c +++ b/src/nvim/ex_cmds2.c @@ -3107,7 +3107,6 @@ int do_source(char_u *fname, int check_other, int is_vimrc) int retval = FAIL; static scid_T last_current_SID = 0; static int last_current_SID_seq = 0; - void *save_funccalp; int save_debug_break_level = debug_break_level; scriptitem_T *si = NULL; proftime_T wait_start; @@ -3228,7 +3227,8 @@ int do_source(char_u *fname, int check_other, int is_vimrc) // Don't use local function variables, if called from a function. // Also starts profiling timer for nested script. - save_funccalp = save_funccal(); + funccal_entry_T funccalp_entry; + save_funccal(&funccalp_entry); // Check if this script was sourced before to finds its SID. // If it's new, generate a new SID. @@ -3353,7 +3353,7 @@ int do_source(char_u *fname, int check_other, int is_vimrc) } current_sctx = save_current_sctx; - restore_funccal(save_funccalp); + restore_funccal(); if (l_do_profiling == PROF_YES) { prof_child_exit(&wait_start); // leaving a child now } diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index b799e86ff7..1a836b7a98 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -432,8 +432,8 @@ static uint8_t *command_line_enter(int firstc, long count, int indent) tv_dict_add_nr(dict, S_LEN("cmdlevel"), ccline.level); tv_dict_set_keys_readonly(dict); // not readonly: - tv_dict_add_special(dict, S_LEN("abort"), - s->gotesc ? kSpecialVarTrue : kSpecialVarFalse); + tv_dict_add_bool(dict, S_LEN("abort"), + s->gotesc ? kBoolVarTrue : kBoolVarFalse); try_enter(&tstate); apply_autocmds(EVENT_CMDLINELEAVE, (char_u *)firstcbuf, (char_u *)firstcbuf, false, curbuf); @@ -5361,12 +5361,12 @@ void globpath(char_u *path, char_u *file, garray_T *ga, int expand_options) // Concatenate new results to previous ones. ga_grow(ga, num_p); + // take over the pointers and put them in "ga" for (int i = 0; i < num_p; i++) { - ((char_u **)ga->ga_data)[ga->ga_len] = vim_strsave(p[i]); + ((char_u **)ga->ga_data)[ga->ga_len] = p[i]; ga->ga_len++; } - - FreeWild(num_p, p); + xfree(p); } } } diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index 2335aba6dd..f29304867a 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -6736,7 +6736,6 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, static int nesting = 0; AutoPatCmd patcmd; AutoPat *ap; - void *save_funccalp; char_u *save_cmdarg; long save_cmdbang; static int filechangeshell_busy = FALSE; @@ -6926,8 +6925,9 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, if (do_profiling == PROF_YES) prof_child_enter(&wait_time); /* doesn't count for the caller itself */ - /* Don't use local function variables, if called from a function */ - save_funccalp = save_funccal(); + // Don't use local function variables, if called from a function. + funccal_entry_T funccal_entry; + save_funccal(&funccal_entry); /* * When starting to execute autocommands, save the search patterns. @@ -7016,9 +7016,10 @@ static bool apply_autocmds_group(event_T event, char_u *fname, char_u *fname_io, autocmd_bufnr = save_autocmd_bufnr; autocmd_match = save_autocmd_match; current_sctx = save_current_sctx; - restore_funccal(save_funccalp); - if (do_profiling == PROF_YES) + restore_funccal(); + if (do_profiling == PROF_YES) { prof_child_exit(&wait_time); + } KeyTyped = save_KeyTyped; xfree(fname); xfree(sfname); diff --git a/src/nvim/globals.h b/src/nvim/globals.h index f102c3ddd8..d6d00d6e83 100644 --- a/src/nvim/globals.h +++ b/src/nvim/globals.h @@ -203,7 +203,7 @@ EXTERN int msg_nowait INIT(= false); // don't wait for this msg EXTERN int emsg_off INIT(= 0); // don't display errors for now, // unless 'debug' is set. EXTERN int info_message INIT(= false); // printing informative message -EXTERN int msg_hist_off INIT(= false); // don't add messages to history +EXTERN bool msg_hist_off INIT(= false); // don't add messages to history EXTERN int need_clr_eos INIT(= false); // need to clear text before // displaying a message. EXTERN int emsg_skip INIT(= 0); // don't display errors for @@ -478,6 +478,8 @@ EXTERN int sc_col; // column for shown command EXTERN int starting INIT(= NO_SCREEN); // true when planning to exit. Might keep running if there is a changed buffer. EXTERN bool exiting INIT(= false); +// internal value of v:dying +EXTERN int v_dying INIT(= 0); // is stdin a terminal? EXTERN int stdin_isatty INIT(= true); // is stdout a terminal? diff --git a/src/nvim/keymap.c b/src/nvim/keymap.c index 4b8b9992f5..a553110552 100644 --- a/src/nvim/keymap.c +++ b/src/nvim/keymap.c @@ -517,8 +517,8 @@ char_u *get_special_key_name(int c, int modifiers) /// @param[in,out] srcp Source from which <> are translated. Is advanced to /// after the <> name if there is a match. /// @param[in] src_len Length of the srcp. -/// @param[out] dst Location where translation result will be kept. Must have -/// at least six bytes. +/// @param[out] dst Location where translation result will be kept. It must +// be at least 19 bytes per "<x>" form. /// @param[in] keycode Prefer key code, e.g. K_DEL in place of DEL. /// @param[in] in_string Inside a double quoted string /// diff --git a/src/nvim/lua/converter.c b/src/nvim/lua/converter.c index fca74b5901..69114c967d 100644 --- a/src/nvim/lua/converter.c +++ b/src/nvim/lua/converter.c @@ -286,10 +286,10 @@ bool nlua_pop_typval(lua_State *lstate, typval_T *ret_tv) break; } case LUA_TBOOLEAN: { - cur.tv->v_type = VAR_SPECIAL; - cur.tv->vval.v_special = (lua_toboolean(lstate, -1) - ? kSpecialVarTrue - : kSpecialVarFalse); + cur.tv->v_type = VAR_BOOL; + cur.tv->vval.v_bool = (lua_toboolean(lstate, -1) + ? kBoolVarTrue + : kBoolVarFalse); break; } case LUA_TSTRING: { diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 144646fca2..327ed6d6b7 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -28,6 +28,8 @@ #include "nvim/ascii.h" #include "nvim/change.h" #include "nvim/eval/userfunc.h" +#include "nvim/event/time.h" +#include "nvim/event/loop.h" #ifdef WIN32 #include "nvim/os/os.h" @@ -255,6 +257,101 @@ static struct luaL_Reg regex_meta[] = { { NULL, NULL } }; +// Dummy timer callback. Used by f_wait(). +static void dummy_timer_due_cb(TimeWatcher *tw, void *data) +{ +} + +// Dummy timer close callback. Used by f_wait(). +static void dummy_timer_close_cb(TimeWatcher *tw, void *data) +{ + xfree(tw); +} + +static bool nlua_wait_condition(lua_State *lstate, int *status, + bool *callback_result) +{ + lua_pushvalue(lstate, 2); + *status = lua_pcall(lstate, 0, 1, 0); + if (*status) { + return true; // break on error, but keep error on stack + } + *callback_result = lua_toboolean(lstate, -1); + lua_pop(lstate, 1); + return *callback_result; // break if true +} + +/// "vim.wait(timeout, condition[, interval])" function +static int nlua_wait(lua_State *lstate) + FUNC_ATTR_NONNULL_ALL +{ + intptr_t timeout = luaL_checkinteger(lstate, 1); + if (timeout < 0) { + return luaL_error(lstate, "timeout must be > 0"); + } + + // Check if condition can be called. + bool is_function = (lua_type(lstate, 2) == LUA_TFUNCTION); + + // Check if condition is callable table + if (!is_function && luaL_getmetafield(lstate, 2, "__call") != 0) { + is_function = (lua_type(lstate, -1) == LUA_TFUNCTION); + lua_pop(lstate, 1); + } + + if (!is_function) { + lua_pushliteral(lstate, "vim.wait: condition must be a function"); + return lua_error(lstate); + } + + intptr_t interval = 200; + if (lua_gettop(lstate) >= 3) { + interval = luaL_checkinteger(lstate, 3); + if (interval < 0) { + return luaL_error(lstate, "interval must be > 0"); + } + } + + TimeWatcher *tw = xmalloc(sizeof(TimeWatcher)); + + // Start dummy timer. + time_watcher_init(&main_loop, tw, NULL); + tw->events = main_loop.events; + tw->blockable = true; + time_watcher_start(tw, dummy_timer_due_cb, + (uint64_t)interval, (uint64_t)interval); + + int pcall_status = 0; + bool callback_result = false; + + LOOP_PROCESS_EVENTS_UNTIL( + &main_loop, + main_loop.events, + (int)timeout, + nlua_wait_condition(lstate, &pcall_status, &callback_result) || got_int); + + // Stop dummy timer + time_watcher_stop(tw); + time_watcher_close(tw, dummy_timer_close_cb); + + if (pcall_status) { + return lua_error(lstate); + } else if (callback_result) { + lua_pushboolean(lstate, 1); + lua_pushnil(lstate); + } else if (got_int) { + got_int = false; + vgetc(); + lua_pushboolean(lstate, 0); + lua_pushinteger(lstate, -2); + } else { + lua_pushboolean(lstate, 0); + lua_pushinteger(lstate, -1); + } + + return 2; +} + /// Initialize lua interpreter state /// /// Called by lua interpreter itself to initialize state. @@ -305,7 +402,6 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL // regex lua_pushcfunction(lstate, &nlua_regex); lua_setfield(lstate, -2, "regex"); - luaL_newmetatable(lstate, "nvim_regex"); luaL_register(lstate, NULL, regex_meta); lua_pushvalue(lstate, -1); // [meta, meta] @@ -320,6 +416,10 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_pushcfunction(lstate, &nlua_rpcnotify); lua_setfield(lstate, -2, "rpcnotify"); + // wait + lua_pushcfunction(lstate, &nlua_wait); + lua_setfield(lstate, -2, "wait"); + // vim.loop luv_set_loop(lstate, &main_loop.uv); luv_set_callback(lstate, nlua_luv_cfpcall); diff --git a/src/nvim/main.c b/src/nvim/main.c index 4a9f2371a2..b3a903cbe3 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -269,6 +269,8 @@ int main(int argc, char **argv) early_init(); + set_argv_var(argv, argc); // set v:argv + // Check if we have an interactive window. check_and_set_isatty(¶ms); @@ -619,7 +621,7 @@ void getout(int exitval) /* Optionally print hashtable efficiency. */ hash_debug_results(); - if (get_vim_var_nr(VV_DYING) <= 1) { + if (v_dying <= 1) { const tabpage_T *next_tp; // Trigger BufWinLeave for all windows, but only once per buffer. @@ -668,8 +670,9 @@ void getout(int exitval) shada_write_file(NULL, false); } - if (get_vim_var_nr(VV_DYING) <= 1) - apply_autocmds(EVENT_VIMLEAVE, NULL, NULL, FALSE, curbuf); + if (v_dying <= 1) { + apply_autocmds(EVENT_VIMLEAVE, NULL, NULL, false, curbuf); + } profile_dump(); diff --git a/src/nvim/message.c b/src/nvim/message.c index a12e665099..8999365d32 100644 --- a/src/nvim/message.c +++ b/src/nvim/message.c @@ -306,11 +306,6 @@ bool msg_attr_keep(char_u *s, int attr, bool keep, bool multiline) add_msg_hist((const char *)s, -1, attr, multiline); } - /* When displaying keep_msg, don't let msg_start() free it, caller must do - * that. */ - if (s == keep_msg) - keep_msg = NULL; - /* Truncate the message if needed. */ msg_start(); buf = msg_strtrunc(s, FALSE); @@ -2574,10 +2569,15 @@ static int do_more_prompt(int typed_char) msgchunk_T *mp; int i; + // If headless mode is enabled and no input is required, this variable + // will be true. However If server mode is enabled, the message "--more--" + // should be displayed. + bool no_need_more = headless_mode && !embedded_mode; + // We get called recursively when a timer callback outputs a message. In // that case don't show another prompt. Also when at the hit-Enter prompt // and nothing was typed. - if (entered || (State == HITRETURN && typed_char == 0)) { + if (no_need_more || entered || (State == HITRETURN && typed_char == 0)) { return false; } entered = true; diff --git a/src/nvim/move.c b/src/nvim/move.c index d4f82bc601..8a8a639a52 100644 --- a/src/nvim/move.c +++ b/src/nvim/move.c @@ -996,7 +996,7 @@ void textpos2screenpos(win_T *wp, pos_T *pos, int *rowp, int *scolp, col -= wp->w_leftcol; - if (col >= 0 && col < width) { + if (col >= 0 && col < wp->w_width) { coloff = col - scol + (local ? 0 : wp->w_wincol) + 1; } else { scol = ccol = ecol = 0; diff --git a/src/nvim/normal.c b/src/nvim/normal.c index 87d687198d..968cfde388 100644 --- a/src/nvim/normal.c +++ b/src/nvim/normal.c @@ -621,6 +621,8 @@ static void normal_redraw_mode_message(NormalState *s) update_screen(0); // now reset it, otherwise it's put in the history again keep_msg = kmsg; + + kmsg = vim_strsave(keep_msg); msg_attr((const char *)kmsg, keep_msg_attr); xfree(kmsg); } @@ -1265,10 +1267,15 @@ static void normal_redraw(NormalState *s) // Display message after redraw. If an external message is still visible, // it contains the kept message already. if (keep_msg != NULL && !msg_ext_is_visible()) { - // msg_attr_keep() will set keep_msg to NULL, must free the string here. - // Don't reset keep_msg, msg_attr_keep() uses it to check for duplicates. - char *p = (char *)keep_msg; - msg_attr(p, keep_msg_attr); + char_u *const p = vim_strsave(keep_msg); + + // msg_start() will set keep_msg to NULL, make a copy + // first. Don't reset keep_msg, msg_attr_keep() uses it to + // check for duplicates. Never put this message in + // history. + msg_hist_off = true; + msg_attr((const char *)p, keep_msg_attr); + msg_hist_off = false; xfree(p); } diff --git a/src/nvim/ops.c b/src/nvim/ops.c index a70224f98b..eb32a1dd9b 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -2740,14 +2740,18 @@ static void do_autocmd_textyankpost(oparg_T *oap, yankreg_T *reg) tv_dict_add_str(dict, S_LEN("regname"), buf); // Motion type: inclusive or exclusive. - tv_dict_add_special(dict, S_LEN("inclusive"), - oap->inclusive ? kSpecialVarTrue : kSpecialVarFalse); + tv_dict_add_bool(dict, S_LEN("inclusive"), + oap->inclusive ? kBoolVarTrue : kBoolVarFalse); // Kind of operation: yank, delete, change). buf[0] = (char)get_op_char(oap->op_type); buf[1] = NUL; tv_dict_add_str(dict, S_LEN("operator"), buf); + // Selection type: visual or not. + tv_dict_add_bool(dict, S_LEN("visual"), + oap->is_VIsual ? kBoolVarTrue : kBoolVarFalse); + tv_dict_set_keys_readonly(dict); textlock++; apply_autocmds(EVENT_TEXTYANKPOST, NULL, NULL, false, curbuf); diff --git a/src/nvim/os/input.c b/src/nvim/os/input.c index c1580c5fc3..b7878d9da8 100644 --- a/src/nvim/os/input.c +++ b/src/nvim/os/input.c @@ -188,8 +188,13 @@ size_t input_enqueue(String keys) char *ptr = keys.data; char *end = ptr + keys.size; - while (rbuffer_space(input_buffer) >= 6 && ptr < end) { - uint8_t buf[6] = { 0 }; + while (rbuffer_space(input_buffer) >= 19 && ptr < end) { + // A "<x>" form occupies at least 1 characters, and produces up + // to 19 characters (1 + 5 * 3 for the char and 3 for a modifier). + // In the case of K_SPECIAL(0x80) or CSI(0x9B), 3 bytes are escaped and + // needed, but since the keys are UTF-8, so the first byte cannot be + // K_SPECIAL(0x80) or CSI(0x9B). + uint8_t buf[19] = { 0 }; unsigned int new_size = trans_special((const uint8_t **)&ptr, (size_t)(end - ptr), buf, true, false); diff --git a/src/nvim/os/os_win_console.c b/src/nvim/os/os_win_console.c index 8a0aa2f5ae..18dcfeafa0 100644 --- a/src/nvim/os/os_win_console.c +++ b/src/nvim/os/os_win_console.c @@ -2,8 +2,14 @@ // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com #include "nvim/vim.h" +#include "nvim/os/input.h" #include "nvim/os/os_win_console.h" +#ifdef INCLUDE_GENERATED_DECLARATIONS +# include "os/os_win_console.c.generated.h" +#endif + + int os_get_conin_fd(void) { const HANDLE conin_handle = CreateFile("CONIN$", @@ -40,3 +46,29 @@ void os_replace_stdout_and_stderr_to_conout(void) const int conerr_fd = _open_osfhandle((intptr_t)conout_handle, 0); assert(conerr_fd == STDERR_FILENO); } + +void os_set_vtp(bool enable) +{ + static TriState is_legacy = kNone; + if (is_legacy == kNone) { + uv_tty_vtermstate_t state; + uv_tty_get_vterm_state(&state); + is_legacy = (state == UV_TTY_UNSUPPORTED) ? kTrue : kFalse; + } + if (!is_legacy && !os_has_vti()) { + uv_tty_set_vterm_state(enable ? UV_TTY_SUPPORTED : UV_TTY_UNSUPPORTED); + } +} + +static bool os_has_vti(void) +{ + static TriState has_vti = kNone; + if (has_vti == kNone) { + HANDLE handle = (HANDLE)_get_osfhandle(input_global_fd()); + DWORD dwMode; + if (handle != INVALID_HANDLE_VALUE && GetConsoleMode(handle, &dwMode)) { + has_vti = !!(dwMode & ENABLE_VIRTUAL_TERMINAL_INPUT) ? kTrue : kFalse; + } + } + return has_vti == kTrue; +} diff --git a/src/nvim/os/os_win_console.h b/src/nvim/os/os_win_console.h index 154ec83d8a..7b5800afa8 100644 --- a/src/nvim/os/os_win_console.h +++ b/src/nvim/os/os_win_console.h @@ -5,4 +5,8 @@ # include "os/os_win_console.h.generated.h" #endif +#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT +# define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200 +#endif + #endif // NVIM_OS_OS_WIN_CONSOLE_H diff --git a/src/nvim/os/signal.c b/src/nvim/os/signal.c index 112de9fed8..bfe230b521 100644 --- a/src/nvim/os/signal.c +++ b/src/nvim/os/signal.c @@ -157,6 +157,7 @@ static void deadly_signal(int signum) { // Set the v:dying variable. set_vim_var_nr(VV_DYING, 1); + v_dying = 1; WLOG("got signal %d (%s)", signum, signal_name(signum)); diff --git a/src/nvim/os/tty.c b/src/nvim/os/tty.c index 4f525bed9a..c80ef99084 100644 --- a/src/nvim/os/tty.c +++ b/src/nvim/os/tty.c @@ -28,7 +28,7 @@ void os_tty_guess_term(const char **term, int out_fd) if (winpty) { // Force TERM=win32con when running in winpty. *term = "win32con"; - uv_set_vterm_state(UV_UNSUPPORTED); + uv_tty_set_vterm_state(UV_TTY_UNSUPPORTED); return; } @@ -55,7 +55,7 @@ void os_tty_guess_term(const char **term, int out_fd) } if (conemu_ansi) { - uv_set_vterm_state(UV_SUPPORTED); + uv_tty_set_vterm_state(UV_TTY_SUPPORTED); } } #endif diff --git a/src/nvim/popupmnu.c b/src/nvim/popupmnu.c index e06433892d..c712762bdf 100644 --- a/src/nvim/popupmnu.c +++ b/src/nvim/popupmnu.c @@ -923,6 +923,6 @@ void pum_set_event_info(dict_T *dict) tv_dict_add_float(dict, S_LEN("row"), r); tv_dict_add_float(dict, S_LEN("col"), c); tv_dict_add_nr(dict, S_LEN("size"), pum_size); - tv_dict_add_special(dict, S_LEN("scrollbar"), - pum_scrollbar ? kSpecialVarTrue : kSpecialVarFalse); + tv_dict_add_bool(dict, S_LEN("scrollbar"), + pum_scrollbar ? kBoolVarTrue : kBoolVarFalse); } diff --git a/src/nvim/regexp_nfa.c b/src/nvim/regexp_nfa.c index f33c61d39f..2ca5f42e51 100644 --- a/src/nvim/regexp_nfa.c +++ b/src/nvim/regexp_nfa.c @@ -249,6 +249,7 @@ static char_u e_nul_found[] = N_( static char_u e_misplaced[] = N_("E866: (NFA regexp) Misplaced %c"); static char_u e_ill_char_class[] = N_( "E877: (NFA regexp) Invalid character class: %" PRId64); +static char_u e_value_too_large[] = N_("E951: \\% value too large"); /* Since the out pointers in the list are always * uninitialized, we use the pointers themselves @@ -1499,7 +1500,8 @@ static int nfa_regatom(void) c = getchr(); while (ascii_isdigit(c)) { if (n > (INT32_MAX - (c - '0')) / 10) { - EMSG(_("E951: \\% value too large")); + // overflow. + EMSG(_(e_value_too_large)); return FAIL; } n = n * 10 + (c - '0'); @@ -1526,7 +1528,7 @@ static int nfa_regatom(void) limit = INT32_MAX / MB_MAXBYTES; } if (n >= limit) { - EMSG(_("E951: \\% value too large")); + EMSG(_(e_value_too_large)); return FAIL; } EMIT((int)n); diff --git a/src/nvim/screen.c b/src/nvim/screen.c index 9e958663aa..ba52f5b489 100644 --- a/src/nvim/screen.c +++ b/src/nvim/screen.c @@ -2994,6 +2994,12 @@ win_line ( c_final = NUL; n_extra = get_breakindent_win(wp, ml_get_buf(wp->w_buffer, lnum, false)); + if (row == startrow) { + n_extra -= win_col_off2(wp); + if (n_extra < 0) { + n_extra = 0; + } + } if (wp->w_skipcol > 0 && wp->w_p_wrap && wp->w_briopt_sbr) { need_showbreak = false; } diff --git a/src/nvim/syntax.c b/src/nvim/syntax.c index ef4dfb3caa..f3b05c3961 100644 --- a/src/nvim/syntax.c +++ b/src/nvim/syntax.c @@ -7444,6 +7444,8 @@ static int syn_add_group(char_u *name) return 0; } + char_u *const name_up = vim_strsave_up(name); + // Append another syntax_highlight entry. struct hl_group* hlgp = GA_APPEND_VIA_PTR(struct hl_group, &highlight_ga); memset(hlgp, 0, sizeof(*hlgp)); @@ -7452,7 +7454,7 @@ static int syn_add_group(char_u *name) hlgp->sg_rgb_fg = -1; hlgp->sg_rgb_sp = -1; hlgp->sg_blend = -1; - hlgp->sg_name_u = vim_strsave_up(name); + hlgp->sg_name_u = name_up; return highlight_ga.ga_len; /* ID is index plus one */ } diff --git a/src/nvim/testdir/runtest.vim b/src/nvim/testdir/runtest.vim index adf7463936..e249d499c4 100644 --- a/src/nvim/testdir/runtest.vim +++ b/src/nvim/testdir/runtest.vim @@ -44,6 +44,10 @@ if &lines < 24 || &columns < 80 qa! endif +if has('reltime') + let s:start_time = reltime() +endif + " Common with all tests on all systems. source setup.vim @@ -100,6 +104,9 @@ endfunc func RunTheTest(test) echo 'Executing ' . a:test + if has('reltime') + let func_start = reltime() + endif " Avoid stopping at the "hit enter" prompt set nomore @@ -124,7 +131,11 @@ func RunTheTest(test) endtry endif - call add(s:messages, 'Executing ' . a:test) + let message = 'Executed ' . a:test + if has('reltime') + let message ..= ' in ' .. reltimestr(reltime(func_start)) .. ' seconds' + endif + call add(s:messages, message) let s:done += 1 if a:test =~ 'Test_nocatch_' @@ -230,6 +241,9 @@ func FinishTesting() else let message = 'Executed ' . s:done . (s:done > 1 ? ' tests' : ' test') endif + if has('reltime') + let message ..= ' in ' .. reltimestr(reltime(s:start_time)) .. ' seconds' + endif echo message call add(s:messages, message) if s:fail > 0 diff --git a/src/nvim/testdir/test_autocmd.vim b/src/nvim/testdir/test_autocmd.vim index 954e5d875f..5217aa7339 100644 --- a/src/nvim/testdir/test_autocmd.vim +++ b/src/nvim/testdir/test_autocmd.vim @@ -1246,23 +1246,23 @@ func Test_TextYankPost() norm "ayiw call assert_equal( - \{'regcontents': ['foo'], 'inclusive': v:true, 'regname': 'a', 'operator': 'y', 'regtype': 'v'}, + \{'regcontents': ['foo'], 'inclusive': v:true, 'regname': 'a', 'operator': 'y', 'visual': v:false, 'regtype': 'v'}, \g:event) norm y_ call assert_equal( - \{'regcontents': ['foo'], 'inclusive': v:false, 'regname': '', 'operator': 'y', 'regtype': 'V'}, + \{'regcontents': ['foo'], 'inclusive': v:false, 'regname': '', 'operator': 'y', 'visual': v:false, 'regtype': 'V'}, \g:event) call feedkeys("\<C-V>y", 'x') call assert_equal( - \{'regcontents': ['f'], 'inclusive': v:true, 'regname': '', 'operator': 'y', 'regtype': "\x161"}, + \{'regcontents': ['f'], 'inclusive': v:true, 'regname': '', 'operator': 'y', 'visual': v:true, 'regtype': "\x161"}, \g:event) norm "xciwbar call assert_equal( - \{'regcontents': ['foo'], 'inclusive': v:true, 'regname': 'x', 'operator': 'c', 'regtype': 'v'}, + \{'regcontents': ['foo'], 'inclusive': v:true, 'regname': 'x', 'operator': 'c', 'visual': v:false, 'regtype': 'v'}, \g:event) norm "bdiw call assert_equal( - \{'regcontents': ['bar'], 'inclusive': v:true, 'regname': 'b', 'operator': 'd', 'regtype': 'v'}, + \{'regcontents': ['bar'], 'inclusive': v:true, 'regname': 'b', 'operator': 'd', 'visual': v:false, 'regtype': 'v'}, \g:event) call assert_equal({}, v:event) diff --git a/src/nvim/testdir/test_breakindent.vim b/src/nvim/testdir/test_breakindent.vim index 5675bf74dd..a4c1f62a43 100644 --- a/src/nvim/testdir/test_breakindent.vim +++ b/src/nvim/testdir/test_breakindent.vim @@ -373,3 +373,52 @@ func Test_breakindent19_sbr_nextpage() call s:compare_lines(expect, lines) call s:close_windows('set breakindent& briopt& sbr&') endfunc + +func Test_breakindent20_cpo_n_nextpage() + let s:input = "" + call s:test_windows('setl breakindent briopt=min:14 cpo+=n number') + call setline(1, repeat('a', 200)) + norm! 1gg + redraw! + let lines = s:screen_lines(1, 20) + let expect = [ + \ " 1 aaaaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaaaa", + \ ] + call s:compare_lines(expect, lines) + " Scroll down one screen line + setl scrolloff=5 + norm! 5gj + redraw! + let lines = s:screen_lines(1, 20) + let expect = [ + \ "--1 aaaaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaaaa", + \ ] + call s:compare_lines(expect, lines) + + setl briopt+=shift:2 + norm! 1gg + let lines = s:screen_lines(1, 20) + let expect = [ + \ " 1 aaaaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaa", + \ ] + call s:compare_lines(expect, lines) + " Scroll down one screen line + norm! 5gj + let lines = s:screen_lines(1, 20) + let expect = [ + \ "--1 aaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaa", + \ " aaaaaaaaaaaaaa", + \ ] + call s:compare_lines(expect, lines) + + call s:close_windows('set breakindent& briopt& cpo& number&') +endfunc + +" vim: shiftwidth=2 sts=2 expandtab diff --git a/src/nvim/testdir/test_cmdline.vim b/src/nvim/testdir/test_cmdline.vim index 2c7d64f078..f8d84f1a49 100644 --- a/src/nvim/testdir/test_cmdline.vim +++ b/src/nvim/testdir/test_cmdline.vim @@ -692,6 +692,22 @@ func Test_getcmdwin_autocmd() augroup END endfunc +" Test error: "E135: *Filter* Autocommands must not change current buffer" +func Test_cmd_bang_E135() + new + call setline(1, ['a', 'b', 'c', 'd']) + augroup test_cmd_filter_E135 + au! + autocmd FilterReadPost * help + augroup END + call assert_fails('2,3!echo "x"', 'E135:') + + augroup test_cmd_filter_E135 + au! + augroup END + %bwipe! +endfunc + func Test_verbosefile() set verbosefile=Xlog echomsg 'foo' diff --git a/src/nvim/testdir/test_cursor_func.vim b/src/nvim/testdir/test_cursor_func.vim index e8e561dfd8..2e190911b2 100644 --- a/src/nvim/testdir/test_cursor_func.vim +++ b/src/nvim/testdir/test_cursor_func.vim @@ -93,3 +93,18 @@ func Test_screenpos() close bwipe! endfunc + +func Test_screenpos_number() + rightbelow new + rightbelow 73vsplit + call setline (1, repeat('x', 66)) + setlocal number + redraw + let winid = win_getid() + let [winrow, wincol] = win_screenpos(winid) + let pos = screenpos(winid, 1, 66) + call assert_equal(winrow, pos.row) + call assert_equal(wincol + 66 + 3, pos.col) + close + bwipe! +endfunc diff --git a/src/nvim/testdir/test_filetype.vim b/src/nvim/testdir/test_filetype.vim index ace56a375f..ffd2cee80f 100644 --- a/src/nvim/testdir/test_filetype.vim +++ b/src/nvim/testdir/test_filetype.vim @@ -151,6 +151,7 @@ let s:filename_checks = { \ 'ecd': ['file.ecd'], \ 'edif': ['file.edf', 'file.edif', 'file.edo'], \ 'elinks': ['/etc/elinks.conf', '/.elinks/elinks.conf'], + \ 'elm': ['file.elm'], \ 'elmfilt': ['filter-rules'], \ 'erlang': ['file.erl', 'file.hrl', 'file.yaws'], \ 'eruby': ['file.erb', 'file.rhtml'], @@ -251,7 +252,7 @@ let s:filename_checks = { \ 'lilo': ['lilo.conf'], \ 'limits': ['/etc/limits', '/etc/anylimits.conf', '/etc/anylimits.d/file.conf'], \ 'liquid': ['file.liquid'], - \ 'lisp': ['sbclrc', '.sbclrc'], + \ 'lisp': ['file.lsp', 'file.lisp', 'file.el', 'file.cl', '.emacs', '.sawfishrc', 'sbclrc', '.sbclrc'], \ 'lite': ['file.lite', 'file.lt'], \ 'litestep': ['/LiteStep/any/file.rc'], \ 'loginaccess': ['/etc/login.access'], diff --git a/src/nvim/testdir/test_ga.vim b/src/nvim/testdir/test_ga.vim index ea3d211aeb..87f1382342 100644 --- a/src/nvim/testdir/test_ga.vim +++ b/src/nvim/testdir/test_ga.vim @@ -24,6 +24,7 @@ func Test_ga_command() " Test a few multi-bytes characters. call assert_equal("\n<é> 233, Hex 00e9, Oct 351, Digr e'", Do_ga('é')) call assert_equal("\n<ẻ> 7867, Hex 1ebb, Oct 17273, Digr e2", Do_ga('ẻ')) + call assert_equal("\n<\U00012345> 74565, Hex 00012345, Octal 221505", Do_ga("\U00012345")) " Test with combining characters. call assert_equal("\n<e> 101, Hex 65, Octal 145 < ́> 769, Hex 0301, Octal 1401", Do_ga("e\u0301")) diff --git a/src/nvim/testdir/test_maparg.vim b/src/nvim/testdir/test_maparg.vim index 5f73bd80ad..238d2f900d 100644 --- a/src/nvim/testdir/test_maparg.vim +++ b/src/nvim/testdir/test_maparg.vim @@ -15,23 +15,23 @@ function Test_maparg() map foo<C-V> is<F4>foo vnoremap <script> <buffer> <expr> <silent> bar isbar call assert_equal("is<F4>foo", maparg('foo<C-V>')) - call assert_equal({'silent': 0, 'noremap': 0, 'lhs': 'foo<C-V>', + call assert_equal({'silent': 0, 'noremap': 0, 'script': 0, 'lhs': 'foo<C-V>', \ 'mode': ' ', 'nowait': 0, 'expr': 0, 'sid': sid, 'lnum': lnum + 1, \ 'rhs': 'is<F4>foo', 'buffer': 0}, \ maparg('foo<C-V>', '', 0, 1)) - call assert_equal({'silent': 1, 'noremap': 1, 'lhs': 'bar', 'mode': 'v', + call assert_equal({'silent': 1, 'noremap': 1, 'script': 1, 'lhs': 'bar', 'mode': 'v', \ 'nowait': 0, 'expr': 1, 'sid': sid, 'lnum': lnum + 2, \ 'rhs': 'isbar', 'buffer': 1}, \ maparg('bar', '', 0, 1)) let lnum = expand('<sflnum>') map <buffer> <nowait> foo bar - call assert_equal({'silent': 0, 'noremap': 0, 'lhs': 'foo', 'mode': ' ', + call assert_equal({'silent': 0, 'noremap': 0, 'script': 0, 'lhs': 'foo', 'mode': ' ', \ 'nowait': 1, 'expr': 0, 'sid': sid, 'lnum': lnum + 1, 'rhs': 'bar', \ 'buffer': 1}, \ maparg('foo', '', 0, 1)) let lnum = expand('<sflnum>') tmap baz foo - call assert_equal({'silent': 0, 'noremap': 0, 'lhs': 'baz', 'mode': 't', + call assert_equal({'silent': 0, 'noremap': 0, 'script': 0, 'lhs': 'baz', 'mode': 't', \ 'nowait': 0, 'expr': 0, 'sid': sid, 'lnum': lnum + 1, 'rhs': 'foo', \ 'buffer': 0}, \ maparg('baz', 't', 0, 1)) diff --git a/src/nvim/testdir/test_match.vim b/src/nvim/testdir/test_match.vim index 7a1894bc16..09448ca71b 100644 --- a/src/nvim/testdir/test_match.vim +++ b/src/nvim/testdir/test_match.vim @@ -149,6 +149,21 @@ function Test_match() highlight MyGroup3 NONE endfunc +func Test_match_error() + call assert_fails('match Error', 'E475:') + call assert_fails('match Error /', 'E475:') + call assert_fails('4match Error /x/', 'E476:') + call assert_fails('match Error /x/ x', 'E488:') +endfunc + +func Test_matchadd_error() + call assert_fails("call matchadd('GroupDoesNotExist', 'X')", 'E28:') + call assert_fails("call matchadd('Search', '\\(')", 'E475:') + call assert_fails("call matchadd('Search', 'XXX', 1, 123, 1)", 'E715:') + call assert_fails("call matchadd('Error', 'XXX', 1, 3)", 'E798:') + call assert_fails("call matchadd('Error', 'XXX', 1, 0)", 'E799:') +endfunc + func Test_matchaddpos() syntax on set hlsearch @@ -263,6 +278,17 @@ func Test_matchaddpos_using_negative_priority() set hlsearch& endfunc +func Test_matchaddpos_error() + call assert_fails("call matchaddpos('Error', 1)", 'E686:') + call assert_fails("call matchaddpos('Error', [1], 1, 1)", 'E798:') + call assert_fails("call matchaddpos('Error', [1], 1, 2)", 'E798:') + call assert_fails("call matchaddpos('Error', [1], 1, 0)", 'E799:') + call assert_fails("call matchaddpos('Error', [1], 1, 123, 1)", 'E715:') + call assert_fails("call matchaddpos('Error', [1], 1, 5, {'window':12345})", 'E957:') + " Why doesn't the following error have an error code E...? + call assert_fails("call matchaddpos('Error', [{}])", 'E5031:') +endfunc + func OtherWindowCommon() let lines =<< trim END call setline(1, 'Hello Vim world') @@ -288,6 +314,11 @@ func Test_matchdelete_other_window() call delete('XscriptMatchCommon') endfunc +func Test_matchdelete_error() + call assert_fails("call matchdelete(0)", 'E802:') + call assert_fails("call matchdelete(1, -1)", 'E957:') +endfunc + func Test_matchclear_other_window() if !CanRunVimInTerminal() throw 'Skipped: cannot make screendumps' diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim index 41f1710faf..04a5c62f66 100644 --- a/src/nvim/testdir/test_options.vim +++ b/src/nvim/testdir/test_options.vim @@ -42,6 +42,13 @@ function Test_wildchar() set wildchar& endfunction +func Test_wildoptions() + set wildoptions= + set wildoptions+=tagfile + set wildoptions+=tagfile + call assert_equal('tagfile', &wildoptions) +endfunc + function! Test_options() let caught = 'ok' try @@ -554,3 +561,18 @@ func Test_visualbell() set novisualbell set belloff=all endfunc + +" Test for setting option values using v:false and v:true +func Test_opt_boolean() + set number& + set number + call assert_equal(1, &nu) + set nonu + call assert_equal(0, &nu) + let &nu = v:true + call assert_equal(1, &nu) + let &nu = v:false + call assert_equal(0, &nu) + set number& +endfunc + diff --git a/src/nvim/testdir/test_startup.vim b/src/nvim/testdir/test_startup.vim index f03c493275..9abaca5957 100644 --- a/src/nvim/testdir/test_startup.vim +++ b/src/nvim/testdir/test_startup.vim @@ -584,3 +584,12 @@ func Test_start_with_tabs() " clean up call StopVimInTerminal(buf) endfunc + +func Test_v_argv() + let out = system(GetVimCommand() . ' -es -V1 -X arg1 --cmd "echo v:argv" --cmd q') + let list = split(out, "', '") + call assert_match('vim', list[0]) + let idx = index(list, 'arg1') + call assert_true(idx > 2) + call assert_equal(['arg1', '--cmd', 'echo v:argv', '--cmd', 'q'']'], list[idx:]) +endfunc diff --git a/src/nvim/tui/tui.c b/src/nvim/tui/tui.c index 2c4d02812b..b4d91a01fc 100644 --- a/src/nvim/tui/tui.c +++ b/src/nvim/tui/tui.c @@ -33,6 +33,9 @@ #include "nvim/os/os.h" #include "nvim/os/signal.h" #include "nvim/os/tty.h" +#ifdef WIN32 +# include "nvim/os/os_win_console.h" +#endif #include "nvim/strings.h" #include "nvim/syntax.h" #include "nvim/ui_bridge.h" @@ -1015,8 +1018,22 @@ static void tui_mouse_on(UI *ui) { TUIData *data = ui->data; if (!data->mouse_enabled) { +#ifdef WIN32 + // Windows versions with vtp(ENABLE_VIRTUAL_TERMINAL_PROCESSING) and + // no vti(ENABLE_VIRTUAL_TERMINAL_INPUT) will need to use mouse traking of + // libuv. For this reason, vtp (vterm) state of libuv is temporarily + // disabled because the control sequence needs to be processed by libuv + // instead of Windows vtp. + // ref. https://docs.microsoft.com/en-us/windows/console/setconsolemode + flush_buf(ui); + os_set_vtp(false); +#endif unibi_out_ext(ui, data->unibi_ext.enable_mouse); data->mouse_enabled = true; +#ifdef WIN32 + flush_buf(ui); + os_set_vtp(true); +#endif } } @@ -1024,8 +1041,22 @@ static void tui_mouse_off(UI *ui) { TUIData *data = ui->data; if (data->mouse_enabled) { +#ifdef WIN32 + // Windows versions with vtp(ENABLE_VIRTUAL_TERMINAL_PROCESSING) and + // no vti(ENABLE_VIRTUAL_TERMINAL_INPUT) will need to use mouse traking of + // libuv. For this reason, vtp (vterm) state of libuv is temporarily + // disabled because the control sequence needs to be processed by libuv + // instead of Windows vtp. + // ref. https://docs.microsoft.com/en-us/windows/console/setconsolemode + flush_buf(ui); + os_set_vtp(false); +#endif unibi_out_ext(ui, data->unibi_ext.disable_mouse); data->mouse_enabled = false; +#ifdef WIN32 + flush_buf(ui); + os_set_vtp(true); +#endif } } diff --git a/src/nvim/vim.h b/src/nvim/vim.h index c1eea1ab90..832703e83d 100644 --- a/src/nvim/vim.h +++ b/src/nvim/vim.h @@ -101,6 +101,7 @@ typedef enum { #define VAR_TYPE_DICT 4 #define VAR_TYPE_FLOAT 5 #define VAR_TYPE_BOOL 6 +#define VAR_TYPE_SPECIAL 7 // values for xp_context when doing command line completion diff --git a/src/tree_sitter/README.md b/src/tree_sitter/README.md new file mode 100644 index 0000000000..20cb35e7c3 --- /dev/null +++ b/src/tree_sitter/README.md @@ -0,0 +1,16 @@ +Tree-sitter vendor runtime +========================== + +This is the vendor runtime code for treesitter. + +The original code can be found [here](https://github.com/tree-sitter/tree-sitter). + +As this code is not ours, if you find any bugs, feel free to open an issue, so that we can +investigate and determine if this should go upstream. + +# Updating + +To update the treesitter runtime, use the `update-ts-runtime.sh` script in the `scripts` directory: +```sh +./scripts/update-ts-runtime.sh <commit you want to update to> +``` diff --git a/src/tree_sitter/alloc.h b/src/tree_sitter/alloc.h index 2229995bd1..d3c6b5eca8 100644 --- a/src/tree_sitter/alloc.h +++ b/src/tree_sitter/alloc.h @@ -51,6 +51,7 @@ static inline void ts_free(void *buffer) { #include <stdlib.h> static inline bool ts_toggle_allocation_recording(bool value) { + (void)value; return false; } diff --git a/src/tree_sitter/language.c b/src/tree_sitter/language.c index a396b4b0b6..c00c49e3c0 100644 --- a/src/tree_sitter/language.c +++ b/src/tree_sitter/language.c @@ -33,8 +33,8 @@ void ts_language_table_entry( assert(symbol < self->token_count); uint32_t action_index = ts_language_lookup(self, state, symbol); const TSParseActionEntry *entry = &self->parse_actions[action_index]; - result->action_count = entry->count; - result->is_reusable = entry->reusable; + result->action_count = entry->entry.count; + result->is_reusable = entry->entry.reusable; result->actions = (const TSParseAction *)(entry + 1); } } diff --git a/src/tree_sitter/language.h b/src/tree_sitter/language.h index f908b4593a..341f0f85af 100644 --- a/src/tree_sitter/language.h +++ b/src/tree_sitter/language.h @@ -93,7 +93,7 @@ static inline TSStateId ts_language_next_state(const TSLanguage *self, if (count > 0) { TSParseAction action = actions[count - 1]; if (action.type == TSParseActionTypeShift) { - return action.params.extra ? state : action.params.state; + return action.params.shift.extra ? state : action.params.shift.state; } } return 0; diff --git a/src/tree_sitter/parser.c b/src/tree_sitter/parser.c index d4b227308b..dd222cd3c4 100644 --- a/src/tree_sitter/parser.c +++ b/src/tree_sitter/parser.c @@ -101,9 +101,10 @@ typedef struct { static const char *ts_string_input_read( void *_self, uint32_t byte, - TSPoint _, + TSPoint pt, uint32_t *length ) { + (void)pt; TSStringInput *self = (TSStringInput *)_self; if (byte >= self->length) { *length = 0; @@ -210,6 +211,7 @@ static ErrorComparison ts_parser__compare_versions( ErrorStatus a, ErrorStatus b ) { + (void)self; if (!a.is_in_error && b.is_in_error) { if (a.cost < b.cost) { return ErrorComparisonTakeLeft; @@ -951,15 +953,15 @@ static bool ts_parser__do_all_potential_reductions( switch (action.type) { case TSParseActionTypeShift: case TSParseActionTypeRecover: - if (!action.params.extra && !action.params.repetition) has_shift_action = true; + if (!action.params.shift.extra && !action.params.shift.repetition) has_shift_action = true; break; case TSParseActionTypeReduce: - if (action.params.child_count > 0) + if (action.params.reduce.child_count > 0) ts_reduce_action_set_add(&self->reduce_actions, (ReduceAction){ - .symbol = action.params.symbol, - .count = action.params.child_count, - .dynamic_precedence = action.params.dynamic_precedence, - .production_id = action.params.production_id, + .symbol = action.params.reduce.symbol, + .count = action.params.reduce.child_count, + .dynamic_precedence = action.params.reduce.dynamic_precedence, + .production_id = action.params.reduce.production_id, }); default: break; @@ -1250,7 +1252,7 @@ static void ts_parser__recover( // be counted in error cost calculations. unsigned n; const TSParseAction *actions = ts_language_actions(self->language, 1, ts_subtree_symbol(lookahead), &n); - if (n > 0 && actions[n - 1].type == TSParseActionTypeShift && actions[n - 1].params.extra) { + if (n > 0 && actions[n - 1].type == TSParseActionTypeShift && actions[n - 1].params.shift.extra) { MutableSubtree mutable_lookahead = ts_subtree_make_mut(&self->tree_pool, lookahead); ts_subtree_set_extra(&mutable_lookahead); lookahead = ts_subtree_from_mut(mutable_lookahead); @@ -1379,9 +1381,9 @@ static bool ts_parser__advance( switch (action.type) { case TSParseActionTypeShift: { - if (action.params.repetition) break; + if (action.params.shift.repetition) break; TSStateId next_state; - if (action.params.extra) { + if (action.params.shift.extra) { // TODO: remove when TREE_SITTER_LANGUAGE_VERSION 9 is out. if (state == ERROR_STATE) continue; @@ -1389,7 +1391,7 @@ static bool ts_parser__advance( next_state = state; LOG("shift_extra"); } else { - next_state = action.params.state; + next_state = action.params.shift.state; LOG("shift state:%u", next_state); } @@ -1398,7 +1400,7 @@ static bool ts_parser__advance( next_state = ts_language_next_state(self->language, state, ts_subtree_symbol(lookahead)); } - ts_parser__shift(self, version, next_state, lookahead, action.params.extra); + ts_parser__shift(self, version, next_state, lookahead, action.params.shift.extra); if (did_reuse) reusable_node_advance(&self->reusable_node); return true; } @@ -1406,10 +1408,10 @@ static bool ts_parser__advance( case TSParseActionTypeReduce: { bool is_fragile = table_entry.action_count > 1; bool is_extra = lookahead.ptr == NULL; - LOG("reduce sym:%s, child_count:%u", SYM_NAME(action.params.symbol), action.params.child_count); + LOG("reduce sym:%s, child_count:%u", SYM_NAME(action.params.reduce.symbol), action.params.reduce.child_count); StackVersion reduction_version = ts_parser__reduce( - self, version, action.params.symbol, action.params.child_count, - action.params.dynamic_precedence, action.params.production_id, + self, version, action.params.reduce.symbol, action.params.reduce.child_count, + action.params.reduce.dynamic_precedence, action.params.reduce.production_id, is_fragile, is_extra ); if (reduction_version != STACK_VERSION_NONE) { diff --git a/src/tree_sitter/parser.h b/src/tree_sitter/parser.h index 9df91f8c3c..11bf4fc42a 100644 --- a/src/tree_sitter/parser.h +++ b/src/tree_sitter/parser.h @@ -62,13 +62,13 @@ typedef struct { TSStateId state; bool extra : 1; bool repetition : 1; - }; + } shift; struct { TSSymbol symbol; int16_t dynamic_precedence; uint8_t child_count; uint8_t production_id; - }; + } reduce; } params; TSParseActionType type : 4; } TSParseAction; @@ -83,7 +83,7 @@ typedef union { struct { uint8_t count; bool reusable : 1; - }; + } entry; } TSParseActionEntry; struct TSLanguage { @@ -167,22 +167,28 @@ struct TSLanguage { #define ACTIONS(id) id -#define SHIFT(state_value) \ - { \ - { \ - .type = TSParseActionTypeShift, \ - .params = {.state = state_value}, \ - } \ +#define SHIFT(state_value) \ + { \ + { \ + .params = { \ + .shift = { \ + .state = state_value \ + } \ + }, \ + .type = TSParseActionTypeShift \ + } \ } #define SHIFT_REPEAT(state_value) \ { \ { \ - .type = TSParseActionTypeShift, \ .params = { \ - .state = state_value, \ - .repetition = true \ + .shift = { \ + .state = state_value, \ + .repetition = true \ + } \ }, \ + .type = TSParseActionTypeShift \ } \ } @@ -194,20 +200,26 @@ struct TSLanguage { #define SHIFT_EXTRA() \ { \ { \ - .type = TSParseActionTypeShift, \ - .params = {.extra = true} \ + .params = { \ + .shift = { \ + .extra = true \ + } \ + }, \ + .type = TSParseActionTypeShift \ } \ } #define REDUCE(symbol_val, child_count_val, ...) \ { \ { \ - .type = TSParseActionTypeReduce, \ .params = { \ - .symbol = symbol_val, \ - .child_count = child_count_val, \ - __VA_ARGS__ \ - } \ + .reduce = { \ + .symbol = symbol_val, \ + .child_count = child_count_val, \ + __VA_ARGS__ \ + }, \ + }, \ + .type = TSParseActionTypeReduce \ } \ } diff --git a/src/tree_sitter/query.c b/src/tree_sitter/query.c index 92a8006179..59902dee3b 100644 --- a/src/tree_sitter/query.c +++ b/src/tree_sitter/query.c @@ -8,6 +8,13 @@ #include "./unicode.h" #include <wctype.h> +// #define LOG(...) fprintf(stderr, __VA_ARGS__) +#define LOG(...) + +#define MAX_STATE_COUNT 256 +#define MAX_CAPTURE_LIST_COUNT 32 +#define MAX_STEP_CAPTURE_COUNT 3 + /* * Stream - A sequence of unicode characters derived from a UTF8 string. * This struct is used in parsing queries from S-expressions. @@ -19,37 +26,35 @@ typedef struct { uint8_t next_size; } Stream; -#define MAX_STEP_CAPTURE_COUNT 4 - /* * QueryStep - A step in the process of matching a query. Each node within * a query S-expression maps to one of these steps. An entire pattern is * represented as a sequence of these steps. Fields: * * - `symbol` - The grammar symbol to match. A zero value represents the - * wildcard symbol, '*'. + * wildcard symbol, '_'. * - `field` - The field name to match. A zero value means that a field name * was not specified. - * - `capture_id` - An integer representing the name of the capture associated - * with this node in the pattern. A `NONE` value means this node is not - * captured in this pattern. + * - `capture_ids` - An array of integers representing the names of captures + * associated with this node in the pattern, terminated by a `NONE` value. * - `depth` - The depth where this node occurs in the pattern. The root node * of the pattern has depth zero. - * - `repeat_step_index` - If this step is part of a repetition, the index of - * the beginning of the repetition. A `NONE` value means this step is not - * part of a repetition. + * - `alternative_index` - The index of a different query step that serves as + * an alternative to this step. */ typedef struct { TSSymbol symbol; TSFieldId field; uint16_t capture_ids[MAX_STEP_CAPTURE_COUNT]; - uint16_t repeat_step_index; - uint16_t depth: 11; + uint16_t alternative_index; + uint16_t depth; bool contains_captures: 1; bool is_pattern_start: 1; bool is_immediate: 1; - bool is_last: 1; - bool is_repeated: 1; + bool is_last_child: 1; + bool is_pass_through: 1; + bool is_dead_end: 1; + bool alternative_is_immediate: 1; } QueryStep; /* @@ -88,18 +93,35 @@ typedef struct { * QueryState - The state of an in-progress match of a particular pattern * in a query. While executing, a `TSQueryCursor` must keep track of a number * of possible in-progress matches. Each of those possible matches is - * represented as one of these states. + * represented as one of these states. Fields: + * - `id` - A numeric id that is exposed to the public API. This allows the + * caller to remove a given match, preventing any more of its captures + * from being returned. + * - `start_depth` - The depth in the tree where the first step of the state's + * pattern was matched. + * - `pattern_index` - The pattern that the state is matching. + * - `consumed_capture_count` - The number of captures from this match that + * have already been returned. + * - `capture_list_id` - A numeric id that can be used to retrieve the state's + * list of captures from the `CaptureListPool`. + * - `seeking_immediate_match` - A flag that indicates that the state's next + * step must be matched by the very next sibling. This is used when + * processing repetitions. + * - `has_in_progress_alternatives` - A flag that indicates that there is are + * other states that have the same captures as this state, but are at + * different steps in their pattern. This means that in order to obey the + * 'longest-match' rule, this state should not be returned as a match until + * it is clear that there can be no longer match. */ typedef struct { uint32_t id; uint16_t start_depth; - uint16_t pattern_index; uint16_t step_index; - uint16_t consumed_capture_count; - uint16_t repeat_match_count; - uint16_t step_index_on_failure; - uint8_t capture_list_id; - bool seeking_non_match; + uint16_t pattern_index; + uint16_t capture_list_id; + uint16_t consumed_capture_count: 14; + bool seeking_immediate_match: 1; + bool has_in_progress_alternatives: 1; } QueryState; typedef Array(TSQueryCapture) CaptureList; @@ -111,7 +133,8 @@ typedef Array(TSQueryCapture) CaptureList; * are currently in use. */ typedef struct { - CaptureList list[32]; + CaptureList list[MAX_CAPTURE_LIST_COUNT]; + CaptureList empty_list; uint32_t usage_map; } CaptureListPool; @@ -152,14 +175,10 @@ struct TSQueryCursor { }; static const TSQueryError PARENT_DONE = -1; -static const uint8_t PATTERN_DONE_MARKER = UINT8_MAX; +static const uint16_t PATTERN_DONE_MARKER = UINT16_MAX; static const uint16_t NONE = UINT16_MAX; static const TSSymbol WILDCARD_SYMBOL = 0; static const TSSymbol NAMED_WILDCARD_SYMBOL = UINT16_MAX - 1; -static const uint16_t MAX_STATE_COUNT = 32; - -// #define LOG(...) fprintf(stderr, __VA_ARGS__) -#define LOG(...) /********** * Stream @@ -242,24 +261,31 @@ static void stream_scan_identifier(Stream *stream) { static CaptureListPool capture_list_pool_new(void) { return (CaptureListPool) { + .empty_list = array_new(), .usage_map = UINT32_MAX, }; } static void capture_list_pool_reset(CaptureListPool *self) { self->usage_map = UINT32_MAX; - for (unsigned i = 0; i < 32; i++) { + for (unsigned i = 0; i < MAX_CAPTURE_LIST_COUNT; i++) { array_clear(&self->list[i]); } } static void capture_list_pool_delete(CaptureListPool *self) { - for (unsigned i = 0; i < 32; i++) { + for (unsigned i = 0; i < MAX_CAPTURE_LIST_COUNT; i++) { array_delete(&self->list[i]); } } -static CaptureList *capture_list_pool_get(CaptureListPool *self, uint16_t id) { +static const CaptureList *capture_list_pool_get(const CaptureListPool *self, uint16_t id) { + if (id >= MAX_CAPTURE_LIST_COUNT) return &self->empty_list; + return &self->list[id]; +} + +static CaptureList *capture_list_pool_get_mut(CaptureListPool *self, uint16_t id) { + assert(id < MAX_CAPTURE_LIST_COUNT); return &self->list[id]; } @@ -273,12 +299,14 @@ static uint16_t capture_list_pool_acquire(CaptureListPool *self) { // the leading zeros in the usage map. An id of zero corresponds to the // highest-order bit in the bitmask. uint16_t id = count_leading_zeros(self->usage_map); - if (id == 32) return NONE; + if (id >= MAX_CAPTURE_LIST_COUNT) return NONE; self->usage_map &= ~bitmask_for_index(id); + array_clear(&self->list[id]); return id; } static void capture_list_pool_release(CaptureListPool *self, uint16_t id) { + if (id >= MAX_CAPTURE_LIST_COUNT) return; array_clear(&self->list[id]); self->usage_map |= bitmask_for_index(id); } @@ -416,13 +444,15 @@ static QueryStep query_step__new( .symbol = symbol, .depth = depth, .field = 0, - .capture_ids = {NONE, NONE, NONE, NONE}, + .capture_ids = {NONE, NONE, NONE}, + .alternative_index = NONE, .contains_captures = false, - .is_repeated = false, - .is_last = false, + .is_last_child = false, .is_pattern_start = false, + .is_pass_through = false, + .is_dead_end = false, .is_immediate = is_immediate, - .repeat_step_index = NONE, + .alternative_is_immediate = false, }; } @@ -511,13 +541,14 @@ static inline bool ts_query__pattern_map_search( static inline void ts_query__pattern_map_insert( TSQuery *self, TSSymbol symbol, - uint32_t start_step_index + uint32_t start_step_index, + uint32_t pattern_index ) { uint32_t index; ts_query__pattern_map_search(self, symbol, &index); array_insert(&self->pattern_map, index, ((PatternEntry) { .step_index = start_step_index, - .pattern_index = self->pattern_map.size, + .pattern_index = pattern_index, })); } @@ -548,12 +579,22 @@ static TSQueryError ts_query__parse_predicate( TSQuery *self, Stream *stream ) { - if (stream->next == ')') return PARENT_DONE; - if (stream->next != '(') return TSQueryErrorSyntax; - stream_advance(stream); + if (!stream_is_ident_start(stream)) return TSQueryErrorSyntax; + const char *predicate_name = stream->input; + stream_scan_identifier(stream); + uint32_t length = stream->input - predicate_name; + uint16_t id = symbol_table_insert_name( + &self->predicate_values, + predicate_name, + length + ); + array_back(&self->predicates_by_pattern)->length++; + array_push(&self->predicate_steps, ((TSQueryPredicateStep) { + .type = TSQueryPredicateStepTypeString, + .value_id = id, + })); stream_skip_whitespace(stream); - unsigned step_count = 0; for (;;) { if (stream->next == ')') { stream_advance(stream); @@ -658,7 +699,6 @@ static TSQueryError ts_query__parse_predicate( return TSQueryErrorSyntax; } - step_count++; stream_skip_whitespace(stream); } @@ -675,102 +715,195 @@ static TSQueryError ts_query__parse_pattern( uint32_t *capture_count, bool is_immediate ) { - uint16_t starting_step_index = self->steps.size; + uint32_t starting_step_index = self->steps.size; if (stream->next == 0) return TSQueryErrorSyntax; - // Finish the parent S-expression - if (stream->next == ')') { + // Finish the parent S-expression. + if (stream->next == ')' || stream->next == ']') { return PARENT_DONE; } - // Parse a parenthesized node expression - else if (stream->next == '(') { + // An open bracket is the start of an alternation. + else if (stream->next == '[') { stream_advance(stream); stream_skip_whitespace(stream); - // Parse a nested list, which represents a pattern followed by - // zero-or-more predicates. - if (stream->next == '(' && depth == 0) { - TSQueryError e = ts_query__parse_pattern(self, stream, 0, capture_count, is_immediate); - if (e) return e; + // Parse each branch, and add a placeholder step in between the branches. + Array(uint32_t) branch_step_indices = array_new(); + for (;;) { + uint32_t start_index = self->steps.size; + TSQueryError e = ts_query__parse_pattern( + self, + stream, + depth, + capture_count, + is_immediate + ); - // Parse the predicates. - stream_skip_whitespace(stream); + if (e == PARENT_DONE && stream->next == ']' && branch_step_indices.size > 0) { + stream_advance(stream); + break; + } else if (e) { + array_delete(&branch_step_indices); + return e; + } + + array_push(&branch_step_indices, start_index); + array_push(&self->steps, query_step__new(0, depth, false)); + } + (void)array_pop(&self->steps); + + // For all of the branches except for the last one, add the subsequent branch as an + // alternative, and link the end of the branch to the current end of the steps. + for (unsigned i = 0; i < branch_step_indices.size - 1; i++) { + uint32_t step_index = branch_step_indices.contents[i]; + uint32_t next_step_index = branch_step_indices.contents[i + 1]; + QueryStep *start_step = &self->steps.contents[step_index]; + QueryStep *end_step = &self->steps.contents[next_step_index - 1]; + start_step->alternative_index = next_step_index; + end_step->alternative_index = self->steps.size; + end_step->is_dead_end = true; + } + + array_delete(&branch_step_indices); + } + + // An open parenthesis can be the start of three possible constructs: + // * A grouped sequence + // * A predicate + // * A named node + else if (stream->next == '(') { + stream_advance(stream); + stream_skip_whitespace(stream); + + // If this parenthesis is followed by a node, then it represents a grouped sequence. + if (stream->next == '(' || stream->next == '"' || stream->next == '[') { + bool child_is_immediate = false; for (;;) { - TSQueryError e = ts_query__parse_predicate(self, stream); - if (e == PARENT_DONE) { + if (stream->next == '.') { + child_is_immediate = true; stream_advance(stream); stream_skip_whitespace(stream); - return 0; + } + TSQueryError e = ts_query__parse_pattern( + self, + stream, + depth, + capture_count, + child_is_immediate + ); + if (e == PARENT_DONE && stream->next == ')') { + stream_advance(stream); + break; } else if (e) { return e; } + + child_is_immediate = false; } } - TSSymbol symbol; - - // Parse the wildcard symbol - if (stream->next == '*') { - symbol = depth > 0 ? NAMED_WILDCARD_SYMBOL : WILDCARD_SYMBOL; + // A pound character indicates the start of a predicate. + else if (stream->next == '#') { stream_advance(stream); + return ts_query__parse_predicate(self, stream); } - // Parse a normal node name - else if (stream_is_ident_start(stream)) { - const char *node_name = stream->input; - stream_scan_identifier(stream); - uint32_t length = stream->input - node_name; - symbol = ts_language_symbol_for_name( - self->language, - node_name, - length, - true - ); - if (!symbol) { - stream_reset(stream, node_name); - return TSQueryErrorNodeType; - } - } else { - return TSQueryErrorSyntax; - } + // Otherwise, this parenthesis is the start of a named node. + else { + TSSymbol symbol; - // Add a step for the node. - array_push(&self->steps, query_step__new(symbol, depth, is_immediate)); + // Parse the wildcard symbol + if ( + stream->next == '_' || - // Parse the child patterns - stream_skip_whitespace(stream); - bool child_is_immediate = false; - uint16_t child_start_step_index = self->steps.size; - for (;;) { - if (stream->next == '.') { - child_is_immediate = true; + // TODO - remove. + // For temporary backward compatibility, handle '*' as a wildcard. + stream->next == '*' + ) { + symbol = depth > 0 ? NAMED_WILDCARD_SYMBOL : WILDCARD_SYMBOL; stream_advance(stream); - stream_skip_whitespace(stream); } - TSQueryError e = ts_query__parse_pattern( - self, - stream, - depth + 1, - capture_count, - child_is_immediate - ); - if (e == PARENT_DONE) { - if (child_is_immediate) { - self->steps.contents[child_start_step_index].is_last = true; + // Parse a normal node name + else if (stream_is_ident_start(stream)) { + const char *node_name = stream->input; + stream_scan_identifier(stream); + uint32_t length = stream->input - node_name; + + // TODO - remove. + // For temporary backward compatibility, handle predicates without the leading '#' sign. + if (length > 0 && (node_name[length - 1] == '!' || node_name[length - 1] == '?')) { + stream_reset(stream, node_name); + return ts_query__parse_predicate(self, stream); } - stream_advance(stream); - break; - } else if (e) { - return e; + + symbol = ts_language_symbol_for_name( + self->language, + node_name, + length, + true + ); + if (!symbol) { + stream_reset(stream, node_name); + return TSQueryErrorNodeType; + } + } else { + return TSQueryErrorSyntax; } - child_is_immediate = false; + // Add a step for the node. + array_push(&self->steps, query_step__new(symbol, depth, is_immediate)); + + // Parse the child patterns + stream_skip_whitespace(stream); + bool child_is_immediate = false; + uint16_t child_start_step_index = self->steps.size; + for (;;) { + if (stream->next == '.') { + child_is_immediate = true; + stream_advance(stream); + stream_skip_whitespace(stream); + } + + TSQueryError e = ts_query__parse_pattern( + self, + stream, + depth + 1, + capture_count, + child_is_immediate + ); + if (e == PARENT_DONE && stream->next == ')') { + if (child_is_immediate) { + self->steps.contents[child_start_step_index].is_last_child = true; + } + stream_advance(stream); + break; + } else if (e) { + return e; + } + + child_is_immediate = false; + } } } + // Parse a wildcard pattern + else if ( + stream->next == '_' || + + // TODO remove. + // For temporary backward compatibility, handle '*' as a wildcard. + stream->next == '*' + ) { + stream_advance(stream); + stream_skip_whitespace(stream); + + // Add a step that matches any kind of node + array_push(&self->steps, query_step__new(WILDCARD_SYMBOL, depth, is_immediate)); + } + // Parse a double-quoted anonymous leaf node expression else if (stream->next == '"') { stream_advance(stream); @@ -842,15 +975,6 @@ static TSQueryError ts_query__parse_pattern( self->steps.contents[step_index].field = field_id; } - // Parse a wildcard pattern - else if (stream->next == '*') { - stream_advance(stream); - stream_skip_whitespace(stream); - - // Add a step that matches any kind of node - array_push(&self->steps, query_step__new(WILDCARD_SYMBOL, depth, is_immediate)); - } - else { return TSQueryErrorSyntax; } @@ -861,22 +985,54 @@ static TSQueryError ts_query__parse_pattern( for (;;) { QueryStep *step = &self->steps.contents[starting_step_index]; + // Parse the one-or-more operator. if (stream->next == '+') { stream_advance(stream); - step->is_repeated = true; - array_back(&self->steps)->repeat_step_index = starting_step_index; stream_skip_whitespace(stream); + + QueryStep repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false); + repeat_step.alternative_index = starting_step_index; + repeat_step.is_pass_through = true; + repeat_step.alternative_is_immediate = true; + array_push(&self->steps, repeat_step); + } + + // Parse the zero-or-more repetition operator. + else if (stream->next == '*') { + stream_advance(stream); + stream_skip_whitespace(stream); + + QueryStep repeat_step = query_step__new(WILDCARD_SYMBOL, depth, false); + repeat_step.alternative_index = starting_step_index; + repeat_step.is_pass_through = true; + repeat_step.alternative_is_immediate = true; + array_push(&self->steps, repeat_step); + + while (step->alternative_index != NONE) { + step = &self->steps.contents[step->alternative_index]; + } + step->alternative_index = self->steps.size; + } + + // Parse the optional operator. + else if (stream->next == '?') { + stream_advance(stream); + stream_skip_whitespace(stream); + + while (step->alternative_index != NONE) { + step = &self->steps.contents[step->alternative_index]; + } + step->alternative_index = self->steps.size; } // Parse an '@'-prefixed capture pattern else if (stream->next == '@') { stream_advance(stream); - - // Parse the capture name if (!stream_is_ident_start(stream)) return TSQueryErrorSyntax; const char *capture_name = stream->input; stream_scan_identifier(stream); uint32_t length = stream->input - capture_name; + stream_skip_whitespace(stream); // Add the capture id to the first step of the pattern uint16_t capture_id = symbol_table_insert_name( @@ -884,10 +1040,22 @@ static TSQueryError ts_query__parse_pattern( capture_name, length ); - query_step__add_capture(step, capture_id); - (*capture_count)++; - stream_skip_whitespace(stream); + for (;;) { + query_step__add_capture(step, capture_id); + if ( + step->alternative_index != NONE && + step->alternative_index > starting_step_index && + step->alternative_index < self->steps.size + ) { + starting_step_index = step->alternative_index; + step = &self->steps.contents[starting_step_index]; + } else { + break; + } + } + + (*capture_count)++; } // No more suffix modifiers @@ -950,6 +1118,7 @@ TSQuery *ts_query_new( Stream stream = stream_new(source, source_len); stream_skip_whitespace(&stream); while (stream.input < stream.end) { + uint32_t pattern_index = self->predicates_by_pattern.size; uint32_t start_step_index = self->steps.size; uint32_t capture_count = 0; array_push(&self->start_bytes_by_pattern, stream.input - source); @@ -963,6 +1132,7 @@ TSQuery *ts_query_new( // If any pattern could not be parsed, then report the error information // and terminate. if (*error_type) { + if (*error_type == PARENT_DONE) *error_type = TSQueryErrorSyntax; *error_offset = stream.input - source; ts_query_delete(self); return NULL; @@ -980,14 +1150,21 @@ TSQuery *ts_query_new( } // Maintain a map that can look up patterns for a given root symbol. - self->steps.contents[start_step_index].is_pattern_start = true; - ts_query__pattern_map_insert( - self, - self->steps.contents[start_step_index].symbol, - start_step_index - ); - if (self->steps.contents[start_step_index].symbol == WILDCARD_SYMBOL) { - self->wildcard_root_pattern_count++; + for (;;) { + QueryStep *step = &self->steps.contents[start_step_index]; + step->is_pattern_start = true; + ts_query__pattern_map_insert(self, step->symbol, start_step_index, pattern_index); + if (step->symbol == WILDCARD_SYMBOL) { + self->wildcard_root_pattern_count++; + } + + // If there are alternatives or options at the root of the pattern, + // then add multiple entries to the pattern map. + if (step->alternative_index != NONE) { + start_step_index = step->alternative_index; + } else { + break; + } } } @@ -1090,7 +1267,7 @@ void ts_query_disable_pattern( * QueryCursor ***************/ -TSQueryCursor *ts_query_cursor_new() { +TSQueryCursor *ts_query_cursor_new(void) { TSQueryCursor *self = ts_malloc(sizeof(TSQueryCursor)); *self = (TSQueryCursor) { .ascending = false, @@ -1103,7 +1280,7 @@ TSQueryCursor *ts_query_cursor_new() { .end_point = POINT_MAX, }; array_reserve(&self->states, MAX_STATE_COUNT); - array_reserve(&self->finished_states, MAX_STATE_COUNT); + array_reserve(&self->finished_states, MAX_CAPTURE_LIST_COUNT); return self; } @@ -1165,6 +1342,9 @@ static bool ts_query_cursor__first_in_progress_capture( uint32_t *pattern_index ) { bool result = false; + *state_index = UINT32_MAX; + *byte_offset = UINT32_MAX; + *pattern_index = UINT32_MAX; for (unsigned i = 0; i < self->states.size; i++) { const QueryState *state = &self->states.contents[i]; const CaptureList *captures = capture_list_pool_get( @@ -1176,10 +1356,7 @@ static bool ts_query_cursor__first_in_progress_capture( if ( !result || capture_byte < *byte_offset || - ( - capture_byte == *byte_offset && - state->pattern_index < *pattern_index - ) + (capture_byte == *byte_offset && state->pattern_index < *pattern_index) ) { result = true; *state_index = i; @@ -1191,85 +1368,138 @@ static bool ts_query_cursor__first_in_progress_capture( return result; } -static bool ts_query__cursor_add_state( - TSQueryCursor *self, - const PatternEntry *pattern -) { - QueryStep *step = &self->query->steps.contents[pattern->step_index]; - - // If this pattern begins with a repetition, then avoid creating - // new states after already matching the repetition one or more times. - // The query should only one match for the repetition - the one that - // started the earliest. - if (step->is_repeated) { - for (unsigned i = 0; i < self->states.size; i++) { - QueryState *state = &self->states.contents[i]; - if (state->step_index == pattern->step_index) return true; - } +// Determine which node is first in a depth-first traversal +int ts_query_cursor__compare_nodes(TSNode left, TSNode right) { + if (left.id != right.id) { + uint32_t left_start = ts_node_start_byte(left); + uint32_t right_start = ts_node_start_byte(right); + if (left_start < right_start) return -1; + if (left_start > right_start) return 1; + uint32_t left_node_count = ts_node_end_byte(left); + uint32_t right_node_count = ts_node_end_byte(right); + if (left_node_count > right_node_count) return -1; + if (left_node_count < right_node_count) return 1; } + return 0; +} - uint32_t list_id = capture_list_pool_acquire(&self->capture_list_pool); - - // If there are no capture lists left in the pool, then terminate whichever - // state has captured the earliest node in the document, and steal its - // capture list. - if (list_id == NONE) { - uint32_t state_index, byte_offset, pattern_index; - if (ts_query_cursor__first_in_progress_capture( - self, - &state_index, - &byte_offset, - &pattern_index - )) { - LOG( - " abandon state. index:%u, pattern:%u, offset:%u.\n", - state_index, pattern_index, byte_offset - ); - list_id = self->states.contents[state_index].capture_list_id; - array_erase(&self->states, state_index); +// Determine if either state contains a superset of the other state's captures. +void ts_query_cursor__compare_captures( + TSQueryCursor *self, + QueryState *left_state, + QueryState *right_state, + bool *left_contains_right, + bool *right_contains_left +) { + const CaptureList *left_captures = capture_list_pool_get( + &self->capture_list_pool, + left_state->capture_list_id + ); + const CaptureList *right_captures = capture_list_pool_get( + &self->capture_list_pool, + right_state->capture_list_id + ); + *left_contains_right = true; + *right_contains_left = true; + unsigned i = 0, j = 0; + for (;;) { + if (i < left_captures->size) { + if (j < right_captures->size) { + TSQueryCapture *left = &left_captures->contents[i]; + TSQueryCapture *right = &right_captures->contents[j]; + if (left->node.id == right->node.id && left->index == right->index) { + i++; + j++; + } else { + switch (ts_query_cursor__compare_nodes(left->node, right->node)) { + case -1: + *right_contains_left = false; + i++; + break; + case 1: + *left_contains_right = false; + j++; + break; + default: + *right_contains_left = false; + *left_contains_right = false; + i++; + j++; + break; + } + } + } else { + *right_contains_left = false; + break; + } } else { - LOG(" too many finished states.\n"); - return false; + if (j < right_captures->size) { + *left_contains_right = false; + } + break; } } +} +static bool ts_query_cursor__add_state( + TSQueryCursor *self, + const PatternEntry *pattern +) { + if (self->states.size >= MAX_STATE_COUNT) { + LOG(" too many states"); + return false; + } LOG( " start state. pattern:%u, step:%u\n", pattern->pattern_index, pattern->step_index ); + QueryStep *step = &self->query->steps.contents[pattern->step_index]; array_push(&self->states, ((QueryState) { - .capture_list_id = list_id, + .capture_list_id = NONE, .step_index = pattern->step_index, .pattern_index = pattern->pattern_index, .start_depth = self->depth - step->depth, .consumed_capture_count = 0, - .repeat_match_count = 0, - .step_index_on_failure = NONE, - .seeking_non_match = false, + .seeking_immediate_match = false, })); return true; } +// Duplicate the given state and insert the newly-created state immediately after +// the given state in the `states` array. static QueryState *ts_query__cursor_copy_state( TSQueryCursor *self, const QueryState *state ) { - uint32_t new_list_id = capture_list_pool_acquire(&self->capture_list_pool); - if (new_list_id == NONE) return NULL; - array_push(&self->states, *state); - QueryState *new_state = array_back(&self->states); - new_state->capture_list_id = new_list_id; - CaptureList *old_captures = capture_list_pool_get( - &self->capture_list_pool, - state->capture_list_id - ); - CaptureList *new_captures = capture_list_pool_get( - &self->capture_list_pool, - new_list_id - ); - array_push_all(new_captures, old_captures); - return new_state; + if (self->states.size >= MAX_STATE_COUNT) { + LOG(" too many states"); + return NULL; + } + + // If the state has captures, copy its capture list. + QueryState copy = *state; + copy.capture_list_id = state->capture_list_id; + if (state->capture_list_id != NONE) { + copy.capture_list_id = capture_list_pool_acquire(&self->capture_list_pool); + if (copy.capture_list_id == NONE) { + LOG(" too many capture lists"); + return NULL; + } + const CaptureList *old_captures = capture_list_pool_get( + &self->capture_list_pool, + state->capture_list_id + ); + CaptureList *new_captures = capture_list_pool_get_mut( + &self->capture_list_pool, + copy.capture_list_id + ); + array_push_all(new_captures, old_captures); + } + + uint32_t index = (state - self->states.contents) + 1; + array_insert(&self->states, index, copy); + return &self->states.contents[index]; } // Walk the tree, processing patterns until at least one pattern finishes, @@ -1281,56 +1511,62 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { if (self->ascending) { LOG("leave node. type:%s\n", ts_node_type(ts_tree_cursor_current_node(&self->cursor))); - // When leaving a node, remove any unfinished states whose next step - // needed to match something within that node. + // Leave this node by stepping to its next sibling or to its parent. + bool did_move = true; + if (ts_tree_cursor_goto_next_sibling(&self->cursor)) { + self->ascending = false; + } else if (ts_tree_cursor_goto_parent(&self->cursor)) { + self->depth--; + } else { + did_move = false; + } + + // After leaving a node, remove any states that cannot make further progress. uint32_t deleted_count = 0; for (unsigned i = 0, n = self->states.size; i < n; i++) { QueryState *state = &self->states.contents[i]; QueryStep *step = &self->query->steps.contents[state->step_index]; - if ((uint32_t)state->start_depth + (uint32_t)step->depth > self->depth) { + // If a state completed its pattern inside of this node, but was deferred from finishing + // in order to search for longer matches, mark it as finished. + if (step->depth == PATTERN_DONE_MARKER) { + if (state->start_depth > self->depth || !did_move) { + LOG(" finish pattern %u\n", state->pattern_index); + state->id = self->next_state_id++; + array_push(&self->finished_states, *state); + deleted_count++; + continue; + } + } + + // If a state needed to match something within this node, then remove that state + // as it has failed to match. + else if ((uint32_t)state->start_depth + (uint32_t)step->depth > self->depth) { LOG( " failed to match. pattern:%u, step:%u\n", state->pattern_index, state->step_index ); - capture_list_pool_release( &self->capture_list_pool, state->capture_list_id ); deleted_count++; - } else if (deleted_count > 0) { + continue; + } + + if (deleted_count > 0) { self->states.contents[i - deleted_count] = *state; } } - self->states.size -= deleted_count; - if (ts_tree_cursor_goto_next_sibling(&self->cursor)) { - self->ascending = false; - } else if (ts_tree_cursor_goto_parent(&self->cursor)) { - self->depth--; - } else { + if (!did_move) { return self->finished_states.size > 0; } } else { - bool has_later_siblings; - bool can_have_later_siblings_with_this_field; - TSFieldId field_id = ts_tree_cursor_current_status( - &self->cursor, - &has_later_siblings, - &can_have_later_siblings_with_this_field - ); + // If this node is before the selected range, then avoid descending into it. TSNode node = ts_tree_cursor_current_node(&self->cursor); - TSSymbol symbol = ts_node_symbol(node); - bool is_named = ts_node_is_named(node); - if (symbol != ts_builtin_sym_error && self->query->symbol_map) { - symbol = self->query->symbol_map[symbol]; - } - - // If this node is before the selected range, then avoid descending - // into it. if ( ts_node_end_byte(node) <= self->start_byte || point_lte(ts_node_end_point(node), self->start_point) @@ -1347,18 +1583,26 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { point_lte(self->end_point, ts_node_start_point(node)) ) return false; + // Get the properties of the current node. + TSSymbol symbol = ts_node_symbol(node); + bool is_named = ts_node_is_named(node); + if (symbol != ts_builtin_sym_error && self->query->symbol_map) { + symbol = self->query->symbol_map[symbol]; + } + bool can_have_later_siblings; + bool can_have_later_siblings_with_this_field; + TSFieldId field_id = ts_tree_cursor_current_status( + &self->cursor, + &can_have_later_siblings, + &can_have_later_siblings_with_this_field + ); LOG( - "enter node. " - "type:%s, field:%s, row:%u state_count:%u, " - "finished_state_count:%u, has_later_siblings:%d, " - "can_have_later_siblings_with_this_field:%d\n", + "enter node. type:%s, field:%s, row:%u state_count:%u, finished_state_count:%u\n", ts_node_type(node), ts_language_field_name_for_id(self->query->language, field_id), ts_node_start_point(node).row, self->states.size, - self->finished_states.size, - has_later_siblings, - can_have_later_siblings_with_this_field + self->finished_states.size ); // Add new states for any patterns whose root node is a wildcard. @@ -1369,7 +1613,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // If this node matches the first step of the pattern, then add a new // state at the start of this pattern. if (step->field && field_id != step->field) continue; - if (!ts_query__cursor_add_state(self, pattern)) break; + if (!ts_query_cursor__add_state(self, pattern)) break; } // Add new states for any patterns whose root node matches this node. @@ -1381,7 +1625,7 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { // If this node matches the first step of the pattern, then add a new // state at the start of this pattern. if (step->field && field_id != step->field) continue; - if (!ts_query__cursor_add_state(self, pattern)) break; + if (!ts_query_cursor__add_state(self, pattern)) break; // Advance to the next pattern whose root node matches this node. i++; @@ -1392,9 +1636,11 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { } // Update all of the in-progress states with current node. - for (unsigned i = 0, n = self->states.size; i < n; i++) { + for (unsigned i = 0, copy_count = 0; i < self->states.size; i += 1 + copy_count) { QueryState *state = &self->states.contents[i]; QueryStep *step = &self->query->steps.contents[state->step_index]; + state->has_in_progress_alternatives = false; + copy_count = 0; // Check that the node matches all of the criteria for the next // step of the pattern. @@ -1407,11 +1653,11 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { step->symbol == symbol || step->symbol == WILDCARD_SYMBOL || (step->symbol == NAMED_WILDCARD_SYMBOL && is_named); - bool later_sibling_can_match = has_later_siblings; - if (step->is_immediate && is_named) { + bool later_sibling_can_match = can_have_later_siblings; + if ((step->is_immediate && is_named) || state->seeking_immediate_match) { later_sibling_can_match = false; } - if (step->is_last && has_later_siblings) { + if (step->is_last_child && can_have_later_siblings) { node_does_match = false; } if (step->field) { @@ -1424,25 +1670,8 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { } } + // Remove states immediately if it is ever clear that they cannot match. if (!node_does_match) { - // If this QueryState has processed a repeating sequence, and that repeating - // sequence has ended, move on to the *next* step of this state's pattern. - if ( - state->step_index_on_failure != NONE && - (!later_sibling_can_match || step->is_repeated) - ) { - LOG( - " finish repetition state. pattern:%u, step:%u\n", - state->pattern_index, - state->step_index - ); - state->step_index = state->step_index_on_failure; - state->step_index_on_failure = NONE; - state->repeat_match_count = 0; - i--; - continue; - } - if (!later_sibling_can_match) { LOG( " discard state. pattern:%u, step:%u\n", @@ -1455,115 +1684,201 @@ static inline bool ts_query_cursor__advance(TSQueryCursor *self) { ); array_erase(&self->states, i); i--; - n--; } - - state->seeking_non_match = false; continue; } - // The `seeking_non_match` flag indicates that a previous QueryState - // has already begun processing this repeating sequence, so that *this* - // QueryState should not begin matching until a separate repeating sequence - // is found. - if (state->seeking_non_match) continue; - - // Some patterns can match their root node in multiple ways, - // capturing different children. If this pattern step could match - // later children within the same parent, then this query state - // cannot simply be updated in place. It must be split into two - // states: one that matches this node, and one which skips over - // this node, to preserve the possibility of matching later - // siblings. - QueryState *next_state = state; + // Some patterns can match their root node in multiple ways, capturing different + // children. If this pattern step could match later children within the same + // parent, then this query state cannot simply be updated in place. It must be + // split into two states: one that matches this node, and one which skips over + // this node, to preserve the possibility of matching later siblings. if ( - !step->is_pattern_start && - step->contains_captures && later_sibling_can_match && - state->repeat_match_count == 0 + !step->is_pattern_start && + step->contains_captures ) { - QueryState *copy = ts_query__cursor_copy_state(self, state); + if (ts_query__cursor_copy_state(self, state)) { + LOG( + " split state for capture. pattern:%u, step:%u\n", + state->pattern_index, + state->step_index + ); + copy_count++; + } + } - // The QueryState that matched this node has begun matching a repeating - // sequence. The QueryState that *skipped* this node should not start - // matching later elements of the same repeating sequence. - if (step->is_repeated) { - state->seeking_non_match = true; + // If the current node is captured in this pattern, add it to the capture list. + // For the first capture in a pattern, lazily acquire a capture list. + if (step->capture_ids[0] != NONE) { + if (state->capture_list_id == NONE) { + state->capture_list_id = capture_list_pool_acquire(&self->capture_list_pool); + + // If there are no capture lists left in the pool, then terminate whichever + // state has captured the earliest node in the document, and steal its + // capture list. + if (state->capture_list_id == NONE) { + uint32_t state_index, byte_offset, pattern_index; + if (ts_query_cursor__first_in_progress_capture( + self, + &state_index, + &byte_offset, + &pattern_index + )) { + LOG( + " abandon state. index:%u, pattern:%u, offset:%u.\n", + state_index, pattern_index, byte_offset + ); + state->capture_list_id = self->states.contents[state_index].capture_list_id; + array_erase(&self->states, state_index); + if (state_index < i) { + i--; + state--; + } + } else { + LOG(" too many finished states.\n"); + array_erase(&self->states, i); + i--; + continue; + } + } } - if (copy) { + CaptureList *capture_list = capture_list_pool_get_mut( + &self->capture_list_pool, + state->capture_list_id + ); + for (unsigned j = 0; j < MAX_STEP_CAPTURE_COUNT; j++) { + uint16_t capture_id = step->capture_ids[j]; + if (step->capture_ids[j] == NONE) break; + array_push(capture_list, ((TSQueryCapture) { node, capture_id })); LOG( - " split state. pattern:%u, step:%u\n", - copy->pattern_index, - copy->step_index + " capture node. pattern:%u, capture_id:%u, capture_count:%u\n", + state->pattern_index, + capture_id, + capture_list->size ); - next_state = copy; - } else { - LOG(" cannot split state.\n"); } } - // If the current node is captured in this pattern, add it to the - // capture list. - for (unsigned j = 0; j < MAX_STEP_CAPTURE_COUNT; j++) { - uint16_t capture_id = step->capture_ids[j]; - if (step->capture_ids[j] == NONE) break; - CaptureList *capture_list = capture_list_pool_get( - &self->capture_list_pool, - next_state->capture_list_id - ); - array_push(capture_list, ((TSQueryCapture) { - node, - capture_id - })); - LOG( - " capture node. pattern:%u, capture_id:%u, capture_count:%u\n", - next_state->pattern_index, - capture_id, - capture_list->size - ); + // Advance this state to the next step of its pattern. + state->step_index++; + state->seeking_immediate_match = false; + LOG( + " advance state. pattern:%u, step:%u\n", + state->pattern_index, + state->step_index + ); + + // If this state's next step has an 'alternative' step (the step is either optional, + // or is the end of a repetition), then copy the state in order to pursue both + // alternatives. The alternative step itself may have an alternative, so this is + // an interative process. + unsigned end_index = i + 1; + for (unsigned j = i; j < end_index; j++) { + QueryState *state = &self->states.contents[j]; + QueryStep *next_step = &self->query->steps.contents[state->step_index]; + if (next_step->alternative_index != NONE) { + if (next_step->is_dead_end) { + state->step_index = next_step->alternative_index; + j--; + continue; + } + + QueryState *copy = ts_query__cursor_copy_state(self, state); + if (next_step->is_pass_through) { + state->step_index++; + j--; + } + if (copy) { + copy_count++; + end_index++; + copy->step_index = next_step->alternative_index; + if (next_step->alternative_is_immediate) { + copy->seeking_immediate_match = true; + } + LOG( + " split state for branch. pattern:%u, step:%u, step:%u, immediate:%d\n", + copy->pattern_index, + state->step_index, + copy->step_index, + copy->seeking_immediate_match + ); + } + } } + } - // If this is the end of a repetition, then jump back to the beginning - // of that repetition. - if (step->repeat_step_index != NONE) { - next_state->step_index_on_failure = next_state->step_index + 1; - next_state->step_index = step->repeat_step_index; - next_state->repeat_match_count++; - LOG( - " continue repeat. pattern:%u, match_count:%u\n", - next_state->pattern_index, - next_state->repeat_match_count - ); - } else { - next_state->step_index++; - LOG( - " advance state. pattern:%u, step:%u\n", - next_state->pattern_index, - next_state->step_index - ); + for (unsigned i = 0; i < self->states.size; i++) { + QueryState *state = &self->states.contents[i]; + bool did_remove = false; - QueryStep *next_step = step + 1; + // Enfore the longest-match criteria. When a query pattern contains optional or + // repeated nodes, this is necesssary to avoid multiple redundant states, where + // one state has a strict subset of another state's captures. + for (unsigned j = i + 1; j < self->states.size; j++) { + QueryState *other_state = &self->states.contents[j]; + if ( + state->pattern_index == other_state->pattern_index && + state->start_depth == other_state->start_depth + ) { + bool left_contains_right, right_contains_left; + ts_query_cursor__compare_captures( + self, + state, + other_state, + &left_contains_right, + &right_contains_left + ); + if (left_contains_right) { + if (state->step_index == other_state->step_index) { + LOG( + " drop shorter state. pattern: %u, step_index: %u\n", + state->pattern_index, + state->step_index + ); + capture_list_pool_release(&self->capture_list_pool, other_state->capture_list_id); + array_erase(&self->states, j); + j--; + continue; + } + other_state->has_in_progress_alternatives = true; + } + if (right_contains_left) { + if (state->step_index == other_state->step_index) { + LOG( + " drop shorter state. pattern: %u, step_index: %u\n", + state->pattern_index, + state->step_index + ); + capture_list_pool_release(&self->capture_list_pool, state->capture_list_id); + array_erase(&self->states, i); + did_remove = true; + break; + } + state->has_in_progress_alternatives = true; + } + } + } - // If the pattern is now done, then remove it from the list of - // in-progress states, and add it to the list of finished states. + // If there the state is at the end of its pattern, remove it from the list + // of in-progress states and add it to the list of finished states. + if (!did_remove) { + QueryStep *next_step = &self->query->steps.contents[state->step_index]; if (next_step->depth == PATTERN_DONE_MARKER) { - LOG(" finish pattern %u\n", next_state->pattern_index); - - next_state->id = self->next_state_id++; - array_push(&self->finished_states, *next_state); - if (next_state == state) { - array_erase(&self->states, i); - i--; - n--; + if (state->has_in_progress_alternatives) { + LOG(" defer finishing pattern %u\n", state->pattern_index); } else { - self->states.size--; + LOG(" finish pattern %u\n", state->pattern_index); + state->id = self->next_state_id++; + array_push(&self->finished_states, *state); + array_erase(&self->states, state - self->states.contents); + i--; } } } } - // Continue descending if possible. if (ts_tree_cursor_goto_first_child(&self->cursor)) { self->depth++; @@ -1589,7 +1904,7 @@ bool ts_query_cursor_next_match( QueryState *state = &self->finished_states.contents[0]; match->id = state->id; match->pattern_index = state->pattern_index; - CaptureList *captures = capture_list_pool_get( + const CaptureList *captures = capture_list_pool_get( &self->capture_list_pool, state->capture_list_id ); @@ -1631,8 +1946,8 @@ bool ts_query_cursor_next_capture( // First, identify the position of the earliest capture in an unfinished // match. For a finished capture to be returned, it must be *before* // this position. - uint32_t first_unfinished_capture_byte = UINT32_MAX; - uint32_t first_unfinished_pattern_index = UINT32_MAX; + uint32_t first_unfinished_capture_byte; + uint32_t first_unfinished_pattern_index; uint32_t first_unfinished_state_index; ts_query_cursor__first_in_progress_capture( self, @@ -1647,7 +1962,7 @@ bool ts_query_cursor_next_capture( uint32_t first_finished_pattern_index = first_unfinished_pattern_index; for (unsigned i = 0; i < self->finished_states.size; i++) { const QueryState *state = &self->finished_states.contents[i]; - CaptureList *captures = capture_list_pool_get( + const CaptureList *captures = capture_list_pool_get( &self->capture_list_pool, state->capture_list_id ); @@ -1685,7 +2000,7 @@ bool ts_query_cursor_next_capture( ]; match->id = state->id; match->pattern_index = state->pattern_index; - CaptureList *captures = capture_list_pool_get( + const CaptureList *captures = capture_list_pool_get( &self->capture_list_pool, state->capture_list_id ); diff --git a/src/tree_sitter/stack.c b/src/tree_sitter/stack.c index ade1577566..6ceee2577f 100644 --- a/src/tree_sitter/stack.c +++ b/src/tree_sitter/stack.c @@ -480,6 +480,7 @@ StackSliceArray ts_stack_pop_count(Stack *self, StackVersion version, uint32_t c } inline StackAction pop_pending_callback(void *payload, const StackIterator *iterator) { + (void)payload; if (iterator->subtree_count >= 1) { if (iterator->is_pending) { return StackActionPop | StackActionStop; @@ -532,6 +533,7 @@ SubtreeArray ts_stack_pop_error(Stack *self, StackVersion version) { } inline StackAction pop_all_callback(void *payload, const StackIterator *iterator) { + (void)payload; return iterator->node->link_count == 0 ? StackActionPop : StackActionNone; } diff --git a/src/tree_sitter/subtree.c b/src/tree_sitter/subtree.c index b98f172339..ef92a32fe4 100644 --- a/src/tree_sitter/subtree.c +++ b/src/tree_sitter/subtree.c @@ -21,7 +21,7 @@ typedef struct { #define TS_MAX_INLINE_TREE_LENGTH UINT8_MAX #define TS_MAX_TREE_POOL_SIZE 32 -static const ExternalScannerState empty_state = {.length = 0, .short_data = {0}}; +static const ExternalScannerState empty_state = {{.short_data = {0}}, .length = 0}; // ExternalScannerState @@ -208,7 +208,7 @@ Subtree ts_subtree_new_leaf( .has_external_tokens = has_external_tokens, .is_missing = false, .is_keyword = is_keyword, - .first_leaf = {.symbol = 0, .parse_state = 0}, + {{.first_leaf = {.symbol = 0, .parse_state = 0}}} }; return (Subtree) {.ptr = data}; } @@ -464,15 +464,17 @@ MutableSubtree ts_subtree_new_node(SubtreePool *pool, TSSymbol symbol, *data = (SubtreeHeapData) { .ref_count = 1, .symbol = symbol, - .production_id = production_id, .visible = metadata.visible, .named = metadata.named, .has_changes = false, .fragile_left = fragile, .fragile_right = fragile, .is_keyword = false, - .node_count = 0, - .first_leaf = {.symbol = 0, .parse_state = 0}, + {{ + .node_count = 0, + .production_id = production_id, + .first_leaf = {.symbol = 0, .parse_state = 0}, + }} }; MutableSubtree result = {.ptr = data}; ts_subtree_set_children(result, children->contents, children->size, language); diff --git a/src/tree_sitter/treesitter_commit_hash.txt b/src/tree_sitter/treesitter_commit_hash.txt new file mode 100644 index 0000000000..bd7fcfbe76 --- /dev/null +++ b/src/tree_sitter/treesitter_commit_hash.txt @@ -0,0 +1 @@ +81d533d2d1b580fdb507accabc91ceddffb5b6f0 diff --git a/test/functional/api/keymap_spec.lua b/test/functional/api/keymap_spec.lua index 210394c83f..5da2c6b531 100644 --- a/test/functional/api/keymap_spec.lua +++ b/test/functional/api/keymap_spec.lua @@ -21,6 +21,7 @@ describe('nvim_get_keymap', function() local foo_bar_string = 'nnoremap foo bar' local foo_bar_map_table = { lhs='foo', + script=0, silent=0, rhs='bar', expr=0, @@ -245,6 +246,7 @@ describe('nvim_get_keymap', function() it('works correctly despite various &cpo settings', function() local cpo_table = { + script=0, silent=0, expr=0, sid=0, @@ -302,6 +304,7 @@ describe('nvim_get_keymap', function() lhs='| |', rhs='| |', mode='n', + script=0, silent=0, expr=0, sid=0, @@ -343,6 +346,7 @@ describe('nvim_set_keymap, nvim_del_keymap', function() to_return.noremap = not opts.noremap and 0 or 1 to_return.lhs = lhs to_return.rhs = rhs + to_return.script = 0 to_return.silent = not opts.silent and 0 or 1 to_return.nowait = not opts.nowait and 0 or 1 to_return.expr = not opts.expr and 0 or 1 diff --git a/test/functional/api/vim_spec.lua b/test/functional/api/vim_spec.lua index 2e9d0f57ac..72e810e3e4 100644 --- a/test/functional/api/vim_spec.lua +++ b/test/functional/api/vim_spec.lua @@ -482,6 +482,11 @@ describe('API', function() eq(true, status) -- nvim_input() did not fail. eq("E117:", v_errnum) -- v:errmsg was updated. end) + + it('does not crash even if trans_special result is largest #11788, #12287', function() + command("call nvim_input('<M-'.nr2char(0x40000000).'>')") + eq(1, eval('1')) + end) end) describe('nvim_paste', function() @@ -1808,7 +1813,7 @@ describe('API', function() eq({id=1}, meths.get_current_buf()) end) - it("doesn't cause BufEnter or BufWinEnter autocmds", function() + it("does not trigger BufEnter, BufWinEnter", function() command("let g:fired = v:false") command("au BufEnter,BufWinEnter * let g:fired = v:true") @@ -1818,7 +1823,7 @@ describe('API', function() eq(false, eval('g:fired')) end) - it('|scratch-buffer|', function() + it('scratch-buffer', function() eq({id=2}, meths.create_buf(false, true)) eq({id=3}, meths.create_buf(true, true)) eq({id=4}, meths.create_buf(true, true)) @@ -1845,6 +1850,7 @@ describe('API', function() eq('nofile', meths.buf_get_option(b, 'buftype')) eq('hide', meths.buf_get_option(b, 'bufhidden')) eq(false, meths.buf_get_option(b, 'swapfile')) + eq(false, meths.buf_get_option(b, 'modeline')) end -- @@ -1860,8 +1866,9 @@ describe('API', function() eq('nofile', meths.buf_get_option(edited_buf, 'buftype')) eq('hide', meths.buf_get_option(edited_buf, 'bufhidden')) eq(false, meths.buf_get_option(edited_buf, 'swapfile')) + eq(false, meths.buf_get_option(edited_buf, 'modeline')) - -- scratch buffer can be wiped without error + -- Scratch buffer can be wiped without error. command('bwipe') screen:expect([[ ^ | diff --git a/test/functional/autocmd/textyankpost_spec.lua b/test/functional/autocmd/textyankpost_spec.lua index 8c23b72cff..3898d59e58 100644 --- a/test/functional/autocmd/textyankpost_spec.lua +++ b/test/functional/autocmd/textyankpost_spec.lua @@ -27,7 +27,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(1, eval('g:count')) @@ -40,7 +41,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'baz ' }, regname = '', - regtype = 'v' + regtype = 'v', + visual = false }, eval('g:event')) eq(2, eval('g:count')) @@ -50,7 +52,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo', 'baz' }, regname = '', - regtype = "\0223" -- ^V + block width + regtype = "\0223", -- ^V + block width + visual = true }, eval('g:event')) eq(3, eval('g:count')) end) @@ -62,7 +65,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) command('set debug=msg') @@ -92,7 +96,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(1, eval('g:count')) eq({ 'foo\nbar' }, funcs.getreg('+',1,1)) @@ -105,7 +110,8 @@ describe('TextYankPost', function() operator = 'd', regcontents = { 'foo' }, regname = '', - regtype = 'v' + regtype = 'v', + visual = false }, eval('g:event')) eq(1, eval('g:count')) @@ -115,7 +121,8 @@ describe('TextYankPost', function() operator = 'd', regcontents = { '\nbar' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(2, eval('g:count')) @@ -125,7 +132,8 @@ describe('TextYankPost', function() operator = 'c', regcontents = { 'baz' }, regname = '', - regtype = 'v' + regtype = 'v', + visual = false }, eval('g:event')) eq(3, eval('g:count')) end) @@ -153,7 +161,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'bar' }, regname = 'b', - regtype = 'v' + regtype = 'v', + visual = false }, eval('g:event')) feed('"*yy') @@ -162,7 +171,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '*', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) command("set clipboard=unnamed") @@ -174,7 +184,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) feed('"*yy') @@ -183,7 +194,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'foo\nbar' }, regname = '*', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) end) @@ -194,7 +206,8 @@ describe('TextYankPost', function() operator = 'd', regcontents = { 'foo\nbar' }, regname = '+', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(1, eval('g:count')) @@ -204,7 +217,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'baz text' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(2, eval('g:count')) @@ -214,7 +228,8 @@ describe('TextYankPost', function() operator = 'y', regcontents = { 'baz ' }, regname = '', - regtype = 'v' + regtype = 'v', + visual = false }, eval('g:event')) eq(3, eval('g:count')) @@ -224,7 +239,8 @@ describe('TextYankPost', function() operator = 'd', regcontents = { 'baz text' }, regname = '', - regtype = 'V' + regtype = 'V', + visual = false }, eval('g:event')) eq(4, eval('g:count')) end) diff --git a/test/functional/core/startup_spec.lua b/test/functional/core/startup_spec.lua index cc10d36a10..3269fbc68d 100644 --- a/test/functional/core/startup_spec.lua +++ b/test/functional/core/startup_spec.lua @@ -277,6 +277,32 @@ describe('startup', function() [4] = {bold = true, foreground = Screen.colors.Blue1}, }}) end) + + it('fixed hang issue with --headless (#11386)', function() + local expected = '' + local period = 100 + for i = 1, period - 1 do + expected = expected .. i .. '\r\n' + end + expected = expected .. period + eq( + expected, + -- FIXME(codehex): We should really set a timeout for the system function. + -- If this test fails, there will be a waiting input state. + funcs.system({nvim_prog, '-u', 'NONE', '-c', + 'for i in range(1, 100) | echo i | endfor | quit', + '--headless' + }) + ) + end) + + it("get command line arguments from v:argv", function() + local out = funcs.system({ nvim_prog, '-u', 'NONE', '-i', 'NONE', '--headless', + '--cmd', nvim_set, + '-c', [[echo v:argv[-1:] len(v:argv) > 1]], + '+q' }) + eq('[\'+q\'] 1', out) + end) end) describe('sysinit', function() diff --git a/test/functional/eval/buf_functions_spec.lua b/test/functional/eval/buf_functions_spec.lua index 37f4c89bfd..06841a4521 100644 --- a/test/functional/eval/buf_functions_spec.lua +++ b/test/functional/eval/buf_functions_spec.lua @@ -31,10 +31,12 @@ for _, func in ipairs({'bufname(%s)', 'bufnr(%s)', 'bufwinnr(%s)', it('errors out when receives v:true/v:false/v:null', function() -- Not compatible with Vim: in Vim it always results in buffer not found -- without any error messages. - for _, var in ipairs({'v:true', 'v:false', 'v:null'}) do - eq('Vim(call):E5300: Expected a Number or a String', + for _, var in ipairs({'v:true', 'v:false'}) do + eq('Vim(call):E5299: Expected a Number or a String, Boolean found', exc_exec('call ' .. func:format(var))) end + eq('Vim(call):E5300: Expected a Number or a String', + exc_exec('call ' .. func:format('v:null'))) end) it('errors out when receives invalid argument', function() eq('Vim(call):E745: Expected a Number or a String, List found', diff --git a/test/functional/eval/map_functions_spec.lua b/test/functional/eval/map_functions_spec.lua index 2747a94570..275c72d212 100644 --- a/test/functional/eval/map_functions_spec.lua +++ b/test/functional/eval/map_functions_spec.lua @@ -13,6 +13,7 @@ describe('maparg()', function() local foo_bar_map_table = { lhs='foo', + script=0, silent=0, rhs='bar', expr=0, @@ -147,6 +148,7 @@ describe('maparg()', function() mode = 'n', noremap = 1, nowait = 0, + script=0, sid = 0, silent = 0, lnum = 0, diff --git a/test/functional/eval/sort_spec.lua b/test/functional/eval/sort_spec.lua index 82557575ce..e1cc2c2924 100644 --- a/test/functional/eval/sort_spec.lua +++ b/test/functional/eval/sort_spec.lua @@ -14,7 +14,7 @@ before_each(clear) describe('sort()', function() it('errors out when sorting special values', function() - eq('Vim(call):E907: Using a special value as a Float', + eq('Vim(call):E362: Using a boolean value as a Float', exc_exec('call sort([v:true, v:false], "f")')) end) @@ -30,6 +30,7 @@ describe('sort()', function() errors[err] = true end eq({ + ['E362: Using a boolean value as a Float']=true, ['E891: Using a Funcref as a Float']=true, ['E892: Using a String as a Float']=true, ['E893: Using a List as a Float']=true, diff --git a/test/functional/eval/uniq_spec.lua b/test/functional/eval/uniq_spec.lua index 0e7a013e32..5cdba0a0f6 100644 --- a/test/functional/eval/uniq_spec.lua +++ b/test/functional/eval/uniq_spec.lua @@ -11,7 +11,7 @@ before_each(clear) describe('uniq()', function() it('errors out when processing special values', function() - eq('Vim(call):E907: Using a special value as a Float', + eq('Vim(call):E362: Using a boolean value as a Float', exc_exec('call uniq([v:true, v:false], "f")')) end) diff --git a/test/functional/legacy/075_maparg_spec.lua b/test/functional/legacy/075_maparg_spec.lua index 0164f5077a..ee2b041b51 100644 --- a/test/functional/legacy/075_maparg_spec.lua +++ b/test/functional/legacy/075_maparg_spec.lua @@ -49,9 +49,9 @@ describe('maparg()', function() -- Assert buffer contents. expect([[ is<F4>foo - {'lnum': 0, 'silent': 0, 'noremap': 0, 'lhs': 'foo<C-V>', 'mode': ' ', 'nowait': 0, 'expr': 0, 'sid': 0, 'rhs': 'is<F4>foo', 'buffer': 0} - {'lnum': 0, 'silent': 1, 'noremap': 1, 'lhs': 'bar', 'mode': 'v', 'nowait': 0, 'expr': 1, 'sid': 0, 'rhs': 'isbar', 'buffer': 1} - {'lnum': 0, 'silent': 0, 'noremap': 0, 'lhs': 'foo', 'mode': ' ', 'nowait': 1, 'expr': 0, 'sid': 0, 'rhs': 'bar', 'buffer': 1} + {'lnum': 0, 'script': 0, 'silent': 0, 'noremap': 0, 'lhs': 'foo<C-V>', 'mode': ' ', 'nowait': 0, 'expr': 0, 'sid': 0, 'rhs': 'is<F4>foo', 'buffer': 0} + {'lnum': 0, 'script': 1, 'silent': 1, 'noremap': 1, 'lhs': 'bar', 'mode': 'v', 'nowait': 0, 'expr': 1, 'sid': 0, 'rhs': 'isbar', 'buffer': 1} + {'lnum': 0, 'script': 0, 'silent': 0, 'noremap': 0, 'lhs': 'foo', 'mode': ' ', 'nowait': 1, 'expr': 0, 'sid': 0, 'rhs': 'bar', 'buffer': 1} xrx yRy abcd]]) diff --git a/test/functional/legacy/memory_usage_spec.lua b/test/functional/legacy/memory_usage_spec.lua new file mode 100644 index 0000000000..28ca749749 --- /dev/null +++ b/test/functional/legacy/memory_usage_spec.lua @@ -0,0 +1,161 @@ +local helpers = require('test.functional.helpers')(after_each) +local clear = helpers.clear +local eval = helpers.eval +local eq = helpers.eq +local feed_command = helpers.feed_command +local iswin = helpers.iswin +local retry = helpers.retry +local ok = helpers.ok +local source = helpers.source + +local monitor_memory_usage = { + memory_usage = function(self) + local handle + if iswin() then + handle = io.popen('wmic process where processid=' ..self.pid..' get WorkingSetSize') + else + handle = io.popen('ps -o rss= -p '..self.pid) + end + return tonumber(handle:read('*a'):match('%d+')) + end, + op = function(self) + retry(nil, 10000, function() + local val = self.memory_usage(self) + if self.max < val then + self.max = val + end + table.insert(self.hist, val) + ok(#self.hist > 20) + local result = {} + for key,value in ipairs(self.hist) do + if value ~= self.hist[key + 1] then + table.insert(result, value) + end + end + table.remove(self.hist, 1) + self.last = self.hist[#self.hist] + eq(#result, 1) + end) + end, + dump = function(self) + return 'max: '..self.max ..', last: '..self.last + end, + monitor_memory_usage = function(self, pid) + local obj = { + pid = pid, + max = 0, + last = 0, + hist = {}, + } + setmetatable(obj, { __index = self }) + obj:op() + return obj + end +} +setmetatable(monitor_memory_usage, +{__call = function(self, pid) + return monitor_memory_usage.monitor_memory_usage(self, pid) +end}) + +describe('memory usage', function() + local function check_result(tbl, status, result) + if not status then + print('') + for key, val in pairs(tbl) do + print(key, val:dump()) + end + error(result) + end + end + + local function isasan() + local version = eval('execute("version")') + return version:match('-fsanitize=[a-z,]*address') + end + + before_each(clear) + + --[[ + Case: if a local variable captures a:000, funccall object will be free + just after it finishes. + ]]-- + it('function capture vargs', function() + if isasan() then + pending('ASAN build is difficult to estimate memory usage') + end + if iswin() and eval("executable('wmic')") == 0 then + pending('missing "wmic" command') + elseif eval("executable('ps')") == 0 then + pending('missing "ps" command') + end + + local pid = eval('getpid()') + local before = monitor_memory_usage(pid) + source([[ + func s:f(...) + let x = a:000 + endfunc + for _ in range(10000) + call s:f(0) + endfor + ]]) + local after = monitor_memory_usage(pid) + -- Estimate the limit of max usage as 2x initial usage. + -- The lower limit can fluctuate a bit, use 97%. + check_result({before=before, after=after}, + pcall(ok, before.last * 97 / 100 < after.max)) + check_result({before=before, after=after}, + pcall(ok, before.last * 2 > after.max)) + -- In this case, garbage collecting is not needed. + -- The value might fluctuate a bit, allow for 3% tolerance below and 5% above. + -- Based on various test runs. + local lower = after.last * 97 / 100 + local upper = after.last * 105 / 100 + check_result({before=before, after=after}, pcall(ok, lower < after.max)) + check_result({before=before, after=after}, pcall(ok, after.max < upper)) + end) + + --[[ + Case: if a local variable captures l: dict, funccall object will not be + free until garbage collector runs, but after that memory usage doesn't + increase so much even when rerun Xtest.vim since system memory caches. + ]]-- + it('function capture lvars', function() + if isasan() then + pending('ASAN build is difficult to estimate memory usage') + end + if iswin() and eval("executable('wmic')") == 0 then + pending('missing "wmic" command') + elseif eval("executable('ps')") == 0 then + pending('missing "ps" command') + end + + local pid = eval('getpid()') + local before = monitor_memory_usage(pid) + local fname = source([[ + if !exists('s:defined_func') + func s:f() + let x = l: + endfunc + endif + let s:defined_func = 1 + for _ in range(10000) + call s:f() + endfor + ]]) + local after = monitor_memory_usage(pid) + for _ = 1, 3 do + feed_command('so '..fname) + end + local last = monitor_memory_usage(pid) + -- The usage may be a bit less than the last value, use 80%. + -- Allow for 20% tolerance at the upper limit. That's very permissive, but + -- otherwise the test fails sometimes. + local lower = before.last * 8 / 10 + local upper = (after.max + (after.last - before.last)) * 12 / 10 + check_result({before=before, after=after, last=last}, + pcall(ok, lower < last.last)) + check_result({before=before, after=after, last=last}, + pcall(ok, last.last < upper)) + end) +end) diff --git a/test/functional/lua/treesitter_spec.lua b/test/functional/lua/treesitter_spec.lua index f93185d1f6..ecee471386 100644 --- a/test/functional/lua/treesitter_spec.lua +++ b/test/functional/lua/treesitter_spec.lua @@ -240,17 +240,17 @@ static int nlua_schedule(lua_State *const lstate) ; TODO(bfredl): overlapping matches are unreliable, ; we need a proper priority mechanism ;(type_identifier) @type -((type_identifier) @Special (eq? @Special "LuaRef")) +((type_identifier) @Special (#eq? @Special "LuaRef")) (primitive_type) @type (sized_type_specifier) @type ; defaults to very magic syntax, for best compatibility -((identifier) @Identifier (match? @Identifier "^l(u)a_")) +((identifier) @Identifier (#match? @Identifier "^l(u)a_")) ; still support \M etc prefixes -((identifier) @Constant (match? @Constant "\M^\[A-Z_]\+$")) +((identifier) @Constant (#match? @Constant "\M^\[A-Z_]\+$")) -((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (eq? @WarningMsg.left @WarningMsg.right)) +((binary_expression left: (identifier) @WarningMsg.left right: (identifier) @WarningMsg.right) (#eq? @WarningMsg.left @WarningMsg.right)) (comment) @comment ]] diff --git a/test/functional/lua/uri_spec.lua b/test/functional/lua/uri_spec.lua index a3b8e685e1..f782769935 100644 --- a/test/functional/lua/uri_spec.lua +++ b/test/functional/lua/uri_spec.lua @@ -112,6 +112,29 @@ describe('URI methods', function() eq('C:\\xy\\åäö\\ɧ\\汉语\\↥\\🤦\\🦄\\å\\بِيَّ.txt', exec_lua(test_case)) end) end) + + describe('decode non-file URI', function() + it('uri_to_fname returns non-file URI unchanged', function() + eq('jdt1.23+x-z://content/%5C/', exec_lua [[ + return vim.uri_to_fname('jdt1.23+x-z://content/%5C/') + ]]) + end) + + it('uri_to_fname returns non-file upper-case scheme URI unchanged', function() + eq('JDT://content/%5C/', exec_lua [[ + return vim.uri_to_fname('JDT://content/%5C/') + ]]) + end) + end) + + describe('decode URI without scheme', function() + it('fails because URI must have a scheme', function() + eq(false, exec_lua [[ + return pcall(vim.uri_to_fname, 'not_an_uri.txt') + ]]) + end) + end) + end) describe('uri to bufnr', function() diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index c68c05dffa..9b2697b3c2 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -243,6 +243,8 @@ describe('lua stdlib', function() { "here be dragons", " ", false, { "here", "be", "dragons"} }, { "axaby", "ab?", false, { '', 'x', 'y' } }, { "f v2v v3v w2w ", "([vw])2%1", false, { 'f ', ' v3v ', ' ' } }, + { "", "", false, {} }, + { "", "a", false, { '' } }, { "x*yz*oo*l", "*", true, { 'x', 'yz', 'oo', 'l' } }, } @@ -1048,13 +1050,13 @@ describe('lua stdlib', function() end) it('vim.defer_fn', function() - exec_lua [[ - vim.g.test = 0 - vim.defer_fn(function() vim.g.test = 1 end, 50) - ]] - eq(0, exec_lua[[return vim.g.test]]) - exec_lua [[vim.cmd("sleep 1000m")]] - eq(1, exec_lua[[return vim.g.test]]) + eq(false, exec_lua [[ + vim.g.test = false + vim.defer_fn(function() vim.g.test = true end, 150) + return vim.g.test + ]]) + exec_lua [[vim.wait(1000, function() return vim.g.test end)]] + eq(true, exec_lua[[return vim.g.test]]) end) it('vim.region', function() @@ -1066,4 +1068,168 @@ describe('lua stdlib', function() eq({5,15}, exec_lua[[ return vim.region(0,{1,5},{1,14},'v',true)[1] ]]) end) + describe('vim.wait', function() + before_each(function() + exec_lua[[ + -- high precision timer + get_time = function() + return vim.fn.reltimefloat(vim.fn.reltime()) + end + ]] + end) + + it('should run from lua', function() + exec_lua[[vim.wait(100, function() return true end)]] + end) + + it('should wait the expected time if false', function() + eq({time = true, wait_result = {false, -1}}, exec_lua[[ + start_time = get_time() + wait_succeed, wait_fail_val = vim.wait(200, function() return false end) + + return { + -- 150ms waiting or more results in true. Flaky tests are bad. + time = (start_time + 0.15) < get_time(), + wait_result = {wait_succeed, wait_fail_val} + } + ]]) + end) + + + it('should not block other events', function() + eq({time = true, wait_result = true}, exec_lua[[ + start_time = get_time() + + vim.g.timer_result = false + timer = vim.loop.new_timer() + timer:start(100, 0, vim.schedule_wrap(function() + vim.g.timer_result = true + end)) + + -- Would wait ten seconds if results blocked. + wait_result = vim.wait(10000, function() return vim.g.timer_result end) + + return { + time = (start_time + 5) > get_time(), + wait_result = wait_result, + } + ]]) + end) + + it('should work with vim.defer_fn', function() + eq({time = true, wait_result = true}, exec_lua[[ + start_time = get_time() + + vim.defer_fn(function() vim.g.timer_result = true end, 100) + wait_result = vim.wait(10000, function() return vim.g.timer_result end) + + return { + time = (start_time + 5) > get_time(), + wait_result = wait_result, + } + ]]) + end) + + it('should require functions to be passed', function() + local pcall_result = exec_lua [[ + return {pcall(function() vim.wait(1000, 13) end)} + ]] + + eq(pcall_result[1], false) + matches('condition must be a function', pcall_result[2]) + end) + + it('should not crash when callback errors', function() + local pcall_result = exec_lua [[ + return {pcall(function() vim.wait(1000, function() error("As Expected") end) end)} + ]] + + eq(pcall_result[1], false) + matches('As Expected', pcall_result[2]) + end) + + it('should call callbacks exactly once if they return true immediately', function() + eq(true, exec_lua [[ + vim.g.wait_count = 0 + vim.wait(1000, function() + vim.g.wait_count = vim.g.wait_count + 1 + return true + end, 20) + return vim.g.wait_count == 1 + ]]) + end) + + it('should call callbacks few times with large `interval`', function() + eq(true, exec_lua [[ + vim.g.wait_count = 0 + vim.wait(50, function() vim.g.wait_count = vim.g.wait_count + 1 end, 200) + return vim.g.wait_count < 5 + ]]) + end) + + it('should play nice with `not` when fails', function() + eq(true, exec_lua [[ + if not vim.wait(50, function() end) then + return true + end + + return false + ]]) + end) + + it('should play nice with `if` when success', function() + eq(true, exec_lua [[ + if vim.wait(50, function() return true end) then + return true + end + + return false + ]]) + end) + + it('should return immediately with false if timeout is 0', function() + eq({false, -1}, exec_lua [[ + return { + vim.wait(0, function() return false end) + } + ]]) + end) + + it('should work with tables with __call', function() + eq(true, exec_lua [[ + local t = setmetatable({}, {__call = function(...) return true end}) + return vim.wait(100, t, 10) + ]]) + end) + + it('should work with tables with __call that change', function() + eq(true, exec_lua [[ + local t = {count = 0} + setmetatable(t, { + __call = function() + t.count = t.count + 1 + return t.count > 3 + end + }) + + return vim.wait(1000, t, 10) + ]]) + end) + + it('should not work with negative intervals', function() + local pcall_result = exec_lua [[ + return pcall(function() vim.wait(1000, function() return false end, -1) end) + ]] + + eq(false, pcall_result) + end) + + it('should not work with weird intervals', function() + local pcall_result = exec_lua [[ + return pcall(function() vim.wait(1000, function() return false end, 'a string value') end) + ]] + + eq(false, pcall_result) + end) end) +end) diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index f41a5323a8..1ab81a0ef8 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -6,6 +6,7 @@ local buf_lines = helpers.buf_lines local dedent = helpers.dedent local exec_lua = helpers.exec_lua local eq = helpers.eq +local pcall_err = helpers.pcall_err local pesc = helpers.pesc local insert = helpers.insert local retry = helpers.retry @@ -705,7 +706,6 @@ describe('LSP', function() end; } end) - end) describe("parsing tests", function() @@ -733,7 +733,23 @@ describe('LSP', function() end; } end) + end) + describe('lsp._cmd_parts test', function() + local function _cmd_parts(input) + return exec_lua([[ + lsp = require('vim.lsp') + return lsp._cmd_parts(...) + ]], input) + end + it('should valid cmd argument', function() + eq(true, pcall(_cmd_parts, {"nvim"})) + eq(true, pcall(_cmd_parts, {"nvim", "--head"})) + end) + it('should invalid cmd argument', function() + eq('Error executing lua: .../shared.lua: cmd: expected list, got nvim', pcall_err(_cmd_parts, "nvim")) + eq('Error executing lua: .../shared.lua: cmd argument: expected string, got number', pcall_err(_cmd_parts, {"nvim", 1})) + end) end) end) @@ -784,20 +800,44 @@ describe('LSP', function() make_edit(0, 0, 0, 0, {"123"}); make_edit(1, 0, 1, 1, {"2"}); make_edit(2, 0, 2, 2, {"3"}); + make_edit(3, 2, 3, 4, {""}); } exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) eq({ '123First line of text'; '2econd line of text'; '3ird line of text'; - 'Fourth line of text'; + 'Foth line of text'; + 'å å ɧ 汉语 ↥ 🤦 🦄'; + }, buf_lines(1)) + end) + it('handles edits with the same start position, applying changes in the order in the array', function() + local edits = { + make_edit(0, 6, 0, 10, {""}); + make_edit(0, 6, 0, 6, {"REPLACE"}); + make_edit(1, 0, 1, 3, {""}); + make_edit(1, 0, 1, 0, {"123"}); + make_edit(2, 16, 2, 18, {""}); + make_edit(2, 16, 2, 16, {"XYZ"}); + make_edit(3, 7, 3, 11, {"this"}); + make_edit(3, 7, 3, 11, {"will"}); + make_edit(3, 7, 3, 11, {"not "}); + make_edit(3, 7, 3, 11, {"show"}); + make_edit(3, 7, 3, 11, {"(but this will)"}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) + eq({ + 'First REPLACE of text'; + '123ond line of text'; + 'Third line of teXYZ'; + 'Fourth (but this will) of text'; 'å å ɧ 汉语 ↥ 🤦 🦄'; }, buf_lines(1)) end) it('applies complex edits', function() local edits = { - make_edit(0, 0, 0, 0, {"", "12"}); make_edit(0, 0, 0, 0, {"3", "foo"}); + make_edit(0, 0, 0, 0, {"", "12"}); make_edit(0, 1, 0, 1, {"bar", "123"}); make_edit(0, #"First ", 0, #"First line of text", {"guy"}); make_edit(1, 0, 1, #'Second', {"baz"}); @@ -957,7 +997,14 @@ describe('LSP', function() { label='foocar', insertText='foobar', textEdit={} }, -- resolves into textEdit.newText { label='foocar', insertText='foodar', textEdit={newText='foobar'} }, - { label='foocar', textEdit={newText='foobar'} } + { label='foocar', textEdit={newText='foobar'} }, + -- real-world snippet text + { label='foocar', insertText='foodar', textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} }, + { label='foocar', insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', textEdit={} }, + -- nested snippet tokens + { label='foocar', insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', textEdit={} }, + -- plain text + { label='foocar', insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} }, } local completion_list_items = {items=completion_list} local expected = { @@ -967,6 +1014,10 @@ describe('LSP', function() { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foobar', textEdit={} } } } } }, { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foodar', textEdit={newText='foobar'} } } } } }, { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar', user_data = { nvim = { lsp = { completion_item = { label='foocar', textEdit={newText='foobar'} } } } } }, + { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foobar(place holder, more ...holder{})', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foodar', textEdit={newText='foobar(${1:place holder}, ${2:more ...holder{\\}})'} } } } } }, + { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ1, var2 *typ2) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foodar(${1:var1} typ1, ${2:var2} *typ2) {$0\\}', textEdit={} } } } } }, + { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(var1 typ2,typ3 tail) {}', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foodar(${1:var1 ${2|typ2,typ3|} ${3:tail}}) {$0\\}', textEdit={} } } } } }, + { abbr = 'foocar', dup = 1, empty = 1, icase = 1, info = ' ', kind = 'Unknown', menu = '', word = 'foodar(${1:var1})', user_data = { nvim = { lsp = { completion_item = { label='foocar', insertText='foodar(${1:var1})', insertTextFormat=1, textEdit={} } } } } }, } eq(expected, exec_lua([[return vim.lsp.util.text_document_completion_list_to_complete_items(...)]], completion_list, prefix)) @@ -1204,7 +1255,7 @@ describe('LSP', function() ]]) end) end) - describe('convert SymbolInformation[] to items', function() + it('convert SymbolInformation[] to items', function() local expected = { { col = 1, @@ -1268,11 +1319,11 @@ describe('LSP', function() end) describe('lsp.util._get_completion_item_kind_name', function() - describe('returns the name specified by protocol', function() + it('returns the name specified by protocol', function() eq("Text", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1)")) eq("TypeParameter", exec_lua("return vim.lsp.util._get_completion_item_kind_name(25)")) end) - describe('returns the name not specified by protocol', function() + it('returns the name not specified by protocol', function() eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(nil)")) eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(vim.NIL)")) eq("Unknown", exec_lua("return vim.lsp.util._get_completion_item_kind_name(1000)")) @@ -1280,14 +1331,93 @@ describe('LSP', function() end) describe('lsp.util._get_symbol_kind_name', function() - describe('returns the name specified by protocol', function() + it('returns the name specified by protocol', function() eq("File", exec_lua("return vim.lsp.util._get_symbol_kind_name(1)")) eq("TypeParameter", exec_lua("return vim.lsp.util._get_symbol_kind_name(26)")) end) - describe('returns the name not specified by protocol', function() + it('returns the name not specified by protocol', function() eq("Unknown", exec_lua("return vim.lsp.util._get_symbol_kind_name(nil)")) eq("Unknown", exec_lua("return vim.lsp.util._get_symbol_kind_name(vim.NIL)")) eq("Unknown", exec_lua("return vim.lsp.util._get_symbol_kind_name(1000)")) end) end) + + describe('lsp.util.jump_to_location', function() + local target_bufnr + + before_each(function() + target_bufnr = exec_lua [[ + local bufnr = vim.uri_to_bufnr("file://fake/uri") + local lines = {"1st line of text", "å å ɧ 汉语 ↥ 🤦 🦄"} + vim.api.nvim_buf_set_lines(bufnr, 0, 1, false, lines) + return bufnr + ]] + end) + + local location = function(start_line, start_char, end_line, end_char) + return { + uri = "file://fake/uri", + range = { + start = { line = start_line, character = start_char }, + ["end"] = { line = end_line, character = end_char }, + }, + } + end + + local jump = function(msg) + eq(true, exec_lua('return vim.lsp.util.jump_to_location(...)', msg)) + eq(target_bufnr, exec_lua[[return vim.fn.bufnr('%')]]) + return { + line = exec_lua[[return vim.fn.line('.')]], + col = exec_lua[[return vim.fn.col('.')]], + } + end + + it('jumps to a Location', function() + local pos = jump(location(0, 9, 0, 9)) + eq(1, pos.line) + eq(10, pos.col) + end) + + it('jumps to a LocationLink', function() + local pos = jump({ + targetUri = "file://fake/uri", + targetSelectionRange = { + start = { line = 0, character = 4 }, + ["end"] = { line = 0, character = 4 }, + }, + targetRange = { + start = { line = 1, character = 5 }, + ["end"] = { line = 1, character = 5 }, + }, + }) + eq(1, pos.line) + eq(5, pos.col) + end) + + it('jumps to the correct multibyte column', function() + local pos = jump(location(1, 2, 1, 2)) + eq(2, pos.line) + eq(4, pos.col) + eq('å', exec_lua[[return vim.fn.expand('<cword>')]]) + end) + end) + + describe('lsp.util._make_floating_popup_size', function() + before_each(function() + exec_lua [[ contents = + {"text tαxt txtα tex", + "text tααt tααt text", + "text tαxt tαxt"} + ]] + end) + + it('calculates size correctly', function() + eq({19,3}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents)} ]]) + end) + + it('calculates size correctly with wrapping', function() + eq({15,5}, exec_lua[[ return {vim.lsp.util._make_floating_popup_size(contents,{width = 15, wrap_at = 14})} ]]) + end) + end) end) diff --git a/test/functional/provider/define_spec.lua b/test/functional/provider/define_spec.lua index 51a8831274..1d50ce0a56 100644 --- a/test/functional/provider/define_spec.lua +++ b/test/functional/provider/define_spec.lua @@ -89,6 +89,21 @@ local function command_specs_for(fn, sync, first_arg_factory, init) runx(sync, handler, on_setup) end) + it('with nargs/double-quote', function() + call(fn, args..', {"nargs": "*"}') + local function on_setup() + command('RpcCommand "arg1" "arg2" "arg3"') + end + + local function handler(method, arguments) + eq('test-handler', method) + eq({'"arg1"', '"arg2"', '"arg3"'}, arguments[1]) + return '' + end + + runx(sync, handler, on_setup) + end) + it('with range', function() call(fn,args..', {"range": ""}') local function on_setup() diff --git a/test/unit/eval/helpers.lua b/test/unit/eval/helpers.lua index bcd7c750c5..b600f01ab2 100644 --- a/test/unit/eval/helpers.lua +++ b/test/unit/eval/helpers.lua @@ -136,11 +136,15 @@ local function typvalt2lua_tab_init() return end typvalt2lua_tab = { + [tonumber(eval.VAR_BOOL)] = function(t) + return ({ + [tonumber(eval.kBoolVarFalse)] = false, + [tonumber(eval.kBoolVarTrue)] = true, + })[tonumber(t.vval.v_bool)] + end, [tonumber(eval.VAR_SPECIAL)] = function(t) return ({ - [tonumber(eval.kSpecialVarFalse)] = false, [tonumber(eval.kSpecialVarNull)] = nil_value, - [tonumber(eval.kSpecialVarTrue)] = true, })[tonumber(t.vval.v_special)] end, [tonumber(eval.VAR_NUMBER)] = function(t) @@ -349,8 +353,8 @@ lua2typvalt = function(l, processed) [null_list] = {'VAR_LIST', {v_list=ffi.cast('list_T*', nil)}}, [null_dict] = {'VAR_DICT', {v_dict=ffi.cast('dict_T*', nil)}}, [nil_value] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarNull}}, - [true] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarTrue}}, - [false] = {'VAR_SPECIAL', {v_special=eval.kSpecialVarFalse}}, + [true] = {'VAR_BOOL', {v_bool=eval.kBoolVarTrue}}, + [false] = {'VAR_BOOL', {v_bool=eval.kBoolVarFalse}}, } for k, v in pairs(special_vals) do diff --git a/test/unit/eval/typval_spec.lua b/test/unit/eval/typval_spec.lua index ea86ccbf1c..06465071c5 100644 --- a/test/unit/eval/typval_spec.lua +++ b/test/unit/eval/typval_spec.lua @@ -48,8 +48,7 @@ local lib = cimport('./src/nvim/eval/typval.h', './src/nvim/memory.h', local function vimconv_alloc() return ffi.gc( - ffi.cast('vimconv_T*', lib.xcalloc(1, ffi.sizeof('vimconv_T'))), - function(vc) + ffi.cast('vimconv_T*', lib.xcalloc(1, ffi.sizeof('vimconv_T'))), function(vc) lib.convert_setup(vc, nil, nil) lib.xfree(vc) end) @@ -2838,6 +2837,7 @@ describe('typval.c', function() {lib.VAR_FUNC, 'E729: using Funcref as a String'}, {lib.VAR_LIST, 'E730: using List as a String'}, {lib.VAR_DICT, 'E731: using Dictionary as a String'}, + {lib.VAR_BOOL, nil}, {lib.VAR_SPECIAL, nil}, {lib.VAR_UNKNOWN, 'E908: using an invalid value as a String'}, }) do @@ -2868,8 +2868,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 1}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 0}, {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, }) do -- Using to_cstr, cannot free with tv_clear @@ -2897,8 +2897,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', 0}, {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', 0}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 1}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 0}, {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', 0}, }) do -- Using to_cstr, cannot free with tv_clear @@ -2931,8 +2931,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E745: Using a List as a Number', -1}, {lib.VAR_DICT, {v_dict=NULL}, 'E728: Using a Dictionary as a Number', -1}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 0}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 1}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 0}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 1}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 0}, {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_number(UNKNOWN)', -1}, }) do lib.curwin.w_cursor.lnum = 46 @@ -2961,8 +2961,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E893: Using a List as a Float', 0}, {lib.VAR_DICT, {v_dict=NULL}, 'E894: Using a Dictionary as a Float', 0}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, 'E907: Using a special value as a Float', 0}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, 'E907: Using a special value as a Float', 0}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, 'E907: Using a special value as a Float', 0}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, 'E362: Using a boolean value as a Float', 0}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, 'E362: Using a boolean value as a Float', 0}, {lib.VAR_UNKNOWN, nil, 'E685: Internal error: tv_get_float(UNKNOWN)', 0}, }) do -- Using to_cstr, cannot free with tv_clear @@ -2993,8 +2993,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 'true'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 'false'}, {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, }) do -- Using to_cstr in place of Neovim allocated string, cannot @@ -3005,7 +3005,8 @@ describe('typval.c', function() local ret = v[4] eq(ret, check_emsg(function() local res = lib.tv_get_string(tv) - if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL + or tv.v_type == lib.VAR_BOOL then eq(buf, res) else neq(buf, res) @@ -3036,8 +3037,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 'true'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 'false'}, {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, }) do -- Using to_cstr, cannot free with tv_clear @@ -3047,7 +3048,8 @@ describe('typval.c', function() local ret = v[4] eq(ret, check_emsg(function() local res = lib.tv_get_string_chk(tv) - if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL + or tv.v_type == lib.VAR_BOOL then eq(buf, res) else neq(buf, res) @@ -3077,8 +3079,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', ''}, {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', ''}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 'true'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 'false'}, {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', ''}, }) do -- Using to_cstr, cannot free with tv_clear @@ -3089,7 +3091,8 @@ describe('typval.c', function() eq(ret, check_emsg(function() local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) local res = lib.tv_get_string_buf(tv, buf) - if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL + or tv.v_type == lib.VAR_BOOL then eq(buf, res) else neq(buf, res) @@ -3119,8 +3122,8 @@ describe('typval.c', function() {lib.VAR_LIST, {v_list=NULL}, 'E730: using List as a String', nil}, {lib.VAR_DICT, {v_dict=NULL}, 'E731: using Dictionary as a String', nil}, {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarNull}, nil, 'null'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarTrue}, nil, 'true'}, - {lib.VAR_SPECIAL, {v_special=lib.kSpecialVarFalse}, nil, 'false'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarTrue}, nil, 'true'}, + {lib.VAR_BOOL, {v_bool=lib.kBoolVarFalse}, nil, 'false'}, {lib.VAR_UNKNOWN, nil, 'E908: using an invalid value as a String', nil}, }) do -- Using to_cstr, cannot free with tv_clear @@ -3131,7 +3134,8 @@ describe('typval.c', function() eq(ret, check_emsg(function() local buf = ffi.new('char[?]', lib.NUMBUFLEN, {0}) local res = lib.tv_get_string_buf_chk(tv, buf) - if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL then + if tv.v_type == lib.VAR_NUMBER or tv.v_type == lib.VAR_SPECIAL + or tv.v_type == lib.VAR_BOOL then eq(buf, res) else neq(buf, res) diff --git a/third-party/CMakeLists.txt b/third-party/CMakeLists.txt index 153028bb2b..5d22332fae 100644 --- a/third-party/CMakeLists.txt +++ b/third-party/CMakeLists.txt @@ -135,12 +135,11 @@ include(ExternalProject) if(WIN32) # "nvim" branch of https://github.com/neovim/libuv - set(LIBUV_URL https://github.com/neovim/libuv/archive/d5ff3004d26b9bb863b76247399a9c72a0ff184c.tar.gz) - set(LIBUV_SHA256 0f5dfd92269713ed275273966ed73578fc68db669c509b01210cd58c1cf6361d) + set(LIBUV_URL https://github.com/neovim/libuv/archive/b899d12b0d56d217f31222da83f8c398355b69ef.tar.gz) + set(LIBUV_SHA256 eb7e37b824887e1b31a4e31d1d9bad4c03d8b98532d9cce5f67a3b70495a4b2a) else() - # blueyed/nvim-fixes (for *BSD build fixes). - set(LIBUV_URL https://github.com/blueyed/libuv/archive/2af4cf2.tar.gz) - set(LIBUV_SHA256 SKIP) + set(LIBUV_URL https://github.com/libuv/libuv/archive/v1.34.2.tar.gz) + set(LIBUV_SHA256 0d9d38558b45c006c1ea4e8529bae64caf8becda570295ea74e3696362aeb7f2) endif() set(MSGPACK_URL https://github.com/msgpack/msgpack-c/releases/download/cpp-3.0.0/msgpack-3.0.0.tar.gz) |