diff options
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r-- | runtime/lua/vim/highlight.lua | 2 | ||||
-rw-r--r-- | runtime/lua/vim/lsp.lua | 363 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/buf.lua | 144 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/callbacks.lua | 98 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/log.lua | 17 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/protocol.lua | 25 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/rpc.lua | 148 | ||||
-rw-r--r-- | runtime/lua/vim/lsp/util.lua | 377 | ||||
-rw-r--r-- | runtime/lua/vim/shared.lua | 93 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter.lua | 272 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/highlighter.lua | 210 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/language.lua | 37 | ||||
-rw-r--r-- | runtime/lua/vim/treesitter/query.lua | 335 | ||||
-rw-r--r-- | runtime/lua/vim/tshighlighter.lua | 116 | ||||
-rw-r--r-- | runtime/lua/vim/uri.lua | 20 |
15 files changed, 1695 insertions, 562 deletions
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua index ce0a3de520..705b34dc99 100644 --- a/runtime/lua/vim/highlight.lua +++ b/runtime/lua/vim/highlight.lua @@ -14,7 +14,7 @@ function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive) inclusive = inclusive or false -- sanity check - if start[2] < 0 or finish[2] < start[2] then return end + if start[2] < 0 or finish[1] < start[1] then return end local region = vim.region(bufnr, start, finish, rtype, inclusive) for linenr, cols in pairs(region) do diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua index 6fe1d15b7e..1a0015e2db 100644 --- a/runtime/lua/vim/lsp.lua +++ b/runtime/lua/vim/lsp.lua @@ -25,13 +25,44 @@ local lsp = { -- format_rpc_error = lsp_rpc.format_rpc_error; } +-- maps request name to the required resolved_capability in the client. +lsp._request_name_to_capability = { + ['textDocument/hover'] = 'hover'; + ['textDocument/signatureHelp'] = 'signature_help'; + ['textDocument/definition'] = 'goto_definition'; + ['textDocument/implementation'] = 'implementation'; + ['textDocument/declaration'] = 'declaration'; + ['textDocument/typeDefinition'] = 'type_definition'; + ['textDocument/documentSymbol'] = 'document_symbol'; + ['textDocument/workspaceSymbol'] = 'workspace_symbol'; + ['textDocument/prepareCallHierarchy'] = 'call_hierarchy'; + ['textDocument/rename'] = 'rename'; + ['textDocument/codeAction'] = 'code_action'; + ['workspace/executeCommand'] = 'execute_command'; + ['textDocument/references'] = 'find_references'; + ['textDocument/rangeFormatting'] = 'document_range_formatting'; + ['textDocument/formatting'] = 'document_formatting'; + ['textDocument/completion'] = 'completion'; + ['textDocument/documentHighlight'] = 'document_highlight'; +} + -- TODO improve handling of scratch buffers with LSP attached. +--@private +--- Concatenates and writes a list of strings to the Vim error buffer. +--- +--@param {...} (List of strings) List to write to the buffer local function err_message(...) nvim_err_writeln(table.concat(vim.tbl_flatten{...})) nvim_command("redraw") end +--@private +--- Returns the buffer number for the given {bufnr}. +--- +--@param bufnr (number) Buffer number to resolve. Defaults to the current +---buffer if not given. +--@returns bufnr (number) Number of requested buffer local function resolve_bufnr(bufnr) validate { bufnr = { bufnr, 'n', true } } if bufnr == nil or bufnr == 0 then @@ -40,6 +71,21 @@ local function resolve_bufnr(bufnr) return bufnr end +--@private +--- callback called by the client when trying to call a method that's not +--- supported in any of the servers registered for the current buffer. +--@param method (string) name of the method +function lsp._unsupported_method(method) + local msg = string.format("method %s is not supported by any of the servers registered for the current buffer", method) + log.warn(msg) + return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) +end + +--@private +--- Checks whether a given path is a directory. +--- +--@param filename (string) path to check +--@returns true if {filename} exists and is a directory, false otherwise local function is_dir(filename) validate{filename={filename,'s'}} local stat = uv.fs_stat(filename) @@ -55,6 +101,10 @@ local valid_encodings = { } local client_index = 0 +--@private +--- Returns a new, unused client id. +--- +--@returns (number) client id local function next_client_id() client_index = client_index + 1 return client_index @@ -64,6 +114,12 @@ local active_clients = {} local all_buffer_active_clients = {} local uninitialized_clients = {} +--@private +--- Invokes a callback for each LSP client attached to the buffer {bufnr}. +--- +--@param bufnr (Number) of buffer +--@param callback (function({client}, {client_id}, {bufnr}) Function to run on +---each client attached to that buffer. local function for_each_buffer_client(bufnr, callback) validate { callback = { callback, 'f' }; @@ -88,6 +144,11 @@ lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_rever ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1; }) +--@private +--- Normalizes {encoding} to valid LSP encoding names. +--- +--@param encoding (string) Encoding to normalize +--@returns (string) normalized encoding name local function validate_encoding(encoding) validate { encoding = { encoding, 's' }; @@ -96,6 +157,13 @@ local function validate_encoding(encoding) or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding)) end +--@internal +--- Parses a command invocation into the command itself and its args. If there +--- are no arguments, an empty table is returned as the second argument. +--- +--@param input (List) +--@returns (string) the command +--@returns (list of strings) its arguments function lsp._cmd_parts(input) vim.validate{cmd={ input, @@ -114,12 +182,27 @@ function lsp._cmd_parts(input) return cmd, cmd_args end +--@private +--- Augments a validator function with support for optional (nil) values. +--- +--@param fn (function(v)) The original validator function; should return a +---bool. +--@returns (function(v)) The augmented function. Also returns true if {v} is +---`nil`. local function optional_validator(fn) return function(v) return v == nil or fn(v) end end +--@private +--- Validates a client configuration as given to |vim.lsp.start_client()|. +--- +--@param config (table) +--@returns (table) "Cleaned" config, containing only the command, its +---arguments, and a valid encoding. +--- +--@see |vim.lsp.start_client()| local function validate_client_config(config) validate { config = { config, 't' }; @@ -148,6 +231,11 @@ local function validate_client_config(config) } end +--@private +--- Returns full text of buffer {bufnr} as a string. +--- +--@param bufnr (number) Buffer handle, or 0 for current. +--@returns Buffer text as string. local function buf_get_full_text(bufnr) local text = table.concat(nvim_buf_get_lines(bufnr, 0, -1, true), '\n') if nvim_buf_get_option(bufnr, 'eol') then @@ -156,6 +244,11 @@ local function buf_get_full_text(bufnr) return text end +--@private +--- Default handler for the 'textDocument/didOpen' LSP notification. +--- +--@param bufnr (Number) Number of the buffer, or 0 for current +--@param client Client object local function text_document_did_open_handler(bufnr, client) if not client.resolved_capabilities.text_document_open_close then return @@ -176,74 +269,90 @@ local function text_document_did_open_handler(bufnr, client) util.buf_versions[bufnr] = params.textDocument.version end ---- LSP client object. +-- FIXME: DOC: Shouldn't need to use a dummy function +-- +--- LSP client object. You can get an active client object via +--- |vim.lsp.get_client_by_id()| or |vim.lsp.get_active_clients()|. --- --- - Methods: --- ---- - request(method, params, [callback]) ---- Send a request to the server. If callback is not specified, it will use ---- {client.callbacks} to try to find a callback. If one is not found there, ---- then an error will occur. +--- - request(method, params, [callback], bufnr) +--- Sends a request to the server. --- This is a thin wrapper around {client.rpc.request} with some additional --- checking. ---- Returns a boolean to indicate if the notification was successful. If it ---- is false, then it will always be false (the client has shutdown). ---- If it was successful, then it will return the request id as the second ---- result. You can use this with `notify("$/cancel", { id = request_id })` ---- to cancel the request. This helper is made automatically with ---- |vim.lsp.buf_request()| ---- Returns: status, [client_id] +--- If {callback} is not specified, it will use {client.callbacks} to try to +--- find a callback. If one is not found there, then an error will occur. +--- Returns: {status}, {[client_id]}. {status} is a boolean indicating if +--- the notification was successful. If it is `false`, then it will always +--- be `false` (the client has shutdown). +--- If {status} is `true`, the function returns {request_id} as the second +--- result. You can use this with `client.cancel_request(request_id)` +--- to cancel the request. --- --- - notify(method, params) ---- This is just {client.rpc.notify}() ---- Returns a boolean to indicate if the notification was successful. If it ---- is false, then it will always be false (the client has shutdown). ---- Returns: status +--- Sends a notification to an LSP server. +--- Returns: a boolean to indicate if the notification was successful. If +--- it is false, then it will always be false (the client has shutdown). --- --- - cancel_request(id) ---- This is just {client.rpc.notify}("$/cancelRequest", { id = id }) ---- Returns the same as `notify()`. +--- Cancels a request with a given request id. +--- Returns: same as `notify()`. --- --- - stop([force]) ---- Stop a client, optionally with force. +--- Stops a client, optionally with force. --- By default, it will just ask the server to shutdown without force. --- If you request to stop a client which has previously been requested to --- shutdown, it will automatically escalate and force shutdown. --- --- - is_stopped() ---- Returns true if the client is fully stopped. +--- Checks whether a client is stopped. +--- Returns: true if the client is fully stopped. +--- +--- - on_attach(bufnr) +--- Runs the on_attach function from the client's config if it was defined. --- --- - Members ---- - id (number): The id allocated to the client. +--- - {id} (number): The id allocated to the client. --- ---- - name (string): If a name is specified on creation, that will be +--- - {name} (string): If a name is specified on creation, that will be --- used. Otherwise it is just the client id. This is used for --- logs and messages. --- ---- - offset_encoding (string): The encoding used for communicating ---- with the server. You can modify this in the `on_init` method +--- - {rpc} (table): RPC client object, for low level interaction with the +--- client. See |vim.lsp.rpc.start()|. +--- +--- - {offset_encoding} (string): The encoding used for communicating +--- with the server. You can modify this in the `config`'s `on_init` method --- before text is sent to the server. --- ---- - callbacks (table): The callbacks used by the client as +--- - {callbacks} (table): The callbacks used by the client as --- described in |lsp-callbacks|. --- ---- - config (table): copy of the table that was passed by the user +--- - {config} (table): copy of the table that was passed by the user --- to |vim.lsp.start_client()|. --- ---- - server_capabilities (table): Response from the server sent on +--- - {server_capabilities} (table): Response from the server sent on --- `initialize` describing the server's capabilities. --- ---- - resolved_capabilities (table): Normalized table of +--- - {resolved_capabilities} (table): Normalized table of --- capabilities that we have detected based on the initialize --- response from the server in `server_capabilities`. function lsp.client() error() end +-- FIXME: DOC: Currently all methods on the `vim.lsp.client` object are +-- documented twice: Here, and on the methods themselves (e.g. +-- `client.request()`). This is a workaround for the vimdoc generator script +-- not handling method names correctly. If you change the documentation on +-- either, please make sure to update the other as well. +-- --- Starts and initializes a client with the given configuration. --- --- Parameters `cmd` and `root_dir` are required. --- +--- The following parameters describe fields in the {config} table. +--- --@param root_dir: (required, string) Directory where the LSP server will base --- its rootUri on initialization. --- @@ -271,8 +380,8 @@ end --- --@param callbacks Map of language server method names to --- `function(err, method, params, client_id)` handler. Invoked for: ---- - Notifications from the server, where `err` will always be `nil`. ---- - Requests initiated by the server. For these you can respond by returning +--- - Notifications to the server, where `err` will always be `nil`. +--- - Requests by the server. For these you can respond by returning --- two values: `result, err` where err must be shaped like a RPC error, --- i.e. `{ code, message, data? }`. Use |vim.lsp.rpc_response_error()| to --- help with this. @@ -297,7 +406,7 @@ end --@param before_init Callback with parameters (initialize_params, config) --- invoked before the LSP "initialize" phase, where `params` contains the --- parameters being sent to the server and `config` is the config that was ---- passed to `start_client()`. You can use this to modify parameters before +--- passed to |vim.lsp.start_client()|. You can use this to modify parameters before --- they are sent. --- --@param on_init Callback (client, initialize_result) invoked after LSP @@ -319,9 +428,8 @@ end --@param trace: "off" | "messages" | "verbose" | nil passed directly to the language --- server in the initialize request. Invalid/empty values will default to "off" --- ---@returns Client id. |vim.lsp.get_client_by_id()| Note: client is only ---- available after it has been initialized, which may happen after a small ---- delay (or never if there is an error). Use `on_init` to do any actions once +--@returns Client id. |vim.lsp.get_client_by_id()| Note: client may not be +--- fully initialized. Use `on_init` to do any actions once --- the client has been initialized. function lsp.start_client(config) local cleaned_config = validate_client_config(config) @@ -335,10 +443,23 @@ function lsp.start_client(config) local handlers = {} + --@private + --- Returns the callback associated with an LSP method. Returns the default + --- callback if the user hasn't set a custom one. + --- + --@param method (string) LSP method name + --@returns (fn) The callback for the given method, if defined, or the default + ---from |lsp-callbacks| local function resolve_callback(method) return callbacks[method] or default_callbacks[method] end + --@private + --- Handles a notification sent by an LSP server by invoking the + --- corresponding callback. + --- + --@param method (string) LSP method name + --@param params (table) The parameters for that method. function handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) local callback = resolve_callback(method) @@ -348,6 +469,12 @@ function lsp.start_client(config) end end + --@private + --- Handles a request from an LSP server by invoking the corresponding + --- callback. + --- + --@param method (string) LSP method name + --@param params (table) The parameters for that method function handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) local callback = resolve_callback(method) @@ -359,6 +486,13 @@ function lsp.start_client(config) return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound) end + --@private + --- Invoked when the client operation throws an error. + --- + --@param code (number) Error code + --@param err (...) Other arguments may be passed depending on the error kind + --@see |vim.lsp.client_errors| for possible errors. Use + ---`vim.lsp.client_errors[code]` to get a human-friendly name. function handlers.on_error(code, err) local _ = log.error() and log.error(log_prefix, "on_error", { code = lsp.client_errors[code], err = err }) err_message(log_prefix, ': Error ', lsp.client_errors[code], ': ', vim.inspect(err)) @@ -371,6 +505,11 @@ function lsp.start_client(config) end end + --@private + --- Invoked on client exit. + --- + --@param code (number) exit code of the process + --@param signal (number) the signal used to terminate (if any) function handlers.on_exit(code, signal) active_clients[client_id] = nil uninitialized_clients[client_id] = nil @@ -411,6 +550,7 @@ function lsp.start_client(config) -- initialize finishes. uninitialized_clients[client_id] = client; + --@private local function initialize() local valid_traces = { off = 'off'; messages = 'messages'; verbose = 'verbose'; @@ -466,6 +606,15 @@ function lsp.start_client(config) -- These are the cleaned up capabilities we use for dynamically deciding -- when to send certain events to clients. client.resolved_capabilities = protocol.resolve_capabilities(client.server_capabilities) + client.supports_method = function(method) + local required_capability = lsp._request_name_to_capability[method] + -- if we don't know about the method, assume that the client supports it. + if not required_capability then + return true + end + + return client.resolved_capabilities[required_capability] + end if config.on_init then local status, err = pcall(config.on_init, client, result) if not status then @@ -488,43 +637,57 @@ function lsp.start_client(config) end) end - local function unsupported_method(method) - local msg = "server doesn't support "..method - local _ = log.warn() and log.warn(msg) - err_message(msg) - return lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound, msg) - end - - --- Checks capabilities before rpc.request-ing. + --@private + --- Sends a request to the server. + --- + --- This is a thin wrapper around {client.rpc.request} with some additional + --- checks for capabilities and callback availability. + --- + --@param method (string) LSP method name. + --@param params (table) LSP request params. + --@param callback (function, optional) Response handler for this method. + ---If {callback} is not specified, it will use {client.callbacks} to try to + ---find a callback. If one is not found there, then an error will occur. + --@param bufnr (number) Buffer handle (0 for current). + --@returns ({status}, [request_id]): {status} is a bool indicating + ---whether the request was successful. If it is `false`, then it will + ---always be `false` (the client has shutdown). If it was + ---successful, then it will return {request_id} as the + ---second result. You can use this with `client.cancel_request(request_id)` + ---to cancel the-request. + --@see |vim.lsp.buf_request()| function client.request(method, params, callback, bufnr) + -- FIXME: callback is optional, but bufnr is apparently not? Shouldn't that + -- require a `select('#', ...)` call? if not callback then callback = resolve_callback(method) or error(string.format("not found: %q request callback for client %q.", method, client.name)) end local _ = log.debug() and log.debug(log_prefix, "client.request", client_id, method, params, callback, bufnr) - -- TODO keep these checks or just let it go anyway? - if (not client.resolved_capabilities.hover and method == 'textDocument/hover') - or (not client.resolved_capabilities.signature_help and method == 'textDocument/signatureHelp') - or (not client.resolved_capabilities.goto_definition and method == 'textDocument/definition') - or (not client.resolved_capabilities.implementation and method == 'textDocument/implementation') - or (not client.resolved_capabilities.declaration and method == 'textDocument/declaration') - or (not client.resolved_capabilities.type_definition and method == 'textDocument/typeDefinition') - or (not client.resolved_capabilities.document_symbol and method == 'textDocument/documentSymbol') - or (not client.resolved_capabilities.workspace_symbol and method == 'textDocument/workspaceSymbol') - or (not client.resolved_capabilities.call_hierarchy and method == 'textDocument/prepareCallHierarchy') - then - callback(unsupported_method(method), method, nil, client_id, bufnr) - return - end return rpc.request(method, params, function(err, result) callback(err, method, result, client_id, bufnr) end) end + --@private + --- Sends a notification to an LSP server. + --- + --@param method (string) LSP method name. + --@param params (optional, table) LSP request params. + --@param bufnr (number) Buffer handle, or 0 for current. + --@returns {status} (bool) true if the notification was successful. + ---If it is false, then it will always be false + ---(the client has shutdown). function client.notify(...) return rpc.notify(...) end + --@private + --- Cancels a request with a given request id. + --- + --@param id (number) id of request to cancel + --@returns true if any client returns true; false otherwise + --@see |vim.lsp.client.notify()| function client.cancel_request(id) validate{id = {id, 'n'}} return rpc.notify("$/cancelRequest", { id = id }) @@ -533,6 +696,14 @@ function lsp.start_client(config) -- Track this so that we can escalate automatically if we've alredy tried a -- graceful shutdown local tried_graceful_shutdown = false + --@private + --- Stops a client, optionally with force. + --- + ---By default, it will just ask the - server to shutdown without force. If + --- you request to stop a client which has previously been requested to + --- shutdown, it will automatically escalate and force shutdown. + --- + --@param force (bool, optional) function client.stop(force) local handle = rpc.handle if handle:is_closing() then @@ -554,10 +725,18 @@ function lsp.start_client(config) end) end + --@private + --- Checks whether a client is stopped. + --- + --@returns (bool) true if client is stopped or in the process of being + ---stopped; false otherwise function client.is_stopped() return rpc.handle:is_closing() end + --@private + --- Runs the on_attach function from the client's config if it was defined. + --@param bufnr (number) Buffer number function client._on_attach(bufnr) text_document_did_open_handler(bufnr, client) if config.on_attach then @@ -571,6 +750,12 @@ function lsp.start_client(config) return client_id end +--@private +--- Memoizes a function. On first run, the function return value is saved and +--- immediately returned on subsequent runs. +--- +--@param fn (function) Function to run +--@returns (function) Memoized function local function once(fn) local value return function(...) @@ -579,14 +764,21 @@ local function once(fn) end end +--@private +--@fn text_document_did_change_handler(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) +--- Notify all attached clients that a buffer has changed. local text_document_did_change_handler do local encoding_index = { ["utf-8"] = 1; ["utf-16"] = 2; ["utf-32"] = 3; } text_document_did_change_handler = function(_, bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size) - local _ = log.debug() and log.debug("on_lines", bufnr, changedtick, firstline, - lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size, nvim_buf_get_lines(bufnr, firstline, new_lastline, true)) + + local _ = log.debug() and log.debug( + string.format("on_lines bufnr: %s, changedtick: %s, firstline: %s, lastline: %s, new_lastline: %s, old_byte_size: %s, old_utf32_size: %s, old_utf16_size: %s", + bufnr, changedtick, firstline, lastline, new_lastline, old_byte_size, old_utf32_size, old_utf16_size), + nvim_buf_get_lines(bufnr, firstline, new_lastline, true) + ) -- Don't do anything if there are no clients attached. if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then @@ -730,14 +922,14 @@ function lsp.buf_is_attached(bufnr, client_id) return (all_buffer_active_clients[bufnr] or {})[client_id] == true end ---- Gets an active client by id, or nil if the id is invalid or the ---- client is not yet initialized. ---- +--- Gets a client by id, or nil if the id is invalid. +--- The returned client may not yet be fully initialized. +-- --@param client_id client id number --- ---@return |vim.lsp.client| object, or nil +--@returns |vim.lsp.client| object, or nil function lsp.get_client_by_id(client_id) - return active_clients[client_id] + return active_clients[client_id] or uninitialized_clients[client_id] end --- Stops a client(s). @@ -746,7 +938,7 @@ end --- To stop all clients: --- --- <pre> ---- vim.lsp.stop_client(lsp.get_active_clients()) +--- vim.lsp.stop_client(vim.lsp.get_active_clients()) --- </pre> --- --- By default asks the server to shutdown, unless stop was requested @@ -769,7 +961,7 @@ end --- Gets all active clients. --- ---@return Table of |vim.lsp.client| objects +--@returns Table of |vim.lsp.client| objects function lsp.get_active_clients() return vim.tbl_values(active_clients) end @@ -818,16 +1010,32 @@ function lsp.buf_request(bufnr, method, params, callback) callback = { callback, 'f', true }; } local client_request_ids = {} - for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) - local request_success, request_id = client.request(method, params, callback, resolved_bufnr) - -- This could only fail if the client shut down in the time since we looked - -- it up and we did the request, which should be rare. - if request_success then - client_request_ids[client_id] = request_id + local method_supported = false + for_each_buffer_client(bufnr, function(client, client_id, resolved_bufnr) + if client.supports_method(method) then + method_supported = true + local request_success, request_id = client.request(method, params, callback, resolved_bufnr) + + -- This could only fail if the client shut down in the time since we looked + -- it up and we did the request, which should be rare. + if request_success then + client_request_ids[client_id] = request_id + end end end) + -- if no clients support the given method, call the callback with the proper + -- error message. + if not method_supported then + local unsupported_err = lsp._unsupported_method(method) + local cb = callback or lsp.callbacks[method] + if cb then + cb(unsupported_err, method, bufnr) + end + return + end + local function _cancel_all_requests() for client_id, request_id in pairs(client_request_ids) do local client = active_clients[client_id] @@ -904,7 +1112,7 @@ end --@param findstart 0 or 1, decides behavior --@param base If findstart=0, text to match against --- ---@return (number) Decided by `findstart`: +--@returns (number) Decided by `findstart`: --- - findstart=0: column where the completion starts, or -2 or -3 --- - findstart=1: list of matches (actually just calls |complete()|) function lsp.omnifunc(findstart, base) @@ -948,6 +1156,10 @@ function lsp.omnifunc(findstart, base) return -2 end +---Checks whether a client is stopped. +--- +--@param client_id (Number) +--@returns true if client is stopped, false otherwise. function lsp.client_is_stopped(client_id) return active_clients[client_id] == nil end @@ -992,12 +1204,17 @@ function lsp.set_log_level(level) end --- Gets the path of the logfile used by the LSP client. +--@returns (String) Path to logfile. function lsp.get_log_path() return log.get_filename() end --- Define the LspDiagnostics signs if they're not defined already. +-- Defines the LspDiagnostics signs if they're not defined already. do + --@private + --- Defines a sign if it isn't already defined. + --@param name (String) Name of the sign + --@param properties (table) Properties to attach to the sign local function define_default_sign(name, properties) if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then vim.fn.sign_define(name, properties) diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua index 2e27617997..c015884f5b 100644 --- a/runtime/lua/vim/lsp/buf.lua +++ b/runtime/lua/vim/lsp/buf.lua @@ -1,20 +1,45 @@ local vim = vim local validate = vim.validate -local api = vim.api local vfn = vim.fn local util = require 'vim.lsp.util' -local list_extend = vim.list_extend local M = {} +--@private +--- Returns nil if {status} is false or nil, otherwise returns the rest of the +--- arguments. local function ok_or_nil(status, ...) if not status then return end return ... end + +--@private +--- Swallows errors. +--- +--@param fn Function to run +--@param ... Function arguments +--@returns Result of `fn(...)` if there are no errors, otherwise nil. +--- Returns nil if errors occur during {fn}, otherwise returns local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end +--@private +--- Sends an async request to all active clients attached to the current +--- buffer. +--- +--@param method (string) LSP method name +--@param params (optional, table) Parameters to send to the server +--@param callback (optional, functionnil) Handler +-- `function(err, method, params, client_id)` for this request. Defaults +-- to the client callback in `client.callbacks`. See |lsp-callbacks|. +-- +--@returns 2-tuple: +--- - Map of client-id:request-id pairs for all successful requests. +--- - Function which can be used to cancel all the requests. You could instead +--- iterate all clients and call their `cancel_request()` methods. +--- +--@see |vim.lsp.buf_request()| local function request(method, params, callback) validate { method = {method, 's'}; @@ -23,9 +48,10 @@ local function request(method, params, callback) return vim.lsp.buf_request(0, method, params, callback) end ---- Sends a notification through all clients associated with current buffer. --- ---@return `true` if server responds. +--- Checks whether the language servers attached to the current buffer are +--- ready. +--- +--@returns `true` if server responds. function M.server_ready() return not not vim.lsp.buf_notify(0, "window/progress", {}) end @@ -74,6 +100,12 @@ end --- Retrieves the completion items at the current cursor position. Can only be --- called in Insert mode. +--- +--@param context (context support not yet implemented) Additional information +--- about the context in which a completion was triggered (how it was triggered, +--- and by which trigger character, if applicable) +--- +--@see |vim.lsp.protocol.constants.CompletionTriggerKind| function M.completion(context) local params = util.make_position_params() params.context = context @@ -82,64 +114,59 @@ end --- Formats the current buffer. --- ---- The optional {options} table can be used to specify FormattingOptions, a ---- list of which is available at ---- https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting. +--@param options (optional, table) Can be used to specify FormattingOptions. --- Some unspecified options will be automatically derived from the current --- Neovim options. +-- +--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_formatting function M.formatting(options) local params = util.make_formatting_params(options) return request('textDocument/formatting', params) end ---- Perform |vim.lsp.buf.formatting()| synchronously. +--- Performs |vim.lsp.buf.formatting()| synchronously. --- --- Useful for running on save, to make sure buffer is formatted prior to being ---- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. +--- saved. {timeout_ms} is passed on to |vim.lsp.buf_request_sync()|. Example: +--- +--- <pre> +--- vim.api.nvim_command[[autocmd BufWritePre <buffer> lua vim.lsp.buf.formatting_sync()]] +--- </pre> +--- +--@param options Table with valid `FormattingOptions` entries +--@param timeout_ms (number) Request timeout function M.formatting_sync(options, timeout_ms) local params = util.make_formatting_params(options) local result = vim.lsp.buf_request_sync(0, "textDocument/formatting", params, timeout_ms) if not result then return end result = result[1].result + if not result then return end vim.lsp.util.apply_text_edits(result) end +--- Formats a given range. +--- +--@param options Table with valid `FormattingOptions` entries. +--@param start_pos ({number, number}, optional) mark-indexed position. +---Defaults to the start of the last visual selection. +--@param start_pos ({number, number}, optional) mark-indexed position. +---Defaults to the end of the last visual selection. 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}; - } + validate { options = {options, 't', true} } local sts = vim.bo.softtabstop; options = vim.tbl_extend('keep', options or {}, { tabSize = (sts > 0 and sts) or (sts < 0 and vim.bo.shiftwidth) or vim.bo.tabstop; insertSpaces = vim.bo.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, A[1], A[2])} - end - if B[2] > 0 then - B = {B[1], util.character_offset(0, B[1], B[2])} - 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; - } + local params = util.make_given_range_params(start_pos, end_pos) + params.options = options return request('textDocument/rangeFormatting', params) end ---- Renames all references to the symbol under the cursor. If {new_name} is not ---- provided, the user will be prompted for a new name using |input()|. +--- Renames all references to the symbol under the cursor. +--- +--@param new_name (string) If not provided, the user will be prompted for a new +---name using |input()|. 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. @@ -152,6 +179,8 @@ end --- Lists all the references to the symbol under the cursor in the quickfix window. --- +--@param context (table) Context for the request +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references function M.references(context) validate { context = { context, 't', true } } local params = util.make_position_params() @@ -169,6 +198,7 @@ function M.document_symbol() request('textDocument/documentSymbol', params) end +--@private local function pick_call_hierarchy_item(call_hierarchy_items) if not call_hierarchy_items then return end if #call_hierarchy_items == 1 then @@ -186,6 +216,9 @@ local function pick_call_hierarchy_item(call_hierarchy_items) return choice end +--- Lists all the call sites of the symbol under the cursor in the +--- |quickfix| window. If the symbol can resolve to multiple +--- items, the user can pick one in the |inputlist|. function M.incoming_calls() local params = util.make_position_params() request('textDocument/prepareCallHierarchy', params, function(_, _, result) @@ -194,6 +227,9 @@ function M.incoming_calls() end) end +--- Lists all the items that are called by the symbol under the +--- cursor in the |quickfix| window. If the symbol can resolve to +--- multiple items, the user can pick one in the |inputlist|. function M.outgoing_calls() local params = util.make_position_params() request('textDocument/prepareCallHierarchy', params, function(_, _, result) @@ -204,9 +240,11 @@ end --- Lists all symbols in the current workspace in the quickfix window. --- ---- The list is filtered against the optional argument {query}; ---- if the argument is omitted from the call, the user is prompted to enter a string on the command line. ---- An empty string means no filtering is done. +--- The list is filtered against {query}; if the argument is omitted from the +--- call, the user is prompted to enter a string on the command line. An empty +--- string means no filtering is done. +--- +--@param query (string, optional) function M.workspace_symbol(query) query = query or npcall(vfn.input, "Query: ") local params = {query = query} @@ -227,10 +265,17 @@ function M.document_highlight() request('textDocument/documentHighlight', params) end +--- Removes document highlights from current buffer. +--- function M.clear_references() util.buf_clear_references() end +--- Selects a code action from the input list that is available at the current +--- cursor position. +-- +--@param context: (table, optional) Valid `CodeActionContext` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction function M.code_action(context) validate { context = { context, 't', true } } context = context or { diagnostics = util.get_line_diagnostics() } @@ -239,6 +284,25 @@ function M.code_action(context) request('textDocument/codeAction', params) end +--- Performs |vim.lsp.buf.code_action()| for a given range. +--- +--@param context: (table, optional) Valid `CodeActionContext` object +--@param start_pos ({number, number}, optional) mark-indexed position. +---Defaults to the start of the last visual selection. +--@param end_pos ({number, number}, optional) mark-indexed position. +---Defaults to the end of the last visual selection. +function M.range_code_action(context, start_pos, end_pos) + validate { context = { context, 't', true } } + context = context or { diagnostics = util.get_line_diagnostics() } + local params = util.make_given_range_params(start_pos, end_pos) + params.context = context + request('textDocument/codeAction', params) +end + +--- Executes an LSP server command. +--- +--@param command A valid `ExecuteCommandParams` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand function M.execute_command(command) validate { command = { command.command, 's' }, diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua index 1ed58995d0..3270d1d2a9 100644 --- a/runtime/lua/vim/lsp/callbacks.lua +++ b/runtime/lua/vim/lsp/callbacks.lua @@ -7,17 +7,24 @@ local buf = require 'vim.lsp.buf' local M = {} +-- FIXME: DOC: Expose in vimdocs + +--@private +--- Writes to error buffer. +--@param ... (table of strings) Will be concatenated before being written local function err_message(...) api.nvim_err_writeln(table.concat(vim.tbl_flatten{...})) api.nvim_command("redraw") end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_executeCommand M['workspace/executeCommand'] = function(err, _) if err then error("Could not execute code action: "..err.message) end end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_codeAction M['textDocument/codeAction'] = function(_, _, actions) if actions == nil or vim.tbl_isempty(actions) then print("No code actions available") @@ -51,6 +58,7 @@ M['textDocument/codeAction'] = function(_, _, actions) end end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit M['workspace/applyEdit'] = function(_, _, workspace_edit) if not workspace_edit then return end -- TODO(ashkan) Do something more with label? @@ -64,6 +72,7 @@ M['workspace/applyEdit'] = function(_, _, workspace_edit) } end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_publishDiagnostics M['textDocument/publishDiagnostics'] = function(_, _, result) if not result then return end local uri = result.uri @@ -73,18 +82,6 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) return end - -- Unloaded buffers should not handle diagnostics. - -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. - -- This should trigger another publish of the diagnostics. - -- - -- In particular, this stops a ton of spam when first starting a server for current - -- unloaded buffers. - if not api.nvim_buf_is_loaded(bufnr) then - return - end - - util.buf_clear_diagnostics(bufnr) - -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic -- The diagnostic's severity. Can be omitted. If omitted it is up to the -- client to interpret diagnostics as error, warning, info or hint. @@ -95,13 +92,30 @@ M['textDocument/publishDiagnostics'] = function(_, _, result) end end + util.buf_clear_diagnostics(bufnr) + + -- Always save the diagnostics, even if the buf is not loaded. + -- Language servers may report compile or build errors via diagnostics + -- Users should be able to find these, even if they're in files which + -- are not loaded. util.buf_diagnostics_save_positions(bufnr, result.diagnostics) + + -- Unloaded buffers should not handle diagnostics. + -- When the buffer is loaded, we'll call on_attach, which sends textDocument/didOpen. + -- This should trigger another publish of the diagnostics. + -- + -- In particular, this stops a ton of spam when first starting a server for current + -- unloaded buffers. + if not api.nvim_buf_is_loaded(bufnr) then + return + end util.buf_diagnostics_underline(bufnr, result.diagnostics) util.buf_diagnostics_virtual_text(bufnr, result.diagnostics) util.buf_diagnostics_signs(bufnr, result.diagnostics) vim.api.nvim_command("doautocmd User LspDiagnosticsChanged") end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_references M['textDocument/references'] = function(_, _, result) if not result then return end util.set_qflist(util.locations_to_items(result)) @@ -109,6 +123,13 @@ M['textDocument/references'] = function(_, _, result) api.nvim_command("wincmd p") end +--@private +--- Prints given list of symbols to the quickfix list. +--@param _ (not used) +--@param _ (not used) +--@param result (list of Symbols) LSP method name +--@param result (table) result of LSP method; a location or a list of locations. +---(`textDocument/definition` can return `Location` or `Location[]` local symbol_callback = function(_, _, result, _, bufnr) if not result or vim.tbl_isempty(result) then return end @@ -116,24 +137,30 @@ local symbol_callback = function(_, _, result, _, bufnr) api.nvim_command("copen") api.nvim_command("wincmd p") end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentSymbol M['textDocument/documentSymbol'] = symbol_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_symbol M['workspace/symbol'] = symbol_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rename M['textDocument/rename'] = function(_, _, result) if not result then return end util.apply_workspace_edit(result) end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_rangeFormatting M['textDocument/rangeFormatting'] = function(_, _, result) if not result then return end util.apply_text_edits(result) end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting M['textDocument/formatting'] = function(_, _, result) if not result then return end util.apply_text_edits(result) end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion M['textDocument/completion'] = function(_, _, result) if vim.tbl_isempty(result or {}) then return end local row, col = unpack(api.nvim_win_get_cursor(0)) @@ -146,6 +173,7 @@ M['textDocument/completion'] = function(_, _, result) vim.fn.complete(textMatch+1, matches) end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover M['textDocument/hover'] = function(_, method, result) util.focusable_float(method, function() if not (result and result.contents) then @@ -166,6 +194,12 @@ M['textDocument/hover'] = function(_, method, result) end) end +--@private +--- Jumps to a location. Used as a callback for multiple LSP methods. +--@param _ (not used) +--@param method (string) LSP method name +--@param result (table) result of LSP method; a location or a list of locations. +---(`textDocument/definition` can return `Location` or `Location[]` 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') @@ -188,35 +222,49 @@ local function location_callback(_, method, result) end end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_declaration M['textDocument/declaration'] = location_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition M['textDocument/definition'] = location_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_typeDefinition M['textDocument/typeDefinition'] = location_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_implementation M['textDocument/implementation'] = location_callback +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp M['textDocument/signatureHelp'] = function(_, method, result) + -- When use `autocmd CompleteDone <silent><buffer> lua vim.lsp.buf.signature_help()` to call signatureHelp callback + -- If the completion item doesn't have signatures It will make noise. Change to use `print` that can use `<silent>` to ignore + if not (result and result.signatures and result.signatures[1]) then + print('No signature help available') + return + end + local lines = util.convert_signature_help_to_markdown_lines(result) + lines = util.trim_empty_lines(lines) + if vim.tbl_isempty(lines) then + print('No signature help available') + return + end util.focusable_preview(method, function() - if not (result and result.signatures and result.signatures[1]) then - return { 'No signature available' } - end - -- TODO show popup when signatures is empty? - local lines = util.convert_signature_help_to_markdown_lines(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 +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_documentHighlight M['textDocument/documentHighlight'] = function(_, _, result, _) if not result then return end local bufnr = api.nvim_get_current_buf() util.buf_highlight_references(bufnr, result) end --- direction is "from" for incoming calls and "to" for outgoing calls +--@private +--- +--- Displays call hierarchy in the quickfix window. +--- +--@param direction `"from"` for incoming calls and `"to"` for outgoing calls +--@returns `CallHierarchyIncomingCall[]` if {direction} is `"from"`, +--@returns `CallHierarchyOutgoingCall[]` if {direction} is `"to"`, local make_call_hierarchy_callback = function(direction) - -- result is a CallHierarchy{Incoming,Outgoing}Call[] return function(_, _, result) if not result then return end local items = {} @@ -237,10 +285,13 @@ local make_call_hierarchy_callback = function(direction) end end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/incomingCalls M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from') +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#callHierarchy/outgoingCalls M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to') +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/logMessage M['window/logMessage'] = function(_, _, result, client_id) local message_type = result.type local message = result.message @@ -261,6 +312,7 @@ M['window/logMessage'] = function(_, _, result, client_id) return result end +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#window/showMessage M['window/showMessage'] = function(_, _, result, client_id) local message_type = result.type local message = result.message diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua index 696ce43a59..587a65cd96 100644 --- a/runtime/lua/vim/lsp/log.lua +++ b/runtime/lua/vim/lsp/log.lua @@ -2,6 +2,9 @@ local log = {} +-- FIXME: DOC +-- Should be exposed in the vim docs. +-- -- Log level dictionary with reverse lookup as well. -- -- Can be used to lookup the number from the name or the name from the number. @@ -21,12 +24,14 @@ local log_date_format = "%FT%H:%M:%S%z" do local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/" + --@private local function path_join(...) return table.concat(vim.tbl_flatten{...}, path_sep) end local logfilename = path_join(vim.fn.stdpath('data'), 'lsp.log') - --- Return the log filename. + --- Returns the log filename. + --@returns (string) log filename function log.get_filename() return logfilename end @@ -36,6 +41,9 @@ do for level, levelnr in pairs(log.levels) do -- Also export the log level on the root object. log[level] = levelnr + -- FIXME: DOC + -- Should be exposed in the vim docs. + -- -- Set the lowercase name as the main use function. -- If called without arguments, it will check whether the log level is -- greater than or equal to this one. When called with arguments, it will @@ -74,6 +82,8 @@ end -- interfere with iterating the levels vim.tbl_add_reverse_lookup(log.levels) +--- Sets the current log level. +--@param level (string or number) One of `vim.lsp.log.levels` function log.set_level(level) if type(level) == 'string' then current_log_level = assert(log.levels[level:upper()], string.format("Invalid log level: %q", level)) @@ -84,8 +94,9 @@ function log.set_level(level) end end --- Return whether the level is sufficient for logging. --- @param level number log level +--- Checks whether the level is sufficient for logging. +--@param level number log level +--@returns (bool) true if would log, false if not function log.should_log(level) return level >= current_log_level end diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua index ef5e08680e..2773f59b45 100644 --- a/runtime/lua/vim/lsp/protocol.lua +++ b/runtime/lua/vim/lsp/protocol.lua @@ -2,6 +2,11 @@ local protocol = {} +--@private +--- Returns {a} if it is not nil, otherwise returns {b}. +--- +--@param a +--@param b local function ifnil(a, b) if a == nil then return b end return a @@ -9,12 +14,14 @@ end --[=[ --- Useful for interfacing with: --- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md +--@private +--- Useful for interfacing with: +--- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md function transform_schema_comments() nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]] nvim.command [[silent! '<,'>s/^\(\s*\) \* \=\(.*\)/\1--\2/]] end +--@private function transform_schema_to_table() transform_schema_comments() nvim.command [[silent! '<,'>s/: \S\+//]] @@ -696,6 +703,10 @@ function protocol.make_client_capabilities() }; hierarchicalDocumentSymbolSupport = true; }; + rename = { + dynamicRegistration = false; + prepareSupport = true; + }; }; workspace = { symbol = { @@ -907,6 +918,7 @@ function protocol.resolve_capabilities(server_capabilities) return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync)) end end + general_properties.completion = server_capabilities.completionProvider ~= nil general_properties.hover = server_capabilities.hoverProvider or false general_properties.goto_definition = server_capabilities.definitionProvider or false general_properties.find_references = server_capabilities.referencesProvider or false @@ -916,6 +928,15 @@ function protocol.resolve_capabilities(server_capabilities) general_properties.document_formatting = server_capabilities.documentFormattingProvider or false general_properties.document_range_formatting = server_capabilities.documentRangeFormattingProvider or false general_properties.call_hierarchy = server_capabilities.callHierarchyProvider or false + general_properties.execute_command = server_capabilities.executeCommandProvider ~= nil + + if server_capabilities.renameProvider == nil then + general_properties.rename = false + elseif type(server_capabilities.renameProvider) == 'boolean' then + general_properties.rename = server_capabilities.renameProvider + else + general_properties.rename = true + end if server_capabilities.codeActionProvider == nil then general_properties.code_action = false diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua index 81c92bfe05..749a51fecc 100644 --- a/runtime/lua/vim/lsp/rpc.lua +++ b/runtime/lua/vim/lsp/rpc.lua @@ -5,6 +5,11 @@ local protocol = require('vim.lsp.protocol') local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap -- TODO replace with a better implementation. +--@private +--- Encodes to JSON. +--- +--@param data (table) Data to encode +--@returns (string) Encoded object local function json_encode(data) local status, result = pcall(vim.fn.json_encode, data) if status then @@ -13,6 +18,11 @@ local function json_encode(data) return nil, result end end +--@private +--- Decodes from JSON. +--- +--@param data (string) Data to decode +--@returns (table) Decoded JSON object local function json_decode(data) local status, result = pcall(vim.fn.json_decode, data) if status then @@ -22,17 +32,26 @@ local function json_decode(data) end end +--@private +--- Checks whether a given path exists and is a directory. +--@param filename (string) path to check +--@returns (bool) local function is_dir(filename) local stat = vim.loop.fs_stat(filename) return stat and stat.type == 'directory' or false end local NIL = vim.NIL +--@private +--- Returns its argument, but converts `vim.NIL` to Lua `nil`. +--@param v (any) Argument +--@returns (any) local function convert_NIL(v) if v == NIL then return nil end return v end +--@private --- Merges current process env with the given env and returns the result as --- a list of "k=v" strings. --- @@ -42,6 +61,8 @@ end --- in: { PRODUCTION="false", PATH="/usr/bin/", PORT=123, HOST="0.0.0.0", } --- out: { "PRODUCTION=false", "PATH=/usr/bin/", "PORT=123", "HOST=0.0.0.0", } --- </pre> +--@param env (table) table of environment variable assignments +--@returns (table) list of `"k=v"` strings local function env_merge(env) if env == nil then return env @@ -56,6 +77,11 @@ local function env_merge(env) return final_env end +--@private +--- Embeds the given string into a table and correctly computes `Content-Length`. +--- +--@param encoded_message (string) +--@returns (table) table containing encoded message and `Content-Length` attribute local function format_message_with_content_length(encoded_message) return table.concat { 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n'; @@ -63,8 +89,11 @@ local function format_message_with_content_length(encoded_message) } end ---- Parse an LSP Message's header --- @param header: The header to parse. +--@private +--- Parses an LSP Message's header +--- +--@param header: The header to parse. +--@returns Parsed headers local function parse_headers(header) if type(header) ~= 'string' then return nil @@ -92,6 +121,8 @@ end -- case insensitive pattern. local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end) +--@private +--- The actual workhorse. local function request_parser_loop() local buffer = '' while true do @@ -138,6 +169,10 @@ local client_errors = vim.tbl_add_reverse_lookup { SERVER_RESULT_CALLBACK_ERROR = 7; } +--- Constructs an error message from an LSP error object. +--- +--@param err (table) The error object +--@returns (string) The formatted error message local function format_rpc_error(err) validate { err = { err, 't' }; @@ -182,23 +217,69 @@ local function rpc_response_error(code, message, data) end local default_handlers = {} +--@private +--- Default handler for notifications sent to an LSP server. +--- +--@param method (string) The invoked LSP method +--@param params (table): Parameters for the invoked LSP method function default_handlers.notification(method, params) local _ = log.debug() and log.debug('notification', method, params) end +--@private +--- Default handler for requests sent to an LSP server. +--- +--@param method (string) The invoked LSP method +--@param params (table): Parameters for the invoked LSP method +--@returns `nil` and `vim.lsp.protocol.ErrorCodes.MethodNotFound`. function default_handlers.server_request(method, params) local _ = log.debug() and log.debug('server_request', method, params) return nil, rpc_response_error(protocol.ErrorCodes.MethodNotFound) end +--@private +--- Default handler for when a client exits. +--- +--@param code (number): Exit code +--@param signal (number): Number describing the signal used to terminate (if +---any) function default_handlers.on_exit(code, signal) - local _ = log.info() and log.info("client exit", { code = code, signal = signal }) + local _ = log.info() and log.info("client_exit", { code = code, signal = signal }) end +--@private +--- Default handler for client errors. +--- +--@param code (number): Error code +--@param err (any): Details about the error +---any) function default_handlers.on_error(code, err) local _ = log.error() and log.error('client_error:', client_errors[code], err) end ---- Create and start an RPC client. --- @param cmd [ -local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_params) +--- Starts an LSP server process and create an LSP RPC client object to +--- interact with it. +--- +--@param cmd (string) Command to start the LSP server. +--@param cmd_args (table) List of additional string arguments to pass to {cmd}. +--@param handlers (table, optional) Handlers for LSP message types. Valid +---handler names are: +--- - `"notification"` +--- - `"server_request"` +--- - `"on_error"` +--- - `"on_exit"` +--@param extra_spawn_params (table, optional) Additional context for the LSP +--- server process. May contain: +--- - {cwd} (string) Working directory for the LSP server process +--- - {env} (table) Additional environment variables for LSP server process +--@returns Client RPC object. +--- +--@returns Methods: +--- - `notify()` |vim.lsp.rpc.notify()| +--- - `request()` |vim.lsp.rpc.request()| +--- +--@returns Members: +--- - {pid} (number) The LSP server's PID. +--- - {handle} A handle for low-level interaction with the LSP server process +--- |vim.loop|. +local function start(cmd, cmd_args, handlers, extra_spawn_params) local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params}) validate { cmd = { cmd, 's' }; @@ -242,6 +323,11 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para local handle, pid do + --@private + --- Callback for |vim.loop.spawn()| Closes all streams and runs the + --- `on_exit` handler. + --@param code (number) Exit code + --@param signal (number) Signal that was used to terminate (if any) local function onexit(code, signal) stdin:close() stdout:close() @@ -265,6 +351,12 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para handle, pid = uv.spawn(cmd, spawn_params, onexit) end + --@private + --- Encodes {payload} into a JSON-RPC message and sends it to the remote + --- process. + --- + --@param payload (table) Converted into a JSON string, see |json_encode()| + --@returns true if the payload could be scheduled, false if the main event-loop is in the process of closing. local function encode_and_send(payload) local _ = log.debug() and log.debug("rpc.send.payload", payload) if handle:is_closing() then return false end @@ -276,8 +368,14 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para return true end - local function send_notification(method, params) - local _ = log.debug() and log.debug("rpc.notify", method, params) + -- FIXME: DOC: Should be placed on the RPC client object returned by + -- `start()` + -- + --- Sends a notification to the LSP server. + --@param method (string) The invoked LSP method + --@param params (table): Parameters for the invoked LSP method + --@returns (bool) `true` if notification could be sent, `false` if not + local function notify(method, params) return encode_and_send { jsonrpc = "2.0"; method = method; @@ -285,6 +383,8 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para } end + --@private + --- sends an error object to the remote LSP process. local function send_response(request_id, err, result) return encode_and_send { id = request_id; @@ -294,7 +394,16 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para } end - local function send_request(method, params, callback) + -- FIXME: DOC: Should be placed on the RPC client object returned by + -- `start()` + -- + --- Sends a request to the LSP server and runs {callback} upon response. + --- + --@param method (string) The invoked LSP method + --@param params (table) Parameters for the invoked LSP method + --@param callback (function) Callback to invoke + --@returns (bool, number) `(true, message_id)` if request could be sent, `false` if not + local function request(method, params, callback) validate { callback = { callback, 'f' }; } @@ -320,11 +429,13 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para end end) + --@private local function on_error(errkind, ...) assert(client_errors[errkind]) -- TODO what to do if this fails? pcall(handlers.on_error, errkind, ...) end + --@private local function pcall_handler(errkind, status, head, ...) if not status then on_error(errkind, head, ...) @@ -332,6 +443,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para end return status, head, ... end + --@private local function try_call(errkind, fn, ...) return pcall_handler(errkind, pcall(fn, ...)) end @@ -340,6 +452,7 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para -- time and log them. This would require storing the timestamp. I could call -- them with an error then, perhaps. + --@private local function handle_body(body) local decoded, err = json_decode(body) if not decoded then @@ -381,10 +494,13 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para decoded.error = convert_NIL(decoded.error) decoded.result = convert_NIL(decoded.result) - -- Do not surface RequestCancelled to users, it is RPC-internal. - if decoded.error - and decoded.error.code == protocol.ErrorCodes.RequestCancelled then - local _ = log.debug() and log.debug("Received cancellation ack", decoded) + -- Do not surface RequestCancelled or ContentModified to users, it is RPC-internal. + if decoded.error then + if decoded.error.code == protocol.ErrorCodes.RequestCancelled then + local _ = log.debug() and log.debug("Received cancellation ack", decoded) + elseif decoded.error.code == protocol.ErrorCodes.ContentModified then + local _ = log.debug() and log.debug("Received content modified ack", decoded) + end local result_id = tonumber(decoded.id) -- Clear any callback since this is cancelled now. -- This is safe to do assuming that these conditions hold: @@ -458,13 +574,13 @@ local function create_and_start_client(cmd, cmd_args, handlers, extra_spawn_para return { pid = pid; handle = handle; - request = send_request; - notify = send_notification; + request = request; + notify = notify } end return { - start = create_and_start_client; + start = start; rpc_response_error = rpc_response_error; format_rpc_error = format_rpc_error; client_errors = client_errors; diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua index 33fca29ecd..775932c7fd 100644 --- a/runtime/lua/vim/lsp/util.lua +++ b/runtime/lua/vim/lsp/util.lua @@ -7,6 +7,7 @@ local highlight = require 'vim.highlight' local M = {} +-- FIXME: DOC: Expose in vimdocs --- Diagnostics received from the server via `textDocument/publishDiagnostics` -- by buffer. -- @@ -33,18 +34,30 @@ local M = {} M.diagnostics_by_buf = {} local split = vim.split +--@private local function split_lines(value) return split(value, '\n', true) end +--@private local function ok_or_nil(status, ...) if not status then return end return ... end +--@private local function npcall(fn, ...) return ok_or_nil(pcall(fn, ...)) end +--- Replaces text in a range with new text. +--- +--- CAUTION: Changes in-place! +--- +--@param lines (table) Original list of strings +--@param A (table) Start position; a 2-tuple of {line, col} numbers +--@param B (table) End position; a 2-tuple of {line, col} numbers +--@param new_lines A list of strings to replace the original +--@returns (table) The modified {lines} object function M.set_lines(lines, A, B, new_lines) -- 0-indexing to 1-indexing local i_0 = A[1] + 1 @@ -78,6 +91,7 @@ function M.set_lines(lines, A, B, new_lines) return lines end +--@private local function sort_by_key(fn) return function(a,b) local ka, kb = fn(a), fn(b) @@ -91,13 +105,15 @@ local function sort_by_key(fn) return false end end +--@private local edit_sort_key = sort_by_key(function(e) return {e.A[1], e.A[2], e.i} end) +--@private --- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position --- Returns a zero-indexed column, since set_lines() does the conversion to --- 1-indexed +--- Returns a zero-indexed column, since set_lines() does the conversion to +--- 1-indexed local function get_line_byte_from_position(bufnr, position) -- LSP's line and characters are 0-indexed -- Vim's line and columns are 1-indexed @@ -114,6 +130,9 @@ local function get_line_byte_from_position(bufnr, position) return col end +--- Applies a list of text edits to a buffer. +--@param text_edits (table) list of `TextEdit` objects +--@param buf_nr (number) Buffer id function M.apply_text_edits(text_edits, bufnr) if not next(text_edits) then return end if not api.nvim_buf_is_loaded(bufnr) then @@ -168,20 +187,30 @@ end -- function M.glob_to_regex(glob) -- end --- textDocument/completion response returns one of CompletionItem[], CompletionList or null. --- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion +--- Can be used to extract the completion items from a +--- `textDocument/completion` request, which may return one of +--- `CompletionItem[]`, `CompletionList` or null. +--@param result (table) The result of a `textDocument/completion` request +--@returns (table) List of completion items +--@see https://microsoft.github.io/language-server-protocol/specification#textDocument_completion function M.extract_completion_items(result) if type(result) == 'table' and result.items then + -- result is a `CompletionList` return result.items elseif result ~= nil then + -- result is `CompletionItem[]` return result else + -- result is `null` return {} end end ---- Apply the TextDocumentEdit response. --- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification +--- Applies a `TextDocumentEdit`, which is a list of changes to a single +-- document. +--- +--@param text_document_edit (table) a `TextDocumentEdit` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentEdit 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) @@ -195,6 +224,13 @@ function M.apply_text_document_edit(text_document_edit) M.apply_text_edits(text_document_edit.edits, bufnr) end +--@private +--- Recursively parses snippets in a completion entry. +--- +--@param input (string) Snippet text to parse for snippets +--@param inner (bool) Whether this function is being called recursively +--@returns 2-tuple of strings: The first is the parsed result, the second is the +---unparsed rest of the input local function parse_snippet_rec(input, inner) local res = "" @@ -248,25 +284,30 @@ local function parse_snippet_rec(input, inner) return res, input end --- Parse completion entries, consuming snippet tokens +--- Parses snippets in a completion entry. +--- +--@param input (string) unparsed snippet +--@returns (string) parsed snippet function M.parse_snippet(input) local res, _ = parse_snippet_rec(input, false) return res end --- Sort by CompletionItem.sortText --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--@private +--- Sorts by CompletionItem.sortText. +--- +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function sort_completion_items(items) - if items[1] and items[1].sortText then - table.sort(items, function(a, b) return a.sortText < b.sortText - end) - end + table.sort(items, function(a, b) + return (a.sortText or a.label) < (b.sortText or b.label) + end) end --- Returns text that should be inserted when selecting completion item. The precedence is as follows: --- textEdit.newText > insertText > label --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--@private +--- Returns text that should be inserted when selecting completion item. The +--- precedence is as follows: textEdit.newText > insertText > label +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion local function get_completion_word(item) if item.textEdit ~= nil and item.textEdit.newText ~= nil then if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then @@ -284,8 +325,10 @@ local function get_completion_word(item) return item.label end --- Some language servers return complementary candidates whose prefixes do not match are also returned. --- So we exclude completion candidates whose prefix does not match. +--@private +--- Some language servers return complementary candidates whose prefixes do not +--- match are also returned. So we exclude completion candidates whose prefix +--- does not match. local function remove_unmatch_completion_items(items, prefix) return vim.tbl_filter(function(item) local word = get_completion_word(item) @@ -293,16 +336,26 @@ local function remove_unmatch_completion_items(items, prefix) end, items) end --- Acording to LSP spec, if the client set "completionItemKind.valueSet", --- the client must handle it properly even if it receives a value outside the specification. --- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion +--- Acording to LSP spec, if the client set `completionItemKind.valueSet`, +--- the client must handle it properly even if it receives a value outside the +--- specification. +--- +--@param completion_item_kind (`vim.lsp.protocol.completionItemKind`) +--@returns (`vim.lsp.protocol.completionItemKind`) +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion function M._get_completion_item_kind_name(completion_item_kind) return protocol.CompletionItemKind[completion_item_kind] or "Unknown" end ---- Getting vim complete-items with incomplete flag. --- @params CompletionItem[], CompletionList or nil (https://microsoft.github.io/language-server-protocol/specification#textDocument_completion) --- @return { matches = complete-items table, incomplete = boolean } +--- Turns the result of a `textDocument/completion` request into vim-compatible +--- |complete-items|. +--- +--@param result The result of a `textDocument/completion` call, e.g. from +---|vim.lsp.buf.completion()|, which may be one of `CompletionItem[]`, +--- `CompletionList` or `null` +--@param prefix (string) the prefix to filter the completion items +--@returns { matches = complete-items table, incomplete = bool } +--@see |complete-items| function M.text_document_completion_list_to_complete_items(result, prefix) local items = M.extract_completion_items(result) if vim.tbl_isempty(items) then @@ -350,7 +403,10 @@ function M.text_document_completion_list_to_complete_items(result, prefix) return matches end --- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification +--- Applies a `WorkspaceEdit`. +--- +--@param workspace_edit (table) `WorkspaceEdit` +-- @see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#workspace_applyEdit function M.apply_workspace_edit(workspace_edit) if workspace_edit.documentChanges then for _, change in ipairs(workspace_edit.documentChanges) do @@ -375,9 +431,15 @@ function M.apply_workspace_edit(workspace_edit) end end ---- Convert any of MarkedString | MarkedString[] | MarkupContent into markdown text lines --- see https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_hover --- Useful for textDocument/hover, textDocument/signatureHelp, and potentially others. +--- Converts any of `MarkedString` | `MarkedString[]` | `MarkupContent` into +--- a list of lines containing valid markdown. Useful to populate the hover +--- window for `textDocument/hover`, for parsing the result of +--- `textDocument/signatureHelp`, and potentially others. +--- +--@param input (`MarkedString` | `MarkedString[]` | `MarkupContent`) +--@param contents (table, optional, default `{}`) List of strings to extend with converted lines +--@returns {contents}, extended with lines of converted markdown. +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_hover function M.convert_input_to_markdown_lines(input, contents) contents = contents or {} -- MarkedString variation 1 @@ -416,8 +478,11 @@ function M.convert_input_to_markdown_lines(input, contents) return contents end ---- Convert SignatureHelp response to markdown lines. --- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp +--- Converts `textDocument/SignatureHelp` response to markdown lines. +--- +--@param signature_help Response of `textDocument/SignatureHelp` +--@returns list of lines of converted markdown. +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_signatureHelp function M.convert_signature_help_to_markdown_lines(signature_help) if not signature_help.signatures then return @@ -427,7 +492,10 @@ function M.convert_signature_help_to_markdown_lines(signature_help) --=== 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 = signature_help.activeSignature or 0 + local active_signature = signature_help.activeSignature + if active_signature == vim.NIL or active_signature == nil then + active_signature = 0 + end -- If the activeSignature is not inside the valid range, then clip it. if active_signature >= #signature_help.signatures then active_signature = 0 @@ -440,13 +508,13 @@ function M.convert_signature_help_to_markdown_lines(signature_help) if signature.documentation then M.convert_input_to_markdown_lines(signature.documentation, contents) end - if signature_help.parameters then + if signature.parameters and #signature.parameters > 0 then local active_parameter = signature_help.activeParameter or 0 -- If the activeParameter is not inside the valid range, then clip it. - if active_parameter >= #signature_help.parameters then + if active_parameter >= #signature.parameters then active_parameter = 0 end - local parameter = signature.parameters and signature.parameters[active_parameter] + local parameter = signature.parameters[active_parameter + 1] if parameter then --[=[ --Represents a parameter of a callable-signature. A parameter can @@ -467,14 +535,21 @@ function M.convert_signature_help_to_markdown_lines(signature_help) } --]=] -- TODO highlight parameter - if parameter.documentation then - M.convert_input_help_to_markdown_lines(parameter.documentation, contents) + if parameter.documentation and parameter.documentation ~= vim.NIL then + M.convert_input_to_markdown_lines(parameter.documentation, contents) end end end return contents end +--- Creates a table with sensible default options for a floating window. The +--- table can be passed to |nvim_open_win()|. +--- +--@param width (number) window width (in character cells) +--@param height (number) window height (in character cells) +--@param opts (table, optional) +--@returns (table) Options function M.make_floating_popup_options(width, height, opts) validate { opts = { opts, 't', true }; @@ -520,6 +595,10 @@ function M.make_floating_popup_options(width, height, opts) } end +--- Jumps to a location. +--- +--@param location (`Location`|`LocationLink`) +--@returns `true` if the jump succeeded function M.jump_to_location(location) -- location may be Location or LocationLink local uri = location.uri or location.targetUri @@ -543,14 +622,14 @@ function M.jump_to_location(location) return true end ---- Preview a location in a floating windows +--- Previews a location in a floating window --- --- behavior depends on type of location: --- - for Location, range is shown (e.g., function definition) --- - for LocationLink, targetRange is shown (e.g., body of function definition) --- ---@param location a single Location or LocationLink ---@return bufnr,winnr buffer and window number of floating window or nil +--@param location a single `Location` or `LocationLink` +--@returns (bufnr,winnr) buffer and window number of floating window or nil function M.preview_location(location) -- location may be LocationLink or Location (more useful for the former) local uri = location.targetUri or location.uri @@ -565,6 +644,7 @@ function M.preview_location(location) return M.open_floating_preview(contents, filetype) end +--@private 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 @@ -573,19 +653,25 @@ local function find_window_by_var(name, value) end end --- Check if a window with `unique_name` tagged is associated with the current --- buffer. If not, make a new preview. --- --- fn()'s return bufnr, winnr --- case that a new floating window should be created. +--- Enters/leaves the focusable window associated with the current buffer via the +--window - variable `unique_name`. If no such window exists, run the function +--{fn}. +--- +--@param unique_name (string) Window variable +--@param fn (function) should return create a new window and return a tuple of +---({focusable_buffer_id}, {window_id}). if {focusable_buffer_id} is a valid +---buffer id, the newly created window will be the new focus associated with +---the current buffer via the tag `unique_name`. +--@returns (pbufnr, pwinnr) if `fn()` has created a new window; nil otherwise function M.focusable_float(unique_name, fn) + -- Go back to previous window if we are in a focusable one if npcall(api.nvim_win_get_var, 0, unique_name) then return api.nvim_command("wincmd p") end local bufnr = api.nvim_get_current_buf() do local win = find_window_by_var(unique_name, bufnr) - if win then + if win and api.nvim_win_is_valid(win) and not vim.fn.pumvisible() then api.nvim_set_current_win(win) api.nvim_command("stopinsert") return @@ -598,18 +684,21 @@ function M.focusable_float(unique_name, fn) end end --- Check if a window with `unique_name` tagged is associated with the current --- buffer. If not, make a new preview. --- --- fn()'s return values will be passed directly to open_floating_preview in the --- case that a new floating window should be created. +--- Focuses/unfocuses the floating preview window associated with the current +--- buffer via the window variable `unique_name`. If no such preview window +--- exists, makes a new one. +--- +--@param unique_name (string) Window variable +--@param fn (function) The return values of this function will be passed +---directly to |vim.lsp.util.open_floating_preview()|, in the case that a new +---floating window should be created function M.focusable_preview(unique_name, fn) return M.focusable_float(unique_name, function() return M.open_floating_preview(fn()) end) end ---- Trim empty lines from input and pad left and right with spaces +--- Trims empty lines from input and pad left and right with spaces --- --@param contents table of lines to trim and pad --@param opts dictionary with optional fields @@ -617,7 +706,7 @@ end -- - pad_right number of columns to pad contents at right (default 1) -- - pad_top number of lines to pad contents at top (default 0) -- - pad_bottom number of lines to pad contents at bottom (default 0) ---@return contents table of trimmed and padded lines +--@returns contents table of trimmed and padded lines function M._trim_and_pad(contents, opts) validate { contents = { contents, 't' }; @@ -645,12 +734,13 @@ end ---- Convert markdown into syntax highlighted regions by stripping the code +-- TODO: refactor to separate stripping/converting and make use of open_floating_preview +-- +--- Converts markdown into syntax highlighted regions by stripping the code --- blocks and converting them into highlighted code. --- This will by default insert a blank line separator after those code block --- regions to improve readability. ---- The result is shown in a floating preview ---- TODO: refactor to separate stripping/converting and make use of open_floating_preview +--- The result is shown in a floating preview. --- --@param contents table of lines to show in window --@param opts dictionary with optional fields @@ -664,7 +754,7 @@ end -- - pad_top number of lines to pad contents at top -- - pad_bottom number of lines to pad contents at bottom -- - separator insert separator after code block ---@return width,height size of float +--@returns width,height size of float function M.fancy_floating_markdown(contents, opts) validate { contents = { contents, 't' }; @@ -713,13 +803,14 @@ function M.fancy_floating_markdown(contents, opts) local width, height = M._make_floating_popup_size(stripped, opts) -- Insert blank line separator after code block - local insert_separator = opts.separator or true + local insert_separator = opts.separator + if insert_separator == nil then insert_separator = true end if insert_separator then for i, h in ipairs(highlights) do h.start = h.start + i - 1 h.finish = h.finish + i - 1 if h.finish + 1 <= #stripped then - table.insert(stripped, h.finish + 1, string.rep("─", width)) + table.insert(stripped, h.finish + 1, string.rep("─", math.min(width, opts.wrap_at or width))) height = height + 1 end end @@ -738,6 +829,7 @@ function M.fancy_floating_markdown(contents, opts) vim.cmd("ownsyntax markdown") local idx = 1 + --@private local function apply_syntax_to_region(ft, start, finish) if ft == '' then return end local name = ft..idx @@ -763,11 +855,17 @@ function M.fancy_floating_markdown(contents, opts) return bufnr, winnr end +--- Creates autocommands to close a preview window when events happen. +--- +--@param events (table) list of events +--@param winnr (number) window id of preview window +--@see |autocmd-events| function M.close_preview_autocmd(events, winnr) api.nvim_command("autocmd "..table.concat(events, ',').." <buffer> ++once lua pcall(vim.api.nvim_win_close, "..winnr..", true)") end ---- Compute size of float needed to show contents (with optional wrapping) +--@internal +--- Computes size of float needed to show contents (with optional wrapping) --- --@param contents table of lines to show in window --@param opts dictionary with optional fields @@ -776,7 +874,7 @@ end -- - wrap_at character to wrap at for computing height -- - max_width maximal width of floating window -- - max_height maximal height of floating window ---@return width,height size of float +--@returns width,height size of float function M._make_floating_popup_size(contents, opts) validate { contents = { contents, 't' }; @@ -827,7 +925,7 @@ function M._make_floating_popup_size(contents, opts) return width, height end ---- Show contents in a floating window +--- Shows contents in a floating window. --- --@param contents table of lines to show in window --@param filetype string of filetype to set for opened buffer @@ -841,7 +939,8 @@ end -- - pad_right number of columns to pad contents at right -- - pad_top number of lines to pad contents at top -- - pad_bottom number of lines to pad contents at bottom ---@return bufnr,winnr buffer and window number of floating window or nil +--@returns bufnr,winnr buffer and window number of the newly created floating +---preview window function M.open_floating_preview(contents, filetype, opts) validate { contents = { contents, 't' }; @@ -912,6 +1011,9 @@ do severity_floating_highlights[severity] = floating_highlight_name end + --- Clears diagnostics for a buffer. + --- + --@param bufnr (number) buffer id function M.buf_clear_diagnostics(bufnr) validate { bufnr = {bufnr, 'n', true} } bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr @@ -923,10 +1025,18 @@ do api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1) end + --- Gets the name of a severity's highlight group. + --- + --@param severity A member of `vim.lsp.protocol.DiagnosticSeverity` + --@returns (string) Highlight group name function M.get_severity_highlight_name(severity) return severity_highlights[severity] end + --- Gets list of diagnostics for the current line. + --- + --@returns (table) list of `Diagnostic` tables + --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic function M.get_line_diagnostics() local bufnr = api.nvim_get_current_buf() local linenr = api.nvim_win_get_cursor(0)[1] - 1 @@ -941,6 +1051,8 @@ do return diagnostics_by_line[linenr] or {} end + --- Displays the diagnostics for the current line in a floating hover + --- window. function M.show_line_diagnostics() -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {}) -- if #marks == 0 then @@ -977,10 +1089,10 @@ do return popup_bufnr, winnr end - --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf + --- Saves diagnostics into vim.lsp.util.diagnostics_by_buf[{bufnr}]. --- - --@param bufnr bufnr for which the diagnostics are for. - --@param diagnostics Diagnostics[] received from the language server. + --@param bufnr (number) buffer id for which the diagnostics are for + --@param diagnostics list of `Diagnostic`s received from the LSP server function M.buf_diagnostics_save_positions(bufnr, diagnostics) validate { bufnr = {bufnr, 'n', true}; @@ -1000,6 +1112,10 @@ do M.diagnostics_by_buf[bufnr] = diagnostics end + --- Highlights a list of diagnostics in a buffer by underlining them. + --- + --@param bufnr (number) buffer id + --@param diagnostics (list of `Diagnostic`s) function M.buf_diagnostics_underline(bufnr, diagnostics) for _, diagnostic in ipairs(diagnostics) do local start = diagnostic.range["start"] @@ -1020,11 +1136,18 @@ do end end + --- Removes document highlights from a buffer. + --- + --@param bufnr buffer id function M.buf_clear_references(bufnr) validate { bufnr = {bufnr, 'n', true} } api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1) end + --- Shows a list of document highlights for a certain buffer. + --- + --@param bufnr buffer id + --@param references List of `DocumentHighlight` objects to highlight function M.buf_highlight_references(bufnr, references) validate { bufnr = {bufnr, 'n', true} } for _, reference in ipairs(references) do @@ -1040,11 +1163,19 @@ do end end + --- Groups a list of diagnostics by line. + --- + --@param diagnostics (table) list of `Diagnostic`s + --@returns (table) dictionary mapping lines to lists of diagnostics valid on + ---those lines + --@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#diagnostic function M.diagnostics_group_by_line(diagnostics) if not diagnostics then return end local diagnostics_by_line = {} for _, diagnostic in ipairs(diagnostics) do local start = diagnostic.range.start + -- TODO: Are diagnostics only valid for a single line? I don't understand + -- why this would be okay otherwise local line_diagnostics = diagnostics_by_line[start.line] if not line_diagnostics then line_diagnostics = {} @@ -1055,6 +1186,11 @@ do return diagnostics_by_line end + --- Given a list of diagnostics, sets the corresponding virtual text for a + --- buffer. + --- + --@param bufnr buffer id + --@param diagnostics (table) list of `Diagnostic`s function M.buf_diagnostics_virtual_text(bufnr, diagnostics) if not diagnostics then return @@ -1093,8 +1229,7 @@ do --- </pre> --- --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity| - --- - --@return Count of diagnostics + --@returns Count of diagnostics function M.buf_diagnostics_count(kind) local bufnr = vim.api.nvim_get_current_buf() local diagnostics = M.diagnostics_by_buf[bufnr] @@ -1115,7 +1250,7 @@ do [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign"; } - --- Place signs for each diagnostic in the sign column. + --- Places signs for each diagnostic in the sign column. --- --- Sign characters can be customized with the following commands: --- @@ -1136,8 +1271,11 @@ local position_sort = sort_by_key(function(v) return {v.start.line, v.start.character} end) --- Returns the items with the byte position calculated correctly and in sorted --- order. +--- Returns the items with the byte position calculated correctly and in sorted +--- order, for display in quickfix and location lists. +--- +--@param locations (table) list of `Location`s or `LocationLink`s +--@returns (table) list of items function M.locations_to_items(locations) local items = {} local grouped = setmetatable({}, { @@ -1180,6 +1318,10 @@ function M.locations_to_items(locations) return items end +--- Fills current window's location list with given list of items. +--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. +--- +--@param items (table) list of items function M.set_loclist(items) vim.fn.setloclist(0, {}, ' ', { title = 'Language Server'; @@ -1187,6 +1329,10 @@ function M.set_loclist(items) }) end +--- Fills quickfix list with given list of items. +--- Can be obtained with e.g. |vim.lsp.util.locations_to_items()|. +--- +--@param items (table) list of items function M.set_qflist(items) vim.fn.setqflist({}, ' ', { title = 'Language Server'; @@ -1201,10 +1347,11 @@ function M._get_symbol_kind_name(symbol_kind) return protocol.SymbolKind[symbol_kind] or "Unknown" end ---- Convert symbols to quickfix list items +--- Converts symbols to quickfix list items. --- --@param symbols DocumentSymbol[] or SymbolInformation[] function M.symbols_to_items(symbols, bufnr) + --@private local function _symbols_to_items(_symbols, _items, _bufnr) for _, symbol in ipairs(_symbols) do if symbol.location then -- SymbolInformation type @@ -1239,7 +1386,9 @@ function M.symbols_to_items(symbols, bufnr) return _symbols_to_items(symbols, {}, bufnr) end --- Remove empty lines from the beginning and end. +--- Removes empty lines from the beginning and end. +--@param lines (table) list of lines to trim +--@returns (table) trimmed list of lines function M.trim_empty_lines(lines) local start = 1 for i = 1, #lines do @@ -1258,11 +1407,13 @@ function M.trim_empty_lines(lines) 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. +--- Accepts markdown lines and tries to reduce them to a filetype if they +--- comprise just a single code block. +--- +--- CAUTION: Modifies the input in-place! +--- +--@param lines (table) list of lines +--@returns (string) 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 @@ -1285,14 +1436,22 @@ function M.try_trim_markdown_code_blocks(lines) end local str_utfindex = vim.str_utfindex +--@private local function make_position_param() 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] + if not line then + return { line = 0; character = 0; } + end col = str_utfindex(line, col) return { line = row; character = col; } end +--- Creates a `TextDocumentPositionParams` object for the current buffer and cursor position. +--- +--@returns `TextDocumentPositionParams` object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentPositionParams function M.make_position_params() return { textDocument = M.make_text_document_params(); @@ -1300,19 +1459,65 @@ function M.make_position_params() } end +--- Using the current position in the current buffer, creates an object that +--- can be used as a building block for several LSP requests, such as +--- `textDocument/codeAction`, `textDocument/colorPresentation`, +--- `textDocument/rangeFormatting`. +--- +--@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---`current_position`, end = `current_position` } } function M.make_range_params() local position = make_position_param() return { - textDocument = { uri = vim.uri_from_bufnr(0) }, + textDocument = M.make_text_document_params(), range = { start = position; ["end"] = position; } } end +--- Using the given range in the current buffer, creates an object that +--- is similar to |vim.lsp.util.make_range_params()|. +--- +--@param start_pos ({number, number}, optional) mark-indexed position. +---Defaults to the start of the last visual selection. +--@param end_pos ({number, number}, optional) mark-indexed position. +---Defaults to the end of the last visual selection. +--@returns { textDocument = { uri = `current_file_uri` }, range = { start = +---`start_position`, end = `end_position` } } +function M.make_given_range_params(start_pos, end_pos) + validate { + start_pos = {start_pos, 't', true}; + end_pos = {end_pos, 't', true}; + } + 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], M.character_offset(0, A[1], A[2])} + end + if B[2] > 0 then + B = {B[1], M.character_offset(0, B[1], B[2])} + end + return { + textDocument = M.make_text_document_params(), + range = { + start = {line = A[1], character = A[2]}, + ['end'] = {line = B[1], character = B[2]} + } + } +end + +--- Creates a `TextDocumentIdentifier` object for the current buffer. +--- +--@returns `TextDocumentIdentifier` +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocumentIdentifier function M.make_text_document_params() return { uri = vim.uri_from_bufnr(0) } end ---- Get visual width of tabstop. +--- Returns visual width of tabstop. --- --@see |softtabstop| --@param bufnr (optional, number): Buffer handle, defaults to current @@ -1324,6 +1529,11 @@ function M.get_effective_tabstop(bufnr) return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop end +--- Creates a `FormattingOptions` object for the current buffer and cursor position. +--- +--@param options Table with valid `FormattingOptions` entries +--@returns `FormattingOptions object +--@see https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_formatting function M.make_formatting_params(options) validate { options = {options, 't', true} } options = vim.tbl_extend('keep', options or {}, { @@ -1336,9 +1546,12 @@ function M.make_formatting_params(options) } end --- @param buf buffer handle or 0 for current. --- @param row 0-indexed line --- @param col 0-indexed byte offset in line +--- Returns the UTF-32 and UTF-16 offsets for a position in a certain buffer. +--- +--@param buf buffer id (0 for current) +--@param row 0-indexed line +--@param col 0-indexed byte offset in line +--@returns (number, number) UTF-32 and UTF-16 index of the character in line {row} column {col} in buffer {buf} 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. diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua index 6e427665f2..995c52e8ed 100644 --- a/runtime/lua/vim/shared.lua +++ b/runtime/lua/vim/shared.lua @@ -190,10 +190,10 @@ function vim.tbl_contains(t, value) return false end --- Returns true if the table is empty, and contains no indexed or keyed values. --- ---@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua --- +--- Checks if a table is empty. +--- +--@see https://github.com/premake/premake-core/blob/master/src/base/table.lua +--- --@param t Table to check function vim.tbl_isempty(t) assert(type(t) == 'table', string.format("Expected table, got %s", type(t))) @@ -347,13 +347,11 @@ function vim.tbl_flatten(t) return result end ---- Determine whether a Lua table can be treated as an array. +--- Tests if a Lua table can be treated as an array. --- ---- An empty table `{}` will default to being treated as an array. ---- Use `vim.emtpy_dict()` to create a table treated as an ---- empty dict. Empty tables returned by `rpcrequest()` and ---- `vim.fn` functions can be checked using this function ---- whether they represent empty API arrays and vimL lists. +--- Empty table `{}` is assumed to be an array, unless it was created by +--- |vim.empty_dict()| or returned as a dict-like |API| or Vimscript result, +--- for example from |rpcrequest()| or |vim.fn|. --- --@param t Table --@returns `true` if array-like table, else `false`. @@ -479,48 +477,77 @@ end --- 2. (arg_value, fn, msg) --- - arg_value: argument value --- - fn: any function accepting one argument, returns true if and ---- only if the argument is valid +--- only if the argument is valid. Can optionally return an additional +--- informative error message as the second returned value. --- - msg: (optional) error string if validation fails function vim.validate(opt) end -- luacheck: no unused -vim.validate = (function() + +do local type_names = { - t='table', s='string', n='number', b='boolean', f='function', c='callable', - ['table']='table', ['string']='string', ['number']='number', - ['boolean']='boolean', ['function']='function', ['callable']='callable', - ['nil']='nil', ['thread']='thread', ['userdata']='userdata', + ['table'] = 'table', t = 'table', + ['string'] = 'string', s = 'string', + ['number'] = 'number', n = 'number', + ['boolean'] = 'boolean', b = 'boolean', + ['function'] = 'function', f = 'function', + ['callable'] = 'callable', c = 'callable', + ['nil'] = 'nil', + ['thread'] = 'thread', + ['userdata'] = 'userdata', } - local function _type_name(t) - local tname = type_names[t] - if tname == nil then - error(string.format('invalid type name: %s', tostring(t))) - end - return tname - end + local function _is_type(val, t) return t == 'callable' and vim.is_callable(val) or type(val) == t end - return function(opt) - assert(type(opt) == 'table', string.format('opt: expected table, got %s', type(opt))) + local function is_valid(opt) + if type(opt) ~= 'table' then + return false, string.format('opt: expected table, got %s', type(opt)) + end + for param_name, spec in pairs(opt) do - assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec))) + if type(spec) ~= 'table' then + return false, string.format('opt[%s]: expected table, got %s', param_name, type(spec)) + end local val = spec[1] -- Argument value. local t = spec[2] -- Type name, or callable. local optional = (true == spec[3]) - if not vim.is_callable(t) then -- Check type name. - if (not optional or val ~= nil) and not _is_type(val, _type_name(t)) then - error(string.format("%s: expected %s, got %s", param_name, _type_name(t), type(val))) + if type(t) == 'string' then + local t_name = type_names[t] + if not t_name then + return false, string.format('invalid type name: %s', t) + end + + if (not optional or val ~= nil) and not _is_type(val, t_name) then + return false, string.format("%s: expected %s, got %s", param_name, t_name, type(val)) end - elseif not t(val) then -- Check user-provided validation function. - error(string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val)) + elseif vim.is_callable(t) then + -- Check user-provided validation function. + local valid, optional_message = t(val) + if not valid then + local error_message = string.format("%s: expected %s, got %s", param_name, (spec[3] or '?'), val) + if optional_message ~= nil then + error_message = error_message .. string.format(". Info: %s", optional_message) + end + + return false, error_message + end + else + return false, string.format("invalid type name: %s", tostring(t)) end end - return true + + return true, nil end -end)() + function vim.validate(opt) + local ok, err_msg = is_valid(opt) + if not ok then + error(debug.traceback(err_msg, 2), 2) + end + end +end --- Returns true if object `f` can be called as a function. --- --@param f Any object diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua index 927456708c..0de3388356 100644 --- a/runtime/lua/vim/treesitter.lua +++ b/runtime/lua/vim/treesitter.lua @@ -1,4 +1,6 @@ local a = vim.api +local query = require'vim.treesitter.query' +local language = require'vim.treesitter.language' -- TODO(bfredl): currently we retain parsers for the lifetime of the buffer. -- Consider use weak references to release parser if all plugins are done with @@ -8,12 +10,20 @@ local parsers = {} local Parser = {} Parser.__index = Parser +--- Parses the buffer if needed and returns a tree. +-- +-- Calling this will call the on_changedtree callbacks if the tree has changed. +-- +-- @returns An up to date tree +-- @returns If the tree changed with this call, the changed ranges function Parser:parse() if self.valid then return self.tree end local changes - self.tree, changes = self._parser:parse_buf(self.bufnr) + + self.tree, changes = self._parser:parse(self:input_source()) + self.valid = true if not vim.tbl_isempty(changes) then @@ -25,61 +35,86 @@ function Parser:parse() return self.tree, changes end -function Parser:_on_lines(bufnr, changed_tick, start_row, old_stop_row, stop_row, old_byte_size) - local start_byte = a.nvim_buf_get_offset(bufnr,start_row) - local stop_byte = a.nvim_buf_get_offset(bufnr,stop_row) - local old_stop_byte = start_byte + old_byte_size - self._parser:edit(start_byte,old_stop_byte,stop_byte, - start_row,0,old_stop_row,0,stop_row,0) +function Parser:input_source() + return self.bufnr or self.str +end + +function Parser:_on_bytes(bufnr, changed_tick, + start_row, start_col, start_byte, + old_row, old_col, old_byte, + new_row, new_col, new_byte) + local old_end_col = old_col + ((old_row == 0) and start_col or 0) + local new_end_col = new_col + ((new_row == 0) and start_col or 0) + self._parser:edit(start_byte,start_byte+old_byte,start_byte+new_byte, + start_row, start_col, + start_row+old_row, old_end_col, + start_row+new_row, new_end_col) self.valid = false - for _, cb in ipairs(self.lines_cbs) do - cb(bufnr, changed_tick, start_row, old_stop_row, stop_row, old_byte_size) + for _, cb in ipairs(self.bytes_cbs) do + cb(bufnr, changed_tick, + start_row, start_col, start_byte, + old_row, old_col, old_byte, + new_row, new_col, new_byte) end end +--- Registers callbacks for the parser +-- @param cbs An `nvim_buf_attach`-like table argument with the following keys : +-- `on_bytes` : see `nvim_buf_attach`, but this will be called _after_ the parsers callback. +-- `on_changedtree` : a callback that will be called everytime the tree has syntactical changes. +-- it will only be passed one argument, that is a table of the ranges (as node ranges) that +-- changed. +function Parser:register_cbs(cbs) + if not cbs then return end + + if cbs.on_changedtree then + table.insert(self.changedtree_cbs, cbs.on_changedtree) + end + + if cbs.on_bytes then + table.insert(self.bytes_cbs, cbs.on_bytes) + end +end + +--- Sets the included ranges for the current parser +-- +-- @param ranges A table of nodes that will be used as the ranges the parser should include. function Parser:set_included_ranges(ranges) self._parser:set_included_ranges(ranges) -- The buffer will need to be parsed again later self.valid = false end -local M = { - parse_query = vim._ts_parse_query, -} +--- Gets the included ranges for the parsers +function Parser:included_ranges() + return self._parser:included_ranges() +end + +local M = vim.tbl_extend("error", query, language) setmetatable(M, { __index = function (t, k) if k == "TSHighlighter" then - t[k] = require'vim.tshighlighter' + a.nvim_err_writeln("vim.TSHighlighter is deprecated, please use vim.treesitter.highlighter") + t[k] = require'vim.treesitter.highlighter' + return t[k] + elseif k == "highlighter" then + t[k] = require'vim.treesitter.highlighter' return t[k] end end }) -function M.require_language(lang, path) - if vim._ts_has_language(lang) then - return true - end - if path == nil then - local fname = 'parser/' .. lang .. '.*' - local paths = a.nvim_get_runtime_file(fname, false) - if #paths == 0 then - -- TODO(bfredl): help tag? - error("no parser for '"..lang.."' language") - end - path = paths[1] - end - vim._ts_add_language(path, lang) -end - -function M.inspect_language(lang) - M.require_language(lang) - return vim._ts_inspect_language(lang) -end - -function M.create_parser(bufnr, lang, id) - M.require_language(lang) +--- Creates a new parser. +-- +-- It is not recommended to use this, use vim.treesitter.get_parser() instead. +-- +-- @param bufnr The buffer the parser will be tied to +-- @param lang The language of the parser. +-- @param id The id the parser will have +function M._create_parser(bufnr, lang, id) + language.require_language(lang) if bufnr == 0 then bufnr = a.nvim_get_current_buf() end @@ -89,12 +124,12 @@ function M.create_parser(bufnr, lang, id) local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser) self._parser = vim._create_ts_parser(lang) self.changedtree_cbs = {} - self.lines_cbs = {} + self.bytes_cbs = {} self:parse() -- TODO(bfredl): use weakref to self, so that the parser is free'd is no plugin is -- using it. - local function lines_cb(_, ...) - return self:_on_lines(...) + local function bytes_cb(_, ...) + return self:_on_bytes(...) end local detach_cb = nil if id ~= nil then @@ -104,157 +139,50 @@ function M.create_parser(bufnr, lang, id) end end end - a.nvim_buf_attach(self.bufnr, false, {on_lines=lines_cb, on_detach=detach_cb}) + a.nvim_buf_attach(self.bufnr, false, {on_bytes=bytes_cb, on_detach=detach_cb}) return self end -function M.get_parser(bufnr, ft, buf_attach_cbs) +--- Gets the parser for this bufnr / ft combination. +-- +-- If needed this will create the parser. +-- Unconditionnally attach the provided callback +-- +-- @param bufnr The buffer the parser should be tied to +-- @param ft The filetype of this parser +-- @param buf_attach_cbs See Parser:register_cbs +-- +-- @returns The parser +function M.get_parser(bufnr, lang, buf_attach_cbs) if bufnr == nil or bufnr == 0 then bufnr = a.nvim_get_current_buf() end - if ft == nil then - ft = a.nvim_buf_get_option(bufnr, "filetype") + if lang == nil then + lang = a.nvim_buf_get_option(bufnr, "filetype") end - local id = tostring(bufnr)..'_'..ft + local id = tostring(bufnr)..'_'..lang if parsers[id] == nil then - parsers[id] = M.create_parser(bufnr, ft, id) - end - - if buf_attach_cbs and buf_attach_cbs.on_changedtree then - table.insert(parsers[id].changedtree_cbs, buf_attach_cbs.on_changedtree) + parsers[id] = M._create_parser(bufnr, lang, id) end - if buf_attach_cbs and buf_attach_cbs.on_lines then - table.insert(parsers[id].lines_cbs, buf_attach_cbs.on_lines) - end + parsers[id]:register_cbs(buf_attach_cbs) return parsers[id] end --- query: pattern matching on trees --- predicate matching is implemented in lua -local Query = {} -Query.__index = Query +function M.get_string_parser(str, lang) + vim.validate { + str = { str, 'string' }, + lang = { lang, 'string' } + } + language.require_language(lang) -local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} -local function check_magic(str) - if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then - return str - end - return '\\v'..str -end + local self = setmetatable({str=str, lang=lang, valid=false}, Parser) + self._parser = vim._create_ts_parser(lang) + self:parse() -function M.parse_query(lang, query) - M.require_language(lang) - local self = setmetatable({}, Query) - self.query = vim._ts_parse_query(lang, vim.fn.escape(query,'\\')) - self.info = self.query:inspect() - self.captures = self.info.captures - self.regexes = {} - for id,preds in pairs(self.info.patterns) do - local regexes = {} - for i, pred in ipairs(preds) do - if (pred[1] == "match?" and type(pred[2]) == "number" - and type(pred[3]) == "string") then - regexes[i] = vim.regex(check_magic(pred[3])) - end - end - if next(regexes) then - self.regexes[id] = regexes - end - end return self end -local function get_node_text(node, bufnr) - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return nil - end - local line = a.nvim_buf_get_lines(bufnr, start_row, start_row+1, true)[1] - return string.sub(line, start_col+1, end_col) -end - -function Query:match_preds(match, pattern, bufnr) - local preds = self.info.patterns[pattern] - if not preds then - return true - end - local regexes = self.regexes[pattern] - for i, pred in pairs(preds) do - -- Here we only want to return if a predicate DOES NOT match, and - -- continue on the other case. This way unknown predicates will not be considered, - -- which allows some testing and easier user extensibility (#12173). - -- Also, tree-sitter strips the leading # from predicates for us. - if pred[1] == "eq?" then - local node = match[pred[2]] - local node_text = get_node_text(node, bufnr) - - local str - if type(pred[3]) == "string" then - -- (#eq? @aa "foo") - str = pred[3] - else - -- (#eq? @aa @bb) - str = get_node_text(match[pred[3]], bufnr) - end - - if node_text ~= str or str == nil then - return false - end - elseif pred[1] == "match?" then - if not regexes or not regexes[i] then - return false - end - local node = match[pred[2]] - local start_row, start_col, end_row, end_col = node:range() - if start_row ~= end_row then - return false - end - if not regexes[i]:match_line(bufnr, start_row, start_col, end_col) then - return false - end - end - end - return true -end - -function Query:iter_captures(node, bufnr, start, stop) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - local raw_iter = node:_rawquery(self.query,true,start,stop) - local function iter() - local capture, captured_node, match = raw_iter() - if match ~= nil then - local active = self:match_preds(match, match.pattern, bufnr) - match.active = active - if not active then - return iter() -- tail call: try next match - end - end - return capture, captured_node - end - return iter -end - -function Query:iter_matches(node, bufnr, start, stop) - if bufnr == 0 then - bufnr = vim.api.nvim_get_current_buf() - end - local raw_iter = node:_rawquery(self.query,false,start,stop) - local function iter() - local pattern, match = raw_iter() - if match ~= nil then - local active = self:match_preds(match, pattern, bufnr) - if not active then - return iter() -- tail call: try next match - end - end - return pattern, match - end - return iter -end - return M diff --git a/runtime/lua/vim/treesitter/highlighter.lua b/runtime/lua/vim/treesitter/highlighter.lua new file mode 100644 index 0000000000..decde08019 --- /dev/null +++ b/runtime/lua/vim/treesitter/highlighter.lua @@ -0,0 +1,210 @@ +local a = vim.api + +-- support reload for quick experimentation +local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} +TSHighlighter.__index = TSHighlighter + +TSHighlighter.active = TSHighlighter.active or {} + +local ns = a.nvim_create_namespace("treesitter/highlighter") + +-- These are conventions defined by nvim-treesitter, though it +-- needs to be user extensible also. +TSHighlighter.hl_map = { + ["error"] = "Error", + +-- Miscs + ["comment"] = "Comment", + ["punctuation.delimiter"] = "Delimiter", + ["punctuation.bracket"] = "Delimiter", + ["punctuation.special"] = "Delimiter", + +-- Constants + ["constant"] = "Constant", + ["constant.builtin"] = "Special", + ["constant.macro"] = "Define", + ["string"] = "String", + ["string.regex"] = "String", + ["string.escape"] = "SpecialChar", + ["character"] = "Character", + ["number"] = "Number", + ["boolean"] = "Boolean", + ["float"] = "Float", + +-- Functions + ["function"] = "Function", + ["function.special"] = "Function", + ["function.builtin"] = "Special", + ["function.macro"] = "Macro", + ["parameter"] = "Identifier", + ["method"] = "Function", + ["field"] = "Identifier", + ["property"] = "Identifier", + ["constructor"] = "Special", + +-- Keywords + ["conditional"] = "Conditional", + ["repeat"] = "Repeat", + ["label"] = "Label", + ["operator"] = "Operator", + ["keyword"] = "Keyword", + ["exception"] = "Exception", + + ["type"] = "Type", + ["type.builtin"] = "Type", + ["structure"] = "Structure", + ["include"] = "Include", +} + +function TSHighlighter.new(parser, query) + local self = setmetatable({}, TSHighlighter) + + self.parser = parser + parser:register_cbs { + on_changedtree = function(...) self:on_changedtree(...) end + } + + self:set_query(query) + self.edit_count = 0 + self.redraw_count = 0 + self.line_count = {} + self.root = self.parser:parse():root() + a.nvim_buf_set_option(self.buf, "syntax", "") + + -- TODO(bfredl): can has multiple highlighters per buffer???? + if not TSHighlighter.active[parser.bufnr] then + TSHighlighter.active[parser.bufnr] = {} + end + + TSHighlighter.active[parser.bufnr][parser.lang] = self + + -- Tricky: if syntax hasn't been enabled, we need to reload color scheme + -- but use synload.vim rather than syntax.vim to not enable + -- syntax FileType autocmds. Later on we should integrate with the + -- `:syntax` and `set syntax=...` machinery properly. + if vim.g.syntax_on ~= 1 then + vim.api.nvim_command("runtime! syntax/synload.vim") + end + return self +end + +local function is_highlight_name(capture_name) + local firstc = string.sub(capture_name, 1, 1) + return firstc ~= string.lower(firstc) +end + +function TSHighlighter:get_hl_from_capture(capture) + + local name = self.query.captures[capture] + + if is_highlight_name(name) then + -- From "Normal.left" only keep "Normal" + return vim.split(name, '.', true)[1] + else + -- Default to false to avoid recomputing + local hl = TSHighlighter.hl_map[name] + return hl and a.nvim_get_hl_id_by_name(hl) or 0 + end +end + +function TSHighlighter:on_changedtree(changes) + for _, ch in ipairs(changes or {}) do + a.nvim__buf_redraw_range(self.buf, ch[1], ch[3]+1) + end +end + +function TSHighlighter:set_query(query) + if type(query) == "string" then + query = vim.treesitter.parse_query(self.parser.lang, query) + end + + self.query = query + + self.hl_cache = setmetatable({}, { + __index = function(table, capture) + local hl = self:get_hl_from_capture(capture) + rawset(table, capture, hl) + + return hl + end + }) + + a.nvim__buf_redraw_range(self.parser.bufnr, 0, a.nvim_buf_line_count(self.parser.bufnr)) +end + +local function iter_active_tshl(buf, fn) + for _, hl in pairs(TSHighlighter.active[buf] or {}) do + fn(hl) + end +end + +local function on_line_impl(self, buf, line) + if self.root == nil then + return -- parser bought the farm already + end + + if self.iter == nil then + self.iter = self.query:iter_captures(self.root,buf,line,self.botline) + end + while line >= self.nextrow do + local capture, node = self.iter() + if capture == nil then + break + end + local start_row, start_col, end_row, end_col = node:range() + local hl = self.hl_cache[capture] + if hl and end_row >= line then + a.nvim_buf_set_extmark(buf, ns, start_row, start_col, + { end_line = end_row, end_col = end_col, + hl_group = hl, + ephemeral = true + }) + end + if start_row > line then + self.nextrow = start_row + end + end +end + +function TSHighlighter._on_line(_, _win, buf, line, highlighter) + -- on_line is only called when this is non-nil + if highlighter then + on_line_impl(highlighter, buf, line) + else + iter_active_tshl(buf, function(self) + on_line_impl(self, buf, line) + end) + end +end + +function TSHighlighter._on_buf(_, buf) + iter_active_tshl(buf, function(self) + if self then + local tree = self.parser:parse() + self.root = (tree and tree:root()) or nil + end + end) +end + +function TSHighlighter._on_win(_, _win, buf, _topline, botline) + iter_active_tshl(buf, function(self) + if not self then + return false + end + + self.iter = nil + self.nextrow = 0 + self.botline = botline + self.redraw_count = self.redraw_count + 1 + return true + end) + return true +end + +a.nvim_set_decoration_provider(ns, { + on_buf = TSHighlighter._on_buf; + on_win = TSHighlighter._on_win; + on_line = TSHighlighter._on_line; +}) + +return TSHighlighter diff --git a/runtime/lua/vim/treesitter/language.lua b/runtime/lua/vim/treesitter/language.lua new file mode 100644 index 0000000000..a7e36a0b89 --- /dev/null +++ b/runtime/lua/vim/treesitter/language.lua @@ -0,0 +1,37 @@ +local a = vim.api + +local M = {} + +--- Asserts that the provided language is installed, and optionnaly provide a path for the parser +-- +-- Parsers are searched in the `parser` runtime directory. +-- +-- @param lang The language the parser should parse +-- @param path Optionnal path the parser is located at +function M.require_language(lang, path) + if vim._ts_has_language(lang) then + return true + end + if path == nil then + local fname = 'parser/' .. lang .. '.*' + local paths = a.nvim_get_runtime_file(fname, false) + if #paths == 0 then + -- TODO(bfredl): help tag? + error("no parser for '"..lang.."' language, see :help treesitter-parsers") + end + path = paths[1] + end + vim._ts_add_language(path, lang) +end + +--- Inspects the provided language. +-- +-- Inspecting provides some useful informations on the language like node names, ... +-- +-- @param lang The language. +function M.inspect_language(lang) + M.require_language(lang) + return vim._ts_inspect_language(lang) +end + +return M diff --git a/runtime/lua/vim/treesitter/query.lua b/runtime/lua/vim/treesitter/query.lua new file mode 100644 index 0000000000..2903c5905c --- /dev/null +++ b/runtime/lua/vim/treesitter/query.lua @@ -0,0 +1,335 @@ +local a = vim.api +local language = require'vim.treesitter.language' + +-- query: pattern matching on trees +-- predicate matching is implemented in lua +local Query = {} +Query.__index = Query + +local M = {} + +-- Filter the runtime query files, the spec is like regular runtime files but in the new `queries` +-- directory. They resemble ftplugins, that is that you can override queries by adding things in the +-- `queries` directory, and extend using the `after/queries` directory. +local function filter_files(file_list) + local main = nil + local after = {} + + for _, fname in ipairs(file_list) do + -- Only get the name of the directory containing the queries directory + if vim.fn.fnamemodify(fname, ":p:h:h:h:t") == "after" then + table.insert(after, fname) + -- The first one is the one with most priority + elseif not main then + main = fname + end + end + + return { main, unpack(after) } +end + +local function runtime_query_path(lang, query_name) + return string.format('queries/%s/%s.scm', lang, query_name) +end + +local function filtered_runtime_queries(lang, query_name) + return filter_files(a.nvim_get_runtime_file(runtime_query_path(lang, query_name), true) or {}) +end + +local function get_query_files(lang, query_name, is_included) + local lang_files = filtered_runtime_queries(lang, query_name) + local query_files = lang_files + + if #query_files == 0 then return {} end + + local base_langs = {} + + -- Now get the base languages by looking at the first line of every file + -- The syntax is the folowing : + -- ;+ inherits: ({language},)*{language} + -- + -- {language} ::= {lang} | ({lang}) + local MODELINE_FORMAT = "^;+%s*inherits%s*:?%s*([a-z_,()]+)%s*$" + + for _, file in ipairs(query_files) do + local modeline = vim.fn.readfile(file, "", 1) + + if #modeline == 1 then + local langlist = modeline[1]:match(MODELINE_FORMAT) + + if langlist then + for _, incllang in ipairs(vim.split(langlist, ',', true)) do + local is_optional = incllang:match("%(.*%)") + + if is_optional then + if not is_included then + table.insert(base_langs, incllang:sub(2, #incllang - 1)) + end + else + table.insert(base_langs, incllang) + end + end + end + end + end + + for _, base_lang in ipairs(base_langs) do + local base_files = get_query_files(base_lang, query_name, true) + vim.list_extend(query_files, base_files) + end + + return query_files +end + +local function read_query_files(filenames) + local contents = {} + + for _,filename in ipairs(filenames) do + vim.list_extend(contents, vim.fn.readfile(filename)) + end + + return table.concat(contents, '\n') +end + +--- Returns the runtime query {query_name} for {lang}. +-- +-- @param lang The language to use for the query +-- @param query_name The name of the query (i.e. "highlights") +-- +-- @return The corresponding query, parsed. +function M.get_query(lang, query_name) + local query_files = get_query_files(lang, query_name) + local query_string = read_query_files(query_files) + + if #query_string > 0 then + return M.parse_query(lang, query_string) + end +end + +--- Parses a query. +-- +-- @param language The language +-- @param query A string containing the query (s-expr syntax) +-- +-- @returns The query +function M.parse_query(lang, query) + language.require_language(lang) + local self = setmetatable({}, Query) + self.query = vim._ts_parse_query(lang, query) + self.info = self.query:inspect() + self.captures = self.info.captures + return self +end + +-- TODO(vigoux): support multiline nodes too + +--- Gets the text corresponding to a given node +-- @param node the node +-- @param bufnr the buffer from which the node in extracted. +function M.get_node_text(node, source) + local start_row, start_col, start_byte = node:start() + local end_row, end_col, end_byte = node:end_() + + if type(source) == "number" then + if start_row ~= end_row then + return nil + end + local line = a.nvim_buf_get_lines(source, start_row, start_row+1, true)[1] + return string.sub(line, start_col+1, end_col) + elseif type(source) == "string" then + return source:sub(start_byte+1, end_byte) + end +end + +-- Predicate handler receive the following arguments +-- (match, pattern, bufnr, predicate) +local predicate_handlers = { + ["eq?"] = function(match, _, source, predicate) + local node = match[predicate[2]] + local node_text = M.get_node_text(node, source) + + local str + if type(predicate[3]) == "string" then + -- (#eq? @aa "foo") + str = predicate[3] + else + -- (#eq? @aa @bb) + str = M.get_node_text(match[predicate[3]], source) + end + + if node_text ~= str or str == nil then + return false + end + + return true + end, + + ["lua-match?"] = function(match, _, source, predicate) + local node = match[predicate[2]] + local regex = predicate[3] + local start_row, _, end_row, _ = node:range() + if start_row ~= end_row then + return false + end + + return string.find(M.get_node_text(node, source), regex) + end, + + ["match?"] = (function() + local magic_prefixes = {['\\v']=true, ['\\m']=true, ['\\M']=true, ['\\V']=true} + local function check_magic(str) + if string.len(str) < 2 or magic_prefixes[string.sub(str,1,2)] then + return str + end + return '\\v'..str + end + + local compiled_vim_regexes = setmetatable({}, { + __index = function(t, pattern) + local res = vim.regex(check_magic(vim.fn.escape(pattern, '\\'))) + rawset(t, pattern, res) + return res + end + }) + + return function(match, _, source, pred) + local node = match[pred[2]] + local start_row, start_col, end_row, end_col = node:range() + if start_row ~= end_row then + return false + end + + local regex = compiled_vim_regexes[pred[3]] + return regex:match_line(source, start_row, start_col, end_col) + end + end)(), + + ["contains?"] = function(match, _, source, predicate) + local node = match[predicate[2]] + local node_text = M.get_node_text(node, source) + + for i=3,#predicate do + if string.find(node_text, predicate[i], 1, true) then + return true + end + end + + return false + end +} + +-- As we provide lua-match? also expose vim-match? +predicate_handlers["vim-match?"] = predicate_handlers["match?"] + +--- Adds a new predicates to be used in queries +-- +-- @param name the name of the predicate, without leading # +-- @param handler the handler function to be used +-- signature will be (match, pattern, bufnr, predicate) +function M.add_predicate(name, handler, force) + if predicate_handlers[name] and not force then + a.nvim_err_writeln(string.format("Overriding %s", name)) + end + + predicate_handlers[name] = handler +end + +--- Returns the list of currently supported predicates +function M.list_predicates() + return vim.tbl_keys(predicate_handlers) +end + +local function xor(x, y) + return (x or y) and not (x and y) +end + +function Query:match_preds(match, pattern, source) + local preds = self.info.patterns[pattern] + + for _, pred in pairs(preds or {}) do + -- Here we only want to return if a predicate DOES NOT match, and + -- continue on the other case. This way unknown predicates will not be considered, + -- which allows some testing and easier user extensibility (#12173). + -- Also, tree-sitter strips the leading # from predicates for us. + local pred_name + local is_not + if string.sub(pred[1], 1, 4) == "not-" then + pred_name = string.sub(pred[1], 5) + is_not = true + else + pred_name = pred[1] + is_not = false + end + + local handler = predicate_handlers[pred_name] + + if not handler then + a.nvim_err_writeln(string.format("No handler for %s", pred[1])) + return false + end + + local pred_matches = handler(match, pattern, source, pred) + + if not xor(is_not, pred_matches) then + return false + end + end + return true +end + +--- Iterates of the captures of self on a given range. +-- +-- @param node The node under witch the search will occur +-- @param buffer The source buffer to search +-- @param start The starting line of the search +-- @param stop The stoping line of the search (end-exclusive) +-- +-- @returns The matching capture id +-- @returns The captured node +function Query:iter_captures(node, source, start, stop) + if type(source) == "number" and source == 0 then + source = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query, true, start, stop) + local function iter() + local capture, captured_node, match = raw_iter() + if match ~= nil then + local active = self:match_preds(match, match.pattern, source) + match.active = active + if not active then + return iter() -- tail call: try next match + end + end + return capture, captured_node + end + return iter +end + +--- Iterates of the matches of self on a given range. +-- +-- @param node The node under witch the search will occur +-- @param buffer The source buffer to search +-- @param start The starting line of the search +-- @param stop The stoping line of the search (end-exclusive) +-- +-- @returns The matching pattern id +-- @returns The matching match +function Query:iter_matches(node, source, start, stop) + if type(source) == "number" and source == 0 then + source = vim.api.nvim_get_current_buf() + end + local raw_iter = node:_rawquery(self.query, false, start, stop) + local function iter() + local pattern, match = raw_iter() + if match ~= nil then + local active = self:match_preds(match, pattern, source) + if not active then + return iter() -- tail call: try next match + end + end + return pattern, match + end + return iter +end + +return M diff --git a/runtime/lua/vim/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua deleted file mode 100644 index 6465751ae8..0000000000 --- a/runtime/lua/vim/tshighlighter.lua +++ /dev/null @@ -1,116 +0,0 @@ -local a = vim.api - --- support reload for quick experimentation -local TSHighlighter = rawget(vim.treesitter, 'TSHighlighter') or {} -TSHighlighter.__index = TSHighlighter -local ts_hs_ns = a.nvim_create_namespace("treesitter_hl") - --- These are conventions defined by tree-sitter, though it --- needs to be user extensible also. --- TODO(bfredl): this is very much incomplete, we will need to --- go through a few tree-sitter provided queries and decide --- on translations that makes the most sense. -TSHighlighter.hl_map = { - keyword="Keyword", - string="String", - type="Type", - comment="Comment", - constant="Constant", - operator="Operator", - number="Number", - label="Label", - ["function"]="Function", - ["function.special"]="Function", -} - -function TSHighlighter.new(query, bufnr, ft) - local self = setmetatable({}, TSHighlighter) - self.parser = vim.treesitter.get_parser( - bufnr, - ft, - { - on_changedtree = function(...) self:on_changedtree(...) end, - on_lines = function() self.root = self.parser:parse():root() end - } - ) - - self.buf = self.parser.bufnr - - local tree = self.parser:parse() - self.root = tree:root() - self:set_query(query) - self.edit_count = 0 - self.redraw_count = 0 - self.line_count = {} - a.nvim_buf_set_option(self.buf, "syntax", "") - - -- Tricky: if syntax hasn't been enabled, we need to reload color scheme - -- but use synload.vim rather than syntax.vim to not enable - -- syntax FileType autocmds. Later on we should integrate with the - -- `:syntax` and `set syntax=...` machinery properly. - if vim.g.syntax_on ~= 1 then - vim.api.nvim_command("runtime! syntax/synload.vim") - end - return self -end - -local function is_highlight_name(capture_name) - local firstc = string.sub(capture_name, 1, 1) - return firstc ~= string.lower(firstc) -end - -function TSHighlighter:get_hl_from_capture(capture) - - local name = self.query.captures[capture] - - if is_highlight_name(name) then - -- From "Normal.left" only keep "Normal" - return vim.split(name, '.', true)[1] - else - -- Default to false to avoid recomputing - return TSHighlighter.hl_map[name] - end -end - -function TSHighlighter:set_query(query) - if type(query) == "string" then - query = vim.treesitter.parse_query(self.parser.lang, query) - end - self.query = query - - self.hl_cache = setmetatable({}, { - __index = function(table, capture) - local hl = self:get_hl_from_capture(capture) - rawset(table, capture, hl) - - return hl - end - }) - - self:on_changedtree({{self.root:range()}}) -end - -function TSHighlighter:on_changedtree(changes) - -- Get a fresh root - self.root = self.parser.tree:root() - - for _, ch in ipairs(changes or {}) do - -- Try to be as exact as possible - local changed_node = self.root:descendant_for_range(ch[1], ch[2], ch[3], ch[4]) - - a.nvim_buf_clear_namespace(self.buf, ts_hs_ns, ch[1], ch[3]) - - for capture, node in self.query:iter_captures(changed_node, self.buf, ch[1], ch[3] + 1) do - local start_row, start_col, end_row, end_col = node:range() - local hl = self.hl_cache[capture] - if hl then - a.nvim__buf_add_decoration(self.buf, ts_hs_ns, hl, - start_row, start_col, - end_row, end_col, - {}) - end - end - end -end - -return TSHighlighter diff --git a/runtime/lua/vim/uri.lua b/runtime/lua/vim/uri.lua index 9c3535c676..f1a12c72ec 100644 --- a/runtime/lua/vim/uri.lua +++ b/runtime/lua/vim/uri.lua @@ -7,6 +7,9 @@ local uri_decode do local schar = string.char + + --- Convert hex to char + --@private local function hex_to_char(hex) return schar(tonumber(hex, 16)) end @@ -34,6 +37,8 @@ do else tohex = function(b) return string.format("%02x", b) end end + + --@private local function percent_encode_char(char) return "%"..tohex(sbyte(char), 2) end @@ -45,10 +50,14 @@ do end +--@private local function is_windows_file_uri(uri) return uri:match('^file:///[a-zA-Z]:') ~= nil end +--- Get a URI from a file path. +--@param path (string): Path to file +--@return URI local function uri_from_fname(path) local volume_path, fname = path:match("^([a-zA-Z]:)(.*)") local is_windows = volume_path ~= nil @@ -67,6 +76,9 @@ end local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*)://.*' +--- Get a URI from a bufnr +--@param bufnr (number): Buffer number +--@return URI local function uri_from_bufnr(bufnr) local fname = vim.api.nvim_buf_get_name(bufnr) local scheme = fname:match(URI_SCHEME_PATTERN) @@ -77,6 +89,9 @@ local function uri_from_bufnr(bufnr) end end +--- Get a filename from a URI +--@param uri (string): The URI +--@return Filename local function uri_to_fname(uri) local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme ~= 'file' then @@ -93,7 +108,10 @@ local function uri_to_fname(uri) return uri end --- Return or create a buffer for a uri. +--- Return or create a buffer for a uri. +--@param uri (string): The URI +--@return bufnr. +--@note Creates buffer but does not load it local function uri_to_bufnr(uri) local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri) if scheme == 'file' then |