2618 lines
82 KiB
Lua
2618 lines
82 KiB
Lua
--- mod-version:3
|
|
--
|
|
-- LSP client for lite-xl
|
|
-- @copyright Jefferson Gonzalez
|
|
-- @license MIT
|
|
--
|
|
-- Note: Annotations syntax documentation which is supported by
|
|
-- https://github.com/sumneko/lua-language-server can be read here:
|
|
-- https://emmylua.github.io/annotation.html
|
|
|
|
-- TODO Change the code to make it possible to use more than one LSP server
|
|
-- for a single file if possible and needed, for eg:
|
|
-- One lsp may not support goto definition but another one registered
|
|
-- for the current document filetype may do.
|
|
|
|
local core = require "core"
|
|
local common = require "core.common"
|
|
local config = require "core.config"
|
|
local command = require "core.command"
|
|
local style = require "core.style"
|
|
local keymap = require "core.keymap"
|
|
local translate = require "core.doc.translate"
|
|
local autocomplete = require "plugins.autocomplete"
|
|
local Doc = require "core.doc"
|
|
local DocView = require "core.docview"
|
|
local StatusView = require "core.statusview"
|
|
local RootView = require "core.rootview"
|
|
local LineWrapping
|
|
-- If the lsp plugin is loaded from users init.lua it will load linewrapping
|
|
-- even if it was disabled from the settings ui, so we queue this check since
|
|
-- there is no way to automatically load settings ui before the user module.
|
|
core.add_thread(function()
|
|
if config.plugins.linewrapping or type(config.plugins.linewrapping) == "nil" then
|
|
LineWrapping = require "plugins.linewrapping"
|
|
end
|
|
end)
|
|
|
|
local json = require "plugins.lsp.json"
|
|
local util = require "plugins.lsp.util"
|
|
local listbox = require "plugins.lsp.listbox"
|
|
local diagnostics = require "plugins.lsp.diagnostics"
|
|
local Server = require "plugins.lsp.server"
|
|
local Timer = require "plugins.lsp.timer"
|
|
local SymbolResults = require "plugins.lsp.symbolresults"
|
|
local MessageBox = require "libraries.widget.messagebox"
|
|
local snippets_found, snippets = pcall(require, "plugins.snippets")
|
|
|
|
---@type lsp.helpdoc
|
|
local HelpDoc = require "plugins.lsp.helpdoc"
|
|
|
|
--
|
|
-- Plugin settings
|
|
--
|
|
|
|
---Configuration options for the LSP plugin.
|
|
---@class config.plugins.lsp
|
|
---Set to a file path to log all json
|
|
---@field log_file string
|
|
---Setting to true prettyfies json for more readability on the log
|
|
---but this setting will impact performance so only enable it when
|
|
---in need of easy to read json output when developing the plugin.
|
|
---@field prettify_json boolean
|
|
---Show a symbol hover information when mouse cursor is on top.
|
|
---@field mouse_hover boolean
|
|
---The amount of time in milliseconds before showing the tooltip.
|
|
---@field mouse_hover_delay integer
|
|
---Show diagnostic messages
|
|
---@field show_diagnostics boolean
|
|
---Amount of milliseconds to delay updating the inline diagnostics.
|
|
---@field diagnostics_delay number
|
|
---Wether to enable snippets processing.
|
|
---@field snippets boolean
|
|
---Stop servers that aren't needed by any of the open files
|
|
---@field stop_unneeded_servers boolean
|
|
---Send a server stderr output to lite log
|
|
---@field log_server_stderr boolean
|
|
---Force verbosity off even if a server is configured with verbosity on
|
|
---@field force_verbosity_off boolean
|
|
---Yield when reading from LSP which may give you better UI responsiveness
|
|
---when receiving large responses, but will affect LSP performance.
|
|
---@field more_yielding boolean
|
|
config.plugins.lsp = common.merge({
|
|
mouse_hover = true,
|
|
mouse_hover_delay = 300,
|
|
show_diagnostics = true,
|
|
diagnostics_delay = 500,
|
|
snippets = true,
|
|
stop_unneeded_servers = true,
|
|
log_file = "",
|
|
prettify_json = false,
|
|
log_server_stderr = false,
|
|
force_verbosity_off = false,
|
|
more_yielding = false,
|
|
autostart_server = true,
|
|
-- The config specification used by the settings gui
|
|
config_spec = {
|
|
name = "Language Server Protocol",
|
|
{
|
|
label = "Mouse Hover",
|
|
description = "Show a symbol hover information when mouse cursor is on top.",
|
|
path = "mouse_hover",
|
|
type = "TOGGLE",
|
|
default = true
|
|
},
|
|
{
|
|
label = "Mouse Hover Delay",
|
|
description = "The amount of time in milliseconds before showing the tooltip.",
|
|
path = "mouse_hover_delay",
|
|
type = "NUMBER",
|
|
default = 300,
|
|
min = 50,
|
|
max = 2000
|
|
},
|
|
{
|
|
label = "Diagnostics",
|
|
description = "Show inline diagnostic messages with lint+.",
|
|
path = "show_diagnostics",
|
|
type = "TOGGLE",
|
|
default = false
|
|
},
|
|
{
|
|
label = "Diagnostics Delay",
|
|
description = "Amount of milliseconds to delay the update of inline diagnostics.",
|
|
path = "diagnostics_delay",
|
|
type = "NUMBER",
|
|
default = 500,
|
|
min = 100,
|
|
max = 10000
|
|
},
|
|
{
|
|
label = "Snippets",
|
|
description = "Snippets processing using lsp_snippets, may need a restart.",
|
|
path = "snippets",
|
|
type = "TOGGLE",
|
|
default = true
|
|
},
|
|
{
|
|
label = "Autostart Server",
|
|
description = "Automatically start server when opening a file",
|
|
path = "autostart_server",
|
|
type = "TOGGLE",
|
|
default = true
|
|
},
|
|
{
|
|
label = "Stop Servers",
|
|
description = "Stop servers that aren't needed by any of the open files.",
|
|
path = "stop_unneeded_servers",
|
|
type = "TOGGLE",
|
|
default = true
|
|
},
|
|
{
|
|
label = "Log File",
|
|
description = "Absolute path to a '.log' file for logging all json.",
|
|
path = "log_file",
|
|
type = "FILE",
|
|
filters = {"%.log$"}
|
|
},
|
|
{
|
|
label = "Prettify JSON",
|
|
description = "Prettify json for more readability but impacts performance.",
|
|
path = "prettify_json",
|
|
type = "TOGGLE",
|
|
default = false
|
|
},
|
|
{
|
|
label = "Log Standard Error",
|
|
description = "Send a server stderr output to lite log.",
|
|
path = "log_server_stderr",
|
|
type = "TOGGLE",
|
|
default = false
|
|
},
|
|
{
|
|
label = "Force Verbosity Off",
|
|
description = "Turn verbosity off even if a server is configured with verbosity on.",
|
|
path = "force_verbosity_off",
|
|
type = "TOGGLE",
|
|
default = false
|
|
},
|
|
{
|
|
label = "More Yielding",
|
|
description = "Yield when reading from LSP which may give you better UI responsiveness.",
|
|
path = "more_yielding",
|
|
type = "TOGGLE",
|
|
default = false
|
|
}
|
|
}
|
|
}, config.plugins.lsp)
|
|
|
|
|
|
--
|
|
-- Main plugin functionality
|
|
--
|
|
local lsp = {}
|
|
|
|
---List of registered servers
|
|
---@type table<string, lsp.server.options>
|
|
lsp.servers = {}
|
|
|
|
---List of running servers
|
|
---@type table<string, lsp.server>
|
|
lsp.servers_running = {}
|
|
|
|
---Flag that indicates if last autocomplete request was a trigger
|
|
---to prevent requesting another autocompletion request until the
|
|
---autocomplete box is hidden since some lsp servers loose context
|
|
---and return wrong results (eg: lua-language-server)
|
|
---@type boolean
|
|
lsp.in_trigger = false
|
|
|
|
---Flag that indicates if the user typed something on the editor to try and
|
|
---call autocomplete only when neccesary.
|
|
---@type boolean
|
|
lsp.user_typed = false
|
|
|
|
---Used on the hover timer to display hover info
|
|
---@class lsp.hover_position
|
|
---@field doc core.doc | nil
|
|
---@field x number
|
|
---@field y number
|
|
---@field triggered boolean
|
|
---@field utf8_range table | nil
|
|
lsp.hover_position = {doc = nil, x = -1, y = -1, triggered = false, utf8_range = nil}
|
|
|
|
---@type lsp.timer
|
|
lsp.hover_timer = Timer(300, true)
|
|
lsp.hover_timer.on_timer = function()
|
|
local doc, line, col = lsp.get_hovered_location(lsp.hover_position.x, lsp.hover_position.y)
|
|
if not doc then return end
|
|
lsp.hover_position.triggered = true
|
|
lsp.hover_position.utf8_range = nil
|
|
lsp.hover_position.doc = doc
|
|
lsp.request_hover(doc, line, col)
|
|
end
|
|
|
|
--
|
|
-- Private functions
|
|
--
|
|
|
|
---Generate an lsp location object
|
|
---@param doc core.doc
|
|
---@param line integer
|
|
---@param col integer
|
|
local function get_buffer_position_params(doc, line, col)
|
|
return {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename)),
|
|
},
|
|
position = {
|
|
line = line - 1,
|
|
character = util.doc_utf8_to_utf16(doc, line, col) - 1
|
|
}
|
|
}
|
|
end
|
|
|
|
---Recursive function to generate a list of symbols ready
|
|
---to use for the lsp.request_document_symbols() action.
|
|
---@param list table<integer, table>
|
|
---@param parent? string
|
|
local function get_symbol_lists(list, parent)
|
|
local symbols = {}
|
|
local symbol_names = {}
|
|
parent = parent or ""
|
|
parent = #parent > 0 and (parent .. "/") or parent
|
|
|
|
for _, symbol in pairs(list) do
|
|
-- Include symbol kind to be able to filter by it
|
|
local symbol_name = parent
|
|
.. symbol.name
|
|
.. "||" .. Server.get_symbol_kind(symbol.kind)
|
|
|
|
table.insert(symbol_names, symbol_name)
|
|
|
|
symbols[symbol_name] = { kind = symbol.kind }
|
|
|
|
if symbol.location then
|
|
symbols[symbol_name].location = symbol.location
|
|
else
|
|
if symbol.range then
|
|
symbols[symbol_name].range = symbol.range
|
|
end
|
|
if symbol.uri then
|
|
symbols[symbol_name].uri = symbol.uri
|
|
end
|
|
end
|
|
|
|
if symbol.children and #symbol.children > 0 then
|
|
local child_symbols, child_names = get_symbol_lists(
|
|
symbol.children, parent .. symbol.name
|
|
)
|
|
|
|
for _, name in pairs(child_names) do
|
|
table.insert(symbol_names, name)
|
|
symbols[name] = child_symbols[name]
|
|
end
|
|
end
|
|
end
|
|
|
|
return symbols, symbol_names
|
|
end
|
|
|
|
local function log(server, message, ...)
|
|
if server.verbose then
|
|
core.log("["..server.name.."] " .. message, ...)
|
|
else
|
|
core.log_quiet("["..server.name.."] " .. message, ...)
|
|
end
|
|
end
|
|
|
|
---Check if active view is a DocView and return it
|
|
---@return core.docview|nil
|
|
local function get_active_docview()
|
|
local av = core.active_view
|
|
if getmetatable(av) == DocView and av.doc and av.doc.filename then
|
|
return av
|
|
end
|
|
return nil
|
|
end
|
|
|
|
---Generates a code preview of a location
|
|
---@param location table
|
|
local function get_location_preview(location)
|
|
local line1, col1 = util.toselection(
|
|
location.range or location.targetRange
|
|
)
|
|
local filename = core.normalize_to_project_dir(
|
|
util.tofilename(location.uri or location.targetUri)
|
|
)
|
|
local abs_filename = core.project_absolute_path(filename)
|
|
|
|
local file = io.open(abs_filename)
|
|
|
|
if not file then
|
|
return "", filename .. ":" .. tostring(line1) .. ":" .. tostring(col1)
|
|
end
|
|
|
|
local preview = ""
|
|
|
|
-- sometimes the lsp can send the location of a definition where the
|
|
-- doc comments should be written but if no docs are written the line
|
|
-- is empty and subsequent line is the one we are interested in.
|
|
local line_count = 1
|
|
for line in file:lines() do
|
|
if line_count >= line1 then
|
|
preview = line:gsub("^%s+", "")
|
|
:gsub("%s+$", "")
|
|
|
|
if preview ~= "" then
|
|
break
|
|
else
|
|
-- change also the location table
|
|
if location.range then
|
|
location.range.start.line = location.range.start.line + 1
|
|
location.range['end'].line = location.range['end'].line + 1
|
|
elseif location.targetRange then
|
|
location.targetRange.start.line = location.targetRange.start.line + 1
|
|
location.targetRange['end'].line = location.targetRange['end'].line + 1
|
|
end
|
|
end
|
|
end
|
|
line_count = line_count + 1
|
|
end
|
|
file:close()
|
|
|
|
local position = filename .. ":" .. tostring(line1) .. ":" .. tostring(col1)
|
|
|
|
return preview, position
|
|
end
|
|
|
|
---Generate a list ready to use for the lsp.request_references() action.
|
|
---@param locations table
|
|
local function get_references_lists(locations)
|
|
local references, reference_names = {}, {}
|
|
|
|
for _, location in pairs(locations) do
|
|
local preview, position = get_location_preview(location)
|
|
local name = preview .. "||" .. position
|
|
table.insert(reference_names, name)
|
|
references[name] = location
|
|
end
|
|
|
|
return references, reference_names
|
|
end
|
|
|
|
---Apply an lsp textEdit to a document if possible.
|
|
---@param server lsp.server
|
|
---@param doc core.doc
|
|
---@param text_edit table
|
|
---@param is_snippet boolean
|
|
---@param update_cursor_position boolean
|
|
---@return boolean True on success
|
|
local function apply_edit(server, doc, text_edit, is_snippet, update_cursor_position)
|
|
local range = nil
|
|
|
|
if text_edit.range then
|
|
range = text_edit.range
|
|
elseif text_edit.insert then
|
|
range = text_edit.insert
|
|
elseif text_edit.replace then
|
|
range = text_edit.replace
|
|
end
|
|
|
|
if not range then return false end
|
|
|
|
local text = text_edit.newText
|
|
local line1, col1, line2, col2
|
|
local current_text = ""
|
|
|
|
if
|
|
not server.capabilities.positionEncoding
|
|
or
|
|
server.capabilities.positionEncoding == Server.position_encoding_kind.UTF16
|
|
then
|
|
line1, col1, line2, col2 = util.toselection(range, doc)
|
|
else
|
|
line1, col1, line2, col2 = util.toselection(range)
|
|
core.error(
|
|
"[LSP] Unsupported position encoding: ",
|
|
server.capabilities.positionEncoding
|
|
)
|
|
end
|
|
|
|
if lsp.in_trigger then
|
|
local cline2, ccol2 = doc:get_selection()
|
|
local cline1, ccol1 = doc:position_offset(line2, col2, translate.start_of_word)
|
|
current_text = doc:get_text(cline1, ccol1, cline2, ccol2)
|
|
end
|
|
|
|
doc:remove(line1, col1, line2, col2+#current_text)
|
|
|
|
if is_snippet and snippets_found and config.plugins.lsp.snippets then
|
|
doc:set_selection(line1, col1, line1, col1)
|
|
snippets.execute {format = 'lsp', template = text}
|
|
return true
|
|
end
|
|
|
|
doc:insert(line1, col1, text)
|
|
if update_cursor_position then
|
|
doc:move_to_cursor(nil, #text)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
---Callback given to autocomplete plugin which is executed once for each
|
|
---element of the autocomplete box which is hovered with the idea of providing
|
|
---better description of the selected element by requesting the LSP server for
|
|
---detailed information/documentation.
|
|
---@param index integer
|
|
---@param item table
|
|
local function autocomplete_onhover(index, item)
|
|
local completion_item = item.data.completion_item
|
|
|
|
if item.data.server.verbose then
|
|
item.data.server:log(
|
|
"Resolve item: %s", util.jsonprettify(json.encode(completion_item))
|
|
)
|
|
end
|
|
|
|
-- Only send resolve request if data field (which should contain
|
|
-- the item id) is available.
|
|
if completion_item.data then
|
|
item.data.server:push_request('completionItem/resolve', {
|
|
params = completion_item,
|
|
callback = function(server, response)
|
|
if response.result then
|
|
local symbol = response.result
|
|
if symbol.detail and #item.desc <= 0 then
|
|
item.desc = symbol.detail
|
|
end
|
|
if symbol.documentation then
|
|
if #item.desc > 0 then
|
|
item.desc = item.desc .. "\n\n"
|
|
end
|
|
if
|
|
type(symbol.documentation) == "table"
|
|
and
|
|
symbol.documentation.value
|
|
then
|
|
item.desc = item.desc .. symbol.documentation.value
|
|
if
|
|
symbol.documentation.kind
|
|
and
|
|
symbol.documentation.kind == "markdown"
|
|
then
|
|
item.desc = util.strip_markdown(item.desc)
|
|
end
|
|
else
|
|
item.desc = item.desc .. symbol.documentation
|
|
end
|
|
end
|
|
item.desc = item.desc:gsub("[%s\n]+$", "")
|
|
:gsub("^[%s\n]+", "")
|
|
:gsub("\n\n\n+", "\n\n")
|
|
if symbol.additionalTextEdits then
|
|
completion_item.additionalTextEdits = symbol.additionalTextEdits
|
|
end
|
|
|
|
if server.verbose then
|
|
server:log(
|
|
"Resolve response: %s", util.jsonprettify(json.encode(symbol))
|
|
)
|
|
end
|
|
elseif server.verbose then
|
|
server:log("Resolve returned empty response")
|
|
end
|
|
end
|
|
})
|
|
end
|
|
end
|
|
|
|
---Callback that handles insertion of an autocompletion item that has
|
|
---the information of insertion
|
|
---@param index integer
|
|
---@param item table
|
|
local function autocomplete_onselect(index, item)
|
|
local completion = item.data.completion_item
|
|
local dv = get_active_docview()
|
|
local edit_applied = false
|
|
if completion.textEdit then
|
|
if dv then
|
|
local is_snippet = completion.insertTextFormat
|
|
and completion.insertTextFormat == Server.insert_text_format.Snippet
|
|
edit_applied = apply_edit(item.data.server, dv.doc, completion.textEdit, is_snippet, true)
|
|
if edit_applied then
|
|
-- Retrigger code completion if last char is a trigger
|
|
-- this is useful for example with clangd when autocompleting
|
|
-- a #include, if user types < a list of paths will appear
|
|
-- when selecting a path that ends with / as <AL/ the
|
|
-- autocompletion will be retriggered to show a list of
|
|
-- header files that belong to that directory.
|
|
lsp.in_trigger = false
|
|
local line, col = dv.doc:get_selection()
|
|
local char = dv.doc:get_char(line, col-1)
|
|
local char_prev = dv.doc:get_char(line, col-2)
|
|
if char:match("%p") or (char == " " and char_prev:match("%p")) then
|
|
if not util.table_empty(dv.doc.lsp_changes) then
|
|
lsp.update_document(dv.doc, true)
|
|
else
|
|
lsp.request_completion(dv.doc, line, col, true)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
elseif
|
|
dv and snippets_found and config.plugins.lsp.snippets
|
|
and
|
|
completion.insertText and completion.insertTextFormat
|
|
and
|
|
completion.insertTextFormat == Server.insert_text_format.Snippet
|
|
then
|
|
---@type core.doc
|
|
local doc = dv.doc
|
|
if dv then
|
|
local line2, col2 = doc:get_selection()
|
|
local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
|
|
doc:set_selection(line1, col1, line2, col2)
|
|
snippets.execute {format = 'lsp', template = completion.insertText}
|
|
edit_applied = true
|
|
end
|
|
end
|
|
if edit_applied and completion.additionalTextEdits and #completion.additionalTextEdits > 0 then
|
|
-- TODO: do we need to sort this? Or is it expected to be already sorted?
|
|
-- TODO: are the edit ranges considered as if the "main" textEdit was applied already?
|
|
|
|
-- Apply the edits in reverse order, so that their ranges are not shifted
|
|
-- around by previous edits
|
|
for i=#completion.additionalTextEdits,1,-1 do
|
|
local edit = completion.additionalTextEdits[i]
|
|
apply_edit(item.data.server, dv.doc, edit, false, false)
|
|
end
|
|
end
|
|
return edit_applied
|
|
end
|
|
|
|
--
|
|
-- Public functions
|
|
--
|
|
|
|
---Open a document location returned by LSP
|
|
---@param location table
|
|
function lsp.goto_location(location)
|
|
local doc_view = core.root_view:open_doc(
|
|
core.open_doc(
|
|
common.home_expand(
|
|
util.tofilename(location.uri or location.targetUri)
|
|
)
|
|
)
|
|
)
|
|
local line1, col1 = util.toselection(
|
|
location.range or location.targetRange, doc_view.doc
|
|
)
|
|
doc_view.doc:set_selection(line1, col1, line1, col1)
|
|
end
|
|
|
|
lsp.get_location_preview = get_location_preview
|
|
|
|
---Register an LSP server to be launched on demand
|
|
---@param options lsp.server.options
|
|
function lsp.add_server(options)
|
|
local required_fields = {
|
|
"name", "language", "file_patterns", "command"
|
|
}
|
|
|
|
for _, field in pairs(required_fields) do
|
|
if not options[field] then
|
|
core.error(
|
|
"[LSP] You need to provide a '%s' field for the server.",
|
|
field
|
|
)
|
|
return false
|
|
end
|
|
end
|
|
|
|
if snippets_found and config.plugins.lsp.snippets then
|
|
options.snippets = true
|
|
end
|
|
|
|
if #options.command <= 0 then
|
|
core.error("[LSP] Provide a command table list with the lsp command.")
|
|
return false
|
|
end
|
|
|
|
-- some lsp servers may be installed with different binary names
|
|
-- so if command name is a list, search for one that exists
|
|
if type(options.command[1]) == "table" then
|
|
options.command[1] = util.get_best_executable(options.command[1])
|
|
end
|
|
|
|
-- On Windows using cmd.exe allows us to take advantage of its ability to run
|
|
-- the correct executable, as well as running scripts.
|
|
if PLATFORM == "Windows" and not options.windows_skip_cmd then
|
|
table.insert(options.command, 1, "/C")
|
|
table.insert(options.command, 1, "cmd.exe")
|
|
end
|
|
|
|
if config.plugins.lsp.force_verbosity_off then
|
|
options.verbose = false
|
|
end
|
|
|
|
lsp.servers[options.name] = options
|
|
|
|
return true
|
|
end
|
|
|
|
---Get valid running lsp servers for a given filename
|
|
---@param filename string
|
|
---@param initialized boolean
|
|
---@return table active_servers
|
|
function lsp.get_active_servers(filename, initialized)
|
|
local servers = {}
|
|
for name, server in pairs(lsp.servers) do
|
|
if common.match_pattern(filename, server.file_patterns) then
|
|
if lsp.servers_running[name] then
|
|
local add_server = true
|
|
if
|
|
initialized
|
|
and
|
|
(
|
|
not lsp.servers_running[name].initialized
|
|
or
|
|
not lsp.servers_running[name].capabilities
|
|
)
|
|
then
|
|
add_server = false
|
|
end
|
|
if add_server then
|
|
table.insert(servers, name)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
return servers
|
|
end
|
|
|
|
-- Used on lsp.get_workspace_settings()
|
|
local cached_workspace_settings = {}
|
|
local cached_workspace_settings_timestamp = 0
|
|
|
|
---Get table of configuration settings in the following way:
|
|
---1. Scan the USERDIR for .lite_lsp.lua or .lite_lsp.json (in that order)
|
|
---2. Merge server.settings
|
|
---4. Scan workspace if set also for .lite_lsp.lua/json and merge them or
|
|
---3. Scan server.path also for .lite_lsp.lua/json and merge them
|
|
---Note: settings are cached for 5 seconds for faster retrieval
|
|
--- on repetitive calls to this function.
|
|
---@param server lsp.server
|
|
---@param workspace? string
|
|
---@return table
|
|
function lsp.get_workspace_settings(server, workspace)
|
|
-- Search settings on the following directories, subsequent settings
|
|
-- overwrite the previous ones
|
|
local paths = { USERDIR }
|
|
local cached_index = USERDIR
|
|
local settings = {}
|
|
|
|
if not workspace and server.path then
|
|
table.insert(paths, server.path)
|
|
cached_index = cached_index .. tostring(server.path)
|
|
elseif workspace then
|
|
table.insert(paths, workspace)
|
|
cached_index = cached_index .. tostring(workspace)
|
|
end
|
|
|
|
if
|
|
cached_workspace_settings_timestamp > os.time()
|
|
and
|
|
cached_workspace_settings[cached_index]
|
|
then
|
|
return cached_workspace_settings[cached_index]
|
|
else
|
|
local position = 1
|
|
for _, path in pairs(paths) do
|
|
if path then
|
|
local settings_new = nil
|
|
path = path:gsub("\\+$", ""):gsub("/+$", "")
|
|
if util.file_exists(path .. "/.lite_lsp.lua") then
|
|
local settings_lua = dofile(path .. "/.lite_lsp.lua")
|
|
if type(settings_lua) == "table" then
|
|
settings_new = settings_lua
|
|
end
|
|
elseif util.file_exists(path .. "/.lite_lsp.json") then
|
|
local file = io.open(path .. "/.lite_lsp.json", "r")
|
|
if file then
|
|
local settings_json = file:read("*a")
|
|
settings_new = json.decode(settings_json)
|
|
file:close()
|
|
end
|
|
end
|
|
|
|
-- overwrite global settings by those specified in the server if any
|
|
if position == 1 and server.settings then
|
|
if settings_new then
|
|
settings_new = util.deep_merge(settings_new, server.settings)
|
|
else
|
|
settings_new = server.settings
|
|
end
|
|
end
|
|
|
|
-- overwrite previous settings with new ones
|
|
if settings_new then
|
|
settings = util.deep_merge(settings, settings_new)
|
|
end
|
|
end
|
|
|
|
position = position + 1
|
|
end
|
|
|
|
-- store settings on cache for 5 seconds for fast repeated calls
|
|
cached_workspace_settings[cached_index] = settings
|
|
cached_workspace_settings_timestamp = os.time() + 5
|
|
end
|
|
|
|
return settings
|
|
end
|
|
|
|
-- TODO Update workspace folders of already running lsp servers if required
|
|
--- Start all applicable lsp servers for a given file.
|
|
--- @param filename string
|
|
--- @param project_directory string
|
|
function lsp.start_server(filename, project_directory)
|
|
local server_started = false
|
|
local server_registered = false
|
|
local servers_not_found = {}
|
|
for name, server in pairs(lsp.servers) do
|
|
if common.match_pattern(filename, server.file_patterns) then
|
|
server_registered = true
|
|
if lsp.servers_running[name] then
|
|
server_started = true
|
|
end
|
|
|
|
local command_exists = false
|
|
if util.command_exists(server.command[1]) then
|
|
command_exists = true
|
|
else
|
|
table.insert(servers_not_found, name)
|
|
end
|
|
|
|
if not lsp.servers_running[name] and command_exists then
|
|
core.log("[LSP] starting " .. name)
|
|
---@type lsp.server
|
|
local client = Server(server)
|
|
client.yield_on_reads = config.plugins.lsp.more_yielding
|
|
|
|
lsp.servers_running[name] = client
|
|
|
|
-- We overwrite the default log function to log messages on lite
|
|
function client:log(message, ...)
|
|
core.log_quiet(
|
|
"[LSP/%s]: " .. message .. "\n",
|
|
self.name,
|
|
...
|
|
)
|
|
end
|
|
|
|
function client:on_shutdown()
|
|
local sname = self.name
|
|
core.log(
|
|
"[LSP]: %s was shutdown, revise your configuration",
|
|
sname
|
|
)
|
|
local last_shutdown = lsp.servers_running[sname].last_shutdown or 0
|
|
lsp.servers_running = util.table_remove_key(
|
|
lsp.servers_running,
|
|
sname
|
|
)
|
|
if system.get_time() - last_shutdown >= 5 then
|
|
lsp.start_servers()
|
|
if lsp.servers_running[sname] then
|
|
lsp.servers_running[sname].last_shutdown = system.get_time()
|
|
core.log(
|
|
"[LSP]: %s automatically restarted",
|
|
sname
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- Respond to workspace/configuration request
|
|
client:add_request_listener(
|
|
"workspace/configuration",
|
|
function(server, request)
|
|
local settings_default = lsp.get_workspace_settings(server)
|
|
|
|
local settings_list = {}
|
|
for _, item in pairs(request.params.items) do
|
|
local value = nil
|
|
-- No workspace was specified so we return from default settings
|
|
if not item.scopeUri then
|
|
value = util.table_get_field(settings_default, item.section)
|
|
-- A workspace was specified so we return from that workspace
|
|
else
|
|
local settings_workspace = lsp.get_workspace_settings(
|
|
server, util.tofilename(item.scopeUri)
|
|
)
|
|
value = util.table_get_field(settings_workspace, item.section)
|
|
end
|
|
|
|
if not value then
|
|
server:log("Asking for '%s' config but not set", item.section)
|
|
else
|
|
server:log("Asking for '%s' config", item.section)
|
|
end
|
|
|
|
table.insert(settings_list, value or json.null)
|
|
end
|
|
server:push_response(request.method, request.id, settings_list)
|
|
end
|
|
)
|
|
|
|
-- Respond to window/showDocument request
|
|
client:add_request_listener(
|
|
"window/showDocument",
|
|
function(server, request)
|
|
if request.params.external then
|
|
MessageBox.info(
|
|
server.name .. " LSP Server",
|
|
"Wants to externally open:\n'" .. request.params.uri .. "'",
|
|
function(_, button_id)
|
|
if button_id == 1 then
|
|
util.open_external(request.params.uri)
|
|
end
|
|
end,
|
|
MessageBox.BUTTONS_YES_NO
|
|
)
|
|
else
|
|
local document = util.tofilename(request.params.uri)
|
|
---@type core.docview
|
|
local doc_view = core.root_view:open_doc(
|
|
core.open_doc(common.home_expand(document))
|
|
)
|
|
if request.params.selection then
|
|
local line1, col1, line2, col2 = util.toselection(
|
|
request.params.selection, doc_view.doc
|
|
)
|
|
doc_view.doc:set_selection(line1, col1, line2, col2)
|
|
end
|
|
if request.params.takeFocus then
|
|
system.raise_window()
|
|
end
|
|
end
|
|
|
|
server:push_response(request.method, request.id, {success=true})
|
|
end
|
|
)
|
|
|
|
-- Display server messages on lite UI
|
|
client:add_message_listener(
|
|
"window/logMessage",
|
|
function(server, params)
|
|
if core.log then
|
|
log(server, "%s", params.message)
|
|
end
|
|
end
|
|
)
|
|
|
|
-- Register/unregister diagnostic messages
|
|
client:add_message_listener(
|
|
"textDocument/publishDiagnostics",
|
|
function(server, params)
|
|
local abs_filename = util.tofilename(params.uri)
|
|
local filename = core.normalize_to_project_dir(abs_filename)
|
|
|
|
if server.verbose then
|
|
core.log_quiet(
|
|
"["..server.name.."] %s diagnostics for: %s",
|
|
filename,
|
|
params.diagnostics and #params.diagnostics or 0
|
|
)
|
|
end
|
|
|
|
if params.diagnostics and #params.diagnostics > 0 then
|
|
local added = diagnostics.add(filename, params.diagnostics)
|
|
|
|
if
|
|
added and diagnostics.lintplus_found
|
|
and
|
|
config.plugins.lsp.show_diagnostics
|
|
and
|
|
util.doc_is_open(abs_filename)
|
|
then
|
|
-- we delay rendering of diagnostics for 2 seconds to prevent
|
|
-- the constant reporting of errors while typing.
|
|
diagnostics.lintplus_populate_delayed(filename)
|
|
end
|
|
else
|
|
diagnostics.clear(filename)
|
|
diagnostics.lintplus_clear_messages(filename)
|
|
end
|
|
end
|
|
)
|
|
|
|
-- Register/unregister diagnostic messages
|
|
client:add_message_listener(
|
|
"window/showMessage",
|
|
function(server, params)
|
|
local log_func = "log_quiet"
|
|
if params.type == Server.message_type.Error then
|
|
log_func = "error"
|
|
elseif params.type == Server.message_type.Warning then
|
|
log_func = "warn"
|
|
elseif params.type == Server.message_type.Info then
|
|
log_func = "log"
|
|
elseif params.type == Server.message_type.Debug then
|
|
log_func = "log_quiet"
|
|
end
|
|
core[log_func]("["..server.name.."] message: %s", params.message)
|
|
end
|
|
)
|
|
|
|
-- Send settings table after initialization if available.
|
|
client:add_event_listener("initialized", function(server)
|
|
if config.plugins.lsp.force_verbosity_off then
|
|
core.log_quiet("["..server.name.."] " .. "Initialized")
|
|
else
|
|
log(server, "Initialized")
|
|
end
|
|
local settings = lsp.get_workspace_settings(server)
|
|
if not util.table_empty(settings) then
|
|
server:push_notification("workspace/didChangeConfiguration", {
|
|
params = {settings = settings}
|
|
})
|
|
end
|
|
|
|
-- Send open document request if needed
|
|
for _, docu in ipairs(core.docs) do
|
|
if docu.filename then
|
|
if common.match_pattern(docu.filename, server.file_patterns) then
|
|
lsp.open_document(docu)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- Start the server initialization process
|
|
client:initialize(project_directory, "Lite XL", VERSION)
|
|
end
|
|
end
|
|
end
|
|
|
|
if server_registered and not server_started then
|
|
for _,_ in pairs(servers_not_found) do
|
|
core.error(
|
|
"[LSP] servers registered but not installed: %s",
|
|
table.concat(servers_not_found, ", ")
|
|
)
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
---Stops all running servers.
|
|
function lsp.stop_servers()
|
|
for name, _ in pairs(lsp.servers) do
|
|
if lsp.servers_running[name] then
|
|
lsp.servers_running[name]:exit()
|
|
core.log("[LSP] stopped %s", name)
|
|
lsp.servers_running = util.table_remove_key(lsp.servers_running, name)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Start only the needed servers by current opened documents.
|
|
function lsp.start_servers()
|
|
for _, doc in ipairs(core.docs) do
|
|
if doc.filename then
|
|
lsp.start_server(doc.filename, core.project_dir)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Returns the hovered doc and the hovered position.
|
|
---Returns nil if no doc with an LSP activated is under the provided coordinates.
|
|
---@param x number
|
|
---@param y number
|
|
---@return core.doc|nil doc
|
|
---@return integer|nil line
|
|
---@return integer|nil col
|
|
function lsp.get_hovered_location(x, y)
|
|
local n = core.root_view.root_node:get_child_overlapping_point(x, y)
|
|
if not n then return end
|
|
local av = n.active_view
|
|
if not av:extends(DocView) then return end
|
|
if av and av.doc.lsp_open then
|
|
---@type core.doc
|
|
local doc = av.doc
|
|
local line, col = av:resolve_screen_position(x, y)
|
|
local last_x = av:get_col_x_offset(line, #av.doc.lines[line])
|
|
local lx, ly = av:get_line_screen_position(line)
|
|
if x > last_x + lx or y > ly + av:get_line_height() then return end
|
|
return doc, line, col
|
|
end
|
|
end
|
|
|
|
---Send notification to applicable LSP servers that a document was opened
|
|
---@param doc core.doc
|
|
function lsp.open_document(doc)
|
|
-- in some rare ocassions this function may return nil when the
|
|
-- user closed lite-xl with files opened, removed the files from system
|
|
-- and opens lite-xl again which loads the non existent files.
|
|
local doc_path = core.project_absolute_path(doc.filename)
|
|
local file_info = system.get_file_info(doc_path)
|
|
if not file_info then
|
|
core.error("[LSP] could not open: %s", tostring(doc.filename))
|
|
return
|
|
end
|
|
|
|
local active_servers = lsp.get_active_servers(doc.filename, true)
|
|
|
|
if #active_servers > 0 then
|
|
doc.disable_symbols = true -- disable symbol parsing on autocomplete plugin
|
|
for _, name in pairs(active_servers) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.textDocumentSync.openClose then
|
|
if server.exit_timer then
|
|
server.exit_timer:stop()
|
|
server.exit_timer = nil
|
|
end
|
|
if file_info.size / 1024 <= 50 then
|
|
-- file size is in range so push the notification as usual.
|
|
server:push_notification('textDocument/didOpen', {
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(doc_path),
|
|
languageId = server:get_language_id(doc),
|
|
version = doc.clean_change_id,
|
|
text = table.concat(doc.lines)
|
|
}
|
|
},
|
|
callback = function() doc.lsp_open = true end
|
|
})
|
|
else
|
|
-- big files too slow for json encoder, also sending a huge file
|
|
-- without yielding would stall the ui, and some lsp servers have
|
|
-- issues with receiving big files in a single chunk.
|
|
local text = table.concat(doc.lines)
|
|
:gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
|
|
:gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
|
|
:gsub('\f', '\\f')
|
|
|
|
server:push_raw("textDocument/didOpen", {
|
|
raw_data = '{\n'
|
|
.. '"jsonrpc": "2.0",\n'
|
|
.. '"method": "textDocument/didOpen",\n'
|
|
.. '"params": {\n'
|
|
.. '"textDocument": {\n'
|
|
.. '"uri": "'..util.touri(doc_path)..'",\n'
|
|
.. '"languageId": "'..server:get_language_id(doc)..'",\n'
|
|
.. '"version": '..doc.clean_change_id..',\n'
|
|
.. '"text": "'..text..'"\n'
|
|
.. '}\n'
|
|
.. '}\n'
|
|
.. '}\n',
|
|
callback = function(server)
|
|
doc.lsp_open = true
|
|
log(server, "Big file '%s' ready for completion!", doc.filename)
|
|
end
|
|
})
|
|
|
|
log(server, "Processing big file '%s'...", doc.filename)
|
|
end
|
|
else
|
|
doc.lsp_open = true
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Send notification to applicable LSP servers that a document was saved
|
|
---@param doc core.doc
|
|
function lsp.save_document(doc)
|
|
if not doc.lsp_open then return end
|
|
|
|
local active_servers = lsp.get_active_servers(doc.filename, true)
|
|
if #active_servers > 0 then
|
|
for _, name in pairs(active_servers) do
|
|
local server = lsp.servers_running[name]
|
|
local save = server.capabilities.textDocumentSync.save
|
|
if save then
|
|
-- Send document content only if required by lsp server
|
|
if save.includeText then
|
|
-- If save should include file content then raw is faster for
|
|
-- huge files that would take too much to encode.
|
|
local text = table.concat(doc.lines)
|
|
:gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
|
|
:gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
|
|
:gsub('\f', '\\f')
|
|
|
|
server:push_raw("textDocument/didSave", {
|
|
raw_data = '{\n'
|
|
.. '"jsonrpc": "2.0",\n'
|
|
.. '"method": "textDocument/didSave",\n'
|
|
.. '"params": {\n'
|
|
.. '"textDocument": {\n'
|
|
.. '"uri": "'..util.touri(core.project_absolute_path(doc.filename))..'"\n'
|
|
.. '},\n'
|
|
.. '"text": "'..text..'"\n'
|
|
.. '}\n'
|
|
.. '}\n'
|
|
})
|
|
else
|
|
server:push_notification('textDocument/didSave', {
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename))
|
|
}
|
|
}
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Send notification to applicable LSP servers that a document was closed
|
|
---@param doc core.doc
|
|
function lsp.close_document(doc)
|
|
if not doc.lsp_open then return end
|
|
|
|
local active_servers = lsp.get_active_servers(doc.filename, true)
|
|
if #active_servers > 0 then
|
|
for _, name in pairs(active_servers) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.textDocumentSync.openClose then
|
|
server:push_notification('textDocument/didClose', {
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename)),
|
|
languageId = server:get_language_id(doc),
|
|
version = doc.clean_change_id
|
|
}
|
|
}
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Helper for lsp.update_document
|
|
---@param doc core.doc
|
|
local function request_signature_completion(doc)
|
|
local line1, col1, line2, col2 = doc:get_selection()
|
|
|
|
if line1 == line2 and col1 == col2 then
|
|
-- First try to display a function signatures and if not possible
|
|
-- do normal code autocomplete
|
|
lsp.request_signature(
|
|
doc,
|
|
line1,
|
|
col1,
|
|
false,
|
|
lsp.request_completion
|
|
)
|
|
end
|
|
end
|
|
|
|
---Send document updates to applicable running LSP servers.
|
|
---@param doc core.doc
|
|
---@param request_completion? boolean
|
|
function lsp.update_document(doc, request_completion)
|
|
if not doc.lsp_open or not doc.lsp_changes or util.table_empty(doc.lsp_changes) then
|
|
return
|
|
end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if not doc.lsp_changes[server] or #doc.lsp_changes[server] <= 0 then
|
|
goto continue
|
|
end
|
|
local sync_kind = server.capabilities.textDocumentSync.change
|
|
if
|
|
sync_kind ~= Server.text_document_sync_kind.None
|
|
and
|
|
server:can_push() -- ensure we don't loose incremental changes
|
|
then
|
|
local completion_callback = nil
|
|
if request_completion then
|
|
completion_callback = function() request_signature_completion(doc) end
|
|
end
|
|
|
|
if
|
|
sync_kind == Server.text_document_sync_kind.Full
|
|
and
|
|
not server.incremental_changes
|
|
then
|
|
-- If sync should be done by sending full file content then lets do
|
|
-- it raw which is faster for big files.
|
|
local text = table.concat(doc.lines)
|
|
:gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
|
|
:gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
|
|
:gsub('\f', '\\f')
|
|
|
|
server:push_raw("textDocument/didChange", {
|
|
overwrite = true,
|
|
raw_data = '{\n'
|
|
.. '"jsonrpc": "2.0",\n'
|
|
.. '"method": "textDocument/didChange",\n'
|
|
.. '"params": {\n'
|
|
.. '"textDocument": {\n'
|
|
.. '"uri": "'..util.touri(core.project_absolute_path(doc.filename))..'",\n'
|
|
.. '"version": '..doc.lsp_version .. "\n"
|
|
.. '},\n'
|
|
.. '"contentChanges": [\n'
|
|
.. '{"text": "'..text..'"}\n'
|
|
.. "]\n"
|
|
.. '}\n'
|
|
.. '}\n',
|
|
callback = function()
|
|
doc.lsp_changes[server] = nil
|
|
if completion_callback then
|
|
completion_callback()
|
|
end
|
|
end
|
|
})
|
|
else
|
|
lsp.servers_running[name]:push_notification('textDocument/didChange', {
|
|
overwrite = true,
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename)),
|
|
version = doc.lsp_version,
|
|
},
|
|
contentChanges = doc.lsp_changes[server]
|
|
},
|
|
callback = function()
|
|
doc.lsp_changes[server] = nil
|
|
if completion_callback then
|
|
completion_callback()
|
|
end
|
|
end
|
|
})
|
|
end
|
|
end
|
|
::continue::
|
|
end
|
|
end
|
|
|
|
--- Enable or disable diagnostic messages
|
|
function lsp.toggle_diagnostics()
|
|
config.plugins.lsp.show_diagnostics = not config.plugins.lsp.show_diagnostics
|
|
|
|
if not config.plugins.lsp.show_diagnostics then
|
|
diagnostics.lintplus_clear_messages()
|
|
core.log("[LSP] Diagnostics disabled")
|
|
else
|
|
diagnostics.lintplus_populate()
|
|
core.log("[LSP] Diagnostics enabled")
|
|
end
|
|
end
|
|
|
|
--- Send to applicable LSP servers a request for code completion
|
|
function lsp.request_completion(doc, line, col, forced)
|
|
if lsp.in_trigger or not doc.lsp_open then
|
|
return
|
|
end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.completionProvider then
|
|
local capabilities = lsp.servers_running[name].capabilities
|
|
local char = doc:get_char(line, col-1)
|
|
local trigger_char = false
|
|
|
|
local request = get_buffer_position_params(doc, line, col)
|
|
|
|
-- without providing context some language servers like the
|
|
-- lua-language-server behave poorly and return garbage.
|
|
if
|
|
capabilities.completionProvider.triggerCharacters
|
|
and
|
|
#capabilities.completionProvider.triggerCharacters > 0
|
|
and
|
|
char:match("%p")
|
|
and
|
|
util.intable(char, capabilities.completionProvider.triggerCharacters)
|
|
then
|
|
request.context = {
|
|
triggerKind = Server.completion_trigger_Kind.TriggerCharacter,
|
|
triggerCharacter = char
|
|
}
|
|
trigger_char = true;
|
|
end
|
|
|
|
if
|
|
not trigger_char
|
|
and
|
|
not autocomplete.can_complete()
|
|
and
|
|
not forced
|
|
then
|
|
return false
|
|
end
|
|
|
|
server:push_request('textDocument/completion', {
|
|
params = request,
|
|
overwrite = true,
|
|
callback = function(server, response)
|
|
lsp.user_typed = false
|
|
|
|
-- don't autocomplete if caret position changed
|
|
local cline, cchar = doc:get_selection()
|
|
if cline ~= line or cchar ~= col then
|
|
return
|
|
end
|
|
|
|
if server.verbose then
|
|
server:log(
|
|
"Completion response received."
|
|
)
|
|
end
|
|
|
|
if not response.result then
|
|
return
|
|
end
|
|
|
|
local result = response.result
|
|
local complete_result = true
|
|
if result.isIncomplete then
|
|
if server.verbose then
|
|
core.log_quiet(
|
|
"["..server.name.."] " .. "Completion list incomplete"
|
|
)
|
|
end
|
|
complete_result = false
|
|
end
|
|
|
|
if not result.items or #result.items <= 0 then
|
|
-- Workaround for some lsp servers that don't return results
|
|
-- in the items property but instead on the results it self
|
|
if #result > 0 then
|
|
local items = result
|
|
result = {items = items}
|
|
else
|
|
return
|
|
end
|
|
end
|
|
|
|
local symbols = {
|
|
name = lsp.servers_running[name].name,
|
|
files = lsp.servers_running[name].file_patterns,
|
|
items = {}
|
|
}
|
|
|
|
local symbol_count = 1
|
|
for _, symbol in ipairs(result.items) do
|
|
local label = symbol.label
|
|
or (
|
|
symbol.textEdit
|
|
and symbol.textEdit.newText
|
|
or symbol.insertText
|
|
)
|
|
|
|
local info = server.get_completion_item_kind(symbol.kind)
|
|
|
|
local desc = symbol.detail or ""
|
|
|
|
-- TODO: maybe we should give priority to insertText above
|
|
if
|
|
symbol.label and
|
|
symbol.insertText and
|
|
#symbol.label > #symbol.insertText
|
|
then
|
|
label = symbol.insertText
|
|
if symbol.label ~= label then
|
|
desc = symbol.label
|
|
end
|
|
if symbol.detail then
|
|
desc = desc .. ": " .. symbol.detail
|
|
end
|
|
end
|
|
|
|
if desc ~= "" then
|
|
desc = desc .. "\n"
|
|
end
|
|
|
|
if
|
|
type(symbol.documentation) == "table"
|
|
and
|
|
symbol.documentation.value
|
|
then
|
|
desc = desc .. "\n" .. symbol.documentation.value
|
|
if
|
|
symbol.documentation.kind
|
|
and
|
|
symbol.documentation.kind == "markdown"
|
|
then
|
|
desc = util.strip_markdown(desc)
|
|
if symbol_count % 10 == 0 then
|
|
coroutine.yield()
|
|
end
|
|
end
|
|
elseif symbol.documentation then
|
|
desc = desc .. "\n" .. symbol.documentation
|
|
end
|
|
|
|
desc = desc:gsub("[%s\n]+$", "")
|
|
:gsub("\n\n\n+", "\n\n")
|
|
|
|
symbols.items[label] = {
|
|
info = info,
|
|
desc = desc,
|
|
data = {
|
|
server = server, completion_item = symbol
|
|
},
|
|
onselect = autocomplete_onselect
|
|
}
|
|
|
|
if
|
|
server.capabilities.completionProvider.resolveProvider
|
|
and
|
|
not symbol.documentation
|
|
then
|
|
symbols.items[label].onhover = autocomplete_onhover
|
|
end
|
|
|
|
symbol_count = symbol_count + 1
|
|
end
|
|
|
|
if trigger_char and complete_result then
|
|
lsp.in_trigger = true
|
|
autocomplete.complete(symbols, function()
|
|
lsp.in_trigger = false
|
|
end)
|
|
else
|
|
autocomplete.complete(symbols)
|
|
end
|
|
end
|
|
})
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Send to applicable LSP servers a request for info about a function
|
|
--- signatures and display them on a tooltip.
|
|
function lsp.request_signature(doc, line, col, forced, fallback)
|
|
if not doc.lsp_open then return end
|
|
|
|
local char = doc:get_char(line, col-1)
|
|
local prev_char = doc:get_char(line, col-2) -- to support ', '
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if
|
|
server.capabilities.signatureHelpProvider
|
|
and
|
|
(
|
|
forced
|
|
or
|
|
(
|
|
server.capabilities.signatureHelpProvider.triggerCharacters
|
|
and
|
|
#server.capabilities.signatureHelpProvider.triggerCharacters > 0
|
|
and
|
|
(
|
|
util.intable(
|
|
char, server.capabilities.signatureHelpProvider.triggerCharacters
|
|
)
|
|
or
|
|
util.intable(
|
|
prev_char,
|
|
server.capabilities.signatureHelpProvider.triggerCharacters
|
|
)
|
|
)
|
|
)
|
|
)
|
|
then
|
|
server:push_request('textDocument/signatureHelp', {
|
|
params = get_buffer_position_params(doc, line, col),
|
|
overwrite = true,
|
|
callback = function(server, response)
|
|
-- don't show signature if caret position changed
|
|
local cline, cchar = doc:get_selection()
|
|
if cline ~= line or cchar ~= col then
|
|
return
|
|
end
|
|
|
|
if
|
|
response.result
|
|
and
|
|
response.result.signatures
|
|
and
|
|
#response.result.signatures > 0
|
|
then
|
|
autocomplete.close()
|
|
listbox.show_signatures(response.result)
|
|
lsp.user_typed = false
|
|
elseif fallback then
|
|
fallback(doc, line, col)
|
|
end
|
|
end
|
|
})
|
|
break
|
|
elseif fallback then
|
|
fallback(doc, line, col)
|
|
end
|
|
end
|
|
end
|
|
|
|
---Returns the "selection" for the token that includes the provided position.
|
|
---@param doc core.doc
|
|
---@param line integer
|
|
---@param col integer
|
|
---@return integer line1
|
|
---@return integer col2
|
|
---@return integer line2
|
|
---@return integer col2
|
|
local function get_token_range(doc, line, col)
|
|
local col1 = 0
|
|
for _, _, text in doc.highlighter:each_token(line) do
|
|
local text_len = #text
|
|
local col2 = col1 + text_len
|
|
if col2 >= col then
|
|
return line, col1 + 1, line, col2 + 1
|
|
end
|
|
col1 = col2
|
|
end
|
|
return line, col, line, col+1
|
|
end
|
|
|
|
---@type core.node
|
|
local help_active_node = nil
|
|
---@type core.node
|
|
local help_bottom_node = nil
|
|
--- Sends a request to applicable LSP servers for information about the
|
|
--- symbol where the cursor is placed and shows it on a tooltip.
|
|
function lsp.request_hover(doc, line, col, in_tab)
|
|
if not doc.lsp_open then return end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.hoverProvider then
|
|
server:push_request('textDocument/hover', {
|
|
params = get_buffer_position_params(doc, line, col),
|
|
callback = function(server, response)
|
|
if response.result and response.result.contents then
|
|
local range = response.result.range
|
|
local line1, col1, line2, col2
|
|
if range then
|
|
line1, col1, line2, col2 = util.toselection(range, doc)
|
|
else
|
|
line1, col1, line2, col2 = get_token_range(doc, line, col)
|
|
end
|
|
lsp.hover_position.utf8_range = { line1 = line1, col1 = col1,
|
|
line2 = line2, col2 = col2 }
|
|
|
|
local content = response.result.contents
|
|
local kind = nil
|
|
local text = ""
|
|
if type(content) == "table" then
|
|
if content.value then
|
|
text = content.value
|
|
if content.kind then kind = content.kind end
|
|
else
|
|
for _, element in pairs(content) do
|
|
if type(element) == "string" then
|
|
text = text .. element
|
|
elseif type(element) == "table" and element.value then
|
|
text = text .. element.value
|
|
if not kind and element.kind then kind = element.kind end
|
|
end
|
|
end
|
|
end
|
|
else -- content should be a string
|
|
text = content
|
|
end
|
|
if text and #text > 0 then
|
|
text = text:gsub("^[\n%s]+", ""):gsub("[\n%s]+$", "")
|
|
if not in_tab then
|
|
if kind == "markdown" then text = util.strip_markdown(text) end
|
|
listbox.show_text(
|
|
text,
|
|
{ line = line, col = col }
|
|
)
|
|
else
|
|
local line1, col1 = translate.start_of_word(doc, line, col)
|
|
local line2, col2 = translate.end_of_word(doc, line1, col1)
|
|
local title = doc:get_text(line1, col1, line2, col2):gsub("%s*", "")
|
|
title = "Help:" .. title .. ".md"
|
|
---@type lsp.helpdoc
|
|
local helpdoc = HelpDoc(title, title)
|
|
helpdoc:set_text(text)
|
|
local helpview = DocView(helpdoc)
|
|
helpview.context = "application"
|
|
helpview.wrapping_enabled = true
|
|
if LineWrapping then
|
|
LineWrapping.update_docview_breaks(helpview)
|
|
end
|
|
if
|
|
not help_bottom_node
|
|
or
|
|
(
|
|
#help_bottom_node.views == 1
|
|
and
|
|
not help_active_node:get_node_for_view(help_bottom_node.views[1])
|
|
)
|
|
then
|
|
help_active_node = core.root_view:get_active_node_default()
|
|
help_bottom_node = help_active_node:split("down", helpview)
|
|
else
|
|
help_bottom_node:add_view(helpview)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
})
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
--- Sends a request to applicable LSP servers for a symbol references
|
|
function lsp.request_references(doc, line, col)
|
|
if not doc.lsp_open then return end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.hoverProvider then
|
|
local request_params = get_buffer_position_params(doc, line, col)
|
|
request_params.context = {includeDeclaration = true}
|
|
server:push_request('textDocument/references', {
|
|
params = request_params,
|
|
callback = function(server, response)
|
|
if response.result and #response.result > 0 then
|
|
local references, reference_names = get_references_lists(response.result)
|
|
core.command_view:enter("Filter References", {
|
|
submit = function(text, item)
|
|
if item then
|
|
local reference = references[item.name]
|
|
lsp.goto_location(reference)
|
|
end
|
|
end,
|
|
suggest = function(text)
|
|
local res = common.fuzzy_match(reference_names, text)
|
|
for i, name in ipairs(res) do
|
|
local reference_info = util.split(name, "||")
|
|
res[i] = {
|
|
text = reference_info[1],
|
|
info = reference_info[2],
|
|
name = name
|
|
}
|
|
end
|
|
return res
|
|
end
|
|
})
|
|
else
|
|
core.log("[LSP] No references found.")
|
|
end
|
|
end
|
|
})
|
|
break
|
|
end
|
|
break
|
|
end
|
|
end
|
|
|
|
---Sends a request to applicable LSP servers to retrieve the
|
|
---hierarchy of calls for the given function under the cursor.
|
|
function lsp.request_call_hierarchy(doc, line, col)
|
|
if not doc.lsp_open then return end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.callHierarchyProvider then
|
|
server:push_request('textDocument/prepareCallHierarchy', {
|
|
params = get_buffer_position_params(doc, line, col),
|
|
callback = function(server, response)
|
|
if response.result and #response.result > 0 then
|
|
-- TODO: Finish implement call hierarchy functionality
|
|
return
|
|
end
|
|
end
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
core.log("[LSP] Call hierarchy not supported.")
|
|
end
|
|
|
|
---Sends a request to applicable LSP servers to rename a symbol.
|
|
---@param doc core.doc
|
|
---@param line integer
|
|
---@param col integer
|
|
---@param new_name string
|
|
function lsp.request_symbol_rename(doc, line, col, new_name)
|
|
if not doc.lsp_open then return end
|
|
|
|
local servers_found = false
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
servers_found = true
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.renameProvider then
|
|
local request_params = get_buffer_position_params(doc, line, col)
|
|
request_params.newName = new_name
|
|
server:push_request('textDocument/rename', {
|
|
params = request_params,
|
|
callback = function(server, response)
|
|
if response.result and #response.result.changes then
|
|
for file_uri, changes in pairs(response.result.changes) do
|
|
core.log(file_uri .. " " .. #changes)
|
|
-- TODO: Finish implement textDocument/rename
|
|
end
|
|
end
|
|
|
|
core.log("%s", json.prettify(json.encode(response)))
|
|
end
|
|
})
|
|
return
|
|
end
|
|
end
|
|
|
|
if not servers_found then
|
|
core.log("[LSP] " .. "No server ready or running")
|
|
else
|
|
core.log("[LSP] " .. "Symbols rename not supported")
|
|
end
|
|
end
|
|
|
|
---Sends a request to applicable LSP servers to search for symbol on workspace.
|
|
---@param doc core.doc
|
|
---@param symbol string
|
|
function lsp.request_workspace_symbol(doc, symbol)
|
|
if not doc.lsp_open then return end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.workspaceSymbolProvider then
|
|
local rs = SymbolResults(symbol)
|
|
core.root_view:get_active_node_default():add_view(rs)
|
|
server:push_request('workspace/symbol', {
|
|
params = {
|
|
query = symbol,
|
|
-- TODO: implement status notifications but seems not supported
|
|
-- by tested lsp servers so far.
|
|
-- workDoneToken = "some-identifier",
|
|
-- partialResultToken = "some-other-identifier"
|
|
},
|
|
callback = function(server, response)
|
|
if response.result and #response.result > 0 then
|
|
for index, result in ipairs(response.result) do
|
|
rs:add_result(result)
|
|
if index % 100 == 0 then
|
|
coroutine.yield()
|
|
rs.list:resize_to_parent()
|
|
end
|
|
end
|
|
rs.list:resize_to_parent()
|
|
end
|
|
rs:stop_searching()
|
|
end
|
|
})
|
|
break
|
|
end
|
|
break
|
|
end
|
|
end
|
|
|
|
--- Request a list of symbols for the given document for easy document
|
|
-- navigation and displays them using core.command_view:enter()
|
|
function lsp.request_document_symbols(doc)
|
|
if not doc.lsp_open then return end
|
|
|
|
local servers_found = false
|
|
local symbols_retrieved = false
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
servers_found = true
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.documentSymbolProvider then
|
|
log(server, "Retrieving document symbols...")
|
|
server:push_request('textDocument/documentSymbol', {
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename)),
|
|
}
|
|
},
|
|
callback = function(server, response)
|
|
if response.result and response.result and #response.result > 0 then
|
|
local symbols, symbol_names = get_symbol_lists(response.result)
|
|
core.command_view:enter("Find Symbol", {
|
|
submit = function(text, item)
|
|
if item then
|
|
local symbol = symbols[item.name]
|
|
-- The lsp may return a location object with range
|
|
-- and uri inside of it or just range as part of
|
|
-- the symbol it self.
|
|
symbol = symbol.location and symbol.location or symbol
|
|
if not symbol.uri then
|
|
local line1, col1 = util.toselection(symbol.range, doc)
|
|
doc:set_selection(line1, col1, line1, col1)
|
|
else
|
|
lsp.goto_location(symbol)
|
|
end
|
|
end
|
|
end,
|
|
suggest = function(text)
|
|
local res = common.fuzzy_match(symbol_names, text)
|
|
for i, name in ipairs(res) do
|
|
res[i] = {
|
|
text = util.split(name, "||")[1],
|
|
info = Server.get_symbol_kind(symbols[name].kind),
|
|
name = name
|
|
}
|
|
end
|
|
return res
|
|
end
|
|
})
|
|
end
|
|
end
|
|
})
|
|
symbols_retrieved = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not servers_found then
|
|
core.log("[LSP] " .. "No server running")
|
|
elseif not symbols_retrieved then
|
|
core.log("[LSP] " .. "Document symbols not supported")
|
|
end
|
|
end
|
|
|
|
--- Format current document if supported by one of the running lsp servers.
|
|
function lsp.request_document_format(doc)
|
|
if not doc.lsp_open then return end
|
|
|
|
local servers_found = false
|
|
local format_executed = false
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
servers_found = true
|
|
local server = lsp.servers_running[name]
|
|
if server.capabilities.documentFormattingProvider then
|
|
local trim_trailing_whitespace = false
|
|
local trim_newlines = false
|
|
if type(config.plugins.trimwhitespace) == "table"
|
|
and config.plugins.trimwhitespace.enabled
|
|
then
|
|
trim_trailing_whitespace = true
|
|
trim_newlines = config.plugins.trimwhitespace.trim_empty_end_lines
|
|
elseif config.plugins.trimwhitespace then -- Plugin enabled with true
|
|
trim_trailing_whitespace = true
|
|
trim_newlines = true
|
|
end
|
|
local indent_type, indent_size, indent_confirmed = doc:get_indent_info()
|
|
if not indent_confirmed then
|
|
indent_type, indent_size = config.tab_type, config.indent_size
|
|
end
|
|
server:push_request('textDocument/formatting', {
|
|
params = {
|
|
textDocument = {
|
|
uri = util.touri(core.project_absolute_path(doc.filename)),
|
|
},
|
|
options = {
|
|
tabSize = indent_size,
|
|
insertSpaces = indent_type == "soft",
|
|
trimTrailingWhitespace = trim_trailing_whitespace,
|
|
insertFinalNewline = false,
|
|
trimFinalNewlines = trim_newlines
|
|
}
|
|
},
|
|
callback = function(server, response)
|
|
if response.error and response.error.message then
|
|
log(server, "Error formatting: " .. response.error.message)
|
|
elseif response.result and #response.result > 0 then
|
|
-- Apply edits in reverse, as the ranges don't consider
|
|
-- the intermediate states.
|
|
-- Consider the TextEdits as already sorted.
|
|
-- If there are servers that don't sort their TextEdits,
|
|
-- we'll add sorting code.
|
|
for i=#response.result,1,-1 do
|
|
apply_edit(server, doc, response.result[i], false, false)
|
|
end
|
|
log(server, "Formatted document")
|
|
else
|
|
log(server, "Formatting not required")
|
|
end
|
|
end
|
|
})
|
|
format_executed = true
|
|
break
|
|
end
|
|
end
|
|
|
|
if not servers_found then
|
|
core.log("[LSP] " .. "No server running")
|
|
elseif not format_executed then
|
|
core.log("[LSP] " .. "Formatting not supported")
|
|
end
|
|
end
|
|
|
|
function lsp.view_document_diagnostics(doc)
|
|
local diagnostic_messages = diagnostics.get(core.project_absolute_path(doc.filename))
|
|
if not diagnostic_messages or #diagnostic_messages <= 0 then
|
|
core.log("[LSP] %s", "No diagnostic messages found.")
|
|
return
|
|
end
|
|
|
|
local diagnostic_labels = { "Error", "Warning", "Info", "Hint" }
|
|
|
|
local indexes, captions = {}, {}
|
|
for index, diagnostic in pairs(diagnostic_messages) do
|
|
local line1, col1 = util.toselection(diagnostic.range)
|
|
local label = diagnostic_labels[diagnostic.severity]
|
|
.. ": " .. diagnostic.message .. " "
|
|
.. tostring(line1) .. ":" .. tostring(col1)
|
|
captions[index] = label
|
|
indexes[label] = index
|
|
end
|
|
|
|
core.command_view:enter("Filter Diagnostics", {
|
|
submit = function(text, item)
|
|
if item then
|
|
local diagnostic = diagnostic_messages[item.index]
|
|
local line1, col1 = util.toselection(diagnostic.range, doc)
|
|
doc:set_selection(line1, col1, line1, col1)
|
|
end
|
|
end,
|
|
suggest = function(text)
|
|
local res = common.fuzzy_match(captions, text)
|
|
for i, name in ipairs(res) do
|
|
local diagnostic = diagnostic_messages[indexes[name]]
|
|
local line1, col1 = util.toselection(diagnostic.range)
|
|
res[i] = {
|
|
text = diagnostics.lintplus_kinds[diagnostic.severity]
|
|
.. ": " .. diagnostic.message,
|
|
info = tostring(line1) .. ":" .. tostring(col1),
|
|
index = indexes[name]
|
|
}
|
|
end
|
|
return res
|
|
end
|
|
})
|
|
end
|
|
|
|
function lsp.view_all_diagnostics()
|
|
if diagnostics.count <= 0 then
|
|
core.log("[LSP] %s", "No diagnostic messages found.")
|
|
return
|
|
end
|
|
|
|
local captions = {}
|
|
for _, diagnostic in ipairs(diagnostics.list) do
|
|
table.insert(
|
|
captions,
|
|
core.normalize_to_project_dir(diagnostic.filename)
|
|
)
|
|
end
|
|
|
|
core.command_view:enter("Filter Files", {
|
|
submit = function(text, item)
|
|
if item then
|
|
core.root_view:open_doc(
|
|
core.open_doc(
|
|
common.home_expand(
|
|
text
|
|
)
|
|
)
|
|
)
|
|
end
|
|
end,
|
|
suggest = function(text)
|
|
local res = common.fuzzy_match(captions, text, true)
|
|
for i, name in ipairs(res) do
|
|
local diagnostics_count = diagnostics.get_messages_count(
|
|
core.project_absolute_path(name)
|
|
)
|
|
res[i] = {
|
|
text = name,
|
|
info = "Messages: " .. diagnostics_count
|
|
}
|
|
end
|
|
return res
|
|
end
|
|
})
|
|
end
|
|
|
|
--- Jumps to the definition or implementation of the symbol where the cursor
|
|
-- is placed if the LSP server supports it
|
|
function lsp.goto_symbol(doc, line, col, implementation)
|
|
if not doc.lsp_open then return end
|
|
|
|
for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
|
|
local method = ""
|
|
if not implementation then
|
|
if server.capabilities.definitionProvider then
|
|
method = method .. "definition"
|
|
elseif server.capabilities.declarationProvider then
|
|
method = method .. "declaration"
|
|
elseif server.capabilities.typeDefinitionProvider then
|
|
method = method .. "typeDefinition"
|
|
else
|
|
log(server, "Goto definition not supported")
|
|
return
|
|
end
|
|
else
|
|
if server.capabilities.implementationProvider then
|
|
method = method .. "implementation"
|
|
else
|
|
log(server, "Goto implementation not supported")
|
|
return
|
|
end
|
|
end
|
|
|
|
-- Send document updates first
|
|
lsp.update_document(doc)
|
|
|
|
server:push_request("textDocument/" .. method, {
|
|
params = get_buffer_position_params(doc, line, col),
|
|
callback = function(server, response)
|
|
local location = response.result
|
|
|
|
if not location or not location.uri and #location == 0 then
|
|
core.log("[LSP] No %s found.", method)
|
|
return
|
|
end
|
|
|
|
if not location.uri and #location > 1 then
|
|
listbox.clear()
|
|
for _, loc in pairs(location) do
|
|
local preview, position = get_location_preview(loc)
|
|
listbox.append {
|
|
text = preview,
|
|
info = position,
|
|
location = loc
|
|
}
|
|
end
|
|
listbox.show_list(nil, function(doc, item)
|
|
lsp.goto_location(item.location)
|
|
end)
|
|
else
|
|
if not location.uri then
|
|
location = location[1]
|
|
end
|
|
lsp.goto_location(location)
|
|
end
|
|
end
|
|
})
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Thread to process server requests and responses
|
|
-- without blocking entirely the editor.
|
|
--
|
|
core.add_thread(function()
|
|
while true do
|
|
local servers_running = false
|
|
for _,server in pairs(lsp.servers_running) do
|
|
-- Send raw data to server which is usually big and slow in a
|
|
-- non blocking way by creating a coroutine just for it.
|
|
if #server.raw_list > 0 then
|
|
local raw_send = coroutine.create(function()
|
|
server:process_raw()
|
|
end)
|
|
coroutine.resume(raw_send)
|
|
while coroutine.status(raw_send) ~= "dead" do
|
|
-- while sending raw request we only read from lsp to not
|
|
-- conflict with the written raw data so remember no calls
|
|
-- here to: server:process_client_responses()
|
|
-- or server:process_notifications()
|
|
server:process_errors(config.plugins.lsp.log_server_stderr)
|
|
server:process_responses()
|
|
coroutine.yield()
|
|
coroutine.resume(raw_send)
|
|
end
|
|
end
|
|
|
|
if not config.plugins.lsp.more_yielding then
|
|
server:process_notifications()
|
|
server:process_requests()
|
|
server:process_responses()
|
|
server:process_client_responses()
|
|
else
|
|
server:process_notifications()
|
|
coroutine.yield()
|
|
server:process_requests()
|
|
coroutine.yield()
|
|
server:process_responses()
|
|
server:process_client_responses()
|
|
coroutine.yield()
|
|
end
|
|
|
|
server:process_errors(config.plugins.lsp.log_server_stderr)
|
|
|
|
servers_running = true
|
|
end
|
|
|
|
if servers_running then
|
|
local wait = 0.01
|
|
if config.plugins.lsp.more_yielding then wait = 0 end
|
|
coroutine.yield(wait)
|
|
else
|
|
coroutine.yield(2)
|
|
end
|
|
end
|
|
end)
|
|
|
|
--
|
|
-- Events patching
|
|
--
|
|
local doc_load = Doc.load
|
|
local doc_save = Doc.save
|
|
local doc_on_close = Doc.on_close
|
|
local doc_raw_insert = Doc.raw_insert
|
|
local doc_raw_remove = Doc.raw_remove
|
|
local root_view_on_text_input = RootView.on_text_input
|
|
local root_view_on_mouse_moved = RootView.on_mouse_moved
|
|
|
|
function Doc:load(...)
|
|
local res = doc_load(self, ...)
|
|
-- skip new files
|
|
if self.filename and config.plugins.lsp.autostart_server then
|
|
diagnostics.lintplus_init_doc(self)
|
|
core.add_thread(function()
|
|
lsp.start_server(self.filename, core.project_dir)
|
|
lsp.open_document(self)
|
|
end)
|
|
end
|
|
return res
|
|
end
|
|
|
|
function Doc:save(...)
|
|
local old_filename = self.filename
|
|
local res = doc_save(self, ...)
|
|
if old_filename ~= self.filename then
|
|
-- seems to be a new document so we send open notification
|
|
diagnostics.lintplus_init_doc(self)
|
|
core.add_thread(function()
|
|
lsp.open_document(self)
|
|
end)
|
|
else
|
|
core.add_thread(function()
|
|
lsp.update_document(self)
|
|
lsp.save_document(self)
|
|
end)
|
|
end
|
|
return res
|
|
end
|
|
|
|
function Doc:on_close()
|
|
doc_on_close(self)
|
|
|
|
-- skip new files
|
|
if not self.filename then return end
|
|
core.add_thread(function()
|
|
lsp.close_document(self)
|
|
end)
|
|
|
|
if not config.plugins.lsp.stop_unneeded_servers then
|
|
return
|
|
end
|
|
|
|
-- Check if any running lsp servers is not needed anymore and stop it
|
|
for name, server in pairs(lsp.servers_running) do
|
|
local doc_found = false
|
|
for _, docu in ipairs(core.docs) do
|
|
if docu.filename then
|
|
if common.match_pattern(docu.filename, server.file_patterns) then
|
|
doc_found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
|
|
if not doc_found and not server.exit_timer then
|
|
local t = Timer(server.quit_timeout * 1000, true)
|
|
t.on_timer = function()
|
|
server:exit()
|
|
core.log("[LSP] stopped %s", name)
|
|
lsp.servers_running = util.table_remove_key(lsp.servers_running, name)
|
|
end
|
|
t:start()
|
|
server.exit_timer = t
|
|
end
|
|
end
|
|
end
|
|
|
|
local function add_change(self, text, line1, col1, line2, col2)
|
|
if not self.lsp_changes then
|
|
self.lsp_changes = {}
|
|
self.lsp_version = 0
|
|
end
|
|
|
|
local change = { range = {}, text = text}
|
|
change.range["start"] = {line = line1-1, character = col1-1}
|
|
change.range["end"] = {line = line2-1, character = col2-1}
|
|
|
|
for _, name in pairs(lsp.get_active_servers(self.filename, true)) do
|
|
local server = lsp.servers_running[name]
|
|
if not self.lsp_changes[server] then
|
|
self.lsp_changes[server] = {}
|
|
end
|
|
table.insert(self.lsp_changes[server], change)
|
|
end
|
|
|
|
-- TODO: this should not be needed but changing documents rapidly causes this
|
|
if type(self.lsp_version) ~= 'nil' then
|
|
self.lsp_version = self.lsp_version + 1
|
|
else
|
|
self.lsp_version = 1
|
|
end
|
|
end
|
|
|
|
function Doc:raw_insert(line, col, text, undo_stack, time)
|
|
doc_raw_insert(self, line, col, text, undo_stack, time)
|
|
|
|
-- skip new files
|
|
if not self.filename then return end
|
|
|
|
col = util.doc_utf8_to_utf16(self, line, col)
|
|
|
|
if self.lsp_open then
|
|
add_change(self, text, line, col, line, col)
|
|
lsp.update_document(self)
|
|
elseif #lsp.get_active_servers(self.filename, true) > 0 then
|
|
add_change(self, text, line, col, line, col)
|
|
end
|
|
end
|
|
|
|
function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
|
|
local lcol1 = util.doc_utf8_to_utf16(self, line1, col1)
|
|
local lcol2 = util.doc_utf8_to_utf16(self, line2, col2)
|
|
|
|
doc_raw_remove(self, line1, col1, line2, col2, undo_stack, time)
|
|
|
|
-- skip new files
|
|
if not self.filename then return end
|
|
|
|
if self.lsp_open then
|
|
add_change(self, "", line1, lcol1, line2, lcol2)
|
|
lsp.update_document(self)
|
|
elseif #lsp.get_active_servers(self.filename, true) > 0 then
|
|
add_change(self, "", line1, lcol1, line2, lcol2)
|
|
end
|
|
end
|
|
|
|
function RootView:on_text_input(text)
|
|
root_view_on_text_input(self, text)
|
|
|
|
-- this part should actually trigger after Doc:raw_insert and Doc:raw_remove
|
|
-- so it is safe to trigger autocompletion from here.
|
|
local av = get_active_docview()
|
|
|
|
if av then
|
|
lsp.user_typed = true
|
|
lsp.update_document(av.doc, true)
|
|
end
|
|
end
|
|
|
|
function RootView:on_mouse_moved(x, y, dx, dy)
|
|
root_view_on_mouse_moved(self, x, y, dx, dy)
|
|
|
|
if not config.plugins.lsp.mouse_hover then return end
|
|
|
|
lsp.hover_position.x = x
|
|
lsp.hover_position.y = y
|
|
if lsp.hover_position.triggered then
|
|
local doc, line, col = lsp.get_hovered_location(x, y)
|
|
if doc == lsp.hover_position.doc and lsp.hover_position.utf8_range then
|
|
local utf8_range = lsp.hover_position.utf8_range
|
|
local line1, col1, line2, col2 = utf8_range.line1, utf8_range.col1,
|
|
utf8_range.line2, utf8_range.col2
|
|
if (line > line1 or (line == line1 and col >= col1)) and
|
|
(line < line2 or (line == line2 and col <= col2)) then
|
|
return
|
|
end
|
|
end
|
|
listbox.hide()
|
|
lsp.hover_position.triggered = false
|
|
end
|
|
lsp.hover_timer:set_interval(config.plugins.lsp.mouse_hover_delay)
|
|
lsp.hover_timer:restart()
|
|
end
|
|
|
|
--
|
|
-- Add status view item to show document diagnostics count
|
|
--
|
|
core.status_view:add_item({
|
|
predicate = function()
|
|
local dv = get_active_docview()
|
|
if dv then
|
|
local filename = core.project_absolute_path(dv.doc.filename)
|
|
local diagnostic_messages = diagnostics.get(filename)
|
|
if diagnostic_messages and #diagnostic_messages > 0 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end,
|
|
name = "lsp:diagnostics",
|
|
alignment = StatusView.Item.RIGHT,
|
|
get_item = function()
|
|
local dv = get_active_docview()
|
|
if dv then
|
|
local filename = core.project_absolute_path(dv.doc.filename)
|
|
local diagnostic_messages = diagnostics.get(filename)
|
|
|
|
if diagnostic_messages and #diagnostic_messages > 0 then
|
|
return {
|
|
style.warn,
|
|
style.icon_font, "!",
|
|
style.font, " " .. tostring(#diagnostic_messages)
|
|
}
|
|
end
|
|
end
|
|
|
|
return {}
|
|
end,
|
|
command = "lsp:view-document-diagnostics",
|
|
position = 1,
|
|
tooltip = "LSP Diagnostics",
|
|
separator = core.status_view.separator2
|
|
})
|
|
|
|
--
|
|
-- Register autocomplete icons
|
|
--
|
|
if autocomplete.add_icon then
|
|
local autocomplete_icons = {
|
|
{ name = "Text", color = "keyword", icon = '' }, -- U+F77E
|
|
{ name = "Method", color = "function", icon = '' }, -- U+F6A6
|
|
{ name = "Function", color = "function", icon = '' }, -- U+F794
|
|
{ name = "Constructor", color = "literal", icon = '' }, -- U+F423
|
|
{ name = "Field", color = "keyword2", icon = 'ﰠ' }, -- U+FC20
|
|
{ name = "Variable", color = "keyword2", icon = '' }, -- U+F52A
|
|
{ name = "Class", color = "literal", icon = 'ﴯ' }, -- U+FD2F
|
|
{ name = "Interface", color = "literal", icon = '' }, -- U+F0E8
|
|
{ name = "Module", color = "literal", icon = '' }, -- U+F487
|
|
{ name = "Property", color = "keyword2", icon = 'ﰠ' }, -- U+FC20
|
|
{ name = "Unit", color = "number", icon = '塞' }, -- U+F96C
|
|
{ name = "Value", color = "string", icon = '' }, -- U+F89F
|
|
{ name = "Enum", color = "keyword2", icon = '' }, -- U+F15D
|
|
{ name = "Keyword", color = "keyword", icon = '' }, -- U+F80A
|
|
{ name = "Snippet", color = "keyword", icon = '' }, -- U+F44F
|
|
{ name = "Color", color = "string", icon = '' }, -- U+F8D7
|
|
{ name = "File", color = "string", icon = '' }, -- U+F718
|
|
{ name = "Reference", color = "string", icon = '' }, -- U+F706
|
|
{ name = "Folder", color = "string", icon = '' }, -- U+F74A
|
|
{ name = "EnumMember", color = "number", icon = '' }, -- U+F15D
|
|
{ name = "Constant", color = "number", icon = '' }, -- U+F8FE
|
|
{ name = "Struct", color = "keyword2", icon = 'פּ' }, -- U+FB44
|
|
{ name = "Event", color = "keyword", icon = '' }, -- U+F0E7
|
|
{ name = "Operator", color = "operator", icon = '' }, -- U+F694
|
|
{ name = "Unknown", color = "keyword", icon = '' }, -- U+F128
|
|
{ name = "TypeParameter", color = "literal", icon = '' } -- U+EA92
|
|
}
|
|
|
|
-- We add the font here to let it automatically scale by the scale plugin
|
|
style.syntax_fonts["lsp_symbols"] = renderer.font.load(
|
|
USERDIR .. "/plugins/lsp/fonts/symbols.ttf",
|
|
15 * SCALE
|
|
)
|
|
|
|
for _, icon in ipairs(autocomplete_icons) do
|
|
autocomplete.add_icon(
|
|
icon.name, icon.icon, style.syntax_fonts["lsp_symbols"], icon.color
|
|
)
|
|
end
|
|
end
|
|
|
|
--
|
|
-- Commands
|
|
--
|
|
command.add(
|
|
function()
|
|
local dv = get_active_docview()
|
|
return dv ~= nil and dv.doc.lsp_open, dv and dv.doc or nil
|
|
end, {
|
|
|
|
["lsp:complete"] = function(doc)
|
|
local line1, col1, line2, col2 = doc:get_selection()
|
|
if line1 == line2 and col1 == col2 then
|
|
lsp.request_completion(doc, line1, col1, true)
|
|
end
|
|
end,
|
|
|
|
["lsp:goto-definition"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.goto_symbol(doc, line1, col1)
|
|
end
|
|
end,
|
|
|
|
["lsp:goto-implementation"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.goto_symbol(doc, line1, col1, true)
|
|
end
|
|
end,
|
|
|
|
["lsp:show-signature"] = function(doc)
|
|
local line1, col1, line2, col2 = doc:get_selection()
|
|
if line1 == line2 and col1 == col2 then
|
|
lsp.request_signature(doc, line1, col1, true)
|
|
end
|
|
end,
|
|
|
|
["lsp:show-symbol-info"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.request_hover(doc, line1, col1)
|
|
end
|
|
end,
|
|
|
|
["lsp:show-symbol-info-in-tab"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.request_hover(doc, line1, col1, true)
|
|
end
|
|
end,
|
|
|
|
["lsp:view-call-hierarchy"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.request_call_hierarchy(doc, line1, col1)
|
|
end
|
|
end,
|
|
|
|
["lsp:view-document-symbols"] = function(doc)
|
|
lsp.request_document_symbols(doc)
|
|
end,
|
|
|
|
["lsp:format-document"] = function(doc)
|
|
lsp.request_document_format(doc)
|
|
end,
|
|
|
|
["lsp:view-document-diagnostics"] = function(doc)
|
|
lsp.view_document_diagnostics(doc)
|
|
end,
|
|
|
|
["lsp:rename-symbol"] = function(doc)
|
|
local symbol = doc:get_text(doc:get_selection())
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if #symbol > 0 and line1 == line2 then
|
|
core.command_view:enter("New Symbol Name", {
|
|
text = symbol,
|
|
submit = function(new_name)
|
|
lsp.request_symbol_rename(doc, line1, col1, new_name)
|
|
end
|
|
})
|
|
else
|
|
core.log("Please select a symbol on the document to rename.")
|
|
end
|
|
end,
|
|
|
|
["lsp:find-references"] = function(doc)
|
|
local line1, col1, line2 = doc:get_selection()
|
|
if line1 == line2 then
|
|
lsp.request_references(doc, line1, col1)
|
|
end
|
|
end
|
|
})
|
|
|
|
command.add(nil, {
|
|
["lsp:view-all-diagnostics"] = function()
|
|
lsp.view_all_diagnostics()
|
|
end,
|
|
|
|
["lsp:find-workspace-symbol"] = function()
|
|
local dv = get_active_docview()
|
|
local doc = dv and dv.doc or nil
|
|
local symbol = doc and doc:get_text(doc:get_selection()) or ""
|
|
core.command_view:enter("Find Workspace Symbol", {
|
|
text = symbol,
|
|
submit = function(query)
|
|
lsp.request_workspace_symbol(doc, query)
|
|
end
|
|
})
|
|
end,
|
|
|
|
["lsp:toggle-diagnostics"] = function()
|
|
if not diagnostics.lintplus_found then
|
|
core.error("[LSP] Please install lintplus for diagnostics rendering.")
|
|
return
|
|
end
|
|
lsp.toggle_diagnostics()
|
|
end,
|
|
|
|
["lsp:stop-servers"] = function()
|
|
lsp.stop_servers()
|
|
end,
|
|
|
|
["lsp:start-servers"] = function()
|
|
lsp.start_servers()
|
|
end,
|
|
|
|
["lsp:restart-servers"] = function()
|
|
lsp.stop_servers()
|
|
lsp.start_servers()
|
|
end
|
|
})
|
|
|
|
--
|
|
-- Default Keybindings
|
|
--
|
|
keymap.add {
|
|
["ctrl+space"] = "lsp:complete",
|
|
["ctrl+shift+space"] = "lsp:show-signature",
|
|
["alt+a"] = "lsp:show-symbol-info",
|
|
["alt+shift+a"] = "lsp:show-symbol-info-in-tab",
|
|
["alt+d"] = "lsp:goto-definition",
|
|
["alt+shift+d"] = "lsp:goto-implementation",
|
|
["alt+s"] = "lsp:view-document-symbols",
|
|
["alt+shift+s"] = "lsp:find-workspace-symbol",
|
|
["alt+f"] = "lsp:find-references",
|
|
["alt+shift+f"] = "lsp:format-document",
|
|
["alt+e"] = "lsp:view-document-diagnostics",
|
|
["ctrl+alt+e"] = "lsp:view-all-diagnostics",
|
|
["alt+shift+e"] = "lsp:toggle-diagnostics",
|
|
["alt+c"] = "lsp:view-call-hierarchy",
|
|
["alt+r"] = "lsp:rename-symbol",
|
|
}
|
|
|
|
--
|
|
-- Register context menu items
|
|
--
|
|
local function lsp_predicate(_, _, also_in_symbol)
|
|
local dv = get_active_docview()
|
|
if dv then
|
|
local doc = dv.doc
|
|
|
|
if #lsp.get_active_servers(doc.filename, true) < 1 then
|
|
return false
|
|
elseif not also_in_symbol then
|
|
return true
|
|
end
|
|
|
|
-- Make sure the cursor is place near a document symbol (word)
|
|
local linem, colm = doc:get_selection()
|
|
local linel, coll = doc:position_offset(linem, colm, translate.start_of_word)
|
|
local liner, colr = doc:position_offset(linem, colm, translate.end_of_word)
|
|
|
|
local word_left = doc:get_text(linel, coll, linem, colm)
|
|
local word_right = doc:get_text(linem, colm, liner, colr)
|
|
|
|
if #word_left > 0 or #word_right > 0 then
|
|
return true
|
|
end
|
|
end
|
|
return false
|
|
end
|
|
|
|
local function lsp_predicate_symbols()
|
|
return lsp_predicate(nil, nil, true)
|
|
end
|
|
|
|
local menu_found, menu = pcall(require, "plugins.contextmenu")
|
|
if menu_found then
|
|
menu:register(lsp_predicate_symbols, {
|
|
menu.DIVIDER,
|
|
{ text = "Show Symbol Info", command = "lsp:show-symbol-info" },
|
|
{ text = "Show Symbol Info in Tab", command = "lsp:show-symbol-info-in-tab" },
|
|
{ text = "Goto Definition", command = "lsp:goto-definition" },
|
|
{ text = "Goto Implementation", command = "lsp:goto-implementation" },
|
|
{ text = "Find References", command = "lsp:find-references" }
|
|
})
|
|
|
|
menu:register(lsp_predicate, {
|
|
menu.DIVIDER,
|
|
{ text = "Document Symbols", command = "lsp:view-document-symbols" },
|
|
{ text = "Document Diagnostics", command = "lsp:view-document-diagnostics" },
|
|
{ text = "Toggle Diagnostics", command = "lsp:toggle-diagnostics" },
|
|
{ text = "Format Document", command = "lsp:format-document" },
|
|
})
|
|
|
|
local menu_show = menu.show
|
|
function menu:show(...)
|
|
lsp.hover_timer:stop()
|
|
lsp.hover_timer:reset()
|
|
listbox.hide()
|
|
lsp.hover_position.triggered = false
|
|
menu_show(self, ...)
|
|
end
|
|
end
|
|
|
|
|
|
return lsp
|
|
|