From a74101ab53e3988a339a4825e61bb97861cfbcd1 Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Fri, 30 May 2025 16:39:11 -0400 Subject: [PATCH 1/7] feat: adding suggestion_history for last NES --- lua/copilot-lsp/config.lua | 2 ++ lua/copilot-lsp/nes/init.lua | 20 +++++++++++++ lua/copilot-lsp/nes/ui.lua | 57 ++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+) diff --git a/lua/copilot-lsp/config.lua b/lua/copilot-lsp/config.lua index b4320a1..32cb44a 100644 --- a/lua/copilot-lsp/config.lua +++ b/lua/copilot-lsp/config.lua @@ -4,6 +4,7 @@ ---@field clear_on_large_distance boolean Whether to clear suggestion when cursor is far away ---@field count_horizontal_moves boolean Whether to count horizontal cursor movements ---@field reset_on_approaching boolean Whether to reset counter when approaching suggestion +---@field enable_history boolean Whether to enable suggestion history for restoration local M = {} @@ -16,6 +17,7 @@ M.defaults = { clear_on_large_distance = true, count_horizontal_moves = true, reset_on_approaching = true, + enable_history = true, }, } diff --git a/lua/copilot-lsp/nes/init.lua b/lua/copilot-lsp/nes/init.lua index 38475cb..f1a63a4 100644 --- a/lua/copilot-lsp/nes/init.lua +++ b/lua/copilot-lsp/nes/init.lua @@ -135,4 +135,24 @@ function M.clear() return false end +--- Check if there's a suggestion in history that can be restored +---@param bufnr? integer +---@return boolean +function M.has_history(bufnr) + bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() + return vim.b[bufnr].copilotlsp_nes_history ~= nil +end + +--- Restore the last suggestion from history +---@param bufnr? integer +---@return boolean -- true if suggestion was restored, false otherwise +function M.restore_suggestion(bufnr) + if not M.has_history(bufnr) then + return false + end + -- Set flag to indicate this is a restoration, not a new suggestion + vim.b[bufnr].copilotlsp_nes_restoring = true + return nes_ui.restore_suggestion(bufnr, nes_ns) +end + return M diff --git a/lua/copilot-lsp/nes/ui.lua b/lua/copilot-lsp/nes/ui.lua index 87b9a21..39842c9 100644 --- a/lua/copilot-lsp/nes/ui.lua +++ b/lua/copilot-lsp/nes/ui.lua @@ -7,6 +7,22 @@ local function _dismiss_suggestion(bufnr, ns_id) pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_id, 0, -1) end +---@private +---@param bufnr integer +---@param state copilotlsp.InlineEdit +local function _store_suggestion_history(bufnr, state) + if not config.nes.enable_history then + return + end + vim.b[bufnr].copilotlsp_nes_history = vim.deepcopy(state) +end + +---@private +---@param bufnr integer +local function _clear_suggestion_history(bufnr) + vim.b[bufnr].copilotlsp_nes_history = nil +end + ---@param bufnr? integer ---@param ns_id integer function M.clear_suggestion(bufnr, ns_id) @@ -22,6 +38,10 @@ function M.clear_suggestion(bufnr, ns_id) _dismiss_suggestion(bufnr, ns_id) ---@type copilotlsp.InlineEdit local state = vim.b[bufnr].nes_state + if state then + _store_suggestion_history(bufnr, state) + end + _dismiss_suggestion(bufnr, ns_id) if not state then return end @@ -33,6 +53,38 @@ function M.clear_suggestion(bufnr, ns_id) vim.b[bufnr].copilotlsp_nes_last_col = nil end +---@param bufnr? integer +---@param ns_id integer +---@return boolean -- true if suggestion was restored, false otherwise +function M.restore_suggestion(bufnr, ns_id) + bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() + if not vim.api.nvim_buf_is_valid(bufnr) then + return false + end + -- Don't restore if there's already an active suggestion + if vim.b[bufnr].nes_state then + return false + end + -- Don't restore if history is disabled + if not config.nes.enable_history then + return false + end + ---@type copilotlsp.InlineEdit + local history = vim.b[bufnr].copilotlsp_nes_history + if not history then + return false + end + -- Validate that the history suggestion is still applicable + local start_line = history.range.start.line + if start_line >= vim.api.nvim_buf_line_count(bufnr) then + _clear_suggestion_history(bufnr) + return false + end + -- Restore the suggestion + M._display_next_suggestion(bufnr, ns_id, { history }) + return true +end + ---@private ---@param bufnr integer ---@param edit lsp.TextEdit @@ -170,6 +222,11 @@ function M._display_next_suggestion(bufnr, ns_id, edits) if not edits or #edits == 0 then return end + -- Clear history when new suggestion arrives (not a restoration) + if config.nes.enable_history and not vim.b[bufnr].copilotlsp_nes_restoring then + _clear_suggestion_history(bufnr) + end + vim.b[bufnr].copilotlsp_nes_restoring = nil local suggestion = edits[1] local preview = M._calculate_preview(bufnr, suggestion) From 54bf4972d9dd0d38793c03414d40870583c00066 Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 11:13:11 -0400 Subject: [PATCH 2/7] feat: Adding history cycle for the last 2 suggestions --- lua/copilot-lsp/nes/init.lua | 4 ++- lua/copilot-lsp/nes/ui.lua | 60 +++++++++++++++++++----------------- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/lua/copilot-lsp/nes/init.lua b/lua/copilot-lsp/nes/init.lua index f1a63a4..d861766 100644 --- a/lua/copilot-lsp/nes/init.lua +++ b/lua/copilot-lsp/nes/init.lua @@ -140,13 +140,15 @@ end ---@return boolean function M.has_history(bufnr) bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() - return vim.b[bufnr].copilotlsp_nes_history ~= nil + local history = vim.b[bufnr].copilotlsp_nes_history + return history ~= nil and #history > 0 end --- Restore the last suggestion from history ---@param bufnr? integer ---@return boolean -- true if suggestion was restored, false otherwise function M.restore_suggestion(bufnr) + bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() if not M.has_history(bufnr) then return false end diff --git a/lua/copilot-lsp/nes/ui.lua b/lua/copilot-lsp/nes/ui.lua index 39842c9..b83a5e8 100644 --- a/lua/copilot-lsp/nes/ui.lua +++ b/lua/copilot-lsp/nes/ui.lua @@ -14,7 +14,12 @@ local function _store_suggestion_history(bufnr, state) if not config.nes.enable_history then return end - vim.b[bufnr].copilotlsp_nes_history = vim.deepcopy(state) + local history = vim.b[bufnr].copilotlsp_nes_history or {} + table.insert(history, 1, vim.deepcopy(state)) + if #history > 2 then + table.remove(history, 3) + end + vim.b[bufnr].copilotlsp_nes_history = history end ---@private @@ -27,7 +32,6 @@ end ---@param ns_id integer function M.clear_suggestion(bufnr, ns_id) bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() - -- Validate buffer exists before accessing buffer-scoped variables if not vim.api.nvim_buf_is_valid(bufnr) then return end @@ -37,16 +41,6 @@ function M.clear_suggestion(bufnr, ns_id) end _dismiss_suggestion(bufnr, ns_id) ---@type copilotlsp.InlineEdit - local state = vim.b[bufnr].nes_state - if state then - _store_suggestion_history(bufnr, state) - end - _dismiss_suggestion(bufnr, ns_id) - if not state then - return - end - - -- Clear buffer variables vim.b[bufnr].nes_state = nil vim.b[bufnr].copilotlsp_nes_cursor_moves = nil vim.b[bufnr].copilotlsp_nes_last_line = nil @@ -61,27 +55,35 @@ function M.restore_suggestion(bufnr, ns_id) if not vim.api.nvim_buf_is_valid(bufnr) then return false end - -- Don't restore if there's already an active suggestion - if vim.b[bufnr].nes_state then - return false - end - -- Don't restore if history is disabled if not config.nes.enable_history then return false end - ---@type copilotlsp.InlineEdit local history = vim.b[bufnr].copilotlsp_nes_history - if not history then + if not history or #history == 0 then return false end - -- Validate that the history suggestion is still applicable - local start_line = history.range.start.line + local restore_index = vim.b[bufnr].copilotlsp_nes_restore_index or 0 + restore_index = restore_index + 1 + -- If we've cycled through all history, wrap around + if restore_index > #history then + restore_index = 1 + end + local suggestion = history[restore_index] + vim.b[bufnr].copilotlsp_nes_restore_index = restore_index + local start_line = suggestion.range.start.line if start_line >= vim.api.nvim_buf_line_count(bufnr) then _clear_suggestion_history(bufnr) return false end - -- Restore the suggestion - M._display_next_suggestion(bufnr, ns_id, { history }) + -- Clear current display and show restored suggestion + _dismiss_suggestion(bufnr, ns_id) + local preview = M._calculate_preview(bufnr, suggestion) + M._display_preview(bufnr, ns_id, preview) + + vim.b[bufnr].nes_state = suggestion + vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id + vim.b[bufnr].copilotlsp_nes_cursor_moves = 1 + return true end @@ -218,15 +220,11 @@ end ---@param ns_id integer ---@param edits copilotlsp.InlineEdit[] function M._display_next_suggestion(bufnr, ns_id, edits) - M.clear_suggestion(bufnr, ns_id) if not edits or #edits == 0 then return end - -- Clear history when new suggestion arrives (not a restoration) - if config.nes.enable_history and not vim.b[bufnr].copilotlsp_nes_restoring then - _clear_suggestion_history(bufnr) - end - vim.b[bufnr].copilotlsp_nes_restoring = nil + -- Clear current suggestion first + M.clear_suggestion(bufnr, ns_id) local suggestion = edits[1] local preview = M._calculate_preview(bufnr, suggestion) @@ -236,6 +234,10 @@ function M._display_next_suggestion(bufnr, ns_id, edits) vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id vim.b[bufnr].copilotlsp_nes_cursor_moves = 1 + -- Store this suggestion in history immediately after displaying it + _store_suggestion_history(bufnr, suggestion) + vim.b[bufnr].copilotlsp_nes_restore_index = 0 + vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, { buffer = bufnr, callback = function() From 5c07cde377d4a60c10f64f48fdc51109d2b8773b Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 11:16:21 -0400 Subject: [PATCH 3/7] tests: adding tests for the history feature --- tests/nes/test_ui_preview.lua | 282 ++++++++++++++++++++++++++++++++++ 1 file changed, 282 insertions(+) diff --git a/tests/nes/test_ui_preview.lua b/tests/nes/test_ui_preview.lua index 2a1f836..d0f0c4d 100644 --- a/tests/nes/test_ui_preview.lua +++ b/tests/nes/test_ui_preview.lua @@ -351,4 +351,286 @@ T["ui_preview"]["suggestion_preserves_on_movement_towards"] = function() ref(child.get_screenshot()) end +T["ui_preview"]["suggestion_history_basic_cycle"] = function() + set_content("line1\nline2\nline3") + + -- Create first suggestion + local edit1 = { + range = { + start = { line = 1, character = 0 }, + ["end"] = { line = 1, character = 0 }, + }, + newText = "-- first suggestion", + } + + -- Display first suggestion + child.g.test_edit = edit1 + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local edits = { vim.g.test_edit } + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + vim.uv.sleep(300) + end) + + -- Create and display second suggestion (should store first in history) + local edit2 = { + range = { + start = { line = 2, character = 0 }, + ["end"] = { line = 2, character = 0 }, + }, + newText = "-- second suggestion", + } + + child.g.test_edit = edit2 + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local edits = { vim.g.test_edit } + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + vim.uv.sleep(300) + end) + + -- Clear current suggestion (second should now be in history too) + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + vim.uv.sleep(300) + end) + + -- First restore should show second suggestion (most recent) + local restored1 = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local result = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + vim.uv.sleep(300) + return result + end) + eq(restored1, true) + + -- Verify we can check history content + local history_size = child.lua_func(function() + return #(vim.b[0].copilotlsp_nes_history or {}) + end) + eq(history_size, 2) + + -- Second restore should show first suggestion + local restored2 = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local restored = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + vim.uv.sleep(300) + return restored + end) + eq(restored2, true) + + -- Third restore should cycle back to second suggestion + local restored3 = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local restored = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + vim.uv.sleep(300) + return restored + end) + eq(restored3, true) +end + +T["ui_preview"]["suggestion_history_max_two_items"] = function() + set_content("line1\nline2\nline3\nline4") + + -- Create and display three suggestions + local suggestions = { + { newText = "-- first", line = 0 }, + { newText = "-- second", line = 1 }, + { newText = "-- third", line = 2 }, + } + + for _, suggestion in ipairs(suggestions) do + local edit = { + range = { + start = { line = suggestion.line, character = 0 }, + ["end"] = { line = suggestion.line, character = 0 }, + }, + newText = suggestion.newText, + } + + child.g.test_edit = edit + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local edits = { vim.g.test_edit } + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + vim.uv.sleep(300) + end) + end + + -- Clear current suggestion + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + vim.uv.sleep(300) + end) + + -- Verify history only keeps 2 most recent + local history_size = child.lua_func(function() + return #(vim.b[0].copilotlsp_nes_history or {}) + end) + eq(history_size, 2) + + -- Verify we can only cycle between 2 suggestions + local restore_results = {} + for _ = 1, 4 do -- Try 4 restores to test cycling + local restored = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + return require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + end) + table.insert(restore_results, restored) + vim.uv.sleep(300) + end + + -- All restores should succeed + for _, result in ipairs(restore_results) do + eq(result, true) + end +end + +T["ui_preview"]["suggestion_history_invalid_after_text_changes"] = function() + set_content("line1\nline2\nline3\nline4\nline5") + + -- Create suggestion on line 4 (0-indexed) + local edit = { + range = { + start = { line = 4, character = 0 }, + ["end"] = { line = 4, character = 0 }, + }, + newText = "-- comment on line 5", + } + + child.g.test_edit = edit + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local edits = { vim.g.test_edit } + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + vim.uv.sleep(300) + end) + + -- Clear suggestion to store in history + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + vim.uv.sleep(300) + end) + + -- Verify history exists + local history_size_before = child.lua_func(function() + return #(vim.b[0].copilotlsp_nes_history or {}) + end) + eq(history_size_before, 1) + + -- Delete lines to make history invalid (keep only first 3 lines) + child.api.nvim_buf_set_lines(0, 3, -1, false, {}) + + -- Try to restore (should fail and clear history) + local restored = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + local result = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + vim.uv.sleep(300) + return result + end) + eq(restored, false) + + -- Verify history was cleared + local history_size_after = child.lua_func(function() + return #(vim.b[0].copilotlsp_nes_history or {}) + end) + eq(history_size_after, 0) +end + +T["ui_preview"]["suggestion_history_restore_index_reset"] = function() + set_content("line1\nline2\nline3") + -- Create and display two suggestions to build history + local edit1 = { + range = { + start = { line = 1, character = 0 }, + ["end"] = { line = 1, character = 0 }, + }, + newText = "-- first", + } + local edit2 = { + range = { + start = { line = 2, character = 0 }, + ["end"] = { line = 2, character = 0 }, + }, + newText = "-- second", + } + + -- Display first, then second (first goes to history) + child.g.test_edit = edit1 + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) + vim.uv.sleep(300) + end) + + child.g.test_edit = edit2 + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) + vim.uv.sleep(300) + end) + + -- Clear to add second to history + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + vim.uv.sleep(300) + end) + + -- Restore once (should show second, index becomes 1) + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + vim.uv.sleep(300) + end) + + -- Get restore index + local index_before = child.lua_func(function() + return vim.b[0].copilotlsp_nes_restore_index or 0 + end) + eq(index_before, 1) + + -- Display new suggestion (should reset index) + local edit3 = { + range = { + start = { line = 0, character = 0 }, + ["end"] = { line = 0, character = 0 }, + }, + newText = "-- third", + } + + child.g.test_edit = edit3 + child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) + vim.uv.sleep(300) + end) + + -- Verify index was reset + local index_after = child.lua_func(function() + return vim.b[0].copilotlsp_nes_restore_index or 0 + end) + eq(index_after, 0) +end + +T["ui_preview"]["suggestion_history_no_restore_when_empty"] = function() + set_content("line1\nline2\nline3") + + -- Try to restore when no history exists + local restored = child.lua_func(function() + local ns_id = vim.api.nvim_create_namespace("nes_test") + return require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + end) + eq(restored, false) + + -- Verify no history exists + local history_size = child.lua_func(function() + return #(vim.b[0].copilotlsp_nes_history or {}) + end) + eq(history_size, 0) +end + return T From 8955136cabf2ab3c4b0b87f806a730e7dfa00517 Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 11:24:09 -0400 Subject: [PATCH 4/7] chore: remove the option to disable history --- lua/copilot-lsp/config.lua | 2 -- lua/copilot-lsp/nes/ui.lua | 6 ------ 2 files changed, 8 deletions(-) diff --git a/lua/copilot-lsp/config.lua b/lua/copilot-lsp/config.lua index 32cb44a..b4320a1 100644 --- a/lua/copilot-lsp/config.lua +++ b/lua/copilot-lsp/config.lua @@ -4,7 +4,6 @@ ---@field clear_on_large_distance boolean Whether to clear suggestion when cursor is far away ---@field count_horizontal_moves boolean Whether to count horizontal cursor movements ---@field reset_on_approaching boolean Whether to reset counter when approaching suggestion ----@field enable_history boolean Whether to enable suggestion history for restoration local M = {} @@ -17,7 +16,6 @@ M.defaults = { clear_on_large_distance = true, count_horizontal_moves = true, reset_on_approaching = true, - enable_history = true, }, } diff --git a/lua/copilot-lsp/nes/ui.lua b/lua/copilot-lsp/nes/ui.lua index b83a5e8..3883c41 100644 --- a/lua/copilot-lsp/nes/ui.lua +++ b/lua/copilot-lsp/nes/ui.lua @@ -11,9 +11,6 @@ end ---@param bufnr integer ---@param state copilotlsp.InlineEdit local function _store_suggestion_history(bufnr, state) - if not config.nes.enable_history then - return - end local history = vim.b[bufnr].copilotlsp_nes_history or {} table.insert(history, 1, vim.deepcopy(state)) if #history > 2 then @@ -55,9 +52,6 @@ function M.restore_suggestion(bufnr, ns_id) if not vim.api.nvim_buf_is_valid(bufnr) then return false end - if not config.nes.enable_history then - return false - end local history = vim.b[bufnr].copilotlsp_nes_history if not history or #history == 0 then return false From 21f00dfdc0ad8c96f64c4896b1ae2cbbb440b20d Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 11:32:48 -0400 Subject: [PATCH 5/7] docs: update the readme with the history feature --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index b14de1a..eb345db 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,20 @@ vim.keymap.set("n", "", function() end, { desc = "Clear Copilot suggestion or fallback" }) ``` + +#### Restoring previous suggestions + +The history automatically stores the last 2 suggestions. Each time you call restore, it cycles to the next previous suggestion. When you reach the end, it wraps back to the most recent one. + +You can restore and cycle through the last 2 suggestions: +```lua +-- Restore previous suggestions (cycles through last 2) +vim.keymap.set("n", "cr", function() + require('copilot-lsp.nes').restore_suggestion() +end, { desc = "Restore previous Copilot suggestion" }) +``` + + ## Default Configuration From 711b37aded36d95a8a57ba69614d151e48bfcbbd Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 16:16:58 -0400 Subject: [PATCH 6/7] feat: Replacing manual handling with vim.ringbuf for history --- lua/copilot-lsp/nes/init.lua | 11 +------- lua/copilot-lsp/nes/ui.lua | 52 +++++++++++++++++++++++------------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/lua/copilot-lsp/nes/init.lua b/lua/copilot-lsp/nes/init.lua index d861766..f95606f 100644 --- a/lua/copilot-lsp/nes/init.lua +++ b/lua/copilot-lsp/nes/init.lua @@ -135,21 +135,12 @@ function M.clear() return false end ---- Check if there's a suggestion in history that can be restored ----@param bufnr? integer ----@return boolean -function M.has_history(bufnr) - bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() - local history = vim.b[bufnr].copilotlsp_nes_history - return history ~= nil and #history > 0 -end - --- Restore the last suggestion from history ---@param bufnr? integer ---@return boolean -- true if suggestion was restored, false otherwise function M.restore_suggestion(bufnr) bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf() - if not M.has_history(bufnr) then + if not nes_ui.has_history(bufnr) then return false end -- Set flag to indicate this is a restoration, not a new suggestion diff --git a/lua/copilot-lsp/nes/ui.lua b/lua/copilot-lsp/nes/ui.lua index 3883c41..2feb770 100644 --- a/lua/copilot-lsp/nes/ui.lua +++ b/lua/copilot-lsp/nes/ui.lua @@ -1,6 +1,8 @@ local M = {} local config = require("copilot-lsp.config").config +local buffer_histories = {} + ---@param bufnr integer ---@param ns_id integer local function _dismiss_suggestion(bufnr, ns_id) @@ -11,18 +13,17 @@ end ---@param bufnr integer ---@param state copilotlsp.InlineEdit local function _store_suggestion_history(bufnr, state) - local history = vim.b[bufnr].copilotlsp_nes_history or {} - table.insert(history, 1, vim.deepcopy(state)) - if #history > 2 then - table.remove(history, 3) + if not buffer_histories[bufnr] then + buffer_histories[bufnr] = vim.ringbuf(2) end - vim.b[bufnr].copilotlsp_nes_history = history + buffer_histories[bufnr]:push(vim.deepcopy(state)) end ---@private ---@param bufnr integer local function _clear_suggestion_history(bufnr) - vim.b[bufnr].copilotlsp_nes_history = nil + buffer_histories[bufnr] = nil + vim.b[bufnr].copilotlsp_nes_restore_index = nil end ---@param bufnr? integer @@ -44,6 +45,19 @@ function M.clear_suggestion(bufnr, ns_id) vim.b[bufnr].copilotlsp_nes_last_col = nil end +--- Check if there's history for a buffer +---@param bufnr integer +---@return boolean +function M.has_history(bufnr) + local history = buffer_histories[bufnr] + if not history then + return false + end + -- Check if ringbuf has any items + local item = history:peek() + return item ~= nil +end + ---@param bufnr? integer ---@param ns_id integer ---@return boolean -- true if suggestion was restored, false otherwise @@ -52,32 +66,27 @@ function M.restore_suggestion(bufnr, ns_id) if not vim.api.nvim_buf_is_valid(bufnr) then return false end - local history = vim.b[bufnr].copilotlsp_nes_history - if not history or #history == 0 then + local history = buffer_histories[bufnr] + if not history then return false end - local restore_index = vim.b[bufnr].copilotlsp_nes_restore_index or 0 - restore_index = restore_index + 1 - -- If we've cycled through all history, wrap around - if restore_index > #history then - restore_index = 1 + local suggestion = history:pop() + if not suggestion then + return false end - local suggestion = history[restore_index] - vim.b[bufnr].copilotlsp_nes_restore_index = restore_index + -- Validate suggestion is still applicable local start_line = suggestion.range.start.line if start_line >= vim.api.nvim_buf_line_count(bufnr) then _clear_suggestion_history(bufnr) return false end - -- Clear current display and show restored suggestion _dismiss_suggestion(bufnr, ns_id) local preview = M._calculate_preview(bufnr, suggestion) M._display_preview(bufnr, ns_id, preview) - vim.b[bufnr].nes_state = suggestion vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id vim.b[bufnr].copilotlsp_nes_cursor_moves = 1 - + history:push(suggestion) return true end @@ -329,4 +338,11 @@ function M._display_next_suggestion(bufnr, ns_id, edits) }) end +-- Clean up history when buffer is deleted +vim.api.nvim_create_autocmd("BufDelete", { + callback = function(ev) + buffer_histories[ev.buf] = nil + end, +}) + return M From 89cdac830376da1466f172eedd68a081d1604ed4 Mon Sep 17 00:00:00 2001 From: Bassam Data Date: Mon, 2 Jun 2025 17:00:04 -0400 Subject: [PATCH 7/7] tests: updated tests for the history feature --- tests/nes/test_ui_preview.lua | 252 +++++++++------------------------- 1 file changed, 66 insertions(+), 186 deletions(-) diff --git a/tests/nes/test_ui_preview.lua b/tests/nes/test_ui_preview.lua index d0f0c4d..b3a0ef5 100644 --- a/tests/nes/test_ui_preview.lua +++ b/tests/nes/test_ui_preview.lua @@ -312,120 +312,72 @@ T["ui_preview"]["cursor_aware_suggestion_clearing"] = function() ref(child.get_screenshot()) end -T["ui_preview"]["suggestion_preserves_on_movement_towards"] = function() - set_content("line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8") - ref(child.get_screenshot()) - - -- Position cursor at line 8 - child.cmd("normal! gg7j") - - -- Create a suggestion at line 3 - local edit = { - range = { - start = { line = 2, character = 0 }, - ["end"] = { line = 2, character = 0 }, - }, - newText = "suggested text ", - } - - -- Display suggestion - child.g.test_edit = edit - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - local edits = { vim.g.test_edit } - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) - end) - ref(child.get_screenshot()) - - -- Test: Moving cursor towards the suggestion (even outside buffer zone) shouldn't clear it - child.cmd("normal! 4k") -- Move to line 4, moving towards the suggestion - child.lua_func(function() - vim.uv.sleep(500) - end) - - -- Verify suggestion still exists - local suggestion_exists = child.lua_func(function() - return vim.b[0].nes_state ~= nil - end) - eq(suggestion_exists, true) - ref(child.get_screenshot()) -end - T["ui_preview"]["suggestion_history_basic_cycle"] = function() set_content("line1\nline2\nline3") - - -- Create first suggestion + -- Create first suggestion and display it local edit1 = { - range = { - start = { line = 1, character = 0 }, - ["end"] = { line = 1, character = 0 }, - }, + range = { start = { line = 1, character = 0 }, ["end"] = { line = 1, character = 0 } }, newText = "-- first suggestion", } - - -- Display first suggestion child.g.test_edit = edit1 child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local edits = { vim.g.test_edit } - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui")._display_next_suggestion(bufnr, ns_id, { vim.g.test_edit }) vim.uv.sleep(300) end) - -- Create and display second suggestion (should store first in history) + -- Create and display second suggestion local edit2 = { - range = { - start = { line = 2, character = 0 }, - ["end"] = { line = 2, character = 0 }, - }, + range = { start = { line = 2, character = 0 }, ["end"] = { line = 2, character = 0 } }, newText = "-- second suggestion", } - child.g.test_edit = edit2 child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local edits = { vim.g.test_edit } - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui")._display_next_suggestion(bufnr, ns_id, { vim.g.test_edit }) vim.uv.sleep(300) end) - -- Clear current suggestion (second should now be in history too) child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui").clear_suggestion(bufnr, ns_id) vim.uv.sleep(300) end) - -- First restore should show second suggestion (most recent) + local has_history = child.lua_func(function() + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").has_history(bufnr) + end) + eq(has_history, true) + + -- Test cycling through suggestions local restored1 = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local result = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + local result = require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) vim.uv.sleep(300) return result end) eq(restored1, true) - -- Verify we can check history content - local history_size = child.lua_func(function() - return #(vim.b[0].copilotlsp_nes_history or {}) - end) - eq(history_size, 2) - - -- Second restore should show first suggestion local restored2 = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local restored = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + local result = require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) vim.uv.sleep(300) - return restored + return result end) eq(restored2, true) - -- Third restore should cycle back to second suggestion local restored3 = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local restored = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + local result = require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) vim.uv.sleep(300) - return restored + return result end) eq(restored3, true) end @@ -452,37 +404,39 @@ T["ui_preview"]["suggestion_history_max_two_items"] = function() child.g.test_edit = edit child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local edits = { vim.g.test_edit } - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui")._display_next_suggestion(bufnr, ns_id, { vim.g.test_edit }) vim.uv.sleep(300) end) end - -- Clear current suggestion child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui").clear_suggestion(bufnr, ns_id) vim.uv.sleep(300) end) - -- Verify history only keeps 2 most recent - local history_size = child.lua_func(function() - return #(vim.b[0].copilotlsp_nes_history or {}) + local has_history = child.lua_func(function() + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").has_history(bufnr) end) - eq(history_size, 2) + eq(has_history, true) - -- Verify we can only cycle between 2 suggestions + -- Verify we can cycle through suggestions (should only have 2 most recent) local restore_results = {} - for _ = 1, 4 do -- Try 4 restores to test cycling + for i = 1, 4 do -- Try 4 restores to test cycling local restored = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - return require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + local result = require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) + vim.uv.sleep(300) + return result end) table.insert(restore_results, restored) - vim.uv.sleep(300) end - -- All restores should succeed + -- All restores should succeed (cycling between 2 items) for _, result in ipairs(restore_results) do eq(result, true) end @@ -491,146 +445,72 @@ end T["ui_preview"]["suggestion_history_invalid_after_text_changes"] = function() set_content("line1\nline2\nline3\nline4\nline5") - -- Create suggestion on line 4 (0-indexed) local edit = { - range = { - start = { line = 4, character = 0 }, - ["end"] = { line = 4, character = 0 }, - }, + range = { start = { line = 4, character = 0 }, ["end"] = { line = 4, character = 0 } }, newText = "-- comment on line 5", } child.g.test_edit = edit child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local edits = { vim.g.test_edit } - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, edits) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui")._display_next_suggestion(bufnr, ns_id, { vim.g.test_edit }) vim.uv.sleep(300) end) -- Clear suggestion to store in history child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + require("copilot-lsp.nes.ui").clear_suggestion(bufnr, ns_id) vim.uv.sleep(300) end) - -- Verify history exists - local history_size_before = child.lua_func(function() - return #(vim.b[0].copilotlsp_nes_history or {}) + -- Verify history exists before deletion + local has_history_before = child.lua_func(function() + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").has_history(bufnr) end) - eq(history_size_before, 1) + eq(has_history_before, true) - -- Delete lines to make history invalid (keep only first 3 lines) + -- Delete lines to make history invalid child.api.nvim_buf_set_lines(0, 3, -1, false, {}) + child.lua_func(function() + vim.uv.sleep(300) + end) -- Try to restore (should fail and clear history) local restored = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - local result = require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + local result = require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) vim.uv.sleep(300) return result end) eq(restored, false) - -- Verify history was cleared - local history_size_after = child.lua_func(function() - return #(vim.b[0].copilotlsp_nes_history or {}) - end) - eq(history_size_after, 0) -end - -T["ui_preview"]["suggestion_history_restore_index_reset"] = function() - set_content("line1\nline2\nline3") - -- Create and display two suggestions to build history - local edit1 = { - range = { - start = { line = 1, character = 0 }, - ["end"] = { line = 1, character = 0 }, - }, - newText = "-- first", - } - local edit2 = { - range = { - start = { line = 2, character = 0 }, - ["end"] = { line = 2, character = 0 }, - }, - newText = "-- second", - } - - -- Display first, then second (first goes to history) - child.g.test_edit = edit1 - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) - vim.uv.sleep(300) - end) - - child.g.test_edit = edit2 - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) - vim.uv.sleep(300) - end) - - -- Clear to add second to history - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui").clear_suggestion(0, ns_id) - vim.uv.sleep(300) - end) - - -- Restore once (should show second, index becomes 1) - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) - vim.uv.sleep(300) + local has_history_after = child.lua_func(function() + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").has_history(bufnr) end) - - -- Get restore index - local index_before = child.lua_func(function() - return vim.b[0].copilotlsp_nes_restore_index or 0 - end) - eq(index_before, 1) - - -- Display new suggestion (should reset index) - local edit3 = { - range = { - start = { line = 0, character = 0 }, - ["end"] = { line = 0, character = 0 }, - }, - newText = "-- third", - } - - child.g.test_edit = edit3 - child.lua_func(function() - local ns_id = vim.api.nvim_create_namespace("nes_test") - require("copilot-lsp.nes.ui")._display_next_suggestion(0, ns_id, { vim.g.test_edit }) - vim.uv.sleep(300) - end) - - -- Verify index was reset - local index_after = child.lua_func(function() - return vim.b[0].copilotlsp_nes_restore_index or 0 - end) - eq(index_after, 0) + eq(has_history_after, false) end T["ui_preview"]["suggestion_history_no_restore_when_empty"] = function() set_content("line1\nline2\nline3") - -- Try to restore when no history exists local restored = child.lua_func(function() local ns_id = vim.api.nvim_create_namespace("nes_test") - return require("copilot-lsp.nes.ui").restore_suggestion(0, ns_id) + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").restore_suggestion(bufnr, ns_id) end) eq(restored, false) - -- Verify no history exists - local history_size = child.lua_func(function() - return #(vim.b[0].copilotlsp_nes_history or {}) + local has_history = child.lua_func(function() + local bufnr = vim.api.nvim_get_current_buf() + return require("copilot-lsp.nes.ui").has_history(bufnr) end) - eq(history_size, 0) + eq(has_history, false) end return T