diff --git a/README.md b/README.md index 610a07c..bbe2ee0 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ auto-completion, go-to-definition, and more all from within neovim 💻🔧 | Formatting | ✅ | | Rename Symbol | ✅ | | Signature Help | ✅ | -| Completions | ❌(Partial) | +| Completions | ✅ | | Code Actions | ❌ | | Inlay Hints | ❌ | | Folding | ❌ | diff --git a/lua/rzls/documentstore.lua b/lua/rzls/documentstore.lua index fc9b1b4..f53a102 100644 --- a/lua/rzls/documentstore.lua +++ b/lua/rzls/documentstore.lua @@ -1,6 +1,7 @@ local razor = require("rzls.razor") local utils = require("rzls.utils") local VirtualDocument = require("rzls.virtual_document") +local Log = require("rzls.log") ---@class rzls.ProjectedDocuments ---@field virtual_html rzls.ProjectedDocument @@ -16,26 +17,13 @@ local M = {} ---@type rzls.VirtualDocument> local virtual_documents = {} ----@param name string ----@return number | nil -local function buffer_with_name(name) - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - local buf_name = vim.api.nvim_buf_get_name(buf) - if buf_name == name then - return buf - end - end -end - -local function get_or_create_buffer_for_filepath(filepath, filetype) - local buf = buffer_with_name(filepath) - if not buf then - buf = vim.api.nvim_create_buf(false, false) - vim.api.nvim_buf_set_name(buf, filepath) - vim.api.nvim_set_option_value("ft", filetype, { buf = buf }) - vim.api.nvim_set_option_value("buftype", "nowrite", { buf = buf }) - end - +---comment +---@param uri string +---@param suffix razor.VirtualSuffix? +---@return number +local function get_or_create_vbuffer_for_uri(uri, suffix) + local name = uri .. razor.virtual_suffixes[suffix] + local buf = vim.uri_to_bufnr(name) return buf end @@ -44,21 +32,30 @@ end function M.register_vbufs_by_path(current_file) -- open virtual files -- - virtual_documents[current_file] = virtual_documents[current_file] or {} + current_file = vim.uri_from_fname(current_file) - if vim.tbl_isempty(virtual_documents[current_file]) then - virtual_documents[current_file] = VirtualDocument:new(current_file, razor.language_kinds.razor) + if not virtual_documents[current_file] then + local buf = vim.uri_to_bufnr(current_file) + virtual_documents[current_file] = VirtualDocument:new(buf, razor.language_kinds.razor) end if virtual_documents[current_file][razor.language_kinds.csharp] == nil then - local buf = get_or_create_buffer_for_filepath(current_file .. razor.virtual_suffixes.csharp, "cs") + local buf = get_or_create_vbuffer_for_uri(current_file, "csharp") + vim.defer_fn(function() + -- Defer setting buftype to nowrite to let LSP attach + vim.api.nvim_set_option_value("buftype", "nowrite", { buf = buf }) + end, 250) virtual_documents[current_file][razor.language_kinds.csharp] = VirtualDocument:new(buf, razor.language_kinds.csharp) end if virtual_documents[current_file][razor.language_kinds.html] == nil then - local buf = get_or_create_buffer_for_filepath(current_file .. razor.virtual_suffixes.html, "html") + local buf = get_or_create_vbuffer_for_uri(current_file, "html") + vim.defer_fn(function() + -- Defer setting buftype to nowrite to let LSP attach + vim.api.nvim_set_option_value("buftype", "nowrite", { buf = buf }) + end, 250) virtual_documents[current_file][razor.language_kinds.html] = VirtualDocument:new(buf, razor.language_kinds.html) end @@ -68,7 +65,8 @@ end ---@param language_kind razor.LanguageKind function M.update_vbuf(result, language_kind) M.register_vbufs_by_path(result.hostDocumentFilePath) - local virtual_document = virtual_documents[result.hostDocumentFilePath][language_kind] + local uri = vim.uri_from_fname(result.hostDocumentFilePath) + local virtual_document = virtual_documents[uri][language_kind] virtual_document:update_content(result) @@ -80,28 +78,58 @@ end ---Creates virtual buffers for the given source buffer ---@param source_buf integer function M.register_vbufs(source_buf) - local currentFile = vim.api.nvim_buf_get_name(source_buf) + -- local currentFile = vim.api.nvim_buf_get_name(source_buf) + local currentFile = vim.uri_from_bufnr(source_buf) + currentFile = vim.uri_to_fname(currentFile) return M.register_vbufs_by_path(currentFile) end ----Converts a RPC return URI to a file path +---@async ---@param uri string ----@return string -local function uri_to_path(uri) - local path = uri:gsub("file://", "") - return path -end - ----@param uri string ----@param _version integer ---@param type razor.LanguageKind +---@param version integer | "any"? ---@return rzls.VirtualDocument | nil -function M.get_virtual_document(uri, _version, type) - local doc = virtual_documents[uri_to_path(uri)] +function M.get_virtual_document(uri, type, version) + local doc = virtual_documents[uri] if type == razor.language_kinds.razor then return doc end - return doc and doc[type] + assert(version, "version is required for virtual documents") + + ---@type rzls.VirtualDocument + local virtual_document = doc and doc[type] + + if virtual_document == nil then + return nil + end + + if version == "any" or virtual_document.host_document_version == version then + return virtual_document + end + + local current_coroutine = coroutine.running() + local dispose_handler = virtual_document.change_event:on(function() + coroutine.resume(current_coroutine) + end) + + while virtual_document.host_document_version < version do + coroutine.yield() + end + + dispose_handler() + + -- The client might be ahead of requested version due to other document + -- changes while we were synchronizing + if virtual_document.host_document_version ~= version then + Log.rzlsnvim = string.format( + 'Mismatch between virtual document version. Uri: "%s". Server: %d. Client: %d', + virtual_document.path, + version, + virtual_document.host_document_version + ) + end + + return virtual_document end local pipe_name diff --git a/lua/rzls/eventemitter.lua b/lua/rzls/eventemitter.lua new file mode 100644 index 0000000..e73bb20 --- /dev/null +++ b/lua/rzls/eventemitter.lua @@ -0,0 +1,40 @@ +---@class rzls.EventEmitter +---@field listeners (fun(...))[] +local EventEmitter = {} + +EventEmitter.__index = EventEmitter + +function EventEmitter:new() + return setmetatable({ + listeners = {}, + }, self) +end + +function EventEmitter:fire(...) + local args = { ... } + vim.schedule(function() + for _, handler in ipairs(self.listeners) do + handler(unpack(args)) + end + end) +end + +function EventEmitter:on(handler) + table.insert(self.listeners, handler) + + return function() + local handler_index = 0 + for index, cur_handler in ipairs(self.listeners) do + if rawequal(cur_handler, handler) then + handler_index = index + break + end + end + + if handler_index > 0 then + table.remove(self.listeners, handler_index) + end + end +end + +return EventEmitter diff --git a/lua/rzls/handlers/completion.lua b/lua/rzls/handlers/completion.lua index 09bb765..3524418 100644 --- a/lua/rzls/handlers/completion.lua +++ b/lua/rzls/handlers/completion.lua @@ -1,38 +1,25 @@ local documentstore = require("rzls.documentstore") +local razor = require("rzls.razor") ----@param err lsp.ResponseError ----@param result razor.DelegatedCompletionParams ----@param _ctx lsp.HandlerContext ----@param _config table -return function(err, result, _ctx, _config) - assert(not err, err) - - local virtual_document = documentstore.get_virtual_document( - result.identifier.textDocumentIdentifier.uri, - result.identifier.version, - result.projectedKind - ) - assert(virtual_document, "No virtual document found") - +---@param virtual_document rzls.VirtualDocument +---@param projected_position lsp.Position +---@param trigger_kind integer +---@param trigger_character string|nil +local function provide_lsp_completions(virtual_document, projected_position, trigger_kind, trigger_character) local virtual_client = virtual_document:get_lsp_client() assert(virtual_client, "No virtual client found") - --- "@" is not a valid trigger character for C# and HTML - local trigger_character = result.context.triggerCharacter == "@" and result.context.triggerCharacter or nil - local trigger_kind = result.context.triggerCharacter == "@" and result.context.triggerKind or 1 -- Invoked - ---@type lsp.CompletionParams local params = { context = { triggerKind = trigger_kind, triggerCharacter = trigger_character, }, - position = result.projectedPosition, + position = projected_position, textDocument = { uri = vim.uri_from_bufnr(virtual_document.buf), }, } - local response = virtual_client.request_sync(vim.lsp.protocol.Methods.textDocument_completion, params, nil, virtual_document.buf) @@ -45,10 +32,104 @@ return function(err, result, _ctx, _config) end if response.err ~= nil then - return nil, err + return nil, response.err end - return response.result or { - items = {}, - } + ---@type lsp.CompletionList | lsp.CompletionItem[] | nil + local completion_items = response.result + + if vim.islist(completion_items) then + return { + items = completion_items --[[@as lsp.CompletionItem[] ]], + isIncomplete = false, + }, + nil + elseif completion_items ~= nil then + return completion_items --[[@as lsp.CompletionList]] + else + return { + isIncomplete = true, + items = {}, + } + end +end + +---@param virtual_document rzls.VirtualDocument +---@param projected_position lsp.Position +---@param provisional_text_edit lsp.TextEdit|nil +---@param trigger_kind integer +---@param trigger_character string|nil +---@return lsp.CompletionList|nil +---@return lsp.ResponseError|nil +local function provide_csharp_completions( + virtual_document, + projected_position, + provisional_text_edit, + trigger_kind, + trigger_character +) + local absolute_index = virtual_document:index_of_position(projected_position) + + virtual_document:clear_resolve_completion_request_variables() + + if provisional_text_edit ~= nil then + if absolute_index == -1 then + return { + items = {}, + isIncomplete = false, + } + end + virtual_document:add_provisional_dot_at(absolute_index) + virtual_document.provisional_dot_position = projected_position + virtual_document:ensure_content() + end + + local lsp_response, err = + provide_lsp_completions(virtual_document, projected_position, trigger_kind, trigger_character) + + if provisional_text_edit ~= nil and virtual_document:remove_provisional_dot() then + virtual_document:ensure_content() + end + + return lsp_response, err +end + +---@param err lsp.ResponseError +---@param result razor.DelegatedCompletionParams +---@param _ctx lsp.HandlerContext +---@param _config table +---@return lsp.CompletionList|nil +---@return lsp.ResponseError|nil +return function(err, result, _ctx, _config) + assert(not err, err) + + local virtual_document = documentstore.get_virtual_document( + result.identifier.textDocumentIdentifier.uri, + result.projectedKind, + result.identifier.version + ) + assert(virtual_document, "No virtual document found") + + if result.identifier.version ~= virtual_document.host_document_version then + return { + items = {}, + isIncomplete = false, + } + end + + --- "@" is not a valid trigger character for C# and HTML + local trigger_character = result.context.triggerCharacter == "@" and nil or result.context.triggerCharacter + local trigger_kind = result.context.triggerCharacter == "@" and 1 or result.context.triggerKind -- Invoked + + if result.projectedKind == razor.language_kinds.csharp then + return provide_csharp_completions( + virtual_document, + result.projectedPosition, + result.provisionalTextEdit, + trigger_kind, + trigger_character + ) + else + return provide_lsp_completions(virtual_document, result.projectedPosition, trigger_kind, trigger_character) + end end diff --git a/lua/rzls/handlers/completionitemresolve.lua b/lua/rzls/handlers/completionitemresolve.lua new file mode 100644 index 0000000..2458f95 --- /dev/null +++ b/lua/rzls/handlers/completionitemresolve.lua @@ -0,0 +1,51 @@ +local documentstore = require("rzls.documentstore") +local razor = require("rzls.razor") + +---@param err lsp.ResponseError +---@param result razor.DelegatedCompletionItemResolveParams +---@param _ctx lsp.HandlerContext +---@param _config table +return function(err, result, _ctx, _config) + assert(not err, err) + + if result.originatingKind == razor.language_kinds.csharp and result.completionItem.data.TextDocument ~= nil then + local virtual_document = documentstore.get_virtual_document( + result.identifier.textDocumentIdentifier.uri, + result.originatingKind, + result.identifier.version + ) + assert(virtual_document, "No virtual document found") + + local virtual_client = virtual_document:get_lsp_client() + assert(virtual_client, "No virtual client found") + + if virtual_document.provisional_dot_position and virtual_document:ensure_resolve_provisional_dot() then + virtual_document:ensure_content() + end + + local response = virtual_client.request_sync( + vim.lsp.protocol.Methods.completionItem_resolve, + result.completionItem, + nil, + virtual_document.buf + ) + + if virtual_document.provisional_dot_position and virtual_document:remove_resolve_provisional_dot() then + virtual_document:ensure_content() + end + + assert(response, "Virtual LSP didn't return any results for completionItem/resolve call") + + if response.err ~= nil then + return nil, response.err + end + + if response.result == nil then + return result.completionItem + end + + return response.result + end + + return result.completionItem +end diff --git a/lua/rzls/handlers/csharppulldiagnostics.lua b/lua/rzls/handlers/csharppulldiagnostics.lua index 0902c32..4690e8b 100644 --- a/lua/rzls/handlers/csharppulldiagnostics.lua +++ b/lua/rzls/handlers/csharppulldiagnostics.lua @@ -8,12 +8,8 @@ local empty_response = {} ---@param _ctx lsp.HandlerContext ---@param _config table return function(_err, result, _ctx, _config) - local virtual_document = documentstore.get_virtual_document( - result.textDocument.uri, - ---@diagnostic disable-next-line: undefined-field - result._razor_hostDocumentVersion, - razor.language_kinds.csharp - ) + local virtual_document = + documentstore.get_virtual_document(result.textDocument.uri, razor.language_kinds.csharp, "any") assert(virtual_document, "csharp document was not found") local virtual_client = virtual_document:get_lsp_client() diff --git a/lua/rzls/handlers/htmlformatting.lua b/lua/rzls/handlers/htmlformatting.lua index c1dcfc8..cbee912 100644 --- a/lua/rzls/handlers/htmlformatting.lua +++ b/lua/rzls/handlers/htmlformatting.lua @@ -19,8 +19,8 @@ return function(err, result, _ctx, _config) local virtual_document = documentstore.get_virtual_document( result.textDocument.uri, - result._razor_hostDocumentVersion, - razor.language_kinds.html + razor.language_kinds.html, + result.hostDocumentVersion ) assert(virtual_document, "Could not find html virtual document") diff --git a/lua/rzls/handlers/init.lua b/lua/rzls/handlers/init.lua index 38b9310..3eb0b7d 100644 --- a/lua/rzls/handlers/init.lua +++ b/lua/rzls/handlers/init.lua @@ -69,6 +69,7 @@ return { ["razor/csharpPullDiagnostics"] = require("rzls.handlers.csharppulldiagnostics"), ["textDocument/colorPresentation"] = not_supported, ["razor/completion"] = require("rzls.handlers.completion"), + ["razor/completionItem/resolve"] = require("rzls.handlers.completionitemresolve"), ["window/logMessage"] = function(_, result) Log.rzls = result.message return vim.lsp.handlers[vim.lsp.protocol.Methods.window_logMessage] diff --git a/lua/rzls/handlers/providesemantictokensrange.lua b/lua/rzls/handlers/providesemantictokensrange.lua index 426f800..b29059e 100644 --- a/lua/rzls/handlers/providesemantictokensrange.lua +++ b/lua/rzls/handlers/providesemantictokensrange.lua @@ -11,8 +11,8 @@ local empty_response = {} return function(_err, result, _ctx, _config) local vd = documentstore.get_virtual_document( result.textDocument.uri, - result.requiredHostDocumentVersion, - razor.language_kinds.csharp + razor.language_kinds.csharp, + result.requiredHostDocumentVersion ) if not vd then return { tokens = empty_response, hostDocumentSyncVersion = result.requiredHostDocumentVersion }, nil diff --git a/lua/rzls/health.lua b/lua/rzls/health.lua index 0f34560..0a1f411 100644 --- a/lua/rzls/health.lua +++ b/lua/rzls/health.lua @@ -18,9 +18,16 @@ M.check = function() for _, lang in pairs({ "csharp", "html" }) do local doc = docs[razor.language_kinds[lang]] - if doc and doc.buf then + if doc and doc.buf and doc.path then vim.health.ok( - " " .. lang .. " virtual document found: " .. doc.buf .. " v: " .. doc.host_document_version + " " + .. lang + .. " virtual document found: [buf:" + .. doc.buf + .. "] [v:" + .. doc.host_document_version + .. "]" + .. doc.path ) else vim.health.error(" " .. lang .. " virtual document not found") diff --git a/lua/rzls/razor.lua b/lua/rzls/razor.lua index ff62d67..ba9e4ba 100644 --- a/lua/rzls/razor.lua +++ b/lua/rzls/razor.lua @@ -41,6 +41,11 @@ local M = {} ---@field provisionalTextEdit lsp.TextEdit ---@field shouldIncludeSnippets boolean +---@class razor.DelegatedCompletionItemResolveParams +---@field identifier { textDocumentIdentifier: lsp.TextDocumentIdentifier, version: integer } +---@field completionItem lsp.CompletionItem +---@field originatingKind razor.LanguageKind + ---@class razor.ProvideDynamicFileParams ---@field razorDocument lsp.TextDocumentIdentifier diff --git a/lua/rzls/roslyn_handlers.lua b/lua/rzls/roslyn_handlers.lua index 07caf18..9f438ac 100644 --- a/lua/rzls/roslyn_handlers.lua +++ b/lua/rzls/roslyn_handlers.lua @@ -11,14 +11,13 @@ local function roslyn_razor_provideDynamicFileHandler(_err, result, _ctx, _confi if result.razorDocument == nil then return nil, vim.lsp.rpc.rpc_response_error(-32602, "Missing razorDocument") end - local vd = documentstore.get_virtual_document(result.razorDocument.uri, 0, razor.language_kinds.csharp) + local vd = documentstore.get_virtual_document(result.razorDocument.uri, razor.language_kinds.csharp, "any") if not vd then return nil, vim.lsp.rpc.rpc_response_error(-32600, "Could not find requested document") end local bufnr = vd.buf if bufnr == nil then - vim.print(vim.inspect(result)) return nil, vim.lsp.rpc.rpc_response_error(-32600, "Could not find requested document") end diff --git a/lua/rzls/server/lsp.lua b/lua/rzls/server/lsp.lua index 061151a..b8efa02 100644 --- a/lua/rzls/server/lsp.lua +++ b/lua/rzls/server/lsp.lua @@ -32,24 +32,28 @@ function M.server() Log.aftershave = "Started aftershave server" function srv.request(method, params, handler) - if requests[method] then - Log.aftershave = "Handled " .. method - local response = requests[method](params) - handler(nil, response) - elseif method == "exit" then - Log.aftershave = "Closing aftershave server" - closing = true - else - Log.aftershave = "Unhandled request " .. method - end + coroutine.wrap(function() + if requests[method] then + Log.aftershave = "Handled " .. method + local response = requests[method](params) + handler(nil, response) + elseif method == "exit" then + Log.aftershave = "Closing aftershave server" + closing = true + else + Log.aftershave = "Unhandled request " .. method + end + end)() end function srv.notify(method, _params) - if method == "exit" then - closing = true - elseif not noops[method] then - Log.aftershave = "Unhandled notification " .. method - end + coroutine.wrap(function() + if method == "exit" then + closing = true + elseif not noops[method] then + Log.aftershave = "Unhandled notification " .. method + end + end)() end function srv.is_closing() diff --git a/lua/rzls/server/methods/definition.lua b/lua/rzls/server/methods/definition.lua index f0adf90..c8eb86c 100644 --- a/lua/rzls/server/methods/definition.lua +++ b/lua/rzls/server/methods/definition.lua @@ -6,26 +6,23 @@ local razor = require("rzls.razor") return function(params) ---@type lsp.Position local position = params.position - ---@type integer - local razor_bufnr = vim.uri_to_bufnr(params.textDocument.uri) - local razor_docname = vim.api.nvim_buf_get_name(razor_bufnr) - local rvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.razor) + local rvd = documentstore.get_virtual_document(params.textDocument.uri, razor.language_kinds.razor) assert(rvd, "Could not find virtual document") local client = rvd:get_lsp_client() assert(client, "Could not find Razor Client") local language_query_response = client.request_sync("razor/languageQuery", { position = position, - uri = vim.uri_from_bufnr(razor_bufnr), - }, nil, razor_bufnr) + uri = rvd.path, + }, nil, rvd.buf) assert(language_query_response) local virtual_document = documentstore.get_virtual_document( - vim.uri_from_bufnr(razor_bufnr), - language_query_response.result.hostDocumentVersion, - language_query_response.result.kind + rvd.path, + language_query_response.result.kind, + language_query_response.result.hostDocumentVersion ) assert(virtual_document) @@ -37,7 +34,7 @@ return function(params) local definition_result = virtual_buf_client.request_sync("textDocument/definition", { textDocument = { - uri = vim.uri_from_bufnr(virtual_document.buf), + uri = virtual_document.path, }, position = language_query_response.result.position, }, nil, virtual_document.buf) @@ -60,10 +57,10 @@ return function(params) table.insert(response, data) elseif v.uri:match(razor.virtual_suffixes.csharp .. "$") then local mapped_loc = client.request_sync("razor/mapToDocumentRanges", { - razorDocumentUri = vim.uri_from_bufnr(razor_bufnr), + razorDocumentUri = rvd.path, kind = language_query_response.result.kind, projectedRanges = { v.range }, - }, nil, razor_bufnr) + }, nil, rvd.buf) if mapped_loc and mapped_loc.result and mapped_loc.result.ranges[1] then ---@type lsp.Definition local data = { diff --git a/lua/rzls/server/methods/hover.lua b/lua/rzls/server/methods/hover.lua index dbdc320..08641ef 100644 --- a/lua/rzls/server/methods/hover.lua +++ b/lua/rzls/server/methods/hover.lua @@ -6,26 +6,23 @@ local razor = require("rzls.razor") return function(params) ---@type lsp.Position local position = params.position - ---@type integer - local razor_bufnr = vim.uri_to_bufnr(params.textDocument.uri) - local razor_docname = vim.api.nvim_buf_get_name(razor_bufnr) - local rvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.razor) + local rvd = documentstore.get_virtual_document(params.textDocument.uri, razor.language_kinds.razor) assert(rvd, "Could not find virtual document") local client = rvd:get_lsp_client() assert(client, "Could not find Razor Client") local language_query_response = client.request_sync("razor/languageQuery", { position = position, - uri = vim.uri_from_bufnr(razor_bufnr), - }, nil, razor_bufnr) + uri = rvd.path, + }, nil, rvd.buf) assert(language_query_response) local virtual_document = documentstore.get_virtual_document( - vim.uri_from_bufnr(razor_bufnr), - language_query_response.result.hostDocumentVersion, - language_query_response.result.kind + rvd.path, + language_query_response.result.kind, + language_query_response.result.hostDocumentVersion ) assert(virtual_document) @@ -37,7 +34,7 @@ return function(params) local hover_result = virtual_buf_client.request_sync("textDocument/hover", { textDocument = { - uri = vim.uri_from_bufnr(virtual_document.buf), + uri = virtual_document.path, }, position = language_query_response.result.position, }, nil, virtual_document.buf) @@ -47,10 +44,10 @@ return function(params) end local response = client.request_sync("razor/mapToDocumentRanges", { - razorDocumentUri = vim.uri_from_bufnr(razor_bufnr), + razorDocumentUri = rvd.path, kind = language_query_response.result.kind, projectedRanges = { hover_result.result.range }, - }, nil, razor_bufnr) + }, nil, rvd.buf) if response and response.result ~= nil and response.result.ranges[1] ~= nil then ---@type lsp.Hover diff --git a/lua/rzls/server/methods/references.lua b/lua/rzls/server/methods/references.lua index 9c90ef7..58b3b4b 100644 --- a/lua/rzls/server/methods/references.lua +++ b/lua/rzls/server/methods/references.lua @@ -7,25 +7,23 @@ return function(params) ---@type lsp.Position local position = params.position ---@type integer - local razor_bufnr = vim.uri_to_bufnr(params.textDocument.uri) - local razor_docname = vim.api.nvim_buf_get_name(razor_bufnr) - local rvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.razor) + local rvd = documentstore.get_virtual_document(params.textDocument.uri, razor.language_kinds.razor) assert(rvd, "Could not find virtual document") local client = rvd:get_lsp_client() assert(client, "Could not find Razor Client") local language_query_response = client.request_sync("razor/languageQuery", { position = position, - uri = vim.uri_from_bufnr(razor_bufnr), - }, nil, razor_bufnr) + uri = rvd.path, + }, nil, rvd.buf) assert(language_query_response) local virtual_document = documentstore.get_virtual_document( - vim.uri_from_bufnr(razor_bufnr), - language_query_response.result.hostDocumentVersion, - language_query_response.result.kind + rvd.path, + language_query_response.result.kind, + language_query_response.result.hostDocumentVersion ) assert(virtual_document) @@ -37,7 +35,7 @@ return function(params) local references_result = virtual_buf_client.request_sync("textDocument/references", { textDocument = { - uri = vim.uri_from_bufnr(virtual_document.buf), + uri = virtual_document.path, }, position = language_query_response.result.position, context = { @@ -63,10 +61,10 @@ return function(params) table.insert(response, data) elseif v.uri:match(razor.virtual_suffixes.csharp .. "$") then local mapped_loc = client.request_sync("razor/mapToDocumentRanges", { - razorDocumentUri = vim.uri_from_bufnr(razor_bufnr), + razorDocumentUri = rvd.path, kind = language_query_response.result.kind, projectedRanges = { v.range }, - }, nil, razor_bufnr) + }, nil, rvd.buf) if mapped_loc and mapped_loc.result and mapped_loc.result.ranges[1] then ---@type lsp.Definition local data = { diff --git a/lua/rzls/server/methods/rename.lua b/lua/rzls/server/methods/rename.lua index 0a4936e..7a8dd35 100644 --- a/lua/rzls/server/methods/rename.lua +++ b/lua/rzls/server/methods/rename.lua @@ -16,18 +16,16 @@ return function(params) ---@type lsp.Position local position = params.position ---@type integer - local razor_bufnr = vim.uri_to_bufnr(params.textDocument.uri) - local razor_docname = vim.api.nvim_buf_get_name(razor_bufnr) - local rvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.razor) + local rvd = documentstore.get_virtual_document(params.textDocument.uri, razor.language_kinds.razor) assert(rvd, "Could not find virtual document") local client = rvd:get_lsp_client() assert(client, "Could not find Razor Client") local language_query_response = client.request_sync("razor/languageQuery", { position = position, - uri = vim.uri_from_bufnr(razor_bufnr), - }, nil, razor_bufnr) + uri = rvd.path, + }, nil, rvd.buf) assert(language_query_response) @@ -37,7 +35,11 @@ return function(params) return nil end - local csvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.csharp) + local csvd = documentstore.get_virtual_document( + params.textDocument.uri, + razor.language_kinds.csharp, + language_query_response.result.hostDocumentVersion + ) assert(csvd, "Could not find virtual document") local roslyn_client = csvd:get_lsp_client() @@ -45,11 +47,11 @@ return function(params) local edits = roslyn_client.request_sync("textDocument/rename", { textDocument = { - uri = vim.uri_from_bufnr(csvd.buf), + uri = csvd.path, }, position = language_query_response.result.position, newName = params.newName, - }, nil, razor_bufnr) + }, nil, rvd.buf) assert(edits and not edits.err and edits.result, "Rename request failed") ---@type lsp.WorkspaceEdit @@ -66,10 +68,10 @@ return function(params) local remapped_edits = {} for _, edit in ipairs(changes.edits) do local remapped_response = razor_client.request_sync("razor/mapToDocumentRanges", { - razorDocumentUri = vim.uri_from_bufnr(razor_bufnr), + razorDocumentUri = rvd.path, kind = razor.language_kinds.csharp, projectedRanges = { edit.range }, - }, nil, razor_bufnr) + }, nil, rvd.buf) if remapped_response and remapped_response.result ~= nil and remapped_response.result.ranges ~= nil then for _, range in ipairs(remapped_response.result.ranges) do diff --git a/lua/rzls/server/methods/signaturehelp.lua b/lua/rzls/server/methods/signaturehelp.lua index a5eb5f8..5515b63 100644 --- a/lua/rzls/server/methods/signaturehelp.lua +++ b/lua/rzls/server/methods/signaturehelp.lua @@ -6,38 +6,39 @@ local razor = require("rzls.razor") return function(params) ---@type lsp.Position local position = params.position - ---@type integer - local razor_bufnr = vim.uri_to_bufnr(params.textDocument.uri) - local razor_docname = vim.api.nvim_buf_get_name(razor_bufnr) - local rvd = documentstore.get_virtual_document(razor_docname, 0, razor.language_kinds.razor) + local rvd = documentstore.get_virtual_document(params.textDocument.uri, razor.language_kinds.razor) assert(rvd, "Could not find virtual document") local client = rvd:get_lsp_client() assert(client, "Could not find Razor Client") local language_query_response = client.request_sync("razor/languageQuery", { position = position, - uri = vim.uri_from_bufnr(razor_bufnr), - }, nil, razor_bufnr) + uri = rvd.path, + }, nil, rvd.buf) assert(language_query_response) local lsp_client = vim.lsp.get_clients({ name = razor.lsp_names[language_query_response.result.kind] })[1] assert(lsp_client, "Could not find LSP Client for response type: " .. language_query_response.result.kind) - local vd = documentstore.get_virtual_document(razor_docname, 0, language_query_response.result.kind) + local vd = documentstore.get_virtual_document( + rvd.path, + language_query_response.result.kind, + language_query_response.result.hostDocumentVersion + ) assert(vd, "Could not find virtual document from projection result") ---@type lsp.SignatureHelpParams local sigHelpReq = { textDocument = { - uri = vim.uri_from_bufnr(vd.buf), + uri = vd.path, }, position = language_query_response.result.position, context = params.context, } - local sig_help = lsp_client.request_sync("textDocument/signatureHelp", sigHelpReq, nil, razor_bufnr) + local sig_help = lsp_client.request_sync("textDocument/signatureHelp", sigHelpReq, nil, rvd.buf) if not sig_help or sig_help.err then return nil diff --git a/lua/rzls/virtual_document.lua b/lua/rzls/virtual_document.lua index 847f608..0c9e62b 100644 --- a/lua/rzls/virtual_document.lua +++ b/lua/rzls/virtual_document.lua @@ -1,4 +1,6 @@ local razor = require("rzls.razor") +local utils = require("rzls.utils") +local EventEmitter = require("rzls.eventemitter") ---@class rzls.VirtualDocument ---@field buf number @@ -6,34 +8,28 @@ local razor = require("rzls.razor") ---@field host_document_version number ---@field content string ---@field kind razor.LanguageKind +---@field change_event rzls.EventEmitter +---@field pre_provisional_content string|nil +---@field pre_resolve_provisional_content string|nil +---@field provisional_edit_at number|nil +---@field resolve_provisional_edit_at number|nil +---@field provisional_dot_position lsp.Position | nil local VirtualDocument = {} VirtualDocument.__index = VirtualDocument ----@param bufnr integer|string +---@param bufnr integer ---@param kind razor.LanguageKind ---@return rzls.VirtualDocument function VirtualDocument:new(bufnr, kind) - if type(bufnr) == "string" then - local virtual_document = setmetatable({ - buf = vim.uri_to_bufnr("file://" .. bufnr), - host_document_version = 0, - content = "", - path = bufnr, - kind = kind, - }, self) - return virtual_document - elseif type(bufnr) == "number" then - local virtual_document = setmetatable({ - buf = bufnr, - host_document_version = 0, - content = "", - path = vim.api.nvim_buf_get_name(bufnr), - kind = kind, - }, self) - return virtual_document - end - error("Invalid buffer number") + return setmetatable({ + buf = bufnr, + host_document_version = 0, + content = "", + path = vim.uri_from_bufnr(bufnr), + kind = kind, + change_event = EventEmitter:new(), + }, self) end ---@param content string @@ -52,6 +48,12 @@ function VirtualDocument:update_content(result) end self.host_document_version = result.hostDocumentVersion + + self.change_event:fire() +end + +function VirtualDocument:ensure_content() + vim.api.nvim_buf_set_lines(self.buf, 0, -1, true, self:lines()) end ---@return vim.lsp.Client|nil @@ -73,4 +75,99 @@ function VirtualDocument:line_at(line) return lines[line] end +---@param index number +function VirtualDocument:add_provisional_dot_at(index) + if self.provisional_edit_at == index then + return + end + + -- reset provisional edits + self:remove_provisional_dot() + self.resolve_provisional_edit_at = nil + self.provisional_dot_position = nil + + local new_content = apply_change(self.content, { + newText = ".", + span = { + start = index, + ["end"] = index, + length = ("."):len(), + }, + }) + + self.pre_provisional_content = self.content + self.provisional_edit_at = index + self.content = new_content +end + +function VirtualDocument:remove_provisional_dot() + if self.provisional_edit_at and self.pre_provisional_content then + self.content = self.pre_provisional_content + self.resolve_provisional_edit_at = self.provisional_edit_at + self.provisional_edit_at = nil + self.pre_provisional_content = nil + + return true + end + + return false +end + +function VirtualDocument:ensure_resolve_provisional_dot() + self.remove_provisional_dot(self) + + if self.resolve_provisional_edit_at then + local new_content = apply_change(self.content, { + newText = ".", + span = { + start = self.resolve_provisional_edit_at, + ["end"] = self.resolve_provisional_edit_at, + length = ("."):len(), + }, + }) + self.pre_resolve_provisional_content = self.content + self.content = new_content + + return true + end + + return false +end + +function VirtualDocument:remove_resolve_provisional_dot() + if self.resolve_provisional_edit_at and self.pre_resolve_provisional_content then + self.content = self.pre_resolve_provisional_content + self.provisional_edit_at = nil + self.pre_resolve_provisional_content = nil + + return true + end + + return false +end + +function VirtualDocument:clear_resolve_completion_request_variables() + self.resolve_provisional_edit_at = nil + self.provisional_dot_position = nil +end + +---@param position lsp.Position +function VirtualDocument:index_of_position(position) + local eol = utils.buffer_eol(self.buf) + + local content = self.content + local line_number = 0 + local index = 0 + + for line in vim.gsplit(content, eol, { plain = true }) do + if line_number == position.line then + return index + position.character - 1 + end + index = index + line:len() + eol:len() + line_number = line_number + 1 + end + + return -1 +end + return VirtualDocument diff --git a/tests/rzls/documentstore_spec.lua b/tests/rzls/documentstore_spec.lua index 92d22c0..1b00548 100644 --- a/tests/rzls/documentstore_spec.lua +++ b/tests/rzls/documentstore_spec.lua @@ -7,12 +7,13 @@ describe("documentstore", function() it("create and retreive docs", function() local path = "tests/rzls/fixtures/test2.razor" local path_prefix = vim.loop.cwd() .. "/" - local full_path = path_prefix .. path + local full_path = "file://" .. path_prefix .. path vim.cmd.edit({ args = { path } }) local init_buf = vim.api.nvim_list_bufs() documentstore.register_vbufs(init_buf[1]) for _, lang in pairs({ 1, 2 }) do - local doc = documentstore.get_virtual_document(full_path, 0, lang) + local doc = documentstore.get_virtual_document(full_path, lang, 0) + assert(doc, "Could not find virtual document") eq(doc.kind, lang) end local bufs = vim.api.nvim_list_bufs() diff --git a/tests/rzls/virtual_document_spec.lua b/tests/rzls/virtual_document_spec.lua index bd246d3..b50fc98 100644 --- a/tests/rzls/virtual_document_spec.lua +++ b/tests/rzls/virtual_document_spec.lua @@ -6,10 +6,11 @@ local eq = assert.are.same describe("virtual document", function() local vd local path = "tests/rzls/fixtures/vdtest.razor__virtual.html" - local full_path = vim.loop.cwd() .. "/" .. path + local full_path = "file://" .. vim.loop.cwd() .. "/" .. path vim.cmd.edit({ args = { path } }) local ls = vim.fn.getbufinfo({ buflisted = 1 }) local bufnr = ls[1].bufnr + it("create virtual document", function() vd = virtual_document:new(bufnr, razor.language_kinds.html) eq({ @@ -18,6 +19,9 @@ describe("virtual document", function() content = "", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) end) @@ -42,6 +46,9 @@ describe("virtual document", function() content = "Hello\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) vd:update_content({ previousWasEmpty = false, @@ -63,6 +70,9 @@ describe("virtual document", function() content = "Hello World\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) vd:update_content({ previousWasEmpty = false, @@ -84,6 +94,9 @@ describe("virtual document", function() content = "Hello stuff\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) vd:update_content({ previousWasEmpty = false, @@ -105,6 +118,9 @@ describe("virtual document", function() content = "Hello in the middle stuff\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) vd:update_content({ previousWasEmpty = false, @@ -126,6 +142,9 @@ describe("virtual document", function() content = "i💩\nHello in the middle stuff\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) vd:update_content({ previousWasEmpty = false, @@ -147,6 +166,57 @@ describe("virtual document", function() content = "Hello in the middle stuff\n", kind = razor.language_kinds.html, path = full_path, + change_event = { + listeners = {}, + }, }, vd) end) + + it("trigger change event when a document content changes", function() + local update_handler_called = false + + local function update_handler() + update_handler_called = true + end + + local dispose_handler = vd.change_event:on(update_handler) + + vd:update_content({ + previousWasEmpty = false, + hostDocumentVersion = 7, + hostDocumentFilePath = full_path, + changes = { + { + newText = "", + span = { + start = 0, + length = 0, + }, + }, + }, + }) + + eq({ + buf = bufnr, + host_document_version = 7, + content = "Hello in the middle stuff\n", + kind = razor.language_kinds.html, + path = full_path, + change_event = { + listeners = { update_handler }, + }, + }, vd) + + -- Schedule resuming to give a change to the `update_handler` to be called + local co = coroutine.running() + vim.schedule(function() + coroutine.resume(co) + end) + coroutine.yield() + + eq(true, update_handler_called) + + dispose_handler() + eq({}, vd.change_event.listeners) + end) end)