diff options
56 files changed, 1141 insertions, 579 deletions
diff --git a/.clang-format b/.clang-format index a31d753217..9f45ac32e2 100644 --- a/.clang-format +++ b/.clang-format @@ -14,7 +14,7 @@ PenaltyReturnTypeOnItsOwnLine: 200 AllowAllParametersOfDeclarationOnNextLine: false AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false -BinPackParameters: false +BinPackParameters: true BreakBeforeBinaryOperators: true BreakBeforeTernaryOperators: true ContinuationIndentWidth: 2 @@ -23,7 +23,7 @@ AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: No AlwaysBreakTemplateDeclarations: No AlignEscapedNewlines: DontAlign -BinPackArguments: false +BinPackArguments: true BraceWrapping: AfterClass: false AfterControlStatement: false diff --git a/.github/workflows/issue-open-check.yml b/.github/workflows/issue-open-check.yml index 2471670dc6..aef1a90c38 100644 --- a/.github/workflows/issue-open-check.yml +++ b/.github/workflows/issue-open-check.yml @@ -17,14 +17,14 @@ jobs: script: | const title = context.payload.issue.title; const titleSplit = title.split(/\s+/).map(e => e.toLowerCase()); - const keywords = ['api', 'treesitter', 'ui', 'lsp', 'doc']; + const keywords = ['api', 'treesitter', 'ui', 'lsp']; var match = new Set(); - for(const keyword of keywords) { - if(titleSplit.includes(keyword)) { + for (const keyword of keywords) { + if (titleSplit.includes(keyword)) { match.add(keyword) } } - if(match.size !== 0){ + if (match.size !== 0) { github.rest.issues.addLabels({ owner: context.repo.owner, repo: context.repo.repo, diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e57d6905f..9b80f81bc8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,10 +146,7 @@ if(CMAKE_C_FLAGS_RELWITHDEBINFO MATCHES DNDEBUG) string(REPLACE " " " " CMAKE_C_FLAGS_RELWITHDEBINFO "${CMAKE_C_FLAGS_RELWITHDEBINFO}") # Remove duplicate whitespace endif() -option(LOG_LIST_ACTIONS "Add list actions logging" OFF) - option(ENABLE_ASAN_UBSAN "Enable Clang address & undefined behavior sanitizer for nvim binary." OFF) -option(LOG_DEBUG "Enable debug log messages even in a release build" OFF) option(ENABLE_MSAN "Enable Clang memory sanitizer for nvim binary." OFF) option(ENABLE_TSAN "Enable Clang thread sanitizer for nvim binary." OFF) diff --git a/cmake.config/config.h.in b/cmake.config/config.h.in index 87b39e8f6f..90916d55bd 100644 --- a/cmake.config/config.h.in +++ b/cmake.config/config.h.in @@ -45,10 +45,6 @@ #cmakedefine HAVE_DIRFD_AND_FLOCK #cmakedefine HAVE_FORKPTY -#ifndef UNIT_TESTING -#cmakedefine LOG_LIST_ACTIONS -#endif - #cmakedefine HAVE_BE64TOH #cmakedefine ORDER_BIG_ENDIAN #define ENDIAN_INCLUDE_FILE <@ENDIAN_INCLUDE_FILE@> diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index f399f1ed25..ed8858820e 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -2614,8 +2614,9 @@ nvim_buf_set_extmark({buffer}, {ns_id}, {line}, {col}, {*opts}) • virt_text_win_col : position the virtual text at a fixed window column (starting from the first text column) • virt_text_hide : hide the virtual text when the background - text is selected or hidden due to horizontal scroll - 'nowrap' + text is selected or hidden because of scrolling with + 'nowrap' or 'smoothscroll'. Currently only affects + "overlay" virt_text. • hl_mode : control how highlights are combined with the highlights of the text. Currently only affects virt_text highlights, but might affect `hl_group` in later versions. diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 98a0801013..581cfd5348 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -619,19 +619,54 @@ to the callback in the "data" table. The token fields are documented in Note: doing anything other than calling |vim.lsp.semantic_tokens.highlight_token()| is considered experimental. -Also the following |User| |autocommand|s are provided: + +LspRequest *LspRequest* + +For each request sent to an LSP server, this event is triggered for every +change to the request's status. The status can be one of `pending`, +`complete`, or `cancel` and is sent as the {type} on the "data" table passed +to the callback function. + +It triggers when the initial request is sent ({type} == `pending`) and when +the LSP server responds ({type} == `complete`). If a cancelation is requested +using `client.cancel_request(request_id)`, then this event will trigger with +{type} == `cancel`. + +When used from Lua, the client ID, request ID, and request are sent in the +"data" table. See {requests} in |vim.lsp.client| for details on the {request} +value. If the request type is `complete`, the request will be deleted from the +client's pending requests table immediately after calling the event's +callbacks. Example: >lua + + vim.api.nvim_create_autocmd('LspRequest', { + callback = function(args) + local bufnr = args.buf + local client_id = args.data.client_id + local request_id = args.data.request_id + local request = args.data.request + if request.type == 'pending' then + -- do something with pending requests + track_pending(client_id, bufnr, request_id, request) + elseif request.type == 'cancel' then + -- do something with pending cancel requests + track_canceling(client_id, bufnr, request_id, request) + elseif request.type == 'complete' then + -- do something with finished requests. this pending + -- request entry is about to be removed since it is complete + track_finish(client_id, bufnr, request_id, request) + end + end, + }) +< + +Also the following |User| |autocommand| is provided: LspProgressUpdate *LspProgressUpdate* Upon receipt of a progress notification from the server. See |vim.lsp.util.get_progress_messages()|. -LspRequest *LspRequest* - After a change to the active set of pending LSP requests. See {requests} - in |vim.lsp.client|. - Example: >vim autocmd User LspProgressUpdate redrawstatus - autocmd User LspRequest redrawstatus < ============================================================================== @@ -764,7 +799,9 @@ client() *vim.lsp.client* server. Entries are key-value pairs with the key being the request ID while the value is a table with `type`, `bufnr`, and `method` key-value pairs. `type` is either "pending" for an active request, or - "cancel" for a cancel request. + "cancel" for a cancel request. It will be "complete" ephemerally while + executing |LspRequest| autocmds when replies are received from the + server. • {config} (table): copy of the table that was passed by the user to |vim.lsp.start_client()|. • {server_capabilities} (table): Response from the server sent on diff --git a/runtime/doc/news.txt b/runtime/doc/news.txt index 795ccc55de..72eb182fa5 100644 --- a/runtime/doc/news.txt +++ b/runtime/doc/news.txt @@ -31,11 +31,15 @@ The following changes may require adaptations in user config or plugins. set keymodel=startsel,stopsel < +• |LspRequest| autocmd was promoted from a |User| autocmd to a first class + citizen. + ============================================================================== ADDED FEATURES *news-added* The following new APIs or features were added. +• Dynamic registration of LSP capabilities. An implication of this change is that checking a client's `server_capabilities` is no longer a sufficient indicator to see if a server supports a feature. Instead use `client.supports_method(<method>)`. It considers both the dynamic capabilities and static `server_capabilities`. • |vim.iter()| provides a generic iterator interface for tables and Lua iterators |luaref-in|. @@ -79,6 +83,9 @@ The following changes to existing APIs or features add new behavior. • The `workspace/didChangeWatchedFiles` LSP client capability is now enabled by default. +• |LspRequest| autocmd callbacks now contain additional information about the LSP + request status update that occurred. + ============================================================================== REMOVED FEATURES *news-removed* diff --git a/runtime/ftplugin.vim b/runtime/ftplugin.vim index feef949dba..d24c0a036e 100644 --- a/runtime/ftplugin.vim +++ b/runtime/ftplugin.vim @@ -28,9 +28,11 @@ augroup filetypeplugin " When there is a dot it is used to separate filetype names. Thus for " "aaa.bbb" load "aaa" and then "bbb". for name in split(s, '\.') - exe 'runtime! ftplugin/' . name . '.vim ftplugin/' . name . '_*.vim ftplugin/' . name . '/*.vim' - " Load lua ftplugins - exe printf('runtime! ftplugin/%s.lua ftplugin/%s_*.lua ftplugin/%s/*.lua', name, name, name) + " Load Lua ftplugins after Vim ftplugins _per directory_ + " TODO(clason): use nvim__get_runtime when supports globs and modeline + exe printf('runtime! ftplugin/%s.vim ftplugin/%s.lua', name, name) + exe printf('runtime! ftplugin/%s_*.vim ftplugin/%s_*.lua', name, name) + exe printf('runtime! ftplugin/%s/*.vim ftplugin/%s/*.lua', name, name) endfor endif endfunc diff --git a/runtime/lua/vim/filetype.lua b/runtime/lua/vim/filetype.lua index 1b04666161..6990de3391 100644 --- a/runtime/lua/vim/filetype.lua +++ b/runtime/lua/vim/filetype.lua @@ -241,6 +241,7 @@ local extension = { copyright = function(path, bufnr) return require('vim.filetype.detect').copyright(bufnr) end, + corn = 'corn', csh = function(path, bufnr) return require('vim.filetype.detect').csh(path, bufnr) end, @@ -1547,6 +1548,7 @@ local filename = { ['man.config'] = 'manconf', ['maxima-init.mac'] = 'maxima', ['meson.build'] = 'meson', + ['meson.options'] = 'meson', ['meson_options.txt'] = 'meson', ['/etc/conf.modules'] = 'modconf', ['/etc/modules'] = 'modconf', diff --git a/runtime/lua/vim/filetype/detect.lua b/runtime/lua/vim/filetype/detect.lua index 94106a3547..70e590ed10 100644 --- a/runtime/lua/vim/filetype/detect.lua +++ b/runtime/lua/vim/filetype/detect.lua @@ -771,14 +771,14 @@ end function M.mod(path, bufnr) if vim.g.filetype_mod then return vim.g.filetype_mod + elseif matchregex(path, [[\c\<go\.mod$]]) then + return 'gomod' elseif is_lprolog(bufnr) then return 'lprolog' elseif matchregex(nextnonblank(bufnr, 1), [[\%(\<MODULE\s\+\w\+\s*;\|^\s*(\*\)]]) then return 'modula2' elseif is_rapid(bufnr) then return 'rapid' - elseif matchregex(path, [[\c\<go\.mod$]]) then - return 'gomod' else -- Nothing recognized, assume modsim3 return 'modsim3' diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 2e6ca7a0ac..d64ed0b5a3 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -50,6 +50,7 @@ lsp._request_name_to_capability = { ['textDocument/codeAction'] = { 'codeActionProvider' }, ['textDocument/codeLens'] = { 'codeLensProvider' }, ['codeLens/resolve'] = { 'codeLensProvider', 'resolveProvider' }, + ['codeAction/resolve'] = { 'codeActionProvider', 'resolveProvider' }, ['workspace/executeCommand'] = { 'executeCommandProvider' }, ['workspace/symbol'] = { 'workspaceSymbolProvider' }, ['textDocument/references'] = { 'referencesProvider' }, @@ -798,7 +799,9 @@ end --- to the server. Entries are key-value pairs with the key --- being the request ID while the value is a table with `type`, --- `bufnr`, and `method` key-value pairs. `type` is either "pending" ---- for an active request, or "cancel" for a cancel request. +--- for an active request, or "cancel" for a cancel request. It will +--- be "complete" ephemerally while executing |LspRequest| autocmds +--- when replies are received from the server. --- --- - {config} (table): copy of the table that was passed by the user --- to |vim.lsp.start_client()|. @@ -886,6 +889,47 @@ function lsp.start(config, opts) return client_id end +---@private +-- Determines whether the given option can be set by `set_defaults`. +local function is_empty_or_default(bufnr, option) + if vim.bo[bufnr][option] == '' then + return true + end + + local info = vim.api.nvim_get_option_info2(option, { buf = bufnr }) + local scriptinfo = vim.tbl_filter(function(e) + return e.sid == info.last_set_sid + end, vim.fn.getscriptinfo()) + + if #scriptinfo ~= 1 then + return false + end + + return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME')) +end + +---@private +---@param client lsp.Client +function lsp._set_defaults(client, bufnr) + if + client.supports_method('textDocument/definition') and is_empty_or_default(bufnr, 'tagfunc') + then + vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' + end + if + client.supports_method('textDocument/completion') and is_empty_or_default(bufnr, 'omnifunc') + then + vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' + end + if + client.supports_method('textDocument/rangeFormatting') + and is_empty_or_default(bufnr, 'formatprg') + and is_empty_or_default(bufnr, 'formatexpr') + then + vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()' + end +end + -- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are -- documented twice: Here, and on the methods themselves (e.g. -- `client.request()`). This is a workaround for the vimdoc generator script @@ -1091,43 +1135,6 @@ function lsp.start_client(config) end ---@private - -- Determines whether the given option can be set by `set_defaults`. - local function is_empty_or_default(bufnr, option) - if vim.bo[bufnr][option] == '' then - return true - end - - local info = vim.api.nvim_get_option_info2(option, { buf = bufnr }) - local scriptinfo = vim.tbl_filter(function(e) - return e.sid == info.last_set_sid - end, vim.fn.getscriptinfo()) - - if #scriptinfo ~= 1 then - return false - end - - return vim.startswith(scriptinfo[1].name, vim.fn.expand('$VIMRUNTIME')) - end - - ---@private - local function set_defaults(client, bufnr) - local capabilities = client.server_capabilities - if capabilities.definitionProvider and is_empty_or_default(bufnr, 'tagfunc') then - vim.bo[bufnr].tagfunc = 'v:lua.vim.lsp.tagfunc' - end - if capabilities.completionProvider and is_empty_or_default(bufnr, 'omnifunc') then - vim.bo[bufnr].omnifunc = 'v:lua.vim.lsp.omnifunc' - end - if - capabilities.documentRangeFormattingProvider - and is_empty_or_default(bufnr, 'formatprg') - and is_empty_or_default(bufnr, 'formatexpr') - then - vim.bo[bufnr].formatexpr = 'v:lua.vim.lsp.formatexpr()' - end - end - - ---@private --- Reset defaults set by `set_defaults`. --- Must only be called if the last client attached to a buffer exits. local function unset_defaults(bufnr) @@ -1228,7 +1235,9 @@ function lsp.start_client(config) requests = {}, -- for $/progress report messages = { name = name, messages = {}, progress = {}, status = {} }, + dynamic_capabilities = require('vim.lsp._dynamic').new(client_id), } + client.config.capabilities = config.capabilities or protocol.make_client_capabilities() -- Store the uninitialized_clients for cleanup in case we exit before initialize finishes. uninitialized_clients[client_id] = client @@ -1291,7 +1300,7 @@ function lsp.start_client(config) -- User provided initialization options. initializationOptions = config.init_options, -- The capabilities provided by the client (editor or tool) - capabilities = config.capabilities or protocol.make_client_capabilities(), + capabilities = config.capabilities, -- The initial trace setting. If omitted trace is disabled ("off"). -- trace = "off" | "messages" | "verbose"; trace = valid_traces[config.trace] or 'off', @@ -1300,6 +1309,26 @@ function lsp.start_client(config) -- TODO(ashkan) handle errors here. pcall(config.before_init, initialize_params, config) end + + --- @param method string + --- @param opts? {bufnr?: number} + client.supports_method = function(method, opts) + opts = opts or {} + local required_capability = lsp._request_name_to_capability[method] + -- if we don't know about the method, assume that the client supports it. + if not required_capability then + return true + end + if vim.tbl_get(client.server_capabilities or {}, unpack(required_capability)) then + return true + else + if client.dynamic_capabilities:supports_registration(method) then + return client.dynamic_capabilities:supports(method, opts) + end + return false + end + end + local _ = log.trace() and log.trace(log_prefix, 'initialize_params', initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) @@ -1314,18 +1343,6 @@ function lsp.start_client(config) client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") client.server_capabilities = protocol.resolve_capabilities(client.server_capabilities) - client.supports_method = function(method) - local required_capability = lsp._request_name_to_capability[method] - -- if we don't know about the method, assume that the client supports it. - if not required_capability then - return true - end - if vim.tbl_get(client.server_capabilities, unpack(required_capability)) then - return true - else - return false - end - end if next(config.settings) then client.notify('workspace/didChangeConfiguration', { settings = config.settings }) @@ -1393,13 +1410,24 @@ function lsp.start_client(config) { method = method, client_id = client_id, bufnr = bufnr, params = params } ) end, function(request_id) + local request = client.requests[request_id] + request.type = 'complete' + nvim_exec_autocmds('LspRequest', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id, request_id = request_id, request = request }, + }) client.requests[request_id] = nil - nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) end) if success and request_id then - client.requests[request_id] = { type = 'pending', bufnr = bufnr, method = method } - nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) + local request = { type = 'pending', bufnr = bufnr, method = method } + client.requests[request_id] = request + nvim_exec_autocmds('LspRequest', { + buffer = bufnr, + modeline = false, + data = { client_id = client_id, request_id = request_id, request = request }, + }) end return success, request_id @@ -1471,7 +1499,11 @@ function lsp.start_client(config) local request = client.requests[id] if request and request.type == 'pending' then request.type = 'cancel' - nvim_exec_autocmds('User', { pattern = 'LspRequest', modeline = false }) + nvim_exec_autocmds('LspRequest', { + buffer = request.bufnr, + modeline = false, + data = { client_id = client_id, request_id = id, request = request }, + }) end return rpc.notify('$/cancelRequest', { id = id }) end @@ -1522,7 +1554,7 @@ function lsp.start_client(config) function client._on_attach(bufnr) text_document_did_open_handler(bufnr, client) - set_defaults(client, bufnr) + lsp._set_defaults(client, bufnr) nvim_exec_autocmds('LspAttach', { buffer = bufnr, @@ -1946,7 +1978,7 @@ function lsp.buf_request(bufnr, method, params, handler) local supported_clients = {} local method_supported = false for_each_buffer_client(bufnr, function(client, client_id) - if client.supports_method(method) then + if client.supports_method(method, { bufnr = bufnr }) then method_supported = true table.insert(supported_clients, client_id) end @@ -2002,7 +2034,7 @@ function lsp.buf_request_all(bufnr, method, params, callback) local set_expected_result_count = once(function() for_each_buffer_client(bufnr, function(client) - if client.supports_method(method) then + if client.supports_method(method, { bufnr = bufnr }) then expected_result_count = expected_result_count + 1 end end) @@ -2243,7 +2275,8 @@ end ---@param client_id (integer) ---@return boolean stopped true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) - return active_clients[client_id] == nil + assert(client_id, 'missing client_id param') + return active_clients[client_id] == nil and not uninitialized_clients[client_id] end --- Gets a map of client_id:client pairs for the given buffer, where each value diff --git a/runtime/lua/vim/lsp/_dynamic.lua b/runtime/lua/vim/lsp/_dynamic.lua new file mode 100644 index 0000000000..04040e8e28 --- /dev/null +++ b/runtime/lua/vim/lsp/_dynamic.lua @@ -0,0 +1,109 @@ +local wf = require('vim.lsp._watchfiles') + +--- @class lsp.DynamicCapabilities +--- @field capabilities table<string, lsp.Registration[]> +--- @field client_id number +local M = {} + +--- @param client_id number +function M.new(client_id) + return setmetatable({ + capabilities = {}, + client_id = client_id, + }, { __index = M }) +end + +function M:supports_registration(method) + local client = vim.lsp.get_client_by_id(self.client_id) + if not client then + return false + end + local capability = vim.tbl_get(client.config.capabilities, unpack(vim.split(method, '/'))) + return type(capability) == 'table' and capability.dynamicRegistration +end + +--- @param registrations lsp.Registration[] +--- @private +function M:register(registrations) + -- remove duplicates + self:unregister(registrations) + for _, reg in ipairs(registrations) do + local method = reg.method + if not self.capabilities[method] then + self.capabilities[method] = {} + end + table.insert(self.capabilities[method], reg) + end +end + +--- @param unregisterations lsp.Unregistration[] +--- @private +function M:unregister(unregisterations) + for _, unreg in ipairs(unregisterations) do + local method = unreg.method + if not self.capabilities[method] then + return + end + local id = unreg.id + for i, reg in ipairs(self.capabilities[method]) do + if reg.id == id then + table.remove(self.capabilities[method], i) + break + end + end + end +end + +--- @param method string +--- @param opts? {bufnr?: number} +--- @return lsp.Registration? (table|nil) the registration if found +--- @private +function M:get(method, opts) + opts = opts or {} + opts.bufnr = opts.bufnr or vim.api.nvim_get_current_buf() + for _, reg in ipairs(self.capabilities[method] or {}) do + if not reg.registerOptions then + return reg + end + local documentSelector = reg.registerOptions.documentSelector + if not documentSelector then + return reg + end + if M.match(opts.bufnr, documentSelector) then + return reg + end + end +end + +--- @param method string +--- @param opts? {bufnr?: number} +--- @private +function M:supports(method, opts) + return self:get(method, opts) ~= nil +end + +--- @param bufnr number +--- @param documentSelector lsp.DocumentSelector +--- @private +function M.match(bufnr, documentSelector) + local ft = vim.bo[bufnr].filetype + local uri = vim.uri_from_bufnr(bufnr) + local fname = vim.uri_to_fname(uri) + for _, filter in ipairs(documentSelector) do + local matches = true + if filter.language and ft ~= filter.language then + matches = false + end + if matches and filter.scheme and not vim.startswith(uri, filter.scheme .. ':') then + matches = false + end + if matches and filter.pattern and not wf._match(filter.pattern, fname) then + matches = false + end + if matches then + return true + end + end +end + +return M diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index a307dea673..bb3ca0e6d6 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -681,13 +681,16 @@ local function on_code_action_results(results, ctx, options) -- command: string -- arguments?: any[] -- + ---@type lsp.Client local client = vim.lsp.get_client_by_id(action_tuple[1]) local action = action_tuple[2] - if - not action.edit - and client - and vim.tbl_get(client.server_capabilities, 'codeActionProvider', 'resolveProvider') - then + + local reg = client.dynamic_capabilities:get('textDocument/codeAction', { bufnr = ctx.bufnr }) + + local supports_resolve = vim.tbl_get(reg or {}, 'registerOptions', 'resolveProvider') + or client.supports_method('codeAction/resolve') + + if not action.edit and client and supports_resolve then client.request('codeAction/resolve', action, function(err, resolved_action) if err then vim.notify(err.code .. ': ' .. err.message, vim.log.levels.ERROR) diff --git a/runtime/lua/vim/lsp/handlers.lua b/runtime/lua/vim/lsp/handlers.lua index 8e926c4644..5346160871 100644 --- a/runtime/lua/vim/lsp/handlers.lua +++ b/runtime/lua/vim/lsp/handlers.lua @@ -118,22 +118,30 @@ end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_registerCapability M['client/registerCapability'] = function(_, result, ctx) - local log_unsupported = false + local client_id = ctx.client_id + ---@type lsp.Client + local client = vim.lsp.get_client_by_id(client_id) + + client.dynamic_capabilities:register(result.registrations) + for bufnr, _ in ipairs(client.attached_buffers) do + vim.lsp._set_defaults(client, bufnr) + end + + ---@type string[] + local unsupported = {} for _, reg in ipairs(result.registrations) do if reg.method == 'workspace/didChangeWatchedFiles' then require('vim.lsp._watchfiles').register(reg, ctx) - else - log_unsupported = true + elseif not client.dynamic_capabilities:supports_registration(reg.method) then + unsupported[#unsupported + 1] = reg.method end end - if log_unsupported then - local client_id = ctx.client_id + if #unsupported > 0 then local warning_tpl = 'The language server %s triggers a registerCapability ' - .. 'handler despite dynamicRegistration set to false. ' + .. 'handler for %s despite dynamicRegistration set to false. ' .. 'Report upstream, this warning is harmless' - local client = vim.lsp.get_client_by_id(client_id) local client_name = client and client.name or string.format('id=%d', client_id) - local warning = string.format(warning_tpl, client_name) + local warning = string.format(warning_tpl, client_name, table.concat(unsupported, ', ')) log.warn(warning) end return vim.NIL @@ -141,6 +149,10 @@ end --see: https://microsoft.github.io/language-server-protocol/specifications/specification-current/#client_unregisterCapability M['client/unregisterCapability'] = function(_, result, ctx) + local client_id = ctx.client_id + local client = vim.lsp.get_client_by_id(client_id) + client.dynamic_capabilities:unregister(result.unregisterations) + for _, unreg in ipairs(result.unregisterations) do if unreg.method == 'workspace/didChangeWatchedFiles' then require('vim.lsp._watchfiles').unregister(unreg, ctx) diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index a7919f12f5..a28ff407b7 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -697,7 +697,7 @@ function protocol.make_client_capabilities() didSave = true, }, codeAction = { - dynamicRegistration = false, + dynamicRegistration = true, codeActionLiteralSupport = { codeActionKind = { @@ -714,6 +714,12 @@ function protocol.make_client_capabilities() properties = { 'edit' }, }, }, + formatting = { + dynamicRegistration = true, + }, + rangeFormatting = { + dynamicRegistration = true, + }, completion = { dynamicRegistration = false, completionItem = { @@ -747,6 +753,7 @@ function protocol.make_client_capabilities() }, definition = { linkSupport = true, + dynamicRegistration = true, }, implementation = { linkSupport = true, @@ -755,7 +762,7 @@ function protocol.make_client_capabilities() linkSupport = true, }, hover = { - dynamicRegistration = false, + dynamicRegistration = true, contentFormat = { protocol.MarkupKind.Markdown, protocol.MarkupKind.PlainText }, }, signatureHelp = { @@ -790,7 +797,7 @@ function protocol.make_client_capabilities() hierarchicalDocumentSymbolSupport = true, }, rename = { - dynamicRegistration = false, + dynamicRegistration = true, prepareSupport = true, }, publishDiagnostics = { diff --git a/runtime/lua/vim/lsp/types.lua b/runtime/lua/vim/lsp/types.lua index 779f313aa7..e77e1fb63a 100644 --- a/runtime/lua/vim/lsp/types.lua +++ b/runtime/lua/vim/lsp/types.lua @@ -35,3 +35,31 @@ ---@field source string ---@field tags? lsp.DiagnosticTag[] ---@field relatedInformation DiagnosticRelatedInformation[] + +--- @class lsp.DocumentFilter +--- @field language? string +--- @field scheme? string +--- @field pattern? string + +--- @alias lsp.DocumentSelector lsp.DocumentFilter[] + +--- @alias lsp.RegisterOptions any | lsp.StaticRegistrationOptions | lsp.TextDocumentRegistrationOptions + +--- @class lsp.Registration +--- @field id string +--- @field method string +--- @field registerOptions? lsp.RegisterOptions + +--- @alias lsp.RegistrationParams {registrations: lsp.Registration[]} + +--- @class lsp.StaticRegistrationOptions +--- @field id? string + +--- @class lsp.TextDocumentRegistrationOptions +--- @field documentSelector? lsp.DocumentSelector + +--- @class lsp.Unregistration +--- @field id string +--- @field method string + +--- @alias lsp.UnregistrationParams {unregisterations: lsp.Unregistration[]} diff --git a/scripts/gen_help_html.lua b/scripts/gen_help_html.lua index 1bddd3aa8b..96289c45ec 100644 --- a/scripts/gen_help_html.lua +++ b/scripts/gen_help_html.lua @@ -696,6 +696,11 @@ local function gen_one(fname, to_fname, old, commit) <link href="/css/main.css" rel="stylesheet"> <link href="help.css" rel="stylesheet"> <link href="/highlight/styles/neovim.min.css" rel="stylesheet"> + + <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ --> + <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@docsearch/css@3" /> + <link rel="preconnect" href="https://X185E15FPG-dsn.algolia.net" crossorigin /> + <script src="/highlight/highlight.min.js"></script> <script>hljs.highlightAll();</script> <title>%s - Neovim docs</title> @@ -766,12 +771,13 @@ local function gen_one(fname, to_fname, old, commit) main = ([[ <header class="container"> <nav class="navbar navbar-expand-lg"> - <div> + <div class="container-fluid"> <a href="/" class="navbar-brand" aria-label="logo"> <!--TODO: use <img src="….svg"> here instead. Need one that has green lettering instead of gray. --> %s <!--<img src="https://neovim.io/logos/neovim-logo.svg" width="173" height="50" alt="Neovim" />--> </a> + <div id="docsearch"></div> <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ --> </div> </nav> </header> @@ -825,6 +831,18 @@ local function gen_one(fname, to_fname, old, commit) parse_errors: %d %s | <span title="%s">noise_lines: %d</span> </div> <div> + + <!-- algolia docsearch https://docsearch.algolia.com/docs/docsearch-v3/ --> + <script src="https://cdn.jsdelivr.net/npm/@docsearch/js@3"></script> + <script type="module"> + docsearch({ + container: '#docsearch', + appId: 'X185E15FPG', + apiKey: 'b5e6b2f9c636b2b471303205e59832ed', + indexName: 'nvim', + }); + </script> + </footer> ]]):format( os.date('%Y-%m-%d %H:%M'), commit, commit:sub(1, 7), #stats.parse_errors, bug_link, diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index 47f32abc07..222b283a5d 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -394,9 +394,6 @@ endif() if($ENV{CI}) # Don't debug log on CI, it gets too verbose in the main build log. # TODO(bfredl): debug log level also exposes some errors with EXITFREE in ASAN build. - set(LOG_DEBUG FALSE) -elseif(LOG_DEBUG) - target_compile_definitions(nvim PRIVATE NVIM_LOG_DEBUG) else() # Minimize logging for release-type builds. target_compile_definitions(nvim PRIVATE $<$<CONFIG:Debug>:NVIM_LOG_DEBUG>) diff --git a/src/nvim/api/extmark.c b/src/nvim/api/extmark.c index 299413e510..aca290494b 100644 --- a/src/nvim/api/extmark.c +++ b/src/nvim/api/extmark.c @@ -483,8 +483,9 @@ Array nvim_buf_get_extmarks(Buffer buffer, Integer ns_id, Object start, Object e /// window column (starting from the first /// text column) /// - virt_text_hide : hide the virtual text when the background -/// text is selected or hidden due to -/// horizontal scroll 'nowrap' +/// text is selected or hidden because of +/// scrolling with 'nowrap' or 'smoothscroll'. +/// Currently only affects "overlay" virt_text. /// - hl_mode : control how highlights are combined with the /// highlights of the text. Currently only affects /// virt_text highlights, but might affect `hl_group` diff --git a/src/nvim/auevents.lua b/src/nvim/auevents.lua index aef08be820..048b8d6631 100644 --- a/src/nvim/auevents.lua +++ b/src/nvim/auevents.lua @@ -72,6 +72,7 @@ return { 'InsertLeavePre', -- just before leaving Insert mode 'LspAttach', -- after an LSP client attaches to a buffer 'LspDetach', -- after an LSP client detaches from a buffer + 'LspRequest', -- after an LSP request is started, canceled, or completed 'LspTokenUpdate', -- after a visible LSP token is updated 'MenuPopup', -- just before popup menu is displayed 'ModeChanged', -- after changing the mode @@ -152,6 +153,7 @@ return { DiagnosticChanged=true, LspAttach=true, LspDetach=true, + LspRequest=true, LspTokenUpdate=true, RecordingEnter=true, RecordingLeave=true, diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index dab07487cd..bc52ab0771 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -248,6 +248,11 @@ int open_buffer(int read_stdin, exarg_T *eap, int flags_arg) return FAIL; } + // Do not sync this buffer yet, may first want to read the file. + if (curbuf->b_ml.ml_mfp != NULL) { + curbuf->b_ml.ml_mfp->mf_dirty = MF_DIRTY_YES_NOSYNC; + } + // The autocommands in readfile() may change the buffer, but only AFTER // reading the file. set_bufref(&old_curbuf, curbuf); @@ -316,6 +321,12 @@ int open_buffer(int read_stdin, exarg_T *eap, int flags_arg) } } + // Can now sync this buffer in ml_sync_all(). + if (curbuf->b_ml.ml_mfp != NULL + && curbuf->b_ml.ml_mfp->mf_dirty == MF_DIRTY_YES_NOSYNC) { + curbuf->b_ml.ml_mfp->mf_dirty = MF_DIRTY_YES; + } + // if first time loading this buffer, init b_chartab[] if (curbuf->b_flags & BF_NEVERLOADED) { (void)buf_init_chartab(curbuf, false); diff --git a/src/nvim/decoration.c b/src/nvim/decoration.c index 81e1cb617c..2027848ccf 100644 --- a/src/nvim/decoration.c +++ b/src/nvim/decoration.c @@ -433,7 +433,9 @@ void decor_redraw_signs(buf_T *buf, int row, int *num_signs, SignTextAttrs sattr if (sattrs[j - 1].priority >= decor->priority) { break; } - sattrs[j] = sattrs[j - 1]; + if (j < SIGN_SHOW_MAX) { + sattrs[j] = sattrs[j - 1]; + } } if (j < SIGN_SHOW_MAX) { sattrs[j] = (SignTextAttrs) { @@ -603,7 +605,7 @@ int decor_virt_lines(win_T *wp, linenr_T lnum, VirtLines *lines, TriState has_fo mtkey_t mark = marktree_itr_current(itr); if (mark.pos.row < 0 || mark.pos.row >= end_row) { break; - } else if (marktree_decor_level(mark) < kDecorLevelVirtLine) { + } else if (mt_end(mark) || marktree_decor_level(mark) < kDecorLevelVirtLine) { goto next_mark; } bool above = mark.pos.row > (lnum - 2); diff --git a/src/nvim/drawline.c b/src/nvim/drawline.c index 557e58ff27..ced7e46287 100644 --- a/src/nvim/drawline.c +++ b/src/nvim/drawline.c @@ -1715,8 +1715,8 @@ int win_line(win_T *wp, linenr_T lnum, int startrow, int endrow, bool number_onl sign_idx = 0; wlv.draw_state = WL_LINE; if (has_decor && wlv.row == startrow + wlv.filler_lines) { - // hide virt_text on text hidden by 'nowrap' - decor_redraw_col(wp, (colnr_T)(ptr - line), wlv.off, true, &decor_state); + // hide virt_text on text hidden by 'nowrap' or 'smoothscroll' + decor_redraw_col(wp, (colnr_T)(ptr - line) - 1, wlv.off, true, &decor_state); } win_line_continue(&wlv); // use wlv.saved_ values } diff --git a/src/nvim/eval/typval.c b/src/nvim/eval/typval.c index cb8f8ce44d..42e9dc8f03 100644 --- a/src/nvim/eval/typval.c +++ b/src/nvim/eval/typval.c @@ -82,70 +82,6 @@ bool tv_in_free_unref_items = false; const char *const tv_empty_string = ""; //{{{1 Lists -//{{{2 List log -#ifdef LOG_LIST_ACTIONS -ListLog *list_log_first = NULL; -ListLog *list_log_last = NULL; - -/// Write list log to the given file -/// -/// @param[in] fname File to write log to. Will be appended to if already -/// present. -void list_write_log(const char *const fname) - FUNC_ATTR_NONNULL_ALL -{ - FileDescriptor fp; - const int fo_ret = file_open(&fp, fname, kFileCreate|kFileAppend, 0600); - if (fo_ret != 0) { - semsg(_("E5142: Failed to open file %s: %s"), fname, os_strerror(fo_ret)); - return; - } - for (ListLog *chunk = list_log_first; chunk != NULL;) { - for (size_t i = 0; i < chunk->size; i++) { - char buf[10 + 1 + ((16 + 3) * 3) + (8 + 2) + 2]; - // act : hex " c:" len "[]" "\n\0" - const ListLogEntry entry = chunk->entries[i]; - const size_t snp_len = (size_t)snprintf(buf, sizeof(buf), - "%-10.10s: l:%016" PRIxPTR "[%08d] 1:%016" PRIxPTR " 2:%016" PRIxPTR - "\n", - entry.action, entry.l, entry.len, entry.li1, - entry.li2); - assert(snp_len + 1 == sizeof(buf)); - const ptrdiff_t fw_ret = file_write(&fp, buf, snp_len); - if (fw_ret != (ptrdiff_t)snp_len) { - assert(fw_ret < 0); - if (i) { - memmove(chunk->entries, chunk->entries + i, - sizeof(chunk->entries[0]) * (chunk->size - i)); - chunk->size -= i; - } - semsg(_("E5143: Failed to write to file %s: %s"), - fname, os_strerror((int)fw_ret)); - return; - } - } - list_log_first = chunk->next; - xfree(chunk); - chunk = list_log_first; - } - const int fc_ret = file_close(&fp, true); - if (fc_ret != 0) { - semsg(_("E5144: Failed to close file %s: %s"), fname, os_strerror(fc_ret)); - } -} - -# ifdef EXITFREE -/// Free list log -void list_free_log(void) -{ - for (ListLog *chunk = list_log_first; chunk != NULL;) { - list_log_first = chunk->next; - xfree(chunk); - chunk = list_log_first; - } -} -# endif -#endif //{{{2 List item /// Allocate a list item @@ -252,7 +188,6 @@ list_T *tv_list_alloc(const ptrdiff_t len) list->lv_used_prev = NULL; list->lv_used_next = gc_first_list; gc_first_list = list; - list_log(list, NULL, (void *)(uintptr_t)len, "alloc"); list->lua_table_ref = LUA_NOREF; return list; } @@ -283,8 +218,6 @@ void tv_list_init_static10(staticList10_T *const sl) li->li_prev = li - 1; li->li_next = li + 1; } - list_log((const list_T *)sl, &sl->sl_items[0], &sl->sl_items[SL_SIZE - 1], - "s10init"); #undef SL_SIZE } @@ -296,7 +229,6 @@ void tv_list_init_static(list_T *const l) { CLEAR_POINTER(l); l->lv_refcount = DO_NOT_FREE_CNT; - list_log(l, NULL, NULL, "sinit"); } /// Free items contained in a list @@ -305,7 +237,6 @@ void tv_list_init_static(list_T *const l) void tv_list_free_contents(list_T *const l) FUNC_ATTR_NONNULL_ALL { - list_log(l, NULL, NULL, "freecont"); for (listitem_T *item = l->lv_first; item != NULL; item = l->lv_first) { // Remove the item before deleting it. l->lv_first = item->li_next; @@ -335,7 +266,6 @@ void tv_list_free_list(list_T *const l) if (l->lv_used_next != NULL) { l->lv_used_next->lv_used_prev = l->lv_used_prev; } - list_log(l, NULL, NULL, "freelist"); NLUA_CLEAR_REF(l->lua_table_ref); xfree(l); @@ -382,7 +312,6 @@ void tv_list_unref(list_T *const l) void tv_list_drop_items(list_T *const l, listitem_T *const item, listitem_T *const item2) FUNC_ATTR_NONNULL_ALL { - list_log(l, item, item2, "drop"); // Notify watchers. for (listitem_T *ip = item; ip != item2->li_next; ip = ip->li_next) { l->lv_len--; @@ -400,14 +329,12 @@ void tv_list_drop_items(list_T *const l, listitem_T *const item, listitem_T *con item->li_prev->li_next = item2->li_next; } l->lv_idx_item = NULL; - list_log(l, l->lv_first, l->lv_last, "afterdrop"); } /// Like tv_list_drop_items, but also frees all removed items void tv_list_remove_items(list_T *const l, listitem_T *const item, listitem_T *const item2) FUNC_ATTR_NONNULL_ALL { - list_log(l, item, item2, "remove"); tv_list_drop_items(l, item, item2); for (listitem_T *li = item;;) { tv_clear(TV_LIST_ITEM_TV(li)); @@ -431,7 +358,6 @@ void tv_list_move_items(list_T *const l, listitem_T *const item, listitem_T *con list_T *const tgt_l, const int cnt) FUNC_ATTR_NONNULL_ALL { - list_log(l, item, item2, "move"); tv_list_drop_items(l, item, item2); item->li_prev = tgt_l->lv_last; item2->li_next = NULL; @@ -442,7 +368,6 @@ void tv_list_move_items(list_T *const l, listitem_T *const item, listitem_T *con } tgt_l->lv_last = item2; tgt_l->lv_len += cnt; - list_log(tgt_l, tgt_l->lv_first, tgt_l->lv_last, "movetgt"); } /// Insert list item @@ -470,7 +395,6 @@ void tv_list_insert(list_T *const l, listitem_T *const ni, listitem_T *const ite } item->li_prev = ni; l->lv_len++; - list_log(l, ni, item, "insert"); } } @@ -496,7 +420,6 @@ void tv_list_insert_tv(list_T *const l, typval_T *const tv, listitem_T *const it void tv_list_append(list_T *const l, listitem_T *const item) FUNC_ATTR_NONNULL_ALL { - list_log(l, item, NULL, "append"); if (l->lv_last == NULL) { // empty list l->lv_first = item; @@ -1416,7 +1339,6 @@ void tv_list_reverse(list_T *const l) if (tv_list_len(l) <= 1) { return; } - list_log(l, NULL, NULL, "reverse"); #define SWAP(a, b) \ do { \ tmp = (a); \ @@ -1454,7 +1376,6 @@ void tv_list_item_sort(list_T *const l, ListSortItem *const ptrs, if (len <= 1) { return; } - list_log(l, NULL, NULL, "sort"); int i = 0; TV_LIST_ITER(l, li, { ptrs[i].item = li; @@ -1543,7 +1464,6 @@ listitem_T *tv_list_find(list_T *const l, int n) // Cache the used index. l->lv_idx = idx; l->lv_idx_item = item; - list_log(l, l->lv_idx_item, (void *)(uintptr_t)l->lv_idx, "find"); return item; } diff --git a/src/nvim/eval/typval.h b/src/nvim/eval/typval.h index 767fd706b3..e7b2499346 100644 --- a/src/nvim/eval/typval.h +++ b/src/nvim/eval/typval.h @@ -18,74 +18,6 @@ #include "nvim/message.h" #include "nvim/types.h" -#ifdef LOG_LIST_ACTIONS -# include "nvim/memory.h" - -extern ListLog *list_log_first; ///< First list log chunk, NULL if missing -extern ListLog *list_log_last; ///< Last list log chunk - -static inline ListLog *list_log_alloc(const size_t size) - REAL_FATTR_ALWAYS_INLINE REAL_FATTR_WARN_UNUSED_RESULT; - -/// Allocate a new log chunk and update globals -/// -/// @param[in] size Number of entries in a new chunk. -/// -/// @return [allocated] Newly allocated chunk. -static inline ListLog *list_log_new(const size_t size) -{ - ListLog *ret = xmalloc(offsetof(ListLog, entries) - + size * sizeof(ret->entries[0])); - ret->size = 0; - ret->capacity = size; - ret->next = NULL; - if (list_log_first == NULL) { - list_log_first = ret; - } else { - list_log_last->next = ret; - } - list_log_last = ret; - return ret; -} - -static inline void list_log(const list_T *const l, const listitem_T *const li1, - const listitem_T *const li2, const char *const action) - REAL_FATTR_ALWAYS_INLINE; - -/// Add new entry to log -/// -/// If last chunk was filled it uses twice as much memory to allocate the next -/// chunk. -/// -/// @param[in] l List to which entry belongs. -/// @param[in] li1 List item 1. -/// @param[in] li2 List item 2, often used for integers and not list items. -/// @param[in] action Logged action. -static inline void list_log(const list_T *const l, const listitem_T *const li1, - const listitem_T *const li2, const char *const action) -{ - ListLog *tgt; - if (list_log_first == NULL) { - tgt = list_log_new(128); - } else if (list_log_last->size == list_log_last->capacity) { - tgt = list_log_new(list_log_last->capacity * 2); - } else { - tgt = list_log_last; - } - tgt->entries[tgt->size++] = (ListLogEntry) { - .l = (uintptr_t)l, - .li1 = (uintptr_t)li1, - .li2 = (uintptr_t)li2, - .len = (l == NULL ? 0 : l->lv_len), - .action = action, - }; -} -#else -# define list_log(...) -# define list_write_log(...) -# define list_free_log() -#endif - // In a hashtab item "hi_key" points to "di_key" in a dictitem. // This avoids adding a pointer to the hashtab item. @@ -174,7 +106,6 @@ static inline int tv_list_len(const list_T *l) /// @param[in] l List to check. static inline int tv_list_len(const list_T *const l) { - list_log(l, NULL, NULL, "len"); if (l == NULL) { return 0; } @@ -258,10 +189,8 @@ static inline listitem_T *tv_list_first(const list_T *l) static inline listitem_T *tv_list_first(const list_T *const l) { if (l == NULL) { - list_log(l, NULL, NULL, "first"); return NULL; } - list_log(l, l->lv_first, NULL, "first"); return l->lv_first; } @@ -276,10 +205,8 @@ static inline listitem_T *tv_list_last(const list_T *l) static inline listitem_T *tv_list_last(const list_T *const l) { if (l == NULL) { - list_log(l, NULL, NULL, "last"); return NULL; } - list_log(l, l->lv_last, NULL, "last"); return l->lv_last; } @@ -416,7 +343,6 @@ extern bool tv_in_free_unref_items; #define _TV_LIST_ITER_MOD(modifier, l, li, code) \ do { \ modifier list_T *const l_ = (l); \ - list_log(l_, NULL, NULL, "iter" #modifier); \ if (l_ != NULL) { \ for (modifier listitem_T *li = l_->lv_first; \ li != NULL; li = li->li_next) { \ diff --git a/src/nvim/eval/typval_defs.h b/src/nvim/eval/typval_defs.h index 3e49417f6f..767603ac0e 100644 --- a/src/nvim/eval/typval_defs.h +++ b/src/nvim/eval/typval_defs.h @@ -375,25 +375,4 @@ typedef struct { typedef int (*ListSorter)(const void *, const void *); -#ifdef LOG_LIST_ACTIONS -/// List actions log entry -typedef struct { - uintptr_t l; ///< List log entry belongs to. - uintptr_t li1; ///< First list item log entry belongs to, if applicable. - uintptr_t li2; ///< Second list item log entry belongs to, if applicable. - int len; ///< List length when log entry was created. - const char *action; ///< Logged action. -} ListLogEntry; - -typedef struct list_log ListLog; - -/// List actions log -struct list_log { - ListLog *next; ///< Next chunk or NULL. - size_t capacity; ///< Number of entries in current chunk. - size_t size; ///< Current chunk size. - ListLogEntry entries[]; ///< Actual log entries. -}; -#endif - #endif // NVIM_EVAL_TYPVAL_DEFS_H diff --git a/src/nvim/eval/vars.c b/src/nvim/eval/vars.c index 64177d13e8..21b25b64f4 100644 --- a/src/nvim/eval/vars.c +++ b/src/nvim/eval/vars.c @@ -701,6 +701,189 @@ static const char *list_arg_vars(exarg_T *eap, const char *arg, int *first) return arg; } +/// Set an environment variable, part of ex_let_one(). +static char *ex_let_env(char *arg, typval_T *const tv, const bool is_const, + const char *const endchars, const char *const op) + FUNC_ATTR_NONNULL_ARG(1, 2) FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (is_const) { + emsg(_("E996: Cannot lock an environment variable")); + return NULL; + } + + // Find the end of the name. + char *arg_end = NULL; + arg++; + char *name = arg; + int len = get_env_len((const char **)&arg); + if (len == 0) { + semsg(_(e_invarg2), name - 1); + } else { + if (op != NULL && vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) { + semsg(_(e_letwrong), op); + } else if (endchars != NULL + && vim_strchr(endchars, (uint8_t)(*skipwhite(arg))) == NULL) { + emsg(_(e_letunexp)); + } else if (!check_secure()) { + char *tofree = NULL; + const char c1 = name[len]; + name[len] = NUL; + const char *p = tv_get_string_chk(tv); + if (p != NULL && op != NULL && *op == '.') { + char *s = vim_getenv(name); + if (s != NULL) { + tofree = concat_str(s, p); + p = tofree; + xfree(s); + } + } + if (p != NULL) { + vim_setenv_ext(name, p); + arg_end = arg; + } + name[len] = c1; + xfree(tofree); + } + } + return arg_end; +} + +/// Set an option, part of ex_let_one(). +static char *ex_let_option(char *arg, typval_T *const tv, const bool is_const, + const char *const endchars, const char *const op) + FUNC_ATTR_NONNULL_ARG(1, 2) FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (is_const) { + emsg(_("E996: Cannot lock an option")); + return NULL; + } + + // Find the end of the name. + char *arg_end = NULL; + int scope; + char *const p = (char *)find_option_end((const char **)&arg, &scope); + if (p == NULL + || (endchars != NULL + && vim_strchr(endchars, (uint8_t)(*skipwhite(p))) == NULL)) { + emsg(_(e_letunexp)); + } else { + varnumber_T n = 0; + getoption_T opt_type; + long numval; + char *stringval = NULL; + const char *s = NULL; + bool failed = false; + uint32_t opt_p_flags; + char *tofree = NULL; + + const char c1 = *p; + *p = NUL; + + opt_type = get_option_value(arg, &numval, &stringval, &opt_p_flags, scope); + if (opt_type == gov_bool + || opt_type == gov_number + || opt_type == gov_hidden_bool + || opt_type == gov_hidden_number) { + // number, possibly hidden + n = (long)tv_get_number(tv); + } + + if ((opt_p_flags & P_FUNC) && tv_is_func(*tv)) { + // If the option can be set to a function reference or a lambda + // and the passed value is a function reference, then convert it to + // the name (string) of the function reference. + s = tofree = encode_tv2string(tv, NULL); + } else if (tv->v_type != VAR_BOOL && tv->v_type != VAR_SPECIAL) { + // Avoid setting a string option to the text "v:false" or similar. + s = tv_get_string_chk(tv); + } + + if (op != NULL && *op != '=') { + if (((opt_type == gov_bool || opt_type == gov_number) && *op == '.') + || (opt_type == gov_string && *op != '.')) { + semsg(_(e_letwrong), op); + failed = true; // don't set the value + } else { + // number or bool + if (opt_type == gov_number || opt_type == gov_bool) { + switch (*op) { + case '+': + n = numval + n; break; + case '-': + n = numval - n; break; + case '*': + n = numval * n; break; + case '/': + n = num_divide(numval, n); break; + case '%': + n = num_modulus(numval, n); break; + } + s = NULL; + } else if (opt_type == gov_string && stringval != NULL && s != NULL) { + // string + char *const oldstringval = stringval; + stringval = concat_str(stringval, s); + xfree(oldstringval); + s = stringval; + } + } + } + + if (!failed) { + if (opt_type != gov_string || s != NULL) { + const char *err = set_option_value(arg, (long)n, s, scope); + arg_end = p; + if (err != NULL) { + emsg(_(err)); + } + } else { + emsg(_(e_stringreq)); + } + } + *p = c1; + xfree(stringval); + xfree(tofree); + } + return arg_end; +} + +/// Set a register, part of ex_let_one(). +static char *ex_let_register(char *arg, typval_T *const tv, const bool is_const, + const char *const endchars, const char *const op) + FUNC_ATTR_NONNULL_ARG(1, 2) FUNC_ATTR_WARN_UNUSED_RESULT +{ + if (is_const) { + emsg(_("E996: Cannot lock a register")); + return NULL; + } + + char *arg_end = NULL; + arg++; + if (op != NULL && vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) { + semsg(_(e_letwrong), op); + } else if (endchars != NULL + && vim_strchr(endchars, (uint8_t)(*skipwhite(arg + 1))) == NULL) { + emsg(_(e_letunexp)); + } else { + char *ptofree = NULL; + const char *p = tv_get_string_chk(tv); + if (p != NULL && op != NULL && *op == '.') { + char *s = get_reg_contents(*arg == '@' ? '"' : *arg, kGRegExprSrc); + if (s != NULL) { + ptofree = concat_str(s, p); + p = ptofree; + xfree(s); + } + } + if (p != NULL) { + write_reg_contents(*arg == '@' ? '"' : *arg, p, (ssize_t)strlen(p), false); + arg_end = arg + 1; + } + xfree(ptofree); + } + return arg_end; +} + /// Set one item of `:let var = expr` or `:let [v1, v2] = list` to its value /// /// @param[in] arg Start of the variable name. @@ -718,172 +901,21 @@ static char *ex_let_one(char *arg, typval_T *const tv, const bool copy, const bo { char *arg_end = NULL; - // ":let $VAR = expr": Set environment variable. if (*arg == '$') { - if (is_const) { - emsg(_("E996: Cannot lock an environment variable")); - return NULL; - } - // Find the end of the name. - arg++; - char *name = arg; - int len = get_env_len((const char **)&arg); - if (len == 0) { - semsg(_(e_invarg2), name - 1); - } else { - if (op != NULL && vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) { - semsg(_(e_letwrong), op); - } else if (endchars != NULL - && vim_strchr(endchars, (uint8_t)(*skipwhite(arg))) == NULL) { - emsg(_(e_letunexp)); - } else if (!check_secure()) { - char *tofree = NULL; - const char c1 = name[len]; - name[len] = NUL; - const char *p = tv_get_string_chk(tv); - if (p != NULL && op != NULL && *op == '.') { - char *s = vim_getenv(name); - if (s != NULL) { - tofree = concat_str(s, p); - p = tofree; - xfree(s); - } - } - if (p != NULL) { - vim_setenv_ext(name, p); - arg_end = arg; - } - name[len] = c1; - xfree(tofree); - } - } + // ":let $VAR = expr": Set environment variable. + return ex_let_env(arg, tv, is_const, endchars, op); + } else if (*arg == '&') { // ":let &option = expr": Set option value. // ":let &l:option = expr": Set local option value. // ":let &g:option = expr": Set global option value. - } else if (*arg == '&') { - if (is_const) { - emsg(_("E996: Cannot lock an option")); - return NULL; - } - // Find the end of the name. - int scope; - char *const p = (char *)find_option_end((const char **)&arg, &scope); - if (p == NULL - || (endchars != NULL - && vim_strchr(endchars, (uint8_t)(*skipwhite(p))) == NULL)) { - emsg(_(e_letunexp)); - } else { - varnumber_T n = 0; - getoption_T opt_type; - long numval; - char *stringval = NULL; - const char *s = NULL; - bool failed = false; - uint32_t opt_p_flags; - char *tofree = NULL; - - const char c1 = *p; - *p = NUL; - - opt_type = get_option_value(arg, &numval, &stringval, &opt_p_flags, scope); - if (opt_type == gov_bool - || opt_type == gov_number - || opt_type == gov_hidden_bool - || opt_type == gov_hidden_number) { - // number, possibly hidden - n = (long)tv_get_number(tv); - } - - if ((opt_p_flags & P_FUNC) && tv_is_func(*tv)) { - // If the option can be set to a function reference or a lambda - // and the passed value is a function reference, then convert it to - // the name (string) of the function reference. - s = tofree = encode_tv2string(tv, NULL); - } else if (tv->v_type != VAR_BOOL && tv->v_type != VAR_SPECIAL) { - // Avoid setting a string option to the text "v:false" or similar. - s = tv_get_string_chk(tv); - } - - if (op != NULL && *op != '=') { - if (((opt_type == gov_bool || opt_type == gov_number) && *op == '.') - || (opt_type == gov_string && *op != '.')) { - semsg(_(e_letwrong), op); - failed = true; // don't set the value - } else { - // number or bool - if (opt_type == gov_number || opt_type == gov_bool) { - switch (*op) { - case '+': - n = numval + n; break; - case '-': - n = numval - n; break; - case '*': - n = numval * n; break; - case '/': - n = num_divide(numval, n); break; - case '%': - n = num_modulus(numval, n); break; - } - s = NULL; - } else if (opt_type == gov_string && stringval != NULL && s != NULL) { - // string - char *const oldstringval = stringval; - stringval = concat_str(stringval, s); - xfree(oldstringval); - s = stringval; - } - } - } - - if (!failed) { - if (opt_type != gov_string || s != NULL) { - const char *err = set_option_value(arg, (long)n, s, scope); - arg_end = p; - if (err != NULL) { - emsg(_(err)); - } - } else { - emsg(_(e_stringreq)); - } - } - *p = c1; - xfree(stringval); - xfree(tofree); - } - // ":let @r = expr": Set register contents. + return ex_let_option(arg, tv, is_const, endchars, op); } else if (*arg == '@') { - if (is_const) { - emsg(_("E996: Cannot lock a register")); - return NULL; - } - arg++; - if (op != NULL && vim_strchr("+-*/%", (uint8_t)(*op)) != NULL) { - semsg(_(e_letwrong), op); - } else if (endchars != NULL - && vim_strchr(endchars, (uint8_t)(*skipwhite(arg + 1))) == NULL) { - emsg(_(e_letunexp)); - } else { - char *ptofree = NULL; - const char *p = tv_get_string_chk(tv); - if (p != NULL && op != NULL && *op == '.') { - char *s = get_reg_contents(*arg == '@' ? '"' : *arg, kGRegExprSrc); - if (s != NULL) { - ptofree = concat_str(s, p); - p = ptofree; - xfree(s); - } - } - if (p != NULL) { - write_reg_contents(*arg == '@' ? '"' : *arg, p, (ssize_t)strlen(p), false); - arg_end = arg + 1; - } - xfree(ptofree); - } + // ":let @r = expr": Set register contents. + return ex_let_register(arg, tv, is_const, endchars, op); + } else if (eval_isnamec1(*arg) || *arg == '{') { // ":let var = expr": Set internal variable. // ":let {expr} = expr": Idem, name made with curly braces - } else if (eval_isnamec1(*arg) || *arg == '{') { lval_T lv; - char *const p = get_lval(arg, tv, &lv, false, false, 0, FNE_CHECK_START); if (p != NULL && lv.ll_name != NULL) { if (endchars != NULL && vim_strchr(endchars, (uint8_t)(*skipwhite(p))) == NULL) { diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 1d8c3c0cf4..40afb3250c 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -3280,9 +3280,11 @@ static int check_regexp_delim(int c) /// /// The usual escapes are supported as described in the regexp docs. /// -/// @param do_buf_event If `true`, send buffer updates. +/// @param cmdpreview_ns The namespace to show 'inccommand' preview highlights. +/// If <= 0, preview shouldn't be shown. /// @return 0, 1 or 2. See show_cmdpreview() for more information on what the return value means. -static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T cmdpreview_bufnr) +static int do_sub(exarg_T *eap, const proftime_T timeout, const long cmdpreview_ns, + const handle_T cmdpreview_bufnr) { #define ADJUST_SUB_FIRSTLNUM() \ do { \ @@ -3400,7 +3402,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T MB_PTR_ADV(cmd); } - if (!eap->skip && !cmdpreview) { + if (!eap->skip && cmdpreview_ns <= 0) { sub_set_replacement((SubReplacementString) { .sub = xstrdup(sub), .timestamp = os_time(), @@ -3420,7 +3422,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T endcolumn = (curwin->w_curswant == MAXCOL); } - if (sub != NULL && sub_joining_lines(eap, pat, sub, cmd, !cmdpreview)) { + if (sub != NULL && sub_joining_lines(eap, pat, sub, cmd, cmdpreview_ns <= 0)) { return 0; } @@ -3465,7 +3467,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T } if (search_regcomp(pat, NULL, RE_SUBST, which_pat, - (cmdpreview ? 0 : SEARCH_HIS), ®match) == FAIL) { + (cmdpreview_ns > 0 ? 0 : SEARCH_HIS), ®match) == FAIL) { if (subflags.do_error) { emsg(_(e_invcmd)); } @@ -3494,7 +3496,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T sub = xstrdup(sub); sub_copy = sub; } else { - char *newsub = regtilde(sub, magic_isset(), cmdpreview); + char *newsub = regtilde(sub, magic_isset(), cmdpreview_ns > 0); if (newsub != sub) { // newsub was allocated, free it later. sub_copy = newsub; @@ -3508,7 +3510,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T for (linenr_T lnum = eap->line1; lnum <= line2 && !got_quit && !aborting() - && (!cmdpreview || preview_lines.lines_needed <= (linenr_T)p_cwh + && (cmdpreview_ns <= 0 || preview_lines.lines_needed <= (linenr_T)p_cwh || lnum <= curwin->w_botline); lnum++) { long nmatch = vim_regexec_multi(®match, curwin, curbuf, lnum, @@ -3669,7 +3671,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T } } - if (subflags.do_ask && !cmdpreview) { + if (subflags.do_ask && cmdpreview_ns <= 0) { int typed = 0; // change State to MODE_CONFIRM, so that the mouse works @@ -3882,7 +3884,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T // Save the line numbers for the preview buffer // NOTE: If the pattern matches a final newline, the next line will // be shown also, but should not be highlighted. Intentional for now. - if (cmdpreview && !has_second_delim) { + if (cmdpreview_ns > 0 && !has_second_delim) { current_match.start.col = regmatch.startpos[0].col; if (current_match.end.lnum == 0) { current_match.end.lnum = sub_firstlnum + (linenr_T)nmatch - 1; @@ -3897,7 +3899,7 @@ static int do_sub(exarg_T *eap, proftime_T timeout, long cmdpreview_ns, handle_T // 3. Substitute the string. During 'inccommand' preview only do this if // there is a replace pattern. - if (!cmdpreview || has_second_delim) { + if (cmdpreview_ns <= 0 || has_second_delim) { long lnum_start = lnum; // save the start lnum int save_ma = curbuf->b_p_ma; int save_sandbox = sandbox; @@ -4147,7 +4149,7 @@ skip: #define PUSH_PREVIEW_LINES() \ do { \ - if (cmdpreview) { \ + if (cmdpreview_ns > 0) { \ linenr_T match_lines = current_match.end.lnum \ - current_match.start.lnum +1; \ if (preview_lines.subresults.size > 0) { \ @@ -4230,7 +4232,7 @@ skip: beginline(BL_WHITE | BL_FIX); } } - if (!cmdpreview && !do_sub_msg(subflags.do_count) && subflags.do_ask && p_ch > 0) { + if (cmdpreview_ns <= 0 && !do_sub_msg(subflags.do_count) && subflags.do_ask && p_ch > 0) { msg(""); } } else { @@ -4269,7 +4271,7 @@ skip: int retv = 0; // Show 'inccommand' preview if there are matched lines. - if (cmdpreview && !aborting()) { + if (cmdpreview_ns > 0 && !aborting()) { if (got_quit || profile_passed_limit(timeout)) { // Too slow, disable. set_string_option_direct("icm", -1, "", OPT_FREE, SID_NONE); } else if (*p_icm != NUL && pat != NULL) { diff --git a/src/nvim/ex_cmds.lua b/src/nvim/ex_cmds.lua index b0938fa711..88f3bc0b43 100644 --- a/src/nvim/ex_cmds.lua +++ b/src/nvim/ex_cmds.lua @@ -2212,7 +2212,7 @@ module.cmds = { }, { command='registers', - flags=bit.bor(EXTRA, NOTRLCOM, TRLBAR, CMDWIN, LOCK_OK), + flags=bit.bor(EXTRA, NOTRLCOM, TRLBAR, SBOXOK, CMDWIN, LOCK_OK), addr_type='ADDR_NONE', func='ex_display', }, diff --git a/src/nvim/ex_getln.c b/src/nvim/ex_getln.c index dd23f6ece9..b2acc561be 100644 --- a/src/nvim/ex_getln.c +++ b/src/nvim/ex_getln.c @@ -1597,7 +1597,7 @@ static int command_line_insert_reg(CommandLineState *s) bool literally = false; if (s->c != ESC) { // use ESC to cancel inserting register - literally = i == Ctrl_R; + literally = i == Ctrl_R || is_literal_register(s->c); cmdline_paste(s->c, literally, false); // When there was a serious error abort getting the diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index e60007bf88..d24ac1c233 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -153,6 +153,7 @@ void filemess(buf_T *buf, char *name, char *s, int attr) int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, linenr_T lines_to_read, exarg_T *eap, int flags, bool silent) { + int retval = FAIL; // jump to "theend" instead of returning int fd = stdin_fd >= 0 ? stdin_fd : 0; int newfile = (flags & READ_NEW); int check_readonly; @@ -240,7 +241,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, && vim_strchr(p_cpo, CPO_FNAMER) != NULL && !(flags & READ_DUMMY)) { if (set_rw_fname(fname, sfname) == FAIL) { - return FAIL; + goto theend; } } @@ -284,10 +285,9 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (newfile) { if (apply_autocmds_exarg(EVENT_BUFREADCMD, NULL, sfname, false, curbuf, eap)) { - int status = OK; - + retval = OK; if (aborting()) { - status = FAIL; + retval = FAIL; } // The BufReadCmd code usually uses ":read" to get the text and @@ -295,14 +295,15 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, // consider this to work like ":edit", thus reset the // BF_NOTEDITED flag. Then ":write" will work to overwrite the // same file. - if (status == OK) { + if (retval == OK) { curbuf->b_flags &= ~BF_NOTEDITED; } - return status; + goto theend; } } else if (apply_autocmds_exarg(EVENT_FILEREADCMD, sfname, sfname, false, NULL, eap)) { - return aborting() ? FAIL : OK; + retval = aborting() ? FAIL : OK; + goto theend; } curbuf->b_op_start = orig_start; @@ -310,7 +311,8 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (flags & READ_NOFILE) { // Return NOTDONE instead of FAIL so that BufEnter can be triggered // and other operations don't fail. - return NOTDONE; + retval = NOTDONE; + goto theend; } } @@ -328,7 +330,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, filemess(curbuf, fname, _("Illegal file name"), 0); msg_end(); msg_scroll = msg_save; - return FAIL; + goto theend; } // If the name ends in a path separator, we can't open it. Check here, @@ -340,7 +342,8 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, } msg_end(); msg_scroll = msg_save; - return NOTDONE; + retval = NOTDONE; + goto theend; } } @@ -365,12 +368,13 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (!silent) { filemess(curbuf, fname, _(msg_is_a_directory), 0); } + retval = NOTDONE; } else { filemess(curbuf, fname, _("is not a file"), 0); } msg_end(); msg_scroll = msg_save; - return S_ISDIR(perm) ? NOTDONE : FAIL; + goto theend; } } @@ -431,7 +435,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (fd < 0) { // cannot open at all msg_scroll = msg_save; if (!newfile) { - return FAIL; + goto theend; } if (perm == UV_ENOENT) { // check if the file exists // Set the 'new-file' flag, so that when the file has @@ -450,7 +454,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, || (using_b_fname && (old_b_fname != curbuf->b_fname))) { emsg(_(e_auchangedbuf)); - return FAIL; + goto theend; } } if (!silent) { @@ -472,10 +476,10 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, // remember the current fileformat save_file_ff(curbuf); - if (aborting()) { // autocmds may abort script processing - return FAIL; + if (!aborting()) { // autocmds may abort script processing + retval = OK; // a new file is not an error } - return OK; // a new file is not an error + goto theend; } #if defined(UNIX) && defined(EOVERFLOW) filemess(curbuf, sfname, ((fd == UV_EFBIG) ? _("[File too big]") : @@ -491,7 +495,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, #endif curbuf->b_p_ro = true; // must use "w!" now - return FAIL; + goto theend; } // Only set the 'ro' flag for readonly files the first time they are @@ -526,7 +530,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (!read_buffer) { close(fd); } - return FAIL; + goto theend; } #ifdef UNIX // Set swap file protection bits after creating it. @@ -561,7 +565,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, if (!read_buffer && !read_stdin) { close(fd); } - return FAIL; + goto theend; } no_wait_return++; // don't wait for return yet @@ -617,7 +621,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, no_wait_return--; msg_scroll = msg_save; curbuf->b_p_ro = true; // must use "w!" now - return FAIL; + goto theend; } // Don't allow the autocommands to change the current buffer. // Try to re-open the file. @@ -636,7 +640,7 @@ int readfile(char *fname, char *sfname, linenr_T from, linenr_T lines_to_skip, emsg(_("E201: *ReadPre autocommands must not change current buffer")); } curbuf->b_p_ro = true; // must use "w!" now - return FAIL; + goto theend; } } @@ -1684,7 +1688,8 @@ failed: } msg_scroll = msg_save; check_marks_read(); - return OK; // an interrupt isn't really an error + retval = OK; // an interrupt isn't really an error + goto theend; } if (!filtering && !(flags & READ_DUMMY) && !silent) { @@ -1860,10 +1865,18 @@ failed: } } - if (recoverymode && error) { - return FAIL; + if (!(recoverymode && error)) { + retval = OK; } - return OK; + +theend: + if (curbuf->b_ml.ml_mfp != NULL + && curbuf->b_ml.ml_mfp->mf_dirty == MF_DIRTY_YES_NOSYNC) { + // OK to sync the swap file now + curbuf->b_ml.ml_mfp->mf_dirty = MF_DIRTY_YES; + } + + return retval; } #ifdef OPEN_CHR_FILES diff --git a/src/nvim/main.c b/src/nvim/main.c index d4fbf8ce93..f27ebb2f67 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -250,6 +250,11 @@ int main(int argc, char **argv) argv0 = argv[0]; + if (!appname_is_valid()) { + os_errmsg("$NVIM_APPNAME is not a valid file name.\n"); + exit(1); + } + if (argc > 1 && STRICMP(argv[1], "-ll") == 0) { if (argc == 2) { print_mainerr(err_arg_missing, argv[1]); diff --git a/src/nvim/memfile.c b/src/nvim/memfile.c index bd8314c679..5d6c58c387 100644 --- a/src/nvim/memfile.c +++ b/src/nvim/memfile.c @@ -103,7 +103,7 @@ memfile_T *mf_open(char *fname, int flags) mfp->mf_free_first = NULL; // free list is empty mfp->mf_used_first = NULL; // used list is empty mfp->mf_used_last = NULL; - mfp->mf_dirty = false; + mfp->mf_dirty = MF_DIRTY_NO; mf_hash_init(&mfp->mf_hash); mf_hash_init(&mfp->mf_trans); mfp->mf_page_size = MEMFILE_PAGE_SIZE; @@ -159,7 +159,7 @@ memfile_T *mf_open(char *fname, int flags) int mf_open_file(memfile_T *mfp, char *fname) { if (mf_do_open(mfp, fname, O_RDWR | O_CREAT | O_EXCL)) { - mfp->mf_dirty = true; + mfp->mf_dirty = MF_DIRTY_YES; return OK; } @@ -269,7 +269,7 @@ bhdr_T *mf_new(memfile_T *mfp, bool negative, unsigned page_count) } } hp->bh_flags = BH_LOCKED | BH_DIRTY; // new block is always dirty - mfp->mf_dirty = true; + mfp->mf_dirty = MF_DIRTY_YES; hp->bh_page_count = page_count; mf_ins_used(mfp, hp); mf_ins_hash(mfp, hp); @@ -342,7 +342,9 @@ void mf_put(memfile_T *mfp, bhdr_T *hp, bool dirty, bool infile) flags &= ~BH_LOCKED; if (dirty) { flags |= BH_DIRTY; - mfp->mf_dirty = true; + if (mfp->mf_dirty != MF_DIRTY_YES_NOSYNC) { + mfp->mf_dirty = MF_DIRTY_YES; + } } hp->bh_flags = flags; if (infile) { @@ -382,8 +384,9 @@ int mf_sync(memfile_T *mfp, int flags) { int got_int_save = got_int; - if (mfp->mf_fd < 0) { // there is no file, nothing to do - mfp->mf_dirty = false; + if (mfp->mf_fd < 0) { + // there is no file, nothing to do + mfp->mf_dirty = MF_DIRTY_NO; return FAIL; } @@ -426,7 +429,7 @@ int mf_sync(memfile_T *mfp, int flags) // If the whole list is flushed, the memfile is not dirty anymore. // In case of an error, dirty flag is also set, to avoid trying all the time. if (hp == NULL || status == FAIL) { - mfp->mf_dirty = false; + mfp->mf_dirty = MF_DIRTY_NO; } if (flags & MFS_FLUSH) { @@ -449,7 +452,7 @@ void mf_set_dirty(memfile_T *mfp) hp->bh_flags |= BH_DIRTY; } } - mfp->mf_dirty = true; + mfp->mf_dirty = MF_DIRTY_YES; } /// Insert block in front of memfile's hash list. diff --git a/src/nvim/memfile_defs.h b/src/nvim/memfile_defs.h index 53152c28f8..917dd6a905 100644 --- a/src/nvim/memfile_defs.h +++ b/src/nvim/memfile_defs.h @@ -38,13 +38,13 @@ typedef struct mf_hashitem { /// mf_hashitem_T which contains the key and linked list pointers. List of items /// in each bucket is doubly-linked. typedef struct mf_hashtab { - size_t mht_mask; /// mask used to mod hash value to array index - /// (nr of items in array is 'mht_mask + 1') - size_t mht_count; /// number of items inserted - mf_hashitem_T **mht_buckets; /// points to the array of buckets (can be - /// mht_small_buckets or a newly allocated array - /// when mht_small_buckets becomes too small) - mf_hashitem_T *mht_small_buckets[MHT_INIT_SIZE]; /// initial buckets + size_t mht_mask; ///< mask used to mod hash value to array index + ///< (nr of items in array is 'mht_mask + 1') + size_t mht_count; ///< number of items inserted + mf_hashitem_T **mht_buckets; ///< points to the array of buckets (can be + ///< mht_small_buckets or a newly allocated array + ///< when mht_small_buckets becomes too small) + mf_hashitem_T *mht_small_buckets[MHT_INIT_SIZE]; ///< initial buckets } mf_hashtab_T; /// A block header. @@ -61,17 +61,17 @@ typedef struct mf_hashtab { /// The blocks in the free list have no block of memory allocated and /// the contents of the block in the file (if any) is irrelevant. typedef struct bhdr { - mf_hashitem_T bh_hashitem; /// header for hash table and key -#define bh_bnum bh_hashitem.mhi_key /// block number, part of bh_hashitem + mf_hashitem_T bh_hashitem; ///< header for hash table and key +#define bh_bnum bh_hashitem.mhi_key ///< block number, part of bh_hashitem - struct bhdr *bh_next; /// next block header in free or used list - struct bhdr *bh_prev; /// previous block header in used list - void *bh_data; /// pointer to memory (for used block) - unsigned bh_page_count; /// number of pages in this block + struct bhdr *bh_next; ///< next block header in free or used list + struct bhdr *bh_prev; ///< previous block header in used list + void *bh_data; ///< pointer to memory (for used block) + unsigned bh_page_count; ///< number of pages in this block #define BH_DIRTY 1U #define BH_LOCKED 2U - unsigned bh_flags; // BH_DIRTY or BH_LOCKED + unsigned bh_flags; ///< BH_DIRTY or BH_LOCKED } bhdr_T; /// A block number translation list item. @@ -81,27 +81,33 @@ typedef struct bhdr { /// number, we remember the translation to the new positive number in the /// double linked trans lists. The structure is the same as the hash lists. typedef struct mf_blocknr_trans_item { - mf_hashitem_T nt_hashitem; /// header for hash table and key -#define nt_old_bnum nt_hashitem.mhi_key /// old, negative, number - blocknr_T nt_new_bnum; /// new, positive, number + mf_hashitem_T nt_hashitem; ///< header for hash table and key +#define nt_old_bnum nt_hashitem.mhi_key ///< old, negative, number + blocknr_T nt_new_bnum; ///< new, positive, number } mf_blocknr_trans_item_T; +typedef enum { + MF_DIRTY_NO = 0, ///< no dirty blocks + MF_DIRTY_YES, ///< there are dirty blocks + MF_DIRTY_YES_NOSYNC, ///< there are dirty blocks, do not sync yet +} mfdirty_T; + /// A memory file. typedef struct memfile { - char *mf_fname; /// name of the file - char *mf_ffname; /// idem, full path - int mf_fd; /// file descriptor - bhdr_T *mf_free_first; /// first block header in free list - bhdr_T *mf_used_first; /// mru block header in used list - bhdr_T *mf_used_last; /// lru block header in used list - mf_hashtab_T mf_hash; /// hash lists - mf_hashtab_T mf_trans; /// trans lists - blocknr_T mf_blocknr_max; /// highest positive block number + 1 - blocknr_T mf_blocknr_min; /// lowest negative block number - 1 - blocknr_T mf_neg_count; /// number of negative blocks numbers - blocknr_T mf_infile_count; /// number of pages in the file - unsigned mf_page_size; /// number of bytes in a page - bool mf_dirty; /// true if there are dirty blocks + char *mf_fname; ///< name of the file + char *mf_ffname; ///< idem, full path + int mf_fd; ///< file descriptor + bhdr_T *mf_free_first; ///< first block header in free list + bhdr_T *mf_used_first; ///< mru block header in used list + bhdr_T *mf_used_last; ///< lru block header in used list + mf_hashtab_T mf_hash; ///< hash lists + mf_hashtab_T mf_trans; ///< trans lists + blocknr_T mf_blocknr_max; ///< highest positive block number + 1 + blocknr_T mf_blocknr_min; ///< lowest negative block number - 1 + blocknr_T mf_neg_count; ///< number of negative blocks numbers + blocknr_T mf_infile_count; ///< number of pages in the file + unsigned mf_page_size; ///< number of bytes in a page + mfdirty_T mf_dirty; } memfile_T; #endif // NVIM_MEMFILE_DEFS_H diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 0815ba411c..eb2afc60b2 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -510,6 +510,8 @@ void ml_open_file(buf_T *buf) continue; } if (mf_open_file(mfp, fname) == OK) { // consumes fname! + // don't sync yet in ml_sync_all() + mfp->mf_dirty = MF_DIRTY_YES_NOSYNC; ml_upd_block0(buf, UB_SAME_DIR); // Flush block zero, so others can read it @@ -1714,7 +1716,7 @@ void ml_sync_all(int check_file, int check_char, bool do_fsync) need_check_timestamps = true; // give message later } } - if (buf->b_ml.ml_mfp->mf_dirty) { + if (buf->b_ml.ml_mfp->mf_dirty == MF_DIRTY_YES) { (void)mf_sync(buf->b_ml.ml_mfp, (check_char ? MFS_STOP : 0) | (do_fsync && bufIsChanged(buf) ? MFS_FLUSH : 0)); if (check_char && os_char_avail()) { // character available now diff --git a/src/nvim/memory.c b/src/nvim/memory.c index 1f550ffb01..6b4d290863 100644 --- a/src/nvim/memory.c +++ b/src/nvim/memory.c @@ -816,7 +816,6 @@ void free_all_mem(void) grid_free_all_mem(); clear_hl_tables(false); - list_free_log(); check_quickfix_busy(); diff --git a/src/nvim/ops.c b/src/nvim/ops.c index de77cdd238..c39a3273da 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -854,15 +854,6 @@ static bool is_append_register(int regname) return ASCII_ISUPPER(regname); } -/// @see get_yank_register -/// @returns true when register should be inserted literally -/// (selection or clipboard) -static inline bool is_literal_register(int regname) - FUNC_ATTR_CONST -{ - return regname == '*' || regname == '+'; -} - /// @return a copy of contents in register `name` for use in do_put. Should be /// freed by caller. yankreg_T *copy_register(int name) diff --git a/src/nvim/ops.h b/src/nvim/ops.h index 81e006be27..e378d2f7b5 100644 --- a/src/nvim/ops.h +++ b/src/nvim/ops.h @@ -123,6 +123,15 @@ static inline int op_reg_index(const int regname) } } +/// @see get_yank_register +/// @return true when register should be inserted literally +/// (selection or clipboard) +static inline bool is_literal_register(const int regname) + FUNC_ATTR_CONST +{ + return regname == '*' || regname == '+'; +} + #ifdef INCLUDE_GENERATED_DECLARATIONS # include "ops.h.generated.h" #endif diff --git a/src/nvim/os/stdpaths.c b/src/nvim/os/stdpaths.c index 8b62b9e895..53ddda22fa 100644 --- a/src/nvim/os/stdpaths.c +++ b/src/nvim/os/stdpaths.c @@ -69,6 +69,19 @@ const char *get_appname(void) return env_val; } +/// Ensure that APPNAME is valid. In particular, it cannot contain directory separators. +bool appname_is_valid(void) +{ + const char *appname = get_appname(); + const size_t appname_len = strlen(appname); + for (size_t i = 0; i < appname_len; i++) { + if (appname[i] == PATHSEP) { + return false; + } + } + return true; +} + /// Return XDG variable value /// /// @param[in] idx XDG variable to use. diff --git a/src/nvim/po/check.vim b/src/nvim/po/check.vim index 7705ba8577..8752af663b 100644 --- a/src/nvim/po/check.vim +++ b/src/nvim/po/check.vim @@ -6,6 +6,9 @@ if 1 " Only execute this if the eval feature is available. +" using line continuation +set cpo&vim + let filename = "check-" . expand("%:t:r") . ".log" exe 'redir! > ' . filename @@ -62,12 +65,18 @@ while 1 if getline(line('.') - 1) !~ "no-c-format" " go over the "msgid" and "msgid_plural" lines let prevfromline = 'foobar' + let plural = 0 while 1 + if getline('.') =~ 'msgid_plural' + let plural += 1 + endif let fromline = GetMline() if prevfromline != 'foobar' && prevfromline != fromline + \ && (plural != 1 + \ || count(prevfromline, '%') + 1 != count(fromline, '%')) echomsg 'Mismatching % in line ' . (line('.') - 1) echomsg 'msgid: ' . prevfromline - echomsg 'msgid ' . fromline + echomsg 'msgid: ' . fromline if error == 0 let error = line('.') endif @@ -89,6 +98,7 @@ while 1 while getline('.') =~ '^msgstr' let toline = GetMline() if fromline != toline + \ && (plural == 0 || count(fromline, '%') != count(toline, '%') + 1) echomsg 'Mismatching % in line ' . (line('.') - 1) echomsg 'msgid: ' . fromline echomsg 'msgstr: ' . toline diff --git a/src/nvim/statusline.c b/src/nvim/statusline.c index 015c578396..b89d346fbf 100644 --- a/src/nvim/statusline.c +++ b/src/nvim/statusline.c @@ -1031,6 +1031,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n int evaldepth = 0; int curitem = 0; + int foldsignitem = -1; bool prevchar_isflag = true; bool prevchar_isitem = false; @@ -1655,6 +1656,7 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n if (width == 0) { break; } + foldsignitem = curitem; char *p = NULL; if (fold) { @@ -1664,32 +1666,22 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n p[n] = NUL; } - *buf_tmp = NUL; + size_t buflen = 0; varnumber_T virtnum = get_vim_var_nr(VV_VIRTNUM); - for (int i = 0; i <= width; i++) { - if (i == width) { - if (*buf_tmp == NUL) { - break; - } - stl_items[curitem].minwid = 0; - } else if (!fold) { + for (int i = 0; i < width; i++) { + if (!fold) { SignTextAttrs *sattr = virtnum ? NULL : sign_get_attr(i, stcp->sattrs, wp->w_scwidth); p = sattr && sattr->text ? sattr->text : " "; stl_items[curitem].minwid = -(sattr ? stcp->sign_cul_id ? stcp->sign_cul_id : sattr->hl_id : (stcp->use_cul ? HLF_CLS : HLF_SC) + 1); } - size_t buflen = strlen(buf_tmp); stl_items[curitem].type = Highlight; stl_items[curitem].start = out_p + buflen; + xstrlcpy(buf_tmp + buflen, p, TMPLEN - buflen); + buflen += strlen(p); curitem++; - if (i == width) { - str = buf_tmp; - break; - } - int rc = snprintf(buf_tmp + buflen, sizeof(buf_tmp) - buflen, "%s", p); - (void)rc; // Avoid unused warning on release build - assert(rc > 0); } + str = buf_tmp; break; } @@ -1832,6 +1824,13 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n } } minwid = 0; + // For a 'statuscolumn' sign or fold item, shift the added items + if (foldsignitem >= 0) { + ptrdiff_t offset = out_p - stl_items[foldsignitem].start; + for (int i = foldsignitem; i < curitem; i++) { + stl_items[i].start += offset; + } + } } else { // Note: The negative value denotes a left aligned item. // Here we switch the minimum width back to a positive value. @@ -1851,6 +1850,14 @@ int build_stl_str_hl(win_T *wp, char *out, size_t outlen, char *fmt, char *opt_n } // } + // For a 'statuscolumn' sign or fold item, add an item to reset the highlight group + if (foldsignitem >= 0) { + foldsignitem = -1; + stl_items[curitem].type = Highlight; + stl_items[curitem].start = out_p; + stl_items[curitem].minwid = 0; + } + // For left-aligned items, fill any remaining space with the fillchar for (; l < minwid && out_p < out_end_p; l++) { MB_CHAR2BYTES(fillchar, out_p); diff --git a/src/nvim/testing.c b/src/nvim/testing.c index 25ec8e898a..eea75f3021 100644 --- a/src/nvim/testing.c +++ b/src/nvim/testing.c @@ -775,5 +775,4 @@ void f_test_write_list_log(typval_T *const argvars, typval_T *const rettv, EvalF if (fname == NULL) { return; } - list_write_log(fname); } diff --git a/test/functional/autocmd/textyankpost_spec.lua b/test/functional/autocmd/textyankpost_spec.lua index 3898d59e58..1640916ad8 100644 --- a/test/functional/autocmd/textyankpost_spec.lua +++ b/test/functional/autocmd/textyankpost_spec.lua @@ -8,7 +8,7 @@ describe('TextYankPost', function() clear() -- emulate the clipboard so system clipboard isn't affected - command('let &rtp = "test/functional/fixtures,".&rtp') + command('set rtp^=test/functional/fixtures') command('let g:count = 0') command('autocmd TextYankPost * let g:event = copy(v:event)') diff --git a/test/functional/options/defaults_spec.lua b/test/functional/options/defaults_spec.lua index 3690b7e97c..60edf219d9 100644 --- a/test/functional/options/defaults_spec.lua +++ b/test/functional/options/defaults_spec.lua @@ -14,6 +14,7 @@ local ok = helpers.ok local funcs = helpers.funcs local insert = helpers.insert local neq = helpers.neq +local nvim_prog = helpers.nvim_prog local mkdir = helpers.mkdir local rmdir = helpers.rmdir local alter_slashes = helpers.alter_slashes @@ -603,6 +604,10 @@ describe('stdpath()', function() eq(appname, funcs.fnamemodify(funcs.stdpath('data_dirs')[1], ':t')) end assert_alive() -- Check for crash. #8393 + + -- Check that nvim rejects invalid APPNAMEs + local child = funcs.jobstart({ nvim_prog }, {env={NVIM_APPNAME='a/b\\c'}}) + eq(1, funcs.jobwait({child}, 3000)[1]) end) context('returns a String', function() diff --git a/test/functional/plugin/lsp_spec.lua b/test/functional/plugin/lsp_spec.lua index b906ae265f..e0ce62c0db 100644 --- a/test/functional/plugin/lsp_spec.lua +++ b/test/functional/plugin/lsp_spec.lua @@ -948,7 +948,7 @@ describe('LSP', function() test_name = "check_tracked_requests_cleared"; on_init = function(_client) command('let g:requests = 0') - command('autocmd User LspRequest let g:requests+=1') + command('autocmd LspRequest * let g:requests+=1') client = _client client.request("slow_request") eq(1, eval('g:requests')) @@ -3765,6 +3765,96 @@ describe('LSP', function() end) end) + describe('#dynamic vim.lsp._dynamic', function() + it('supports dynamic registration', function() + local root_dir = helpers.tmpname() + os.remove(root_dir) + mkdir(root_dir) + local tmpfile = root_dir .. '/dynamic.foo' + local file = io.open(tmpfile, 'w') + file:close() + + exec_lua(create_server_definition) + local result = exec_lua([[ + local root_dir, tmpfile = ... + + local server = _create_server() + local client_id = vim.lsp.start({ + name = 'dynamic-test', + cmd = server.cmd, + root_dir = root_dir, + capabilities = { + textDocument = { + formatting = { + dynamicRegistration = true, + }, + rangeFormatting = { + dynamicRegistration = true, + }, + }, + }, + }) + + local expected_messages = 2 -- initialize, initialized + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'formatting', + method = 'textDocument/formatting', + registerOptions = { + documentSelector = {{ + pattern = root_dir .. '/*.foo', + }}, + }, + }, + }, + }, { client_id = client_id }) + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'range-formatting', + method = 'textDocument/rangeFormatting', + }, + }, + }, { client_id = client_id }) + + vim.lsp.handlers['client/registerCapability'](nil, { + registrations = { + { + id = 'completion', + method = 'textDocument/completion', + }, + }, + }, { client_id = client_id }) + + local result = {} + local function check(method, fname) + local bufnr = fname and vim.fn.bufadd(fname) or nil + local client = vim.lsp.get_client_by_id(client_id) + result[#result + 1] = {method = method, fname = fname, supported = client.supports_method(method, {bufnr = bufnr})} + end + + + check("textDocument/formatting") + check("textDocument/formatting", tmpfile) + check("textDocument/rangeFormatting") + check("textDocument/rangeFormatting", tmpfile) + check("textDocument/completion") + + return result + ]], root_dir, tmpfile) + + eq(5, #result) + eq({method = 'textDocument/formatting', supported = false}, result[1]) + eq({method = 'textDocument/formatting', supported = true, fname = tmpfile}, result[2]) + eq({method = 'textDocument/rangeFormatting', supported = true}, result[3]) + eq({method = 'textDocument/rangeFormatting', supported = true, fname = tmpfile}, result[4]) + eq({method = 'textDocument/completion', supported = false}, result[5]) + end) + end) + describe('vim.lsp._watchfiles', function() it('sends notifications when files change', function() local root_dir = helpers.tmpname() diff --git a/test/functional/provider/clipboard_spec.lua b/test/functional/provider/clipboard_spec.lua index 2c5185a974..c8f1518283 100644 --- a/test/functional/provider/clipboard_spec.lua +++ b/test/functional/provider/clipboard_spec.lua @@ -301,7 +301,7 @@ end) describe('clipboard (with fake clipboard.vim)', function() local function reset(...) - clear('--cmd', 'let &rtp = "test/functional/fixtures,".&rtp', ...) + clear('--cmd', 'set rtp^=test/functional/fixtures', ...) end before_each(function() diff --git a/test/functional/provider/provider_spec.lua b/test/functional/provider/provider_spec.lua index 3895b8613f..b1c326d04c 100644 --- a/test/functional/provider/provider_spec.lua +++ b/test/functional/provider/provider_spec.lua @@ -7,7 +7,7 @@ local pcall_err = helpers.pcall_err describe('providers', function() before_each(function() - clear('--cmd', 'let &rtp = "test/functional/fixtures,".&rtp') + clear('--cmd', 'set rtp^=test/functional/fixtures') end) it('with #Call(), missing g:loaded_xx_provider', function() diff --git a/test/functional/ui/decorations_spec.lua b/test/functional/ui/decorations_spec.lua index b9080cbcf5..54441984a3 100644 --- a/test/functional/ui/decorations_spec.lua +++ b/test/functional/ui/decorations_spec.lua @@ -8,6 +8,7 @@ local exec_lua = helpers.exec_lua local exec = helpers.exec local expect_events = helpers.expect_events local meths = helpers.meths +local funcs = helpers.funcs local curbufmeths = helpers.curbufmeths local command = helpers.command local assert_alive = helpers.assert_alive @@ -651,7 +652,7 @@ describe('extmark decorations', function() [16] = {blend = 30, background = Screen.colors.Red1, foreground = Screen.colors.Magenta1}; [17] = {bold = true, foreground = Screen.colors.Brown, background = Screen.colors.LightGrey}; [18] = {background = Screen.colors.LightGrey}; - [19] = {foreground = Screen.colors.Cyan4, background = Screen.colors.LightGrey}; + [19] = {foreground = Screen.colors.DarkCyan, background = Screen.colors.LightGrey}; [20] = {foreground = tonumber('0x180606'), background = tonumber('0xf13f3f')}; [21] = {foreground = Screen.colors.Gray0, background = tonumber('0xf13f3f')}; [22] = {foreground = tonumber('0xb20000'), background = tonumber('0xf13f3f')}; @@ -662,6 +663,9 @@ describe('extmark decorations', function() [27] = {background = Screen.colors.Plum1}; [28] = {underline = true, foreground = Screen.colors.SlateBlue}; [29] = {foreground = Screen.colors.SlateBlue, background = Screen.colors.LightGray, underline = true}; + [30] = {foreground = Screen.colors.DarkCyan, background = Screen.colors.LightGray, underline = true}; + [31] = {underline = true, foreground = Screen.colors.DarkCyan}; + [32] = {underline = true}; } ns = meths.create_namespace 'test' @@ -827,26 +831,62 @@ describe('extmark decorations', function() end -- ?古古古古?古古 | | ]]} + end) - screen:try_resize(50, 2) + it('virt_text_hide hides overlay virtual text when extmark is off-screen', function() + screen:try_resize(50, 3) command('set nowrap') - meths.buf_set_lines(0, 12, 12, true, {'-- ' .. ('…'):rep(57)}) - feed('G') - meths.buf_set_extmark(0, ns, 12, 123, { virt_text={{'!!!!!', 'ErrorMsg'}}, virt_text_pos='overlay', virt_text_hide=true}) + meths.buf_set_lines(0, 0, -1, true, {'-- ' .. ('…'):rep(57)}) + meths.buf_set_extmark(0, ns, 0, 0, { virt_text={{'?????', 'ErrorMsg'}}, virt_text_pos='overlay', virt_text_hide=true}) + meths.buf_set_extmark(0, ns, 0, 123, { virt_text={{'!!!!!', 'ErrorMsg'}}, virt_text_pos='overlay', virt_text_hide=true}) screen:expect{grid=[[ - ^-- …………………………………………………………………………………………………………{4:!!!!!}……| + {4:^?????}……………………………………………………………………………………………………{4:!!!!!}……| + {1:~ }| | ]]} feed('40zl') screen:expect{grid=[[ ^………{4:!!!!!}……………………………… | + {1:~ }| + | + ]]} + feed('3zl') + screen:expect{grid=[[ + {4:^!!!!!}……………………………… | + {1:~ }| + | + ]]} + feed('7zl') + screen:expect{grid=[[ + ^………………………… | + {1:~ }| | ]]} - feed('10zl') + + command('set wrap smoothscroll') screen:expect{grid=[[ + {4:?????}……………………………………………………………………………………………………{4:!!!!!}……| ^………………………… | | ]]} + feed('<C-E>') + screen:expect{grid=[[ + {1:<<<}………………^… | + {1:~ }| + | + ]]} + screen:try_resize(40, 3) + screen:expect{grid=[[ + {1:<<<}{4:!!!!!}……………………………^… | + {1:~ }| + | + ]]} + feed('<C-Y>') + screen:expect{grid=[[ + {4:?????}……………………………………………………………………………………………| + ………{4:!!!!!}……………………………^… | + | + ]]} end) it('can have virtual text of overlay position and styling', function() @@ -1338,7 +1378,56 @@ describe('extmark decorations', function() screen:expect_unchanged(true) end) - it('highlights the beginning of a TAB char correctly', function() + it('highlight is combined with syntax and sign linehl #20004', function() + screen:try_resize(50, 3) + insert([[ + function Func() + end]]) + feed('gg') + command('set ft=lua') + command('syntax on') + meths.buf_set_extmark(0, ns, 0, 0, { end_col = 3, hl_mode = 'combine', hl_group = 'Visual' }) + command('hi default MyLine gui=underline') + command('sign define CurrentLine linehl=MyLine') + funcs.sign_place(6, 'Test', 'CurrentLine', '', { lnum = 1 }) + screen:expect{grid=[[ + {30:^fun}{31:ction}{32: Func() }| + {6:end} | + | + ]]} + end) + + it('highlight works after TAB with sidescroll #14201', function() + screen:try_resize(50, 3) + command('set nowrap') + meths.buf_set_lines(0, 0, -1, true, {'\tword word word word'}) + meths.buf_set_extmark(0, ns, 0, 1, { end_col = 3, hl_group = 'ErrorMsg' }) + screen:expect{grid=[[ + ^ {4:wo}rd word word word | + {1:~ }| + | + ]]} + feed('7zl') + screen:expect{grid=[[ + {4:^wo}rd word word word | + {1:~ }| + | + ]]} + feed('zl') + screen:expect{grid=[[ + {4:^wo}rd word word word | + {1:~ }| + | + ]]} + feed('zl') + screen:expect{grid=[[ + {4:^o}rd word word word | + {1:~ }| + | + ]]} + end) + + it('highlights the beginning of a TAB char correctly #23734', function() screen:try_resize(50, 3) meths.buf_set_lines(0, 0, -1, true, {'this is the\ttab'}) meths.buf_set_extmark(0, ns, 0, 11, { end_col = 15, hl_group = 'ErrorMsg' }) @@ -1357,7 +1446,20 @@ describe('extmark decorations', function() ]]} end) - pending('highlight applies to a full Tab in visual block mode #23734', function() + it('highlight applies to a full TAB on line with matches #20885', function() + screen:try_resize(50, 3) + meths.buf_set_lines(0, 0, -1, true, {'\t-- match1', ' -- match2'}) + funcs.matchadd('Underlined', 'match') + meths.buf_set_extmark(0, ns, 0, 0, { end_row = 1, end_col = 0, hl_group = 'Visual' }) + meths.buf_set_extmark(0, ns, 1, 0, { end_row = 2, end_col = 0, hl_group = 'Visual' }) + screen:expect{grid=[[ + {18: ^ -- }{29:match}{18:1} | + {18: -- }{29:match}{18:2} | + | + ]]} + end) + + pending('highlight applies to a full TAB in visual block mode', function() screen:try_resize(50, 8) meths.buf_set_lines(0, 0, -1, true, {'asdf', '\tasdf', '\tasdf', '\tasdf', 'asdf'}) meths.buf_set_extmark(0, ns, 0, 0, {end_row = 5, end_col = 0, hl_group = 'Underlined'}) @@ -2125,34 +2227,55 @@ bbbbbbb]]) end) it('does not crash at column 0 when folded in a wide window', function() - screen:try_resize(82, 4) + screen:try_resize(82, 5) command('hi! CursorLine guibg=NONE guifg=Red gui=NONE') command('set cursorline') insert([[ aaaaa bbbbb + ccccc]]) meths.buf_set_extmark(0, ns, 0, 0, { virt_text = {{'foo'}}, virt_text_pos = 'inline' }) + meths.buf_set_extmark(0, ns, 2, 0, { virt_text = {{'bar'}}, virt_text_pos = 'inline' }) screen:expect{grid=[[ fooaaaaa | bbbbb | + bar | {16:cccc^c }| | ]]} command('1,2fold') screen:expect{grid=[[ {17:+-- 2 lines: aaaaa·······························································}| + bar | {16:cccc^c }| {1:~ }| | ]]} - feed('k') + feed('2k') screen:expect{grid=[[ {18:^+-- 2 lines: aaaaa·······························································}| + bar | ccccc | {1:~ }| | ]]} + command('3,4fold') + screen:expect{grid=[[ + {18:^+-- 2 lines: aaaaa·······························································}| + {17:+-- 2 lines: ccccc·······························································}| + {1:~ }| + {1:~ }| + | + ]]} + feed('j') + screen:expect{grid=[[ + {17:+-- 2 lines: aaaaa·······························································}| + {18:^+-- 2 lines: ccccc·······························································}| + {1:~ }| + {1:~ }| + | + ]]} end) end) @@ -2164,7 +2287,7 @@ describe('decorations: virtual lines', function() screen:attach() screen:set_default_attr_ids { [1] = {bold=true, foreground=Screen.colors.Blue}; - [2] = {foreground = Screen.colors.Cyan4}; + [2] = {foreground = Screen.colors.DarkCyan}; [3] = {background = Screen.colors.Yellow1}; [4] = {bold = true}; [5] = {background = Screen.colors.Yellow, foreground = Screen.colors.Blue}; @@ -2883,6 +3006,30 @@ if (h->n_buckets < new_n_buckets) { // expand ]]} end) + it('does not show twice if end_row or end_col is specified #18622', function() + insert([[ + aaa + bbb + ccc + ddd]]) + meths.buf_set_extmark(0, ns, 0, 0, {end_row = 2, virt_lines = {{{'VIRT LINE 1', 'NonText'}}}}) + meths.buf_set_extmark(0, ns, 3, 0, {end_col = 2, virt_lines = {{{'VIRT LINE 2', 'NonText'}}}}) + screen:expect{grid=[[ + aaa | + {1:VIRT LINE 1} | + bbb | + ccc | + dd^d | + {1:VIRT LINE 2} | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + end) + end) describe('decorations: signs', function() @@ -3195,8 +3342,8 @@ l5 insert(example_test3) feed 'gg' - helpers.command('sign define Oldsign text=O3') - helpers.command([[exe 'sign place 42 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command('sign define Oldsign text=O3') + command([[exe 'sign place 42 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S4', priority=100}) meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S2', priority=5}) @@ -3219,6 +3366,39 @@ l5 ]]} end) + it('does not overflow with many old signs #23852', function() + screen:try_resize(20, 3) + + command('set signcolumn:auto:9') + command('sign define Oldsign text=O3') + command([[exe 'sign place 01 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 02 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 03 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 04 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 05 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 06 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 07 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 08 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + command([[exe 'sign place 09 line=1 name=Oldsign priority=10 buffer=' . bufnr('')]]) + screen:expect{grid=[[ + O3O3O3O3O3O3O3O3O3^ | + {2:~ }| + | + ]]} + + meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S1', priority=1}) + screen:expect_unchanged() + + meths.buf_set_extmark(0, ns, 0, -1, {sign_text='S5', priority=200}) + screen:expect{grid=[[ + O3O3O3O3O3O3O3O3S5^ | + {2:~ }| + | + ]]} + + assert_alive() + end) + it('does not set signcolumn for signs without text', function() screen:try_resize(20, 3) meths.set_option_value('signcolumn', 'auto', {}) diff --git a/test/functional/ui/highlight_spec.lua b/test/functional/ui/highlight_spec.lua index 9a4be4573c..c68f4cf34c 100644 --- a/test/functional/ui/highlight_spec.lua +++ b/test/functional/ui/highlight_spec.lua @@ -426,7 +426,7 @@ describe('highlight', function() ^ | {2:~ }| | - ]],{ + ]], { [1] = {strikethrough = true}, [2] = {bold = true, foreground = Screen.colors.Blue1}, }) @@ -515,7 +515,7 @@ describe('highlight', function() {1:neovim} tabbed^ | {0:~ }| {5:-- INSERT --} | - ]],{ + ]], { [0] = {bold=true, foreground=Screen.colors.Blue}, [1] = {background = Screen.colors.Yellow, foreground = Screen.colors.Red, special = Screen.colors.Red}, @@ -527,7 +527,7 @@ describe('highlight', function() end) - it("'diff', syntax and extmark", function() + it("'diff', syntax and extmark #23722", function() local screen = Screen.new(25,10) screen:attach() exec([[ @@ -549,7 +549,7 @@ describe('highlight', function() {4:~ }| {8:[No Name] }| | - ]],{ + ]], { [0] = {Screen.colors.WebGray, foreground = Screen.colors.DarkBlue}, [1] = {background = Screen.colors.Grey, foreground = Screen.colors.Blue4}, [2] = {foreground = Screen.colors.Red, background = Screen.colors.LightBlue}, diff --git a/test/functional/ui/inccommand_user_spec.lua b/test/functional/ui/inccommand_user_spec.lua index 6329ece40a..9cc6e095c5 100644 --- a/test/functional/ui/inccommand_user_spec.lua +++ b/test/functional/ui/inccommand_user_spec.lua @@ -401,6 +401,40 @@ describe("'inccommand' for user commands", function() feed('e') assert_alive() end) + + it('no crash when adding highlight after :substitute #21495', function() + command('set inccommand=nosplit') + exec_lua([[ + vim.api.nvim_create_user_command("Crash", function() end, { + preview = function(_, preview_ns, _) + vim.cmd("%s/text/cats/g") + vim.api.nvim_buf_add_highlight(0, preview_ns, "Search", 0, 0, -1) + return 1 + end, + }) + ]]) + feed(':C') + screen:expect([[ + {1: cats on line 1} | + more cats on line 2 | + oh no, even more cats | + will the cats ever stop | + oh well | + did the cats stop | + why won't it stop | + make the cats stop | + | + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + {2:~ }| + :C^ | + ]]) + assert_alive() + end) end) describe("'inccommand' with multiple buffers", function() diff --git a/test/functional/ui/searchhl_spec.lua b/test/functional/ui/searchhl_spec.lua index 3c8dceb8cb..1e42689200 100644 --- a/test/functional/ui/searchhl_spec.lua +++ b/test/functional/ui/searchhl_spec.lua @@ -512,6 +512,36 @@ describe('search highlighting', function() {1:~ }│{1:~ }| /file^ | ]]) + feed('<Esc>') + + command('set rtp^=test/functional/fixtures') + -- incsearch works after c_CTRL-R inserts clipboard register + + command('let @* = "first"') + feed('/<C-R>*') + screen:expect([[ + the {3:first} line │the {2:first} line | + in a little file │in a little file | + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + /first^ | + ]]) + feed('<Esc>') + + command('let @+ = "little"') + feed('/<C-R>+') + screen:expect([[ + the first line │the first line | + in a {3:little} file │in a {2:little} file | + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + {1:~ }│{1:~ }| + /little^ | + ]]) + feed('<Esc>') end) it('works with incsearch and offset', function() diff --git a/test/functional/ui/statuscolumn_spec.lua b/test/functional/ui/statuscolumn_spec.lua index c218bd8fd6..6624fb008d 100644 --- a/test/functional/ui/statuscolumn_spec.lua +++ b/test/functional/ui/statuscolumn_spec.lua @@ -424,6 +424,21 @@ describe('statuscolumn', function() ]]) end) + it('does not corrupt the screen with minwid sign item', function() + screen:try_resize(screen._width, 3) + screen:set_default_attr_ids({ + [0] = {foreground = Screen.colors.Brown}, + [1] = {foreground = Screen.colors.Blue4, background = Screen.colors.Gray}, + }) + command([[set stc=%6s\ %l]]) + exec_lua('vim.api.nvim_buf_set_extmark(0, ns, 7, 0, {sign_text = "𒀀"})') + screen:expect([[ + {0: 𒀀 8}^aaaaa | + {0: }{1: }{0: 9}aaaaa | + | + ]]) + end) + for _, model in ipairs(mousemodels) do it("works with 'statuscolumn' clicks with mousemodel=" .. model, function() command('set mousemodel=' .. model) diff --git a/test/functional/vimscript/api_functions_spec.lua b/test/functional/vimscript/api_functions_spec.lua index 3404b06a55..14678a966d 100644 --- a/test/functional/vimscript/api_functions_spec.lua +++ b/test/functional/vimscript/api_functions_spec.lua @@ -133,7 +133,7 @@ describe('eval-API', function() }) command("set ft=vim") - command("let &rtp='build/runtime/,'.&rtp") + command("set rtp^=build/runtime/") command("syntax on") insert([[ call bufnr('%') diff --git a/test/old/testdir/test_filetype.vim b/test/old/testdir/test_filetype.vim index 2858bf1add..9ca267f78c 100644 --- a/test/old/testdir/test_filetype.vim +++ b/test/old/testdir/test_filetype.vim @@ -126,6 +126,7 @@ let s:filename_checks = { \ 'confini': ['/etc/pacman.conf', 'any/etc/pacman.conf', 'mpv.conf', 'any/.aws/config', 'any/.aws/credentials', 'file.nmconnection'], \ 'context': ['tex/context/any/file.tex', 'file.mkii', 'file.mkiv', 'file.mkvi', 'file.mkxl', 'file.mklx'], \ 'cook': ['file.cook'], + \ 'corn': ['file.corn'], \ 'cpon': ['file.cpon'], \ 'cpp': ['file.cxx', 'file.c++', 'file.hh', 'file.hxx', 'file.hpp', 'file.ipp', 'file.moc', 'file.tcc', 'file.inl', 'file.tlh', 'file.cppm', 'file.ccm', 'file.cxxm', 'file.c++m'], \ 'cqlang': ['file.cql'], @@ -370,7 +371,7 @@ let s:filename_checks = { \ 'file.wxm', 'maxima-init.mac'], \ 'mel': ['file.mel'], \ 'mermaid': ['file.mmd', 'file.mmdc', 'file.mermaid'], - \ 'meson': ['meson.build', 'meson_options.txt'], + \ 'meson': ['meson.build', 'meson.options', 'meson_options.txt'], \ 'messages': ['/log/auth', '/log/cron', '/log/daemon', '/log/debug', '/log/kern', '/log/lpr', '/log/mail', '/log/messages', '/log/news/news', '/log/syslog', '/log/user', \ '/log/auth.log', '/log/cron.log', '/log/daemon.log', '/log/debug.log', '/log/kern.log', '/log/lpr.log', '/log/mail.log', '/log/messages.log', '/log/news/news.log', '/log/syslog.log', '/log/user.log', \ '/log/auth.err', '/log/cron.err', '/log/daemon.err', '/log/debug.err', '/log/kern.err', '/log/lpr.err', '/log/mail.err', '/log/messages.err', '/log/news/news.err', '/log/syslog.err', '/log/user.err', @@ -1449,6 +1450,12 @@ func Test_mod_file() bwipe! call delete('go.mod') + call writefile(['module M'], 'go.mod') + split go.mod + call assert_equal('gomod', &filetype) + bwipe! + call delete('go.mod') + filetype off endfunc diff --git a/test/old/testdir/test_hlsearch.vim b/test/old/testdir/test_hlsearch.vim index 043d378a39..fb1695220a 100644 --- a/test/old/testdir/test_hlsearch.vim +++ b/test/old/testdir/test_hlsearch.vim @@ -85,4 +85,23 @@ func Test_hlsearch_Ctrl_R() call StopVimInTerminal(buf) endfunc +func Test_hlsearch_clipboard() + CheckRunVimInTerminal + CheckFeature clipboard_working + + let lines =<< trim END + set incsearch hlsearch + let @* = "text" + put * + END + call writefile(lines, 'XhlsearchClipboard', 'D') + let buf = RunVimInTerminal('-S XhlsearchClipboard', #{rows: 6, cols: 60}) + + call term_sendkeys(buf, "/\<C-R>*") + call VerifyScreenDump(buf, 'Test_hlsearch_ctrlr_1', {}) + + call term_sendkeys(buf, "\<Esc>") + call StopVimInTerminal(buf) +endfunc + " vim: shiftwidth=2 sts=2 expandtab diff --git a/test/old/testdir/test_registers.vim b/test/old/testdir/test_registers.vim index bbf1aa53b5..70dac535b4 100644 --- a/test/old/testdir/test_registers.vim +++ b/test/old/testdir/test_registers.vim @@ -55,8 +55,9 @@ func Test_display_registers() call feedkeys("i\<C-R>=2*4\n\<esc>") call feedkeys(":ls\n", 'xt') - let a = execute('display') - let b = execute('registers') + " these commands work in the sandbox + let a = execute('sandbox display') + let b = execute('sandbox registers') call assert_equal(a, b) call assert_match('^\nType Name Content\n' |