Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: full completion support #18

Merged
merged 10 commits into from
Nov 11, 2024
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
Loading