Skip to content

Commit

Permalink
feat: full completion support (#18)
Browse files Browse the repository at this point in the history
* feat: full completion support

* test: add tests for virtual document change event

* fix: use vim.validate(spec)

* refactor: simplify buffer creation logic

* refactor: enforce version for vbufs

* refactor: use uris instead of fnames

* refactor: tidy up print messages

* fix: wrap `aftershave` request and notifications in coroutines

* docs: mark completions as done :)

* fix: log mismatch between virtual document versions instead of notifying

---------

Co-authored-by: tris203 <[email protected]>
  • Loading branch information
neubaner and tris203 authored Nov 11, 2024
1 parent 5474f68 commit 5b2e186
Show file tree
Hide file tree
Showing 21 changed files with 546 additions and 171 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down
106 changes: 67 additions & 39 deletions lua/rzls/documentstore.lua
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -16,26 +17,13 @@ local M = {}
---@type rzls.VirtualDocument<string, table<razor.LanguageKind, 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

Expand All @@ -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
Expand All @@ -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)

Expand All @@ -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
Expand Down
40 changes: 40 additions & 0 deletions lua/rzls/eventemitter.lua
Original file line number Diff line number Diff line change
@@ -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
129 changes: 105 additions & 24 deletions lua/rzls/handlers/completion.lua
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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
Loading

0 comments on commit 5b2e186

Please sign in to comment.