aboutsummaryrefslogtreecommitdiff
path: root/runtime/lua/vim/lsp
diff options
context:
space:
mode:
Diffstat (limited to 'runtime/lua/vim/lsp')
-rw-r--r--runtime/lua/vim/lsp/builtin_callbacks.lua296
-rw-r--r--runtime/lua/vim/lsp/log.lua95
-rw-r--r--runtime/lua/vim/lsp/protocol.lua936
-rw-r--r--runtime/lua/vim/lsp/rpc.lua451
-rw-r--r--runtime/lua/vim/lsp/util.lua557
5 files changed, 2335 insertions, 0 deletions
diff --git a/runtime/lua/vim/lsp/builtin_callbacks.lua b/runtime/lua/vim/lsp/builtin_callbacks.lua
new file mode 100644
index 0000000000..cc739ce3ad
--- /dev/null
+++ b/runtime/lua/vim/lsp/builtin_callbacks.lua
@@ -0,0 +1,296 @@
+--- Implements the following default callbacks:
+--
+-- vim.api.nvim_buf_set_lines(0, 0, 0, false, vim.tbl_keys(vim.lsp.builtin_callbacks))
+--
+
+-- textDocument/completion
+-- textDocument/declaration
+-- textDocument/definition
+-- textDocument/hover
+-- textDocument/implementation
+-- textDocument/publishDiagnostics
+-- textDocument/rename
+-- textDocument/signatureHelp
+-- textDocument/typeDefinition
+-- TODO codeLens/resolve
+-- TODO completionItem/resolve
+-- TODO documentLink/resolve
+-- TODO textDocument/codeAction
+-- TODO textDocument/codeLens
+-- TODO textDocument/documentHighlight
+-- TODO textDocument/documentLink
+-- TODO textDocument/documentSymbol
+-- TODO textDocument/formatting
+-- TODO textDocument/onTypeFormatting
+-- TODO textDocument/rangeFormatting
+-- TODO textDocument/references
+-- window/logMessage
+-- window/showMessage
+
+local log = require 'vim.lsp.log'
+local protocol = require 'vim.lsp.protocol'
+local util = require 'vim.lsp.util'
+local api = vim.api
+
+local function split_lines(value)
+ return vim.split(value, '\n', true)
+end
+
+local builtin_callbacks = {}
+
+-- textDocument/completion
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+builtin_callbacks['textDocument/completion'] = function(_, _, result)
+ if not result or vim.tbl_isempty(result) then
+ return
+ end
+ local pos = api.nvim_win_get_cursor(0)
+ local row, col = pos[1], pos[2]
+ local line = assert(api.nvim_buf_get_lines(0, row-1, row, false)[1])
+ local line_to_cursor = line:sub(col+1)
+
+ local matches = util.text_document_completion_list_to_complete_items(result, line_to_cursor)
+ local match_result = vim.fn.matchstrpos(line_to_cursor, '\\k\\+$')
+ local match_start, match_finish = match_result[2], match_result[3]
+
+ vim.fn.complete(col + 1 - (match_finish - match_start), matches)
+end
+
+-- textDocument/rename
+builtin_callbacks['textDocument/rename'] = function(_, _, result)
+ if not result then return end
+ util.workspace_apply_workspace_edit(result)
+end
+
+local function uri_to_bufnr(uri)
+ return vim.fn.bufadd((vim.uri_to_fname(uri)))
+end
+
+builtin_callbacks['textDocument/publishDiagnostics'] = function(_, _, result)
+ if not result then return end
+ local uri = result.uri
+ local bufnr = uri_to_bufnr(uri)
+ if not bufnr then
+ api.nvim_err_writeln(string.format("LSP.publishDiagnostics: Couldn't find buffer for %s", uri))
+ return
+ end
+ util.buf_clear_diagnostics(bufnr)
+ util.buf_diagnostics_save_positions(bufnr, result.diagnostics)
+ util.buf_diagnostics_underline(bufnr, result.diagnostics)
+ util.buf_diagnostics_virtual_text(bufnr, result.diagnostics)
+ -- util.buf_loclist(bufnr, result.diagnostics)
+end
+
+-- textDocument/hover
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_hover
+-- @params MarkedString | MarkedString[] | MarkupContent
+builtin_callbacks['textDocument/hover'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then
+ return
+ end
+
+ if result.contents ~= nil then
+ local markdown_lines = util.convert_input_to_markdown_lines(result.contents)
+ if vim.tbl_isempty(markdown_lines) then
+ markdown_lines = { 'No information available' }
+ end
+ util.open_floating_preview(markdown_lines, 'markdown')
+ end
+end
+
+builtin_callbacks['textDocument/peekDefinition'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then return end
+ -- TODO(ashkan) what to do with multiple locations?
+ result = result[1]
+ local bufnr = uri_to_bufnr(result.uri)
+ assert(bufnr)
+ local start = result.range.start
+ local finish = result.range["end"]
+ util.open_floating_peek_preview(bufnr, start, finish, { offset_x = 1 })
+ util.open_floating_preview({"*Peek:*", string.rep(" ", finish.character - start.character + 1) }, 'markdown', { offset_y = -(finish.line - start.line) })
+end
+
+--- Convert SignatureHelp response to preview contents.
+-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_signatureHelp
+local function signature_help_to_preview_contents(input)
+ if not input.signatures then
+ return
+ end
+ --The active signature. If omitted or the value lies outside the range of
+ --`signatures` the value defaults to zero or is ignored if `signatures.length
+ --=== 0`. Whenever possible implementors should make an active decision about
+ --the active signature and shouldn't rely on a default value.
+ local contents = {}
+ local active_signature = input.activeSignature or 0
+ -- If the activeSignature is not inside the valid range, then clip it.
+ if active_signature >= #input.signatures then
+ active_signature = 0
+ end
+ local signature = input.signatures[active_signature + 1]
+ if not signature then
+ return
+ end
+ vim.list_extend(contents, split_lines(signature.label))
+ if signature.documentation then
+ util.convert_input_to_markdown_lines(signature.documentation, contents)
+ end
+ if input.parameters then
+ local active_parameter = input.activeParameter or 0
+ -- If the activeParameter is not inside the valid range, then clip it.
+ if active_parameter >= #input.parameters then
+ active_parameter = 0
+ end
+ local parameter = signature.parameters and signature.parameters[active_parameter]
+ if parameter then
+ --[=[
+ --Represents a parameter of a callable-signature. A parameter can
+ --have a label and a doc-comment.
+ interface ParameterInformation {
+ --The label of this parameter information.
+ --
+ --Either a string or an inclusive start and exclusive end offsets within its containing
+ --signature label. (see SignatureInformation.label). The offsets are based on a UTF-16
+ --string representation as `Position` and `Range` does.
+ --
+ --*Note*: a label of type string should be a substring of its containing signature label.
+ --Its intended use case is to highlight the parameter label part in the `SignatureInformation.label`.
+ label: string | [number, number];
+ --The human-readable doc-comment of this parameter. Will be shown
+ --in the UI but can be omitted.
+ documentation?: string | MarkupContent;
+ }
+ --]=]
+ -- TODO highlight parameter
+ if parameter.documentation then
+ util.convert_input_to_markdown_lines(parameter.documentation, contents)
+ end
+ end
+ end
+ return contents
+end
+
+-- textDocument/signatureHelp
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_signatureHelp
+builtin_callbacks['textDocument/signatureHelp'] = function(_, _, result)
+ if result == nil or vim.tbl_isempty(result) then
+ return
+ end
+
+ -- TODO show empty popup when signatures is empty?
+ if #result.signatures > 0 then
+ local markdown_lines = signature_help_to_preview_contents(result)
+ if vim.tbl_isempty(markdown_lines) then
+ markdown_lines = { 'No signature available' }
+ end
+ util.open_floating_preview(markdown_lines, 'markdown')
+ end
+end
+
+local function update_tagstack()
+ local bufnr = api.nvim_get_current_buf()
+ local line = vim.fn.line('.')
+ local col = vim.fn.col('.')
+ local tagname = vim.fn.expand('<cWORD>')
+ local item = { bufnr = bufnr, from = { bufnr, line, col, 0 }, tagname = tagname }
+ local winid = vim.fn.win_getid()
+ local tagstack = vim.fn.gettagstack(winid)
+
+ local action
+
+ if tagstack.length == tagstack.curidx then
+ action = 'r'
+ tagstack.items[tagstack.curidx] = item
+ elseif tagstack.length > tagstack.curidx then
+ action = 'r'
+ if tagstack.curidx > 1 then
+ tagstack.items = table.insert(tagstack.items[tagstack.curidx - 1], item)
+ else
+ tagstack.items = { item }
+ end
+ else
+ action = 'a'
+ tagstack.items = { item }
+ end
+
+ tagstack.curidx = tagstack.curidx + 1
+ vim.fn.settagstack(winid, tagstack, action)
+end
+
+local function handle_location(result)
+ -- We can sometimes get a list of locations, so set the first value as the
+ -- only value we want to handle
+ -- TODO(ashkan) was this correct^? We could use location lists.
+ if result[1] ~= nil then
+ result = result[1]
+ end
+ if result.uri == nil then
+ api.nvim_err_writeln('[LSP] Could not find a valid location')
+ return
+ end
+ local result_file = vim.uri_to_fname(result.uri)
+ local bufnr = vim.fn.bufadd(result_file)
+ update_tagstack()
+ api.nvim_set_current_buf(bufnr)
+ local start = result.range.start
+ api.nvim_win_set_cursor(0, {start.line + 1, start.character})
+end
+
+local function location_callback(_, method, result)
+ if result == nil or vim.tbl_isempty(result) then
+ local _ = log.info() and log.info(method, 'No location found')
+ return nil
+ end
+ handle_location(result)
+ return true
+end
+
+local location_callbacks = {
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_declaration
+ 'textDocument/declaration';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_definition
+ 'textDocument/definition';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_implementation
+ 'textDocument/implementation';
+ -- https://microsoft.github.io/language-server-protocol/specification#textDocument_typeDefinition
+ 'textDocument/typeDefinition';
+}
+
+for _, location_method in ipairs(location_callbacks) do
+ builtin_callbacks[location_method] = location_callback
+end
+
+local function log_message(_, _, result, client_id)
+ local message_type = result.type
+ local message = result.message
+ local client = vim.lsp.get_client_by_id(client_id)
+ local client_name = client and client.name or string.format("id=%d", client_id)
+ if not client then
+ api.nvim_err_writeln(string.format("LSP[%s] client has shut down after sending the message", client_name))
+ end
+ if message_type == protocol.MessageType.Error then
+ -- Might want to not use err_writeln,
+ -- but displaying a message with red highlights or something
+ api.nvim_err_writeln(string.format("LSP[%s] %s", client_name, message))
+ else
+ local message_type_name = protocol.MessageType[message_type]
+ api.nvim_out_write(string.format("LSP[%s][%s] %s\n", client_name, message_type_name, message))
+ end
+ return result
+end
+
+builtin_callbacks['window/showMessage'] = log_message
+builtin_callbacks['window/logMessage'] = log_message
+
+-- Add boilerplate error validation and logging for all of these.
+for k, fn in pairs(builtin_callbacks) do
+ builtin_callbacks[k] = function(err, method, params, client_id)
+ local _ = log.debug() and log.debug('builtin_callback', method, { params = params, client_id = client_id, err = err })
+ if err then
+ error(tostring(err))
+ end
+ return fn(err, method, params, client_id)
+ end
+end
+
+return builtin_callbacks
+-- vim:sw=2 ts=2 et
diff --git a/runtime/lua/vim/lsp/log.lua b/runtime/lua/vim/lsp/log.lua
new file mode 100644
index 0000000000..974eaae38c
--- /dev/null
+++ b/runtime/lua/vim/lsp/log.lua
@@ -0,0 +1,95 @@
+-- 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;
+ -- FATAL = 4;
+}
+
+-- Default log level is warn.
+local current_log_level = log.levels.WARN
+local log_date_format = "%FT%H:%M:%SZ%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'), 'vim-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..1413a88ce2
--- /dev/null
+++ b/runtime/lua/vim/lsp/protocol.lua
@@ -0,0 +1,936 @@
+-- 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/blob/gh-pages/_specifications/specification-3-14.md
+-- https://github.com/microsoft/language-server-protocol/raw/gh-pages/_specifications/specification-3-14.md
+function transform_schema_comments()
+ nvim.command [[silent! '<,'>g/\/\*\*\|\*\/\|^$/d]]
+ 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;
+}
+--]=]
+
+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;
+ };
+ completion = {
+ dynamicRegistration = false;
+ completionItem = {
+
+ -- TODO(tjdevries): Is it possible to implement this in plain lua?
+ snippetSupport = false;
+ 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;
+ };
+ 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) == 'string' then table.insert(res, k) end
+ -- end
+ -- return res
+ -- end)();
+ -- };
+ -- hierarchicalDocumentSymbolSupport = false;
+ -- };
+ };
+ workspace = nil;
+ experimental = nil;
+ }
+end
+
+function protocol.make_text_document_position_params()
+ local position = vim.api.nvim_win_get_cursor(0)
+ return {
+ textDocument = {
+ uri = vim.uri_from_bufnr()
+ };
+ position = {
+ line = position[1] - 1;
+ character = position[2];
+ }
+ }
+end
+
+--[=[
+export interface DocumentFilter {
+ --A language id, like `typescript`.
+ 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;
+}
+--]]
+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(textDocumentSync.save 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
+
+ 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.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..e0ec8863d6
--- /dev/null
+++ b/runtime/lua/vim/lsp/rpc.lua
@@ -0,0 +1,451 @@
+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
+
+-- If a dictionary is passed in, turn it into a list of string of "k=v"
+-- Accepts a table which can be composed of k=v strings or map-like
+-- specification, such as:
+--
+-- ```
+-- {
+-- "PRODUCTION=false";
+-- "PATH=/usr/bin/";
+-- PORT = 123;
+-- HOST = "0.0.0.0";
+-- }
+-- ```
+--
+-- Non-string values will be cast with `tostring`
+local function force_env_list(final_env)
+ if final_env then
+ local env = final_env
+ final_env = {}
+ for k,v in pairs(env) do
+ -- If it's passed in as a dict, then convert to list of "k=v"
+ if type(k) == "string" then
+ table.insert(final_env, k..'='..tostring(v))
+ elseif type(v) == 'string' then
+ table.insert(final_env, v)
+ else
+ -- TODO is this right or should I exception here?
+ -- Try to coerce other values to string.
+ table.insert(final_env, tostring(v))
+ end
+ end
+ return final_env
+ end
+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' };
+ }
+ local code_name = assert(protocol.ErrorCodes[err.code], "err.code is invalid")
+ local message_parts = {"RPC", code_name}
+ 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
+
+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 = force_env_list(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)
+ 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)
+
+ -- 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..f96e0f01a8
--- /dev/null
+++ b/runtime/lua/vim/lsp/util.lua
@@ -0,0 +1,557 @@
+local protocol = require 'vim.lsp.protocol'
+local validate = vim.validate
+local api = vim.api
+
+local M = {}
+
+local split = vim.split
+local function split_lines(value)
+ return split(value, '\n', true)
+end
+
+local list_extend = vim.list_extend
+
+--- Find the longest shared prefix between prefix and word.
+-- e.g. remove_prefix("123tes", "testing") == "ting"
+local function remove_prefix(prefix, word)
+ local max_prefix_length = math.min(#prefix, #word)
+ local prefix_length = 0
+ for i = 1, max_prefix_length do
+ local current_line_suffix = prefix:sub(-i)
+ local word_prefix = word:sub(1, i)
+ if current_line_suffix == word_prefix then
+ prefix_length = i
+ end
+ end
+ return word:sub(prefix_length + 1)
+end
+
+local function resolve_bufnr(bufnr)
+ if bufnr == nil or bufnr == 0 then
+ return api.nvim_get_current_buf()
+ end
+ return bufnr
+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
+
+--- Apply the TextEdit response.
+-- @params TextEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.text_document_apply_text_edit(text_edit, bufnr)
+ bufnr = resolve_bufnr(bufnr)
+ local range = text_edit.range
+ local start = range.start
+ local finish = range['end']
+ local new_lines = split_lines(text_edit.newText)
+ if start.character == 0 and finish.character == 0 then
+ api.nvim_buf_set_lines(bufnr, start.line, finish.line, false, new_lines)
+ return
+ end
+ api.nvim_err_writeln('apply_text_edit currently only supports character ranges starting at 0')
+ error('apply_text_edit currently only supports character ranges starting at 0')
+ return
+ -- TODO test and finish this support for character ranges.
+-- local lines = api.nvim_buf_get_lines(0, start.line, finish.line + 1, false)
+-- local suffix = lines[#lines]:sub(finish.character+2)
+-- local prefix = lines[1]:sub(start.character+2)
+-- new_lines[#new_lines] = new_lines[#new_lines]..suffix
+-- new_lines[1] = prefix..new_lines[1]
+-- api.nvim_buf_set_lines(0, start.line, finish.line, false, new_lines)
+end
+
+-- textDocument/completion response returns one of CompletionItem[], CompletionList or null.
+-- https://microsoft.github.io/language-server-protocol/specification#textDocument_completion
+function M.extract_completion_items(result)
+ 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.text_document_apply_text_document_edit(text_document_edit, bufnr)
+ -- local text_document = text_document_edit.textDocument
+ -- TODO use text_document_version?
+ -- local text_document_version = text_document.version
+
+ -- TODO technically, you could do this without doing multiple buf_get/set
+ -- by getting the full region (smallest line and largest line) and doing
+ -- the edits on the buffer, and then applying the buffer at the end.
+ -- I'm not sure if that's better.
+ for _, text_edit in ipairs(text_document_edit.edits) do
+ M.text_document_apply_text_edit(text_edit, bufnr)
+ end
+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
+
+--- 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, line_prefix)
+ local items = M.extract_completion_items(result)
+ if vim.tbl_isempty(items) then
+ return {}
+ end
+ -- Only initialize if we have some items.
+ if not line_prefix then
+ line_prefix = M.get_current_line_to_cursor()
+ end
+
+ 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 = completion_item.insertText or completion_item.label
+
+ -- Ref: `:h complete-items`
+ table.insert(matches, {
+ word = remove_prefix(line_prefix, word),
+ abbr = completion_item.label,
+ kind = protocol.CompletionItemKind[completion_item.kind] or '',
+ menu = completion_item.detail or '',
+ info = info,
+ icase = 1,
+ dup = 0,
+ empty = 1,
+ })
+ end
+
+ return matches
+end
+
+-- @params WorkspaceEdit [table] see https://microsoft.github.io/language-server-protocol/specification
+function M.workspace_apply_workspace_edit(workspace_edit)
+ if workspace_edit.documentChanges then
+ for _, change in ipairs(workspace_edit.documentChanges) do
+ if change.kind then
+ -- TODO(ashkan) handle CreateFile/RenameFile/DeleteFile
+ error(string.format("Unsupported change: %q", vim.inspect(change)))
+ else
+ M.text_document_apply_text_document_edit(change)
+ end
+ end
+ return
+ end
+
+ if workspace_edit.changes == nil or #workspace_edit.changes == 0 then
+ return
+ end
+
+ for uri, changes in pairs(workspace_edit.changes) do
+ local fname = vim.uri_to_fname(uri)
+ -- TODO improve this approach. Try to edit open buffers without switching.
+ -- Not sure how to handle files which aren't open. This is deprecated
+ -- anyway, so I guess it could be left as is.
+ api.nvim_command('edit '..fname)
+ for _, change in ipairs(changes) do
+ M.text_document_apply_text_edit(change)
+ end
+ 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 then
+ return {}
+ 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
+
+ if vim.fn.winline() <= height then
+ anchor = anchor..'N'
+ row = 1
+ else
+ anchor = anchor..'S'
+ 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.open_floating_preview(contents, filetype, opts)
+ validate {
+ contents = { contents, 't' };
+ filetype = { filetype, 's', true };
+ opts = { opts, 't', true };
+ }
+
+ -- Trim empty lines from the end.
+ for i = #contents, 1, -1 do
+ if #contents[i] == 0 then
+ table.remove(contents)
+ else
+ break
+ end
+ end
+
+ local width = 0
+ local height = #contents
+ for i, line in ipairs(contents) do
+ -- Clean up the input and add left pad.
+ line = " "..line:gsub("\r", "")
+ -- TODO(ashkan) use nvim_strdisplaywidth if/when that is introduced.
+ local line_width = vim.fn.strdisplaywidth(line)
+ width = math.max(line_width, width)
+ contents[i] = line
+ end
+ -- Add right padding of 1 each.
+ width = width + 1
+
+ local floating_bufnr = api.nvim_create_buf(false, true)
+ if filetype then
+ api.nvim_buf_set_option(floating_bufnr, 'filetype', filetype)
+ 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)
+ api.nvim_command("autocmd CursorMoved <buffer> ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_bufnr, floating_winnr
+end
+
+local function validate_lsp_position(pos)
+ validate { pos = {pos, 't'} }
+ validate {
+ line = {pos.line, 'n'};
+ character = {pos.character, 'n'};
+ }
+ return true
+end
+
+function M.open_floating_peek_preview(bufnr, start, finish, opts)
+ validate {
+ bufnr = {bufnr, 'n'};
+ start = {start, validate_lsp_position, 'valid start Position'};
+ finish = {finish, validate_lsp_position, 'valid finish Position'};
+ opts = { opts, 't', true };
+ }
+ local width = math.max(finish.character - start.character + 1, 1)
+ local height = math.max(finish.line - start.line + 1, 1)
+ local floating_winnr = api.nvim_open_win(bufnr, false, M.make_floating_popup_options(width, height, opts))
+ api.nvim_win_set_cursor(floating_winnr, {start.line+1, start.character})
+ api.nvim_command("autocmd CursorMoved * ++once lua pcall(vim.api.nvim_win_close, "..floating_winnr..", true)")
+ return floating_winnr
+end
+
+
+local function highlight_range(bufnr, ns, hiname, start, finish)
+ if start[1] == finish[1] then
+ -- TODO care about encoding here since this is in byte index?
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], finish[2])
+ else
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, start[1], start[2], -1)
+ for line = start[1] + 1, finish[1] - 1 do
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, line, 0, -1)
+ end
+ api.nvim_buf_add_highlight(bufnr, ns, hiname, finish[1], 0, finish[2])
+ end
+end
+
+do
+ local all_buffer_diagnostics = {}
+
+ local diagnostic_ns = api.nvim_create_namespace("vim_lsp_diagnostics")
+
+ local default_severity_highlight = {
+ [protocol.DiagnosticSeverity.Error] = { guifg = "Red" };
+ [protocol.DiagnosticSeverity.Warning] = { guifg = "Orange" };
+ [protocol.DiagnosticSeverity.Information] = { guifg = "LightBlue" };
+ [protocol.DiagnosticSeverity.Hint] = { guifg = "LightGrey" };
+ }
+
+ local underline_highlight_name = "LspDiagnosticsUnderline"
+ api.nvim_command(string.format("highlight %s gui=underline cterm=underline", underline_highlight_name))
+
+ local function find_color_rgb(color)
+ local rgb_hex = api.nvim_get_color_by_name(color)
+ validate { color = {color, function() return rgb_hex ~= -1 end, "valid color name"} }
+ return rgb_hex
+ end
+
+ --- Determine whether to use black or white text
+ -- Ref: https://stackoverflow.com/a/1855903/837964
+ -- https://stackoverflow.com/questions/596216/formula-to-determine-brightness-of-rgb-color
+ local function color_is_bright(r, g, b)
+ -- Counting the perceptive luminance - human eye favors green color
+ local luminance = (0.299*r + 0.587*g + 0.114*b)/255
+ if luminance > 0.5 then
+ return true -- Bright colors, black font
+ else
+ return false -- Dark colors, white font
+ end
+ end
+
+ local severity_highlights = {}
+
+ function M.set_severity_highlights(highlights)
+ validate {highlights = {highlights, 't'}}
+ for severity, default_color in pairs(default_severity_highlight) do
+ local severity_name = protocol.DiagnosticSeverity[severity]
+ local highlight_name = "LspDiagnostics"..severity_name
+ local hi_info = highlights[severity] or default_color
+ -- Try to fill in the foreground color with a sane default.
+ if not hi_info.guifg and hi_info.guibg then
+ -- TODO(ashkan) move this out when bitop is guaranteed to be included.
+ local bit = require 'bit'
+ local band, rshift = bit.band, bit.rshift
+ local rgb = find_color_rgb(hi_info.guibg)
+ local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
+ hi_info.guifg = is_bright and "Black" or "White"
+ end
+ if not hi_info.ctermfg and hi_info.ctermbg then
+ -- TODO(ashkan) move this out when bitop is guaranteed to be included.
+ local bit = require 'bit'
+ local band, rshift = bit.band, bit.rshift
+ local rgb = find_color_rgb(hi_info.ctermbg)
+ local is_bright = color_is_bright(rshift(rgb, 16), band(rshift(rgb, 8), 0xFF), band(rgb, 0xFF))
+ hi_info.ctermfg = is_bright and "Black" or "White"
+ end
+ local cmd_parts = {"highlight", highlight_name}
+ for k, v in pairs(hi_info) do
+ table.insert(cmd_parts, k.."="..v)
+ end
+ api.nvim_command(table.concat(cmd_parts, ' '))
+ severity_highlights[severity] = highlight_name
+ end
+ end
+
+ function M.buf_clear_diagnostics(bufnr)
+ validate { bufnr = {bufnr, 'n', true} }
+ bufnr = bufnr == 0 and api.nvim_get_current_buf() or bufnr
+ api.nvim_buf_clear_namespace(bufnr, diagnostic_ns, 0, -1)
+ end
+
+ -- Initialize with the defaults.
+ M.set_severity_highlights(default_severity_highlight)
+
+ function M.get_severity_highlight_name(severity)
+ return severity_highlights[severity]
+ end
+
+ function M.show_line_diagnostics()
+ local bufnr = api.nvim_get_current_buf()
+ local line = api.nvim_win_get_cursor(0)[1] - 1
+ -- local marks = api.nvim_buf_get_extmarks(bufnr, diagnostic_ns, {line, 0}, {line, -1}, {})
+ -- if #marks == 0 then
+ -- return
+ -- end
+ -- local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ local lines = {"Diagnostics:"}
+ local highlights = {{0, "Bold"}}
+
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_diagnostics then return end
+ local line_diagnostics = buffer_diagnostics[line]
+ if not 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_highlights[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
+
+ 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 all_buffer_diagnostics[bufnr] then
+ -- Clean up our data when the buffer unloads.
+ api.nvim_buf_attach(bufnr, false, {
+ on_detach = function(b)
+ all_buffer_diagnostics[b] = nil
+ end
+ })
+ end
+ all_buffer_diagnostics[bufnr] = {}
+ local buffer_diagnostics = all_buffer_diagnostics[bufnr]
+
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ -- local mark_id = api.nvim_buf_set_extmark(bufnr, diagnostic_ns, 0, start.line, 0, {})
+ -- buffer_diagnostics[mark_id] = diagnostic
+ local line_diagnostics = buffer_diagnostics[start.line]
+ if not line_diagnostics then
+ line_diagnostics = {}
+ buffer_diagnostics[start.line] = line_diagnostics
+ end
+ table.insert(line_diagnostics, diagnostic)
+ end
+ end
+
+
+ function M.buf_diagnostics_underline(bufnr, diagnostics)
+ for _, diagnostic in ipairs(diagnostics) do
+ local start = diagnostic.range.start
+ local finish = diagnostic.range["end"]
+
+ -- TODO care about encoding here since this is in byte index?
+ highlight_range(bufnr, diagnostic_ns, underline_highlight_name,
+ {start.line, start.character},
+ {finish.line, finish.character}
+ )
+ end
+ end
+
+ function M.buf_diagnostics_virtual_text(bufnr, diagnostics)
+ local buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ M.buf_diagnostics_save_positions(bufnr, diagnostics)
+ end
+ buffer_line_diagnostics = all_buffer_diagnostics[bufnr]
+ if not buffer_line_diagnostics then
+ return
+ end
+ 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
+end
+
+function M.buf_loclist(bufnr, locations)
+ local targetwin
+ for _, winnr in ipairs(api.nvim_list_wins()) do
+ local winbuf = api.nvim_win_get_buf(winnr)
+ if winbuf == bufnr then
+ targetwin = winnr
+ break
+ end
+ end
+ if not targetwin then return end
+
+ local items = {}
+ local path = api.nvim_buf_get_name(bufnr)
+ for _, d in ipairs(locations) do
+ -- TODO: URL parsing here?
+ local start = d.range.start
+ table.insert(items, {
+ filename = path,
+ lnum = start.line + 1,
+ col = start.character + 1,
+ text = d.message,
+ })
+ end
+ vim.fn.setloclist(targetwin, items, ' ', 'Language Server')
+end
+
+return M
+-- vim:sw=2 ts=2 et