diff options
52 files changed, 1816 insertions, 1293 deletions
diff --git a/.luacheckrc b/.luacheckrc index b945835bba..9c8bddb88e 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -2,6 +2,8 @@ -- Ignore W211 (unused variable) with preload files. files["**/preload.lua"] = {ignore = { "211" }} +-- Allow vim module to modify itself, but only here. +files["src/nvim/lua/vim.lua"] = {ignore = { "122/vim" }} -- Don't report unused self arguments of methods. self = false @@ -119,7 +119,7 @@ oldtest: | nvim build/runtime/doc/tags ifeq ($(strip $(TEST_FILE)),) +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" $(MAKEOVERRIDES) else - +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" NEW_TESTS=$(TEST_FILE) SCRIPTS= $(MAKEOVERRIDES) + +$(SINGLE_MAKE) -C src/nvim/testdir NVIM_PRG="$(realpath build/bin/nvim)" SCRIPTS= $(MAKEOVERRIDES) $(TEST_FILE) endif build/runtime/doc/tags helptags: | nvim diff --git a/runtime/autoload/lsp.vim b/runtime/autoload/lsp.vim deleted file mode 100644 index 4c8f8b396a..0000000000 --- a/runtime/autoload/lsp.vim +++ /dev/null @@ -1,45 +0,0 @@ -function! lsp#add_filetype_config(config) abort - call luaeval('vim.lsp.add_filetype_config(_A)', a:config) -endfunction - -function! lsp#set_log_level(level) abort - call luaeval('vim.lsp.set_log_level(_A)', a:level) -endfunction - -function! lsp#get_log_path() abort - return luaeval('vim.lsp.get_log_path()') -endfunction - -function! lsp#omnifunc(findstart, base) abort - return luaeval("vim.lsp.omnifunc(_A[1], _A[2])", [a:findstart, a:base]) -endfunction - -function! lsp#text_document_hover() abort - lua vim.lsp.buf_request(nil, 'textDocument/hover', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_declaration() abort - lua vim.lsp.buf_request(nil, 'textDocument/declaration', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_definition() abort - lua vim.lsp.buf_request(nil, 'textDocument/definition', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_signature_help() abort - lua vim.lsp.buf_request(nil, 'textDocument/signatureHelp', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_type_definition() abort - lua vim.lsp.buf_request(nil, 'textDocument/typeDefinition', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction - -function! lsp#text_document_implementation() abort - lua vim.lsp.buf_request(nil, 'textDocument/implementation', vim.lsp.protocol.make_text_document_position_params()) - return '' -endfunction diff --git a/runtime/autoload/man.vim b/runtime/autoload/man.vim index 36f42c0003..809e4a19d8 100644 --- a/runtime/autoload/man.vim +++ b/runtime/autoload/man.vim @@ -1,4 +1,4 @@ -" Maintainer: Anmol Sethi <anmol@aubble.com> +" Maintainer: Anmol Sethi <hi@nhooyr.io> if exists('s:loaded_man') finish @@ -139,7 +139,7 @@ function! s:get_page(path) abort " Disable hard-wrap by using a big $MANWIDTH (max 1000 on some systems #9065). " Soft-wrap: ftplugin/man.vim sets wrap/breakindent/ā¦. " Hard-wrap: driven by `man`. - let manwidth = !get(g:,'man_hardwrap') ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH) + let manwidth = !get(g:,'man_hardwrap', 1) ? 999 : (empty($MANWIDTH) ? winwidth(0) : $MANWIDTH) " Force MANPAGER=cat to ensure Vim is not recursively invoked (by man-db). " http://comments.gmane.org/gmane.editors.vim.devel/29085 " Set MAN_KEEP_FORMATTING so Debian man doesn't discard backspaces. @@ -357,6 +357,10 @@ function! s:format_candidate(path, psect) abort endfunction function! man#init_pager() abort + " https://github.com/neovim/neovim/issues/6828 + let og_modifiable = &modifiable + setlocal modifiable + if getline(1) =~# '^\s*$' silent keepjumps 1delete _ else @@ -374,6 +378,8 @@ function! man#init_pager() abort if -1 == match(bufname('%'), 'man:\/\/') " Avoid duplicate buffers, E95. execute 'silent file man://'.tolower(fnameescape(ref)) endif + + let &l:modifiable = og_modifiable endfunction function! man#goto_tag(pattern, flags, info) abort diff --git a/runtime/doc/api.txt b/runtime/doc/api.txt index 57a72e6173..f97795b0ee 100644 --- a/runtime/doc/api.txt +++ b/runtime/doc/api.txt @@ -933,10 +933,11 @@ nvim_open_win({buffer}, {enter}, {config}) *nvim_open_win()* 'number', 'relativenumber', 'cursorline', 'cursorcolumn', 'foldcolumn', 'spell' and 'list' options. 'signcolumn' is changed to - `auto` . The end-of-buffer region is hidden - by setting `eob` flag of 'fillchars' to a - space char, and clearing the |EndOfBuffer| - region in 'winhighlight'. + `auto` and 'colorcolumn' is cleared. The + end-of-buffer region is hidden by setting + `eob` flag of 'fillchars' to a space char, and + clearing the |EndOfBuffer| region in + 'winhighlight'. Return: ~ Window handle, or 0 on error diff --git a/runtime/doc/filetype.txt b/runtime/doc/filetype.txt index c579c390c6..c649688d99 100644 --- a/runtime/doc/filetype.txt +++ b/runtime/doc/filetype.txt @@ -549,7 +549,9 @@ Variables: *b:man_default_sects* Comma-separated, ordered list of preferred sections. For example in C one usually wants section 3 or 2: > :let b:man_default_sections = '3,2' -*g:man_hardwrap* Hard-wrap to $MANWIDTH. May improve layout. +*g:man_hardwrap* Hard-wrap to $MANWIDTH or window width if $MANWIDTH is + empty. Enabled by default. Set |FALSE| to enable soft + wrapping. To use Nvim as a manpager: > export MANPAGER='nvim +Man!' @@ -558,10 +560,13 @@ Note that when running `man` from the shell and with that `MANPAGER` in your environment, `man` will pre-format the manpage using `groff`. Thus, Neovim will inevitably display the manual page as it was passed to it from stdin. One of the caveats of this is that the width will _always_ be hard-wrapped and not -soft wrapped as with `:Man`. You can set in your environment: > +soft wrapped as with `g:man_hardwrap=0`. You can set in your environment: > export MANWIDTH=999 -So `groff`'s pre-formatting output will be the same as with `:Man` i.e soft-wrapped. +So `groff`'s pre-formatting output will be the same as with `g:man_hardwrap=0` i.e soft-wrapped. + +To disable bold highlighting: > + :highlight link manBold Normal PDF *ft-pdf-plugin* diff --git a/runtime/doc/lsp.txt b/runtime/doc/lsp.txt index 26850b3683..3de2a9e4e6 100644 --- a/runtime/doc/lsp.txt +++ b/runtime/doc/lsp.txt @@ -24,106 +24,15 @@ After installing a language server to your machine, you must let Neovim know how to start and interact with that language server. To do so, you can either: -- Use the |vim.lsp.add_filetype_config()|, which solves the common use-case of - a single server for one or more filetypes. This can also be used from vim - via |lsp#add_filetype_config()|. +- Use https://github.com/neovim/nvim-lsp and one of the existing servers there + or set up a new one using the `nvim_lsp/skeleton` interface (and contribute + it if you find it useful). This uses |vim.lsp.start_client()| under the + hood. - Or |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()|. These are the backbone of the LSP API. These are easy to use enough for basic or more complex configurations such as in |lsp-advanced-js-example|. ================================================================================ - *lsp-filetype-config* - -These are utilities specific to filetype based configurations. - - *lsp#add_filetype_config()* - *vim.lsp.add_filetype_config()* -lsp#add_filetype_config({config}) for Vim. -vim.lsp.add_filetype_config({config}) for Lua - - These are functions which can be used to create a simple configuration which - will start a language server for a list of filetypes based on the |FileType| - event. - It will lazily start start the server, meaning that it will only start once - a matching filetype is encountered. - - The {config} options are the same as |vim.lsp.start_client()|, but - with a few additions and distinctions: - - Additional parameters:~ - `filetype` - {string} or {list} of filetypes to attach to. - `name` - A unique identifying string among all other servers configured with - |vim.lsp.add_filetype_config|. - - Differences:~ - `root_dir` - Will default to |getcwd()| instead of being required. - - NOTE: the function options in {config} like {config.on_init} are for Lua - callbacks, not Vim callbacks. -> - " Go example - call lsp#add_filetype_config({ - \ 'filetype': 'go', - \ 'name': 'gopls', - \ 'cmd': 'gopls' - \ }) - " Python example - call lsp#add_filetype_config({ - \ 'filetype': 'python', - \ 'name': 'pyls', - \ 'cmd': 'pyls' - \ }) - " Rust example - call lsp#add_filetype_config({ - \ 'filetype': 'rust', - \ 'name': 'rls', - \ 'cmd': 'rls', - \ 'capabilities': { - \ 'clippy_preference': 'on', - \ 'all_targets': v:false, - \ 'build_on_save': v:true, - \ 'wait_to_build': 0 - \ }}) -< -> - -- From Lua - vim.lsp.add_filetype_config { - name = "clangd"; - filetype = {"c", "cpp"}; - cmd = "clangd -background-index"; - capabilities = { - offsetEncoding = {"utf-8", "utf-16"}; - }; - on_init = vim.schedule_wrap(function(client, result) - if result.offsetEncoding then - client.offset_encoding = result.offsetEncoding - end - end) - } -< - *vim.lsp.copy_filetype_config()* -vim.lsp.copy_filetype_config({existing_name}, [{override_config}]) - - You can use this to copy an existing filetype configuration and change it by - specifying {override_config} which will override any properties in the - existing configuration. If you don't specify a new unique name with - {override_config.name} then it will try to create one and return it. - - Returns:~ - `name` the new configuration name. - - *vim.lsp.get_filetype_client_by_name()* -vim.lsp.get_filetype_client_by_name({name}) - - Use this to look up a client by its name created from - |vim.lsp.add_filetype_config()|. - - Returns nil if the client is not active or the name is not valid. - -================================================================================ *lsp-core-api* These are the core api functions for working with clients. You will mainly be using |vim.lsp.start_client()| and |vim.lsp.buf_attach_client()| for operations @@ -178,8 +87,8 @@ vim.lsp.start_client({config}) `callbacks` A {table} of whose keys are language server method names and the values are `function(err, method, params, client_id)` See |lsp-callbacks| for - more. This will be combined with |lsp-builtin-callbacks| to provide - defaults. + more. This will be combined with |lsp-default-callbacks| to resolve + the callbacks for a client as a fallback. `init_options` A {table} of values to pass in the initialization request as @@ -203,6 +112,12 @@ vim.lsp.start_client({config}) `vim.lsp.client_errors[code]` can be used to retrieve a human understandable string. + `before_init(initialize_params, config)` + A function which is called *before* the request `initialize` is completed. + `initialize_params` contains the parameters we are sending to the server + and `config` is the config that was passed to `start_client()` for + convenience. You can use this to modify parameters before they are sent. + `on_init(client, initialize_result)` A function which is called after the request `initialize` is completed. `initialize_result` contains `capabilities` and anything else the server @@ -346,31 +261,34 @@ vim.lsp.rpc_response_error({code}, [{message}], [{data}]) the server. ================================================================================ - *vim.lsp.builtin_callbacks* + *vim.lsp.default_callbacks* -The |vim.lsp.builtin_callbacks| table contains the default |lsp-callbacks| +The |vim.lsp.default_callbacks| table contains the default |lsp-callbacks| that are used when creating a new client. The keys are the LSP method names. The following requests and notifications have built-in callbacks defined to handle the response in an idiomatic way. - textDocument/completion - textDocument/declaration - textDocument/definition - textDocument/hover - textDocument/implementation - textDocument/rename - textDocument/signatureHelp - textDocument/typeDefinition + textDocument/publishDiagnostics window/logMessage window/showMessage -You can check these via `vim.tbl_keys(vim.lsp.builtin_callbacks)`. +You can check these via `vim.tbl_keys(vim.lsp.default_callbacks)`. + +These will be used preferrentially in `vim.lsp.buf` methods when handling +requests. They will also be used when responding to server requests and +notifications. -These will be automatically used and can be overridden by users (either by -modifying the |vim.lsp.builtin_callbacks| object or on a per-client basis -by passing in a table via the {callbacks} parameter on |vim.lsp.start_client| -or |vim.lsp.add_filetype_config|. +Use cases: +- Users can modify this to customize to their preferences. +- UI plugins can modify this by assigning to + `vim.lsp.default_callbacks[method]` so as to provide more specialized + handling, allowing you to leverage the UI capabilities available. UIs should + try to be conscientious of any existing changes the user may have set + already by checking for existing values. + +Any callbacks passed directly to `request` methods on a server client will +have the highest precedence, followed by the `default_callbacks`. More information about callbacks can be found in |lsp-callbacks|. @@ -379,8 +297,8 @@ More information about callbacks can be found in |lsp-callbacks|. Callbacks are functions which are called in a variety of situations by the client. Their signature is `function(err, method, params, client_id)` They can -be set by the {callbacks} parameter for |vim.lsp.start_client| and -|vim.lsp.add_filetype_config| or via the |vim.lsp.builtin_callbacks|. +be set by the {callbacks} parameter for |vim.lsp.start_client| or via the +|vim.lsp.default_callbacks|. This will be called for: - notifications from the server, where `err` will always be `nil` @@ -409,12 +327,10 @@ vim.lsp.protocol vim.lsp.protocol.TextDocumentSyncKind[1] == "Full" Utility functions used internally are: - `vim.lsp.make_client_capabilities()` + `vim.lsp.protocol.make_client_capabilities()` Make a ClientCapabilities object. These are the builtin capabilities. - `vim.lsp.make_text_document_position_params()` - Make a TextDocumentPositionParams object. - `vim.lsp.resolve_capabilities(server_capabilites)` + `vim.lsp.protocol.resolve_capabilities(server_capabilites)` Creates a normalized object describing capabilities from the server capabilities. @@ -482,18 +398,16 @@ vim.lsp.buf_notify({bufnr}, {method}, {params}) ================================================================================ *lsp-logging* - *lsp#set_log_level()* -lsp#set_log_level({level}) + *vim.lsp.set_log_level()* +vim.lsp.set_log_level({level}) You can set the log level for language server client logging. Possible values: "trace", "debug", "info", "warn", "error" Default: "warn" - Example: `call lsp#set_log_level("debug")` + Example: `lua vim.lsp.set_log_level("debug")` - *lsp#get_log_path()* *vim.lsp.get_log_path()* -lsp#get_log_path() vim.lsp.get_log_path() Returns the path that LSP logs are written. @@ -508,43 +422,43 @@ vim.lsp.log_levels ================================================================================ *lsp-omnifunc* *vim.lsp.omnifunc()* - *lsp#omnifunc* -lsp#omnifunc({findstart}, {base}) vim.lsp.omnifunc({findstart}, {base}) To configure omnifunc, add the following in your init.vim: > - set omnifunc=lsp#omnifunc - - " This is optional, but you may find it useful - autocmd CompleteDone * pclose + " Configure for python + autocmd Filetype python setl omnifunc=v:lua.vim.lsp.omnifunc + + " Or with on_attach + start_client { + ... + on_attach = function(client, bufnr) + vim.api.nvim_buf_set_option(bufnr, 'omnifunc', 'v:lua.vim.lsp.omnifunc') + end; + } + + " This is optional, but you may find it useful + autocmd CompleteDone * pclose < ================================================================================ *lsp-vim-functions* +To use the functions from vim, it is recommended to use |v:lua| to interface +with the Lua functions. No direct vim functions are provided, but the +interface is still easy to use from mappings. + These methods can be used in mappings and are the equivalent of using the request from lua as follows: > - lua vim.lsp.buf_request(0, "textDocument/hover", vim.lsp.protocol.make_text_document_position_params()) -< - - lsp#text_document_declaration() - lsp#text_document_definition() - lsp#text_document_hover() - lsp#text_document_implementation() - lsp#text_document_signature_help() - lsp#text_document_type_definition() - -> " Example config - autocmd Filetype rust,python,go,c,cpp setl omnifunc=lsp#omnifunc - nnoremap <silent> ;dc :call lsp#text_document_declaration()<CR> - nnoremap <silent> ;df :call lsp#text_document_definition()<CR> - nnoremap <silent> ;h :call lsp#text_document_hover()<CR> - nnoremap <silent> ;i :call lsp#text_document_implementation()<CR> - nnoremap <silent> ;s :call lsp#text_document_signature_help()<CR> - nnoremap <silent> ;td :call lsp#text_document_type_definition()<CR> + autocmd Filetype rust,python,go,c,cpp setl omnifunc=v:lua.vim.lsp.omnifunc + nnoremap <silent> ;dc <cmd>lua vim.lsp.buf.declaration()<CR> + nnoremap <silent> ;df <cmd>lua vim.lsp.buf.definition()<CR> + nnoremap <silent> ;h <cmd>lua vim.lsp.buf.hover()<CR> + nnoremap <silent> ;i <cmd>lua vim.lsp.buf.implementation()<CR> + nnoremap <silent> ;s <cmd>lua vim.lsp.buf.signature_help()<CR> + nnoremap <silent> ;td <cmd>lua vim.lsp.buf.type_definition()<CR> < ================================================================================ *lsp-advanced-js-example* diff --git a/runtime/ftplugin/man.vim b/runtime/ftplugin/man.vim index b3b23833ba..7c535dc839 100644 --- a/runtime/ftplugin/man.vim +++ b/runtime/ftplugin/man.vim @@ -1,4 +1,4 @@ -" Maintainer: Anmol Sethi <anmol@aubble.com> +" Maintainer: Anmol Sethi <hi@nhooyr.io> " Previous Maintainer: SungHyun Nam <goweol@gmail.com> if exists('b:did_ftplugin') || &filetype !=# 'man' diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 0c1a145c04..928d19177a 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -1,9 +1,10 @@ -local builtin_callbacks = require 'vim.lsp.builtin_callbacks' +local default_callbacks = require 'vim.lsp.default_callbacks' local log = require 'vim.lsp.log' local lsp_rpc = require 'vim.lsp.rpc' local protocol = require 'vim.lsp.protocol' local util = require 'vim.lsp.util' +local vim = vim local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option local uv = vim.loop @@ -12,7 +13,8 @@ local validate = vim.validate local lsp = { protocol = protocol; - builtin_callbacks = builtin_callbacks; + default_callbacks = default_callbacks; + buf = require'vim.lsp.buf'; util = util; -- Allow raw RPC access. rpc = lsp_rpc; @@ -25,6 +27,11 @@ local lsp = { -- TODO improve handling of scratch buffers with LSP attached. +local function err_message(...) + nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + nvim_command("redraw") +end + local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -92,11 +99,7 @@ local function for_each_buffer_client(bufnr, callback) return end for client_id in pairs(client_ids) do - -- This is unlikely to happen. Could only potentially happen in a race - -- condition between literally a single statement. - -- We could skip this error, but let's error for now. local client = active_clients[client_id] - -- or error(string.format("Client %d has already shut down.", client_id)) if client then callback(client, client_id) end @@ -154,13 +157,13 @@ local function validate_client_config(config) root_dir = { config.root_dir, is_dir, "directory" }; callbacks = { config.callbacks, "t", true }; capabilities = { config.capabilities, "t", true }; - -- cmd = { config.cmd, "s", false }; cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" }; cmd_env = { config.cmd_env, "f", true }; name = { config.name, 's', true }; on_error = { config.on_error, "f", true }; on_exit = { config.on_exit, "f", true }; on_init = { config.on_init, "f", true }; + before_init = { config.before_init, "f", true }; offset_encoding = { config.offset_encoding, "s", true }; } local cmd, cmd_args = validate_command(config.cmd) @@ -261,6 +264,12 @@ end -- possible errors. `vim.lsp.client_errors[code]` can be used to retrieve a -- human understandable string. -- +-- before_init(initialize_params, config): A function which is called *before* +-- the request `initialize` is completed. `initialize_params` contains +-- the parameters we are sending to the server and `config` is the config that +-- was passed to `start_client()` for convenience. You can use this to modify +-- parameters before they are sent. +-- -- on_init(client, initialize_result): A function which is called after the -- request `initialize` is completed. `initialize_result` contains -- `capabilities` and anything else the server may send. For example, `clangd` @@ -290,19 +299,19 @@ function lsp.start_client(config) local client_id = next_client_id() - local callbacks = tbl_extend("keep", config.callbacks or {}, builtin_callbacks) - -- Copy metatable if it has one. - if config.callbacks and config.callbacks.__metatable then - setmetatable(callbacks, getmetatable(config.callbacks)) - end + local callbacks = config.callbacks or {} local name = config.name or tostring(client_id) local log_prefix = string.format("LSP[%s]", name) local handlers = {} + local function resolve_callback(method) + return callbacks[method] or default_callbacks[method] + end + function handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then -- Method name is provided here for convenience. callback(nil, method, params, client_id) @@ -311,7 +320,7 @@ function lsp.start_client(config) function handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) - local callback = callbacks[method] + local callback = resolve_callback(method) if callback then local _ = log.debug() and log.debug("server_request: found callback for", method) return callback(nil, method, params, client_id) @@ -322,12 +331,12 @@ function lsp.start_client(config) function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) - nvim_err_writeln(string.format('%s: Error %s: %q', log_prefix, lsp.client_errors[code], vim.inspect(err))) + err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) if config.on_error then local status, usererr = pcall(config.on_error, code, err) if not status then local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr }) - nvim_err_writeln(log_prefix.." user on_error failed: "..tostring(usererr)) + err_message(log_prefix, ' user on_error failed: ', tostring(usererr)) end end end @@ -335,9 +344,19 @@ function lsp.start_client(config) function handlers.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil - for _, client_ids in pairs(all_buffer_active_clients) do + local active_buffers = {} + for bufnr, client_ids in pairs(all_buffer_active_clients) do + if client_ids[client_id] then + table.insert(active_buffers, bufnr) + end client_ids[client_id] = nil end + -- Buffer level cleanup + vim.schedule(function() + for _, bufnr in ipairs(active_buffers) do + util.buf_clear_diagnostics(bufnr) + end + end) if config.on_exit then pcall(config.on_exit, code, signal, client_id) end @@ -379,7 +398,6 @@ function lsp.start_client(config) -- The rootUri of the workspace. Is null if no folder is open. If both -- `rootPath` and `rootUri` are set `rootUri` wins. rootUri = vim.uri_from_fname(config.root_dir); --- rootUri = vim.uri_from_fname(vim.fn.expand("%:p:h")); -- User provided initialization options. initializationOptions = config.init_options; -- The capabilities provided by the client (editor or tool) @@ -403,11 +421,15 @@ function lsp.start_client(config) -- } workspaceFolders = nil; } + if config.before_init then + -- TODO(ashkan) handle errors here. + pcall(config.before_init, initialize_params, config) + end local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params) rpc.request('initialize', initialize_params, function(init_err, result) assert(not init_err, tostring(init_err)) assert(result, "server sent empty result") - rpc.notify('initialized', {}) + rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary}) client.initialized = true uninitialized_clients[client_id] = nil client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities") @@ -439,14 +461,14 @@ function lsp.start_client(config) local function unsupported_method(method) local msg = "server doesn't support "..method local _ = log.warn() and log.warn(msg) - nvim_err_writeln(msg) + err_message(msg) return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) end --- Checks capabilities before rpc.request-ing. function client.request(method, params, callback) if not callback then - callback = client.callbacks[method] + callback = resolve_callback(method) or error(string.format("request callback is empty and no default was found for client %s", client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback) @@ -851,7 +873,7 @@ function lsp.omnifunc(findstart, base) position = { -- 0-indexed for both line and character line = pos[1] - 1, - character = pos[2], + character = vim.str_utfindex(line, pos[2]), }; -- The completion context. This is only available if the client specifies -- to send this using `ClientCapabilities.textDocument.completion.contextSupport === true` @@ -876,134 +898,8 @@ function lsp.omnifunc(findstart, base) end end ---- ---- FileType based configuration utility ---- - -local all_filetype_configs = {} - --- Lookup a filetype config client by its name. -function lsp.get_filetype_client_by_name(name) - local config = all_filetype_configs[name] - if config.client_id then - return active_clients[config.client_id] - end -end - -local function start_filetype_config(config) - config.client_id = lsp.start_client(config) - nvim_command(string.format( - "autocmd FileType %s silent lua vim.lsp.buf_attach_client(0, %d)", - table.concat(config.filetypes, ','), - config.client_id)) - return config.client_id -end - --- Easy configuration option for common LSP use-cases. --- This will lazy initialize the client when the filetypes specified are --- encountered and attach to those buffers. --- --- The configuration options are the same as |vim.lsp.start_client()|, but --- with a few additions and distinctions: --- --- Additional parameters: --- - filetype: {string} or {list} of filetypes to attach to. --- - name: A unique string among all other servers configured with --- |vim.lsp.add_filetype_config|. --- --- Differences: --- - root_dir: will default to |getcwd()| --- -function lsp.add_filetype_config(config) - -- Additional defaults. - -- Keep a copy of the user's input for debugging reasons. - local user_config = config - config = tbl_extend("force", {}, user_config) - config.root_dir = config.root_dir or uv.cwd() - -- Validate config. - validate_client_config(config) - validate { - name = { config.name, 's' }; - } - assert(config.filetype, "config must have 'filetype' key") - - local filetypes - if type(config.filetype) == 'string' then - filetypes = { config.filetype } - elseif type(config.filetype) == 'table' then - filetypes = config.filetype - assert(not tbl_isempty(filetypes), "config.filetype must not be an empty table") - else - error("config.filetype must be a string or a list of strings") - end - - if all_filetype_configs[config.name] then - -- If the client exists, then it is likely that they are doing some kind of - -- reload flow, so let's not throw an error here. - if all_filetype_configs[config.name].client_id then - -- TODO log here? It might be unnecessarily annoying. - return - end - error(string.format('A configuration with the name %q already exists. They must be unique', config.name)) - end - - all_filetype_configs[config.name] = tbl_extend("keep", config, { - client_id = nil; - filetypes = filetypes; - user_config = user_config; - }) - - nvim_command(string.format( - "autocmd FileType %s ++once silent lua vim.lsp._start_filetype_config_client(%q)", - table.concat(filetypes, ','), - config.name)) -end - --- Create a copy of an existing configuration, and override config with values --- from new_config. --- This is useful if you wish you create multiple LSPs with different root_dirs --- or other use cases. --- --- You can specify a new unique name, but if you do not, a unique name will be --- created like `name-dup_count`. --- --- existing_name: the name of the existing config to copy. --- new_config: the new configuration options. @see |vim.lsp.start_client()|. --- @returns string the new name. -function lsp.copy_filetype_config(existing_name, new_config) - local config = all_filetype_configs[existing_name] - or error(string.format("Configuration with name %q doesn't exist", existing_name)) - config = tbl_extend("force", config, new_config or {}) - config.client_id = nil - config.original_config_name = existing_name - - -- If the user didn't rename it, we will. - if config.name == existing_name then - -- Create a new, unique name. - local duplicate_count = 0 - for _, conf in pairs(all_filetype_configs) do - if conf.original_config_name == existing_name then - duplicate_count = duplicate_count + 1 - end - end - config.name = string.format("%s-%d", existing_name, duplicate_count + 1) - end - print("New config name:", config.name) - lsp.add_filetype_config(config) - return config.name -end - --- Autocmd handler to actually start the client when an applicable filetype is --- encountered. -function lsp._start_filetype_config_client(name) - local config = all_filetype_configs[name] - -- If it exists and is running, don't make it again. - if config.client_id and active_clients[config.client_id] then - -- TODO log here? - return - end - lsp.buf_attach_client(0, start_filetype_config(config)) - return config.client_id +function lsp.client_is_stopped(client_id) + return active_clients[client_id] == nil end --- @@ -1030,7 +926,7 @@ end -- Print some debug information about all LSP related things. -- The output of this function should not be relied upon and may change. function lsp.print_debug_info() - print(vim.inspect({ clients = active_clients, filetype_configs = all_filetype_configs })) + print(vim.inspect({ clients = active_clients })) end -- Log level dictionary with reverse lookup as well. diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua new file mode 100644 index 0000000000..51295e5570 --- /dev/null +++ b/runtime/lua/vim/lsp/buf.lua @@ -0,0 +1,349 @@ +local vim = vim +local validate = vim.validate +local api = vim.api +local vfn = vim.fn +local util = require 'vim.lsp.util' +local log = require 'vim.lsp.log' +local list_extend = vim.list_extend + +local M = {} + +local function ok_or_nil(status, ...) + if not status then return end + return ... +end +local function npcall(fn, ...) + return ok_or_nil(pcall(fn, ...)) +end + +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + +local function find_window_by_var(name, value) + for _, win in ipairs(api.nvim_list_wins()) do + if npcall(api.nvim_win_get_var, win, name) == value then + return win + end + end +end + +local function request(method, params, callback) + -- TODO(ashkan) enable this. + -- callback = vim.lsp.default_callbacks[method] or callback + validate { + method = {method, 's'}; + callback = {callback, 'f'}; + } + return vim.lsp.buf_request(0, method, params, function(err, _, result, client_id) + local _ = log.debug() and log.debug("vim.lsp.buf", method, client_id, err, result) + if err then error(tostring(err)) end + return callback(err, method, result, client_id) + end) +end + +local function focusable_preview(method, params, fn) + if npcall(api.nvim_win_get_var, 0, method) then + return api.nvim_command("wincmd p") + end + + local bufnr = api.nvim_get_current_buf() + do + local win = find_window_by_var(method, bufnr) + if win then + api.nvim_set_current_win(win) + api.nvim_command("stopinsert") + return + end + end + return request(method, params, function(_, _, result, _) + -- TODO(ashkan) could show error in preview... + local lines, filetype, opts = fn(result) + if lines then + local _, winnr = util.open_floating_preview(lines, filetype, opts) + api.nvim_win_set_var(winnr, method, bufnr) + end + end) +end + +function M.hover() + local params = util.make_position_params() + focusable_preview('textDocument/hover', params, function(result) + if not (result and result.contents) then return end + + local markdown_lines = util.convert_input_to_markdown_lines(result.contents) + markdown_lines = util.trim_empty_lines(markdown_lines) + if vim.tbl_isempty(markdown_lines) then + return { 'No information available' } + end + return markdown_lines, util.try_trim_markdown_code_blocks(markdown_lines) + end) +end + +function M.peek_definition() + local params = util.make_position_params() + request('textDocument/peekDefinition', params, function(_, _, result, _) + if not (result and result[1]) then return end + local loc = result[1] + local bufnr = vim.uri_to_bufnr(loc.uri) or error("couldn't find file "..tostring(loc.uri)) + local start = loc.range.start + local finish = loc.range["end"] + util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) + local headbuf = util.open_floating_preview({"Peek:"}, nil, { + offset_y = -(finish.line - start.line); + width = finish.character - start.character + 2; + }) + -- TODO(ashkan) change highlight group? + api.nvim_buf_add_highlight(headbuf, -1, 'Keyword', 0, -1) + end) +end + + +local function update_tagstack() + local bufnr = api.nvim_get_current_buf() + local line = vfn.line('.') + local col = vfn.col('.') + local tagname = vfn.expand('<cWORD>') + local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } + local winid = vfn.win_getid() + local tagstack = vfn.gettagstack(winid) + local action + if tagstack.length == tagstack.curidx then + action = 'r' + tagstack.items[tagstack.curidx] = item + elseif tagstack.length > tagstack.curidx then + action = 'r' + if tagstack.curidx > 1 then + tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) + else + tagstack.items = { item } + end + else + action = 'a' + tagstack.items = { item } + end + tagstack.curidx = tagstack.curidx + 1 + vfn.settagstack(winid, tagstack, action) +end +local function handle_location(result) + -- We can sometimes get a list of locations, so set the first value as the + -- only value we want to handle + -- TODO(ashkan) was this correct^? We could use location lists. + if result[1] ~= nil then + result = result[1] + end + if result.uri == nil then + err_message('[LSP] Could not find a valid location') + return + end + local bufnr = vim.uri_to_bufnr(result.uri) + update_tagstack() + api.nvim_set_current_buf(bufnr) + local row = result.range.start.line + local col = result.range.start.character + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = #line:sub(1, col) + api.nvim_win_set_cursor(0, {row + 1, col}) + return true +end +local function location_callback(_, method, result) + if result == nil or vim.tbl_isempty(result) then + local _ = log.info() and log.info(method, 'No location found') + return nil + end + return handle_location(result) +end + +function M.declaration() + local params = util.make_position_params() + request('textDocument/declaration', params, location_callback) +end + +function M.definition() + local params = util.make_position_params() + request('textDocument/definition', params, location_callback) +end + +function M.type_definition() + local params = util.make_position_params() + request('textDocument/typeDefinition', params, location_callback) +end + +function M.implementation() + local params = util.make_position_params() + request('textDocument/implementation', params, location_callback) +end + +--- Convert SignatureHelp response to preview contents. +-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +local function signature_help_to_preview_contents(input) + if not input.signatures then + return + end + --The active signature. If omitted or the value lies outside the range of + --`signatures` the value defaults to zero or is ignored if `signatures.length + --=== 0`. Whenever possible implementors should make an active decision about + --the active signature and shouldn't rely on a default value. + local contents = {} + local active_signature = input.activeSignature or 0 + -- If the activeSignature is not inside the valid range, then clip it. + if active_signature >= #input.signatures then + active_signature = 0 + end + local signature = input.signatures[active_signature + 1] + if not signature then + return + end + list_extend(contents, vim.split(signature.label, '\n', true)) + if signature.documentation then + util.convert_input_to_markdown_lines(signature.documentation, contents) + end + if input.parameters then + local active_parameter = input.activeParameter or 0 + -- If the activeParameter is not inside the valid range, then clip it. + if active_parameter >= #input.parameters then + active_parameter = 0 + end + local parameter = signature.parameters and signature.parameters[active_parameter] + if parameter then + --[=[ + --Represents a parameter of a callable-signature. A parameter can + --have a label and a doc-comment. + interface ParameterInformation { + --The label of this parameter information. + -- + --Either a string or an inclusive start and exclusive end offsets within its containing + --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 + --string representation as `Position` and `Range` does. + -- + --*Note*: a label of type string should be a substring of its containing signature label. + --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. + label: string | [number, number]; + --The human-readable doc-comment of this parameter. Will be shown + --in the UI but can be omitted. + documentation?: string | MarkupContent; + } + --]=] + -- TODO highlight parameter + if parameter.documentation then + util.convert_input_to_markdown_lines(parameter.documentation, contents) + end + end + end + return contents +end + +function M.signature_help() + local params = util.make_position_params() + focusable_preview('textDocument/signatureHelp', params, function(result) + if not (result and result.signatures and result.signatures[1]) then + return { 'No signature available' } + end + + -- TODO show empty popup when signatures is empty? + local lines = signature_help_to_preview_contents(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + return { 'No signature available' } + end + return lines, util.try_trim_markdown_code_blocks(lines) + end) +end + +-- TODO(ashkan) ? +function M.completion(context) + local params = util.make_position_params() + params.context = context + return request('textDocument/completion', params, function(_, _, result) + if vim.tbl_isempty(result or {}) then return end + local row, col = unpack(api.nvim_win_get_cursor(0)) + local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) + local line_to_cursor = line:sub(col+1) + + local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) + vim.fn.complete(col, matches) + end) +end + +function M.formatting(options) + validate { options = {options, 't', true} } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + options = options; + } + return request('textDocument/formatting', params, function(_, _, result) + if not result then return end + util.apply_text_edits(result) + end) +end + +function M.range_formatting(options, start_pos, end_pos) + validate { + options = {options, 't', true}; + start_pos = {start_pos, 't', true}; + end_pos = {end_pos, 't', true}; + } + options = vim.tbl_extend('keep', options or {}, { + tabSize = api.nvim_buf_get_option(0, 'tabstop'); + insertSpaces = api.nvim_buf_get_option(0, 'expandtab'); + }) + local A = list_extend({}, start_pos or api.nvim_buf_get_mark(0, '<')) + local B = list_extend({}, end_pos or api.nvim_buf_get_mark(0, '>')) + -- convert to 0-index + A[1] = A[1] - 1 + B[1] = B[1] - 1 + -- account for encoding. + if A[2] > 0 then + A = {A[1], util.character_offset(0, unpack(A))} + end + if B[2] > 0 then + B = {B[1], util.character_offset(0, unpack(B))} + end + local params = { + textDocument = { uri = vim.uri_from_bufnr(0) }; + range = { + start = { line = A[1]; character = A[2]; }; + ["end"] = { line = B[1]; character = B[2]; }; + }; + options = options; + } + return request('textDocument/rangeFormatting', params, function(_, _, result) + if not result then return end + util.apply_text_edits(result) + end) +end + +function M.rename(new_name) + -- TODO(ashkan) use prepareRename + -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position. + local params = util.make_position_params() + new_name = new_name or npcall(vfn.input, "New Name: ") + if not (new_name and #new_name > 0) then return end + params.newName = new_name + request('textDocument/rename', params, function(_, _, result) + if not result then return end + util.apply_workspace_edit(result) + end) +end + +function M.references(context) + validate { context = { context, 't', true } } + local params = util.make_position_params() + params.context = context or { + includeDeclaration = true; + } + params[vim.type_idx] = vim.types.dictionary + request('textDocument/references', params, function(_, _, result) + if not result then return end + util.set_qflist(result) + vim.api.nvim_command("copen") + end) +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua deleted file mode 100644 index cc739ce3ad..0000000000 --- a/runtime/lua/vim/lsp/builtin_callbacks.lua +++ /dev/null @@ -1,296 +0,0 @@ ---- Implements the following default callbacks: --- --- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks)) --- - --- textDocument/completion --- textDocument/declaration --- textDocument/definition --- textDocument/hover --- textDocument/implementation --- textDocument/publishDiagnostics --- textDocument/rename --- textDocument/signatureHelp --- textDocument/typeDefinition --- TODO codeLens/resolve --- TODO completionItem/resolve --- TODO documentLink/resolve --- TODO textDocument/codeAction --- TODO textDocument/codeLens --- TODO textDocument/documentHighlight --- TODO textDocument/documentLink --- TODO textDocument/documentSymbol --- TODO textDocument/formatting --- TODO textDocument/onTypeFormatting --- TODO textDocument/rangeFormatting --- TODO textDocument/references --- window/logMessage --- window/showMessage - -local log = require 'vim.lsp.log' -local protocol = require 'vim.lsp.protocol' -local util = require 'vim.lsp.util' -local api = vim.api - -local function split_lines(value) - return vim.split(value, '\n', true) -end - -local builtin_callbacks = {} - --- textDocument/completion --- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion -builtin_callbacks['textDocument/completion'] = function(_, _, result) - if not result or vim.tbl_isempty(result) then - return - end - local pos = api.nvim_win_get_cursor(0) - local row, col = pos[1], pos[2] - local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1]) - local line_to_cursor = line:sub(col+1) - - local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor) - local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$') - local match_start, match_finish = match_result[2], match_result[3] - - vim.fn.complete(col + 1 - (match_finish - match_start), matches) -end - --- textDocument/rename -builtin_callbacks['textDocument/rename'] = function(_, _, result) - if not result then return end - util.workspace_apply_workspace_edit(result) -end - -local function uri_to_bufnr(uri) - return vim.fn.bufadd((vim.uri_to_fname(uri))) -end - -builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result) - if not result then return end - local uri = result.uri - local bufnr = uri_to_bufnr(uri) - if not bufnr then - api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri)) - return - end - util.buf_clear_diagnostics(bufnr) - util.buf_diagnostics_save_positions(bufnr, result.diagnostics) - util.buf_diagnostics_underline(bufnr, result.diagnostics) - util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) - -- util.buf_loclist(bufnr, result.diagnostics) -end - --- textDocument/hover --- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover --- @params MarkedString | MarkedString[] | MarkupContent -builtin_callbacks['textDocument/hover'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - if result.contents ~= nil then - local markdown_lines = util.convert_input_to_markdown_lines(result.contents) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No information available' } - end - util.open_floating_preview(markdown_lines, 'markdown') - end -end - -builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then return end - -- TODO(ashkan) what to do with multiple locations? - result = result[1] - local bufnr = uri_to_bufnr(result.uri) - assert(bufnr) - local start = result.range.start - local finish = result.range["end"] - util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 }) - util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) }) -end - ---- Convert SignatureHelp response to preview contents. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp -local function signature_help_to_preview_contents(input) - if not input.signatures then - return - end - --The active signature. If omitted or the value lies outside the range of - --`signatures` the value defaults to zero or is ignored if `signatures.length - --=== 0`. Whenever possible implementors should make an active decision about - --the active signature and shouldn't rely on a default value. - local contents = {} - local active_signature = input.activeSignature or 0 - -- If the activeSignature is not inside the valid range, then clip it. - if active_signature >= #input.signatures then - active_signature = 0 - end - local signature = input.signatures[active_signature + 1] - if not signature then - return - end - vim.list_extend(contents, split_lines(signature.label)) - if signature.documentation then - util.convert_input_to_markdown_lines(signature.documentation, contents) - end - if input.parameters then - local active_parameter = input.activeParameter or 0 - -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #input.parameters then - active_parameter = 0 - end - local parameter = signature.parameters and signature.parameters[active_parameter] - if parameter then - --[=[ - --Represents a parameter of a callable-signature. A parameter can - --have a label and a doc-comment. - interface ParameterInformation { - --The label of this parameter information. - -- - --Either a string or an inclusive start and exclusive end offsets within its containing - --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16 - --string representation as `Position` and `Range` does. - -- - --*Note*: a label of type string should be a substring of its containing signature label. - --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`. - label: string | [number, number]; - --The human-readable doc-comment of this parameter. Will be shown - --in the UI but can be omitted. - documentation?: string | MarkupContent; - } - --]=] - -- TODO highlight parameter - if parameter.documentation then - util.convert_input_to_markdown_lines(parameter.documentation, contents) - end - end - end - return contents -end - --- textDocument/signatureHelp --- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp -builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result) - if result == nil or vim.tbl_isempty(result) then - return - end - - -- TODO show empty popup when signatures is empty? - if #result.signatures > 0 then - local markdown_lines = signature_help_to_preview_contents(result) - if vim.tbl_isempty(markdown_lines) then - markdown_lines = { 'No signature available' } - end - util.open_floating_preview(markdown_lines, 'markdown') - end -end - -local function update_tagstack() - local bufnr = api.nvim_get_current_buf() - local line = vim.fn.line('.') - local col = vim.fn.col('.') - local tagname = vim.fn.expand('<cWORD>') - local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname } - local winid = vim.fn.win_getid() - local tagstack = vim.fn.gettagstack(winid) - - local action - - if tagstack.length == tagstack.curidx then - action = 'r' - tagstack.items[tagstack.curidx] = item - elseif tagstack.length > tagstack.curidx then - action = 'r' - if tagstack.curidx > 1 then - tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item) - else - tagstack.items = { item } - end - else - action = 'a' - tagstack.items = { item } - end - - tagstack.curidx = tagstack.curidx + 1 - vim.fn.settagstack(winid, tagstack, action) -end - -local function handle_location(result) - -- We can sometimes get a list of locations, so set the first value as the - -- only value we want to handle - -- TODO(ashkan) was this correct^? We could use location lists. - if result[1] ~= nil then - result = result[1] - end - if result.uri == nil then - api.nvim_err_writeln('[LSP] Could not find a valid location') - return - end - local result_file = vim.uri_to_fname(result.uri) - local bufnr = vim.fn.bufadd(result_file) - update_tagstack() - api.nvim_set_current_buf(bufnr) - local start = result.range.start - api.nvim_win_set_cursor(0, {start.line + 1, start.character}) -end - -local function location_callback(_, method, result) - if result == nil or vim.tbl_isempty(result) then - local _ = log.info() and log.info(method, 'No location found') - return nil - end - handle_location(result) - return true -end - -local location_callbacks = { - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration - 'textDocument/declaration'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition - 'textDocument/definition'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation - 'textDocument/implementation'; - -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition - 'textDocument/typeDefinition'; -} - -for _, location_method in ipairs(location_callbacks) do - builtin_callbacks[location_method] = location_callback -end - -local function log_message(_, _, result, client_id) - local message_type = result.type - local message = result.message - local client = vim.lsp.get_client_by_id(client_id) - local client_name = client and client.name or string.format("id=%d", client_id) - if not client then - api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name)) - end - if message_type == protocol.MessageType.Error then - -- Might want to not use err_writeln, - -- but displaying a message with red highlights or something - api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message)) - else - local message_type_name = protocol.MessageType[message_type] - api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) - end - return result -end - -builtin_callbacks['window/showMessage'] = log_message -builtin_callbacks['window/logMessage'] = log_message - --- Add boilerplate error validation and logging for all of these. -for k, fn in pairs(builtin_callbacks) do - builtin_callbacks[k] = function(err, method, params, client_id) - local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err }) - if err then - error(tostring(err)) - end - return fn(err, method, params, client_id) - end -end - -return builtin_callbacks --- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/default_callbacks.lua b/runtime/lua/vim/lsp/default_callbacks.lua new file mode 100644 index 0000000000..2a891e7d1d --- /dev/null +++ b/runtime/lua/vim/lsp/default_callbacks.lua @@ -0,0 +1,69 @@ +local log = require 'vim.lsp.log' +local protocol = require 'vim.lsp.protocol' +local util = require 'vim.lsp.util' +local api = vim.api + +local M = {} + +local function err_message(...) + api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) + api.nvim_command("redraw") +end + +M['workspace/applyEdit'] = function(_, _, workspace_edit) + if not workspace_edit then return end + -- TODO(ashkan) Do something more with label? + if workspace_edit.label then + print("Workspace edit", workspace_edit.label) + end + util.apply_workspace_edit(workspace_edit.edit) +end + +M['textDocument/publishDiagnostics'] = function(_, _, result) + if not result then return end + local uri = result.uri + local bufnr = vim.uri_to_bufnr(uri) + if not bufnr then + err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri) + return + end + util.buf_clear_diagnostics(bufnr) + util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + util.buf_diagnostics_underline(bufnr, result.diagnostics) + util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) + -- util.set_loclist(result.diagnostics) +end + +local function log_message(_, _, result, client_id) + local message_type = result.type + local message = result.message + local client = vim.lsp.get_client_by_id(client_id) + local client_name = client and client.name or string.format("id=%d", client_id) + if not client then + err_message("LSP[", client_name, "] client has shut down after sending the message") + end + if message_type == protocol.MessageType.Error then + err_message("LSP[", client_name, "] ", message) + else + local message_type_name = protocol.MessageType[message_type] + api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message)) + end + return result +end + +M['window/showMessage'] = log_message +M['window/logMessage'] = log_message + +-- Add boilerplate error validation and logging for all of these. +for k, fn in pairs(M) do + M[k] = function(err, method, params, client_id) + local _ = log.debug() and log.debug('default_callback', method, { params = params, client_id = client_id, err = err }) + if err then + error(tostring(err)) + end + return fn(err, method, params, client_id) + end +end + +return M +-- vim:sw=2 ts=2 et diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index 1413a88ce2..ead90cc75a 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -10,7 +10,6 @@ end --[=[ -- Useful for interfacing with: --- https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-14.md -- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md function transform_schema_comments() nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]] @@ -681,19 +680,6 @@ function protocol.make_client_capabilities() } end -function protocol.make_text_document_position_params() - local position = vim.api.nvim_win_get_cursor(0) - return { - textDocument = { - uri = vim.uri_from_bufnr() - }; - position = { - line = position[1] - 1; - character = position[2]; - } - } -end - --[=[ export interface DocumentFilter { --A language id, like `typescript`. diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index e0ec8863d6..a558f66a42 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -340,6 +340,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para local decoded, err = json_decode(body) if not decoded then on_error(client_errors.INVALID_SERVER_JSON, err) + return end local _ = log.debug() and log.debug("decoded", decoded) diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index f96e0f01a8..2dfcdfc70c 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -1,4 +1,5 @@ local protocol = require 'vim.lsp.protocol' +local vim = vim local validate = vim.validate local api = vim.api @@ -26,11 +27,90 @@ local function remove_prefix(prefix, word) return word:sub(prefix_length + 1) end -local function resolve_bufnr(bufnr) - if bufnr == nil or bufnr == 0 then - return api.nvim_get_current_buf() +-- TODO(ashkan) @performance this could do less copying. +function M.set_lines(lines, A, B, new_lines) + -- 0-indexing to 1-indexing + local i_0 = A[1] + 1 + local i_n = B[1] + 1 + if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then + error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines}) + end + local prefix = "" + local suffix = lines[i_n]:sub(B[2]+1) + if A[2] > 0 then + prefix = lines[i_0]:sub(1, A[2]) + end + local n = i_n - i_0 + 1 + if n ~= #new_lines then + for _ = 1, n - #new_lines do table.remove(lines, i_0) end + for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end + end + for i = 1, #new_lines do + lines[i - 1 + i_0] = new_lines[i] + end + if #suffix > 0 then + local i = i_0 + #new_lines - 1 + lines[i] = lines[i]..suffix + end + if #prefix > 0 then + lines[i_0] = prefix..lines[i_0] + end + return lines +end + +local function sort_by_key(fn) + return function(a,b) + local ka, kb = fn(a), fn(b) + assert(#ka == #kb) + for i = 1, #ka do + if ka[i] ~= kb[i] then + return ka[i] < kb[i] + end + end + -- every value must have been equal here, which means it's not less than. + return false end - return bufnr +end +local edit_sort_key = sort_by_key(function(e) + return {e.A[1], e.A[2], e.i} +end) + +function M.apply_text_edits(text_edits, bufnr) + if not next(text_edits) then return end + local start_line, finish_line = math.huge, -1 + local cleaned = {} + for i, e in ipairs(text_edits) do + start_line = math.min(e.range.start.line, start_line) + finish_line = math.max(e.range["end"].line, finish_line) + -- TODO(ashkan) sanity check ranges for overlap. + table.insert(cleaned, { + i = i; + A = {e.range.start.line; e.range.start.character}; + B = {e.range["end"].line; e.range["end"].character}; + lines = vim.split(e.newText, '\n', true); + }) + end + + -- Reverse sort the orders so we can apply them without interfering with + -- eachother. Also add i as a sort key to mimic a stable sort. + table.sort(cleaned, edit_sort_key) + local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false) + local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol') + local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) == finish_line + 1 + if set_eol and #lines[#lines] ~= 0 then + table.insert(lines, '') + end + + for i = #cleaned, 1, -1 do + local e = cleaned[i] + local A = {e.A[1] - start_line, e.A[2]} + local B = {e.B[1] - start_line, e.B[2]} + lines = M.set_lines(lines, A, B, e.lines) + end + if set_eol and #lines[#lines] == 0 then + table.remove(lines) + end + api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines) end -- local valid_windows_path_characters = "[^<>:\"/\\|?*]" @@ -40,30 +120,6 @@ end -- function M.glob_to_regex(glob) -- end ---- Apply the TextEdit response. --- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_edit(text_edit, bufnr) - bufnr = resolve_bufnr(bufnr) - local range = text_edit.range - local start = range.start - local finish = range['end'] - local new_lines = split_lines(text_edit.newText) - if start.character == 0 and finish.character == 0 then - api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines) - return - end - api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0') - error('apply_text_edit currently only supports character ranges starting at 0') - return - -- TODO test and finish this support for character ranges. --- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false) --- local suffix = lines[#lines]:sub(finish.character+2) --- local prefix = lines[1]:sub(start.character+2) --- new_lines[#new_lines] = new_lines[#new_lines]..suffix --- new_lines[1] = prefix..new_lines[1] --- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines) -end - -- textDocument/completion response returns one of CompletionItem[], CompletionList or null. -- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) @@ -78,18 +134,15 @@ end --- Apply the TextDocumentEdit response. -- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.text_document_apply_text_document_edit(text_document_edit, bufnr) - -- local text_document = text_document_edit.textDocument - -- TODO use text_document_version? - -- local text_document_version = text_document.version - - -- TODO technically, you could do this without doing multiple buf_get/set - -- by getting the full region (smallest line and largest line) and doing - -- the edits on the buffer, and then applying the buffer at the end. - -- I'm not sure if that's better. - for _, text_edit in ipairs(text_document_edit.edits) do - M.text_document_apply_text_edit(text_edit, bufnr) +function M.apply_text_document_edit(text_document_edit) + local text_document = text_document_edit.textDocument + local bufnr = vim.uri_to_bufnr(text_document.uri) + -- TODO(ashkan) check this is correct. + if api.nvim_buf_get_changedtick(bufnr) > text_document.version then + print("Buffer ", text_document.uri, " newer than edits.") + return end + M.apply_text_edits(text_document_edit.edits, bufnr) end function M.get_current_line_to_cursor() @@ -145,32 +198,27 @@ function M.text_document_completion_list_to_complete_items(result, line_prefix) end -- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification -function M.workspace_apply_workspace_edit(workspace_edit) +function M.apply_workspace_edit(workspace_edit) if workspace_edit.documentChanges then for _, change in ipairs(workspace_edit.documentChanges) do if change.kind then -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile error(string.format("Unsupported change: %q", vim.inspect(change))) else - M.text_document_apply_text_document_edit(change) + M.apply_text_document_edit(change) end end return end - if workspace_edit.changes == nil or #workspace_edit.changes == 0 then + local all_changes = workspace_edit.changes + if not (all_changes and not vim.tbl_isempty(all_changes)) then return end - for uri, changes in pairs(workspace_edit.changes) do - local fname = vim.uri_to_fname(uri) - -- TODO improve this approach. Try to edit open buffers without switching. - -- Not sure how to handle files which aren't open. This is deprecated - -- anyway, so I guess it could be left as is. - api.nvim_command('edit '..fname) - for _, change in ipairs(changes) do - M.text_document_apply_text_edit(change) - end + for uri, changes in pairs(all_changes) do + local bufnr = vim.uri_to_bufnr(uri) + M.apply_text_edits(changes, bufnr) end end @@ -261,29 +309,27 @@ function M.open_floating_preview(contents, filetype, opts) filetype = { filetype, 's', true }; opts = { opts, 't', true }; } + opts = opts or {} -- Trim empty lines from the end. - for i = #contents, 1, -1 do - if #contents[i] == 0 then - table.remove(contents) - else - break + contents = M.trim_empty_lines(contents) + + local width = opts.width + local height = opts.height or #contents + if not width then + width = 0 + for i, line in ipairs(contents) do + -- Clean up the input and add left pad. + line = " "..line:gsub("\r", "") + -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. + local line_width = vim.fn.strdisplaywidth(line) + width = math.max(line_width, width) + contents[i] = line end + -- Add right padding of 1 each. + width = width + 1 end - local width = 0 - local height = #contents - for i, line in ipairs(contents) do - -- Clean up the input and add left pad. - line = " "..line:gsub("\r", "") - -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced. - local line_width = vim.fn.strdisplaywidth(line) - width = math.max(line_width, width) - contents[i] = line - end - -- Add right padding of 1 each. - width = width + 1 - local floating_bufnr = api.nvim_create_buf(false, true) if filetype then api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype) @@ -295,7 +341,8 @@ function M.open_floating_preview(contents, filetype, opts) end api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents) api.nvim_buf_set_option(floating_bufnr, 'modifiable', false) - api.nvim_command("autocmd CursorMoved <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") + -- TODO make InsertCharPre disappearing optional? + api.nvim_command("autocmd CursorMoved,BufHidden,InsertCharPre <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)") return floating_bufnr, floating_winnr end @@ -527,30 +574,140 @@ do end end -function M.buf_loclist(bufnr, locations) - local targetwin - for _, winnr in ipairs(api.nvim_list_wins()) do - local winbuf = api.nvim_win_get_buf(winnr) - if winbuf == bufnr then - targetwin = winnr - break - end - end - if not targetwin then return end +local position_sort = sort_by_key(function(v) + return {v.line, v.character} +end) +-- Returns the items with the byte position calculated correctly and in sorted +-- order. +function M.locations_to_items(locations) local items = {} - local path = api.nvim_buf_get_name(bufnr) + local grouped = setmetatable({}, { + __index = function(t, k) + local v = {} + rawset(t, k, v) + return v + end; + }) for _, d in ipairs(locations) do - -- TODO: URL parsing here? local start = d.range.start - table.insert(items, { - filename = path, - lnum = start.line + 1, - col = start.character + 1, - text = d.message, - }) + local fname = assert(vim.uri_to_fname(d.uri)) + table.insert(grouped[fname], start) + end + local keys = vim.tbl_keys(grouped) + table.sort(keys) + -- TODO(ashkan) I wish we could do this lazily. + for _, fname in ipairs(keys) do + local rows = grouped[fname] + table.sort(rows, position_sort) + local i = 0 + for line in io.lines(fname) do + for _, pos in ipairs(rows) do + local row = pos.line + if i == row then + local col + if pos.character > #line then + col = #line + else + col = vim.str_byteindex(line, pos.character) + end + table.insert(items, { + filename = fname, + lnum = row + 1, + col = col + 1; + }) + end + end + i = i + 1 + end + end + return items +end + +-- locations is Location[] +-- Only sets for the current window. +function M.set_loclist(locations) + vim.fn.setloclist(0, {}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) +end + +-- locations is Location[] +function M.set_qflist(locations) + vim.fn.setqflist({}, ' ', { + title = 'Language Server'; + items = M.locations_to_items(locations); + }) +end + +-- Remove empty lines from the beginning and end. +function M.trim_empty_lines(lines) + local start = 1 + for i = 1, #lines do + if #lines[i] > 0 then + start = i + break + end + end + local finish = 1 + for i = #lines, 1, -1 do + if #lines[i] > 0 then + finish = i + break + end + end + return vim.list_extend({}, lines, start, finish) +end + +-- Accepts markdown lines and tries to reduce it to a filetype if it is +-- just a single code block. +-- Note: This modifies the input. +-- +-- Returns: filetype or 'markdown' if it was unchanged. +function M.try_trim_markdown_code_blocks(lines) + local language_id = lines[1]:match("^```(.*)") + if language_id then + local has_inner_code_fence = false + for i = 2, (#lines - 1) do + local line = lines[i] + if line:sub(1,3) == '```' then + has_inner_code_fence = true + break + end + end + -- No inner code fences + starting with code fence = hooray. + if not has_inner_code_fence then + table.remove(lines, 1) + table.remove(lines) + return language_id + end + end + return 'markdown' +end + +local str_utfindex = vim.str_utfindex +function M.make_position_params() + local row, col = unpack(api.nvim_win_get_cursor(0)) + row = row - 1 + local line = api.nvim_buf_get_lines(0, row, row+1, true)[1] + col = str_utfindex(line, col) + return { + textDocument = { uri = vim.uri_from_bufnr(0) }; + position = { line = row; character = col; } + } +end + +-- @param buf buffer handle or 0 for current. +-- @param row 0-indexed line +-- @param col 0-indexed byte offset in line +function M.character_offset(buf, row, col) + local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1] + -- If the col is past the EOL, use the line length. + if col > #line then + return str_utfindex(line) end - vim.fn.setloclist(targetwin, items, ' ', 'Language Server') + return str_utfindex(line, col) end return M diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index ff89acc524..1912d3708d 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -4,7 +4,7 @@ -- test-suite. If, in the future, Nvim itself is used to run the test-suite -- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua -local vim = {} +local vim = vim or {} --- Returns a deep copy of the given object. Non-table objects are copied as --- in a typical Lua assignment, whereas table objects are copied recursively. @@ -226,18 +226,25 @@ function vim.tbl_add_reverse_lookup(o) return o end ---- Extends a list-like table with the values of another list-like table. ---- ---NOTE: This *mutates* dst! ---@see |extend()| ---- ---@param dst The list which will be modified and appended to. ---@param src The list from which values will be inserted. -function vim.list_extend(dst, src) - assert(type(dst) == 'table', "dst must be a table") - assert(type(src) == 'table', "src must be a table") - for _, v in ipairs(src) do - table.insert(dst, v) +-- Extends a list-like table with the values of another list-like table. +-- +-- NOTE: This *mutates* dst! +-- @see |extend()| +-- +-- @param dst list which will be modified and appended to. +-- @param src list from which values will be inserted. +-- @param start Start index on src. defaults to 1 +-- @param finish Final index on src. defaults to #src +-- @returns dst +function vim.list_extend(dst, src, start, finish) + vim.validate { + dst = {dst, 't'}; + src = {src, 't'}; + start = {start, 'n', true}; + finish = {finish, 'n', true}; + } + for i = start or 1, finish or #src do + table.insert(dst, src[i]) end return dst end diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 0a6e0fcb97..1065f84f4c 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -77,13 +77,18 @@ local function uri_to_fname(uri) else uri = uri:gsub('^file://', '') end - return uri_decode(uri) end +-- Return or create a buffer for a uri. +local function uri_to_bufnr(uri) + return vim.fn.bufadd((uri_to_fname(uri))) +end + return { uri_from_fname = uri_from_fname, uri_from_bufnr = uri_from_bufnr, uri_to_fname = uri_to_fname, + uri_to_bufnr = uri_to_bufnr, } -- vim:sw=2 ts=2 et diff --git a/runtime/nvim.appdata.xml b/runtime/nvim.appdata.xml index ceffc7ba98..025de1b5a9 100644 --- a/runtime/nvim.appdata.xml +++ b/runtime/nvim.appdata.xml @@ -26,6 +26,7 @@ </screenshots> <releases> + <release date="2019-11-06" version="0.4.3"/> <release date="2019-09-15" version="0.4.2"/> <release date="2019-09-15" version="0.4.1"/> <release date="2019-09-15" version="0.4.0"/> diff --git a/runtime/plugin/man.vim b/runtime/plugin/man.vim index e18a5528bb..e762eb3664 100644 --- a/runtime/plugin/man.vim +++ b/runtime/plugin/man.vim @@ -1,4 +1,4 @@ -" Maintainer: Anmol Sethi <anmol@aubble.com> +" Maintainer: Anmol Sethi <hi@nhooyr.io> if exists('g:loaded_man') finish diff --git a/runtime/syntax/man.vim b/runtime/syntax/man.vim index 6afe56a6e3..7ac02c3f63 100644 --- a/runtime/syntax/man.vim +++ b/runtime/syntax/man.vim @@ -1,4 +1,4 @@ -" Maintainer: Anmol Sethi <anmol@aubble.com> +" Maintainer: Anmol Sethi <hi@nhooyr.io> " Previous Maintainer: SungHyun Nam <goweol@gmail.com> if exists('b:current_syntax') @@ -30,6 +30,7 @@ endif if !exists('b:man_sect') call man#init_pager() endif + if b:man_sect =~# '^[023]' syntax case match syntax include @c $VIMRUNTIME/syntax/c.vim diff --git a/scripts/release.sh b/scripts/release.sh index 67268ba9bf..5b4902a2d7 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -93,6 +93,7 @@ fi _do_bump_commit echo " Next steps: + - Update runtime/nvim.appdata.xml on _master_ - Run tests/CI (version_spec.lua)! - Push the tag: git push --follow-tags diff --git a/src/nvim/CMakeLists.txt b/src/nvim/CMakeLists.txt index b00ac866b7..988021ca7a 100644 --- a/src/nvim/CMakeLists.txt +++ b/src/nvim/CMakeLists.txt @@ -52,7 +52,8 @@ set(UNICODE_TABLES_GENERATOR ${GENERATOR_DIR}/gen_unicode_tables.lua) set(UNICODE_DIR ${PROJECT_SOURCE_DIR}/unicode) set(GENERATED_UNICODE_TABLES ${GENERATED_DIR}/unicode_tables.generated.h) set(VIM_MODULE_FILE ${GENERATED_DIR}/lua/vim_module.generated.h) -set(VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/lua/vim.lua) +set(LUA_VIM_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/src/nvim/lua/vim.lua) +set(LUA_SHARED_MODULE_SOURCE ${PROJECT_SOURCE_DIR}/runtime/lua/vim/shared.lua) set(CHAR_BLOB_GENERATOR ${GENERATOR_DIR}/gen_char_blob.lua) set(LINT_SUPPRESS_FILE ${PROJECT_BINARY_DIR}/errors.json) set(LINT_SUPPRESS_URL_BASE "https://raw.githubusercontent.com/neovim/doc/gh-pages/reports/clint") @@ -317,11 +318,13 @@ add_custom_command( add_custom_command( OUTPUT ${VIM_MODULE_FILE} - COMMAND ${LUA_PRG} ${CHAR_BLOB_GENERATOR} ${VIM_MODULE_SOURCE} - ${VIM_MODULE_FILE} vim_module + COMMAND ${LUA_PRG} ${CHAR_BLOB_GENERATOR} ${VIM_MODULE_FILE} + ${LUA_VIM_MODULE_SOURCE} vim_module + ${LUA_SHARED_MODULE_SOURCE} shared_module DEPENDS ${CHAR_BLOB_GENERATOR} - ${VIM_MODULE_SOURCE} + ${LUA_VIM_MODULE_SOURCE} + ${LUA_SHARED_MODULE_SOURCE} ) list(APPEND NVIM_GENERATED_SOURCES diff --git a/src/nvim/api/vim.c b/src/nvim/api/vim.c index 10f7dd1a7b..3535bc3186 100644 --- a/src/nvim/api/vim.c +++ b/src/nvim/api/vim.c @@ -1074,9 +1074,10 @@ fail: /// float where the text should not be edited. Disables /// 'number', 'relativenumber', 'cursorline', 'cursorcolumn', /// 'foldcolumn', 'spell' and 'list' options. 'signcolumn' -/// is changed to `auto`. The end-of-buffer region is hidden -/// by setting `eob` flag of 'fillchars' to a space char, -/// and clearing the |EndOfBuffer| region in 'winhighlight'. +/// is changed to `auto` and 'colorcolumn' is cleared. The +/// end-of-buffer region is hidden by setting `eob` flag of +/// 'fillchars' to a space char, and clearing the +/// |EndOfBuffer| region in 'winhighlight'. /// @param[out] err Error details, if any /// /// @return Window handle, or 0 on error diff --git a/src/nvim/buffer.c b/src/nvim/buffer.c index c01364aadd..e3b8e9cc6d 100644 --- a/src/nvim/buffer.c +++ b/src/nvim/buffer.c @@ -411,11 +411,11 @@ bool buf_valid(buf_T *buf) /// caller should get a new buffer very soon! /// The 'bufhidden' option can force freeing and deleting. /// @param abort_if_last -/// If TRUE, do not close the buffer if autocommands cause +/// If true, do not close the buffer if autocommands cause /// there to be only one window with this buffer. e.g. when /// ":quit" is supposed to close the window but autocommands /// close all other windows. -void close_buffer(win_T *win, buf_T *buf, int action, int abort_if_last) +void close_buffer(win_T *win, buf_T *buf, int action, bool abort_if_last) { bool unload_buf = (action != 0); bool del_buf = (action == DOBUF_DEL || action == DOBUF_WIPE); @@ -1585,10 +1585,12 @@ void enter_buffer(buf_T *buf) open_buffer(false, NULL, 0); } else { - if (!msg_silent) { + if (!msg_silent && !shortmess(SHM_FILEINFO)) { need_fileinfo = true; // display file info after redraw } - (void)buf_check_timestamp(curbuf, false); // check if file changed + // check if file changed + (void)buf_check_timestamp(curbuf, false); + curwin->w_topline = 1; curwin->w_topfill = 0; apply_autocmds(EVENT_BUFENTER, NULL, NULL, false, curbuf); @@ -2692,7 +2694,7 @@ setfname( buf_T *buf, char_u *ffname, char_u *sfname, - int message // give message when buffer already exists + bool message // give message when buffer already exists ) { buf_T *obuf = NULL; @@ -5626,6 +5628,86 @@ void bufhl_mark_adjust(buf_T* buf, } } +/// Adjust a placed highlight for column changes and joined/broken lines +bool bufhl_mark_col_adjust(buf_T *buf, + linenr_T lnum, + colnr_T mincol, + long lnum_amount, + long col_amount) +{ + bool moved = false; + BufhlLine *lineinfo = bufhl_tree_ref(&buf->b_bufhl_info, lnum, false); + if (!lineinfo) { + // Old line empty, nothing to do + return false; + } + // Create the new line below only if needed + BufhlLine *lineinfo2 = NULL; + + colnr_T delcol = MAXCOL; + if (lnum_amount == 0 && col_amount < 0) { + delcol = mincol+(int)col_amount; + } + + size_t newidx = 0; + for (size_t i = 0; i < kv_size(lineinfo->items); i++) { + BufhlItem *item = &kv_A(lineinfo->items, i); + bool delete = false; + if (item->start >= mincol) { + moved = true; + item->start += (int)col_amount; + if (item->stop < MAXCOL) { + item->stop += (int)col_amount; + } + if (lnum_amount != 0) { + if (lineinfo2 == NULL) { + lineinfo2 = bufhl_tree_ref(&buf->b_bufhl_info, + lnum+lnum_amount, true); + } + kv_push(lineinfo2->items, *item); + delete = true; + } + } else { + if (item->start >= delcol) { + moved = true; + item->start = delcol; + } + if (item->stop == MAXCOL || item->stop+1 >= mincol) { + if (item->stop == MAXCOL) { + if (delcol < MAXCOL + && delcol > (colnr_T)STRLEN(ml_get_buf(buf, lnum, false))) { + delete = true; + } + } else { + moved = true; + item->stop += (int)col_amount; + } + assert(lnum_amount >= 0); + if (lnum_amount > 0) { + item->stop = MAXCOL; + } + } else if (item->stop+1 >= delcol) { + moved = true; + item->stop = delcol-1; + } + // we covered the entire range with a visual delete or something + if (item->stop < item->start) { + delete = true; + } + } + + if (!delete) { + if (i != newidx) { + kv_A(lineinfo->items, newidx) = kv_A(lineinfo->items, i); + } + newidx++; + } + } + kv_size(lineinfo->items) = newidx; + + return moved; +} + /// Get highlights to display at a specific line /// diff --git a/src/nvim/change.c b/src/nvim/change.c index 7558055696..8a782c2b20 100644 --- a/src/nvim/change.c +++ b/src/nvim/change.c @@ -359,6 +359,24 @@ void changed_bytes(linenr_T lnum, colnr_T col) } } +/// insert/delete bytes at column +/// +/// Like changed_bytes() but also adjust extmark for "added" bytes. +/// When "added" is negative text was deleted. +static void inserted_bytes(linenr_T lnum, colnr_T col, int added) +{ + if (added > 0) { + extmark_col_adjust(curbuf, lnum, col+1, 0, added, kExtmarkUndo); + } else if (added < 0) { + // TODO(bfredl): next revision of extmarks should handle both these + // with the same entry point. Also with more sane params.. + extmark_col_adjust_delete(curbuf, lnum, col+2, + col+(-added)+1, kExtmarkUndo, 0); + } + + changed_bytes(lnum, col); +} + /// Appended "count" lines below line "lnum" in the current buffer. /// Must be called AFTER the change and after mark_adjust(). /// Takes care of marking the buffer to be redrawn and sets the changed flag. @@ -630,7 +648,7 @@ void ins_char_bytes(char_u *buf, size_t charlen) ml_replace(lnum, newp, false); // mark the buffer as changed and prepare for displaying - changed_bytes(lnum, (colnr_T)col); + inserted_bytes(lnum, (colnr_T)col, (int)(newlen - oldlen)); // If we're in Insert or Replace mode and 'showmatch' is set, then briefly // show the match for right parens and braces. @@ -676,7 +694,7 @@ void ins_str(char_u *s) assert(bytes >= 0); memmove(newp + col + newlen, oldp + col, (size_t)bytes); ml_replace(lnum, newp, false); - changed_bytes(lnum, col); + inserted_bytes(lnum, col, newlen); curwin->w_cursor.col += newlen; } @@ -797,7 +815,7 @@ int del_bytes(colnr_T count, bool fixpos_arg, bool use_delcombine) } // mark the buffer as changed and prepare for displaying - changed_bytes(lnum, curwin->w_cursor.col); + inserted_bytes(lnum, col, -count); return OK; } diff --git a/src/nvim/edit.c b/src/nvim/edit.c index cd0f3f4b9d..eecea03a19 100644 --- a/src/nvim/edit.c +++ b/src/nvim/edit.c @@ -5600,9 +5600,6 @@ insertchar ( do_digraph(buf[i-1]); /* may be the start of a digraph */ buf[i] = NUL; ins_str(buf); - extmark_col_adjust(curbuf, curwin->w_cursor.lnum, - (colnr_T)(curwin->w_cursor.col + 1), 0, - (long)STRLEN(buf), kExtmarkUndo); if (flags & INSCHAR_CTRLV) { redo_literal(*buf); i = 1; @@ -5613,9 +5610,6 @@ insertchar ( } else { int cc; - extmark_col_adjust(curbuf, curwin->w_cursor.lnum, - (colnr_T)(curwin->w_cursor.col + 1), 0, - 1, kExtmarkUndo); if ((cc = utf_char2len(c)) > 1) { char_u buf[MB_MAXBYTES + 1]; @@ -8506,14 +8500,6 @@ static bool ins_tab(void) temp -= get_nolist_virtcol() % temp; - // Move extmarks - extmark_col_adjust(curbuf, - curwin->w_cursor.lnum, - curwin->w_cursor.col, - 0, - temp, - kExtmarkUndo); - /* * Insert the first space with ins_char(). It will delete one char in * replace mode. Insert the rest with ins_str(); it will not delete any diff --git a/src/nvim/ex_cmds.c b/src/nvim/ex_cmds.c index 4725246764..0c3b467612 100644 --- a/src/nvim/ex_cmds.c +++ b/src/nvim/ex_cmds.c @@ -1587,7 +1587,7 @@ int rename_buffer(char_u *new_fname) xfname = curbuf->b_fname; curbuf->b_ffname = NULL; curbuf->b_sfname = NULL; - if (setfname(curbuf, new_fname, NULL, TRUE) == FAIL) { + if (setfname(curbuf, new_fname, NULL, true) == FAIL) { curbuf->b_ffname = fname; curbuf->b_sfname = sfname; return FAIL; diff --git a/src/nvim/fileio.c b/src/nvim/fileio.c index fcf15638c7..f518e59acc 100644 --- a/src/nvim/fileio.c +++ b/src/nvim/fileio.c @@ -3731,8 +3731,9 @@ static int set_rw_fname(char_u *fname, char_u *sfname) return FAIL; } - if (setfname(curbuf, fname, sfname, FALSE) == OK) + if (setfname(curbuf, fname, sfname, false) == OK) { curbuf->b_flags |= BF_NOTEDITED; + } /* ....and a new named one is created */ apply_autocmds(EVENT_BUFNEW, NULL, NULL, FALSE, curbuf); diff --git a/src/nvim/generators/gen_char_blob.lua b/src/nvim/generators/gen_char_blob.lua index 1702add2e4..a7dad50d48 100644 --- a/src/nvim/generators/gen_char_blob.lua +++ b/src/nvim/generators/gen_char_blob.lua @@ -1,49 +1,59 @@ if arg[1] == '--help' then print('Usage:') - print(' gencharblob.lua source target varname') + print(' '..arg[0]..' target source varname [source varname]...') print('') print('Generates C file with big uint8_t blob.') print('Blob will be stored in a static const array named varname.') os.exit() end -assert(#arg == 3) +assert(#arg >= 3 and (#arg - 1) % 2 == 0) -local source_file = arg[1] -local target_file = arg[2] -local varname = arg[3] - -local source = io.open(source_file, 'r') +local target_file = arg[1] or error('Need a target file') local target = io.open(target_file, 'w') target:write('#include <stdint.h>\n\n') -target:write(('static const uint8_t %s[] = {\n'):format(varname)) - -local num_bytes = 0 -local MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line -target:write(' ') - -local increase_num_bytes -increase_num_bytes = function() - num_bytes = num_bytes + 1 - if num_bytes == MAX_NUM_BYTES then - num_bytes = 0 - target:write('\n ') + +local varnames = {} +for argi = 2, #arg, 2 do + local source_file = arg[argi] + local varname = arg[argi + 1] + if varnames[varname] then + error(string.format("varname %q is already specified for file %q", varname, varnames[varname])) end -end + varnames[varname] = source_file + + local source = io.open(source_file, 'r') + or error(string.format("source_file %q doesn't exist", source_file)) + + target:write(('static const uint8_t %s[] = {\n'):format(varname)) -for line in source:lines() do - for i = 1,string.len(line) do - local byte = string.byte(line, i) - assert(byte ~= 0) - target:write(string.format(' %3u,', byte)) + local num_bytes = 0 + local MAX_NUM_BYTES = 15 -- 78 / 5: maximum number of bytes on one line + target:write(' ') + + local increase_num_bytes + increase_num_bytes = function() + num_bytes = num_bytes + 1 + if num_bytes == MAX_NUM_BYTES then + num_bytes = 0 + target:write('\n ') + end + end + + for line in source:lines() do + for i = 1, string.len(line) do + local byte = line:byte(i) + assert(byte ~= 0) + target:write(string.format(' %3u,', byte)) + increase_num_bytes() + end + target:write(string.format(' %3u,', string.byte('\n', 1))) increase_num_bytes() end - target:write(string.format(' %3u,', string.byte('\n', 1))) - increase_num_bytes() -end -target:write(' 0};\n') + target:write(' 0};\n') + source:close() +end -source:close() target:close() diff --git a/src/nvim/lua/executor.c b/src/nvim/lua/executor.c index 093c130c5f..5450f62f54 100644 --- a/src/nvim/lua/executor.c +++ b/src/nvim/lua/executor.c @@ -268,12 +268,7 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL #endif // vim - const char *code = (char *)&vim_module[0]; - if (luaL_loadbuffer(lstate, code, strlen(code), "@vim.lua") - || lua_pcall(lstate, 0, LUA_MULTRET, 0)) { - nlua_error(lstate, _("E5106: Error while creating vim module: %.*s")); - return 1; - } + lua_newtable(lstate); // vim.api nlua_add_api_functions(lstate); // vim.types, vim.type_idx, vim.val_idx @@ -334,6 +329,24 @@ static int nlua_state_init(lua_State *const lstate) FUNC_ATTR_NONNULL_ALL lua_setglobal(lstate, "vim"); + { + const char *code = (char *)&shared_module[0]; + if (luaL_loadbuffer(lstate, code, strlen(code), "@shared.lua") + || lua_pcall(lstate, 0, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating shared module: %.*s")); + return 1; + } + } + + { + const char *code = (char *)&vim_module[0]; + if (luaL_loadbuffer(lstate, code, strlen(code), "@vim.lua") + || lua_pcall(lstate, 0, 0, 0)) { + nlua_error(lstate, _("E5106: Error while creating vim module: %.*s")); + return 1; + } + } + return 0; } diff --git a/src/nvim/lua/vim.lua b/src/nvim/lua/vim.lua index 1665a55aff..8019511317 100644 --- a/src/nvim/lua/vim.lua +++ b/src/nvim/lua/vim.lua @@ -33,35 +33,35 @@ -- - https://github.com/bakpakin/Fennel (pretty print, repl) -- - https://github.com/howl-editor/howl/tree/master/lib/howl/util +local vim = vim +assert(vim) -- Internal-only until comments in #8107 are addressed. -- Returns: -- {errcode}, {output} -local function _system(cmd) - local out = vim.api.nvim_call_function('system', { cmd }) - local err = vim.api.nvim_get_vvar('shell_error') +function vim._system(cmd) + local out = vim.fn.system(cmd) + local err = vim.v.shell_error return err, out end -- Gets process info from the `ps` command. -- Used by nvim_get_proc() as a fallback. -local function _os_proc_info(pid) +function vim._os_proc_info(pid) if pid == nil or pid <= 0 or type(pid) ~= 'number' then error('invalid pid') end local cmd = { 'ps', '-p', pid, '-o', 'comm=', } - local err, name = _system(cmd) - if 1 == err and string.gsub(name, '%s*', '') == '' then + local err, name = vim._system(cmd) + if 1 == err and vim.trim(name) == '' then return {} -- Process not found. elseif 0 ~= err then - local args_str = vim.api.nvim_call_function('string', { cmd }) - error('command failed: '..args_str) + error('command failed: '..vim.fn.string(cmd)) end - local _, ppid = _system({ 'ps', '-p', pid, '-o', 'ppid=', }) + local _, ppid = vim._system({ 'ps', '-p', pid, '-o', 'ppid=', }) -- Remove trailing whitespace. - name = string.gsub(string.gsub(name, '%s+$', ''), '^.*/', '') - ppid = string.gsub(ppid, '%s+$', '') - ppid = tonumber(ppid) == nil and -1 or tonumber(ppid) + name = vim.trim(name):gsub('^.*/', '') + ppid = tonumber(ppid) or -1 return { name = name, pid = pid, @@ -71,20 +71,19 @@ end -- Gets process children from the `pgrep` command. -- Used by nvim_get_proc_children() as a fallback. -local function _os_proc_children(ppid) +function vim._os_proc_children(ppid) if ppid == nil or ppid <= 0 or type(ppid) ~= 'number' then error('invalid ppid') end local cmd = { 'pgrep', '-P', ppid, } - local err, rv = _system(cmd) - if 1 == err and string.gsub(rv, '%s*', '') == '' then + local err, rv = vim._system(cmd) + if 1 == err and vim.trim(rv) == '' then return {} -- Process not found. elseif 0 ~= err then - local args_str = vim.api.nvim_call_function('string', { cmd }) - error('command failed: '..args_str) + error('command failed: '..vim.fn.string(cmd)) end local children = {} - for s in string.gmatch(rv, '%S+') do + for s in rv:gmatch('%S+') do local i = tonumber(s) if i ~= nil then table.insert(children, i) @@ -98,7 +97,7 @@ end -- Last inserted paths. Used to clear out items from package.[c]path when they -- are no longer in &runtimepath. local last_nvim_paths = {} -local function _update_package_paths() +function vim._update_package_paths() local cur_nvim_paths = {} local rtps = vim.api.nvim_list_runtime_paths() local sep = package.config:sub(1, 1) @@ -162,35 +161,35 @@ local function inspect(object, options) -- luacheck: no unused error(object, options) -- Stub for gen_vimdoc.py end ---- Paste handler, invoked by |nvim_paste()| when a conforming UI ---- (such as the |TUI|) pastes text into the editor. ---- ---- Example: To remove ANSI color codes when pasting: ---- <pre> ---- vim.paste = (function(overridden) ---- return function(lines, phase) ---- for i,line in ipairs(lines) do ---- -- Scrub ANSI color codes from paste input. ---- lines[i] = line:gsub('\27%[[0-9;mK]+', '') ---- end ---- overridden(lines, phase) ---- end ---- end)(vim.paste) ---- </pre> ---- ---@see |paste| ---- ---@param lines |readfile()|-style list of lines to paste. |channel-lines| ---@param phase -1: "non-streaming" paste: the call contains all lines. ---- If paste is "streamed", `phase` indicates the stream state: ---- - 1: starts the paste (exactly once) ---- - 2: continues the paste (zero or more times) ---- - 3: ends the paste (exactly once) ---@returns false if client should cancel the paste. -local function paste(lines, phase) end -- luacheck: no unused -paste = (function() +do local tdots, tick, got_line1 = 0, 0, false - return function(lines, phase) + + --- Paste handler, invoked by |nvim_paste()| when a conforming UI + --- (such as the |TUI|) pastes text into the editor. + --- + --- Example: To remove ANSI color codes when pasting: + --- <pre> + --- vim.paste = (function(overridden) + --- return function(lines, phase) + --- for i,line in ipairs(lines) do + --- -- Scrub ANSI color codes from paste input. + --- lines[i] = line:gsub('\27%[[0-9;mK]+', '') + --- end + --- overridden(lines, phase) + --- end + --- end)(vim.paste) + --- </pre> + --- + --@see |paste| + --- + --@param lines |readfile()|-style list of lines to paste. |channel-lines| + --@param phase -1: "non-streaming" paste: the call contains all lines. + --- If paste is "streamed", `phase` indicates the stream state: + --- - 1: starts the paste (exactly once) + --- - 2: continues the paste (zero or more times) + --- - 3: ends the paste (exactly once) + --@returns false if client should cancel the paste. + function vim.paste(lines, phase) local call = vim.api.nvim_call_function local now = vim.loop.now() local mode = call('mode', {}):sub(1,1) @@ -230,20 +229,33 @@ paste = (function() end return true -- Paste will not continue if not returning `true`. end -end)() +end --- Defers callback `cb` until the Nvim API is safe to call. --- ---@see |lua-loop-callbacks| ---@see |vim.schedule()| ---@see |vim.in_fast_event()| -local function schedule_wrap(cb) +function vim.schedule_wrap(cb) return (function (...) local args = {...} vim.schedule(function() cb(unpack(args)) end) end) end +-- vim.fn.{func}(...) +vim.fn = setmetatable({}, { + __index = function(t, key) + local function _fn(...) + return vim.call(key, ...) + end + t[key] = _fn + return _fn + end +}) + +-- These are for loading runtime modules lazily since they aren't available in +-- the nvim binary as specified in executor.c local function __index(t, key) if key == 'inspect' then t.inspect = require('vim.inspect') @@ -251,10 +263,6 @@ local function __index(t, key) elseif key == 'treesitter' then t.treesitter = require('vim.treesitter') return t.treesitter - elseif require('vim.shared')[key] ~= nil then - -- Expose all `vim.shared` functions on the `vim` module. - t[key] = require('vim.shared')[key] - return t[key] elseif require('vim.uri')[key] ~= nil then -- Expose all `vim.uri` functions on the `vim` module. t[key] = require('vim.uri')[key] @@ -265,29 +273,100 @@ local function __index(t, key) end end +setmetatable(vim, { + __index = __index +}) --- vim.fn.{func}(...) -local function _fn_index(t, key) - local function _fn(...) - return vim.call(key, ...) +do + local a = vim.api + local validate = vim.validate + local function make_meta_accessor(get, set, del) + validate { + get = {get, 'f'}; + set = {set, 'f'}; + del = {del, 'f', true}; + } + local mt = {} + if del then + function mt:__newindex(k, v) + if v == nil then + return del(k) + end + return set(k, v) + end + else + function mt:__newindex(k, v) + return set(k, v) + end + end + function mt:__index(k) + return get(k) + end + return setmetatable({}, mt) + end + local function pcall_ret(status, ...) + if status then return ... end + end + local function nil_wrap(fn) + return function(...) + return pcall_ret(pcall(fn, ...)) + end + end + vim.g = make_meta_accessor(nil_wrap(a.nvim_get_var), a.nvim_set_var, a.nvim_del_var) + vim.v = make_meta_accessor(nil_wrap(a.nvim_get_vvar), a.nvim_set_vvar) + vim.o = make_meta_accessor(nil_wrap(a.nvim_get_option), a.nvim_set_option) + vim.env = make_meta_accessor(vim.fn.getenv, vim.fn.setenv) + -- TODO(ashkan) if/when these are available from an API, generate them + -- instead of hardcoding. + local window_options = { + arab = true; arabic = true; breakindent = true; breakindentopt = true; + bri = true; briopt = true; cc = true; cocu = true; + cole = true; colorcolumn = true; concealcursor = true; conceallevel = true; + crb = true; cuc = true; cul = true; cursorbind = true; + cursorcolumn = true; cursorline = true; diff = true; fcs = true; + fdc = true; fde = true; fdi = true; fdl = true; + fdm = true; fdn = true; fdt = true; fen = true; + fillchars = true; fml = true; fmr = true; foldcolumn = true; + foldenable = true; foldexpr = true; foldignore = true; foldlevel = true; + foldmarker = true; foldmethod = true; foldminlines = true; foldnestmax = true; + foldtext = true; lbr = true; lcs = true; linebreak = true; + list = true; listchars = true; nu = true; number = true; + numberwidth = true; nuw = true; previewwindow = true; pvw = true; + relativenumber = true; rightleft = true; rightleftcmd = true; rl = true; + rlc = true; rnu = true; scb = true; scl = true; + scr = true; scroll = true; scrollbind = true; signcolumn = true; + spell = true; statusline = true; stl = true; wfh = true; + wfw = true; winbl = true; winblend = true; winfixheight = true; + winfixwidth = true; winhighlight = true; winhl = true; wrap = true; + } + local function new_buf_opt_accessor(bufnr) + local function get(k) + if window_options[k] then + return a.nvim_err_writeln(k.." is a window option, not a buffer option") + end + return a.nvim_buf_get_option(bufnr, k) + end + local function set(k, v) + if window_options[k] then + return a.nvim_err_writeln(k.." is a window option, not a buffer option") + end + return a.nvim_buf_set_option(bufnr, k, v) + end + return make_meta_accessor(nil_wrap(get), set) + end + vim.bo = new_buf_opt_accessor(0) + getmetatable(vim.bo).__call = function(_, bufnr) + return new_buf_opt_accessor(bufnr) + end + local function new_win_opt_accessor(winnr) + local function get(k) return a.nvim_win_get_option(winnr, k) end + local function set(k, v) return a.nvim_win_set_option(winnr, k, v) end + return make_meta_accessor(nil_wrap(get), set) + end + vim.wo = new_win_opt_accessor(0) + getmetatable(vim.wo).__call = function(_, winnr) + return new_win_opt_accessor(winnr) end - t[key] = _fn - return _fn end -local fn = setmetatable({}, {__index=_fn_index}) - -local module = { - _update_package_paths = _update_package_paths, - _os_proc_children = _os_proc_children, - _os_proc_info = _os_proc_info, - _system = _system, - paste = paste, - schedule_wrap = schedule_wrap, - fn=fn, -} - -setmetatable(module, { - __index = __index -}) return module diff --git a/src/nvim/main.c b/src/nvim/main.c index e39eec4038..c8959d9ad1 100644 --- a/src/nvim/main.c +++ b/src/nvim/main.c @@ -1516,7 +1516,7 @@ static void create_windows(mparm_T *parmp) /* We can't close the window, it would disturb what * happens next. Clear the file name and set the arg * index to -1 to delete it later. */ - setfname(curbuf, NULL, NULL, FALSE); + setfname(curbuf, NULL, NULL, false); curwin->w_arg_idx = -1; swap_exists_action = SEA_NONE; } else diff --git a/src/nvim/mark_extended.c b/src/nvim/mark_extended.c index 01745f484d..91c2f919ce 100644 --- a/src/nvim/mark_extended.c +++ b/src/nvim/mark_extended.c @@ -910,6 +910,9 @@ void extmark_col_adjust(buf_T *buf, linenr_T lnum, bool marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, lnum_amount, false, col_amount); + marks_moved |= bufhl_mark_col_adjust(buf, lnum, mincol, + lnum_amount, col_amount); + if (undo == kExtmarkUndo && marks_moved) { u_extmark_col_adjust(buf, lnum, mincol, lnum_amount, col_amount); } @@ -938,6 +941,7 @@ void extmark_col_adjust_delete(buf_T *buf, linenr_T lnum, marks_moved = extmark_col_adjust_impl(buf, lnum, mincol, 0, true, (long)endcol); + marks_moved |= bufhl_mark_col_adjust(buf, lnum, endcol, 0, mincol-(endcol+1)); // Deletes at the end of the line have different behaviour than the normal // case when deleted. // Cleanup any marks that are floating beyond the end of line. diff --git a/src/nvim/memline.c b/src/nvim/memline.c index 2824d57f49..e5ba17a0a7 100644 --- a/src/nvim/memline.c +++ b/src/nvim/memline.c @@ -540,7 +540,7 @@ void ml_open_file(buf_T *buf) /// file, or reading into an existing buffer, create a swap file now. /// /// @param newfile reading file into new buffer -void check_need_swap(int newfile) +void check_need_swap(bool newfile) { int old_msg_silent = msg_silent; // might be reset by an E325 message msg_silent = 0; // If swap dialog prompts for input, user needs to see it! @@ -937,8 +937,9 @@ void ml_recover(bool checkext) */ if (directly) { expand_env(b0p->b0_fname, NameBuff, MAXPATHL); - if (setfname(curbuf, NameBuff, NULL, TRUE) == FAIL) + if (setfname(curbuf, NameBuff, NULL, true) == FAIL) { goto theend; + } } home_replace(NULL, mfp->mf_fname, NameBuff, MAXPATHL, TRUE); diff --git a/src/nvim/ops.c b/src/nvim/ops.c index 2301b2159f..f2d35d5e43 100644 --- a/src/nvim/ops.c +++ b/src/nvim/ops.c @@ -1644,8 +1644,6 @@ int op_delete(oparg_T *oap) curwin->w_cursor.col = 0; (void)del_bytes((colnr_T)n, !virtual_op, oap->op_type == OP_DELETE && !oap->is_VIsual); - extmark_col_adjust(curbuf, curwin->w_cursor.lnum, - (colnr_T)0, 0L, (long)-n, kExtmarkUndo); curwin->w_cursor = curpos; // restore curwin->w_cursor (void)do_join(2, false, false, false, false); } @@ -1685,7 +1683,6 @@ setmarks: if (oap->is_VIsual == false) { endcol = MAX(endcol - 1, mincol); } - extmark_col_adjust_delete(curbuf, lnum, mincol, endcol, kExtmarkUndo, 0); } return OK; } @@ -2279,7 +2276,7 @@ void op_insert(oparg_T *oap, long count1) colnr_T col = oap->start.col; for (linenr_T lnum = oap->start.lnum; lnum <= oap->end.lnum; lnum++) { extmark_col_adjust(curbuf, lnum, col, 0, 1, kExtmarkUndo); - } + } } /* @@ -4279,14 +4276,14 @@ format_lines( if (next_leader_len > 0) { (void)del_bytes(next_leader_len, false, false); mark_col_adjust(curwin->w_cursor.lnum, (colnr_T)0, 0L, - (long)-next_leader_len, 0, kExtmarkUndo); + (long)-next_leader_len, 0, kExtmarkNOOP); } else if (second_indent > 0) { // the "leader" for FO_Q_SECOND int indent = (int)getwhitecols_curline(); if (indent > 0) { - (void)del_bytes(indent, FALSE, FALSE); + (void)del_bytes(indent, false, false); mark_col_adjust(curwin->w_cursor.lnum, - (colnr_T)0, 0L, (long)-indent, 0, kExtmarkUndo); + (colnr_T)0, 0L, (long)-indent, 0, kExtmarkNOOP); } } curwin->w_cursor.lnum--; @@ -4951,23 +4948,6 @@ int do_addsub(int op_type, pos_T *pos, int length, linenr_T Prenum1) curbuf->b_op_end.col--; } - // if buf1 wasn't allocated, only a singe ASCII char was changed in-place. - if (did_change && buf1 != NULL) { - extmark_col_adjust_delete(curbuf, - pos->lnum, - startpos.col + 2, - endpos.col + 1 + length, - kExtmarkUndo, - 0); - long col_amount = (long)STRLEN(buf1); - extmark_col_adjust(curbuf, - pos->lnum, - startpos.col + 1, - 0, - col_amount, - kExtmarkUndo); - } - theend: xfree(buf1); if (visual) { diff --git a/src/nvim/quickfix.c b/src/nvim/quickfix.c index ed57b28029..194cc5781b 100644 --- a/src/nvim/quickfix.c +++ b/src/nvim/quickfix.c @@ -1,9 +1,7 @@ // This is an open source non-commercial project. Dear PVS-Studio, please check // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com -/* - * quickfix.c: functions for quickfix mode, using a file with error messages - */ +// quickfix.c: functions for quickfix mode, using a file with error messages #include <assert.h> #include <inttypes.h> @@ -53,9 +51,7 @@ struct dir_stack_T { char_u *dirname; }; -/* - * For each error the next struct is allocated and linked in a list. - */ +// For each error the next struct is allocated and linked in a list. typedef struct qfline_S qfline_T; struct qfline_S { qfline_T *qf_next; ///< pointer to next error in the list @@ -74,9 +70,7 @@ struct qfline_S { char_u qf_valid; ///< valid error message detected }; -/* - * There is a stack of error lists. - */ +// There is a stack of error lists. #define LISTCOUNT 10 #define INVALID_QFIDX (-1) @@ -120,15 +114,13 @@ typedef struct qf_list_S { /// Quickfix/Location list stack definition /// Contains a list of quickfix/location lists (qf_list_T) struct qf_info_S { - /* - * Count of references to this list. Used only for location lists. - * When a location list window reference this list, qf_refcount - * will be 2. Otherwise, qf_refcount will be 1. When qf_refcount - * reaches 0, the list is freed. - */ + // Count of references to this list. Used only for location lists. + // When a location list window reference this list, qf_refcount + // will be 2. Otherwise, qf_refcount will be 1. When qf_refcount + // reaches 0, the list is freed. int qf_refcount; - int qf_listcount; /* current number of lists */ - int qf_curlist; /* current error list */ + int qf_listcount; // current number of lists + int qf_curlist; // current error list qf_list_T qf_lists[LISTCOUNT]; qfltype_T qfl_type; // type of list }; @@ -138,31 +130,29 @@ static unsigned last_qf_id = 0; // Last Used quickfix list id #define FMT_PATTERNS 11 // maximum number of % recognized -/* - * Structure used to hold the info of one part of 'errorformat' - */ +// Structure used to hold the info of one part of 'errorformat' typedef struct efm_S efm_T; struct efm_S { - regprog_T *prog; /* pre-formatted part of 'errorformat' */ - efm_T *next; /* pointer to next (NULL if last) */ - char_u addr[FMT_PATTERNS]; /* indices of used % patterns */ - char_u prefix; /* prefix of this format line: */ - /* 'D' enter directory */ - /* 'X' leave directory */ - /* 'A' start of multi-line message */ - /* 'E' error message */ - /* 'W' warning message */ - /* 'I' informational message */ - /* 'C' continuation line */ - /* 'Z' end of multi-line message */ - /* 'G' general, unspecific message */ - /* 'P' push file (partial) message */ - /* 'Q' pop/quit file (partial) message */ - /* 'O' overread (partial) message */ - char_u flags; /* additional flags given in prefix */ - /* '-' do not include this line */ - /* '+' include whole line in message */ - int conthere; /* %> used */ + regprog_T *prog; // pre-formatted part of 'errorformat' + efm_T *next; // pointer to next (NULL if last) + char_u addr[FMT_PATTERNS]; // indices of used % patterns + char_u prefix; // prefix of this format line: + // 'D' enter directory + // 'X' leave directory + // 'A' start of multi-line message + // 'E' error message + // 'W' warning message + // 'I' informational message + // 'C' continuation line + // 'Z' end of multi-line message + // 'G' general, unspecific message + // 'P' push file (partial) message + // 'Q' pop/quit file (partial) message + // 'O' overread (partial) message + char_u flags; // additional flags given in prefix + // '-' do not include this line + // '+' include whole line in message + int conthere; // %> used }; /// List of location lists to be deleted. @@ -221,7 +211,7 @@ static char_u *e_no_more_items = (char_u *)N_("E553: No more items"); // Quickfix window check helper macro #define IS_QF_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref == NULL) -/* Location list window check helper macro */ +// Location list window check helper macro #define IS_LL_WINDOW(wp) (bt_quickfix(wp->w_buffer) && wp->w_llist_ref != NULL) // Quickfix and location list stack check helper macros @@ -1156,16 +1146,12 @@ qf_init_ext( goto error2; } - /* - * got_int is reset here, because it was probably set when killing the - * ":make" command, but we still want to read the errorfile then. - */ - got_int = FALSE; + // got_int is reset here, because it was probably set when killing the + // ":make" command, but we still want to read the errorfile then. + got_int = false; - /* - * Read the lines in the error file one by one. - * Try to recognize one of the error formats in each line. - */ + // Read the lines in the error file one by one. + // Try to recognize one of the error formats in each line. while (!got_int) { status = qf_init_process_nextline(qfl, fmt_first, &state, &fields); if (status == QF_END_OF_INPUT) { // end of input @@ -1263,10 +1249,8 @@ static void qf_new_list(qf_info_T *qi, const char_u *qf_title) qf_free(&qi->qf_lists[--qi->qf_listcount]); } - /* - * When the stack is full, remove to oldest entry - * Otherwise, add a new entry. - */ + // When the stack is full, remove to oldest entry + // Otherwise, add a new entry. if (qi->qf_listcount == LISTCOUNT) { qf_free(&qi->qf_lists[0]); for (i = 1; i < LISTCOUNT; i++) { @@ -1714,7 +1698,7 @@ static void ll_free_all(qf_info_T **pqi) qi = *pqi; if (qi == NULL) return; - *pqi = NULL; /* Remove reference to this list */ + *pqi = NULL; // Remove reference to this list qi->qf_refcount--; if (qi->qf_refcount < 1) { @@ -1739,7 +1723,7 @@ void qf_free_all(win_T *wp) qf_info_T *qi = &ql_info; if (wp != NULL) { - /* location list */ + // location list ll_free_all(&wp->w_llist); ll_free_all(&wp->w_llist_ref); } else { @@ -1892,14 +1876,13 @@ static qf_info_T *qf_alloc_stack(qfltype_T qfltype) static qf_info_T *ll_get_or_alloc_list(win_T *wp) FUNC_ATTR_NONNULL_ALL FUNC_ATTR_NONNULL_RET { - if (IS_LL_WINDOW(wp)) - /* For a location list window, use the referenced location list */ + if (IS_LL_WINDOW(wp)) { + // For a location list window, use the referenced location list return wp->w_llist_ref; + } - /* - * For a non-location list window, w_llist_ref should not point to a - * location list. - */ + // For a non-location list window, w_llist_ref should not point to a + // location list. ll_free_all(&wp->w_llist_ref); if (wp->w_llist == NULL) { @@ -2133,22 +2116,21 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr, { struct dir_stack_T *ds_ptr; - /* allocate new stack element and hook it in */ + // allocate new stack element and hook it in struct dir_stack_T *ds_new = xmalloc(sizeof(struct dir_stack_T)); ds_new->next = *stackptr; *stackptr = ds_new; - /* store directory on the stack */ + // store directory on the stack if (vim_isAbsName(dirbuf) || (*stackptr)->next == NULL - || (*stackptr && is_file_stack)) + || (*stackptr && is_file_stack)) { (*stackptr)->dirname = vim_strsave(dirbuf); - else { - /* Okay we don't have an absolute path. - * dirbuf must be a subdir of one of the directories on the stack. - * Let's search... - */ + } else { + // Okay we don't have an absolute path. + // dirbuf must be a subdir of one of the directories on the stack. + // Let's search... ds_new = (*stackptr)->next; (*stackptr)->dirname = NULL; while (ds_new) { @@ -2161,7 +2143,7 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr, ds_new = ds_new->next; } - /* clean up all dirs we already left */ + // clean up all dirs we already left while ((*stackptr)->next != ds_new) { ds_ptr = (*stackptr)->next; (*stackptr)->next = (*stackptr)->next->next; @@ -2169,7 +2151,7 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr, xfree(ds_ptr); } - /* Nothing found -> it must be on top level */ + // Nothing found -> it must be on top level if (ds_new == NULL) { xfree((*stackptr)->dirname); (*stackptr)->dirname = vim_strsave(dirbuf); @@ -2187,18 +2169,16 @@ static char_u *qf_push_dir(char_u *dirbuf, struct dir_stack_T **stackptr, } -/* - * pop dirbuf from the directory stack and return previous directory or NULL if - * stack is empty - */ +// pop dirbuf from the directory stack and return previous directory or NULL if +// stack is empty static char_u *qf_pop_dir(struct dir_stack_T **stackptr) { struct dir_stack_T *ds_ptr; - /* TODO: Should we check if dirbuf is the directory on top of the stack? - * What to do if it isn't? */ + // TODO(vim): Should we check if dirbuf is the directory on top of the stack? + // What to do if it isn't? - /* pop top element and free it */ + // pop top element and free it if (*stackptr != NULL) { ds_ptr = *stackptr; *stackptr = (*stackptr)->next; @@ -2206,13 +2186,11 @@ static char_u *qf_pop_dir(struct dir_stack_T **stackptr) xfree(ds_ptr); } - /* return NEW top element as current dir or NULL if stack is empty*/ + // return NEW top element as current dir or NULL if stack is empty return *stackptr ? (*stackptr)->dirname : NULL; } -/* - * clean up directory stack - */ +// clean up directory stack static void qf_clean_dir_stack(struct dir_stack_T **stackptr) { struct dir_stack_T *ds_ptr; @@ -2887,10 +2865,8 @@ void qf_jump(qf_info_T *qi, int dir, int errornr, int forceit) } } - /* - * If there is a file name, - * read the wanted file if needed, and check autowrite etc. - */ + // If there is a file name, + // read the wanted file if needed, and check autowrite etc. old_curbuf = curbuf; old_lnum = curwin->w_cursor.lnum; @@ -2938,8 +2914,8 @@ theend: qfl->qf_index = qf_index; } if (p_swb != old_swb && opened_window) { - /* Restore old 'switchbuf' value, but not when an autocommand or - * modeline has changed the value. */ + // Restore old 'switchbuf' value, but not when an autocommand or + // modeline has changed the value. if (p_swb == empty_option) { p_swb = old_swb; swb_flags = old_swb_flags; @@ -3038,10 +3014,8 @@ static void qf_list_entry(qfline_T *qfp, int qf_idx, bool cursel) ui_flush(); // show one line at a time } -/* - * ":clist": list all errors - * ":llist": list all locations - */ +// ":clist": list all errors +// ":llist": list all locations void qf_list(exarg_T *eap) { qf_list_T *qfl; @@ -3116,10 +3090,8 @@ void qf_list(exarg_T *eap) } } -/* - * Remove newlines and leading whitespace from an error message. - * Put the result in "buf[bufsize]". - */ +// Remove newlines and leading whitespace from an error message. +// Put the result in "buf[bufsize]". static void qf_fmt_text(const char_u *restrict text, char_u *restrict buf, int bufsize) FUNC_ATTR_NONNULL_ALL @@ -3277,9 +3249,7 @@ static void qf_free(qf_list_T *qfl) qfl->qf_changedtick = 0L; } -/* - * qf_mark_adjust: adjust marks - */ +// qf_mark_adjust: adjust marks bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount, long amount_after) { @@ -3321,21 +3291,19 @@ bool qf_mark_adjust(win_T *wp, linenr_T line1, linenr_T line2, long amount, return found_one; } -/* - * Make a nice message out of the error character and the error number: - * char number message - * e or E 0 " error" - * w or W 0 " warning" - * i or I 0 " info" - * 0 0 "" - * other 0 " c" - * e or E n " error n" - * w or W n " warning n" - * i or I n " info n" - * 0 n " error n" - * other n " c n" - * 1 x "" :helpgrep - */ +// Make a nice message out of the error character and the error number: +// char number message +// e or E 0 " error" +// w or W 0 " warning" +// i or I 0 " info" +// 0 0 "" +// other 0 " c" +// e or E n " error n" +// w or W n " warning n" +// i or I n " info n" +// 0 n " error n" +// other n " c n" +// 1 x "" :helpgrep static char_u *qf_types(int c, int nr) { static char_u buf[20]; @@ -3396,12 +3364,10 @@ void qf_view_result(bool split) do_cmdline_cmd((IS_LL_WINDOW(curwin) ? ".ll" : ".cc")); } -/* - * ":cwindow": open the quickfix window if we have errors to display, - * close it if not. - * ":lwindow": open the location list window if we have locations to display, - * close it if not. - */ +// ":cwindow": open the quickfix window if we have errors to display, +// close it if not. +// ":lwindow": open the location list window if we have locations to display, +// close it if not. void ex_cwindow(exarg_T *eap) { qf_info_T *qi; @@ -3414,14 +3380,12 @@ void ex_cwindow(exarg_T *eap) qfl = qf_get_curlist(qi); - /* Look for an existing quickfix window. */ + // Look for an existing quickfix window. win = qf_find_win(qi); - /* - * If a quickfix window is open but we have no errors to display, - * close the window. If a quickfix window is not open, then open - * it if we have errors; otherwise, leave it closed. - */ + // If a quickfix window is open but we have no errors to display, + // close the window. If a quickfix window is not open, then open + // it if we have errors; otherwise, leave it closed. if (qf_stack_empty(qi) || qfl->qf_nonevalid || qf_list_empty(qf_get_curlist(qi))) { @@ -3433,10 +3397,8 @@ void ex_cwindow(exarg_T *eap) } } -/* - * ":cclose": close the window showing the list of errors. - * ":lclose": close the window showing the location list - */ +// ":cclose": close the window showing the list of errors. +// ":lclose": close the window showing the location list void ex_cclose(exarg_T *eap) { win_T *win = NULL; @@ -3446,7 +3408,7 @@ void ex_cclose(exarg_T *eap) return; } - /* Find existing quickfix window and close it. */ + // Find existing quickfix window and close it. win = qf_find_win(qi); if (win != NULL) { win_close(win, false); @@ -3642,38 +3604,32 @@ void ex_cbottom(exarg_T *eap) } } -/* - * Return the number of the current entry (line number in the quickfix - * window). - */ +// Return the number of the current entry (line number in the quickfix +// window). linenr_T qf_current_entry(win_T *wp) { qf_info_T *qi = &ql_info; - if (IS_LL_WINDOW(wp)) - /* In the location list window, use the referenced location list */ + if (IS_LL_WINDOW(wp)) { + // In the location list window, use the referenced location list qi = wp->w_llist_ref; + } return qf_get_curlist(qi)->qf_index; } -/* - * Update the cursor position in the quickfix window to the current error. - * Return TRUE if there is a quickfix window. - */ -static int -qf_win_pos_update ( +// Update the cursor position in the quickfix window to the current error. +// Return TRUE if there is a quickfix window. +static int qf_win_pos_update( qf_info_T *qi, - int old_qf_index /* previous qf_index or zero */ + int old_qf_index // previous qf_index or zero ) { win_T *win; int qf_index = qf_get_curlist(qi)->qf_index; - /* - * Put the cursor on the current error in the quickfix window, so that - * it's viewable. - */ + // Put the cursor on the current error in the quickfix window, so that + // it's viewable. win = qf_find_win(qi); if (win != NULL && qf_index <= win->w_buffer->b_ml.ml_line_count @@ -3725,10 +3681,8 @@ static win_T *qf_find_win(const qf_info_T *qi) return NULL; } -/* - * Find a quickfix buffer. - * Searches in windows opened in all the tabs. - */ +// Find a quickfix buffer. +// Searches in windows opened in all the tabs. static buf_T *qf_find_buf(const qf_info_T *qi) FUNC_ATTR_PURE FUNC_ATTR_WARN_UNUSED_RESULT { @@ -3754,16 +3708,14 @@ static void qf_update_win_titlevar(qf_info_T *qi) } } -/* - * Find the quickfix buffer. If it exists, update the contents. - */ +// Find the quickfix buffer. If it exists, update the contents. static void qf_update_buffer(qf_info_T *qi, qfline_T *old_last) { buf_T *buf; win_T *win; aco_save_T aco; - /* Check if a buffer for the quickfix list exists. Update it. */ + // Check if a buffer for the quickfix list exists. Update it. buf = qf_find_buf(qi); if (buf != NULL) { linenr_T old_line_count = buf->b_ml.ml_line_count; @@ -3936,7 +3888,7 @@ static void qf_fill_buffer(qf_list_T *qfl, buf_T *buf, qfline_T *old_last) redraw_curbuf_later(NOT_VALID); } - /* Restore KeyTyped, setting 'filetype' may reset it. */ + // Restore KeyTyped, setting 'filetype' may reset it. KeyTyped = old_KeyTyped; } @@ -3990,9 +3942,7 @@ static void qf_jump_first(qf_info_T *qi, unsigned save_qfid, int forceit) } } -/* - * Return TRUE when using ":vimgrep" for ":grep". - */ +// Return TRUE when using ":vimgrep" for ":grep". int grep_internal(cmdidx_T cmdidx) { return (cmdidx == CMD_grep @@ -4055,9 +4005,7 @@ static char *make_get_fullcmd(const char_u *makecmd, const char_u *fname) return cmd; } -/* - * Used for ":make", ":lmake", ":grep", ":lgrep", ":grepadd", and ":lgrepadd" - */ +// Used for ":make", ":lmake", ":grep", ":lgrep", ":grepadd", and ":lgrepadd" void ex_make(exarg_T *eap) { char_u *fname; @@ -4128,11 +4076,9 @@ cleanup: xfree(cmd); } -/* - * Return the name for the errorfile, in allocated memory. - * Find a new unique name when 'makeef' contains "##". - * Returns NULL for error. - */ +// Return the name for the errorfile, in allocated memory. +// Find a new unique name when 'makeef' contains "##". +// Returns NULL for error. static char_u *get_mef_name(void) { char_u *p; @@ -4154,7 +4100,7 @@ static char_u *get_mef_name(void) if (*p == NUL) return vim_strsave(p_mef); - /* Keep trying until the name doesn't exist yet. */ + // Keep trying until the name doesn't exist yet. for (;; ) { if (start == -1) { start = (int)os_get_pid(); @@ -4703,10 +4649,8 @@ static char_u * cfile_get_auname(cmdidx_T cmdidx) } -/* - * ":cfile"/":cgetfile"/":caddfile" commands. - * ":lfile"/":lgetfile"/":laddfile" commands. - */ +// ":cfile"/":cgetfile"/":caddfile" commands. +// ":lfile"/":lgetfile"/":laddfile" commands. void ex_cfile(exarg_T *eap) { win_T *wp = NULL; @@ -4947,12 +4891,10 @@ static void vgr_jump_to_match(qf_info_T *qi, int forceit, int *redraw_for_dummy, } } -/* - * ":vimgrep {pattern} file(s)" - * ":vimgrepadd {pattern} file(s)" - * ":lvimgrep {pattern} file(s)" - * ":lvimgrepadd {pattern} file(s)" - */ +// ":vimgrep {pattern} file(s)" +// ":vimgrepadd {pattern} file(s)" +// ":lvimgrep {pattern} file(s)" +// ":lvimgrepadd {pattern} file(s)" void ex_vimgrep(exarg_T *eap) { regmmatch_T regmatch; @@ -4994,7 +4936,7 @@ void ex_vimgrep(exarg_T *eap) else tomatch = MAXLNUM; - /* Get the search pattern: either white-separated or enclosed in // */ + // Get the search pattern: either white-separated or enclosed in // regmatch.regprog = NULL; char_u *title = vim_strsave(qf_cmdtitle(*eap->cmdlinep)); p = skip_vimgrep_pat(eap->arg, &s, &flags); @@ -5021,9 +4963,10 @@ void ex_vimgrep(exarg_T *eap) qf_new_list(qi, title); } - /* parse the list of arguments */ - if (get_arglist_exp(p, &fcount, &fnames, true) == FAIL) + // parse the list of arguments + if (get_arglist_exp(p, &fcount, &fnames, true) == FAIL) { goto theend; + } if (fcount == 0) { EMSG(_(e_nomatch)); goto theend; @@ -5032,8 +4975,8 @@ void ex_vimgrep(exarg_T *eap) dirname_start = xmalloc(MAXPATHL); dirname_now = xmalloc(MAXPATHL); - /* Remember the current directory, because a BufRead autocommand that does - * ":lcd %:p:h" changes the meaning of short path names. */ + // Remember the current directory, because a BufRead autocommand that does + // ":lcd %:p:h" changes the meaning of short path names. os_dirname(dirname_start, MAXPATHL); incr_quickfix_busy(); @@ -5046,15 +4989,15 @@ void ex_vimgrep(exarg_T *eap) for (fi = 0; fi < fcount && !got_int && tomatch > 0; fi++) { fname = path_try_shorten_fname(fnames[fi]); if (time(NULL) > seconds) { - /* Display the file name every second or so, show the user we are - * working on it. */ + // Display the file name every second or so, show the user we are + // working on it. seconds = time(NULL); vgr_display_fname(fname); } buf = buflist_findname_exp(fnames[fi]); if (buf == NULL || buf->b_ml.ml_mfp == NULL) { - /* Remember that a buffer with this name already exists. */ + // Remember that a buffer with this name already exists. duplicate_name = (buf != NULL); using_dummy = TRUE; redraw_for_dummy = TRUE; @@ -5087,20 +5030,20 @@ void ex_vimgrep(exarg_T *eap) if (found_match && first_match_buf == NULL) first_match_buf = buf; if (duplicate_name) { - /* Never keep a dummy buffer if there is another buffer - * with the same name. */ + // Never keep a dummy buffer if there is another buffer + // with the same name. wipe_dummy_buffer(buf, dirname_start); buf = NULL; } else if (!cmdmod.hide - || buf->b_p_bh[0] == 'u' /* "unload" */ - || buf->b_p_bh[0] == 'w' /* "wipe" */ - || buf->b_p_bh[0] == 'd') { /* "delete" */ - /* When no match was found we don't need to remember the - * buffer, wipe it out. If there was a match and it - * wasn't the first one or we won't jump there: only - * unload the buffer. - * Ignore 'hidden' here, because it may lead to having too - * many swap files. */ + || buf->b_p_bh[0] == 'u' // "unload" + || buf->b_p_bh[0] == 'w' // "wipe" + || buf->b_p_bh[0] == 'd') { // "delete" + // When no match was found we don't need to remember the + // buffer, wipe it out. If there was a match and it + // wasn't the first one or we won't jump there: only + // unload the buffer. + // Ignore 'hidden' here, because it may lead to having too + // many swap files. if (!found_match) { wipe_dummy_buffer(buf, dirname_start); buf = NULL; @@ -5124,10 +5067,10 @@ void ex_vimgrep(exarg_T *eap) target_dir = vim_strsave(dirname_now); } - /* The buffer is still loaded, the Filetype autocommands - * need to be done now, in that buffer. And the modelines - * need to be done (again). But not the window-local - * options! */ + // The buffer is still loaded, the Filetype autocommands + // need to be done now, in that buffer. And the modelines + // need to be done (again). But not the window-local + // options! aucmd_prepbuf(&aco, buf); apply_autocmds(EVENT_FILETYPE, buf->b_p_ft, buf->b_fname, TRUE, buf); @@ -5171,8 +5114,8 @@ void ex_vimgrep(exarg_T *eap) decr_quickfix_busy(); - /* If we loaded a dummy buffer into the current window, the autocommands - * may have messed up things, need to redraw and recompute folds. */ + // If we loaded a dummy buffer into the current window, the autocommands + // may have messed up things, need to redraw and recompute folds. if (redraw_for_dummy) { foldUpdateAll(curwin); } @@ -5185,18 +5128,16 @@ theend: vim_regfree(regmatch.regprog); } -/* - * Restore current working directory to "dirname_start" if they differ, taking - * into account whether it is set locally or globally. - */ +// Restore current working directory to "dirname_start" if they differ, taking +// into account whether it is set locally or globally. static void restore_start_dir(char_u *dirname_start) { char_u *dirname_now = xmalloc(MAXPATHL); os_dirname(dirname_now, MAXPATHL); if (STRCMP(dirname_start, dirname_now) != 0) { - /* If the directory has changed, change it back by building up an - * appropriate ex command and executing it. */ + // If the directory has changed, change it back by building up an + // appropriate ex command and executing it. exarg_T ea = { .arg = dirname_start, .cmdidx = (curwin->w_localdir == NULL) ? CMD_cd : CMD_lcd, @@ -5206,23 +5147,21 @@ static void restore_start_dir(char_u *dirname_start) xfree(dirname_now); } -/* - * Load file "fname" into a dummy buffer and return the buffer pointer, - * placing the directory resulting from the buffer load into the - * "resulting_dir" pointer. "resulting_dir" must be allocated by the caller - * prior to calling this function. Restores directory to "dirname_start" prior - * to returning, if autocmds or the 'autochdir' option have changed it. - * - * If creating the dummy buffer does not fail, must call unload_dummy_buffer() - * or wipe_dummy_buffer() later! - * - * Returns NULL if it fails. - */ +// Load file "fname" into a dummy buffer and return the buffer pointer, +// placing the directory resulting from the buffer load into the +// "resulting_dir" pointer. "resulting_dir" must be allocated by the caller +// prior to calling this function. Restores directory to "dirname_start" prior +// to returning, if autocmds or the 'autochdir' option have changed it. +// +// If creating the dummy buffer does not fail, must call unload_dummy_buffer() +// or wipe_dummy_buffer() later! +// +// Returns NULL if it fails. static buf_T * load_dummy_buffer ( char_u *fname, - char_u *dirname_start, /* in: old directory */ - char_u *resulting_dir /* out: new directory */ + char_u *dirname_start, // in: old directory + char_u *resulting_dir // out: new directory ) { buf_T *newbuf; @@ -5239,24 +5178,24 @@ load_dummy_buffer ( } set_bufref(&newbufref, newbuf); - /* Init the options. */ + // Init the options. buf_copy_options(newbuf, BCO_ENTER | BCO_NOHELP); - /* need to open the memfile before putting the buffer in a window */ + // need to open the memfile before putting the buffer in a window if (ml_open(newbuf) == OK) { // Make sure this buffer isn't wiped out by autocommands. newbuf->b_locked++; // set curwin/curbuf to buf and save a few things aucmd_prepbuf(&aco, newbuf); - /* Need to set the filename for autocommands. */ - (void)setfname(curbuf, fname, NULL, FALSE); + // Need to set the filename for autocommands. + (void)setfname(curbuf, fname, NULL, false); - /* Create swap file now to avoid the ATTENTION message. */ - check_need_swap(TRUE); + // Create swap file now to avoid the ATTENTION message. + check_need_swap(true); - /* Remove the "dummy" flag, otherwise autocommands may not - * work. */ + // Remove the "dummy" flag, otherwise autocommands may not + // work. curbuf->b_flags &= ~BF_DUMMY; newbuf_to_wipe.br_buf = NULL; @@ -5289,11 +5228,9 @@ load_dummy_buffer ( newbuf->b_flags |= BF_DUMMY; } - /* - * When autocommands/'autochdir' option changed directory: go back. - * Let the caller know what the resulting dir was first, in case it is - * important. - */ + // When autocommands/'autochdir' option changed directory: go back. + // Let the caller know what the resulting dir was first, in case it is + // important. os_dirname(resulting_dir, MAXPATHL); restore_start_dir(dirname_start); @@ -5307,42 +5244,38 @@ load_dummy_buffer ( return newbuf; } -/* - * Wipe out the dummy buffer that load_dummy_buffer() created. Restores - * directory to "dirname_start" prior to returning, if autocmds or the - * 'autochdir' option have changed it. - */ +// Wipe out the dummy buffer that load_dummy_buffer() created. Restores +// directory to "dirname_start" prior to returning, if autocmds or the +// 'autochdir' option have changed it. static void wipe_dummy_buffer(buf_T *buf, char_u *dirname_start) { - if (curbuf != buf) { /* safety check */ + if (curbuf != buf) { // safety check cleanup_T cs; - /* Reset the error/interrupt/exception state here so that aborting() - * returns FALSE when wiping out the buffer. Otherwise it doesn't - * work when got_int is set. */ + // Reset the error/interrupt/exception state here so that aborting() + // returns FALSE when wiping out the buffer. Otherwise it doesn't + // work when got_int is set. enter_cleanup(&cs); wipe_buffer(buf, FALSE); - /* Restore the error/interrupt/exception state if not discarded by a - * new aborting error, interrupt, or uncaught exception. */ + // Restore the error/interrupt/exception state if not discarded by a + // new aborting error, interrupt, or uncaught exception. leave_cleanup(&cs); - /* When autocommands/'autochdir' option changed directory: go back. */ + // When autocommands/'autochdir' option changed directory: go back. restore_start_dir(dirname_start); } } -/* - * Unload the dummy buffer that load_dummy_buffer() created. Restores - * directory to "dirname_start" prior to returning, if autocmds or the - * 'autochdir' option have changed it. - */ +// Unload the dummy buffer that load_dummy_buffer() created. Restores +// directory to "dirname_start" prior to returning, if autocmds or the +// 'autochdir' option have changed it. static void unload_dummy_buffer(buf_T *buf, char_u *dirname_start) { - if (curbuf != buf) { /* safety check */ - close_buffer(NULL, buf, DOBUF_UNLOAD, FALSE); + if (curbuf != buf) { // safety check + close_buffer(NULL, buf, DOBUF_UNLOAD, false); - /* When autocommands/'autochdir' option changed directory: go back. */ + // When autocommands/'autochdir' option changed directory: go back. restore_start_dir(dirname_start); } } @@ -6297,14 +6230,12 @@ static int cbuffer_process_args(exarg_T *eap, return OK; } -/* - * ":[range]cbuffer [bufnr]" command. - * ":[range]caddbuffer [bufnr]" command. - * ":[range]cgetbuffer [bufnr]" command. - * ":[range]lbuffer [bufnr]" command. - * ":[range]laddbuffer [bufnr]" command. - * ":[range]lgetbuffer [bufnr]" command. - */ +// ":[range]cbuffer [bufnr]" command. +// ":[range]caddbuffer [bufnr]" command. +// ":[range]cgetbuffer [bufnr]" command. +// ":[range]lbuffer [bufnr]" command. +// ":[range]laddbuffer [bufnr]" command. +// ":[range]lgetbuffer [bufnr]" command. void ex_cbuffer(exarg_T *eap) { buf_T *buf = NULL; @@ -6405,8 +6336,8 @@ void ex_cexpr(exarg_T *eap) qf_info_T *qi = qf_cmd_get_or_alloc_stack(eap, &wp); - /* Evaluate the expression. When the result is a string or a list we can - * use it to fill the errorlist. */ + // Evaluate the expression. When the result is a string or a list we can + // use it to fill the errorlist. typval_T tv; if (eval0(eap->arg, &tv, NULL, true) != FAIL) { if ((tv.v_type == VAR_STRING && tv.vval.v_string != NULL) diff --git a/src/nvim/search.c b/src/nvim/search.c index a298f7333e..5e32715e49 100644 --- a/src/nvim/search.c +++ b/src/nvim/search.c @@ -3777,30 +3777,31 @@ find_prev_quote( return col_start; } -/* - * Find quote under the cursor, cursor at end. - * Returns TRUE if found, else FALSE. - */ -int -current_quote( +// Find quote under the cursor, cursor at end. +// Returns true if found, else false. +bool current_quote( oparg_T *oap, long count, - int include, /* TRUE == include quote char */ - int quotechar /* Quote character */ + bool include, // true == include quote char + int quotechar // Quote character ) + FUNC_ATTR_NONNULL_ALL { char_u *line = get_cursor_line_ptr(); int col_end; int col_start = curwin->w_cursor.col; bool inclusive = false; - int vis_empty = true; // Visual selection <= 1 char - int vis_bef_curs = false; // Visual starts before cursor - int inside_quotes = false; // Looks like "i'" done before - int selected_quote = false; // Has quote inside selection + bool vis_empty = true; // Visual selection <= 1 char + bool vis_bef_curs = false; // Visual starts before cursor + bool did_exclusive_adj = false; // adjusted pos for 'selection' + bool inside_quotes = false; // Looks like "i'" done before + bool selected_quote = false; // Has quote inside selection int i; - int restore_vis_bef = false; // resotre VIsual on abort + bool restore_vis_bef = false; // resotre VIsual on abort - // Correct cursor when 'selection' is "exclusive". + // When 'selection' is "exclusive" move the cursor to where it would be + // with 'selection' "inclusive", so that the logic is the same for both. + // The cursor then is moved forward after adjusting the area. if (VIsual_active) { // this only works within one line if (VIsual.lnum != curwin->w_cursor.lnum) { @@ -3810,6 +3811,14 @@ current_quote( vis_bef_curs = lt(VIsual, curwin->w_cursor); vis_empty = equalpos(VIsual, curwin->w_cursor); if (*p_sel == 'e') { + if (vis_bef_curs) { + dec_cursor(); + did_exclusive_adj = true; + } else if (!vis_empty) { + dec(&VIsual); + did_exclusive_adj = true; + } + vis_empty = equalpos(VIsual, curwin->w_cursor); if (!vis_bef_curs && !vis_empty) { // VIsual needs to be start of Visual selection. pos_T t = curwin->w_cursor; @@ -3819,8 +3828,6 @@ current_quote( vis_bef_curs = true; restore_vis_bef = true; } - dec_cursor(); - vis_empty = equalpos(VIsual, curwin->w_cursor); } } @@ -3846,7 +3853,7 @@ current_quote( /* Find out if we have a quote in the selection. */ while (i <= col_end) if (line[i++] == quotechar) { - selected_quote = TRUE; + selected_quote = true; break; } } @@ -3935,8 +3942,8 @@ current_quote( } } - /* When "include" is TRUE, include spaces after closing quote or before - * the starting quote. */ + // When "include" is true, include spaces after closing quote or before + // the starting quote. if (include) { if (ascii_iswhite(line[col_end + 1])) while (ascii_iswhite(line[col_end + 1])) @@ -3982,9 +3989,10 @@ current_quote( inclusive = true; if (VIsual_active) { if (vis_empty || vis_bef_curs) { - /* decrement cursor when 'selection' is not exclusive */ - if (*p_sel != 'e') + // decrement cursor when 'selection' is not exclusive + if (*p_sel != 'e') { dec_cursor(); + } } else { /* Cursor is at start of Visual area. Set the end of the Visual * area when it was just inside quotes or it didn't end at a @@ -4008,11 +4016,13 @@ current_quote( oap->inclusive = inclusive; } - return OK; + return true; abort_search: if (VIsual_active && *p_sel == 'e') { - inc_cursor(); + if (did_exclusive_adj) { + inc_cursor(); + } if (restore_vis_bef) { pos_T t = curwin->w_cursor; diff --git a/src/nvim/tag.c b/src/nvim/tag.c index 9e8c05fb1e..3629b37c32 100644 --- a/src/nvim/tag.c +++ b/src/nvim/tag.c @@ -1194,12 +1194,10 @@ static int find_tagfunc_tags( taglist = rettv.vval.v_list; TV_LIST_ITER_CONST(taglist, li, { - char_u *mfp; char_u *res_name; char_u *res_fname; char_u *res_cmd; char_u *res_kind; - int len; int has_extra = 0; int name_only = flags & TAG_NAMES; @@ -1208,7 +1206,7 @@ static int find_tagfunc_tags( break; } - len = 2; + size_t len = 2; res_name = NULL; res_fname = NULL; res_cmd = NULL; @@ -1254,15 +1252,7 @@ static int find_tagfunc_tags( break; } - if (name_only) { - mfp = vim_strsave(res_name); - } else { - mfp = (char_u *)xmalloc((int)sizeof(char_u) + len + 1); - } - - if (mfp == NULL) { - continue; - } + char_u *const mfp = name_only ? vim_strsave(res_name) : xmalloc(len + 2); if (!name_only) { char_u *p = mfp; @@ -1669,12 +1659,9 @@ find_tags( break; /* End the binary search without a match. */ else search_info.curr_offset = offset; - } - /* - * Skipping back (after a match during binary search). - */ - else if (state == TS_SKIP_BACK) { - search_info.curr_offset -= LSIZE * 2; + } else if (state == TS_SKIP_BACK) { + // Skipping back (after a match during binary search). + search_info.curr_offset -= lbuf_size * 2; if (search_info.curr_offset < 0) { search_info.curr_offset = 0; rewind(fp); @@ -1690,7 +1677,7 @@ find_tags( /* Adjust the search file offset to the correct position */ search_info.curr_offset_used = search_info.curr_offset; vim_fseek(fp, search_info.curr_offset, SEEK_SET); - eof = vim_fgets(lbuf, LSIZE, fp); + eof = vim_fgets(lbuf, lbuf_size, fp); if (!eof && search_info.curr_offset != 0) { /* The explicit cast is to work around a bug in gcc 3.4.2 * (repeated below). */ @@ -1700,12 +1687,12 @@ find_tags( vim_fseek(fp, search_info.low_offset, SEEK_SET); search_info.curr_offset = search_info.low_offset; } - eof = vim_fgets(lbuf, LSIZE, fp); + eof = vim_fgets(lbuf, lbuf_size, fp); } /* skip empty and blank lines */ while (!eof && vim_isblankline(lbuf)) { search_info.curr_offset = vim_ftell(fp); - eof = vim_fgets(lbuf, LSIZE, fp); + eof = vim_fgets(lbuf, lbuf_size, fp); } if (eof) { /* Hit end of file. Skip backwards. */ @@ -1721,10 +1708,9 @@ find_tags( else { /* skip empty and blank lines */ do { - if (use_cscope) - eof = cs_fgets(lbuf, LSIZE); - else - eof = vim_fgets(lbuf, LSIZE, fp); + eof = use_cscope + ? cs_fgets(lbuf, lbuf_size) + : vim_fgets(lbuf, lbuf_size, fp); } while (!eof && vim_isblankline(lbuf)); if (eof) { @@ -1849,19 +1835,14 @@ parse_line: // When the line is too long the NUL will not be in the // last-but-one byte (see vim_fgets()). // Has been reported for Mozilla JS with extremely long names. - // In that case we can't parse it and we ignore the line. - if (lbuf[LSIZE - 2] != NUL && !use_cscope) { - if (p_verbose >= 5) { - verbose_enter(); - MSG(_("Ignoring long line in tags file")); - verbose_leave(); - } - if (state != TS_LINEAR) { - // Avoid getting stuck. - linear = true; - state = TS_LINEAR; - vim_fseek(fp, search_info.low_offset, SEEK_SET); - } + // In that case we need to increase lbuf_size. + if (lbuf[lbuf_size - 2] != NUL && !use_cscope) { + lbuf_size *= 2; + xfree(lbuf); + lbuf = xmalloc(lbuf_size); + // this will try the same thing again, make sure the offset is + // different + search_info.curr_offset = 0; continue; } @@ -2664,6 +2645,9 @@ static int jumpto_tag( str = tagp.command; for (pbuf_end = pbuf; *str && *str != '\n' && *str != '\r'; ) { *pbuf_end++ = *str++; + if (pbuf_end - pbuf + 1 >= LSIZE) { + break; + } } *pbuf_end = NUL; diff --git a/src/nvim/testdir/Makefile b/src/nvim/testdir/Makefile index 08353509af..1e9031e098 100644 --- a/src/nvim/testdir/Makefile +++ b/src/nvim/testdir/Makefile @@ -48,7 +48,8 @@ NEW_TESTS_IGNORE := $(NEW_TESTS_IN_ALOT) $(NEW_TESTS_ALOT) \ test_listlbr \ test_largefile \ -NEW_TESTS ?= $(addsuffix .res,$(sort $(filter-out $(NEW_TESTS_IGNORE),$(basename $(notdir $(wildcard test_*.vim))))) $(NEW_TESTS_ALOT)) +NEW_TESTS ?= $(sort $(filter-out $(NEW_TESTS_IGNORE),$(basename $(notdir $(wildcard test_*.vim))))) $(NEW_TESTS_ALOT) +NEW_TESTS_RES ?= $(addsuffix .res,$(NEW_TESTS)) ifdef VALGRIND_GDB @@ -112,6 +113,16 @@ fixff: -$(NVIM_PRG) $(NO_INITS) -u unix.vim "+argdo set ff=dos|upd" +q \ dotest.in +# Execute an individual new style test, e.g.: +# make test_largefile +$(NEW_TESTS): + rm -f $@.res test.log messages + @MAKEFLAGS=--no-print-directory $(MAKE) -f Makefile $@.res + @cat messages + @if test -f test.log; then \ + exit 1; \ + fi + RM_ON_RUN := test.out X* viminfo RM_ON_START := test.ok RUN_VIM := $(TOOL) $(NVIM_PRG) -u unix.vim -U NONE -i viminfo --headless --noplugin -s dotest.in @@ -172,7 +183,7 @@ newtests: newtestssilent cat messages && cat test.log; \ fi" -newtestssilent: $(NEW_TESTS) +newtestssilent: $(NEW_TESTS_RES) %.res: %.vim .gdbinit @echo "[OLDTEST] Running" $* diff --git a/src/nvim/testdir/runtest.vim b/src/nvim/testdir/runtest.vim index 5c2e570adf..2d4134a644 100644 --- a/src/nvim/testdir/runtest.vim +++ b/src/nvim/testdir/runtest.vim @@ -297,6 +297,8 @@ let s:flaky_tests = [ \ 'Test_repeat_three()', \ 'Test_state()', \ 'Test_stop_all_in_callback()', + \ 'Test_term_mouse_double_click_to_create_tab', + \ 'Test_term_mouse_multiple_clicks_to_visually_select()', \ 'Test_terminal_composing_unicode()', \ 'Test_terminal_redir_file()', \ 'Test_terminal_tmap()', diff --git a/src/nvim/testdir/shared.vim b/src/nvim/testdir/shared.vim index a5d83d6a25..b0b59db686 100644 --- a/src/nvim/testdir/shared.vim +++ b/src/nvim/testdir/shared.vim @@ -69,7 +69,8 @@ endfunc " Read the port number from the Xportnr file. func GetPort() let l = [] - for i in range(200) + " with 200 it sometimes failed + for i in range(400) try let l = readfile("Xportnr") catch @@ -279,11 +280,15 @@ func GetVimCommand(...) return cmd endfunc -" Get the command to run Vim, with --clean. +" Get the command to run Vim, with --clean instead of "-u NONE". func GetVimCommandClean() let cmd = GetVimCommand() let cmd = substitute(cmd, '-u NONE', '--clean', '') let cmd = substitute(cmd, '--headless', '', '') + + " Optionally run Vim under valgrind + " let cmd = 'valgrind --tool=memcheck --leak-check=yes --num-callers=25 --log-file=valgrind ' . cmd + return cmd endfunc diff --git a/src/nvim/testdir/test_options.vim b/src/nvim/testdir/test_options.vim index f4f5cbca61..6fcc372591 100644 --- a/src/nvim/testdir/test_options.vim +++ b/src/nvim/testdir/test_options.vim @@ -476,13 +476,19 @@ func Test_shortmess_F2() call assert_match('file2', execute('bn', '')) set shortmess+=F call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) set hidden call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) set nohidden call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) call assert_true(empty(execute('bn', ''))) + " call assert_false(test_getvalue('need_fileinfo')) " Accommodate Nvim default. set shortmess-=F call assert_match('file1', execute('bn', '')) diff --git a/src/nvim/testdir/test_substitute.vim b/src/nvim/testdir/test_substitute.vim index e209310a05..e94bd22cea 100644 --- a/src/nvim/testdir/test_substitute.vim +++ b/src/nvim/testdir/test_substitute.vim @@ -241,7 +241,7 @@ func Test_sub_cmd_3() call Run_SubCmd_Tests(tests) endfunc -" Test for submatch() on :substitue. +" Test for submatch() on :substitute. func Test_sub_cmd_4() set magic& set cpo& diff --git a/src/nvim/testdir/test_tagjump.vim b/src/nvim/testdir/test_tagjump.vim index f93af76f17..fe98ef1ae2 100644 --- a/src/nvim/testdir/test_tagjump.vim +++ b/src/nvim/testdir/test_tagjump.vim @@ -449,7 +449,8 @@ func Test_tag_line_toolong() call assert_report(v:exception) catch /.*/ endtry - call assert_equal('Ignoring long line in tags file', split(execute('messages'), '\n')[-1]) + call assert_equal('Searching tags file Xtags', split(execute('messages'), '\n')[-1]) + call writefile([ \ '123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567 django/contrib/admin/templates/admin/edit_inline/stacked.html 16;" j line:16 language:HTML' \ ], 'Xtags') @@ -460,8 +461,26 @@ func Test_tag_line_toolong() call assert_report(v:exception) catch /.*/ endtry - call assert_equal('Ignoring long line in tags file', split(execute('messages'), '\n')[-1]) + call assert_equal('Searching tags file Xtags', split(execute('messages'), '\n')[-1]) + + " binary search works in file with long line + call writefile([ + \ 'asdfasfd nowhere 16', + \ 'foobar Xsomewhere 3; " 12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567', + \ 'zasdfasfd nowhere 16', + \ ], 'Xtags') + call writefile([ + \ 'one', + \ 'two', + \ 'trhee', + \ 'four', + \ ], 'Xsomewhere') + tag foobar + call assert_equal('Xsomewhere', expand('%')) + call assert_equal(3, getcurpos()[1]) + call delete('Xtags') + call delete('Xsomewhere') set tags& let &verbose = old_vbs endfunc diff --git a/src/nvim/testdir/test_textobjects.vim b/src/nvim/testdir/test_textobjects.vim index 448b2dc51c..b20c4df311 100644 --- a/src/nvim/testdir/test_textobjects.vim +++ b/src/nvim/testdir/test_textobjects.vim @@ -46,11 +46,18 @@ func Test_quote_selection_selection_exclusive() new call setline(1, "a 'bcde' f") set selection=exclusive + exe "norm! fdvhi'y" call assert_equal('bcde', @") + let @"='dummy' exe "norm! $gevi'y" call assert_equal('bcde', @") + + let @"='dummy' + exe "norm! 0fbhvi'y" + call assert_equal('bcde', @") + set selection&vim bw! endfunc diff --git a/src/nvim/window.c b/src/nvim/window.c index 2a7578e33c..dee36df433 100644 --- a/src/nvim/window.c +++ b/src/nvim/window.c @@ -633,6 +633,12 @@ void win_set_minimal_style(win_T *wp) xfree(wp->w_p_scl); wp->w_p_scl = (char_u *)xstrdup("auto"); } + + // colorcolumn: cleared + if (wp->w_p_cc != NULL && *wp->w_p_cc != NUL) { + xfree(wp->w_p_cc); + wp->w_p_cc = (char_u *)xstrdup(""); + } } void win_config_float(win_T *wp, FloatConfig fconfig) diff --git a/test/functional/api/mark_extended_spec.lua b/test/functional/api/mark_extended_spec.lua index 76db9f9d81..1f6c00b7d2 100644 --- a/test/functional/api/mark_extended_spec.lua +++ b/test/functional/api/mark_extended_spec.lua @@ -422,7 +422,7 @@ describe('Extmarks buffer api', function() set_extmark(ns, marks[1], 1, 2) -- Insert a fullwidth (two col) tilde, NICE feed('0iļ½<esc>') - check_undo_redo(ns, marks[1], 1, 2, 1, 3) + check_undo_redo(ns, marks[1], 1, 2, 1, 5) end) it('marks move with blockwise inserts #extmarks', function() @@ -475,6 +475,13 @@ describe('Extmarks buffer api', function() check_undo_redo(ns, marks[2], 0, 3, 1, 2) end) + it('deleting right before a mark works #extmarks', function() + -- op_delete in ops.c + set_extmark(ns, marks[1], 0, 2) + feed('0lx') + check_undo_redo(ns, marks[1], 0, 2, 0, 1) + end) + it('deleting on a mark works #extmarks', function() -- op_delete in ops.c set_extmark(ns, marks[1], 0, 2) diff --git a/test/functional/lua/vim_spec.lua b/test/functional/lua/vim_spec.lua index 028f2dcd52..ebe394f164 100644 --- a/test/functional/lua/vim_spec.lua +++ b/test/functional/lua/vim_spec.lua @@ -353,10 +353,14 @@ describe('lua stdlib', function() it('vim.list_extend', function() eq({1,2,3}, exec_lua [[ return vim.list_extend({1}, {2,3}) ]]) - eq('Error executing lua: .../shared.lua: src must be a table', + eq('Error executing lua: .../shared.lua: src: expected table, got nil', pcall_err(exec_lua, [[ return vim.list_extend({1}, nil) ]])) eq({1,2}, exec_lua [[ return vim.list_extend({1}, {2;a=1}) ]]) eq(true, exec_lua [[ local a = {1} return vim.list_extend(a, {2;a=1}) == a ]]) + eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1) ]]) + eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 2) ]]) + eq({}, exec_lua [[ return vim.list_extend({}, {2;a=1}, 1, -1) ]]) + eq({2}, exec_lua [[ return vim.list_extend({}, {2;a=1}, -1, 2) ]]) end) it('vim.tbl_add_reverse_lookup', function() @@ -549,4 +553,61 @@ describe('lua stdlib', function() eq(false, exec_lua("return vim.is_callable('foo')")) eq(false, exec_lua("return vim.is_callable({})")) end) + + it('vim.g', function() + exec_lua [[ + vim.api.nvim_set_var("testing", "hi") + vim.api.nvim_set_var("other", 123) + ]] + eq('hi', funcs.luaeval "vim.g.testing") + eq(123, funcs.luaeval "vim.g.other") + eq(NIL, funcs.luaeval "vim.g.nonexistant") + end) + + it('vim.env', function() + exec_lua [[ + vim.fn.setenv("A", 123) + ]] + eq('123', funcs.luaeval "vim.env.A") + eq(NIL, funcs.luaeval "vim.env.B") + end) + + it('vim.v', function() + eq(funcs.luaeval "vim.api.nvim_get_vvar('progpath')", funcs.luaeval "vim.v.progpath") + eq(false, funcs.luaeval "vim.v['false']") + eq(NIL, funcs.luaeval "vim.v.null") + end) + + it('vim.bo', function() + eq('', funcs.luaeval "vim.bo.filetype") + exec_lua [[ + vim.api.nvim_buf_set_option(0, "filetype", "markdown") + BUF = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(BUF, "modifiable", false) + ]] + eq(false, funcs.luaeval "vim.bo.modified") + eq('markdown', funcs.luaeval "vim.bo.filetype") + eq(false, funcs.luaeval "vim.bo(BUF).modifiable") + exec_lua [[ + vim.bo.filetype = '' + vim.bo(BUF).modifiable = true + ]] + eq('', funcs.luaeval "vim.bo.filetype") + eq(true, funcs.luaeval "vim.bo(BUF).modifiable") + end) + + it('vim.wo', function() + eq('', funcs.luaeval "vim.bo.filetype") + exec_lua [[ + vim.api.nvim_win_set_option(0, "cole", 2) + BUF = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_option(BUF, "modifiable", false) + ]] + eq(2, funcs.luaeval "vim.wo.cole") + exec_lua [[ + vim.wo.conceallevel = 0 + vim.bo(BUF).modifiable = true + ]] + eq(0, funcs.luaeval "vim.wo.cole") + end) end) diff --git a/test/functional/plugin/lsp/util_spec.lua b/test/functional/plugin/lsp/util_spec.lua new file mode 100644 index 0000000000..1cf0e48be4 --- /dev/null +++ b/test/functional/plugin/lsp/util_spec.lua @@ -0,0 +1,76 @@ +local helpers = require('test.functional.helpers')(after_each) +local eq = helpers.eq +local exec_lua = helpers.exec_lua +local dedent = helpers.dedent +local insert = helpers.insert +local clear = helpers.clear + +describe('LSP util', function() + local test_text = dedent([[ + First line of text + Second line of text + Third line of text + Fourth line of text]]) + + local function reset() + clear() + insert(test_text) + end + + before_each(reset) + + local function make_edit(y_0, x_0, y_1, x_1, text) + return { + range = { + start = { line = y_0, character = x_0 }; + ["end"] = { line = y_1, character = x_1 }; + }; + newText = type(text) == 'table' and table.concat(text, '\n') or (text or ""); + } + end + + local function buf_lines(bufnr) + return exec_lua("return vim.api.nvim_buf_get_lines((...), 0, -1, false)", bufnr) + end + + describe('apply_edits', function() + it('should apply simple edits', function() + local edits = { + make_edit(0, 0, 0, 0, {"123"}); + make_edit(1, 0, 1, 1, {"2"}); + make_edit(2, 0, 2, 2, {"3"}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) + eq({ + '123First line of text'; + '2econd line of text'; + '3ird line of text'; + 'Fourth line of text'; + }, buf_lines(1)) + end) + + it('should apply complex edits', function() + local edits = { + make_edit(0, 0, 0, 0, {"", "12"}); + make_edit(0, 0, 0, 0, {"3", "foo"}); + make_edit(0, 1, 0, 1, {"bar", "123"}); + make_edit(0, #"First ", 0, #"First line of text", {"guy"}); + make_edit(1, 0, 1, #'Second', {"baz"}); + make_edit(2, #'Th', 2, #"Third", {"e next"}); + make_edit(3, #'', 3, #"Fourth", {"another line of text", "before this"}); + make_edit(3, #'Fourth', 3, #"Fourth line of text", {"!"}); + } + exec_lua('vim.lsp.util.apply_text_edits(...)', edits, 1) + eq({ + ''; + '123'; + 'fooFbar'; + '123irst guy'; + 'baz line of text'; + 'The next line of text'; + 'another line of text'; + 'before this!'; + }, buf_lines(1)) + end) + end) +end) diff --git a/test/functional/ui/bufhl_spec.lua b/test/functional/ui/bufhl_spec.lua index 65c5f67726..f589bb0e83 100644 --- a/test/functional/ui/bufhl_spec.lua +++ b/test/functional/ui/bufhl_spec.lua @@ -217,6 +217,161 @@ describe('Buffer highlighting', function() | ]]) end) + + it('and adjusting columns', function() + -- insert before + feed('ggiquite <esc>') + screen:expect{grid=[[ + quite^ a {5:longer} example | + in {6:order} to {7:de}{5:monstr}{7:ate} | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + + feed('u') + screen:expect{grid=[[ + ^a {5:longer} example | + in {6:order} to {7:de}{5:monstr}{7:ate} | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + 1 change; before #2 {MATCH:.*}| + ]]} + + -- change/insert in the middle + feed('+fesAAAA') + screen:expect{grid=[[ + a {5:longer} example | + in {6:ordAAAA^r} to {7:de}{5:monstr}{7:ate} | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + {7:-- INSERT --} | + ]]} + + feed('<esc>tdD') + screen:expect{grid=[[ + a {5:longer} example | + in {6:ordAAAAr} t^o | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + + feed('u') + screen:expect{grid=[[ + a {5:longer} example | + in {6:ordAAAAr} to^ demonstrate | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + 1 change; before #4 {MATCH:.*}| + ]]} + + feed('u') + screen:expect{grid=[[ + a {5:longer} example | + in {6:ord^er} to demonstrate | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + 1 change; before #3 {MATCH:.*}| + ]]} + end) + + it('and joining lines', function() + feed('ggJJJ') + screen:expect{grid=[[ + a {5:longer} example in {6:order} to {7:de}{5:monstr}{7:ate}| + {7: combin}{8:ing hi}{7:ghlights^ }{8:from diff}{7:erent sou}| + {7:rces} | + {1:~ }| + {1:~ }| + {1:~ }| + {1:~ }| + | + ]]} + + -- TODO(bfredl): perhaps better undo + feed('uuu') + screen:expect{grid=[[ + ^a longer example | + in order to demonstrate | + combining highlights | + from different sources | + {1:~ }| + {1:~ }| + {1:~ }| + 1 more line; before #2 {MATCH:.*}| + ]]} + end) + + it('and splitting lines', function() + feed('2Gtti<cr>') + screen:expect{grid=[[ + a {5:longer} example | + in {6:order} | + ^ to {7:de}{5:monstr}{7:ate} | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {7:-- INSERT --} | + ]]} + + -- TODO(bfredl): keep both "parts" after split, requires proper extmark ranges + feed('<esc>tsi<cr>') + screen:expect{grid=[[ + a {5:longer} example | + in {6:order} | + to {7:de}{5:mo} | + ^nstrate | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {7:-- INSERT --} | + ]]} + + -- TODO(bfredl): perhaps better undo + feed('<esc>u') + screen:expect{grid=[[ + a {5:longer} example | + in {6:order} | + to demo{7:^nstrat}{8:e} | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + 1 line less; before #3 {MATCH:.*}| + ]]} + + feed('<esc>u') + screen:expect{grid=[[ + a {5:longer} example | + in order^ to demonstrate | + {7:combin}{8:ing}{9: hi}ghlights | + {9:from }{8:diff}{7:erent} sources | + {1:~ }| + {1:~ }| + {1:~ }| + 1 line less; before #2 {MATCH:.*}| + ]]} + end) end) it('prioritizes latest added highlight', function() diff --git a/test/functional/ui/float_spec.lua b/test/functional/ui/float_spec.lua index dbaf6f802b..8ddb2e90a6 100644 --- a/test/functional/ui/float_spec.lua +++ b/test/functional/ui/float_spec.lua @@ -39,6 +39,7 @@ describe('floating windows', function() [19] = {foreground = Screen.colors.DarkBlue, background = Screen.colors.WebGray}, [20] = {bold = true, foreground = Screen.colors.Brown}, [21] = {background = Screen.colors.Gray90}, + [22] = {background = Screen.colors.LightRed}, } it('behavior', function() @@ -398,6 +399,7 @@ describe('floating windows', function() it("can use 'minimal' style", function() command('set number') command('set signcolumn=yes') + command('set colorcolumn=1') command('set cursorline') command('set foldcolumn=1') command('hi NormalFloat guibg=#333333') @@ -414,9 +416,9 @@ describe('floating windows', function() [2:----------------------------------------]| [3:----------------------------------------]| ## grid 2 - {19: }{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } | + {19: }{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } | {0:~ }| {0:~ }| {0:~ }| @@ -430,9 +432,9 @@ describe('floating windows', function() ]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}} else screen:expect{grid=[[ - {19: }{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } {15:x } | + {19: }{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } {15:x } | {0:~ }{15:y }{0: }| {0:~ }{15: }{0: }| {0:~ }{15: }{0: }| @@ -454,9 +456,9 @@ describe('floating windows', function() [2:----------------------------------------]| [3:----------------------------------------]| ## grid 2 - {19: }{17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } | + {19: }{17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } | {0:~ }| {0:~ }| {0:~ }| @@ -471,9 +473,9 @@ describe('floating windows', function() else screen:expect([[ - {19: }{17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } {17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{15:x } | + {19: }{17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } {17:š¢ĢĢĢĢĢ
Ģš¢ĢĢĢĢĢ
Ģ}{15:x } | {0:~ }{19: }{15:y }{0: }| {0:~ }{19: }{15: }{0: }| {0:~ }{15: }{0: }| @@ -495,9 +497,9 @@ describe('floating windows', function() [2:----------------------------------------]| [3:----------------------------------------]| ## grid 2 - {19: }{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } | + {19: }{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } | {0:~ }| {0:~ }| {0:~ }| @@ -511,9 +513,9 @@ describe('floating windows', function() ]], float_pos={[4] = {{id = 1001}, "NW", 1, 4, 10, true}}} else screen:expect([[ - {19: }{20: 1 }{21:^x }| - {19: }{14: 2 }y | - {19: }{14: 3 } {15: } | + {19: }{20: 1 }{22:^x}{21: }| + {19: }{14: 2 }{22:y} | + {19: }{14: 3 }{22: } {15: } | {0:~ }{15: }{0: }| {0:~ }{15: }{0: }| {0:~ }{15: }{0: }| diff --git a/test/helpers.lua b/test/helpers.lua index 3f29a28c0d..98f003f208 100644 --- a/test/helpers.lua +++ b/test/helpers.lua @@ -99,6 +99,9 @@ function module.pcall_err(fn, ...) -- to this: -- Error executing lua: .../foo.lua:186: Expected string, got number errmsg = errmsg:gsub([[lua: [a-zA-Z]?:?[^:]-[/\]([^:/\]+):%d+: ]], 'lua: .../%1: ') + -- Compiled modules will not have a path and will just be a name like + -- shared.lua:186, so strip the number. + errmsg = errmsg:gsub([[lua: ([^:/\ ]+):%d+: ]], 'lua: .../%1: ') -- ^ Windows drive-letter (C:) return errmsg end |