Skip to content

Commit a45b3d9

Browse files
authored
feat(ui): inline edit preview (#34)
* feat(ui): inline edit * feat(ui): define preview for inline edits * feat(ui): added preview * feat: support lines insertion above and below the current line * fix: tests * chore: removed unused code
1 parent 3510b84 commit a45b3d9

24 files changed

+1134
-263
lines changed

lsp/copilot_ls.lua

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,6 @@ return {
3030
}),
3131
root_dir = vim.uv.cwd(),
3232
on_init = function(client)
33-
vim.api.nvim_set_hl(0, "CopilotLspNesAdd", { link = "DiffAdd", default = true })
34-
vim.api.nvim_set_hl(0, "CopilotLspNesDelete", { link = "DiffDelete", default = true })
35-
vim.api.nvim_set_hl(0, "CopilotLspNesApply", { link = "DiffText", default = true })
36-
3733
local au = vim.api.nvim_create_augroup("copilotlsp.init", { clear = true })
3834
--NOTE: Inline Completions
3935
--TODO: We dont currently use this code path, so comment for now until a UI is built

lua/copilot-lsp/nes/init.lua

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ local nes_ns = vim.api.nvim_create_namespace("copilotlsp.nes")
88

99
---@param err lsp.ResponseError?
1010
---@param result copilotlsp.copilotInlineEditResponse
11-
local function handle_nes_response(err, result)
11+
---@param ctx lsp.HandlerContext
12+
local function handle_nes_response(err, result, ctx)
1213
if err then
1314
-- vim.notify(err.message)
1415
return
@@ -17,14 +18,17 @@ local function handle_nes_response(err, result)
1718
--- Convert to textEdit fields
1819
edit.newText = edit.text
1920
end
20-
nes_ui._display_next_suggestion(result.edits, nes_ns)
21+
nes_ui._display_next_suggestion(ctx.bufnr, nes_ns, result.edits)
2122
end
2223

2324
--- Requests the NextEditSuggestion from the current cursor position
24-
---@param copilot_lss vim.lsp.Client?
25+
---@param copilot_lss? vim.lsp.Client|string
2526
function M.request_nes(copilot_lss)
2627
local pos_params = vim.lsp.util.make_position_params(0, "utf-16")
2728
local version = vim.lsp.util.buf_versions[vim.api.nvim_get_current_buf()]
29+
if type(copilot_lss) == "string" then
30+
copilot_lss = vim.lsp.get_clients({ name = copilot_lss })[1]
31+
end
2832
assert(copilot_lss, errs.ErrNotStarted)
2933
---@diagnostic disable-next-line: inject-field
3034
pos_params.textDocument.version = version
@@ -109,4 +113,10 @@ function M.apply_pending_nes(bufnr)
109113
return true
110114
end
111115

116+
---@param bufnr? integer
117+
function M.clear_suggestion(bufnr)
118+
bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
119+
nes_ui.clear_suggestion(bufnr, nes_ns)
120+
end
121+
112122
return M

lua/copilot-lsp/nes/state.lua

Whitespace-only changes.

lua/copilot-lsp/nes/ui.lua

Lines changed: 125 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -14,108 +14,159 @@ function M.clear_suggestion(bufnr, ns_id)
1414
vim.b[bufnr].nes_jump = false
1515
return
1616
end
17-
vim.api.nvim_buf_clear_namespace(bufnr, ns_id, 0, -1)
17+
_dismiss_suggestion(bufnr, ns_id)
1818
---@type copilotlsp.InlineEdit
1919
local state = vim.b[bufnr].nes_state
2020
if not state then
2121
return
2222
end
2323

24-
_dismiss_suggestion(bufnr, ns_id)
2524
vim.b[bufnr].nes_state = nil
2625
end
2726

28-
local function trim_end(s)
29-
return s:gsub("%s+$", "")
30-
end
31-
3227
---@private
33-
---@param suggestion copilotlsp.InlineEdit
34-
---@return copilotlsp.nes.LineCalculationResult
35-
function M._calculate_lines(suggestion)
36-
local deleted_lines_count = suggestion.range["end"].line - suggestion.range.start.line
37-
local added_lines = vim.split(trim_end(suggestion.newText), "\n")
38-
local added_lines_count = suggestion.newText == "" and 0 or #added_lines
39-
local same_line = 0
40-
41-
if deleted_lines_count == 0 and added_lines_count == 1 then
42-
---changing within line
43-
deleted_lines_count = 1
44-
same_line = 1
28+
---@param bufnr integer
29+
---@param edit lsp.TextEdit
30+
---@return copilotlsp.nes.InlineEditPreview
31+
function M._calculate_preview(bufnr, edit)
32+
local text = edit.newText
33+
local range = edit.range
34+
local start_line = range.start.line
35+
local start_char = range.start.character
36+
local end_line = range["end"].line
37+
local end_char = range["end"].character
38+
39+
-- Split text by newline. Use plain=true to handle trailing newline correctly.
40+
local new_lines = vim.split(text, "\n", { plain = true })
41+
local num_new_lines = #new_lines
42+
43+
local old_lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false)
44+
local num_old_lines = #old_lines
45+
46+
local is_same_line = start_line == end_line
47+
local is_deletion = text == ""
48+
local lines_edit = is_same_line or (start_char == 0 and end_char == 0)
49+
local is_insertion = is_same_line and start_char == end_char
50+
51+
if is_deletion and is_insertion then
52+
-- no-op
53+
return {}
4554
end
4655

47-
-- if
48-
-- suggestion.range.start.line == suggestion.range["end"].line
49-
-- and suggestion.range.start.character == suggestion.range["end"].character
50-
-- then
51-
-- --add only
52-
-- TODO: Do we need to position specifically for add only?
53-
-- UI tests seem to say no
54-
-- end
55-
56-
-- Calculate positions for delete highlight extmark
57-
---@type copilotlsp.nes.DeleteExtmark
58-
local delete_extmark = {
59-
row = suggestion.range.start.line,
60-
end_row = (
61-
suggestion.range["end"].character ~= 0 and suggestion.range["end"].line + 1
62-
or suggestion.range["end"].line
63-
),
64-
}
56+
if is_deletion and lines_edit then
57+
return {
58+
deletion = {
59+
range = edit.range,
60+
},
61+
}
62+
end
6563

66-
-- Calculate positions for virtual lines extmark
67-
---@type copilotlsp.nes.AddExtmark
68-
local virt_lines_extmark = {
69-
row = (
70-
suggestion.range["end"].character ~= 0 and suggestion.range["end"].line
71-
or suggestion.range["end"].line - 1
72-
),
73-
virt_lines_count = added_lines_count,
74-
}
64+
if is_insertion and num_new_lines == 1 and text ~= "" then
65+
-- inline insertion
66+
return {
67+
inline_insertion = {
68+
text = text,
69+
line = start_line,
70+
character = start_char,
71+
},
72+
}
73+
end
74+
75+
if is_insertion and num_new_lines > 1 then
76+
if start_char == #old_lines[1] and new_lines[1] == "" then
77+
-- insert lines after the start line
78+
return {
79+
lines_insertion = {
80+
text = table.concat(vim.list_slice(new_lines, 2), "\n"),
81+
line = start_line,
82+
},
83+
}
84+
end
85+
86+
if end_char == 0 and new_lines[num_new_lines] == "" then
87+
-- insert lines before the end line
88+
return {
89+
lines_insertion = {
90+
text = table.concat(vim.list_slice(new_lines, 1, num_new_lines - 1), "\n"),
91+
line = start_line,
92+
above = true,
93+
},
94+
}
95+
end
96+
end
97+
98+
-- insert lines in the middle
99+
local prefix = old_lines[1]:sub(1, start_char)
100+
local suffix = old_lines[num_old_lines]:sub(end_char + 1)
101+
local new_lines_extend = vim.deepcopy(new_lines)
102+
new_lines_extend[1] = prefix .. new_lines_extend[1]
103+
new_lines_extend[num_new_lines] = new_lines_extend[num_new_lines] .. suffix
104+
local insertion = table.concat(new_lines_extend, "\n")
75105

76106
return {
77-
deleted_lines_count = deleted_lines_count,
78-
added_lines = added_lines,
79-
added_lines_count = added_lines_count,
80-
same_line = same_line,
81-
delete_extmark = delete_extmark,
82-
virt_lines_extmark = virt_lines_extmark,
107+
deletion = {
108+
range = {
109+
start = { line = start_line, character = 0 },
110+
["end"] = { line = end_line, character = #old_lines[num_old_lines] },
111+
},
112+
},
113+
lines_insertion = {
114+
text = insertion,
115+
line = end_line,
116+
},
83117
}
84118
end
85119

86120
---@private
87-
---@param edits copilotlsp.InlineEdit[]
121+
---@param bufnr integer
88122
---@param ns_id integer
89-
function M._display_next_suggestion(edits, ns_id)
90-
local state = vim.b[vim.api.nvim_get_current_buf()].nes_state
91-
if state then
92-
M.clear_suggestion(vim.api.nvim_get_current_buf(), ns_id)
93-
end
94-
95-
if not edits or #edits == 0 then
96-
-- vim.notify("No suggestion available", vim.log.levels.INFO)
97-
return
123+
---@param preview copilotlsp.nes.InlineEditPreview
124+
function M._display_preview(bufnr, ns_id, preview)
125+
if preview.deletion then
126+
local range = preview.deletion.range
127+
vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
128+
hl_group = "CopilotLspNesDelete",
129+
end_row = range["end"].line,
130+
end_col = range["end"].character,
131+
})
98132
end
99-
local bufnr = vim.uri_to_bufnr(edits[1].textDocument.uri)
100-
local suggestion = edits[1]
101133

102-
local lines = M._calculate_lines(suggestion)
103-
104-
if lines.deleted_lines_count > 0 then
105-
-- Deleted range red highlight
106-
vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines.delete_extmark.row, 0, {
107-
hl_group = "CopilotLspNesDelete",
108-
end_row = lines.delete_extmark.end_row,
134+
local inline_insertion = preview.inline_insertion
135+
if inline_insertion then
136+
local virt_lines =
137+
require("copilot-lsp.util").hl_text_to_virt_lines(inline_insertion.text, vim.bo[bufnr].filetype)
138+
vim.api.nvim_buf_set_extmark(bufnr, ns_id, inline_insertion.line, inline_insertion.character, {
139+
virt_text = virt_lines[1],
140+
virt_text_pos = "inline",
109141
})
110142
end
111-
if lines.added_lines_count > 0 then
112-
local text = trim_end(edits[1].text)
113-
local virt_lines = require("copilot-lsp.util").hl_text_to_virt_lines(text, vim.bo[bufnr].filetype)
114143

115-
vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines.virt_lines_extmark.row, 0, {
144+
local lines_insertion = preview.lines_insertion
145+
if lines_insertion then
146+
local virt_lines =
147+
require("copilot-lsp.util").hl_text_to_virt_lines(lines_insertion.text, vim.bo[bufnr].filetype)
148+
vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines_insertion.line, 0, {
116149
virt_lines = virt_lines,
150+
virt_lines_above = lines_insertion.above,
117151
})
118152
end
153+
end
154+
155+
---@private
156+
---@param bufnr integer
157+
---@param ns_id integer
158+
---@param edits copilotlsp.InlineEdit[]
159+
function M._display_next_suggestion(bufnr, ns_id, edits)
160+
M.clear_suggestion(bufnr, ns_id)
161+
if not edits or #edits == 0 then
162+
-- vim.notify("No suggestion available", vim.log.levels.INFO)
163+
return
164+
end
165+
166+
local suggestion = edits[1]
167+
168+
local preview = M._calculate_preview(bufnr, suggestion)
169+
M._display_preview(bufnr, ns_id, preview)
119170

120171
vim.b[bufnr].nes_state = suggestion
121172

lua/copilot-lsp/types.lua

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,25 @@
1-
---@class copilotlsp.InlineEdit
1+
---@class copilotlsp.InlineEdit : lsp.TextEdit
22
---@field command lsp.Command
3-
---@field range lsp.Range
43
---@field text string
5-
---@field newText string
64
---@field textDocument lsp.VersionedTextDocumentIdentifier
75

86
---@class copilotlsp.copilotInlineEditResponse
97
---@field edits copilotlsp.InlineEdit[]
108

11-
---@class copilotlsp.nes.EditSuggestionUI
12-
---@field preview_winnr? integer
9+
---@class copilotlsp.nes.TextDeletion
10+
---@field range lsp.Range
1311

14-
---@class copilotlsp.nes.DeleteExtmark
15-
--- Holds row information for delete highlight extmark.
16-
---@field row number
17-
---@field end_row number
12+
---@class copilotlsp.nes.InlineInsertion
13+
---@field text string
14+
---@field line integer
15+
---@field character integer
1816

19-
---@class copilotlsp.nes.AddExtmark
20-
-- Holds row and virtual lines count for virtual lines extmark.
21-
---@field row number
22-
---@field virt_lines_count number
17+
---@class copilotlsp.nes.TextInsertion
18+
---@field text string
19+
---@field line integer insert lines at this line
20+
---@field above? boolean above the line
2321

24-
---@class copilotlsp.nes.LineCalculationResult
25-
--- The result of calculating lines for inline suggestion UI.
26-
---@field deleted_lines_count number
27-
---@field added_lines string[]
28-
---@field added_lines_count number
29-
---@field same_line number
30-
---@field delete_extmark copilotlsp.nes.DeleteExtmark
31-
---@field virt_lines_extmark copilotlsp.nes.AddExtmark
22+
---@class copilotlsp.nes.InlineEditPreview
23+
---@field deletion? copilotlsp.nes.TextDeletion
24+
---@field inline_insertion? copilotlsp.nes.InlineInsertion
25+
---@field lines_insertion? copilotlsp.nes.TextInsertion

plugin/copilot-lsp.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
vim.api.nvim_set_hl(0, "CopilotLspNesAdd", { link = "DiffAdd", default = true })
2+
vim.api.nvim_set_hl(0, "CopilotLspNesDelete", { link = "DiffDelete", default = true })
3+
vim.api.nvim_set_hl(0, "CopilotLspNesApply", { link = "DiffText", default = true })

tests/mock_lsp.lua

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ local function getNesResponse(td)
7474
command = { title = "mock", command = "mock" },
7575
range = {
7676
start = { line = 4, character = 0 },
77-
["end"] = { line = 4, character = 30 },
77+
["end"] = { line = 4, character = 31 },
7878
},
7979
textDocument = td,
8080
text = [[ printf("Goodb, %s!\n", name);]],

0 commit comments

Comments
 (0)