dotfiles/.config/lite-xl/plugins/lsp/server.lua

1662 lines
48 KiB
Lua

-- Class in charge of establishing communication with an LSP server and
-- managing requests, notifications and responses from both the server
-- and the client that is establishing the connection.
--
-- @copyright Jefferson Gonzalez
-- @license MIT
-- @inspiration: https://github.com/orbitalquark/textadept-lsp
--
-- LSP Documentation:
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-17
local json = require "plugins.lsp.json"
local util = require "plugins.lsp.util"
local diagnostics = require "plugins.lsp.diagnostics"
local Object = require "core.object"
---@alias lsp.server.callback fun(server: lsp.server, ...)
---@alias lsp.server.timeoutcb fun(server: lsp.server, ...)
---@alias lsp.server.notificationcb fun(server: lsp.server, params: table)
---@alias lsp.server.responsecb fun(server: lsp.server, response: table, request?: lsp.server.request)
---@class lsp.server.languagematch
---@field id string
---@field pattern string
---@class lsp.server.request
---@field id integer
---@field method string
---@field data table|nil
---@field params table
---@field callback lsp.server.responsecb | nil
---@field overwritten boolean
---@field overwritten_callback lsp.server.responsecb | nil
---@field sending boolean
---@field raw_data string
---@field timeout number
---@field timeout_callback lsp.server.timeoutcb | nil
---@field timestamp number
---@field times_sent integer
---LSP Server communication library.
---@class lsp.server : core.object
---@field public name string
---@field public language string | lsp.server.languagematch[]
---@field public file_patterns table
---@field public current_request integer
---@field public init_options table
---@field public settings table | nil
---@field public event_listeners table
---@field public message_listeners table
---@field public request_listeners table
---@field public request_list lsp.server.request[]
---@field public response_list table
---@field public notification_list lsp.server.request[]
---@field public raw_list lsp.server.request[]
---@field public command table
---@field public write_fails integer
---@field public write_fails_before_shutdown integer
---@field public verbose boolean
---@field public initialized boolean
---@field public hitrate_list table
---@field public requests_per_second integer
---@field public proc process | nil
---@field public quit_timeout number
---@field public exit_timer lsp.timer | nil
---@field public capabilities table
---@field public custom_capabilities table
---@field public yield_on_reads boolean
---@field public running boolean
local Server = Object:extend()
---LSP Server constructor options
---@class lsp.server.options
---@field name string
---@field language string | lsp.server.languagematch[]
---@field file_patterns table<integer, string>
---@field command table<integer, string>
---@field quit_timeout number
---@field windows_skip_cmd boolean
---@field env table<string, string>
---@field settings table
---@field init_options table
---@field custom_capabilities table
---@field on_start? fun(server: lsp.server)
---@field requests_per_second number
---@field incremental_changes boolean
Server.options = {
---Name of the server
name = "",
---Programming language identifier.
---Can be a string or a table.
---If the table is empty, the file extension will be used instead.
---The table should be an array of tables containing `id` and `pattern`.
---The `pattern` will be matched with the file path.
---Will use the `id` of the first `pattern` that matches.
---If no pattern matches, the file extension will be used instead.
language = {},
---Patterns to match the language files
file_patterns = {},
---Command to launch LSP server and optional arguments
command = {},
---On Windows, avoid running the LSP server with cmd.exe
windows_skip_cmd = false,
---Enviroment variables to set for the server command
env = {},
---Seconds before closing the server when not needed anymore
quit_timeout = 60,
---Optional table of settings to pass into the LSP
---Note that also having a settings.json or settings.lua in
---your workspace directory is supported
settings = {},
---Optional table of initializationOptions for the LSP
init_options = {},
---Optional table of capabilities that will be merged with our default one
custom_capabilities = {},
---Function called when the server has been started
on_start = nil,
---Set by default to 16 should only be modified if having issues with a server
requests_per_second = 32,
---Some servers like bash language server support incremental changes
---which are more performant but don't advertise it, set to true to force
---incremental changes even if server doesn't advertise them
incremental_changes = false,
---True to debug the lsp client when developing it
verbose = false,
}
---Default timeout when sending a request to lsp server.
---@type integer Time in seconds
Server.DEFAULT_TIMEOUT = 10
---The maximum amount of data to retrieve when reading from server.
---@type integer Amount of bytes
Server.BUFFER_SIZE = 1024 * 10
---LSP Docs: /#errorCodes
Server.error_code = {
ParseError = -32700,
InvalidRequest = -32600,
MethodNotFound = -32601,
InvalidParams = -32602,
InternalError = -32603,
jsonrpcReservedErrorRangeStart = -32099,
serverErrorStart = -32099,
ServerNotInitialized = -32002,
UnknownErrorCode = -32001,
jsonrpcReservedErrorRangeEnd = -32000,
serverErrorEnd = -32000,
lspReservedErrorRangeStart = -32899,
ContentModified = -32801,
RequestCancelled = -32800,
lspReservedErrorRangeEnd = -32800,
}
---LSP Docs: /#completionTriggerKind
Server.completion_trigger_Kind = {
Invoked = 1,
TriggerCharacter = 2,
TriggerForIncompleteCompletions = 3
}
---LSP Docs: /#diagnosticSeverity
Server.diagnostic_severity = {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
}
---LSP Docs: /#textDocumentSyncKind
Server.text_document_sync_kind = {
None = 0,
Full = 1,
Incremental = 2
}
---LSP Docs: /#completionItemKind
Server.completion_item_kind = {
'Text', 'Method', 'Function', 'Constructor', 'Field', 'Variable', 'Class',
'Interface', 'Module', 'Property', 'Unit', 'Value', 'Enum', 'Keyword',
'Snippet', 'Color', 'File', 'Reference', 'Folder', 'EnumMember',
'Constant', 'Struct', 'Event', 'Operator', 'TypeParameter'
}
---LSP Docs: /#symbolKind
Server.symbol_kind = {
'File', 'Module', 'Namespace', 'Package', 'Class', 'Method', 'Property',
'Field', 'Constructor', 'Enum', 'Interface', 'Function', 'Variable',
'Constant', 'String', 'Number', 'Boolean', 'Array', 'Object', 'Key',
'Null', 'EnumMember', 'Struct', 'Event', 'Operator', 'TypeParameter'
}
---LSP Docs: /#insertTextFormat
Server.insert_text_format = {
PlainText = 1,
Snippet = 2
}
---LSP Docs: /#messageType
---@enum
Server.message_type = {
Error = 1,
Warning = 2,
Info = 3,
Log = 4,
Debug = 5
}
---LSP Docs: /#positionEncodingKind
---@enum
Server.position_encoding_kind = {
UTF8 = 'utf-8',
UTF16 = 'utf-16',
UTF32 = 'utf-32'
}
---@class lsp.server.requestoptions
---@field params? table<string,any>
---@field data? table @Optional data appended to request.
---@field callback? lsp.server.responsecb @Default callback executed when a response is received.
---@field overwrite? boolean @Substitute same previous request with new one if not sent.
---@field overwritten_callback? lsp.server.responsecb @Executed in place of original response callback if the request should have been overwritten but was already sent.
---@field raw_data? string @Request body used when sending a raw request.
---@field timeout? number @Timeout in seconds to consider the request unanswered.
---@field timeout_callback? lsp.server.timeoutcb @Callback executed when the request times out.
---Get a completion kind label from its id or empty string if not found.
---@param id integer
---@return string
function Server.get_completion_item_kind(id)
return Server.completion_item_kind[id] or ""
end
---Get list of completion kinds.
---@return table
function Server.get_completion_items_kind_list()
local list = {}
for i = 1, #Server.completion_item_kind do
if i ~= 15 then --Disable snippets
table.insert(list, i)
end
end
return list
end
---Get a symbol kind label from its id or empty string if not found.
---@param id integer
---@return string
function Server.get_symbol_kind(id)
return Server.symbol_kind[id] or ""
end
---Get list of symbol kinds.
---@return table
function Server.get_symbols_kind_list()
local list = {}
for i = 1, #Server.symbol_kind do
list[i] = i
end
return list
end
---Given a ServerCapabilities object, return a "normalized" version
---that simplifies capabilities checks.
---@param capabilities table
---returns table
function Server.normalize_server_capabilities(capabilities)
local cap = util.deep_merge({ }, capabilities)
local tds = {
openClose = false,
change = false,
willSave = false,
willSaveWaitUntil = false,
save = false
}
if cap.textDocumentSync then
if type(cap.textDocumentSync) ~= "table" then
-- Convert TextDocumentSyncKind into TextDocumentSyncOptions
tds = util.deep_merge(tds, {
openClose = true,
change = cap.textDocumentSync,
save = {
includeText = false
}
})
cap.textDocumentSync = nil
else
tds = util.deep_merge(tds, cap.textDocumentSync)
if type(tds.save) ~= "table" and tds.save then
tds.save = {
includeText = false
}
end
end
end
cap.textDocumentSync = util.deep_merge(cap.textDocumentSync, tds)
return cap
end
---Instantiates a new LSP server.
---@param options lsp.server.options
function Server:new(options)
Server.super.new(self)
self.name = options.name
self.language = options.language
self.file_patterns = options.file_patterns
self.current_request = 0
self.init_options = options.init_options or {}
self.settings = options.settings or nil
self.event_listeners = {}
self.message_listeners = {}
self.request_listeners = {}
self.request_list = {}
self.response_list = {}
self.notification_list = {}
self.raw_list = {}
self.command = options.command
self.write_fails = 0
self.fatal_error = false
self.snippets = options.snippets
self.fake_snippets = options.fake_snippets or false
-- TODO: We may need to lower this but tests so far show that some servers
-- may actually fail to write many of the request sent to it if it is
-- indexing the workspace source code or other heavy tasks.
self.write_fails_before_shutdown = 60
self.verbose = options.verbose or false
self.last_restart = system.get_time()
self.initialized = false
self.hitrate_list = {}
self.requests_per_second = options.requests_per_second or 16
self.proc = process.start(
options.command, {
stderr = process.REDIRECT_PIPE,
env = options.env
}
)
self.quit_timeout = options.quit_timeout or 60
self.exit_timer = nil
self.capabilities = nil
self.custom_capabilities = options.custom_capabilities
self.yield_on_reads = false
self.incremental_changes = options.incremental_changes or false
self.read_responses_coroutine = nil
if options.on_start then options.on_start(self) end
end
---Starts the LSP server process, any listeners should be registered before
---calling this method and this method should be called before any pushes.
---@param workspace string
---@param editor_name? string
---@param editor_version? string
function Server:initialize(workspace, editor_name, editor_version)
local root_uri = util.touri(workspace);
self.path = workspace or ""
self.editor_name = editor_name or "unknown"
self.editor_version = editor_version or "0.1"
self:push_request('initialize', {
timeout = 10,
params = {
processId = system["get_process_id"] and system.get_process_id() or nil,
clientInfo = {
name = editor_name or "unknown",
version = editor_version or "0.1"
},
-- TODO: locale
rootPath = workspace,
rootUri = root_uri,
workspaceFolders = {
{uri = root_uri, name = util.getpathname(workspace)}
},
initializationOptions = self.init_options,
capabilities = util.deep_merge({
workspace = {
configuration = true -- 'workspace/configuration' requests
},
textDocument = {
synchronization = {
-- willSave = true,
-- willSaveWaitUntil = true,
didSave = true,
-- dynamicRegistration = false -- not supported
},
completion = {
-- dynamicRegistration = false, -- not supported
completionItem = {
-- Snippets are required by css-languageserver
snippetSupport = self.snippets or self.fake_snippets,
-- commitCharactersSupport = true,
documentationFormat = {'plaintext'},
-- deprecatedSupport = false, -- simple autocompletion list
-- preselectSupport = true
-- tagSupport = {valueSet = {}},
insertReplaceSupport = true,
resolveSupport = {properties = {'documentation', 'detail', 'additionalTextEdits'}},
-- insertTextModeSupport = {valueSet = {}}
},
completionItemKind = {
valueSet = Server.get_completion_items_kind_list()
}
-- contextSupport = true
},
hover = {
-- dynamicRegistration = false, -- not supported
contentFormat = {'markdown', 'plaintext'}
},
signatureHelp = {
-- dynamicRegistration = false, -- not supported
signatureInformation = {
documentationFormat = {'plaintext'}
-- parameterInformation = {labelOffsetSupport = true},
-- activeParameterSupport = true
}
-- contextSupport = true
},
-- references = {dynamicRegistration = false}, -- not supported
-- documentHighlight = {dynamicRegistration = false}, -- not supported
documentSymbol = {
-- dynamicRegistration = false, -- not supported
symbolKind = {valueSet = Server.get_symbols_kind_list()}
-- hierarchicalDocumentSymbolSupport = true,
-- tagSupport = {valueSet = {}},
-- labelSupport = true
},
-- diagnostic = {
-- dynamicRegistration = true,
-- relatedDocumentSupport = false
-- },
-- formatting = {dynamicRegistration = false},-- not supported
-- rangeFormatting = {dynamicRegistration = false}, -- not supported
-- onTypeFormatting = {dynamicRegistration = false}, -- not supported
-- declaration = {
-- dynamicRegistration = false, -- not supported
-- linkSupport = true
-- }
-- definition = {
-- dynamicRegistration = false, -- not supported
-- linkSupport = true
-- },
-- typeDefinition = {
-- dynamicRegistration = false, -- not supported
-- linkSupport = true
-- },
-- implementation = {
-- dynamicRegistration = false, -- not supported
-- linkSupport = true
-- },
-- codeAction = {
-- dynamicRegistration = false, -- not supported
-- codeActionLiteralSupport = {valueSet = {}},
-- isPreferredSupport = true,
-- disabledSupport = true,
-- dataSupport = true,
-- resolveSupport = {properties = {}},
-- honorsChangeAnnotations = true
-- },
-- codeLens = {dynamicRegistration = false}, -- not supported
-- documentLink = {
-- dynamicRegistration = false, -- not supported
-- tooltipSupport = true
-- },
-- colorProvider = {dynamicRegistration = false}, -- not supported
-- rename = {
-- dynamicRegistration = false, -- not supported
-- prepareSupport = false
-- },
publishDiagnostics = {
relatedInformation = true,
tagSupport = {
valueSet = {
diagnostics.tag.UNNECESSARY,
diagnostics.tag.DEPRECATED
}
},
versionSupport = true,
codeDescriptionSupport = true,
dataSupport = false
},
-- foldingRange = {
-- dynamicRegistration = false, -- not supported
-- rangeLimit = ?,
-- lineFoldingOnly = true
-- },
-- selectionRange = {dynamicRegistration = false}, -- not supported
-- linkedEditingRange = {dynamicRegistration = false}, -- not supported
-- callHierarchy = {dynamicRegistration = false}, -- not supported
-- semanticTokens = {
-- dynamicRegistration = false, -- not supported
-- requests = {},
-- tokenTypes = {},
-- tokenModifiers = {},
-- formats = {},
-- overlappingTokenSupport = true,
-- multilineTokenSupport = true
-- },
-- moniker = {dynamicRegistration = false} -- not supported
},
window = {
-- workDoneProgress = true,
-- showMessage = {},
showDocument = { support = true }
},
general = {
-- regularExpressions = {},
-- markdown = {},
positionEncodings = {
Server.position_encoding_kind.UTF16
}
},
-- experimental = nil
}, self.custom_capabilities)
},
callback = function(server, response)
if server.verbose then
server:log(
"Processing initialization response:\n%s",
util.jsonprettify(json.encode(response))
)
end
local result = response.result
if result then
server.capabilities = Server.normalize_server_capabilities(result.capabilities)
server.info = result.serverInfo
if server.info then
server:log(
'Connected to %s %s',
server.info.name,
server.info.version or '(unknown version)'
)
end
while not server:notify('initialized') do end -- required by protocol
-- We wait a few seconds to prevent initialization issues
coroutine.yield(3)
server.initialized = true;
server:send_event_signal("initialized", server, result)
end
end
})
end
---Register an event listener.
---@param event_name string
---@param callback lsp.server.callback
function Server:add_event_listener(event_name, callback)
if self.verbose then
self:log(
"Listening for event '%s'",
event_name
)
end
if not self.event_listeners[event_name] then
self.event_listeners[event_name] = {}
end
table.insert(self.event_listeners[event_name], callback)
end
function Server:send_event_signal(event_name, ...)
if self.event_listeners[event_name] then
for _, l in ipairs(self.event_listeners[event_name]) do
l(self, ...)
end
else
self:on_event(event_name)
end
end
function Server:on_event(event_name)
if self.verbose then
self:log("Received event '%s'", event_name)
end
end
---Send a message to the server that doesn't needs a response.
---@param method string
---@param params? table
---@return boolean sent
function Server:notify(method, params)
local message = {
jsonrpc = '2.0',
method = method,
params = params or {}
}
local data = json.encode(message)
if self.verbose then
self:log("Sending notification:\n%s", util.jsonprettify(data))
end
local sent, errmsg = self:write_request(data)
if not sent and self.verbose then
self:log(
"Could not send '%s' notification with error: %s",
method,
errmsg or "unknown"
)
end
return sent
end
---Reply to a server request.
---@param id integer
---@param result table
---@return boolean sent
function Server:respond(id, result)
local message = {
jsonrpc = '2.0',
id = id,
result = result
}
local data = json.encode(message)
if self.verbose then
self:log("Responding to '%d':\n%s", id, util.jsonprettify(data))
end
local sent, errmsg = self:write_request(data)
if not sent and self.verbose then
self:log("Could not send response with error: %s", errmsg or "unknown")
end
return sent
end
---Respond to a an unknown server request with a method not found error code.
---@param id integer
---@param error_message? string
---@param error_code? integer
---@return boolean sent
function Server:respond_error(id, error_message, error_code)
local message = {
jsonrpc = '2.0',
id = id,
error = {
code = error_code or Server.error_code.MethodNotFound,
message = error_message or "method not found"
}
}
local data = json.encode(message)
if self.verbose then
self:log("Responding error to '%d':\n%s", id, util.jsonprettify(data))
end
local sent, errmsg = self:write_request(data)
if not sent and self.verbose then
self:log("Could not send response with error: %s", errmsg or "unknown")
end
return sent
end
---Sends one of the queued notifications.
function Server:process_notifications()
if not self.initialized then return end
-- Clone table as we remove elements while iterating it
local notifications = {}
for index, request in ipairs(self.notification_list) do
notifications[index] = request
end
for index, request in ipairs(notifications) do
request.sending = true
local message = {
jsonrpc = '2.0',
method = request.method,
params = request.params or {}
}
local data = json.encode(message)
if self.verbose then
self:log(
"Sending notification '%s':\n%s",
request.method,
util.jsonprettify(data)
)
end
local written, errmsg = self:write_request(data)
if self.verbose then
if not written then
self:log(
"Failed sending notification '%s' with error: %s",
request.method,
errmsg or "unknown"
)
end
end
if written then
if request.callback then
request.callback(self)
end
table.remove(self.notification_list, index)
self.write_fails = 0
return request
else
self:shutdown_if_needed()
return
end
end
end
---Sends one of the queued client requests.
function Server:process_requests()
if not self.proc then return end
local remove_request = nil
for index, request in ipairs(self.request_list) do
if request.timestamp < os.time() then
-- only process when initialized or the initialize request
-- which should be the first one.
if not self.initialized and request.id ~= 1 then
return nil
end
local message = {
jsonrpc = '2.0',
id = request.id,
method = request.method,
params = request.params or {}
}
local data = json.encode(message)
local written, errmsg = self:write_request(data)
if self.verbose then
if written then
self:log(
"Sent request '%s':\n%s",
request.method,
util.jsonprettify(data)
)
else
self:log(
"Failed sending request '%s' with error: %s\n%s",
request.method,
errmsg or "unknown",
util.jsonprettify(data)
)
end
end
if written then
local time = request.timeout or 1
request.timestamp = os.time() + time
self.write_fails = 0
-- if request has been sent more than 2 times remove them
request.times_sent = request.times_sent + 1
if
request.times_sent > 1
and
request.id ~= 1 -- Initialize request may take some time
then
remove_request = index
break
else
return request
end
else
request.timestamp = os.time() + 1
self:shutdown_if_needed()
return nil
end
end
end
if remove_request then
local request = table.remove(self.request_list, remove_request)
if self.verbose then
self:log("Request '%s' expired without response", remove_request)
end
if request.timeout_callback then
request.timeout_callback(request)
end
end
return nil
end
---Read the lsp server stdout, parse any responses, requests or
---notifications and properly dispatch signals to any listeners.
function Server:process_responses()
if not self.proc then return end
local responses = self:read_responses(0)
if type(responses) == "table" then
for _, response in pairs(responses) do
if self.verbose then
self:log(
"Processing Response:\n%s",
util.jsonprettify(json.encode(response))
)
end
if not response.id then
-- A notification, event or generic message was received
self:send_message_signal(response)
elseif
response.result
or
(not response.params and not response.method)
then
-- An actual request response was received
self:send_response_signal(response)
else
-- The server is making a request
self:send_request_signal(response)
end
end
end
return responses
end
---Sends all queued client responses to server.
function Server:process_client_responses()
if not self.initialized then return end
::send_responses::
for index, response in ipairs(self.response_list) do
local message = {
jsonrpc = '2.0',
id = response.id
}
if response.result then
message.result = response.result
else
message.error = response.error
end
local data = json.encode(message)
if self.verbose then
self:log("Sending client response:\n%s", util.jsonprettify(data))
end
local written, errmsg = self:write_request(data)
if self.verbose then
if not written then
self:log(
"Failed sending client response '%s' with error: %s",
response.id,
errmsg or "unknown"
)
end
end
if written then
self.write_fails = 0
table.remove(self.response_list, index)
-- restart loop after removing from table to prevent issues
goto send_responses
else
self:shutdown_if_needed()
return
end
end
end
---Should be called periodically to prevent the server from stalling
---because of not flushing the stderr (especially true of clangd).
---@param log_errors boolean
function Server:process_errors(log_errors)
if not self.proc then return end
local errors = self:read_errors(0)
if #errors > 0 and log_errors then
self:log("Error: \n'%s'", errors)
end
return errors
end
---Sends raw data to the server process and ensures that all of it is written
---if no errors occur, otherwise it returns false and the error message. Notice
---that this function can perform yielding when ran inside of a coroutine.
---@param data string
---@return boolean sent
---@return string? errmsg
function Server:send_data(data)
local proc = self.proc -- save current process to avoid it changing
if not proc then return false end
local failures, data_len = 0, #data
local written, errmsg = proc:write(data)
local total_written = written or 0
while total_written < data_len and not errmsg do
written, errmsg = proc:write(data:sub(total_written + 1))
total_written = total_written + (written or 0)
if (not written or written <= 0) and not errmsg and coroutine.running() then
-- with each consecutive fail the yield timeout is increased by 5ms
coroutine.yield((failures * 5) / 1000)
failures = failures + 1
if failures > 19 then -- after ~1000ms we error out
errmsg = "maximum amount of consecutive failures reached"
break
end
else
failures = 0
end
end
if errmsg then
self:log("Error sending data: '%s'\n%s", errmsg, data)
end
return total_written == data_len, errmsg
end
---Send one of the queued chunks of raw data to lsp server which are
---usually huge, like the textDocument/didOpen notification.
function Server:process_raw()
if not self.initialized then return end
-- Wait until everything else is processed to prevent initialization issues
if
#self.notification_list > 0
or
#self.request_list > 0
or
#self.response_list > 0
then
return
end
if not self.proc or not self.proc:running() then
self.raw_list = {}
return
end
local sent = false
for index, raw in ipairs(self.raw_list) do
raw.sending = true
-- first send the header
if
not self:send_data(string.format(
'Content-Length: %d\r\n\r\n', #raw.raw_data
))
then
break
end
if self.verbose then
self:log("Raw header written")
end
-- send content in chunks
local chunks = 10 * 1024
raw.raw_data = raw.raw_data
while #raw.raw_data > 0 do
if not self.proc or not self.proc:running() then
self.raw_list = {}
return
end
if #raw.raw_data > chunks then
-- TODO: perform proper error handling
self:send_data(raw.raw_data:sub(1, chunks))
raw.raw_data = raw.raw_data:sub(chunks+1)
else
-- TODO: perform proper error handling
self:send_data(raw.raw_data)
raw.raw_data = ""
end
self.write_fails = 0
coroutine.yield()
end
if self.verbose then
self:log("Raw content written")
end
if raw.callback then
raw.callback(self, raw)
end
table.remove(self.raw_list, index)
sent = true
break
end
if sent then collectgarbage("collect") end
end
---Help controls the amount of requests sent to the lsp server per second
---which prevents overloading it and causing a pipe hang.
---@param type string
---@return boolean true if max hitrate was reached
function Server:hitrate_reached(type)
if not self.hitrate_list[type] then
self.hitrate_list[type] = {
count = 1,
timestamp = os.time() + 1
}
elseif self.hitrate_list[type].timestamp > os.time() then
if self.hitrate_list[type].count >= self.requests_per_second then
return true
end
self.hitrate_list[type].count = self.hitrate_list[type].count + 1
else
self.hitrate_list[type].timestamp = os.time() + 1
self.hitrate_list[type].count = 1
end
return false
end
---Check if it is possible to queue a new request of any kind except
---raw ones. This is useful to delay a request and not loose it in case
---the lsp reached maximum amount of hit rate per second.
function Server:can_push()
local type = "request"
if not self.hitrate_list[type] then
return self.initialized
elseif self.hitrate_list[type].timestamp > os.time() then
if self.hitrate_list[type].count >= self.requests_per_second then
return false
end
end
return self.initialized
end
-- Notifications that should bypass the hitrate limit
local notifications_whitelist = {
"textDocument/didOpen",
"textDocument/didSave",
"textDocument/didClose"
}
---Queue a new notification but ignores new ones if the hit rate was reached.
---@param method string
---@param options lsp.server.requestoptions
function Server:push_notification(method, options)
assert(options.params, "please provide the parameters for the notification")
if options.overwrite then
for _, notification in ipairs(self.notification_list) do
if notification.method == method and not notification.sending then
if self.verbose then
self:log("Overwriting notification %s", tostring(method))
end
notification.params = options.params
notification.callback = options.callback
notification.data = options.data
return
end
end
end
if
method ~= "textDocument/didOpen"
and
self:hitrate_reached("request")
and
not util.intable(method, notifications_whitelist)
then
return
end
if self.verbose then
self:log(
"Pushing notification '%s':\n%s",
method,
util.jsonprettify(json.encode(options.params))
)
end
-- Store the notification for later processing on responses_loop
table.insert(self.notification_list, {
method = method,
params = options.params,
callback = options.callback,
data = options.data,
})
end
-- Requests that should bypass the hitrate limit
local requests_whitelist = {
"completionItem/resolve"
}
---Queue a new request but ignores new ones if the hit rate was reached.
---@param method string
---@param options lsp.server.requestoptions
function Server:push_request(method, options)
if not self.initialized and method ~= "initialize" then
return
end
assert(options.params, "please provide the parameters for the request")
if options.overwrite then
for _, request in ipairs(self.request_list) do
if request.method == method then
if request.times_sent > 0 then
request.overwritten = true
break
else
request.params = options.params
request.callback = options.callback
request.overwritten_callback = options.overwritten_callback
request.data = options.data
request.timeout = options.timeout
request.timeout_callback = options.timeout_callback
request.timestamp = 0
if self.verbose then
self:log("Overwriting request %s", tostring(method))
end
return
end
end
end
end
if
method ~= "initialize"
and
self:hitrate_reached("request")
and
not util.intable(method, requests_whitelist)
then
return
end
if self.verbose then
self:log("Adding request %s", tostring(method))
end
-- Set the request id
self.current_request = self.current_request + 1
-- Store the request for later processing on responses_loop
table.insert(self.request_list, {
id = self.current_request,
method = method,
params = options.params,
callback = options.callback,
overwritten_callback = options.overwritten_callback,
data = options.data,
timeout = options.timeout,
timeout_callback = options.timeout_callback,
timestamp = 0,
times_sent = 0
})
end
---Queue a client response to a server request which can be an error
---or a regular response, one of both. This may ignore new ones if
---the hit rate was reached.
---@param method string
---@param id integer
---@param result table|nil
---@param error table|nil
function Server:push_response(method, id, result, error)
if self:hitrate_reached("request") then
return
end
if self.verbose then
self:log("Adding response %s to %s", tostring(id), tostring(method))
end
-- Store the response for later processing on loop
local response = {
id = id
}
if result then
response.result = result
else
response.error = error
end
table.insert(self.response_list, response)
end
---Send raw json strings to server in cases where the json encoder
---would be too slow to convert a lua table into a json representation.
---@param name string A name to identify the request when overwriting.
---@param options lsp.server.requestoptions
function Server:push_raw(name, options)
assert(options.raw_data, "please provide the raw_data for request")
if options.overwrite then
for _, request in ipairs(self.raw_list) do
if request.method == name then
if not request.sending then
request.raw_data = options.raw_data
request.callback = options.callback
request.data = options.data
if self.verbose then
self:log("Overwriting raw request %s", tostring(name))
end
return
end
break
end
end
end
if self.verbose then
self:log("Adding raw request %s", name)
end
-- Store the request for later processing on responses_loop
table.insert(self.raw_list, {
method = name,
raw_data = options.raw_data,
callback = options.callback,
data = options.data,
})
end
---Retrieve a request and removes it from the internal requests list
---@param id integer
---@return lsp.server.request | nil
function Server:pop_request(id)
for index, request in ipairs(self.request_list) do
if request.id == id then
table.remove(self.request_list, index)
return request
end
end
return nil
end
---Try to fetch a server responses, notifications or requests
---in a specific amount of time.
---@param timeout integer Time in seconds, set to 0 to not wait
---@return table[]|boolean Responses list or false if failed
function Server:read_responses(timeout)
local proc = self.proc -- save current process to avoid it changing
if not proc or not proc:running() then
return false
end
if not self.read_responses_coroutine then
self.read_responses_coroutine = coroutine.create(function()
local buffer = ""
while true do
-- Read out all the headers
local output = buffer .. (proc:read_stdout(Server.BUFFER_SIZE) or "")
local content_start = output:match("\r\n\r\n()")
local buf = output
while not content_start do
if #output > 1024 then
-- After a kilobyte, still no end in sight for headers. Error out.
return error(string.format("Can't find headers delimiter after %d bytes. "..
"Something wrong with the server configuration?\nGot:\n%s", #output, output))
end
coroutine.yield(#buf > 0)
buf = proc:read_stdout(Server.BUFFER_SIZE)
if not buf then
-- If we stopped in the middle of a read, error out
if #output > 0 then
return error(string.format("Can't continue reading stdout:\n%s", output))
end
return
end
if #buf > 0 then
output = output .. buf
content_start = output:match("\r\n\r\n()")
end
end
-- Parse headers
local headers_data = output:sub(1, content_start - 4 - 1)
local content_length = 0
local headers = util.split(headers_data, "\r\n")
for _, header in ipairs(headers) do
-- We only care for Content-Length for now
local length = header:match("^Content%-Length: (%d+)$")
if length then
content_length = tonumber(length)
break
end
end
if not content_length then
return error(string.format("Bad header content:\n%s\n", headers_data))
end
-- Read all the expected content data
local content_data_t = { output:sub(content_start) }
buf = content_data_t[1]
local content_read_length = #buf
while content_read_length < content_length do
coroutine.yield(#buf > 0)
buf = proc:read_stdout(Server.BUFFER_SIZE)
if not buf then
return error(string.format("Can't continue reading stdout. Stopped at %d/%d.\n%s",
content_read_length, content_length, table.concat(content_data_t)))
end
content_read_length = content_read_length + #buf
table.insert(content_data_t, buf)
end
local content_data = table.concat(content_data_t)
-- We only need content_length bytes, so queue the rest for the next loop
buffer = content_data:sub(content_length + 1)
content_data = content_data:sub(1, content_length)
if self.verbose then
self:log("Got data.\nHeaders:\n%s\n\nContent:\n%s", headers_data, content_data)
end
coroutine.yield(#buffer > 0, content_data)
end
end)
end
if coroutine.status(self.read_responses_coroutine) == "dead" then
self.fatal_error = true
self:shutdown_if_needed()
return false
end
timeout = timeout or Server.DEFAULT_TIMEOUT
local max_time = timeout == 0 and math.huge or system.get_time() + timeout
local responses = {}
repeat
local status, has_more_data, response = coroutine.resume(self.read_responses_coroutine)
if response then table.insert(responses, response) end
if not status then
local error_msg = has_more_data
self:log("Disconnecting from server:\n%s", error_msg)
self.fatal_error = true
self:shutdown_if_needed()
return false
end
until not has_more_data or (timeout > 0 and system.get_time() >= max_time)
if #responses > 0 then
for index, data in ipairs(responses) do
local json_data = json.decode(data)
if json_data ~= false then
responses[index] = json_data
else
responses[index] = nil
self:log(
"JSON Parser Error: %s\n%s\n%s",
json.last_error(),
"-----",
data
)
return false
end
end
if #responses > 0 then
-- Reset write fails since server is sending responses
self.write_fails = 0
return responses
end
elseif self.verbose and timeout > 0 then
self:log("Could not read a response in %d seconds", timeout)
end
return false
end
---Get messages thrown by the stderr pipe of the server.
---@param timeout integer Time in seconds, set to 0 to not wait
---@return string|nil
function Server:read_errors(timeout)
local proc = self.proc -- save current process to avoid it changing
if not proc then return "" end
timeout = timeout or Server.DEFAULT_TIMEOUT
local inside_coroutine = self.yield_on_reads and coroutine.running() or false
local max_time = os.time() + timeout
if timeout == 0 then max_time = max_time + 1 end
local output = ""
while max_time > os.time() and output == "" do
output = proc:read_stderr(Server.BUFFER_SIZE)
if timeout == 0 then break end
if output == "" and inside_coroutine then
coroutine.yield()
end
end
if timeout == 0 and output ~= "" then
local new_output = nil
while new_output ~= "" do
new_output = proc:read_stderr(Server.BUFFER_SIZE)
if new_output ~= "" then
if new_output == nil then
break
end
output = output .. new_output
if inside_coroutine then
coroutine.yield()
end
end
end
end
return output or ""
end
---Try to send a request to a server in a specific amount of time.
---@param data table | string Table or string with the json request
---@return boolean written
---@return string? errmsg
function Server:write_request(data)
if not self.proc or not self.proc:running() then
return false
end
if type(data) == "table" then
data = json.encode(data)
end
-- WARNING: send_data performs yielding which can pontentially cause a
-- race condition, in case of future issues this may be the root cause.
return self:send_data(string.format(
'Content-Length: %d\r\n\r\n%s',
#data,
data
))
end
function Server:log(message, ...)
print (string.format("%s: " .. message .. "\n", self.name, ...))
end
---Call an apropriate signal handler for a given response.
---@param response table
function Server:send_response_signal(response)
local request = self:pop_request(response.id)
if request then
if not request.overwritten and request.callback then
request.callback(self, response, request)
elseif request.overwritten and request.overwritten_callback then
request.overwritten_callback(self, response, request)
end
return
end
self:on_response(response, request)
end
---Called for each response that doesn't has a signal handler.
---@param response table
---@param request lsp.server.request | nil
function Server:on_response(response, request)
if self.verbose then
self:log(
"Received response '%s' with result:\n%s",
response.id,
util.jsonprettify(json.encode(response))
)
end
end
---Register a request handler.
---@param method string
---@param callback lsp.server.responsecb
function Server:add_request_listener(method, callback)
if self.verbose then
self:log(
"Registering listener for '%s' requests",
method
)
end
if not self.request_listeners[method] then
self.request_listeners[method] = {}
end
table.insert(self.request_listeners[method], callback)
end
---Call an apropriate signal handler for a given request.
---@param request table
function Server:send_request_signal(request)
if not request.method then
if self.verbose and request.id then
self:log(
"Received empty response for previous request '%s'",
request.id
)
end
return
end
if self.request_listeners[request.method] then
for _, l in ipairs(self.request_listeners[request.method]) do
l(self, request)
end
else
self:on_request(request)
end
end
---Called for each request that doesn't has a signal handler.
---@param request table
function Server:on_request(request)
if self.verbose then
self:log(
"Received request '%s' with data:\n%s",
request.method,
util.jsonprettify(json.encode(request))
)
end
self:push_response(
request.method,
request.id,
nil,
{
code = Server.error_code.MethodNotFound,
message = "Method not found"
}
)
end
---Register a specialized message or notification listener.
---Notice that if no specialized listener is registered the
---on_notification() method will be called instead.
---@param method string
---@param callback lsp.server.notificationcb
function Server:add_message_listener(method, callback)
if self.verbose then
self:log(
"Registering listener for '%s' messages",
method
)
end
if not self.message_listeners[method] then
self.message_listeners[method] = {}
end
table.insert(self.message_listeners[method], callback)
end
---Call an apropriate signal handler for a given message or notification.
---@param message table
function Server:send_message_signal(message)
if self.message_listeners[message.method] then
for _, l in ipairs(self.message_listeners[message.method]) do
l(self, message.params)
end
else
self:on_message(message.method, message.params)
end
end
---Called for every message or notification without a signal handler.
---@param method string
---@Param params table
function Server:on_message(method, params)
if self.verbose then
self:log(
"Received notification '%s' with params:\n%s",
method,
util.jsonprettify(json.encode(params))
)
end
end
---Return the languageId for the specified doc.
---@param doc core.doc
---@return string
function Server:get_language_id(doc)
if type(self.language) == "string" then
return self.language
else
for _, l in ipairs(self.language) do
if string.match(doc.abs_filename, l.pattern) then
return l.id
end
end
end
return util.file_extension(doc.filename)
end
---Kills the server process and deinitialize the server object state.
function Server:stop()
self.initialized = false
self.proc = nil
self.request_list = {}
self.response_list = {}
self.notification_list = {}
self.raw_list = {}
end
---Shutdown the server if not running or amount of write fails
---reached the maximum allowed.
function Server:shutdown_if_needed()
if
self.write_fails >= self.write_fails_before_shutdown
or
(self.proc and not self.proc:running())
or
self.fatal_error
then
self:stop()
self:on_shutdown()
return
end
self.write_fails = self.write_fails + 1
end
---Can be overwritten to handle server shutdowns.
function Server:on_shutdown()
self:log("The server was shutdown.")
end
---Sends a shutdown notification to lsp and then stop it.
function Server:exit()
self.initialized = false
-- Send shutdown request
local message = {
jsonrpc = '2.0',
id = self.current_request + 1,
method = "shutdown",
params = {}
}
self:write_request(json.encode(message))
-- send exit notification
self:notify('exit')
self:stop()
end
return Server