1662 lines
48 KiB
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
|