From 110abba8b040e3ed3f4dc43e147bd2330c2b5904 Mon Sep 17 00:00:00 2001 From: monkoose Date: Mon, 16 Sep 2024 19:35:08 +0300 Subject: [PATCH] Combine completer and renderer into one module My attempt to completely decouple this two is failed, so it is better to just combine them to simplify codebase. --- README.md | 15 -- lua/neocodeium/completer.lua | 495 +++++++++++++++++++++++++++++++++-- lua/neocodeium/events.lua | 10 - lua/neocodeium/init.lua | 13 +- lua/neocodeium/renderer.lua | 484 ---------------------------------- lua/neocodeium/server.lua | 10 - 6 files changed, 472 insertions(+), 555 deletions(-) delete mode 100644 lua/neocodeium/renderer.lua diff --git a/README.md b/README.md index dcf395e..6dc50ab 100644 --- a/README.md +++ b/README.md @@ -453,21 +453,6 @@ your code base. When you switch buffers, this context should be updated automatically (it takes some time). You can see current chat context in the left bottom corner. -### 🚗 Roadmap - -- [x] Add vimdoc help -- [x] Add the command to open buffer with the log output -- [x] Add :checkhealth -- [x] Add support for Codeium Chat (in browser only) -- [ ] Completely decouple renderer from completer. -Add an custom renderer setup option. -Renderer is already pretty much decoupled, but still transforms data it receives and -so completer receives from it more data than required. Its API should -consist of: display(), update() and clear() methods and do not pass any data to -the completer. Not sure if someone will use it, but there are always some enthusiasts and hackers. -With this feature implementing some custom renderer (like for any extendable completion engine) would be easy. -Provide an example of custom renderer with built-in completion (omnifunc). - ### 💐 Credits - [codeium.vim] - The main source for understanding how to use Codeium. diff --git a/lua/neocodeium/completer.lua b/lua/neocodeium/completer.lua index aadc334..9404ce5 100644 --- a/lua/neocodeium/completer.lua +++ b/lua/neocodeium/completer.lua @@ -6,9 +6,7 @@ local utils = require("neocodeium.utils") local options = require("neocodeium.options").options local types = require("neocodeium._types") local server = require("neocodeium.server") -local renderer = require("neocodeium.renderer") local events = require("neocodeium.events") -local event = events.event local fn = vim.fn local uv = vim.uv @@ -17,6 +15,15 @@ local json = vim.json local nvim_feedkeys = vim.api.nvim_feedkeys local nvim_get_current_buf = vim.api.nvim_get_current_buf local nvim_replace_termcodes = vim.api.nvim_replace_termcodes +local nvim_get_current_line = vim.api.nvim_get_current_line +local nvim_buf_set_extmark = vim.api.nvim_buf_set_extmark +local nvim_get_hl_id_by_name = vim.api.nvim_get_hl_id_by_name +local nvim_create_namespace = vim.api.nvim_create_namespace +local nvim_buf_del_extmark = vim.api.nvim_buf_del_extmark +local nvim_buf_clear_namespace = vim.api.nvim_buf_clear_namespace + +local hlgroup = nvim_get_hl_id_by_name("NeoCodeiumSuggestion") +local ns = nvim_create_namespace("neocodeium_compl") -- Completer ----------------------------------------------- {{{1 @@ -27,8 +34,27 @@ local status = { completed = 2, } +---@class inline +---@field id? integer +---@field text? string +---@field prefix? string + +---@class block +---@field text? string +---@field id? integer + +---@class label +---@field enabled boolean +---@field id? integer + ---@class Completer ---@field pos pos +---@field tick integer +---@field clear_timer uv.uv_timer_t +---@field fulltext string +---@field label label +---@field inline inline[] +---@field block block ---@field data compl.data ---@field status compl.status ---@field debounce_timer uv.uv_timer_t @@ -42,6 +68,12 @@ local Completer = { request_id = 0, allowed_encoding = false, other_docs = {}, + pos = { 0, 0 }, + clear_timer = assert(uv.new_timer()), + fulltext = "", + label = { enabled = false }, + inline = {}, + block = {}, } -- Auxiliary functions ------------------------------------- {{{1 @@ -55,6 +87,105 @@ local function get_editor_opts() } end +---@param id extmark_id +---@param text string +---@param lnum lnum +local function show_label(id, text, lnum) + return nvim_buf_set_extmark(0, ns, lnum, 0, { + id = id, + virt_text = { { text, "NeoCodeiumLabel" } }, + virt_text_win_col = -1 - #text, + }) +end + +---Adds virtual text into the `lnum` line number and `col` column. +---If `id` is nil then a new id will be generated. +---@param id? extmark_id +---@param str string text to display +---@param lnum lnum +---@param col col +---@return extmark_id +local function show_inline(id, str, lnum, col) + return nvim_buf_set_extmark(0, ns, lnum, col, { + id = id, + virt_text_pos = "inline", + virt_text = { { str, hlgroup } }, + undo_restore = false, + strict = false, + }) +end + +---Returns `str` with leading tabs converted to spaces. +---@param str string +---@return string +local function leading_tabs_to_spaces(str) + ---@diagnostic disable-next-line: redundant-return-value + return str:gsub("^\t*", function(m) + -- faster than string.rep + return string.sub( + [[ ]], + 1, + #m * fn.shiftwidth() + ) + end) +end + +---Adds virtual text below the line with `lnum` number. +---If `id` is nil then a new id will be generated. +---@param id? extmark_id +---@param text string text to display, will be split into lines at "\n" +---@param lnum lnum +---@return extmark_id +local function show_block(id, text, lnum) + local block_lines = {} + -- XXX: should it have {trimempty = true}? + for line in vim.gsplit(text, "\n") do + table.insert(block_lines, { { leading_tabs_to_spaces(line), hlgroup } }) + end + + return nvim_buf_set_extmark(0, ns, lnum, 0, { + id = id, + virt_lines = block_lines, + undo_restore = false, + strict = false, + }) +end + +---Deletes virtual text by it's extmark `id` +---@param id extmark_id +---@return boolean true if deleted +local function delete_virttext(id) + return nvim_buf_del_extmark(0, ns, id) +end + +---Returns length of the common prefix of two strings +---@param s1 string +---@param s2 string +---@return integer +local function same_prefix_index(s1, s2) + local len = math.min(#s1, #s2) + for i = 1, len do + if s1:sub(i, i) ~= s2:sub(i, i) then + return i - 1 + end + end + return len +end + +---@param len integer +---@param idx integer +---@param col col +---@return integer +local function calc_inline_delta(len, idx, col) + local result = 0 + if col > len then + result = col - len + elseif col < len then + result = idx >= len and idx - len or col - len + end + return result +end + -- Completer methods --------------------------------------- {{{1 ---Returns `true` if completion data is present and valid. @@ -78,6 +209,315 @@ function Completer:enabled() return self.allowed_encoding and options.status() == 0 end +function Completer:update_label() + if utils.is_insert() and utils.is_empty(self.inline) and not self.block.text then + vim.schedule(function() + self:display_label() + end) + end +end + +---@private +---@param contents inline_content[] +function Completer:display_inline(contents) + -- clear extra inline items + local contents_len = #contents + local leftover_ids = #self.inline - contents_len + if leftover_ids > 0 then + for _ = 1, leftover_ids do + local item = table.remove(self.inline) + delete_virttext(item.id) + end + end + -- change inline virtual text + if contents_len > 0 then + for i, c in ipairs(contents) do + if not self.inline[i] then + self.inline[i] = {} + end + self.inline[i].text = c.text + self.inline[i].prefix = c.prefix + self.inline[i].id = show_inline(self.inline[i].id, c.text, c.lnum, c.col) + end + end +end + +---@private +---@param lnum lnum +---@param text? string +function Completer:display_block(text, lnum) + if text then + if not self.block.id or self.block.text ~= text then + self.block.text = text + self.block.id = show_block(self.block.id, text, lnum) + end + else + self:clear_block() + end +end + +---@private +function Completer:display_label() + if not (options.show_label and self.label.enabled) then + return + end + + local lnum = self.pos[1] + if self.status == status.pending then + self.label.id = show_label(self.label.id, " * ", lnum) + elseif utils.is_empty(self.data.items) then + self.label.id = show_label(self.label.id, " 0 ", lnum) + else + self.label.id = show_label(self.label.id, self.data.index .. "/" .. #self.data.items, lnum) + end +end + +---Displays completion item +function Completer:display() + if not utils.is_insert() then + self:clear_all(true) + return + end + + local lnum, col = unpack(self.pos) + local items = self.data.items or {} + local index = self.data.index or 1 + local item = items[index] or {} + local parts = item.completionParts or {} + + if utils.is_empty(parts) then + return + end + + -- When only block part is present and text was changed compared to when + -- request was sent, return false, so it will dispatch new request + if not self.fulltext:match("^%s*$") and item.completion.text:match("^\n") then + self:request() + return + end + + local block_text ---@type string? + local inline_contents = {} ---@type inline_content[] + local cummulative_cols = 0 + local delta = 0 + + for i, part in ipairs(parts) do + -- process only correct parts + if lnum == (tonumber(part.line) or 0) then + local text = part.text + + if part.type == types.part.inline then + local prefix = part.prefix or "" + local prefix_len = #prefix + local column = prefix_len + cummulative_cols + cummulative_cols = column + + if i == 1 then + local compl_line = prefix .. text + local match_prefix_idx = same_prefix_index(compl_line, self.fulltext) + -- When actual text doesn't match prefix return false, so it will + -- dispatch new request for the completion + if match_prefix_idx ~= col then + self:request() + return + end + + delta = calc_inline_delta(prefix_len, match_prefix_idx, col) + if delta < 0 then + text = prefix:sub(delta) .. text + elseif delta > 0 then + text = text:sub(delta + 1) + end + prefix = "" + end + table.insert( + inline_contents, + { lnum = lnum, col = column + delta, text = text, prefix = prefix } + ) + elseif part.type == types.part.block then + block_text = text + end + end + end + + self.clear_timer:stop() + self:display_inline(inline_contents) + self:display_block(block_text, lnum) + if block_text or #inline_contents > 0 then + self:display_label() + end + events.emit("NeoCodeiumCompletionDisplayed", nil, true) +end + +---@private +function Completer:start_clear_timer() + if not self.clear_timer:is_active() then + self.clear_timer:start( + 350, + 0, + vim.schedule_wrap(function() + self:clear_all() + end) + ) + end +end + +---@private +function Completer:update_forward_line() + if self.block.text and self.block.text ~= "" then + -- find if block.text has multiple lines + local index = self.block.text:find("\n") + self.inline = { { prefix = "" } } + local lnum, col = unpack(self.pos) + if index then + -- starting index `self.pos[2] + 1` is start of the line with indentation + -- prevents shifting of the inline text + self.inline[1].text = self.block.text:sub(col + 1, index - 1) + self.block.text = self.block.text:sub(index + 1) + -- self.block.id already exists, no need to set it + show_block(self.block.id, self.block.text, lnum) + else + self.inline[1].text = self.block.text:sub(col + 1) + self:clear_block() + -- required to update label position + nvim_buf_set_extmark(0, ns, self.pos[1], 0, { + id = self.label.id, + virt_text = { { " 0 ", "NeoCodeiumLabel" } }, + virt_text_win_col = -4, + }) + end + self.inline[1].id = show_inline(nil, self.inline[1].text, lnum, col) + end + self:start_clear_timer() +end + +---@private +function Completer:update_backward_line() + if #self.inline == 1 then + if self.block.text then + self.block.text = self.inline[1].text .. "\n" .. self.block.text + else + self.block.text = self.inline[1].text + end + self:clear_inline() + -- self.block.id could be nil, so we need to set it + self.block.id = show_block(self.block.id, self.block.text, self.pos[1]) + end + self:start_clear_timer() +end + +---@param prev_pos pos +---@param new_fulltext string +function Completer:update_horz_move(prev_pos, new_fulltext) + local lnum, col = unpack(self.pos) + local prev_col = prev_pos[2] + local horz_move = col - prev_col + local first_inline = self.inline[1] + + if horz_move >= 0 then -- added some text + if horz_move > #first_inline.text then + self:clear_inline() + self:start_clear_timer() + else + local prefix = first_inline.text:sub(1, horz_move) + self.inline[1].text = first_inline.text:sub(horz_move + 1) + show_inline(first_inline.id, first_inline.text, lnum, col) + if new_fulltext:sub(prev_col) ~= prefix then + self:start_clear_timer() + end + end + else -- deleted some text + if self.fulltext:match("^%s*$") then + self:clear_inline() + self:start_clear_timer() + else + local prefix = self.fulltext:sub(col + 1, col - horz_move) + self.inline[1].text = prefix .. first_inline.text + show_inline(first_inline.id, first_inline.text, lnum, col) + self.clear_timer:stop() + self:start_clear_timer() + end + end +end + +function Completer:update() + local prev_pos = self.pos + self.pos = utils.get_cursor() + local vert_move = self.pos[1] - prev_pos[1] + + if self.tick == vim.b.changedtick or math.abs(vert_move) > 1 then + self.clear_timer:stop() + self:clear_all() + self.fulltext = nvim_get_current_line() + else + local fulltext = nvim_get_current_line() + if vert_move == 1 then + self.clear_timer:stop() + self:update_forward_line() + elseif vert_move == -1 then + self.clear_timer:stop() + self:update_backward_line() + else -- cursor movement happened on the same line + if not self.inline[1] then + self:clear_inline() + else + self:update_horz_move(prev_pos, fulltext) + end + end + self.fulltext = fulltext + end + + self.tick = vim.b.changedtick +end + +---Clears the block virtual text and removes block.id cache +function Completer:clear_block() + if self.block.id == nil then + return + end + + delete_virttext(self.block.id) + self.block.text = nil + self.block.id = nil +end + +---Clears the inline virtual text and resets `self.inline` to empty table +function Completer:clear_inline() + for _, item in ipairs(self.inline) do + delete_virttext(item.id) + end + self.inline = {} +end + +---Clears the label virtual text and removes label.id cache +function Completer:clear_label() + if self.label.id == nil then + return + end + + delete_virttext(self.label.id) + self.label.id = nil +end + +---Clears plugin's namespace and resets cache +---@param with_reset? boolean +function Completer:clear_all(with_reset) + if with_reset then + self.clear_timer:stop() + nvim_buf_clear_namespace(0, ns, 0, -1) + self.label.id = nil + self.inline = {} + self.block.id = nil + self.block.text = nil + self.fulltext = "" + events.emit("NeoCodeiumCompletionCleared", nil, true) + else + -- self:clear_label() + self:clear_inline() + self:clear_block() + end +end + ---Cycles completions by amount `n`, wraps around if necessary. ---Use negative value to cycle backwards. ---@param n integer amount to cycle @@ -96,7 +536,9 @@ function Completer:cycle(n) self.data.index = items_len end - events.emit(event.display, { items = self.data.items, index = self.data.index }, true) + vim.schedule(function() + self:display() + end) end ---Cycles completions or request to complete if there isn't one. @@ -133,7 +575,9 @@ function Completer:handle_response(r) self.data.items = response.completionItems self.data.index = 1 - events.emit(event.display, { items = self.data.items or {}, index = 1 }, true) + vim.schedule(function() + self:display() + end) end ---Accepts a suggestion till regex match end. @@ -145,19 +589,19 @@ function Completer:accept_regex(regex) end local text = "" - for _, item in ipairs(renderer.inline) do + for _, item in ipairs(self.inline) do text = text .. item.prefix .. item.text end if text ~= "" then text = fn.matchstr(text, regex) - if #renderer.inline > 1 then + if #self.inline > 1 then local text_len = #text - local combined_text = renderer.inline[1].text + local combined_text = self.inline[1].text if text_len > #combined_text then local combined_prefix = "" - for i = 2, #renderer.inline do - local item = renderer.inline[i] + for i = 2, #self.inline do + local item = self.inline[i] combined_text = combined_text .. item.prefix local combined_len = #combined_text if text_len >= combined_len then @@ -178,14 +622,14 @@ function Completer:accept_regex(regex) end end else - text = renderer.block.text or "" + text = self.block.text or "" if text == "" then return end text = vim.split(text, "\n")[1] text = fn.matchstr(text, regex) - local lnum1 = renderer.pos[1] + 1 + local lnum1 = self.pos[1] + 1 utils.set_lines(lnum1, lnum1, { "" }) utils.set_cursor({ lnum1, 0 }) end @@ -214,11 +658,11 @@ function Completer:request() end self.status = status.pending - events.emit(event.status, self.status, true) + self:update_label() self.request_id = self.request_id + 1 local curr_bufnr = nvim_get_current_buf() - local pos = renderer.pos + local pos = self.pos local metadata = server.metadata metadata.request_id = self.request_id local data = { @@ -230,7 +674,7 @@ function Completer:request() server:request("GetCompletions", data, function(r) self.status = status.completed - events.emit(event.status, self.status, true) + self:update_label() self:handle_response(r) end) @@ -258,18 +702,18 @@ function Completer:clear(force) end if force then - events.emit(event.clear, true) + self:clear_all(true) end end ---Initiates a completion. ---@param omit_manual? boolean function Completer:initiate(omit_manual) - events.emit(event.update) + self:update() self:clear() if options.manual and not omit_manual then - events.emit(event.clear) + self:clear_all() return end @@ -318,10 +762,13 @@ function Completer:accept() return end - events.emit(event.accept, curr_item.completion.completionId) + server:request("AcceptCompletion", { + metadata = server.metadata, + completion_id = curr_item.completion.completionId, + }) local pos ---@type pos - local lnum = renderer.pos[1] + 1 + local lnum = self.pos[1] + 1 if block then local block_len = #block local delta = curr_item.suffix and curr_item.suffix.deltaCursorOffset or 0 @@ -340,21 +787,15 @@ function Completer:accept() -- scheduling prevents pasting block before accept_line(), -- because accept_line() using some type of scheduling too with nvim_feedkeys() vim.schedule(function() - events.emit(event.clear, true) + self:clear_all(true) if block then utils.set_lines(lnum, lnum, block) utils.set_cursor(pos) -- required to update label position - renderer.pos = pos + self.pos = pos end end) end - --- Subscribed events --------------------------------------- {{{1 - -events.subscribe(event.request, function() - Completer:request() -end) -- }}}1 return Completer diff --git a/lua/neocodeium/events.lua b/lua/neocodeium/events.lua index 3510712..c9594fb 100644 --- a/lua/neocodeium/events.lua +++ b/lua/neocodeium/events.lua @@ -6,16 +6,6 @@ local nvim_create_augroup = vim.api.nvim_create_augroup local augroup = nvim_create_augroup("neocodeium_events", {}) ----@enum event -events.event = { - display = "_NeoCodeiumDisplay", - clear = "_NeoCodeiumClear", - update = "_NeoCodeiumUpdate", - status = "_NeoCodeiumStatus", - request = "_NeoCodeiumRequest", - accept = "_NeoCodeiumAccept", -} - ---Trigger an event ---@param event string The event pattern ---@param data? any The event data diff --git a/lua/neocodeium/init.lua b/lua/neocodeium/init.lua index 17952c1..5313255 100644 --- a/lua/neocodeium/init.lua +++ b/lua/neocodeium/init.lua @@ -43,10 +43,7 @@ end local function enable_autocmds() local completer = require("neocodeium.completer") - local renderer = require("neocodeium.renderer") local doc = require("neocodeium.doc") - local events = require("neocodeium.events") - local event = events.event local function utf8_or_latin1() local encoding = vim.o.fileencoding @@ -85,20 +82,20 @@ local function enable_autocmds() pattern = "*:i*", once = true, callback = function() - renderer.label.enabled = nu_or_rnu() + completer.label.enabled = nu_or_rnu() end, }) create_autocmd("WinEnter", { callback = function() - renderer.label.enabled = nu_or_rnu() + completer.label.enabled = nu_or_rnu() end, }) create_autocmd("OptionSet", { pattern = "number,relativenumber", callback = function() - renderer.label.enabled = nu_or_rnu() + completer.label.enabled = nu_or_rnu() end, }) @@ -123,9 +120,7 @@ local function enable_autocmds() create_autocmd("InsertEnter", { callback = function() - if completer:enabled() then - events.emit(event.status, completer.status, true) - end + completer:update_label() completer:initiate() end, }) diff --git a/lua/neocodeium/renderer.lua b/lua/neocodeium/renderer.lua deleted file mode 100644 index 7f1654f..0000000 --- a/lua/neocodeium/renderer.lua +++ /dev/null @@ -1,484 +0,0 @@ --- Imports ------------------------------------------------- {{{1 - -local utils = require("neocodeium.utils") -local types = require("neocodeium._types") -local options = require("neocodeium.options").options -local events = require("neocodeium.events") -local event = events.event - -local fn = vim.fn -local uv = vim.uv - -local nvim_get_current_line = vim.api.nvim_get_current_line -local nvim_buf_set_extmark = vim.api.nvim_buf_set_extmark -local nvim_get_hl_id_by_name = vim.api.nvim_get_hl_id_by_name -local nvim_create_namespace = vim.api.nvim_create_namespace -local nvim_buf_del_extmark = vim.api.nvim_buf_del_extmark -local nvim_buf_clear_namespace = vim.api.nvim_buf_clear_namespace - -local hlgroup = nvim_get_hl_id_by_name("NeoCodeiumSuggestion") -local ns = nvim_create_namespace("neocodeium_compl") - --- Renderer ------------------------------------------------- {{{1 - ----@class inline ----@field id? integer ----@field text? string ----@field prefix? string - ----@class block ----@field text? string ----@field id? integer - ----@class label ----@field enabled boolean ----@field id? integer - ----@class Renderer ----@field pos pos ----@field tick integer ----@field timer uv.uv_timer_t ----@field fulltext string ----@field label label ----@field inline inline[] ----@field block block -local Renderer = { - pos = { 0, 0 }, - timer = assert(uv.new_timer()), - fulltext = "", - label = { enabled = false }, - inline = {}, - block = {}, -} - --- Auxiliary functions ------------------------------------- {{{1 - ----Adds virtual text into the `lnum` line number and `col` column. ----If `id` is nil then a new id will be generated. ----@param id? extmark_id ----@param str string text to display ----@param lnum lnum ----@param col col ----@return extmark_id -local function show_inline(id, str, lnum, col) - return nvim_buf_set_extmark(0, ns, lnum, col, { - id = id, - virt_text_pos = "inline", - virt_text = { { str, hlgroup } }, - undo_restore = false, - strict = false, - }) -end - ----Returns `str` with leading tabs converted to spaces. ----@param str string ----@return string -local function leading_tabs_to_spaces(str) - ---@diagnostic disable-next-line: redundant-return-value - return str:gsub("^\t*", function(m) - -- faster than string.rep - return string.sub( - [[ ]], - 1, - #m * fn.shiftwidth() - ) - end) -end - ----Adds virtual text below the line with `lnum` number. ----If `id` is nil then a new id will be generated. ----@param id? extmark_id ----@param text string text to display, will be split into lines at "\n" ----@param lnum lnum ----@return extmark_id -local function show_block(id, text, lnum) - local block_lines = {} - -- XXX: should it have {trimempty = true}? - for line in vim.gsplit(text, "\n") do - table.insert(block_lines, { { leading_tabs_to_spaces(line), hlgroup } }) - end - - return nvim_buf_set_extmark(0, ns, lnum, 0, { - id = id, - virt_lines = block_lines, - undo_restore = false, - strict = false, - }) -end - ----Deletes virtual text by it's extmark `id` ----@param id extmark_id ----@return boolean true if deleted -local function delete_virttext(id) - return nvim_buf_del_extmark(0, ns, id) -end - ----Returns length of the common prefix of two strings ----@param s1 string ----@param s2 string ----@return integer -local function same_prefix_index(s1, s2) - local len = math.min(#s1, #s2) - for i = 1, len do - if s1:sub(i, i) ~= s2:sub(i, i) then - return i - 1 - end - end - return len -end - ----@param len integer ----@param idx integer ----@param col col ----@return integer -local function calc_inline_delta(len, idx, col) - local result = 0 - if col > len then - result = col - len - elseif col < len then - result = idx >= len and idx - len or col - len - end - return result -end - --- Renderer methods ---------------------------------------- {{{1 - --- TODO: show pending status ----@param id extmark_id ----@param text string -function Renderer:show_label(id, text) - self.label.id = nvim_buf_set_extmark(0, ns, self.pos[1], 0, { - id = id, - virt_text = { { text, "NeoCodeiumLabel" } }, - virt_text_win_col = -1 - #text, - }) -end - ----@private ----@param contents inline_content[] -function Renderer:display_inline(contents) - -- clear extra inline items - local contents_len = #contents - local leftover_ids = #self.inline - contents_len - if leftover_ids > 0 then - for _ = 1, leftover_ids do - local item = table.remove(self.inline) - delete_virttext(item.id) - end - end - -- change inline virtual text - if contents_len > 0 then - for i, c in ipairs(contents) do - if not self.inline[i] then - self.inline[i] = {} - end - self.inline[i].text = c.text - self.inline[i].prefix = c.prefix - self.inline[i].id = show_inline(self.inline[i].id, c.text, c.lnum, c.col) - end - end -end - ----@private ----@param lnum lnum ----@param text? string -function Renderer:display_block(text, lnum) - if text then - if not self.block.id or self.block.text ~= text then - self.block.text = text - self.block.id = show_block(self.block.id, text, lnum) - end - else - self:clear_block() - end -end - ----@private ----@param items compl.item[] ----@param index integer ----@param pending? boolean -function Renderer:display_label(items, index, pending) - if not (options.show_label and self.label.enabled) then - return - end - - if pending then - self:show_label(self.label.id, " * ") - elseif utils.is_empty(items) then - self:show_label(self.label.id, " 0 ") - else - self:show_label(self.label.id, index .. "/" .. #items) - end -end - ----Displays completion item ----@param items compl.item[] ----@param index integer -function Renderer:display(items, index) - if not utils.is_insert() then - self:clear(true) - return - end - - local lnum, col = unpack(self.pos) - local item = items[index] or {} - local parts = item.completionParts or {} - - if utils.is_empty(parts) then - return - end - - -- When only block part is present and text was changed compared to when - -- request was sent, return false, so it will dispatch new request - if not self.fulltext:match("^%s*$") and item.completion.text:match("^\n") then - events.emit(event.request) - return - end - - local block_text ---@type string? - local inline_contents = {} ---@type inline_content[] - local cummulative_cols = 0 - local delta = 0 - - for i, part in ipairs(parts) do - -- process only correct parts - if lnum == (tonumber(part.line) or 0) then - local text = part.text - - if part.type == types.part.inline then - local prefix = part.prefix or "" - local prefix_len = #prefix - local column = prefix_len + cummulative_cols - cummulative_cols = column - - if i == 1 then - local compl_line = prefix .. text - local match_prefix_idx = same_prefix_index(compl_line, self.fulltext) - -- When actual text doesn't match prefix return false, so it will - -- dispatch new request for the completion - if match_prefix_idx ~= col then - events.emit(event.request) - return - end - - delta = calc_inline_delta(prefix_len, match_prefix_idx, col) - if delta < 0 then - text = prefix:sub(delta) .. text - elseif delta > 0 then - text = text:sub(delta + 1) - end - prefix = "" - end - table.insert( - inline_contents, - { lnum = lnum, col = column + delta, text = text, prefix = prefix } - ) - elseif part.type == types.part.block then - block_text = text - end - end - end - - self.timer:stop() - self:display_inline(inline_contents) - self:display_block(block_text, lnum) - if block_text or #inline_contents > 0 then - self:display_label(items, index) - end - events.emit("NeoCodeiumCompletionDisplayed", nil, true) -end - ----@private -function Renderer:start_clear_timer() - if not self.timer:is_active() then - self.timer:start( - 350, - 0, - vim.schedule_wrap(function() - self:clear() - end) - ) - end -end - ----@private -function Renderer:update_forward_line() - if self.block.text and self.block.text ~= "" then - -- find if block.text has multiple lines - local index = self.block.text:find("\n") - self.inline = { { prefix = "" } } - local lnum, col = unpack(self.pos) - if index then - -- starting index `self.pos[2] + 1` is start of the line with indentation - -- prevents shifting of the inline text - self.inline[1].text = self.block.text:sub(col + 1, index - 1) - self.block.text = self.block.text:sub(index + 1) - -- self.block.id already exists, no need to set it - show_block(self.block.id, self.block.text, lnum) - else - self.inline[1].text = self.block.text:sub(col + 1) - self:clear_block() - -- required to update label position - nvim_buf_set_extmark(0, ns, self.pos[1], 0, { - id = self.label.id, - virt_text = { { " 0 ", "NeoCodeiumLabel" } }, - virt_text_win_col = -4, - }) - end - self.inline[1].id = show_inline(nil, self.inline[1].text, lnum, col) - end - self:start_clear_timer() -end - ----@private -function Renderer:update_backward_line() - if #self.inline == 1 then - if self.block.text then - self.block.text = self.inline[1].text .. "\n" .. self.block.text - else - self.block.text = self.inline[1].text - end - self:clear_inline() - -- self.block.id could be nil, so we need to set it - self.block.id = show_block(self.block.id, self.block.text, self.pos[1]) - end - self:start_clear_timer() -end - ----@param prev_pos pos ----@param new_fulltext string -function Renderer:update_horz_move(prev_pos, new_fulltext) - local lnum, col = unpack(self.pos) - local prev_col = prev_pos[2] - local horz_move = col - prev_col - local first_inline = self.inline[1] - - if horz_move >= 0 then -- added some text - if horz_move > #first_inline.text then - self:clear_inline() - self:start_clear_timer() - else - local prefix = first_inline.text:sub(1, horz_move) - self.inline[1].text = first_inline.text:sub(horz_move + 1) - show_inline(first_inline.id, first_inline.text, lnum, col) - if new_fulltext:sub(prev_col) ~= prefix then - self:start_clear_timer() - end - end - else -- deleted some text - if self.fulltext:match("^%s*$") then - self:clear_inline() - self:start_clear_timer() - else - local prefix = self.fulltext:sub(col + 1, col - horz_move) - self.inline[1].text = prefix .. first_inline.text - show_inline(first_inline.id, first_inline.text, lnum, col) - self.timer:stop() - self:start_clear_timer() - end - end -end - -function Renderer:update() - local prev_pos = self.pos - self.pos = utils.get_cursor() - local vert_move = self.pos[1] - prev_pos[1] - - if self.tick == vim.b.changedtick or math.abs(vert_move) > 1 then - self.timer:stop() - self:clear() - self.fulltext = nvim_get_current_line() - else - local fulltext = nvim_get_current_line() - if vert_move == 1 then - self.timer:stop() - self:update_forward_line() - elseif vert_move == -1 then - self.timer:stop() - self:update_backward_line() - else -- cursor movement happened on the same line - if not self.inline[1] then - self:clear_inline() - else - self:update_horz_move(prev_pos, fulltext) - end - end - self.fulltext = fulltext - end - - self.tick = vim.b.changedtick -end - ----Clears the block virtual text and removes block.id cache -function Renderer:clear_block() - if self.block.id == nil then - return - end - - delete_virttext(self.block.id) - self.block.text = nil - self.block.id = nil -end - ----Clears the inline virtual text and resets `self.inline` to empty table -function Renderer:clear_inline() - for _, item in ipairs(self.inline) do - delete_virttext(item.id) - end - self.inline = {} -end - ----Clears the label virtual text and removes label.id cache -function Renderer:clear_label() - if self.label.id == nil then - return - end - - delete_virttext(self.label.id) - self.label.id = nil -end - ----Clears plugin's namespace and resets cache ----@param with_reset? boolean -function Renderer:clear(with_reset) - if with_reset then - self.timer:stop() - nvim_buf_clear_namespace(0, ns, 0, -1) - self.label.id = nil - self.inline = {} - self.block.id = nil - self.block.text = nil - self.fulltext = "" - events.emit("NeoCodeiumCompletionCleared", nil, true) - else - -- self:clear_label() - self:clear_inline() - self:clear_block() - end -end - --- Subscribed events --------------------------------------- {{{1 - -events.subscribe(event.display, function(data) - Renderer:display(data.items, data.index) -end) - -events.subscribe(event.clear, function(data) - Renderer:clear(data) -end) - -events.subscribe(event.update, function(_) - Renderer:update() -end) - -events.subscribe(event.status, function(data) - if utils.is_insert() and utils.is_empty(Renderer.inline) and not Renderer.block.text then - local pending = data == 1 - Renderer:display_label({}, 1, pending) - end -end) --- }}}1 - -return Renderer - --- vim: fdm=marker diff --git a/lua/neocodeium/server.lua b/lua/neocodeium/server.lua index df57a50..89d5cb4 100644 --- a/lua/neocodeium/server.lua +++ b/lua/neocodeium/server.lua @@ -6,7 +6,6 @@ local options = require("neocodeium.options").options local stdio = require("neocodeium.utils.stdio") local echo = require("neocodeium.utils.echo") local events = require("neocodeium.events") -local event = events.event local Bin = require("neocodeium.binary") local fn = vim.fn @@ -271,15 +270,6 @@ function Server:init(timer, manager_dir) end) end end - --- Subscribed events --------------------------------------- {{{1 -events.subscribe(event.accept, function(data) - Server:request("AcceptCompletion", { - metadata = Server.metadata, - completion_id = data, - }) -end) - -- }}}1 return Server