aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim')
-rw-r--r--runtime/lua/vim/highlight.lua77
-rw-r--r--runtime/lua/vim/inspect.lua7
-rw-r--r--runtime/lua/vim/lsp.lua1013
-rw-r--r--runtime/lua/vim/lsp/buf.lua251
-rw-r--r--runtime/lua/vim/lsp/callbacks.lua293
-rw-r--r--runtime/lua/vim/lsp/log.lua94
-rw-r--r--runtime/lua/vim/lsp/protocol.lua988
-rw-r--r--runtime/lua/vim/lsp/rpc.lua472
-rw-r--r--runtime/lua/vim/lsp/util.lua1360
-rw-r--r--runtime/lua/vim/shared.lua457
-rw-r--r--runtime/lua/vim/treesitter.lua260
-rw-r--r--runtime/lua/vim/tshighlighter.lua116
-rw-r--r--runtime/lua/vim/uri.lua112
13 files changed, 5438 insertions, 62 deletions
diff --git a/runtime/lua/vim/highlight.lua b/runtime/lua/vim/highlight.lua
new file mode 100644
index 0000000000..ce0a3de520
--- /dev/null
+++ b/runtime/lua/vim/highlight.lua
@@ -0,0 +1,77 @@
+local api = vim.api
+
+local highlight = {}
+
+--- Highlight range between two positions
+---
+--@param bufnr number of buffer to apply highlighting to
+--@param ns namespace to add highlight to
+--@param higroup highlight group to use for highlighting
+--@param rtype type of range (:help setreg, default charwise)
+--@param inclusive boolean indicating whether the range is end-inclusive (default false)
+function highlight.range(bufnr, ns, higroup, start, finish, rtype, inclusive)
+ rtype = rtype or 'v'
+ inclusive = inclusive or false
+
+ -- sanity check
+ if start[2] < 0 or finish[2] < start[2] then return end
+
+ local region = vim.region(bufnr, start, finish, rtype, inclusive)
+ for linenr, cols in pairs(region) do
+ api.nvim_buf_add_highlight(bufnr, ns, higroup, linenr, cols[1], cols[2])
+ end
+
+end
+
+local yank_ns = api.nvim_create_namespace('hlyank')
+--- Highlight the yanked region
+---
+--- use from init.vim via
+--- au TextYankPost * lua vim.highlight.on_yank()
+--- customize highlight group and timeout via
+--- au TextYankPost * lua vim.highlight.on_yank {higroup="IncSearch", timeout=150}
+--- customize conditions (here: do not highlight a visual selection) via
+--- au TextYankPost * lua vim.highlight.on_yank {on_visual=false}
+---
+-- @param opts dictionary with options controlling the highlight:
+-- - higroup highlight group for yanked region (default "IncSearch")
+-- - timeout time in ms before highlight is cleared (default 150)
+-- - on_macro highlight when executing macro (default false)
+-- - on_visual highlight when yanking visual selection (default true)
+-- - event event structure (default vim.v.event)
+function highlight.on_yank(opts)
+ vim.validate {
+ opts = { opts,
+ function(t) if t == nil then return true else return type(t) == 'table' end end,
+ 'a table or nil to configure options (see `:h highlight.on_yank`)',
+ }}
+ opts = opts or {}
+ local event = opts.event or vim.v.event
+ local on_macro = opts.on_macro or false
+ local on_visual = (opts.on_visual ~= false)
+
+ if (not on_macro) and vim.fn.reg_executing() ~= '' then return end
+ if event.operator ~= 'y' or event.regtype == '' then return end
+ if (not on_visual) and event.visual then return end
+
+ local higroup = opts.higroup or "IncSearch"
+ local timeout = opts.timeout or 150
+
+ local bufnr = api.nvim_get_current_buf()
+ api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1)
+
+ local pos1 = vim.fn.getpos("'[")
+ local pos2 = vim.fn.getpos("']")
+
+ pos1 = {pos1[2] - 1, pos1[3] - 1 + pos1[4]}
+ pos2 = {pos2[2] - 1, pos2[3] - 1 + pos2[4]}
+
+ highlight.range(bufnr, yank_ns, higroup, pos1, pos2, event.regtype, event.inclusive)
+
+ vim.defer_fn(
+ function() api.nvim_buf_clear_namespace(bufnr, yank_ns, 0, -1) end,
+ timeout
+ )
+end
+
+return highlight
diff --git a/runtime/lua/vim/inspect.lua b/runtime/lua/vim/inspect.lua
index 7cb40ca64d..0448ea487f 100644
--- a/runtime/lua/vim/inspect.lua
+++ b/runtime/lua/vim/inspect.lua
@@ -244,6 +244,11 @@ function Inspector:putTable(t)
local nonSequentialKeys, nonSequentialKeysLength, sequenceLength = getNonSequentialKeys(t)
local mt = getmetatable(t)
+ if (vim and sequenceLength == 0 and nonSequentialKeysLength == 0
+ and mt == vim._empty_dict_mt) then
+ self:puts(tostring(t))
+ return
+ end
self:puts('{')
self:down(function()
@@ -289,7 +294,7 @@ function Inspector:putValue(v)
if tv == 'string' then
self:puts(smartQuote(escape(v)))
elseif tv == 'number' or tv == 'boolean' or tv == 'nil' or
- tv == 'cdata' or tv == 'ctype' then
+ tv == 'cdata' or tv == 'ctype' or (vim and v == vim.NIL) then
self:puts(tostring(v))
elseif tv == 'table' then
self:putTable(v)
diff --git a/runtime/lua/vim/lsp.lua b/runtime/lua/vim/lsp.lua
new file mode 100644
index 0000000000..6fe1d15b7e
--- /dev/null
+++ b/runtime/lua/vim/lsp.lua
@@ -0,0 +1,1013 @@
+local default_callbacks = require 'vim.lsp.callbacks'
+local log = require 'vim.lsp.log'
+local lsp_rpc = require 'vim.lsp.rpc'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+
+local vim = vim
+local nvim_err_writeln, nvim_buf_get_lines, nvim_command, nvim_buf_get_option
+ = vim.api.nvim_err_writeln, vim.api.nvim_buf_get_lines, vim.api.nvim_command, vim.api.nvim_buf_get_option
+local uv = vim.loop
+local tbl_isempty, tbl_extend = vim.tbl_isempty, vim.tbl_extend
+local validate = vim.validate
+
+local lsp = {
+ protocol = protocol;
+ callbacks = default_callbacks;
+ buf = require'vim.lsp.buf';
+ util = util;
+ -- Allow raw RPC access.
+ rpc = lsp_rpc;
+ -- Export these directly from rpc.
+ rpc_response_error = lsp_rpc.rpc_response_error;
+ -- You probably won't need this directly, since __tostring is set for errors
+ -- by the RPC.
+ -- format_rpc_error = lsp_rpc.format_rpc_error;
+}
+
+-- TODO improve handling of scratch buffers with LSP attached.
+
+local function err_message(...)
+ nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
+ nvim_command("redraw")
+end
+
+local function resolve_bufnr(bufnr)
+ validate { bufnr = { bufnr, 'n', true } }
+ if bufnr == nil or bufnr == 0 then
+ return vim.api.nvim_get_current_buf()
+ end
+ return bufnr
+end
+
+local function is_dir(filename)
+ validate{filename={filename,'s'}}
+ local stat = uv.fs_stat(filename)
+ return stat and stat.type == 'directory' or false
+end
+
+local wait_result_reason = { [-1] = "timeout"; [-2] = "interrupted"; [-3] = "error" }
+
+local valid_encodings = {
+ ["utf-8"] = 'utf-8'; ["utf-16"] = 'utf-16'; ["utf-32"] = 'utf-32';
+ ["utf8"] = 'utf-8'; ["utf16"] = 'utf-16'; ["utf32"] = 'utf-32';
+ UTF8 = 'utf-8'; UTF16 = 'utf-16'; UTF32 = 'utf-32';
+}
+
+local client_index = 0
+local function next_client_id()
+ client_index = client_index + 1
+ return client_index
+end
+-- Tracks all clients created via lsp.start_client
+local active_clients = {}
+local all_buffer_active_clients = {}
+local uninitialized_clients = {}
+
+local function for_each_buffer_client(bufnr, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local client_ids = all_buffer_active_clients[bufnr]
+ if not client_ids or tbl_isempty(client_ids) then
+ return
+ end
+ for client_id in pairs(client_ids) do
+ local client = active_clients[client_id]
+ if client then
+ callback(client, client_id, bufnr)
+ end
+ end
+end
+
+-- Error codes to be used with `on_error` from |vim.lsp.start_client|.
+-- Can be used to look up the string from a the number or the number
+-- from the string.
+lsp.client_errors = tbl_extend("error", lsp_rpc.client_errors, vim.tbl_add_reverse_lookup {
+ ON_INIT_CALLBACK_ERROR = table.maxn(lsp_rpc.client_errors) + 1;
+})
+
+local function validate_encoding(encoding)
+ validate {
+ encoding = { encoding, 's' };
+ }
+ return valid_encodings[encoding:lower()]
+ or error(string.format("Invalid offset encoding %q. Must be one of: 'utf-8', 'utf-16', 'utf-32'", encoding))
+end
+
+function lsp._cmd_parts(input)
+ vim.validate{cmd={
+ input,
+ function() return vim.tbl_islist(input) end,
+ "list"}}
+
+ local cmd = input[1]
+ local cmd_args = {}
+ -- Don't mutate our input.
+ for i, v in ipairs(input) do
+ vim.validate{["cmd argument"]={v, "s"}}
+ if i > 1 then
+ table.insert(cmd_args, v)
+ end
+ end
+ return cmd, cmd_args
+end
+
+local function optional_validator(fn)
+ return function(v)
+ return v == nil or fn(v)
+ end
+end
+
+local function validate_client_config(config)
+ validate {
+ config = { config, 't' };
+ }
+ validate {
+ root_dir = { config.root_dir, is_dir, "directory" };
+ callbacks = { config.callbacks, "t", true };
+ capabilities = { config.capabilities, "t", true };
+ cmd_cwd = { config.cmd_cwd, optional_validator(is_dir), "directory" };
+ cmd_env = { config.cmd_env, "t", true };
+ name = { config.name, 's', true };
+ on_error = { config.on_error, "f", true };
+ on_exit = { config.on_exit, "f", true };
+ on_init = { config.on_init, "f", true };
+ before_init = { config.before_init, "f", true };
+ offset_encoding = { config.offset_encoding, "s", true };
+ }
+ local cmd, cmd_args = lsp._cmd_parts(config.cmd)
+ local offset_encoding = valid_encodings.UTF16
+ if config.offset_encoding then
+ offset_encoding = validate_encoding(config.offset_encoding)
+ end
+ return {
+ cmd = cmd; cmd_args = cmd_args;
+ offset_encoding = offset_encoding;
+ }
+end
+
+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
+ text = text .. '\n'
+ end
+ return text
+end
+
+local function text_document_did_open_handler(bufnr, client)
+ if not client.resolved_capabilities.text_document_open_close then
+ return
+ end
+ if not vim.api.nvim_buf_is_loaded(bufnr) then
+ return
+ end
+ local params = {
+ textDocument = {
+ version = 0;
+ uri = vim.uri_from_bufnr(bufnr);
+ -- TODO make sure our filetypes are compatible with languageId names.
+ languageId = nvim_buf_get_option(bufnr, 'filetype');
+ text = buf_get_full_text(bufnr);
+ }
+ }
+ client.notify('textDocument/didOpen', params)
+ util.buf_versions[bufnr] = params.textDocument.version
+end
+
+--- LSP client object.
+---
+--- - 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.
+--- 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]
+---
+--- - 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
+---
+--- - cancel_request(id)
+--- This is just {client.rpc.notify}("$/cancelRequest", { id = id })
+--- Returns the same as `notify()`.
+---
+--- - stop([force])
+--- Stop 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.
+---
+--- - Members
+--- - id (number): The id allocated to the client.
+---
+--- - 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
+--- before text is sent to the server.
+---
+--- - 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
+--- to |vim.lsp.start_client()|.
+---
+--- - server_capabilities (table): Response from the server sent on
+--- `initialize` describing the server's capabilities.
+---
+--- - 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
+
+--- Starts and initializes a client with the given configuration.
+---
+--- Parameters `cmd` and `root_dir` are required.
+---
+--@param root_dir: (required, string) Directory where the LSP server will base
+--- its rootUri on initialization.
+---
+--@param cmd: (required, string or list treated like |jobstart()|) Base command
+--- that initiates the LSP client.
+---
+--@param cmd_cwd: (string, default=|getcwd()|) Directory to launch
+--- the `cmd` process. Not related to `root_dir`.
+---
+--@param cmd_env: (table) Environment flags to pass to the LSP on
+--- spawn. Can be specified using keys like a map or as a list with `k=v`
+--- pairs or both. Non-string values are coerced to string.
+--- Example:
+--- <pre>
+--- { "PRODUCTION=true"; "TEST=123"; PORT = 8080; HOST = "0.0.0.0"; }
+--- </pre>
+---
+--@param capabilities Map overriding the default capabilities defined by
+--- |vim.lsp.protocol.make_client_capabilities()|, passed to the language
+--- server on initialization. Hint: use make_client_capabilities() and modify
+--- its result.
+--- - Note: To send an empty dictionary use
+--- `{[vim.type_idx]=vim.types.dictionary}`, else it will be encoded as an
+--- array.
+---
+--@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
+--- 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.
+--- - Default callback for client requests not explicitly specifying
+--- a callback.
+---
+--@param init_options Values to pass in the initialization request
+--- as `initializationOptions`. See `initialize` in the LSP spec.
+---
+--@param name (string, default=client-id) Name in log messages.
+---
+--@param offset_encoding (default="utf-16") One of "utf-8", "utf-16",
+--- or "utf-32" which is the encoding that the LSP server expects. Client does
+--- not verify this is correct.
+---
+--@param on_error Callback with parameters (code, ...), invoked
+--- when the client operation throws an error. `code` is a number describing
+--- the error. 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 human-friendly name.
+---
+--@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
+--- they are sent.
+---
+--@param on_init Callback (client, initialize_result) invoked after LSP
+--- "initialize", where `result` is a table of `capabilities` and anything else
+--- the server may send. For example, clangd sends
+--- `initialize_result.offsetEncoding` if `capabilities.offsetEncoding` was
+--- sent to it. You can only modify the `client.offset_encoding` here before
+--- any notifications are sent.
+---
+--@param on_exit Callback (code, signal, client_id) invoked on client
+--- exit.
+--- - code: exit code of the process
+--- - signal: number describing the signal used to terminate (if any)
+--- - client_id: client handle
+---
+--@param on_attach Callback (client, bufnr) invoked when client
+--- attaches to a buffer.
+---
+--@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
+--- the client has been initialized.
+function lsp.start_client(config)
+ local cleaned_config = validate_client_config(config)
+ local cmd, cmd_args, offset_encoding = cleaned_config.cmd, cleaned_config.cmd_args, cleaned_config.offset_encoding
+
+ local client_id = next_client_id()
+
+ local callbacks = config.callbacks or {}
+ local name = config.name or tostring(client_id)
+ local log_prefix = string.format("LSP[%s]", name)
+
+ local handlers = {}
+
+ local function resolve_callback(method)
+ return callbacks[method] or default_callbacks[method]
+ end
+
+ function handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+ local callback = resolve_callback(method)
+ if callback then
+ -- Method name is provided here for convenience.
+ callback(nil, method, params, client_id)
+ end
+ end
+
+ function handlers.server_request(method, params)
+ local _ = log.debug() and log.debug('server_request', method, params)
+ local callback = resolve_callback(method)
+ if callback then
+ local _ = log.debug() and log.debug("server_request: found callback for", method)
+ return callback(nil, method, params, client_id)
+ end
+ local _ = log.debug() and log.debug("server_request: no callback found for", method)
+ return nil, lsp.rpc_response_error(protocol.ErrorCodes.MethodNotFound)
+ end
+
+ 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))
+ if config.on_error then
+ local status, usererr = pcall(config.on_error, code, err)
+ if not status then
+ local _ = log.error() and log.error(log_prefix, "user on_error failed", { err = usererr })
+ err_message(log_prefix, ' user on_error failed: ', tostring(usererr))
+ end
+ end
+ end
+
+ function handlers.on_exit(code, signal)
+ active_clients[client_id] = nil
+ uninitialized_clients[client_id] = nil
+ local active_buffers = {}
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ table.insert(active_buffers, bufnr)
+ end
+ client_ids[client_id] = nil
+ end
+ -- Buffer level cleanup
+ vim.schedule(function()
+ for _, bufnr in ipairs(active_buffers) do
+ util.buf_clear_diagnostics(bufnr)
+ end
+ end)
+ if config.on_exit then
+ pcall(config.on_exit, code, signal, client_id)
+ end
+ end
+
+ -- Start the RPC client.
+ local rpc = lsp_rpc.start(cmd, cmd_args, handlers, {
+ cwd = config.cmd_cwd;
+ env = config.cmd_env;
+ })
+
+ local client = {
+ id = client_id;
+ name = name;
+ rpc = rpc;
+ offset_encoding = offset_encoding;
+ callbacks = callbacks;
+ config = config;
+ }
+
+ -- Store the uninitialized_clients for cleanup in case we exit before
+ -- initialize finishes.
+ uninitialized_clients[client_id] = client;
+
+ local function initialize()
+ local valid_traces = {
+ off = 'off'; messages = 'messages'; verbose = 'verbose';
+ }
+ local initialize_params = {
+ -- The process Id of the parent process that started the server. Is null if
+ -- the process has not been started by another process. If the parent
+ -- process is not alive then the server should exit (see exit notification)
+ -- its process.
+ processId = uv.getpid();
+ -- The rootPath of the workspace. Is null if no folder is open.
+ --
+ -- @deprecated in favour of rootUri.
+ rootPath = config.root_dir;
+ -- The rootUri of the workspace. Is null if no folder is open. If both
+ -- `rootPath` and `rootUri` are set `rootUri` wins.
+ rootUri = vim.uri_from_fname(config.root_dir);
+ -- User provided initialization options.
+ initializationOptions = config.init_options;
+ -- The capabilities provided by the client (editor or tool)
+ capabilities = config.capabilities or protocol.make_client_capabilities();
+ -- The initial trace setting. If omitted trace is disabled ("off").
+ -- trace = "off" | "messages" | "verbose";
+ trace = valid_traces[config.trace] or 'off';
+ -- The workspace folders configured in the client when the server starts.
+ -- This property is only available if the client supports workspace folders.
+ -- It can be `null` if the client supports workspace folders but none are
+ -- configured.
+ --
+ -- Since 3.6.0
+ -- workspaceFolders?: WorkspaceFolder[] | null;
+ -- export interface WorkspaceFolder {
+ -- -- The associated URI for this workspace folder.
+ -- uri
+ -- -- The name of the workspace folder. Used to refer to this
+ -- -- workspace folder in the user interface.
+ -- name
+ -- }
+ workspaceFolders = nil;
+ }
+ if config.before_init then
+ -- TODO(ashkan) handle errors here.
+ pcall(config.before_init, initialize_params, config)
+ end
+ local _ = log.debug() and log.debug(log_prefix, "initialize_params", initialize_params)
+ rpc.request('initialize', initialize_params, function(init_err, result)
+ assert(not init_err, tostring(init_err))
+ assert(result, "server sent empty result")
+ rpc.notify('initialized', {[vim.type_idx]=vim.types.dictionary})
+ client.initialized = true
+ uninitialized_clients[client_id] = nil
+ client.server_capabilities = assert(result.capabilities, "initialize result doesn't contain capabilities")
+ -- 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)
+ if config.on_init then
+ local status, err = pcall(config.on_init, client, result)
+ if not status then
+ pcall(handlers.on_error, lsp.client_errors.ON_INIT_CALLBACK_ERROR, err)
+ end
+ end
+ local _ = log.debug() and log.debug(log_prefix, "server_capabilities", client.server_capabilities)
+ local _ = log.info() and log.info(log_prefix, "initialized", { resolved_capabilities = client.resolved_capabilities })
+
+ -- Only assign after initialized.
+ active_clients[client_id] = client
+ -- If we had been registered before we start, then send didOpen This can
+ -- happen if we attach to buffers before initialize finishes or if
+ -- someone restarts a client.
+ for bufnr, client_ids in pairs(all_buffer_active_clients) do
+ if client_ids[client_id] then
+ client._on_attach(bufnr)
+ end
+ end
+ 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.
+ function client.request(method, params, callback, bufnr)
+ 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
+
+ function client.notify(...)
+ return rpc.notify(...)
+ end
+
+ function client.cancel_request(id)
+ validate{id = {id, 'n'}}
+ return rpc.notify("$/cancelRequest", { id = id })
+ end
+
+ -- Track this so that we can escalate automatically if we've alredy tried a
+ -- graceful shutdown
+ local tried_graceful_shutdown = false
+ function client.stop(force)
+ local handle = rpc.handle
+ if handle:is_closing() then
+ return
+ end
+ if force or (not client.initialized) or tried_graceful_shutdown then
+ handle:kill(15)
+ return
+ end
+ tried_graceful_shutdown = true
+ -- Sending a signal after a process has exited is acceptable.
+ rpc.request('shutdown', nil, function(err, _)
+ if err == nil then
+ rpc.notify('exit')
+ else
+ -- If there was an error in the shutdown request, then term to be safe.
+ handle:kill(15)
+ end
+ end)
+ end
+
+ function client.is_stopped()
+ return rpc.handle:is_closing()
+ end
+
+ function client._on_attach(bufnr)
+ text_document_did_open_handler(bufnr, client)
+ if config.on_attach then
+ -- TODO(ashkan) handle errors.
+ pcall(config.on_attach, client, bufnr)
+ end
+ end
+
+ initialize()
+
+ return client_id
+end
+
+local function once(fn)
+ local value
+ return function(...)
+ if not value then value = fn(...) end
+ return value
+ end
+end
+
+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))
+
+ -- Don't do anything if there are no clients attached.
+ if tbl_isempty(all_buffer_active_clients[bufnr] or {}) then
+ return
+ end
+
+ util.buf_versions[bufnr] = changedtick
+ -- Lazy initialize these because clients may not even need them.
+ local incremental_changes = once(function(client)
+ local size_index = encoding_index[client.offset_encoding]
+ local length = select(size_index, old_byte_size, old_utf16_size, old_utf32_size)
+ local lines = nvim_buf_get_lines(bufnr, firstline, new_lastline, true)
+ -- This is necessary because we are specifying the full line including the
+ -- newline in range. Therefore, we must replace the newline as well.
+ if #lines > 0 then
+ table.insert(lines, '')
+ end
+ return {
+ range = {
+ start = { line = firstline, character = 0 };
+ ["end"] = { line = lastline, character = 0 };
+ };
+ rangeLength = length;
+ text = table.concat(lines, '\n');
+ };
+ end)
+ local full_changes = once(function()
+ return {
+ text = buf_get_full_text(bufnr);
+ };
+ end)
+ local uri = vim.uri_from_bufnr(bufnr)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ local text_document_did_change = client.resolved_capabilities.text_document_did_change
+ local changes
+ if text_document_did_change == protocol.TextDocumentSyncKind.None then
+ return
+ --[=[ TODO(ashkan) there seem to be problems with the byte_sizes sent by
+ -- neovim right now so only send the full content for now. In general, we
+ -- can assume that servers *will* support both versions anyway, as there
+ -- is no way to specify the sync capability by the client.
+ -- See https://github.com/palantir/python-language-server/commit/cfd6675bc10d5e8dbc50fc50f90e4a37b7178821#diff-f68667852a14e9f761f6ebf07ba02fc8 for an example of pyls handling both.
+ --]=]
+ elseif true or text_document_did_change == protocol.TextDocumentSyncKind.Full then
+ changes = full_changes(client)
+ elseif text_document_did_change == protocol.TextDocumentSyncKind.Incremental then
+ changes = incremental_changes(client)
+ end
+ client.notify("textDocument/didChange", {
+ textDocument = {
+ uri = uri;
+ version = changedtick;
+ };
+ contentChanges = { changes; }
+ })
+ end)
+ end
+end
+
+-- Buffer lifecycle handler for textDocument/didSave
+function lsp._text_document_did_save_handler(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local uri = vim.uri_from_bufnr(bufnr)
+ local text = once(function()
+ return table.concat(nvim_buf_get_lines(bufnr, 0, -1, false), '\n')
+ end)
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_save then
+ local included_text
+ if client.resolved_capabilities.text_document_save_include_text then
+ included_text = text()
+ end
+ client.notify('textDocument/didSave', {
+ textDocument = {
+ uri = uri;
+ text = included_text;
+ }
+ })
+ end
+ end)
+end
+
+--- Implements the `textDocument/did…` notifications required to track a buffer
+--- for any language server.
+---
+--- Without calling this, the server won't be notified of changes to a buffer.
+---
+--- @param bufnr (number) Buffer handle, or 0 for current
+--- @param client_id (number) Client id
+function lsp.buf_attach_client(bufnr, client_id)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ client_id = {client_id, 'n'};
+ }
+ bufnr = resolve_bufnr(bufnr)
+ local buffer_client_ids = all_buffer_active_clients[bufnr]
+ -- This is our first time attaching to this buffer.
+ if not buffer_client_ids then
+ buffer_client_ids = {}
+ all_buffer_active_clients[bufnr] = buffer_client_ids
+
+ local uri = vim.uri_from_bufnr(bufnr)
+ nvim_command(string.format("autocmd BufWritePost <buffer=%d> lua vim.lsp._text_document_did_save_handler(0)", bufnr))
+ -- First time, so attach and set up stuff.
+ vim.api.nvim_buf_attach(bufnr, false, {
+ on_lines = text_document_did_change_handler;
+ on_detach = function()
+ local params = { textDocument = { uri = uri; } }
+ for_each_buffer_client(bufnr, function(client, _client_id)
+ if client.resolved_capabilities.text_document_open_close then
+ client.notify('textDocument/didClose', params)
+ end
+ end)
+ util.buf_versions[bufnr] = nil
+ all_buffer_active_clients[bufnr] = nil
+ end;
+ -- TODO if we know all of the potential clients ahead of time, then we
+ -- could conditionally set this.
+ -- utf_sizes = size_index > 1;
+ utf_sizes = true;
+ })
+ end
+ if buffer_client_ids[client_id] then return end
+ -- This is our first time attaching this client to this buffer.
+ buffer_client_ids[client_id] = true
+
+ local client = active_clients[client_id]
+ -- Send didOpen for the client if it is initialized. If it isn't initialized
+ -- then it will send didOpen on initialize.
+ if client then
+ client._on_attach(bufnr)
+ end
+ return true
+end
+
+--- Checks if a buffer is attached for a particular client.
+---
+---@param bufnr (number) Buffer handle, or 0 for current
+---@param client_id (number) the client id
+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.
+---
+--@param client_id client id number
+---
+--@return |vim.lsp.client| object, or nil
+function lsp.get_client_by_id(client_id)
+ return active_clients[client_id]
+end
+
+--- Stops a client(s).
+---
+--- You can also use the `stop()` function on a |vim.lsp.client| object.
+--- To stop all clients:
+---
+--- <pre>
+--- vim.lsp.stop_client(lsp.get_active_clients())
+--- </pre>
+---
+--- By default asks the server to shutdown, unless stop was requested
+--- already for this client, then force-shutdown is attempted.
+---
+--@param client_id client id or |vim.lsp.client| object, or list thereof
+--@param force boolean (optional) shutdown forcefully
+function lsp.stop_client(client_id, force)
+ local ids = type(client_id) == 'table' and client_id or {client_id}
+ for _, id in ipairs(ids) do
+ if type(id) == 'table' and id.stop ~= nil then
+ id.stop(force)
+ elseif active_clients[id] then
+ active_clients[id].stop(force)
+ elseif uninitialized_clients[id] then
+ uninitialized_clients[id].stop(true)
+ end
+ end
+end
+
+--- Gets all active clients.
+---
+--@return Table of |vim.lsp.client| objects
+function lsp.get_active_clients()
+ return vim.tbl_values(active_clients)
+end
+
+function lsp._vim_exit_handler()
+ log.info("exit_handler", active_clients)
+ for _, client in pairs(uninitialized_clients) do
+ client.stop(true)
+ end
+ -- TODO handle v:dying differently?
+ if tbl_isempty(active_clients) then
+ return
+ end
+ for _, client in pairs(active_clients) do
+ client.stop()
+ end
+
+ if not vim.wait(500, function() return tbl_isempty(active_clients) end, 50) then
+ for _, client in pairs(active_clients) do
+ client.stop(true)
+ end
+ end
+end
+
+nvim_command("autocmd VimLeavePre * lua vim.lsp._vim_exit_handler()")
+
+
+--- Sends an async request for all active clients attached to the
+--- buffer.
+---
+--@param bufnr (number) Buffer handle, or 0 for current.
+--@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.
+function lsp.buf_request(bufnr, method, params, callback)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ 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
+ end
+ end)
+
+ local function _cancel_all_requests()
+ for client_id, request_id in pairs(client_request_ids) do
+ local client = active_clients[client_id]
+ client.cancel_request(request_id)
+ end
+ end
+
+ return client_request_ids, _cancel_all_requests
+end
+
+--- Sends a request to a server and waits for the response.
+---
+--- Calls |vim.lsp.buf_request()| but blocks Nvim while awaiting the result.
+--- Parameters are the same as |vim.lsp.buf_request()| but the return result is
+--- different. Wait maximum of {timeout_ms} (default 100) ms.
+---
+--@param bufnr (number) Buffer handle, or 0 for current.
+--@param method (string) LSP method name
+--@param params (optional, table) Parameters to send to the server
+--@param timeout_ms (optional, number, default=100) Maximum time in
+--- milliseconds to wait for a result.
+---
+--@returns Map of client_id:request_result. On timeout, cancel or error,
+--- returns `(nil, err)` where `err` is a string describing the failure
+--- reason.
+function lsp.buf_request_sync(bufnr, method, params, timeout_ms)
+ local request_results = {}
+ local result_count = 0
+ local function _callback(err, _method, result, client_id)
+ request_results[client_id] = { error = err, result = result }
+ result_count = result_count + 1
+ end
+ local client_request_ids, cancel = lsp.buf_request(bufnr, method, params, _callback)
+ local expected_result_count = 0
+ for _ in pairs(client_request_ids) do
+ expected_result_count = expected_result_count + 1
+ end
+
+ local wait_result, reason = vim.wait(timeout_ms or 100, function()
+ return result_count >= expected_result_count
+ end, 10)
+
+ if not wait_result then
+ cancel()
+ return nil, wait_result_reason[reason]
+ end
+ return request_results
+end
+
+--- Send a notification to a server
+--@param bufnr [number] (optional): The number of the buffer
+--@param method [string]: Name of the request method
+--@param params [string]: Arguments to send to the server
+---
+--@returns true if any client returns true; false otherwise
+function lsp.buf_notify(bufnr, method, params)
+ validate {
+ bufnr = { bufnr, 'n', true };
+ method = { method, 's' };
+ }
+ local resp = false
+ for_each_buffer_client(bufnr, function(client, _client_id, _resolved_bufnr)
+ if client.rpc.notify(method, params) then resp = true end
+ end)
+ return resp
+end
+
+--- Implements 'omnifunc' compatible LSP completion.
+---
+--@see |complete-functions|
+--@see |complete-items|
+--@see |CompleteDone|
+---
+--@param findstart 0 or 1, decides behavior
+--@param base If findstart=0, text to match against
+---
+--@return (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)
+ local _ = log.debug() and log.debug("omnifunc.findstart", { findstart = findstart, base = base })
+
+ local bufnr = resolve_bufnr()
+ local has_buffer_clients = not tbl_isempty(all_buffer_active_clients[bufnr] or {})
+ if not has_buffer_clients then
+ if findstart == 1 then
+ return -1
+ else
+ return {}
+ end
+ end
+
+ -- Then, perform standard completion request
+ local _ = log.info() and log.info("base ", base)
+
+ local pos = vim.api.nvim_win_get_cursor(0)
+ local line = vim.api.nvim_get_current_line()
+ local line_to_cursor = line:sub(1, pos[2])
+ local _ = log.trace() and log.trace("omnifunc.line", pos, line)
+
+ -- Get the start position of the current keyword
+ local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
+ local prefix = line_to_cursor:sub(textMatch+1)
+
+ local params = util.make_position_params()
+
+ local items = {}
+ lsp.buf_request(bufnr, 'textDocument/completion', params, function(err, _, result)
+ if err or not result then return end
+ local matches = util.text_document_completion_list_to_complete_items(result, prefix)
+ -- TODO(ashkan): is this the best way to do this?
+ vim.list_extend(items, matches)
+ vim.fn.complete(textMatch+1, items)
+ end)
+
+ -- Return -2 to signal that we should continue completion so that we can
+ -- async complete.
+ return -2
+end
+
+function lsp.client_is_stopped(client_id)
+ return active_clients[client_id] == nil
+end
+
+--- Gets a map of client_id:client pairs for the given buffer, where each value
+--- is a |vim.lsp.client| object.
+---
+--@param bufnr (optional, number): Buffer handle, or 0 for current
+function lsp.buf_get_clients(bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local result = {}
+ for_each_buffer_client(bufnr, function(client, client_id)
+ result[client_id] = client
+ end)
+ return result
+end
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the
+-- name from the number.
+-- Levels by name: "trace", "debug", "info", "warn", "error"
+-- Level numbers begin with "trace" at 0
+lsp.log_levels = log.levels
+
+--- Sets the global log level for LSP logging.
+---
+--- Levels by name: "trace", "debug", "info", "warn", "error"
+--- Level numbers begin with "trace" at 0
+---
+--- Use `lsp.log_levels` for reverse lookup.
+---
+--@see |vim.lsp.log_levels|
+---
+--@param level [number|string] the case insensitive level name or number
+function lsp.set_log_level(level)
+ if type(level) == 'string' or type(level) == 'number' then
+ log.set_level(level)
+ else
+ error(string.format("Invalid log level: %q", level))
+ end
+end
+
+--- Gets the path of the logfile used by the LSP client.
+function lsp.get_log_path()
+ return log.get_filename()
+end
+
+-- Define the LspDiagnostics signs if they're not defined already.
+do
+ local function define_default_sign(name, properties)
+ if vim.tbl_isempty(vim.fn.sign_getdefined(name)) then
+ vim.fn.sign_define(name, properties)
+ end
+ end
+ define_default_sign('LspDiagnosticsErrorSign', {text='E', texthl='LspDiagnosticsErrorSign', linehl='', numhl=''})
+ define_default_sign('LspDiagnosticsWarningSign', {text='W', texthl='LspDiagnosticsWarningSign', linehl='', numhl=''})
+ define_default_sign('LspDiagnosticsInformationSign', {text='I', texthl='LspDiagnosticsInformationSign', linehl='', numhl=''})
+ define_default_sign('LspDiagnosticsHintSign', {text='H', texthl='LspDiagnosticsHintSign', linehl='', numhl=''})
+end
+
+return lsp
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/buf.lua b/runtime/lua/vim/lsp/buf.lua
new file mode 100644
index 0000000000..2e27617997
--- /dev/null
+++ b/runtime/lua/vim/lsp/buf.lua
@@ -0,0 +1,251 @@
+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 = {}
+
+local function ok_or_nil(status, ...)
+ if not status then return end
+ return ...
+end
+local function npcall(fn, ...)
+ return ok_or_nil(pcall(fn, ...))
+end
+
+local function request(method, params, callback)
+ validate {
+ method = {method, 's'};
+ callback = {callback, 'f', true};
+ }
+ 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.
+function M.server_ready()
+ return not not vim.lsp.buf_notify(0, "window/progress", {})
+end
+
+--- Displays hover information about the symbol under the cursor in a floating
+--- window. Calling the function twice will jump into the floating window.
+function M.hover()
+ local params = util.make_position_params()
+ request('textDocument/hover', params)
+end
+
+--- Jumps to the declaration of the symbol under the cursor.
+---
+function M.declaration()
+ local params = util.make_position_params()
+ request('textDocument/declaration', params)
+end
+
+--- Jumps to the definition of the symbol under the cursor.
+---
+function M.definition()
+ local params = util.make_position_params()
+ request('textDocument/definition', params)
+end
+
+--- Jumps to the definition of the type of the symbol under the cursor.
+---
+function M.type_definition()
+ local params = util.make_position_params()
+ request('textDocument/typeDefinition', params)
+end
+
+--- Lists all the implementations for the symbol under the cursor in the
+--- quickfix window.
+function M.implementation()
+ local params = util.make_position_params()
+ request('textDocument/implementation', params)
+end
+
+--- Displays signature information about the symbol under the cursor in a
+--- floating window.
+function M.signature_help()
+ local params = util.make_position_params()
+ request('textDocument/signatureHelp', params)
+end
+
+--- Retrieves the completion items at the current cursor position. Can only be
+--- called in Insert mode.
+function M.completion(context)
+ local params = util.make_position_params()
+ params.context = context
+ return request('textDocument/completion', params)
+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.
+--- Some unspecified options will be automatically derived from the current
+--- Neovim options.
+function M.formatting(options)
+ local params = util.make_formatting_params(options)
+ return request('textDocument/formatting', params)
+end
+
+--- Perform |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()|.
+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
+ vim.lsp.util.apply_text_edits(result)
+end
+
+function M.range_formatting(options, start_pos, end_pos)
+ validate {
+ options = {options, 't', true};
+ start_pos = {start_pos, 't', true};
+ end_pos = {end_pos, 't', true};
+ }
+ 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;
+ }
+ 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()|.
+function M.rename(new_name)
+ -- TODO(ashkan) use prepareRename
+ -- * result: [`Range`](#range) \| `{ range: Range, placeholder: string }` \| `null` describing the range of the string to rename and optionally a placeholder text of the string content to be renamed. If `null` is returned then it is deemed that a 'textDocument/rename' request is not valid at the given position.
+ local params = util.make_position_params()
+ new_name = new_name or npcall(vfn.input, "New Name: ", vfn.expand('<cword>'))
+ if not (new_name and #new_name > 0) then return end
+ params.newName = new_name
+ request('textDocument/rename', params)
+end
+
+--- Lists all the references to the symbol under the cursor in the quickfix window.
+---
+function M.references(context)
+ validate { context = { context, 't', true } }
+ local params = util.make_position_params()
+ params.context = context or {
+ includeDeclaration = true;
+ }
+ params[vim.type_idx] = vim.types.dictionary
+ request('textDocument/references', params)
+end
+
+--- Lists all symbols in the current buffer in the quickfix window.
+---
+function M.document_symbol()
+ local params = { textDocument = util.make_text_document_params() }
+ request('textDocument/documentSymbol', params)
+end
+
+local function pick_call_hierarchy_item(call_hierarchy_items)
+ if not call_hierarchy_items then return end
+ if #call_hierarchy_items == 1 then
+ return call_hierarchy_items[1]
+ end
+ local items = {}
+ for i, item in ipairs(call_hierarchy_items) do
+ local entry = item.detail or item.name
+ table.insert(items, string.format("%d. %s", i, entry))
+ end
+ local choice = vim.fn.inputlist(items)
+ if choice < 1 or choice > #items then
+ return
+ end
+ return choice
+end
+
+function M.incoming_calls()
+ local params = util.make_position_params()
+ request('textDocument/prepareCallHierarchy', params, function(_, _, result)
+ local call_hierarchy_item = pick_call_hierarchy_item(result)
+ vim.lsp.buf_request(0, 'callHierarchy/incomingCalls', { item = call_hierarchy_item })
+ end)
+end
+
+function M.outgoing_calls()
+ local params = util.make_position_params()
+ request('textDocument/prepareCallHierarchy', params, function(_, _, result)
+ local call_hierarchy_item = pick_call_hierarchy_item(result)
+ vim.lsp.buf_request(0, 'callHierarchy/outgoingCalls', { item = call_hierarchy_item })
+ end)
+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.
+function M.workspace_symbol(query)
+ query = query or npcall(vfn.input, "Query: ")
+ local params = {query = query}
+ request('workspace/symbol', params)
+end
+
+--- Send request to server to resolve document highlights for the
+--- current text document position. This request can be associated
+--- to key mapping or to events such as `CursorHold`, eg:
+---
+--- <pre>
+--- vim.api.nvim_command [[autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()]]
+--- vim.api.nvim_command [[autocmd CursorHoldI <buffer> lua vim.lsp.buf.document_highlight()]]
+--- vim.api.nvim_command [[autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()]]
+--- </pre>
+function M.document_highlight()
+ local params = util.make_position_params()
+ request('textDocument/documentHighlight', params)
+end
+
+function M.clear_references()
+ util.buf_clear_references()
+end
+
+function M.code_action(context)
+ validate { context = { context, 't', true } }
+ context = context or { diagnostics = util.get_line_diagnostics() }
+ local params = util.make_range_params()
+ params.context = context
+ request('textDocument/codeAction', params)
+end
+
+function M.execute_command(command)
+ validate {
+ command = { command.command, 's' },
+ arguments = { command.arguments, 't', true }
+ }
+ request('workspace/executeCommand', command)
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/callbacks.lua b/runtime/lua/vim/lsp/callbacks.lua
new file mode 100644
index 0000000000..1ed58995d0
--- /dev/null
+++ b/runtime/lua/vim/lsp/callbacks.lua
@@ -0,0 +1,293 @@
+local log = require 'vim.lsp.log'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+local vim = vim
+local api = vim.api
+local buf = require 'vim.lsp.buf'
+
+local M = {}
+
+local function err_message(...)
+ api.nvim_err_writeln(table.concat(vim.tbl_flatten{...}))
+ api.nvim_command("redraw")
+end
+
+M['workspace/executeCommand'] = function(err, _)
+ if err then
+ error("Could not execute code action: "..err.message)
+ end
+end
+
+M['textDocument/codeAction'] = function(_, _, actions)
+ if actions == nil or vim.tbl_isempty(actions) then
+ print("No code actions available")
+ return
+ end
+
+ local option_strings = {"Code Actions:"}
+ for i, action in ipairs(actions) do
+ local title = action.title:gsub('\r\n', '\\r\\n')
+ title = title:gsub('\n', '\\n')
+ table.insert(option_strings, string.format("%d. %s", i, title))
+ end
+
+ local choice = vim.fn.inputlist(option_strings)
+ if choice < 1 or choice > #actions then
+ return
+ end
+ local action_chosen = actions[choice]
+ -- textDocument/codeAction can return either Command[] or CodeAction[].
+ -- If it is a CodeAction, it can have either an edit, a command or both.
+ -- Edits should be executed first
+ if action_chosen.edit or type(action_chosen.command) == "table" then
+ if action_chosen.edit then
+ util.apply_workspace_edit(action_chosen.edit)
+ end
+ if type(action_chosen.command) == "table" then
+ buf.execute_command(action_chosen.command)
+ end
+ else
+ buf.execute_command(action_chosen)
+ end
+end
+
+M['workspace/applyEdit'] = function(_, _, workspace_edit)
+ if not workspace_edit then return end
+ -- TODO(ashkan) Do something more with label?
+ if workspace_edit.label then
+ print("Workspace edit", workspace_edit.label)
+ end
+ local status, result = pcall(util.apply_workspace_edit, workspace_edit.edit)
+ return {
+ applied = status;
+ failureReason = result;
+ }
+end
+
+M['textDocument/publishDiagnostics'] = function(_, _, result)
+ if not result then return end
+ local uri = result.uri
+ local bufnr = vim.uri_to_bufnr(uri)
+ if not bufnr then
+ err_message("LSP.publishDiagnostics: Couldn't find buffer for ", uri)
+ return
+ end
+
+ -- 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.
+ -- TODO: Replace this with server-specific heuristics to infer severity.
+ for _, diagnostic in ipairs(result.diagnostics) do
+ if diagnostic.severity == nil then
+ diagnostic.severity = protocol.DiagnosticSeverity.Error
+ end
+ end
+
+ util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
+ util.buf_diagnostics_underline(bufnr, result.diagnostics)
+ util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
+ util.buf_diagnostics_signs(bufnr, result.diagnostics)
+ vim.api.nvim_command("doautocmd User LspDiagnosticsChanged")
+end
+
+M['textDocument/references'] = function(_, _, result)
+ if not result then return end
+ util.set_qflist(util.locations_to_items(result))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+end
+
+local symbol_callback = function(_, _, result, _, bufnr)
+ if not result or vim.tbl_isempty(result) then return end
+
+ util.set_qflist(util.symbols_to_items(result, bufnr))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+end
+M['textDocument/documentSymbol'] = symbol_callback
+M['workspace/symbol'] = symbol_callback
+
+M['textDocument/rename'] = function(_, _, result)
+ if not result then return end
+ util.apply_workspace_edit(result)
+end
+
+M['textDocument/rangeFormatting'] = function(_, _, result)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+M['textDocument/formatting'] = function(_, _, result)
+ if not result then return end
+ util.apply_text_edits(result)
+end
+
+M['textDocument/completion'] = function(_, _, result)
+ if vim.tbl_isempty(result or {}) then return end
+ local row, col = unpack(api.nvim_win_get_cursor(0))
+ local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
+ local line_to_cursor = line:sub(col+1)
+ local textMatch = vim.fn.match(line_to_cursor, '\\k*$')
+ local prefix = line_to_cursor:sub(textMatch+1)
+
+ local matches = util.text_document_completion_list_to_complete_items(result, prefix)
+ vim.fn.complete(textMatch+1, matches)
+end
+
+M['textDocument/hover'] = function(_, method, result)
+ util.focusable_float(method, function()
+ if not (result and result.contents) then
+ -- return { 'No information available' }
+ return
+ end
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ markdown_lines = util.trim_empty_lines(markdown_lines)
+ if vim.tbl_isempty(markdown_lines) then
+ -- return { 'No information available' }
+ return
+ end
+ local bufnr, winnr = util.fancy_floating_markdown(markdown_lines, {
+ pad_left = 1; pad_right = 1;
+ })
+ util.close_preview_autocmd({"CursorMoved", "BufHidden", "InsertCharPre"}, winnr)
+ return bufnr, winnr
+ end)
+end
+
+local function location_callback(_, method, result)
+ if result == nil or vim.tbl_isempty(result) then
+ local _ = log.info() and log.info(method, 'No location found')
+ return nil
+ end
+
+ -- textDocument/definition can return Location or Location[]
+ -- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_definition
+
+ if vim.tbl_islist(result) then
+ util.jump_to_location(result[1])
+
+ if #result > 1 then
+ util.set_qflist(util.locations_to_items(result))
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+ end
+ else
+ util.jump_to_location(result)
+ end
+end
+
+M['textDocument/declaration'] = location_callback
+M['textDocument/definition'] = location_callback
+M['textDocument/typeDefinition'] = location_callback
+M['textDocument/implementation'] = location_callback
+
+M['textDocument/signatureHelp'] = function(_, method, result)
+ 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
+
+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
+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 = {}
+ for _, call_hierarchy_call in pairs(result) do
+ local call_hierarchy_item = call_hierarchy_call[direction]
+ for _, range in pairs(call_hierarchy_call.fromRanges) do
+ table.insert(items, {
+ filename = assert(vim.uri_to_fname(call_hierarchy_item.uri)),
+ text = call_hierarchy_item.name,
+ lnum = range.start.line + 1,
+ col = range.start.character + 1,
+ })
+ end
+ end
+ util.set_qflist(items)
+ api.nvim_command("copen")
+ api.nvim_command("wincmd p")
+ end
+end
+
+M['callHierarchy/incomingCalls'] = make_call_hierarchy_callback('from')
+
+M['callHierarchy/outgoingCalls'] = make_call_hierarchy_callback('to')
+
+M['window/logMessage'] = function(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format("id=%d", client_id)
+ if not client then
+ err_message("LSP[", client_name, "] client has shut down after sending the message")
+ end
+ if message_type == protocol.MessageType.Error then
+ log.error(message)
+ elseif message_type == protocol.MessageType.Warning then
+ log.warn(message)
+ elseif message_type == protocol.MessageType.Info then
+ log.info(message)
+ else
+ log.debug(message)
+ end
+ return result
+end
+
+M['window/showMessage'] = function(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format("id=%d", client_id)
+ if not client then
+ err_message("LSP[", client_name, "] client has shut down after sending the message")
+ end
+ if message_type == protocol.MessageType.Error then
+ err_message("LSP[", client_name, "] ", message)
+ else
+ local message_type_name = protocol.MessageType[message_type]
+ api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ end
+ return result
+end
+
+-- Add boilerplate error validation and logging for all of these.
+for k, fn in pairs(M) do
+ M[k] = function(err, method, params, client_id, bufnr)
+ log.debug('default_callback', method, { params = params, client_id = client_id, err = err, bufnr = bufnr })
+ if err then
+ error(tostring(err))
+ end
+ return fn(err, method, params, client_id, bufnr)
+ end
+end
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
new file mode 100644
index 0000000000..696ce43a59
--- /dev/null
+++ b/runtime/lua/vim/lsp/log.lua
@@ -0,0 +1,94 @@
+-- Logger for language client plugin.
+
+local log = {}
+
+-- Log level dictionary with reverse lookup as well.
+--
+-- Can be used to lookup the number from the name or the name from the number.
+-- Levels by name: 'trace', 'debug', 'info', 'warn', 'error'
+-- Level numbers begin with 'trace' at 0
+log.levels = {
+ TRACE = 0;
+ DEBUG = 1;
+ INFO = 2;
+ WARN = 3;
+ ERROR = 4;
+}
+
+-- Default log level is warn.
+local current_log_level = log.levels.WARN
+local log_date_format = "%FT%H:%M:%S%z"
+
+do
+ local path_sep = vim.loop.os_uname().sysname == "Windows" and "\\" or "/"
+ 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.
+ function log.get_filename()
+ return logfilename
+ end
+
+ vim.fn.mkdir(vim.fn.stdpath('data'), "p")
+ local logfile = assert(io.open(logfilename, "a+"))
+ for level, levelnr in pairs(log.levels) do
+ -- Also export the log level on the root object.
+ log[level] = levelnr
+ -- 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
+ -- log at that level (if applicable, it is checked either way).
+ --
+ -- Recommended usage:
+ -- ```
+ -- local _ = log.warn() and log.warn("123")
+ -- ```
+ --
+ -- This way you can avoid string allocations if the log level isn't high enough.
+ log[level:lower()] = function(...)
+ local argc = select("#", ...)
+ if levelnr < current_log_level then return false end
+ if argc == 0 then return true end
+ local info = debug.getinfo(2, "Sl")
+ local fileinfo = string.format("%s:%s", info.short_src, info.currentline)
+ local parts = { table.concat({"[", level, "]", os.date(log_date_format), "]", fileinfo, "]"}, " ") }
+ for i = 1, argc do
+ local arg = select(i, ...)
+ if arg == nil then
+ table.insert(parts, "nil")
+ else
+ table.insert(parts, vim.inspect(arg, {newline=''}))
+ end
+ end
+ logfile:write(table.concat(parts, '\t'), "\n")
+ logfile:flush()
+ end
+ end
+ -- Add some space to make it easier to distinguish different neovim runs.
+ logfile:write("\n")
+end
+
+-- This is put here on purpose after the loop above so that it doesn't
+-- interfere with iterating the levels
+vim.tbl_add_reverse_lookup(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))
+ else
+ assert(type(level) == 'number', "level must be a number or string")
+ assert(log.levels[level], string.format("Invalid log level: %d", level))
+ current_log_level = level
+ end
+end
+
+-- Return whether the level is sufficient for logging.
+-- @param level number log level
+function log.should_log(level)
+ return level >= current_log_level
+end
+
+return log
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/protocol.lua b/runtime/lua/vim/lsp/protocol.lua
new file mode 100644
index 0000000000..ef5e08680e
--- /dev/null
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -0,0 +1,988 @@
+-- Protocol for the Microsoft Language Server Protocol (mslsp)
+
+local protocol = {}
+
+local function ifnil(a, b)
+ if a == nil then return b end
+ return a
+end
+
+
+--[=[
+-- 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
+function transform_schema_to_table()
+ transform_schema_comments()
+ nvim.command [[silent! '<,'>s/: \S\+//]]
+ nvim.command [[silent! '<,'>s/export const //]]
+ nvim.command [[silent! '<,'>s/export namespace \(\S*\)\s*{/protocol.\1 = {/]]
+ nvim.command [[silent! '<,'>s/namespace \(\S*\)\s*{/protocol.\1 = {/]]
+end
+--]=]
+
+local constants = {
+ DiagnosticSeverity = {
+ -- Reports an error.
+ Error = 1;
+ -- Reports a warning.
+ Warning = 2;
+ -- Reports an information.
+ Information = 3;
+ -- Reports a hint.
+ Hint = 4;
+ };
+
+ MessageType = {
+ -- An error message.
+ Error = 1;
+ -- A warning message.
+ Warning = 2;
+ -- An information message.
+ Info = 3;
+ -- A log message.
+ Log = 4;
+ };
+
+ -- The file event type.
+ FileChangeType = {
+ -- The file got created.
+ Created = 1;
+ -- The file got changed.
+ Changed = 2;
+ -- The file got deleted.
+ Deleted = 3;
+ };
+
+ -- The kind of a completion entry.
+ CompletionItemKind = {
+ Text = 1;
+ Method = 2;
+ Function = 3;
+ Constructor = 4;
+ Field = 5;
+ Variable = 6;
+ Class = 7;
+ Interface = 8;
+ Module = 9;
+ Property = 10;
+ Unit = 11;
+ Value = 12;
+ Enum = 13;
+ Keyword = 14;
+ Snippet = 15;
+ Color = 16;
+ File = 17;
+ Reference = 18;
+ Folder = 19;
+ EnumMember = 20;
+ Constant = 21;
+ Struct = 22;
+ Event = 23;
+ Operator = 24;
+ TypeParameter = 25;
+ };
+
+ -- How a completion was triggered
+ CompletionTriggerKind = {
+ -- Completion was triggered by typing an identifier (24x7 code
+ -- complete), manual invocation (e.g Ctrl+Space) or via API.
+ Invoked = 1;
+ -- Completion was triggered by a trigger character specified by
+ -- the `triggerCharacters` properties of the `CompletionRegistrationOptions`.
+ TriggerCharacter = 2;
+ -- Completion was re-triggered as the current completion list is incomplete.
+ TriggerForIncompleteCompletions = 3;
+ };
+
+ -- A document highlight kind.
+ DocumentHighlightKind = {
+ -- A textual occurrence.
+ Text = 1;
+ -- Read-access of a symbol, like reading a variable.
+ Read = 2;
+ -- Write-access of a symbol, like writing to a variable.
+ Write = 3;
+ };
+
+ -- A symbol kind.
+ SymbolKind = {
+ File = 1;
+ Module = 2;
+ Namespace = 3;
+ Package = 4;
+ Class = 5;
+ Method = 6;
+ Property = 7;
+ Field = 8;
+ Constructor = 9;
+ Enum = 10;
+ Interface = 11;
+ Function = 12;
+ Variable = 13;
+ Constant = 14;
+ String = 15;
+ Number = 16;
+ Boolean = 17;
+ Array = 18;
+ Object = 19;
+ Key = 20;
+ Null = 21;
+ EnumMember = 22;
+ Struct = 23;
+ Event = 24;
+ Operator = 25;
+ TypeParameter = 26;
+ };
+
+ -- Represents reasons why a text document is saved.
+ TextDocumentSaveReason = {
+ -- Manually triggered, e.g. by the user pressing save, by starting debugging,
+ -- or by an API call.
+ Manual = 1;
+ -- Automatic after a delay.
+ AfterDelay = 2;
+ -- When the editor lost focus.
+ FocusOut = 3;
+ };
+
+ ErrorCodes = {
+ -- Defined by JSON RPC
+ ParseError = -32700;
+ InvalidRequest = -32600;
+ MethodNotFound = -32601;
+ InvalidParams = -32602;
+ InternalError = -32603;
+ serverErrorStart = -32099;
+ serverErrorEnd = -32000;
+ ServerNotInitialized = -32002;
+ UnknownErrorCode = -32001;
+ -- Defined by the protocol.
+ RequestCancelled = -32800;
+ ContentModified = -32801;
+ };
+
+ -- Describes the content type that a client supports in various
+ -- result literals like `Hover`, `ParameterInfo` or `CompletionItem`.
+ --
+ -- Please note that `MarkupKinds` must not start with a `$`. This kinds
+ -- are reserved for internal usage.
+ MarkupKind = {
+ -- Plain text is supported as a content format
+ PlainText = 'plaintext';
+ -- Markdown is supported as a content format
+ Markdown = 'markdown';
+ };
+
+ ResourceOperationKind = {
+ -- Supports creating new files and folders.
+ Create = 'create';
+ -- Supports renaming existing files and folders.
+ Rename = 'rename';
+ -- Supports deleting existing files and folders.
+ Delete = 'delete';
+ };
+
+ FailureHandlingKind = {
+ -- Applying the workspace change is simply aborted if one of the changes provided
+ -- fails. All operations executed before the failing operation stay executed.
+ Abort = 'abort';
+ -- All operations are executed transactionally. That means they either all
+ -- succeed or no changes at all are applied to the workspace.
+ Transactional = 'transactional';
+ -- If the workspace edit contains only textual file changes they are executed transactionally.
+ -- If resource changes (create, rename or delete file) are part of the change the failure
+ -- handling strategy is abort.
+ TextOnlyTransactional = 'textOnlyTransactional';
+ -- The client tries to undo the operations already executed. But there is no
+ -- guarantee that this succeeds.
+ Undo = 'undo';
+ };
+
+ -- Known error codes for an `InitializeError`;
+ InitializeError = {
+ -- If the protocol version provided by the client can't be handled by the server.
+ -- @deprecated This initialize error got replaced by client capabilities. There is
+ -- no version handshake in version 3.0x
+ unknownProtocolVersion = 1;
+ };
+
+ -- Defines how the host (editor) should sync document changes to the language server.
+ TextDocumentSyncKind = {
+ -- Documents should not be synced at all.
+ None = 0;
+ -- Documents are synced by always sending the full content
+ -- of the document.
+ Full = 1;
+ -- Documents are synced by sending the full content on open.
+ -- After that only incremental updates to the document are
+ -- send.
+ Incremental = 2;
+ };
+
+ WatchKind = {
+ -- Interested in create events.
+ Create = 1;
+ -- Interested in change events
+ Change = 2;
+ -- Interested in delete events
+ Delete = 4;
+ };
+
+ -- Defines whether the insert text in a completion item should be interpreted as
+ -- plain text or a snippet.
+ InsertTextFormat = {
+ -- The primary text to be inserted is treated as a plain string.
+ PlainText = 1;
+ -- The primary text to be inserted is treated as a snippet.
+ --
+ -- A snippet can define tab stops and placeholders with `$1`, `$2`
+ -- and `${3:foo};`. `$0` defines the final tab stop, it defaults to
+ -- the end of the snippet. Placeholders with equal identifiers are linked,
+ -- that is typing in one will update others too.
+ Snippet = 2;
+ };
+
+ -- A set of predefined code action kinds
+ CodeActionKind = {
+ -- Empty kind.
+ Empty = '';
+ -- Base kind for quickfix actions
+ QuickFix = 'quickfix';
+ -- Base kind for refactoring actions
+ Refactor = 'refactor';
+ -- Base kind for refactoring extraction actions
+ --
+ -- Example extract actions:
+ --
+ -- - Extract method
+ -- - Extract function
+ -- - Extract variable
+ -- - Extract interface from class
+ -- - ...
+ RefactorExtract = 'refactor.extract';
+ -- Base kind for refactoring inline actions
+ --
+ -- Example inline actions:
+ --
+ -- - Inline function
+ -- - Inline variable
+ -- - Inline constant
+ -- - ...
+ RefactorInline = 'refactor.inline';
+ -- Base kind for refactoring rewrite actions
+ --
+ -- Example rewrite actions:
+ --
+ -- - Convert JavaScript function to class
+ -- - Add or remove parameter
+ -- - Encapsulate field
+ -- - Make method static
+ -- - Move method to base class
+ -- - ...
+ RefactorRewrite = 'refactor.rewrite';
+ -- Base kind for source actions
+ --
+ -- Source code actions apply to the entire file.
+ Source = 'source';
+ -- Base kind for an organize imports source action
+ SourceOrganizeImports = 'source.organizeImports';
+ };
+}
+
+for k, v in pairs(constants) do
+ vim.tbl_add_reverse_lookup(v)
+ protocol[k] = v
+end
+
+--[=[
+--Text document specific client capabilities.
+export interface TextDocumentClientCapabilities {
+ synchronization?: {
+ --Whether text document synchronization supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports sending will save notifications.
+ willSave?: boolean;
+ --The client supports sending a will save request and
+ --waits for a response providing text edits which will
+ --be applied to the document before it is saved.
+ willSaveWaitUntil?: boolean;
+ --The client supports did save notifications.
+ didSave?: boolean;
+ }
+ --Capabilities specific to the `textDocument/completion`
+ completion?: {
+ --Whether completion supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `CompletionItem` specific
+ --capabilities.
+ completionItem?: {
+ --The client supports snippets as insert text.
+ --
+ --A snippet can define tab stops and placeholders with `$1`, `$2`
+ --and `${3:foo}`. `$0` defines the final tab stop, it defaults to
+ --the end of the snippet. Placeholders with equal identifiers are linked,
+ --that is typing in one will update others too.
+ snippetSupport?: boolean;
+ --The client supports commit characters on a completion item.
+ commitCharactersSupport?: boolean
+ --The client supports the following content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --The client supports the deprecated property on a completion item.
+ deprecatedSupport?: boolean;
+ --The client supports the preselect property on a completion item.
+ preselectSupport?: boolean;
+ }
+ completionItemKind?: {
+ --The completion item kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the completion items kinds from `Text` to `Reference` as defined in
+ --the initial version of the protocol.
+ valueSet?: CompletionItemKind[];
+ },
+ --The client supports to send additional context information for a
+ --`textDocument/completion` request.
+ contextSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/hover`
+ hover?: {
+ --Whether hover supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the follow content formats for the content
+ --property. The order describes the preferred format of the client.
+ contentFormat?: MarkupKind[];
+ };
+ --Capabilities specific to the `textDocument/signatureHelp`
+ signatureHelp?: {
+ --Whether signature help supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports the following `SignatureInformation`
+ --specific properties.
+ signatureInformation?: {
+ --The client supports the follow content formats for the documentation
+ --property. The order describes the preferred format of the client.
+ documentationFormat?: MarkupKind[];
+ --Client capabilities specific to parameter information.
+ parameterInformation?: {
+ --The client supports processing label offsets instead of a
+ --simple label string.
+ --
+ --Since 3.14.0
+ labelOffsetSupport?: boolean;
+ }
+ };
+ };
+ --Capabilities specific to the `textDocument/references`
+ references?: {
+ --Whether references supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentHighlight`
+ documentHighlight?: {
+ --Whether document highlight supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentSymbol`
+ documentSymbol?: {
+ --Whether document symbol supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind`.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ --The client supports hierarchical document symbols.
+ hierarchicalDocumentSymbolSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/formatting`
+ formatting?: {
+ --Whether formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/rangeFormatting`
+ rangeFormatting?: {
+ --Whether range formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/onTypeFormatting`
+ onTypeFormatting?: {
+ --Whether on type formatting supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/declaration`
+ declaration?: {
+ --Whether declaration supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of declaration links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/definition`.
+ --
+ --Since 3.14.0
+ definition?: {
+ --Whether definition supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/typeDefinition`
+ --
+ --Since 3.6.0
+ typeDefinition?: {
+ --Whether typeDefinition supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/implementation`.
+ --
+ --Since 3.6.0
+ implementation?: {
+ --Whether implementation supports dynamic registration. If this is set to `true`
+ --the client supports the new `(TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The client supports additional metadata in the form of definition links.
+ --
+ --Since 3.14.0
+ linkSupport?: boolean;
+ };
+ --Capabilities specific to the `textDocument/codeAction`
+ codeAction?: {
+ --Whether code action supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client support code action literals as a valid
+ --response of the `textDocument/codeAction` request.
+ --
+ --Since 3.8.0
+ codeActionLiteralSupport?: {
+ --The code action kind is support with the following value
+ --set.
+ codeActionKind: {
+ --The code action kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ valueSet: CodeActionKind[];
+ };
+ };
+ };
+ --Capabilities specific to the `textDocument/codeLens`
+ codeLens?: {
+ --Whether code lens supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentLink`
+ documentLink?: {
+ --Whether document link supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `textDocument/documentColor` and the
+ --`textDocument/colorPresentation` request.
+ --
+ --Since 3.6.0
+ colorProvider?: {
+ --Whether colorProvider supports dynamic registration. If this is set to `true`
+ --the client supports the new `(ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ }
+ --Capabilities specific to the `textDocument/rename`
+ rename?: {
+ --Whether rename supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --The client supports testing for validity of rename operations
+ --before execution.
+ prepareSupport?: boolean;
+ };
+ --Capabilities specific to `textDocument/publishDiagnostics`.
+ publishDiagnostics?: {
+ --Whether the clients accepts diagnostics with related information.
+ relatedInformation?: boolean;
+ };
+ --Capabilities specific to `textDocument/foldingRange` requests.
+ --
+ --Since 3.10.0
+ foldingRange?: {
+ --Whether implementation supports dynamic registration for folding range providers. If this is set to `true`
+ --the client supports the new `(FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions)`
+ --return value for the corresponding server capability as well.
+ dynamicRegistration?: boolean;
+ --The maximum number of folding ranges that the client prefers to receive per document. The value serves as a
+ --hint, servers are free to follow the limit.
+ rangeLimit?: number;
+ --If set, the client signals that it only supports folding complete lines. If set, client will
+ --ignore specified `startCharacter` and `endCharacter` properties in a FoldingRange.
+ lineFoldingOnly?: boolean;
+ };
+}
+--]=]
+
+--[=[
+--Workspace specific client capabilities.
+export interface WorkspaceClientCapabilities {
+ --The client supports applying batch edits to the workspace by supporting
+ --the request 'workspace/applyEdit'
+ applyEdit?: boolean;
+ --Capabilities specific to `WorkspaceEdit`s
+ workspaceEdit?: {
+ --The client supports versioned document changes in `WorkspaceEdit`s
+ documentChanges?: boolean;
+ --The resource operations the client supports. Clients should at least
+ --support 'create', 'rename' and 'delete' files and folders.
+ resourceOperations?: ResourceOperationKind[];
+ --The failure handling strategy of a client if applying the workspace edit
+ --fails.
+ failureHandling?: FailureHandlingKind;
+ };
+ --Capabilities specific to the `workspace/didChangeConfiguration` notification.
+ didChangeConfiguration?: {
+ --Did change configuration notification supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/didChangeWatchedFiles` notification.
+ didChangeWatchedFiles?: {
+ --Did change watched files notification supports dynamic registration. Please note
+ --that the current protocol doesn't support static configuration for file changes
+ --from the server side.
+ dynamicRegistration?: boolean;
+ };
+ --Capabilities specific to the `workspace/symbol` request.
+ symbol?: {
+ --Symbol request supports dynamic registration.
+ dynamicRegistration?: boolean;
+ --Specific capabilities for the `SymbolKind` in the `workspace/symbol` request.
+ symbolKind?: {
+ --The symbol kind values the client supports. When this
+ --property exists the client also guarantees that it will
+ --handle values outside its set gracefully and falls back
+ --to a default value when unknown.
+ --
+ --If this property is not present the client only supports
+ --the symbol kinds from `File` to `Array` as defined in
+ --the initial version of the protocol.
+ valueSet?: SymbolKind[];
+ }
+ };
+ --Capabilities specific to the `workspace/executeCommand` request.
+ executeCommand?: {
+ --Execute command supports dynamic registration.
+ dynamicRegistration?: boolean;
+ };
+ --The client has support for workspace folders.
+ --
+ --Since 3.6.0
+ workspaceFolders?: boolean;
+ --The client supports `workspace/configuration` requests.
+ --
+ --Since 3.6.0
+ configuration?: boolean;
+}
+--]=]
+
+--- Gets a new ClientCapabilities object describing the LSP client
+--- capabilities.
+function protocol.make_client_capabilities()
+ return {
+ textDocument = {
+ synchronization = {
+ dynamicRegistration = false;
+
+ -- TODO(ashkan) Send textDocument/willSave before saving (BufWritePre)
+ willSave = false;
+
+ -- TODO(ashkan) Implement textDocument/willSaveWaitUntil
+ willSaveWaitUntil = false;
+
+ -- Send textDocument/didSave after saving (BufWritePost)
+ didSave = true;
+ };
+ codeAction = {
+ dynamicRegistration = false;
+
+ codeActionLiteralSupport = {
+ codeActionKind = {
+ valueSet = {};
+ };
+ };
+ };
+ completion = {
+ dynamicRegistration = false;
+ completionItem = {
+
+ snippetSupport = true;
+ commitCharactersSupport = false;
+ preselectSupport = false;
+ deprecatedSupport = false;
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ completionItemKind = {
+ valueSet = (function()
+ local res = {}
+ for k in pairs(protocol.CompletionItemKind) do
+ if type(k) == 'number' then table.insert(res, k) end
+ end
+ return res
+ end)();
+ };
+
+ -- TODO(tjdevries): Implement this
+ contextSupport = false;
+ };
+ declaration = {
+ linkSupport = true;
+ };
+ definition = {
+ linkSupport = true;
+ };
+ implementation = {
+ linkSupport = true;
+ };
+ typeDefinition = {
+ linkSupport = true;
+ };
+ hover = {
+ dynamicRegistration = false;
+ contentFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ };
+ signatureHelp = {
+ dynamicRegistration = false;
+ signatureInformation = {
+ documentationFormat = { protocol.MarkupKind.Markdown; protocol.MarkupKind.PlainText };
+ -- parameterInformation = {
+ -- labelOffsetSupport = false;
+ -- };
+ };
+ };
+ references = {
+ dynamicRegistration = false;
+ };
+ documentHighlight = {
+ dynamicRegistration = false
+ };
+ documentSymbol = {
+ dynamicRegistration = false;
+ symbolKind = {
+ valueSet = (function()
+ local res = {}
+ for k in pairs(protocol.SymbolKind) do
+ if type(k) == 'number' then table.insert(res, k) end
+ end
+ return res
+ end)();
+ };
+ hierarchicalDocumentSymbolSupport = true;
+ };
+ };
+ workspace = {
+ symbol = {
+ dynamicRegistration = false;
+ symbolKind = {
+ valueSet = (function()
+ local res = {}
+ for k in pairs(protocol.SymbolKind) do
+ if type(k) == 'number' then table.insert(res, k) end
+ end
+ return res
+ end)();
+ };
+ hierarchicalWorkspaceSymbolSupport = true;
+ };
+ applyEdit = true;
+ };
+ callHierarchy = {
+ dynamicRegistration = false;
+ };
+ experimental = nil;
+ }
+end
+
+--[=[
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+--]=]
+
+--[[
+--Static registration options to be returned in the initialize request.
+interface StaticRegistrationOptions {
+ --The id used to register the request. The id can be used to deregister
+ --the request again. See also Registration#id.
+ id?: string;
+}
+
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ language?: string;
+ --A Uri [scheme](#Uri.scheme), like `file` or `untitled`.
+ scheme?: string;
+ --A glob pattern, like `*.{ts,js}`.
+ --
+ --Glob patterns can have the following syntax:
+ --- `*` to match one or more characters in a path segment
+ --- `?` to match on one character in a path segment
+ --- `**` to match any number of path segments, including none
+ --- `{}` to group conditions (e.g. `**​/*.{ts,js}` matches all TypeScript and JavaScript files)
+ --- `[]` to declare a range of characters to match in a path segment (e.g., `example.[0-9]` to match on `example.0`, `example.1`, …)
+ --- `[!...]` to negate a range of characters to match in a path segment (e.g., `example.[!0-9]` to match on `example.a`, `example.b`, but not `example.0`)
+ pattern?: string;
+}
+export type DocumentSelector = DocumentFilter[];
+export interface TextDocumentRegistrationOptions {
+ --A document selector to identify the scope of the registration. If set to null
+ --the document selector provided on the client side will be used.
+ documentSelector: DocumentSelector | null;
+}
+
+--Code Action options.
+export interface CodeActionOptions {
+ --CodeActionKinds that this server may return.
+ --
+ --The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
+ --may list out every specific kind they provide.
+ codeActionKinds?: CodeActionKind[];
+}
+
+interface ServerCapabilities {
+ --Defines how text documents are synced. Is either a detailed structure defining each notification or
+ --for backwards compatibility the TextDocumentSyncKind number. If omitted it defaults to `TextDocumentSyncKind.None`.
+ textDocumentSync?: TextDocumentSyncOptions | number;
+ --The server provides hover support.
+ hoverProvider?: boolean;
+ --The server provides completion support.
+ completionProvider?: CompletionOptions;
+ --The server provides signature help support.
+ signatureHelpProvider?: SignatureHelpOptions;
+ --The server provides goto definition support.
+ definitionProvider?: boolean;
+ --The server provides Goto Type Definition support.
+ --
+ --Since 3.6.0
+ typeDefinitionProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides Goto Implementation support.
+ --
+ --Since 3.6.0
+ implementationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides find references support.
+ referencesProvider?: boolean;
+ --The server provides document highlight support.
+ documentHighlightProvider?: boolean;
+ --The server provides document symbol support.
+ documentSymbolProvider?: boolean;
+ --The server provides workspace symbol support.
+ workspaceSymbolProvider?: boolean;
+ --The server provides code actions. The `CodeActionOptions` return type is only
+ --valid if the client signals code action literal support via the property
+ --`textDocument.codeAction.codeActionLiteralSupport`.
+ codeActionProvider?: boolean | CodeActionOptions;
+ --The server provides code lens.
+ codeLensProvider?: CodeLensOptions;
+ --The server provides document formatting.
+ documentFormattingProvider?: boolean;
+ --The server provides document range formatting.
+ documentRangeFormattingProvider?: boolean;
+ --The server provides document formatting on typing.
+ documentOnTypeFormattingProvider?: DocumentOnTypeFormattingOptions;
+ --The server provides rename support. RenameOptions may only be
+ --specified if the client states that it supports
+ --`prepareSupport` in its initial `initialize` request.
+ renameProvider?: boolean | RenameOptions;
+ --The server provides document link support.
+ documentLinkProvider?: DocumentLinkOptions;
+ --The server provides color provider support.
+ --
+ --Since 3.6.0
+ colorProvider?: boolean | ColorProviderOptions | (ColorProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides folding provider support.
+ --
+ --Since 3.10.0
+ foldingRangeProvider?: boolean | FoldingRangeProviderOptions | (FoldingRangeProviderOptions & TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides go to declaration support.
+ --
+ --Since 3.14.0
+ declarationProvider?: boolean | (TextDocumentRegistrationOptions & StaticRegistrationOptions);
+ --The server provides execute command support.
+ executeCommandProvider?: ExecuteCommandOptions;
+ --Workspace specific server capabilities
+ workspace?: {
+ --The server supports workspace folder.
+ --
+ --Since 3.6.0
+ workspaceFolders?: {
+ * The server has support for workspace folders
+ supported?: boolean;
+ * Whether the server wants to receive workspace folder
+ * change notifications.
+ *
+ * If a strings is provided the string is treated as a ID
+ * under which the notification is registered on the client
+ * side. The ID can be used to unregister for these events
+ * using the `client/unregisterCapability` request.
+ changeNotifications?: string | boolean;
+ }
+ }
+ --Experimental server capabilities.
+ experimental?: any;
+}
+--]]
+
+--- Creates a normalized object describing LSP server capabilities.
+function protocol.resolve_capabilities(server_capabilities)
+ local general_properties = {}
+ local text_document_sync_properties
+ do
+ local TextDocumentSyncKind = protocol.TextDocumentSyncKind
+ local textDocumentSync = server_capabilities.textDocumentSync
+ if textDocumentSync == nil then
+ -- Defaults if omitted.
+ text_document_sync_properties = {
+ text_document_open_close = false;
+ text_document_did_change = TextDocumentSyncKind.None;
+-- text_document_did_change = false;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'number' then
+ -- Backwards compatibility
+ if not TextDocumentSyncKind[textDocumentSync] then
+ return nil, "Invalid server TextDocumentSyncKind for textDocumentSync"
+ end
+ text_document_sync_properties = {
+ text_document_open_close = true;
+ text_document_did_change = textDocumentSync;
+ text_document_will_save = false;
+ text_document_will_save_wait_until = false;
+ text_document_save = false;
+ text_document_save_include_text = false;
+ }
+ elseif type(textDocumentSync) == 'table' then
+ text_document_sync_properties = {
+ text_document_open_close = ifnil(textDocumentSync.openClose, false);
+ text_document_did_change = ifnil(textDocumentSync.change, TextDocumentSyncKind.None);
+ text_document_will_save = ifnil(textDocumentSync.willSave, false);
+ text_document_will_save_wait_until = ifnil(textDocumentSync.willSaveWaitUntil, false);
+ text_document_save = ifnil(textDocumentSync.save, false);
+ text_document_save_include_text = ifnil(type(textDocumentSync.save) == 'table'
+ and textDocumentSync.save.includeText, false);
+ }
+ else
+ return nil, string.format("Invalid type for textDocumentSync: %q", type(textDocumentSync))
+ end
+ end
+ 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
+ general_properties.document_highlight = server_capabilities.documentHighlightProvider or false
+ general_properties.document_symbol = server_capabilities.documentSymbolProvider or false
+ general_properties.workspace_symbol = server_capabilities.workspaceSymbolProvider or false
+ 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
+
+ if server_capabilities.codeActionProvider == nil then
+ general_properties.code_action = false
+ elseif type(server_capabilities.codeActionProvider) == 'boolean' then
+ general_properties.code_action = server_capabilities.codeActionProvider
+ elseif type(server_capabilities.codeActionProvider) == 'table' then
+ -- TODO(ashkan) support CodeActionKind
+ general_properties.code_action = false
+ else
+ error("The server sent invalid codeActionProvider")
+ end
+
+ if server_capabilities.declarationProvider == nil then
+ general_properties.declaration = false
+ elseif type(server_capabilities.declarationProvider) == 'boolean' then
+ general_properties.declaration = server_capabilities.declarationProvider
+ elseif type(server_capabilities.declarationProvider) == 'table' then
+ -- TODO: support more detailed declarationProvider options.
+ general_properties.declaration = false
+ else
+ error("The server sent invalid declarationProvider")
+ end
+
+ if server_capabilities.typeDefinitionProvider == nil then
+ general_properties.type_definition = false
+ elseif type(server_capabilities.typeDefinitionProvider) == 'boolean' then
+ general_properties.type_definition = server_capabilities.typeDefinitionProvider
+ elseif type(server_capabilities.typeDefinitionProvider) == 'table' then
+ -- TODO: support more detailed typeDefinitionProvider options.
+ general_properties.type_definition = false
+ else
+ error("The server sent invalid typeDefinitionProvider")
+ end
+
+ if server_capabilities.implementationProvider == nil then
+ general_properties.implementation = false
+ elseif type(server_capabilities.implementationProvider) == 'boolean' then
+ general_properties.implementation = server_capabilities.implementationProvider
+ elseif type(server_capabilities.implementationProvider) == 'table' then
+ -- TODO(ashkan) support more detailed implementation options.
+ general_properties.implementation = false
+ else
+ error("The server sent invalid implementationProvider")
+ end
+
+ local signature_help_properties
+ if server_capabilities.signatureHelpProvider == nil then
+ signature_help_properties = {
+ signature_help = false;
+ signature_help_trigger_characters = {};
+ }
+ elseif type(server_capabilities.signatureHelpProvider) == 'table' then
+ signature_help_properties = {
+ signature_help = true;
+ -- The characters that trigger signature help automatically.
+ signature_help_trigger_characters = server_capabilities.signatureHelpProvider.triggerCharacters or {};
+ }
+ else
+ error("The server sent invalid signatureHelpProvider")
+ end
+
+ return vim.tbl_extend("error"
+ , text_document_sync_properties
+ , signature_help_properties
+ , general_properties
+ )
+end
+
+return protocol
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/rpc.lua b/runtime/lua/vim/lsp/rpc.lua
new file mode 100644
index 0000000000..81c92bfe05
--- /dev/null
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -0,0 +1,472 @@
+local vim = vim
+local uv = vim.loop
+local log = require('vim.lsp.log')
+local protocol = require('vim.lsp.protocol')
+local validate, schedule, schedule_wrap = vim.validate, vim.schedule, vim.schedule_wrap
+
+-- TODO replace with a better implementation.
+local function json_encode(data)
+ local status, result = pcall(vim.fn.json_encode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+local function json_decode(data)
+ local status, result = pcall(vim.fn.json_decode, data)
+ if status then
+ return result
+ else
+ return nil, result
+ end
+end
+
+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
+local function convert_NIL(v)
+ if v == NIL then return nil end
+ return v
+end
+
+--- Merges current process env with the given env and returns the result as
+--- a list of "k=v" strings.
+---
+--- <pre>
+--- Example:
+---
+--- 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>
+local function env_merge(env)
+ if env == nil then
+ return env
+ end
+ -- Merge.
+ env = vim.tbl_extend('force', vim.fn.environ(), env)
+ local final_env = {}
+ for k,v in pairs(env) do
+ assert(type(k) == 'string', 'env must be a dict')
+ table.insert(final_env, k..'='..tostring(v))
+ end
+ return final_env
+end
+
+local function format_message_with_content_length(encoded_message)
+ return table.concat {
+ 'Content-Length: '; tostring(#encoded_message); '\r\n\r\n';
+ encoded_message;
+ }
+end
+
+--- Parse an LSP Message's header
+-- @param header: The header to parse.
+local function parse_headers(header)
+ if type(header) ~= 'string' then
+ return nil
+ end
+ local headers = {}
+ for line in vim.gsplit(header, '\r\n', true) do
+ if line == '' then
+ break
+ end
+ local key, value = line:match("^%s*(%S+)%s*:%s*(.+)%s*$")
+ if key then
+ key = key:lower():gsub('%-', '_')
+ headers[key] = value
+ else
+ local _ = log.error() and log.error("invalid header line %q", line)
+ error(string.format("invalid header line %q", line))
+ end
+ end
+ headers.content_length = tonumber(headers.content_length)
+ or error(string.format("Content-Length not found in headers. %q", header))
+ return headers
+end
+
+-- This is the start of any possible header patterns. The gsub converts it to a
+-- case insensitive pattern.
+local header_start_pattern = ("content"):gsub("%w", function(c) return "["..c..c:upper().."]" end)
+
+local function request_parser_loop()
+ local buffer = ''
+ while true do
+ -- A message can only be complete if it has a double CRLF and also the full
+ -- payload, so first let's check for the CRLFs
+ local start, finish = buffer:find('\r\n\r\n', 1, true)
+ -- Start parsing the headers
+ if start then
+ -- This is a workaround for servers sending initial garbage before
+ -- sending headers, such as if a bash script sends stdout. It assumes
+ -- that we know all of the headers ahead of time. At this moment, the
+ -- only valid headers start with "Content-*", so that's the thing we will
+ -- be searching for.
+ -- TODO(ashkan) I'd like to remove this, but it seems permanent :(
+ local buffer_start = buffer:find(header_start_pattern)
+ local headers = parse_headers(buffer:sub(buffer_start, start-1))
+ buffer = buffer:sub(finish+1)
+ local content_length = headers.content_length
+ -- Keep waiting for data until we have enough.
+ while #buffer < content_length do
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ end
+ local body = buffer:sub(1, content_length)
+ buffer = buffer:sub(content_length + 1)
+ -- Yield our data.
+ buffer = buffer..(coroutine.yield(headers, body)
+ or error("Expected more data for the body. The server may have died.")) -- TODO hmm.
+ else
+ -- Get more data since we don't have enough.
+ buffer = buffer..(coroutine.yield()
+ or error("Expected more data for the header. The server may have died.")) -- TODO hmm.
+ end
+ end
+end
+
+local client_errors = vim.tbl_add_reverse_lookup {
+ INVALID_SERVER_MESSAGE = 1;
+ INVALID_SERVER_JSON = 2;
+ NO_RESULT_CALLBACK_FOUND = 3;
+ READ_ERROR = 4;
+ NOTIFICATION_HANDLER_ERROR = 5;
+ SERVER_REQUEST_HANDLER_ERROR = 6;
+ SERVER_RESULT_CALLBACK_ERROR = 7;
+}
+
+local function format_rpc_error(err)
+ validate {
+ err = { err, 't' };
+ }
+
+ -- There is ErrorCodes in the LSP specification,
+ -- but in ResponseError.code it is not used and the actual type is number.
+ local code
+ if protocol.ErrorCodes[err.code] then
+ code = string.format("code_name = %s,", protocol.ErrorCodes[err.code])
+ else
+ code = string.format("code_name = unknown, code = %s,", err.code)
+ end
+
+ local message_parts = {"RPC[Error]", code}
+ if err.message then
+ table.insert(message_parts, "message =")
+ table.insert(message_parts, string.format("%q", err.message))
+ end
+ if err.data then
+ table.insert(message_parts, "data =")
+ table.insert(message_parts, vim.inspect(err.data))
+ end
+ return table.concat(message_parts, ' ')
+end
+
+--- Creates an RPC response object/table.
+---
+--@param code RPC error code defined in `vim.lsp.protocol.ErrorCodes`
+--@param message (optional) arbitrary message to send to server
+--@param data (optional) arbitrary data to send to server
+local function rpc_response_error(code, message, data)
+ -- TODO should this error or just pick a sane error (like InternalError)?
+ local code_name = assert(protocol.ErrorCodes[code], 'Invalid RPC error code')
+ return setmetatable({
+ code = code;
+ message = message or code_name;
+ data = data;
+ }, {
+ __tostring = format_rpc_error;
+ })
+end
+
+local default_handlers = {}
+function default_handlers.notification(method, params)
+ local _ = log.debug() and log.debug('notification', method, params)
+end
+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
+function default_handlers.on_exit(code, signal)
+ local _ = log.info() and log.info("client exit", { code = code, signal = signal })
+end
+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)
+ local _ = log.info() and log.info("Starting RPC client", {cmd = cmd, args = cmd_args, extra = extra_spawn_params})
+ validate {
+ cmd = { cmd, 's' };
+ cmd_args = { cmd_args, 't' };
+ handlers = { handlers, 't', true };
+ }
+
+ if not (vim.fn.executable(cmd) == 1) then
+ error(string.format("The given command %q is not executable.", cmd))
+ end
+ if handlers then
+ local user_handlers = handlers
+ handlers = {}
+ for handle_name, default_handler in pairs(default_handlers) do
+ local user_handler = user_handlers[handle_name]
+ if user_handler then
+ if type(user_handler) ~= 'function' then
+ error(string.format("handler.%s must be a function", handle_name))
+ end
+ -- server_request is wrapped elsewhere.
+ if not (handle_name == 'server_request'
+ or handle_name == 'on_exit') -- TODO this blocks the loop exiting for some reason.
+ then
+ user_handler = schedule_wrap(user_handler)
+ end
+ handlers[handle_name] = user_handler
+ else
+ handlers[handle_name] = default_handler
+ end
+ end
+ else
+ handlers = default_handlers
+ end
+
+ local stdin = uv.new_pipe(false)
+ local stdout = uv.new_pipe(false)
+ local stderr = uv.new_pipe(false)
+
+ local message_index = 0
+ local message_callbacks = {}
+
+ local handle, pid
+ do
+ local function onexit(code, signal)
+ stdin:close()
+ stdout:close()
+ stderr:close()
+ handle:close()
+ -- Make sure that message_callbacks can be gc'd.
+ message_callbacks = nil
+ handlers.on_exit(code, signal)
+ end
+ local spawn_params = {
+ args = cmd_args;
+ stdio = {stdin, stdout, stderr};
+ }
+ if extra_spawn_params then
+ spawn_params.cwd = extra_spawn_params.cwd
+ if spawn_params.cwd then
+ assert(is_dir(spawn_params.cwd), "cwd must be a directory")
+ end
+ spawn_params.env = env_merge(extra_spawn_params.env)
+ end
+ handle, pid = uv.spawn(cmd, spawn_params, onexit)
+ end
+
+ local function encode_and_send(payload)
+ local _ = log.debug() and log.debug("rpc.send.payload", payload)
+ if handle:is_closing() then return false end
+ -- TODO(ashkan) remove this once we have a Lua json_encode
+ schedule(function()
+ local encoded = assert(json_encode(payload))
+ stdin:write(format_message_with_content_length(encoded))
+ end)
+ return true
+ end
+
+ local function send_notification(method, params)
+ local _ = log.debug() and log.debug("rpc.notify", method, params)
+ return encode_and_send {
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ end
+
+ local function send_response(request_id, err, result)
+ return encode_and_send {
+ id = request_id;
+ jsonrpc = "2.0";
+ error = err;
+ result = result;
+ }
+ end
+
+ local function send_request(method, params, callback)
+ validate {
+ callback = { callback, 'f' };
+ }
+ message_index = message_index + 1
+ local message_id = message_index
+ local result = encode_and_send {
+ id = message_id;
+ jsonrpc = "2.0";
+ method = method;
+ params = params;
+ }
+ if result then
+ message_callbacks[message_id] = schedule_wrap(callback)
+ return result, message_id
+ else
+ return false
+ end
+ end
+
+ stderr:read_start(function(_err, chunk)
+ if chunk then
+ local _ = log.error() and log.error("rpc", cmd, "stderr", chunk)
+ end
+ end)
+
+ local function on_error(errkind, ...)
+ assert(client_errors[errkind])
+ -- TODO what to do if this fails?
+ pcall(handlers.on_error, errkind, ...)
+ end
+ local function pcall_handler(errkind, status, head, ...)
+ if not status then
+ on_error(errkind, head, ...)
+ return status, head
+ end
+ return status, head, ...
+ end
+ local function try_call(errkind, fn, ...)
+ return pcall_handler(errkind, pcall(fn, ...))
+ end
+
+ -- TODO periodically check message_callbacks for old requests past a certain
+ -- time and log them. This would require storing the timestamp. I could call
+ -- them with an error then, perhaps.
+
+ local function handle_body(body)
+ local decoded, err = json_decode(body)
+ if not decoded then
+ on_error(client_errors.INVALID_SERVER_JSON, err)
+ return
+ end
+ local _ = log.debug() and log.debug("decoded", decoded)
+
+ if type(decoded.method) == 'string' and decoded.id then
+ -- Server Request
+ decoded.params = convert_NIL(decoded.params)
+ -- Schedule here so that the users functions don't trigger an error and
+ -- we can still use the result.
+ schedule(function()
+ local status, result
+ status, result, err = try_call(client_errors.SERVER_REQUEST_HANDLER_ERROR,
+ handlers.server_request, decoded.method, decoded.params)
+ local _ = log.debug() and log.debug("server_request: callback result", { status = status, result = result, err = err })
+ if status then
+ if not (result or err) then
+ -- TODO this can be a problem if `null` is sent for result. needs vim.NIL
+ error(string.format("method %q: either a result or an error must be sent to the server in response", decoded.method))
+ end
+ if err then
+ assert(type(err) == 'table', "err must be a table. Use rpc_response_error to help format errors.")
+ local code_name = assert(protocol.ErrorCodes[err.code], "Errors must use protocol.ErrorCodes. Use rpc_response_error to help format errors.")
+ err.message = err.message or code_name
+ end
+ else
+ -- On an exception, result will contain the error message.
+ err = rpc_response_error(protocol.ErrorCodes.InternalError, result)
+ result = nil
+ end
+ send_response(decoded.id, err, result)
+ end)
+ -- This works because we are expecting vim.NIL here
+ elseif decoded.id and (decoded.result or decoded.error) then
+ -- Server Result
+ 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)
+ local result_id = tonumber(decoded.id)
+ -- Clear any callback since this is cancelled now.
+ -- This is safe to do assuming that these conditions hold:
+ -- - The server will not send a result callback after this cancellation.
+ -- - If the server sent this cancellation ACK after sending the result, the user of this RPC
+ -- client will ignore the result themselves.
+ if result_id then
+ message_callbacks[result_id] = nil
+ end
+ return
+ end
+
+ -- We sent a number, so we expect a number.
+ local result_id = tonumber(decoded.id)
+ local callback = message_callbacks[result_id]
+ if callback then
+ message_callbacks[result_id] = nil
+ validate {
+ callback = { callback, 'f' };
+ }
+ if decoded.error then
+ decoded.error = setmetatable(decoded.error, {
+ __tostring = format_rpc_error;
+ })
+ end
+ try_call(client_errors.SERVER_RESULT_CALLBACK_ERROR,
+ callback, decoded.error, decoded.result)
+ else
+ on_error(client_errors.NO_RESULT_CALLBACK_FOUND, decoded)
+ local _ = log.error() and log.error("No callback found for server response id "..result_id)
+ end
+ elseif type(decoded.method) == 'string' then
+ -- Notification
+ decoded.params = convert_NIL(decoded.params)
+ try_call(client_errors.NOTIFICATION_HANDLER_ERROR,
+ handlers.notification, decoded.method, decoded.params)
+ else
+ -- Invalid server message
+ on_error(client_errors.INVALID_SERVER_MESSAGE, decoded)
+ end
+ end
+ -- TODO(ashkan) remove this once we have a Lua json_decode
+ handle_body = schedule_wrap(handle_body)
+
+ local request_parser = coroutine.wrap(request_parser_loop)
+ request_parser()
+ stdout:read_start(function(err, chunk)
+ if err then
+ -- TODO better handling. Can these be intermittent errors?
+ on_error(client_errors.READ_ERROR, err)
+ return
+ end
+ -- This should signal that we are done reading from the client.
+ if not chunk then return end
+ -- Flush anything in the parser by looping until we don't get a result
+ -- anymore.
+ while true do
+ local headers, body = request_parser(chunk)
+ -- If we successfully parsed, then handle the response.
+ if headers then
+ handle_body(body)
+ -- Set chunk to empty so that we can call request_parser to get
+ -- anything existing in the parser to flush.
+ chunk = ''
+ else
+ break
+ end
+ end
+ end)
+
+ return {
+ pid = pid;
+ handle = handle;
+ request = send_request;
+ notify = send_notification;
+ }
+end
+
+return {
+ start = create_and_start_client;
+ rpc_response_error = rpc_response_error;
+ format_rpc_error = format_rpc_error;
+ client_errors = client_errors;
+}
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/util.lua b/runtime/lua/vim/lsp/util.lua
new file mode 100644
index 0000000000..d286f28d0c
--- /dev/null
+++ b/runtime/lua/vim/lsp/util.lua
@@ -0,0 +1,1360 @@
+local protocol = require 'vim.lsp.protocol'
+local vim = vim
+local validate = vim.validate
+local api = vim.api
+local list_extend = vim.list_extend
+local highlight = require 'vim.highlight'
+
+local M = {}
+
+--- Diagnostics received from the server via `textDocument/publishDiagnostics`
+-- by buffer.
+--
+-- {<bufnr>: {diagnostics}}
+--
+-- This contains only entries for active buffers. Entries for detached buffers
+-- are discarded.
+--
+-- If you override the `textDocument/publishDiagnostic` callback,
+-- this will be empty unless you call `buf_diagnostics_save_positions`.
+--
+--
+-- Diagnostic is:
+--
+-- {
+-- range: Range
+-- message: string
+-- severity?: DiagnosticSeverity
+-- code?: number | string
+-- source?: string
+-- tags?: DiagnosticTag[]
+-- relatedInformation?: DiagnosticRelatedInformation[]
+-- }
+M.diagnostics_by_buf = {}
+
+local split = vim.split
+local function split_lines(value)
+ return split(value, '\n', true)
+end
+
+local function ok_or_nil(status, ...)
+ if not status then return end
+ return ...
+end
+local function npcall(fn, ...)
+ return ok_or_nil(pcall(fn, ...))
+end
+
+function M.set_lines(lines, A, B, new_lines)
+ -- 0-indexing to 1-indexing
+ local i_0 = A[1] + 1
+ -- If it extends past the end, truncate it to the end. This is because the
+ -- way the LSP describes the range including the last newline is by
+ -- specifying a line number after what we would call the last line.
+ local i_n = math.min(B[1] + 1, #lines)
+ if not (i_0 >= 1 and i_0 <= #lines and i_n >= 1 and i_n <= #lines) then
+ error("Invalid range: "..vim.inspect{A = A; B = B; #lines, new_lines})
+ end
+ local prefix = ""
+ local suffix = lines[i_n]:sub(B[2]+1)
+ if A[2] > 0 then
+ prefix = lines[i_0]:sub(1, A[2])
+ end
+ local n = i_n - i_0 + 1
+ if n ~= #new_lines then
+ for _ = 1, n - #new_lines do table.remove(lines, i_0) end
+ for _ = 1, #new_lines - n do table.insert(lines, i_0, '') end
+ end
+ for i = 1, #new_lines do
+ lines[i - 1 + i_0] = new_lines[i]
+ end
+ if #suffix > 0 then
+ local i = i_0 + #new_lines - 1
+ lines[i] = lines[i]..suffix
+ end
+ if #prefix > 0 then
+ lines[i_0] = prefix..lines[i_0]
+ end
+ return lines
+end
+
+local function sort_by_key(fn)
+ return function(a,b)
+ local ka, kb = fn(a), fn(b)
+ assert(#ka == #kb)
+ for i = 1, #ka do
+ if ka[i] ~= kb[i] then
+ return ka[i] < kb[i]
+ end
+ end
+ -- every value must have been equal here, which means it's not less than.
+ return false
+ end
+end
+local edit_sort_key = sort_by_key(function(e)
+ return {e.A[1], e.A[2], e.i}
+end)
+
+--- Position is a https://microsoft.github.io/language-server-protocol/specifications/specification-current/#position
+-- Returns a zero-indexed column, since set_lines() does the conversion to
+-- 1-indexed
+local function get_line_byte_from_position(bufnr, position)
+ -- LSP's line and characters are 0-indexed
+ -- Vim's line and columns are 1-indexed
+ local col = position.character
+ -- When on the first character, we can ignore the difference between byte and
+ -- character
+ if col > 0 then
+ local line = position.line
+ local lines = api.nvim_buf_get_lines(bufnr, line, line + 1, false)
+ if #lines > 0 then
+ return vim.str_byteindex(lines[1], col)
+ end
+ end
+ return col
+end
+
+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
+ vim.fn.bufload(bufnr)
+ end
+ api.nvim_buf_set_option(bufnr, 'buflisted', true)
+ local start_line, finish_line = math.huge, -1
+ local cleaned = {}
+ for i, e in ipairs(text_edits) do
+ -- adjust start and end column for UTF-16 encoding of non-ASCII characters
+ local start_row = e.range.start.line
+ local start_col = get_line_byte_from_position(bufnr, e.range.start)
+ local end_row = e.range["end"].line
+ local end_col = get_line_byte_from_position(bufnr, e.range['end'])
+ start_line = math.min(e.range.start.line, start_line)
+ finish_line = math.max(e.range["end"].line, finish_line)
+ -- TODO(ashkan) sanity check ranges for overlap.
+ table.insert(cleaned, {
+ i = i;
+ A = {start_row; start_col};
+ B = {end_row; end_col};
+ lines = vim.split(e.newText, '\n', true);
+ })
+ end
+
+ -- Reverse sort the orders so we can apply them without interfering with
+ -- eachother. Also add i as a sort key to mimic a stable sort.
+ table.sort(cleaned, edit_sort_key)
+ local lines = api.nvim_buf_get_lines(bufnr, start_line, finish_line + 1, false)
+ local fix_eol = api.nvim_buf_get_option(bufnr, 'fixeol')
+ local set_eol = fix_eol and api.nvim_buf_line_count(bufnr) <= finish_line + 1
+ if set_eol and #lines[#lines] ~= 0 then
+ table.insert(lines, '')
+ end
+
+ for i = #cleaned, 1, -1 do
+ local e = cleaned[i]
+ local A = {e.A[1] - start_line, e.A[2]}
+ local B = {e.B[1] - start_line, e.B[2]}
+ lines = M.set_lines(lines, A, B, e.lines)
+ end
+ if set_eol and #lines[#lines] == 0 then
+ table.remove(lines)
+ end
+ api.nvim_buf_set_lines(bufnr, start_line, finish_line + 1, false, lines)
+end
+
+-- local valid_windows_path_characters = "[^<>:\"/\\|?*]"
+-- local valid_unix_path_characters = "[^/]"
+-- https://github.com/davidm/lua-glob-pattern
+-- https://stackoverflow.com/questions/1976007/what-characters-are-forbidden-in-windows-and-linux-directory-names
+-- 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
+function M.extract_completion_items(result)
+ if type(result) == 'table' and result.items then
+ return result.items
+ elseif result ~= nil then
+ return result
+ else
+ return {}
+ end
+end
+
+--- Apply the TextDocumentEdit response.
+-- @params TextDocumentEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+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)
+ if text_document.version then
+ -- `VersionedTextDocumentIdentifier`s version may be null https://microsoft.github.io/language-server-protocol/specification#versionedTextDocumentIdentifier
+ if text_document.version ~= vim.NIL and M.buf_versions[bufnr] ~= nil and M.buf_versions[bufnr] > text_document.version then
+ print("Buffer ", text_document.uri, " newer than edits.")
+ return
+ end
+ end
+ M.apply_text_edits(text_document_edit.edits, bufnr)
+end
+
+function M.get_current_line_to_cursor()
+ local pos = api.nvim_win_get_cursor(0)
+ local line = assert(api.nvim_buf_get_lines(0, pos[1]-1, pos[1], false)[1])
+ return line:sub(pos[2]+1)
+end
+
+local function parse_snippet_rec(input, inner)
+ local res = ""
+
+ local close, closeend = nil, nil
+ if inner then
+ close, closeend = input:find("}", 1, true)
+ while close ~= nil and input:sub(close-1,close-1) == "\\" do
+ close, closeend = input:find("}", closeend+1, true)
+ end
+ end
+
+ local didx = input:find('$', 1, true)
+ if didx == nil and close == nil then
+ return input, ""
+ elseif close ~=nil and (didx == nil or close < didx) then
+ -- No inner placeholders
+ return input:sub(0, close-1), input:sub(closeend+1)
+ end
+
+ res = res .. input:sub(0, didx-1)
+ input = input:sub(didx+1)
+
+ local tabstop, tabstopend = input:find('^%d+')
+ local placeholder, placeholderend = input:find('^{%d+:')
+ local choice, choiceend = input:find('^{%d+|')
+
+ if tabstop then
+ input = input:sub(tabstopend+1)
+ elseif choice then
+ input = input:sub(choiceend+1)
+ close, closeend = input:find("|}", 1, true)
+
+ res = res .. input:sub(0, close-1)
+ input = input:sub(closeend+1)
+ elseif placeholder then
+ -- TODO: add support for variables
+ input = input:sub(placeholderend+1)
+
+ -- placeholders and variables are recursive
+ while input ~= "" do
+ local r, tail = parse_snippet_rec(input, true)
+ r = r:gsub("\\}", "}")
+
+ res = res .. r
+ input = tail
+ end
+ else
+ res = res .. "$"
+ end
+
+ return res, input
+end
+
+-- Parse completion entries, consuming snippet tokens
+function M.parse_snippet(input)
+ local res, _ = parse_snippet_rec(input, false)
+
+ return res
+end
+
+-- Sort by CompletionItem.sortText
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-current/#textDocument_completion
+local function sort_completion_items(items)
+ if items[1] and items[1].sortText then
+ table.sort(items, function(a, b) return a.sortText < b.sortText
+ end)
+ 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
+local function get_completion_word(item)
+ if item.textEdit ~= nil and item.textEdit.newText ~= nil then
+ if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ return item.textEdit.newText
+ else
+ return M.parse_snippet(item.textEdit.newText)
+ end
+ elseif item.insertText ~= nil then
+ if protocol.InsertTextFormat[item.insertTextFormat] == "PlainText" then
+ return item.insertText
+ else
+ return M.parse_snippet(item.insertText)
+ end
+ end
+ 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.
+local function remove_unmatch_completion_items(items, prefix)
+ return vim.tbl_filter(function(item)
+ local word = get_completion_word(item)
+ return vim.startswith(word, 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
+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 }
+function M.text_document_completion_list_to_complete_items(result, prefix)
+ local items = M.extract_completion_items(result)
+ if vim.tbl_isempty(items) then
+ return {}
+ end
+
+ items = remove_unmatch_completion_items(items, prefix)
+ sort_completion_items(items)
+
+ local matches = {}
+
+ for _, completion_item in ipairs(items) do
+ local info = ' '
+ local documentation = completion_item.documentation
+ if documentation then
+ if type(documentation) == 'string' and documentation ~= '' then
+ info = documentation
+ elseif type(documentation) == 'table' and type(documentation.value) == 'string' then
+ info = documentation.value
+ -- else
+ -- TODO(ashkan) Validation handling here?
+ end
+ end
+
+ local word = get_completion_word(completion_item)
+ table.insert(matches, {
+ word = word,
+ abbr = completion_item.label,
+ kind = M._get_completion_item_kind_name(completion_item.kind),
+ menu = completion_item.detail or '',
+ info = info,
+ icase = 1,
+ dup = 1,
+ empty = 1,
+ user_data = {
+ nvim = {
+ lsp = {
+ completion_item = completion_item
+ }
+ }
+ },
+ })
+ end
+
+ return matches
+end
+
+-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.apply_workspace_edit(workspace_edit)
+ if workspace_edit.documentChanges then
+ for _, change in ipairs(workspace_edit.documentChanges) do
+ if change.kind then
+ -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
+ error(string.format("Unsupported change: %q", vim.inspect(change)))
+ else
+ M.apply_text_document_edit(change)
+ end
+ end
+ return
+ end
+
+ local all_changes = workspace_edit.changes
+ if not (all_changes and not vim.tbl_isempty(all_changes)) then
+ return
+ end
+
+ for uri, changes in pairs(all_changes) do
+ local bufnr = vim.uri_to_bufnr(uri)
+ M.apply_text_edits(changes, bufnr)
+ 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.
+function M.convert_input_to_markdown_lines(input, contents)
+ contents = contents or {}
+ -- MarkedString variation 1
+ if type(input) == 'string' then
+ list_extend(contents, split_lines(input))
+ else
+ assert(type(input) == 'table', "Expected a table for Hover.contents")
+ -- MarkupContent
+ if input.kind then
+ -- The kind can be either plaintext or markdown. However, either way we
+ -- will just be rendering markdown, so we handle them both the same way.
+ -- TODO these can have escaped/sanitized html codes in markdown. We
+ -- should make sure we handle this correctly.
+
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ list_extend(contents, split_lines(input.value or ''))
+ -- MarkupString variation 2
+ elseif input.language then
+ -- Some servers send input.value as empty, so let's ignore this :(
+ -- assert(type(input.value) == 'string')
+ table.insert(contents, "```"..input.language)
+ list_extend(contents, split_lines(input.value or ''))
+ table.insert(contents, "```")
+ -- By deduction, this must be MarkedString[]
+ else
+ -- Use our existing logic to handle MarkedString
+ for _, marked_string in ipairs(input) do
+ M.convert_input_to_markdown_lines(marked_string, contents)
+ end
+ end
+ end
+ if (contents[1] == '' or contents[1] == nil) and #contents == 1 then
+ return {}
+ end
+ return contents
+end
+
+--- Convert SignatureHelp response to markdown lines.
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
+function M.convert_signature_help_to_markdown_lines(signature_help)
+ if not signature_help.signatures then
+ return
+ end
+ --The active signature. If omitted or the value lies outside the range of
+ --`signatures` the value defaults to zero or is ignored if `signatures.length
+ --=== 0`. Whenever possible implementors should make an active decision about
+ --the active signature and shouldn't rely on a default value.
+ local contents = {}
+ local active_signature = signature_help.activeSignature or 0
+ -- If the activeSignature is not inside the valid range, then clip it.
+ if active_signature >= #signature_help.signatures then
+ active_signature = 0
+ end
+ local signature = signature_help.signatures[active_signature + 1]
+ if not signature then
+ return
+ end
+ vim.list_extend(contents, vim.split(signature.label, '\n', true))
+ if signature.documentation then
+ M.convert_input_to_markdown_lines(signature.documentation, contents)
+ end
+ if signature_help.parameters 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
+ active_parameter = 0
+ end
+ local parameter = signature.parameters and signature.parameters[active_parameter]
+ if parameter then
+ --[=[
+ --Represents a parameter of a callable-signature. A parameter can
+ --have a label and a doc-comment.
+ interface ParameterInformation {
+ --The label of this parameter information.
+ --
+ --Either a string or an inclusive start and exclusive end offsets within its containing
+ --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
+ --string representation as `Position` and `Range` does.
+ --
+ --*Note*: a label of type string should be a substring of its containing signature label.
+ --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
+ label: string | [number, number];
+ --The human-readable doc-comment of this parameter. Will be shown
+ --in the UI but can be omitted.
+ documentation?: string | MarkupContent;
+ }
+ --]=]
+ -- TODO highlight parameter
+ if parameter.documentation then
+ M.convert_input_help_to_markdown_lines(parameter.documentation, contents)
+ end
+ end
+ end
+ return contents
+end
+
+function M.make_floating_popup_options(width, height, opts)
+ validate {
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+ validate {
+ ["opts.offset_x"] = { opts.offset_x, 'n', true };
+ ["opts.offset_y"] = { opts.offset_y, 'n', true };
+ }
+
+ local anchor = ''
+ local row, col
+
+ local lines_above = vim.fn.winline() - 1
+ local lines_below = vim.fn.winheight(0) - lines_above
+
+ if lines_above < lines_below then
+ anchor = anchor..'N'
+ height = math.min(lines_below, height)
+ row = 1
+ else
+ anchor = anchor..'S'
+ height = math.min(lines_above, height)
+ row = 0
+ end
+
+ if vim.fn.wincol() + width <= api.nvim_get_option('columns') then
+ anchor = anchor..'W'
+ col = 0
+ else
+ anchor = anchor..'E'
+ col = 1
+ end
+
+ return {
+ anchor = anchor,
+ col = col + (opts.offset_x or 0),
+ height = height,
+ relative = 'cursor',
+ row = row + (opts.offset_y or 0),
+ style = 'minimal',
+ width = width,
+ }
+end
+
+function M.jump_to_location(location)
+ -- location may be Location or LocationLink
+ local uri = location.uri or location.targetUri
+ if uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(uri)
+ -- Save position in jumplist
+ vim.cmd "normal! m'"
+
+ -- Push a new item into tagstack
+ local from = {vim.fn.bufnr('%'), vim.fn.line('.'), vim.fn.col('.'), 0}
+ local items = {{tagname=vim.fn.expand('<cword>'), from=from}}
+ vim.fn.settagstack(vim.fn.win_getid(), {items=items}, 't')
+
+ --- Jump to new location (adjusting for UTF-16 encoding of characters)
+ api.nvim_set_current_buf(bufnr)
+ api.nvim_buf_set_option(0, 'buflisted', true)
+ local range = location.range or location.targetSelectionRange
+ local row = range.start.line
+ local col = get_line_byte_from_position(0, range.start)
+ api.nvim_win_set_cursor(0, {row + 1, col})
+ return true
+end
+
+--- Preview a location in a floating windows
+---
+--- behavior depends on type of location:
+--- - for Location, range is shown (e.g., function definition)
+--- - for LocationLink, targetRange is shown (e.g., body of function definition)
+---
+--@param location a single Location or LocationLink
+--@return bufnr,winnr buffer and window number of floating window or nil
+function M.preview_location(location)
+ -- location may be LocationLink or Location (more useful for the former)
+ local uri = location.targetUri or location.uri
+ if uri == nil then return end
+ local bufnr = vim.uri_to_bufnr(uri)
+ if not api.nvim_buf_is_loaded(bufnr) then
+ vim.fn.bufload(bufnr)
+ end
+ local range = location.targetRange or location.range
+ local contents = api.nvim_buf_get_lines(bufnr, range.start.line, range["end"].line+1, false)
+ local filetype = api.nvim_buf_get_option(bufnr, 'filetype')
+ return M.open_floating_preview(contents, filetype)
+end
+
+local function find_window_by_var(name, value)
+ for _, win in ipairs(api.nvim_list_wins()) do
+ if npcall(api.nvim_win_get_var, win, name) == value then
+ return win
+ end
+ 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.
+function M.focusable_float(unique_name, fn)
+ 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
+ api.nvim_set_current_win(win)
+ api.nvim_command("stopinsert")
+ return
+ end
+ end
+ local pbufnr, pwinnr = fn()
+ if pbufnr then
+ api.nvim_win_set_var(pwinnr, unique_name, bufnr)
+ return pbufnr, pwinnr
+ 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.
+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
+---
+--@param contents table of lines to trim and pad
+--@param opts dictionary with optional fields
+-- - pad_left number of columns to pad contents at left (default 1)
+-- - 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
+function M._trim_and_pad(contents, opts)
+ validate {
+ contents = { contents, 't' };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+ local left_padding = (" "):rep(opts.pad_left or 1)
+ local right_padding = (" "):rep(opts.pad_right or 1)
+ contents = M.trim_empty_lines(contents)
+ for i, line in ipairs(contents) do
+ contents[i] = string.format('%s%s%s', left_padding, line:gsub("\r", ""), right_padding)
+ end
+ if opts.pad_top then
+ for _ = 1, opts.pad_top do
+ table.insert(contents, 1, "")
+ end
+ end
+ if opts.pad_bottom then
+ for _ = 1, opts.pad_bottom do
+ table.insert(contents, "")
+ end
+ end
+ return contents
+end
+
+
+
+--- Convert markdown into syntax highlighted regions by stripping the code
+--- blocks and converting them into highlighted code.
+--- This will by default insert a blank line separator after those code block
+--- regions to improve readability.
+--- The result is shown in a floating preview
+--- TODO: refactor to separate stripping/converting and make use of open_floating_preview
+---
+--@param contents table of lines to show in window
+--@param opts dictionary with optional fields
+-- - height of floating window
+-- - width of floating window
+-- - wrap_at character to wrap at for computing height
+-- - max_width maximal width of floating window
+-- - max_height maximal height of floating window
+-- - pad_left number of columns to pad contents at left
+-- - 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
+-- - separator insert separator after code block
+--@return width,height size of float
+function M.fancy_floating_markdown(contents, opts)
+ validate {
+ contents = { contents, 't' };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+
+ local stripped = {}
+ local highlights = {}
+ do
+ local i = 1
+ while i <= #contents do
+ local line = contents[i]
+ -- TODO(ashkan): use a more strict regex for filetype?
+ local ft = line:match("^```([a-zA-Z0-9_]*)$")
+ -- local ft = line:match("^```(.*)$")
+ -- TODO(ashkan): validate the filetype here.
+ if ft then
+ local start = #stripped
+ i = i + 1
+ while i <= #contents do
+ line = contents[i]
+ if line == "```" then
+ i = i + 1
+ break
+ end
+ table.insert(stripped, line)
+ i = i + 1
+ end
+ table.insert(highlights, {
+ ft = ft;
+ start = start + 1;
+ finish = #stripped + 1 - 1;
+ })
+ else
+ table.insert(stripped, line)
+ i = i + 1
+ end
+ end
+ end
+ -- Clean up and add padding
+ stripped = M._trim_and_pad(stripped, opts)
+
+ -- Compute size of float needed to show (wrapped) lines
+ opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ local width, height = M._make_floating_popup_size(stripped, opts)
+
+ -- Insert blank line separator after code block
+ local insert_separator = opts.separator or true
+ if insert_separator then
+ for i, h in ipairs(highlights) do
+ h.start = h.start + i - 1
+ h.finish = h.finish + i - 1
+ if h.finish + 1 <= #stripped then
+ table.insert(stripped, h.finish + 1, string.rep("─", width))
+ height = height + 1
+ end
+ end
+ end
+
+ -- Make the floating window.
+ local bufnr = api.nvim_create_buf(false, true)
+ local winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
+ vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, stripped)
+ api.nvim_buf_set_option(bufnr, 'modifiable', false)
+
+ -- Switch to the floating window to apply the syntax highlighting.
+ -- This is because the syntax command doesn't accept a target.
+ local cwin = vim.api.nvim_get_current_win()
+ vim.api.nvim_set_current_win(winnr)
+
+ vim.cmd("ownsyntax markdown")
+ local idx = 1
+ local function apply_syntax_to_region(ft, start, finish)
+ if ft == '' then return end
+ local name = ft..idx
+ idx = idx + 1
+ local lang = "@"..ft:upper()
+ -- TODO(ashkan): better validation before this.
+ if not pcall(vim.cmd, string.format("syntax include %s syntax/%s.vim", lang, ft)) then
+ return
+ end
+ vim.cmd(string.format("syntax region %s start=+\\%%%dl+ end=+\\%%%dl+ contains=%s", name, start, finish + 1, lang))
+ end
+ -- Previous highlight region.
+ -- TODO(ashkan): this wasn't working for some reason, but I would like to
+ -- make sure that regions between code blocks are definitely markdown.
+ -- local ph = {start = 0; finish = 1;}
+ for _, h in ipairs(highlights) do
+ -- apply_syntax_to_region('markdown', ph.finish, h.start)
+ apply_syntax_to_region(h.ft, h.start, h.finish)
+ -- ph = h
+ end
+
+ vim.api.nvim_set_current_win(cwin)
+ return bufnr, winnr
+end
+
+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)
+---
+--@param contents table of lines to show in window
+--@param opts dictionary with optional fields
+-- - height of floating window
+-- - width of floating window
+-- - wrap_at character to wrap at for computing height
+-- - max_width maximal width of floating window
+-- - max_height maximal height of floating window
+--@return width,height size of float
+function M._make_floating_popup_size(contents, opts)
+ validate {
+ contents = { contents, 't' };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+
+ local width = opts.width
+ local height = opts.height
+ local wrap_at = opts.wrap_at
+ local max_width = opts.max_width
+ local max_height = opts.max_height
+ local line_widths = {}
+
+ if not width then
+ width = 0
+ for i, line in ipairs(contents) do
+ -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
+ line_widths[i] = vim.fn.strdisplaywidth(line)
+ width = math.max(line_widths[i], width)
+ end
+ end
+ if max_width then
+ width = math.min(width, max_width)
+ wrap_at = math.min(wrap_at or max_width, max_width)
+ end
+
+ if not height then
+ height = #contents
+ if wrap_at and width >= wrap_at then
+ height = 0
+ if vim.tbl_isempty(line_widths) then
+ for _, line in ipairs(contents) do
+ local line_width = vim.fn.strdisplaywidth(line)
+ height = height + math.ceil(line_width/wrap_at)
+ end
+ else
+ for i = 1, #contents do
+ height = height + math.max(1, math.ceil(line_widths[i]/wrap_at))
+ end
+ end
+ end
+ end
+ if max_height then
+ height = math.min(height, max_height)
+ end
+
+ return width, height
+end
+
+--- Show contents in a floating window
+---
+--@param contents table of lines to show in window
+--@param filetype string of filetype to set for opened buffer
+--@param opts dictionary with optional fields
+-- - height of floating window
+-- - width of floating window
+-- - wrap_at character to wrap at for computing height
+-- - max_width maximal width of floating window
+-- - max_height maximal height of floating window
+-- - pad_left number of columns to pad contents at left
+-- - 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
+function M.open_floating_preview(contents, filetype, opts)
+ validate {
+ contents = { contents, 't' };
+ filetype = { filetype, 's', true };
+ opts = { opts, 't', true };
+ }
+ opts = opts or {}
+
+ -- Clean up input: trim empty lines from the end, pad
+ contents = M._trim_and_pad(contents, opts)
+
+ -- Compute size of float needed to show (wrapped) lines
+ opts.wrap_at = opts.wrap_at or (vim.wo["wrap"] and api.nvim_win_get_width(0))
+ local width, height = M._make_floating_popup_size(contents, opts)
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ if filetype then
+ api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
+ end
+ local float_option = M.make_floating_popup_options(width, height, opts)
+ local floating_winnr = api.nvim_open_win(floating_bufnr, false, float_option)
+ if filetype == 'markdown' then
+ api.nvim_win_set_option(floating_winnr, 'conceallevel', 2)
+ end
+ api.nvim_buf_set_lines(floating_bufnr, 0, -1, true, contents)
+ api.nvim_buf_set_option(floating_bufnr, 'modifiable', false)
+ M.close_preview_autocmd({"CursorMoved", "CursorMovedI", "BufHidden", "BufLeave"}, floating_winnr)
+ return floating_bufnr, floating_winnr
+end
+
+do
+ local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
+ local reference_ns = api.nvim_create_namespace("vim_lsp_references")
+ local sign_ns = 'vim_lsp_signs'
+ local underline_highlight_name = "LspDiagnosticsUnderline"
+ vim.cmd(string.format("highlight default %s gui=underline cterm=underline", underline_highlight_name))
+ for kind, _ in pairs(protocol.DiagnosticSeverity) do
+ if type(kind) == 'string' then
+ vim.cmd(string.format("highlight default link %s%s %s", underline_highlight_name, kind, underline_highlight_name))
+ end
+ end
+
+ local severity_highlights = {}
+
+ local severity_floating_highlights = {}
+
+ local default_severity_highlight = {
+ [protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
+ [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
+ [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
+ [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
+ }
+
+ -- Initialize default severity highlights
+ for severity, hi_info in pairs(default_severity_highlight) do
+ local severity_name = protocol.DiagnosticSeverity[severity]
+ local highlight_name = "LspDiagnostics"..severity_name
+ local floating_highlight_name = highlight_name.."Floating"
+ -- Try to fill in the foreground color with a sane default.
+ local cmd_parts = {"highlight", "default", highlight_name}
+ for k, v in pairs(hi_info) do
+ table.insert(cmd_parts, k.."="..v)
+ end
+ api.nvim_command(table.concat(cmd_parts, ' '))
+ api.nvim_command('highlight link ' .. highlight_name .. 'Sign ' .. highlight_name)
+ api.nvim_command('highlight link ' .. highlight_name .. 'Floating ' .. highlight_name)
+ severity_highlights[severity] = highlight_name
+ severity_floating_highlights[severity] = floating_highlight_name
+ end
+
+ function M.buf_clear_diagnostics(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+
+ -- clear sign group
+ vim.fn.sign_unplace(sign_ns, {buffer=bufnr})
+
+ -- clear virtual text namespace
+ api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
+ end
+
+ function M.get_severity_highlight_name(severity)
+ return severity_highlights[severity]
+ end
+
+ function M.get_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ local linenr = api.nvim_win_get_cursor(0)[1] - 1
+
+ local buffer_diagnostics = M.diagnostics_by_buf[bufnr]
+
+ if not buffer_diagnostics then
+ return {}
+ end
+
+ local diagnostics_by_line = M.diagnostics_group_by_line(buffer_diagnostics)
+ return diagnostics_by_line[linenr] or {}
+ end
+
+ function M.show_line_diagnostics()
+ -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
+ -- if #marks == 0 then
+ -- return
+ -- end
+ local lines = {"Diagnostics:"}
+ local highlights = {{0, "Bold"}}
+ local line_diagnostics = M.get_line_diagnostics()
+ if vim.tbl_isempty(line_diagnostics) then return end
+
+ for i, diagnostic in ipairs(line_diagnostics) do
+ -- for i, mark in ipairs(marks) do
+ -- local mark_id = mark[1]
+ -- local diagnostic = buffer_diagnostics[mark_id]
+
+ -- TODO(ashkan) make format configurable?
+ local prefix = string.format("%d. ", i)
+ local hiname = severity_floating_highlights[diagnostic.severity]
+ assert(hiname, 'unknown severity: ' .. tostring(diagnostic.severity))
+ local message_lines = split_lines(diagnostic.message)
+ table.insert(lines, prefix..message_lines[1])
+ table.insert(highlights, {#prefix + 1, hiname})
+ for j = 2, #message_lines do
+ table.insert(lines, message_lines[j])
+ table.insert(highlights, {0, hiname})
+ end
+ end
+ local popup_bufnr, winnr = M.open_floating_preview(lines, 'plaintext')
+ for i, hi in ipairs(highlights) do
+ local prefixlen, hiname = unpack(hi)
+ -- Start highlight after the prefix
+ api.nvim_buf_add_highlight(popup_bufnr, -1, hiname, i-1, prefixlen, -1)
+ end
+ return popup_bufnr, winnr
+ end
+
+ --- Saves the diagnostics (Diagnostic[]) into diagnostics_by_buf
+ ---
+ --@param bufnr bufnr for which the diagnostics are for.
+ --@param diagnostics Diagnostics[] received from the language server.
+ function M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ validate {
+ bufnr = {bufnr, 'n', true};
+ diagnostics = {diagnostics, 't', true};
+ }
+ if not diagnostics then return end
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+
+ if not M.diagnostics_by_buf[bufnr] then
+ -- Clean up our data when the buffer unloads.
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b)
+ M.diagnostics_by_buf[b] = nil
+ end
+ })
+ end
+ M.diagnostics_by_buf[bufnr] = diagnostics
+ end
+
+ function M.buf_diagnostics_underline(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range["start"]
+ local finish = diagnostic.range["end"]
+
+ local hlmap = {
+ [protocol.DiagnosticSeverity.Error]='Error',
+ [protocol.DiagnosticSeverity.Warning]='Warning',
+ [protocol.DiagnosticSeverity.Information]='Information',
+ [protocol.DiagnosticSeverity.Hint]='Hint',
+ }
+
+ highlight.range(bufnr, diagnostic_ns,
+ underline_highlight_name..hlmap[diagnostic.severity],
+ {start.line, start.character},
+ {finish.line, finish.character}
+ )
+ end
+ end
+
+ function M.buf_clear_references(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ api.nvim_buf_clear_namespace(bufnr, reference_ns, 0, -1)
+ end
+
+ function M.buf_highlight_references(bufnr, references)
+ validate { bufnr = {bufnr, 'n', true} }
+ for _, reference in ipairs(references) do
+ local start_pos = {reference["range"]["start"]["line"], reference["range"]["start"]["character"]}
+ local end_pos = {reference["range"]["end"]["line"], reference["range"]["end"]["character"]}
+ local document_highlight_kind = {
+ [protocol.DocumentHighlightKind.Text] = "LspReferenceText";
+ [protocol.DocumentHighlightKind.Read] = "LspReferenceRead";
+ [protocol.DocumentHighlightKind.Write] = "LspReferenceWrite";
+ }
+ local kind = reference["kind"] or protocol.DocumentHighlightKind.Text
+ highlight.range(bufnr, reference_ns, document_highlight_kind[kind], start_pos, end_pos)
+ end
+ end
+
+ 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
+ local line_diagnostics = diagnostics_by_line[start.line]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ diagnostics_by_line[start.line] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ return diagnostics_by_line
+ end
+
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
+ if not diagnostics then
+ return
+ end
+ local buffer_line_diagnostics = M.diagnostics_group_by_line(diagnostics)
+ for line, line_diags in pairs(buffer_line_diagnostics) do
+ local virt_texts = {}
+ for i = 1, #line_diags - 1 do
+ table.insert(virt_texts, {"â– ", severity_highlights[line_diags[i].severity]})
+ end
+ local last = line_diags[#line_diags]
+ -- TODO(ashkan) use first line instead of subbing 2 spaces?
+ table.insert(virt_texts, {"â–  "..last.message:gsub("\r", ""):gsub("\n", " "), severity_highlights[last.severity]})
+ api.nvim_buf_set_virtual_text(bufnr, diagnostic_ns, line, virt_texts, {})
+ end
+ end
+
+ --- Returns the number of diagnostics of given kind for current buffer.
+ ---
+ --- Useful for showing diagnostic counts in statusline. eg:
+ ---
+ --- <pre>
+ --- function! LspStatus() abort
+ --- let sl = ''
+ --- if luaeval('not vim.tbl_isempty(vim.lsp.buf_get_clients(0))')
+ --- let sl.='%#MyStatuslineLSP#E:'
+ --- let sl.='%#MyStatuslineLSPErrors#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Error]])")}'
+ --- let sl.='%#MyStatuslineLSP# W:'
+ --- let sl.='%#MyStatuslineLSPWarnings#%{luaeval("vim.lsp.util.buf_diagnostics_count([[Warning]])")}'
+ --- else
+ --- let sl.='%#MyStatuslineLSPErrors#off'
+ --- endif
+ --- return sl
+ --- endfunction
+ --- let &l:statusline = '%#MyStatuslineLSP#LSP '.LspStatus()
+ --- </pre>
+ ---
+ --@param kind Diagnostic severity kind: See |vim.lsp.protocol.DiagnosticSeverity|
+ ---
+ --@return Count of diagnostics
+ function M.buf_diagnostics_count(kind)
+ local bufnr = vim.api.nvim_get_current_buf()
+ local diagnostics = M.diagnostics_by_buf[bufnr]
+ if not diagnostics then return end
+ local count = 0
+ for _, diagnostic in pairs(diagnostics) do
+ if protocol.DiagnosticSeverity[kind] == diagnostic.severity then
+ count = count + 1
+ end
+ end
+ return count
+ end
+
+ local diagnostic_severity_map = {
+ [protocol.DiagnosticSeverity.Error] = "LspDiagnosticsErrorSign";
+ [protocol.DiagnosticSeverity.Warning] = "LspDiagnosticsWarningSign";
+ [protocol.DiagnosticSeverity.Information] = "LspDiagnosticsInformationSign";
+ [protocol.DiagnosticSeverity.Hint] = "LspDiagnosticsHintSign";
+ }
+
+ --- Place signs for each diagnostic in the sign column.
+ ---
+ --- Sign characters can be customized with the following commands:
+ ---
+ --- <pre>
+ --- sign define LspDiagnosticsErrorSign text=E texthl=LspDiagnosticsError linehl= numhl=
+ --- sign define LspDiagnosticsWarningSign text=W texthl=LspDiagnosticsWarning linehl= numhl=
+ --- sign define LspDiagnosticsInformationSign text=I texthl=LspDiagnosticsInformation linehl= numhl=
+ --- sign define LspDiagnosticsHintSign text=H texthl=LspDiagnosticsHint linehl= numhl=
+ --- </pre>
+ function M.buf_diagnostics_signs(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ vim.fn.sign_place(0, sign_ns, diagnostic_severity_map[diagnostic.severity], bufnr, {lnum=(diagnostic.range.start.line+1)})
+ end
+ end
+end
+
+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.
+function M.locations_to_items(locations)
+ local items = {}
+ local grouped = setmetatable({}, {
+ __index = function(t, k)
+ local v = {}
+ rawset(t, k, v)
+ return v
+ end;
+ })
+ for _, d in ipairs(locations) do
+ -- locations may be Location or LocationLink
+ local uri = d.uri or d.targetUri
+ local range = d.range or d.targetSelectionRange
+ table.insert(grouped[uri], {start = range.start})
+ end
+
+
+ local keys = vim.tbl_keys(grouped)
+ table.sort(keys)
+ -- TODO(ashkan) I wish we could do this lazily.
+ for _, uri in ipairs(keys) do
+ local rows = grouped[uri]
+ table.sort(rows, position_sort)
+ local bufnr = vim.uri_to_bufnr(uri)
+ vim.fn.bufload(bufnr)
+ local filename = vim.uri_to_fname(uri)
+ for _, temp in ipairs(rows) do
+ local pos = temp.start
+ local row = pos.line
+ local line = (api.nvim_buf_get_lines(bufnr, row, row + 1, false) or {""})[1]
+ local col = M.character_offset(bufnr, row, pos.character)
+ table.insert(items, {
+ filename = filename,
+ lnum = row + 1,
+ col = col + 1;
+ text = line;
+ })
+ end
+ end
+ return items
+end
+
+function M.set_loclist(items)
+ vim.fn.setloclist(0, {}, ' ', {
+ title = 'Language Server';
+ items = items;
+ })
+end
+
+function M.set_qflist(items)
+ vim.fn.setqflist({}, ' ', {
+ title = 'Language Server';
+ items = items;
+ })
+end
+
+-- Acording to LSP spec, if the client set "symbolKind.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_documentSymbol
+function M._get_symbol_kind_name(symbol_kind)
+ return protocol.SymbolKind[symbol_kind] or "Unknown"
+end
+
+--- Convert symbols to quickfix list items
+---
+--@param symbols DocumentSymbol[] or SymbolInformation[]
+function M.symbols_to_items(symbols, bufnr)
+ local function _symbols_to_items(_symbols, _items, _bufnr)
+ for _, symbol in ipairs(_symbols) do
+ if symbol.location then -- SymbolInformation type
+ local range = symbol.location.range
+ local kind = M._get_symbol_kind_name(symbol.kind)
+ table.insert(_items, {
+ filename = vim.uri_to_fname(symbol.location.uri),
+ lnum = range.start.line + 1,
+ col = range.start.character + 1,
+ kind = kind,
+ text = '['..kind..'] '..symbol.name,
+ })
+ elseif symbol.range then -- DocumentSymbole type
+ local kind = M._get_symbol_kind_name(symbol.kind)
+ table.insert(_items, {
+ -- bufnr = _bufnr,
+ filename = vim.api.nvim_buf_get_name(_bufnr),
+ lnum = symbol.range.start.line + 1,
+ col = symbol.range.start.character + 1,
+ kind = kind,
+ text = '['..kind..'] '..symbol.name
+ })
+ if symbol.children then
+ for _, v in ipairs(_symbols_to_items(symbol.children, _items, _bufnr)) do
+ vim.list_extend(_items, v)
+ end
+ end
+ end
+ end
+ return _items
+ end
+ return _symbols_to_items(symbols, {}, bufnr)
+end
+
+-- Remove empty lines from the beginning and end.
+function M.trim_empty_lines(lines)
+ local start = 1
+ for i = 1, #lines do
+ if #lines[i] > 0 then
+ start = i
+ break
+ end
+ end
+ local finish = 1
+ for i = #lines, 1, -1 do
+ if #lines[i] > 0 then
+ finish = i
+ break
+ end
+ end
+ return vim.list_extend({}, lines, start, finish)
+end
+
+-- Accepts markdown lines and tries to reduce it to a filetype if it is
+-- just a single code block.
+-- Note: This modifies the input.
+--
+-- Returns: filetype or 'markdown' if it was unchanged.
+function M.try_trim_markdown_code_blocks(lines)
+ local language_id = lines[1]:match("^```(.*)")
+ if language_id then
+ local has_inner_code_fence = false
+ for i = 2, (#lines - 1) do
+ local line = lines[i]
+ if line:sub(1,3) == '```' then
+ has_inner_code_fence = true
+ break
+ end
+ end
+ -- No inner code fences + starting with code fence = hooray.
+ if not has_inner_code_fence then
+ table.remove(lines, 1)
+ table.remove(lines)
+ return language_id
+ end
+ end
+ return 'markdown'
+end
+
+local str_utfindex = vim.str_utfindex
+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]
+ col = str_utfindex(line, col)
+ return { line = row; character = col; }
+end
+
+function M.make_position_params()
+ return {
+ textDocument = M.make_text_document_params();
+ position = make_position_param()
+ }
+end
+
+function M.make_range_params()
+ local position = make_position_param()
+ return {
+ textDocument = { uri = vim.uri_from_bufnr(0) },
+ range = { start = position; ["end"] = position; }
+ }
+end
+
+function M.make_text_document_params()
+ return { uri = vim.uri_from_bufnr(0) }
+end
+
+--- Get visual width of tabstop.
+---
+--@see |softtabstop|
+--@param bufnr (optional, number): Buffer handle, defaults to current
+--@returns (number) tabstop visual width
+function M.get_effective_tabstop(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ local bo = bufnr and vim.bo[bufnr] or vim.bo
+ local sts = bo.softtabstop
+ return (sts > 0 and sts) or (sts < 0 and bo.shiftwidth) or bo.tabstop
+end
+
+function M.make_formatting_params(options)
+ validate { options = {options, 't', true} }
+ options = vim.tbl_extend('keep', options or {}, {
+ tabSize = M.get_effective_tabstop();
+ insertSpaces = vim.bo.expandtab;
+ })
+ return {
+ textDocument = { uri = vim.uri_from_bufnr(0) };
+ options = options;
+ }
+end
+
+-- @param buf buffer handle or 0 for current.
+-- @param row 0-indexed line
+-- @param col 0-indexed byte offset in line
+function M.character_offset(buf, row, col)
+ local line = api.nvim_buf_get_lines(buf, row, row+1, true)[1]
+ -- If the col is past the EOL, use the line length.
+ if col > #line then
+ return str_utfindex(line)
+ end
+ return str_utfindex(line, col)
+end
+
+M.buf_versions = {}
+
+return M
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/shared.lua b/runtime/lua/vim/shared.lua
index cd6f8a04d8..6e427665f2 100644
--- a/runtime/lua/vim/shared.lua
+++ b/runtime/lua/vim/shared.lua
@@ -4,34 +4,51 @@
-- test-suite. If, in the future, Nvim itself is used to run the test-suite
-- instead of "vanilla Lua", these functions could move to src/nvim/lua/vim.lua
+local vim = vim or {}
--- Returns a deep copy of the given object. Non-table objects are copied as
--- in a typical Lua assignment, whereas table objects are copied recursively.
+--- Functions are naively copied, so functions in the copied table point to the
+--- same functions as those in the input table. Userdata and threads are not
+--- copied and will throw an error.
---
--@param orig Table to copy
--@returns New table of copied keys and (nested) values.
-local function deepcopy(orig)
- error(orig)
-end
-local function _id(v)
- return v
-end
-local deepcopy_funcs = {
- table = function(orig)
- local copy = {}
- for k, v in pairs(orig) do
- copy[deepcopy(k)] = deepcopy(v)
+function vim.deepcopy(orig) end -- luacheck: no unused
+vim.deepcopy = (function()
+ local function _id(v)
+ return v
+ end
+
+ local deepcopy_funcs = {
+ table = function(orig)
+ local copy = {}
+
+ if vim._empty_dict_mt ~= nil and getmetatable(orig) == vim._empty_dict_mt then
+ copy = vim.empty_dict()
+ end
+
+ for k, v in pairs(orig) do
+ copy[vim.deepcopy(k)] = vim.deepcopy(v)
+ end
+ return copy
+ end,
+ number = _id,
+ string = _id,
+ ['nil'] = _id,
+ boolean = _id,
+ ['function'] = _id,
+ }
+
+ return function(orig)
+ local f = deepcopy_funcs[type(orig)]
+ if f then
+ return f(orig)
+ else
+ error("Cannot deepcopy object of type "..type(orig))
end
- return copy
- end,
- number = _id,
- string = _id,
- ['nil'] = _id,
- boolean = _id,
-}
-deepcopy = function(orig)
- return deepcopy_funcs[type(orig)](orig)
-end
+ end
+end)()
--- Splits a string at each instance of a separator.
---
@@ -43,10 +60,8 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns Iterator over the split components
-local function gsplit(s, sep, plain)
- assert(type(s) == "string")
- assert(type(sep) == "string")
- assert(type(plain) == "boolean" or type(plain) == "nil")
+function vim.gsplit(s, sep, plain)
+ vim.validate{s={s,'s'},sep={sep,'s'},plain={plain,'b',true}}
local start = 1
local done = false
@@ -64,7 +79,7 @@ local function gsplit(s, sep, plain)
end
return function()
- if done then
+ if done or (s == '' and sep == '') then
return
end
if sep == '' then
@@ -92,20 +107,81 @@ end
--@param sep Separator string or pattern
--@param plain If `true` use `sep` literally (passed to String.find)
--@returns List-like table of the split components.
-local function split(s,sep,plain)
- local t={} for c in gsplit(s, sep, plain) do table.insert(t,c) end
+function vim.split(s,sep,plain)
+ local t={} for c in vim.gsplit(s, sep, plain) do table.insert(t,c) end
return t
end
+--- Return a list of all keys used in a table.
+--- However, the order of the return table of keys is not guaranteed.
+---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
+--@param t Table
+--@returns list of keys
+function vim.tbl_keys(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local keys = {}
+ for k, _ in pairs(t) do
+ table.insert(keys, k)
+ end
+ return keys
+end
+
+--- Return a list of all values used in a table.
+--- However, the order of the return table of values is not guaranteed.
+---
+--@param t Table
+--@returns list of values
+function vim.tbl_values(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+
+ local values = {}
+ for _, v in pairs(t) do
+ table.insert(values, v)
+ end
+ return values
+end
+
+--- Apply a function to all values of a table.
+---
+--@param func function or callable table
+--@param t table
+function vim.tbl_map(func, t)
+ vim.validate{func={func,'c'},t={t,'t'}}
+
+ local rettab = {}
+ for k, v in pairs(t) do
+ rettab[k] = func(v)
+ end
+ return rettab
+end
+
+--- Filter a table using a predicate function
+---
+--@param func function or callable table
+--@param t table
+function vim.tbl_filter(func, t)
+ vim.validate{func={func,'c'},t={t,'t'}}
+
+ local rettab = {}
+ for _, entry in pairs(t) do
+ if func(entry) then
+ table.insert(rettab, entry)
+ end
+ end
+ return rettab
+end
+
--- Checks if a list-like (vector) table contains `value`.
---
--@param t Table to check
--@param value Value to compare
--@returns true if `t` contains `value`
-local function tbl_contains(t, value)
- if type(t) ~= 'table' then
- error('t must be a table')
- end
+function vim.tbl_contains(t, value)
+ vim.validate{t={t,'t'}}
+
for _,v in ipairs(t) do
if v == value then
return true
@@ -114,25 +190,38 @@ local function tbl_contains(t, value)
return false
end
---- Merges two or more map-like tables.
----
---@see |extend()|
----
---@param behavior Decides what to do if a key is found in more than one map:
---- - "error": raise an error
---- - "keep": use value from the leftmost map
---- - "force": use value from the rightmost map
---@param ... Two or more map-like tables.
-local function tbl_extend(behavior, ...)
+-- 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
+--
+--@param t Table to check
+function vim.tbl_isempty(t)
+ assert(type(t) == 'table', string.format("Expected table, got %s", type(t)))
+ return next(t) == nil
+end
+
+local function tbl_extend(behavior, deep_extend, ...)
if (behavior ~= 'error' and behavior ~= 'keep' and behavior ~= 'force') then
error('invalid "behavior": '..tostring(behavior))
end
+
+ if select('#', ...) < 2 then
+ error('wrong number of arguments (given '..tostring(1 + select('#', ...))..', expected at least 3)')
+ end
+
local ret = {}
+ if vim._empty_dict_mt ~= nil and getmetatable(select(1, ...)) == vim._empty_dict_mt then
+ ret = vim.empty_dict()
+ end
+
for i = 1, select('#', ...) do
local tbl = select(i, ...)
+ vim.validate{["after the second argument"] = {tbl,'t'}}
if tbl then
for k, v in pairs(tbl) do
- if behavior ~= 'force' and ret[k] ~= nil then
+ if type(v) == 'table' and deep_extend and not vim.tbl_islist(v) then
+ ret[k] = tbl_extend(behavior, true, ret[k] or vim.empty_dict(), v)
+ elseif behavior ~= 'force' and ret[k] ~= nil then
if behavior == 'error' then
error('key found in more than one map: '..k)
end -- Else behavior is "keep".
@@ -145,13 +234,103 @@ local function tbl_extend(behavior, ...)
return ret
end
+--- Merges two or more map-like tables.
+---
+--@see |extend()|
+---
+--@param behavior Decides what to do if a key is found in more than one map:
+--- - "error": raise an error
+--- - "keep": use value from the leftmost map
+--- - "force": use value from the rightmost map
+--@param ... Two or more map-like tables.
+function vim.tbl_extend(behavior, ...)
+ return tbl_extend(behavior, false, ...)
+end
+
+--- Merges recursively two or more map-like tables.
+---
+--@see |tbl_extend()|
+---
+--@param behavior Decides what to do if a key is found in more than one map:
+--- - "error": raise an error
+--- - "keep": use value from the leftmost map
+--- - "force": use value from the rightmost map
+--@param ... Two or more map-like tables.
+function vim.tbl_deep_extend(behavior, ...)
+ return tbl_extend(behavior, true, ...)
+end
+
+--- Deep compare values for equality
+function vim.deep_equal(a, b)
+ if a == b then return true end
+ if type(a) ~= type(b) then return false end
+ if type(a) == 'table' then
+ -- TODO improve this algorithm's performance.
+ for k, v in pairs(a) do
+ if not vim.deep_equal(v, b[k]) then
+ return false
+ end
+ end
+ for k, v in pairs(b) do
+ if not vim.deep_equal(v, a[k]) then
+ return false
+ end
+ end
+ return true
+ end
+ return false
+end
+
+--- Add the reverse lookup values to an existing table.
+--- For example:
+--- `tbl_add_reverse_lookup { A = 1 } == { [1] = 'A', A = 1 }`
+--
+--Do note that it *modifies* the input.
+--@param o table The table to add the reverse to.
+function vim.tbl_add_reverse_lookup(o)
+ local keys = vim.tbl_keys(o)
+ for _, k in ipairs(keys) do
+ local v = o[k]
+ if o[v] then
+ error(string.format("The reverse lookup found an existing value for %q while processing key %q", tostring(v), tostring(k)))
+ end
+ o[v] = k
+ end
+ return o
+end
+
+--- Extends a list-like table with the values of another list-like table.
+---
+--- NOTE: This mutates dst!
+---
+--@see |vim.tbl_extend()|
+---
+--@param dst list which will be modified and appended to.
+--@param src list from which values will be inserted.
+--@param start Start index on src. defaults to 1
+--@param finish Final index on src. defaults to #src
+--@returns dst
+function vim.list_extend(dst, src, start, finish)
+ vim.validate {
+ dst = {dst, 't'};
+ src = {src, 't'};
+ start = {start, 'n', true};
+ finish = {finish, 'n', true};
+ }
+ for i = start or 1, finish or #src do
+ table.insert(dst, src[i])
+ end
+ return dst
+end
+
--- Creates a copy of a list-like table such that any nested tables are
--- "unrolled" and appended to the result.
---
+--@see From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+---
--@param t List-like table
--@returns Flattened copy of the given list-like table.
-local function tbl_flatten(t)
- -- From https://github.com/premake/premake-core/blob/master/src/base/table.lua
+function vim.tbl_flatten(t)
local result = {}
local function _tbl_flatten(_t)
local n = #_t
@@ -168,34 +347,190 @@ local function tbl_flatten(t)
return result
end
+--- Determine whether 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.
+---
+--@param t Table
+--@returns `true` if array-like table, else `false`.
+function vim.tbl_islist(t)
+ if type(t) ~= 'table' then
+ return false
+ end
+
+ local count = 0
+
+ for k, _ in pairs(t) do
+ if type(k) == "number" then
+ count = count + 1
+ else
+ return false
+ end
+ end
+
+ if count > 0 then
+ return true
+ else
+ -- TODO(bfredl): in the future, we will always be inside nvim
+ -- then this check can be deleted.
+ if vim._empty_dict_mt == nil then
+ return nil
+ end
+ return getmetatable(t) ~= vim._empty_dict_mt
+ end
+end
+
+--- Counts the number of non-nil values in table `t`.
+---
+--- <pre>
+--- vim.tbl_count({ a=1, b=2 }) => 2
+--- vim.tbl_count({ 1, 2 }) => 2
+--- </pre>
+---
+--@see https://github.com/Tieske/Penlight/blob/master/lua/pl/tablex.lua
+--@param t Table
+--@returns Number that is the number of the value in table
+function vim.tbl_count(t)
+ vim.validate{t={t,'t'}}
+
+ local count = 0
+ for _ in pairs(t) do count = count + 1 end
+ return count
+end
+
--- Trim whitespace (Lua pattern "%s") from both sides of a string.
---
--@see https://www.lua.org/pil/20.2.html
--@param s String to trim
--@returns String with whitespace removed from its beginning and end
-local function trim(s)
- assert(type(s) == 'string', 'Only strings can be trimmed')
+function vim.trim(s)
+ vim.validate{s={s,'s'}}
return s:match('^%s*(.*%S)') or ''
end
---- Escapes magic chars in a Lua pattern string.
+--- Escapes magic chars in a Lua pattern.
---
--@see https://github.com/rxi/lume
--@param s String to escape
--@returns %-escaped pattern string
-local function pesc(s)
- assert(type(s) == 'string')
+function vim.pesc(s)
+ vim.validate{s={s,'s'}}
return s:gsub('[%(%)%.%%%+%-%*%?%[%]%^%$]', '%%%1')
end
-local module = {
- deepcopy = deepcopy,
- gsplit = gsplit,
- pesc = pesc,
- split = split,
- tbl_contains = tbl_contains,
- tbl_extend = tbl_extend,
- tbl_flatten = tbl_flatten,
- trim = trim,
-}
-return module
+--- Tests if `s` starts with `prefix`.
+---
+--@param s (string) a string
+--@param prefix (string) a prefix
+--@return (boolean) true if `prefix` is a prefix of s
+function vim.startswith(s, prefix)
+ vim.validate { s = {s, 's'}; prefix = {prefix, 's'}; }
+ return s:sub(1, #prefix) == prefix
+end
+
+--- Tests if `s` ends with `suffix`.
+---
+--@param s (string) a string
+--@param suffix (string) a suffix
+--@return (boolean) true if `suffix` is a suffix of s
+function vim.endswith(s, suffix)
+ vim.validate { s = {s, 's'}; suffix = {suffix, 's'}; }
+ return #suffix == 0 or s:sub(-#suffix) == suffix
+end
+
+--- Validates a parameter specification (types and values).
+---
+--- Usage example:
+--- <pre>
+--- function user.new(name, age, hobbies)
+--- vim.validate{
+--- name={name, 'string'},
+--- age={age, 'number'},
+--- hobbies={hobbies, 'table'},
+--- }
+--- ...
+--- end
+--- </pre>
+---
+--- Examples with explicit argument values (can be run directly):
+--- <pre>
+--- vim.validate{arg1={{'foo'}, 'table'}, arg2={'foo', 'string'}}
+--- => NOP (success)
+---
+--- vim.validate{arg1={1, 'table'}}
+--- => error('arg1: expected table, got number')
+---
+--- vim.validate{arg1={3, function(a) return (a % 2) == 0 end, 'even number'}}
+--- => error('arg1: expected even number, got 3')
+--- </pre>
+---
+--@param opt Map of parameter names to validations. Each key is a parameter
+--- name; each value is a tuple in one of these forms:
+--- 1. (arg_value, type_name, optional)
+--- - arg_value: argument value
+--- - type_name: string type name, one of: ("table", "t", "string",
+--- "s", "number", "n", "boolean", "b", "function", "f", "nil",
+--- "thread", "userdata")
+--- - optional: (optional) boolean, if true, `nil` is valid
+--- 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
+--- - msg: (optional) error string if validation fails
+function vim.validate(opt) end -- luacheck: no unused
+vim.validate = (function()
+ 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',
+ }
+ 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)))
+ for param_name, spec in pairs(opt) do
+ assert(type(spec) == 'table', string.format('%s: expected table, got %s', param_name, type(spec)))
+
+ 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)))
+ 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))
+ end
+ end
+ return true
+ end
+end)()
+
+--- Returns true if object `f` can be called as a function.
+---
+--@param f Any object
+--@return true if `f` is callable, else false
+function vim.is_callable(f)
+ if type(f) == 'function' then return true end
+ local m = getmetatable(f)
+ if m == nil then return false end
+ return type(m.__call) == 'function'
+end
+
+return vim
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/treesitter.lua b/runtime/lua/vim/treesitter.lua
new file mode 100644
index 0000000000..927456708c
--- /dev/null
+++ b/runtime/lua/vim/treesitter.lua
@@ -0,0 +1,260 @@
+local a = vim.api
+
+-- 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
+-- it.
+local parsers = {}
+
+local Parser = {}
+Parser.__index = Parser
+
+function Parser:parse()
+ if self.valid then
+ return self.tree
+ end
+ local changes
+ self.tree, changes = self._parser:parse_buf(self.bufnr)
+ self.valid = true
+
+ if not vim.tbl_isempty(changes) then
+ for _, cb in ipairs(self.changedtree_cbs) do
+ cb(changes)
+ end
+ end
+
+ 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)
+ 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)
+ end
+end
+
+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,
+}
+
+setmetatable(M, {
+ __index = function (t, k)
+ if k == "TSHighlighter" then
+ t[k] = require'vim.tshighlighter'
+ 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)
+ if bufnr == 0 then
+ bufnr = a.nvim_get_current_buf()
+ end
+
+ vim.fn.bufload(bufnr)
+
+ local self = setmetatable({bufnr=bufnr, lang=lang, valid=false}, Parser)
+ self._parser = vim._create_ts_parser(lang)
+ self.changedtree_cbs = {}
+ self.lines_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(...)
+ end
+ local detach_cb = nil
+ if id ~= nil then
+ detach_cb = function()
+ if parsers[id] == self then
+ parsers[id] = nil
+ end
+ end
+ end
+ a.nvim_buf_attach(self.bufnr, false, {on_lines=lines_cb, on_detach=detach_cb})
+ return self
+end
+
+function M.get_parser(bufnr, ft, 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")
+ end
+ local id = tostring(bufnr)..'_'..ft
+
+ 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)
+ end
+
+ if buf_attach_cbs and buf_attach_cbs.on_lines then
+ table.insert(parsers[id].lines_cbs, buf_attach_cbs.on_lines)
+ end
+
+ return parsers[id]
+end
+
+-- query: pattern matching on trees
+-- predicate matching is implemented in lua
+local Query = {}
+Query.__index = Query
+
+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
+
+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/tshighlighter.lua b/runtime/lua/vim/tshighlighter.lua
new file mode 100644
index 0000000000..6465751ae8
--- /dev/null
+++ b/runtime/lua/vim/tshighlighter.lua
@@ -0,0 +1,116 @@
+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
new file mode 100644
index 0000000000..9c3535c676
--- /dev/null
+++ b/runtime/lua/vim/uri.lua
@@ -0,0 +1,112 @@
+--- TODO: This is implemented only for files now.
+-- https://tools.ietf.org/html/rfc3986
+-- https://tools.ietf.org/html/rfc2732
+-- https://tools.ietf.org/html/rfc2396
+
+
+local uri_decode
+do
+ local schar = string.char
+ local function hex_to_char(hex)
+ return schar(tonumber(hex, 16))
+ end
+ uri_decode = function(str)
+ return str:gsub("%%([a-fA-F0-9][a-fA-F0-9])", hex_to_char)
+ end
+end
+
+local uri_encode
+do
+ local PATTERNS = {
+ --- RFC 2396
+ -- https://tools.ietf.org/html/rfc2396#section-2.2
+ rfc2396 = "^A-Za-z0-9%-_.!~*'()";
+ --- RFC 2732
+ -- https://tools.ietf.org/html/rfc2732
+ rfc2732 = "^A-Za-z0-9%-_.!~*'()[]";
+ --- RFC 3986
+ -- https://tools.ietf.org/html/rfc3986#section-2.2
+ rfc3986 = "^A-Za-z0-9%-._~!$&'()*+,;=:@/";
+ }
+ local sbyte, tohex = string.byte
+ if jit then
+ tohex = require'bit'.tohex
+ else
+ tohex = function(b) return string.format("%02x", b) end
+ end
+ local function percent_encode_char(char)
+ return "%"..tohex(sbyte(char), 2)
+ end
+ uri_encode = function(text, rfc)
+ if not text then return end
+ local pattern = PATTERNS[rfc] or PATTERNS.rfc3986
+ return text:gsub("(["..pattern.."])", percent_encode_char)
+ end
+end
+
+
+local function is_windows_file_uri(uri)
+ return uri:match('^file:///[a-zA-Z]:') ~= nil
+end
+
+local function uri_from_fname(path)
+ local volume_path, fname = path:match("^([a-zA-Z]:)(.*)")
+ local is_windows = volume_path ~= nil
+ if is_windows then
+ path = volume_path..uri_encode(fname:gsub("\\", "/"))
+ else
+ path = uri_encode(path)
+ end
+ local uri_parts = {"file://"}
+ if is_windows then
+ table.insert(uri_parts, "/")
+ end
+ table.insert(uri_parts, path)
+ return table.concat(uri_parts)
+end
+
+local URI_SCHEME_PATTERN = '^([a-zA-Z]+[a-zA-Z0-9+-.]*)://.*'
+
+local function uri_from_bufnr(bufnr)
+ local fname = vim.api.nvim_buf_get_name(bufnr)
+ local scheme = fname:match(URI_SCHEME_PATTERN)
+ if scheme then
+ return fname
+ else
+ return uri_from_fname(fname)
+ end
+end
+
+local function uri_to_fname(uri)
+ local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri)
+ if scheme ~= 'file' then
+ return uri
+ end
+ uri = uri_decode(uri)
+ -- TODO improve this.
+ if is_windows_file_uri(uri) then
+ uri = uri:gsub('^file:///', '')
+ uri = uri:gsub('/', '\\')
+ else
+ uri = uri:gsub('^file://', '')
+ end
+ return uri
+end
+
+-- Return or create a buffer for a uri.
+local function uri_to_bufnr(uri)
+ local scheme = assert(uri:match(URI_SCHEME_PATTERN), 'URI must contain a scheme: ' .. uri)
+ if scheme == 'file' then
+ return vim.fn.bufadd(uri_to_fname(uri))
+ else
+ return vim.fn.bufadd(uri)
+ end
+end
+
+return {
+ uri_from_fname = uri_from_fname,
+ uri_from_bufnr = uri_from_bufnr,
+ uri_to_fname = uri_to_fname,
+ uri_to_bufnr = uri_to_bufnr,
+}
+-- vim:sw=2 ts=2 et