Skip to content

Commit f343fcd

Browse files
author
Jan Steinke
authoredApr 6, 2024
Merge pull request #4 from jan-xyz/make_edits_selectable_2
Make individual edits selectable
2 parents 1526d0b + e1d3cf1 commit f343fcd

File tree

9 files changed

+354
-294
lines changed

9 files changed

+354
-294
lines changed
 

‎README.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ lazy.nvim:
2929
dependencies = {
3030
"nvim-telescope/telescope.nvim",
3131
"nvim-lua/plenary.nvim",
32+
{
33+
"echasnovski/mini.nvim",
34+
config = function()
35+
require("mini.diff").setup({})
36+
end,
37+
},
3238
},
3339
}
3440
```
@@ -60,10 +66,6 @@ require("lsp-preview").setup({
6066
apply = true,
6167
--Preview all changes per default.
6268
preview = false,
63-
--Configuration provided to vim.diff (see `:h vim.diff()`)
64-
diff = {
65-
ctxlen = 5,
66-
},
6769
})
6870
```
6971

‎lua/lsp-preview/config.lua

-5
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,9 @@ local M = {}
55
---@field apply boolean
66
---Preview all changes per default.
77
---@field previe boolean
8-
---Configuration provided to vim.diff (see `:h vim.diff()`)
9-
---@field diff table
108
local default_config = {
119
apply = true,
1210
preview = false,
13-
diff = {
14-
ctxlen = 5,
15-
},
1611
}
1712

1813

‎lua/lsp-preview/diff.lua

+200-111
Large diffs are not rendered by default.

‎lua/lsp-preview/telescope.lua

+40-138
Original file line numberDiff line numberDiff line change
@@ -4,64 +4,54 @@
44

55
local action_state = require("telescope.actions.state")
66

7+
---The diff hunk
78
---@class Previewable
8-
---@field title fun(self): string
9-
---@field preview fun(self, opts: table): {text: string, syntax: string}
10-
11-
---@class Value
129
---@field title string
13-
---@field index string
14-
---@field type string
15-
---@field entry Previewable
10+
---@field filename fun(self): string
11+
---Return the index in the workspace_edit, sub_index is only provided for
12+
---Edits.
13+
---@field index number | {primary: number, secondary: number}
14+
---@field preview fun(self, bufnr: integer, winid: integer, opts: table): string
1615

17-
local M = {}
16+
---The Entry passed around in Telescope
17+
---@class Entry
18+
---@field value Previewable
19+
---@field display fun(entry: Entry)
20+
---@field ordinal string
1821

19-
---@param entry Previewable
20-
local default_make_value = function(entry)
21-
return {
22-
title = entry:title(),
23-
}
24-
end
2522

26-
---@param values Value[]
27-
local default_make_make_display = function(values)
23+
local M = {}
24+
25+
---@param values Previewable[]
26+
local make_make_display = function(values)
2827
local entry_display = require("telescope.pickers.entry_display")
2928
local strings = require("plenary.strings")
3029

31-
local index_width = 0
3230
local title_width = 0
3331
for _, value in ipairs(values) do
34-
index_width = math.max(index_width, strings.strdisplaywidth(value.index))
3532
title_width = math.max(title_width, strings.strdisplaywidth(value.title))
3633
end
3734

3835
local displayer = entry_display.create({
3936
separator = " ",
4037
items = {
41-
{ width = index_width + 1 },
4238
{ width = title_width },
4339
},
4440
})
4541
return function(entry)
4642
return displayer({
47-
{ entry.value.index .. ":", "TelescopePromptPrefix" },
4843
{ entry.value.title },
4944
})
5045
end
5146
end
5247

53-
---@param job_id string
54-
---@return boolean
55-
local job_is_running = function(job_id)
56-
return vim.fn.jobwait({ job_id }, 0)[1] == -1
57-
end
58-
5948
---@param prompt_bufnr integer
60-
---@return integer[]
49+
---@return Entry[]
6150
local get_selected_diffs = function(prompt_bufnr)
51+
---@type Entry[]
6252
local selected = {}
6353
local current_picker = action_state.get_current_picker(prompt_bufnr)
64-
---@type Value[]
54+
---@type Previewable[]
6555
local selections = current_picker:get_multi_selection()
6656
if vim.tbl_isempty(selections) then
6757
vim.notify("no change selected")
@@ -73,162 +63,74 @@ local get_selected_diffs = function(prompt_bufnr)
7363
return selected
7464
end
7565

76-
---@param documentChanges Previewable[]
7766
---@param changes Previewable[]
7867
---@param apply_selection fun(selected_indices: integer[])
79-
function M.apply_action(opts, documentChanges, changes, apply_selection)
68+
function M.apply_action(opts, changes, apply_selection)
8069
local actions = require("telescope.actions")
8170
local pickers = require("telescope.pickers")
82-
local Previewer = require("telescope.previewers.previewer")
71+
local previewers = require("telescope.previewers")
8372
local finders = require("telescope.finders")
8473
local conf = require("telescope.config").values
8574
local utils = require("telescope.utils")
8675
local putils = require("telescope.previewers.utils")
8776

88-
local make_value = default_make_value
89-
---@type Value[]
90-
local values = {}
91-
for index, entry in ipairs(documentChanges) do
92-
local value = make_value(entry)
77+
-- validate the table to have everything we need
78+
for _, value in ipairs(changes) do
9379
if type(value) ~= "table" then
9480
error("'make_value' must return a table")
9581
end
96-
if value.title == nil then
82+
if type(value.title) ~= "string" then
9783
error("'make_value' must return a table containing a field 'title'")
9884
end
99-
100-
value.index = index
101-
value.entry = entry
102-
value.type = "documentChanges"
103-
104-
table.insert(values, value)
10585
end
106-
for index, entry in ipairs(changes) do
107-
local value = make_value(entry)
108-
if type(value) ~= "table" then
109-
error("'make_value' must return a table")
110-
end
111-
if value.title == nil then
112-
error("'make_value' must return a table containing a field 'title'")
113-
end
11486

115-
value.index = index
116-
value.entry = entry
117-
value.type = "changes"
87+
local make_display = make_make_display(changes)
11888

119-
table.insert(values, value)
120-
end
121-
122-
local make_display = default_make_make_display(values)
123-
124-
local buffers = {}
125-
local term_ids = {}
126-
127-
local previewer = Previewer:new({
128-
title = "Code Action Preview",
129-
setup = function(_self)
89+
local previewer = previewers.new_buffer_previewer({
90+
setup = function(self)
13091
-- pre-select all changes on picker creation
13192
local prompt_bufnr = vim.api.nvim_get_current_buf()
13293
actions.select_all(prompt_bufnr)
13394
return {}
13495
end,
135-
teardown = function(self)
136-
if not self.state then
137-
return
138-
end
139-
140-
self.state.winid = nil
141-
self.state.bufnr = nil
142-
143-
for _, bufnr in ipairs(buffers) do
144-
local term_id = term_ids[bufnr]
145-
if term_id and job_is_running(term_id) then
146-
vim.fn.jobstop(term_id)
147-
end
148-
utils.buf_delete(bufnr)
149-
end
150-
151-
buffers = {}
152-
term_ids = {}
153-
end,
154-
---@param entry {value: Value}
155-
preview_fn = function(self, entry, status)
156-
local preview_winid = status.layout and status.layout.preview and status.layout.preview.winid or
157-
status.preview_win
158-
159-
local do_preview = false
160-
local bufnr = buffers[entry.value.index]
161-
if not bufnr then
162-
bufnr = vim.api.nvim_create_buf(false, true)
163-
buffers[entry.value.index] = bufnr
164-
do_preview = true
165-
166-
vim.api.nvim_win_set_option(preview_winid, "winhl", "Normal:TelescopePreviewNormal")
167-
vim.api.nvim_win_set_option(preview_winid, "signcolumn", "no")
168-
vim.api.nvim_win_set_option(preview_winid, "foldlevel", 100)
169-
vim.api.nvim_win_set_option(preview_winid, "wrap", false)
170-
vim.api.nvim_win_set_option(preview_winid, "scrollbind", false)
171-
end
172-
173-
utils.win_set_buf_noautocmd(preview_winid, bufnr)
174-
self.state.winid = preview_winid
175-
self.state.bufnr = bufnr
176-
177-
if do_preview then
178-
local preview = entry.value.entry:preview(opts)
179-
preview = preview or { syntax = "", text = "preview not available" }
180-
181-
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, vim.split(preview.text, "\n", { plain = true }))
182-
putils.highlighter(bufnr, preview.syntax, {})
183-
end
96+
---@param entry Entry
97+
define_preview = function(self, entry, status)
98+
local filetype = entry.value:preview(self.state.bufnr, self.state.winid, opts)
99+
putils.highlighter(self.state.bufnr, filetype, {})
184100
end,
185-
scroll_fn = function(self, direction)
186-
if not self.state then
187-
return
188-
end
189-
190-
local count = math.abs(direction)
191-
local term_id = term_ids[self.state.bufnr]
192-
if term_id and job_is_running(term_id) then
193-
local input = direction > 0 and "d" or "u"
194-
195-
local termcode = vim.api.nvim_replace_termcodes(count .. input, true, false, true)
196-
vim.fn.chansend(term_id, termcode)
197-
else
198-
local input = direction > 0 and [[]] or [[]]
199-
200-
vim.api.nvim_win_call(self.state.winid, function()
201-
vim.cmd([[normal! ]] .. count .. input)
202-
end)
203-
end
101+
---@param entry Entry
102+
---@return string
103+
get_buffer_by_name = function(self, entry)
104+
-- create a single buffer per file.
105+
return entry.value:filename()
204106
end,
205107
})
206108

207109
local finder = finders.new_table({
208-
results = values,
110+
results = changes,
209111
entry_maker = function(value)
210112
return {
211113
display = make_display,
212-
ordinal = value.index .. value.title,
114+
ordinal = value.title,
213115
value = value,
214116
}
215117
end,
216118
})
217119

218120
pickers.new(opts, {
219-
prompt_title = "Code Actions",
121+
prompt_title = "Edits to apply",
220122
previewer = previewer,
221123
finder = finder,
222124
sorter = conf.generic_sorter(opts),
223125
attach_mappings = function(prompt_bufnr, map)
224126
map("i", "<c-a>", actions.toggle_all)
225127

226128
actions.select_default:replace(function()
227-
local selections = get_selected_diffs(prompt_bufnr)
129+
local selected_entries = get_selected_diffs(prompt_bufnr)
228130

229131
actions.close(prompt_bufnr)
230132

231-
apply_selection(selections)
133+
apply_selection(selected_entries)
232134
end)
233135

234136
return true

‎lua/lsp-preview/workspace_edit.lua

+2-34
Original file line numberDiff line numberDiff line change
@@ -4,52 +4,20 @@ local config = require("lsp-preview.config")
44

55
local M = {}
66

7-
---Used as injection for the telescope picker to apply the selection.
8-
---Filters the workspace edit for the selected hunks and applies it.
9-
---@param workspace_edit WorkspaceEdit
10-
---@param offset_encoding string
11-
---@return fun(selected_indices: {value: Value}[])
12-
local make_apply_func = function(workspace_edit, offset_encoding, orig_apply_workspace_edits)
13-
return function(selected_indices)
14-
local documentChanges = {}
15-
local changes = {}
16-
for _, selection in ipairs(selected_indices) do
17-
if selection.value.type == "documentChanges" then
18-
local index = selection.value.index
19-
local edit = workspace_edit.documentChanges[index]
20-
table.insert(documentChanges, edit)
21-
elseif selection.value.type == "changes" then
22-
local entry = selection.value.entry
23-
---@cast entry Edit
24-
local edit = workspace_edit.changes[entry.uri]
25-
changes[entry.uri] = edit
26-
end
27-
end
28-
29-
if not vim.tbl_isempty(documentChanges) then
30-
workspace_edit.documentChanges = documentChanges
31-
end
32-
if not vim.tbl_isempty(changes) then
33-
workspace_edit.changes = changes
34-
end
35-
orig_apply_workspace_edits(workspace_edit, offset_encoding)
36-
end
37-
end
387

398
---Overwriting the built-in function with the selection and preview capabilities
409
---@param orig_apply_workspace_edits fun(workspace_edit: WorkspaceEdit, offset_encoding: string)
4110
---@return fun(workspace_edit: WorkspaceEdit, offset_encoding: string)
4211
M.make_apply_workspace_edit = function(orig_apply_workspace_edits)
4312
return function(workspace_edit, offset_encoding)
44-
local documentChanges, changes = lDiff.get_changes(workspace_edit, offset_encoding)
13+
local changes = lDiff.get_changes(workspace_edit, offset_encoding)
4514

4615
local opts = config
4716

4817
lTelescope.apply_action(
4918
opts,
50-
documentChanges,
5119
changes,
52-
make_apply_func(workspace_edit, offset_encoding, orig_apply_workspace_edits)
20+
lDiff.make_apply_func(workspace_edit, offset_encoding, orig_apply_workspace_edits)
5321
)
5422
end
5523
end

‎selection.png

58.9 KB
Loading

‎tests/init.vim

-2
This file was deleted.

‎tests/minimal_init.lua

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ local lazypath = vim.fn.stdpath("data") .. "/lazy"
22
vim.notify = print
33
vim.opt.rtp:append(".")
44
vim.opt.rtp:append(lazypath .. "/plenary.nvim")
5+
vim.opt.rtp:append(lazypath .. "/mini.nvim")
56
vim.opt.rtp:append(lazypath .. "/nvim-treesitter")
67
vim.opt.rtp:append(lazypath .. "/nvim-nio")
78

‎tests/unit/diff_spec.lua

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
local describe = require("plenary.busted").describe
2+
local it = require("plenary.busted").it
3+
local before_each = require("plenary.busted").before_each
4+
local assert = require("luassert.assert")
5+
6+
local diff = require("lsp-preview.diff")
7+
8+
describe("make_apply_func", function()
9+
it("filters the workspace_edit documentChanges correctly", function()
10+
-- Given
11+
---@type WorkspaceEdit
12+
local input_workspace_edit = {
13+
documentChanges = {
14+
{ kind = "create", uri = "create-some-uri" },
15+
{ kind = "rename", uri = "rename-some-uri" },
16+
{ kind = "delete", uri = "delete-some-uri" },
17+
{
18+
textDocument = { uri = "edit-some-uri" },
19+
edits = {
20+
{ range = { start = { line = 1, character = 2 }, ["end"] = { line = 1, character = 10 } }, newText = "foo" },
21+
{ range = { start = { line = 2, character = 3 }, ["end"] = { line = 2, character = 11 } }, newText = "bar" },
22+
{ range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 12 } }, newText = "baz" },
23+
},
24+
},
25+
},
26+
}
27+
local input_selected_entries = {
28+
{ value = { index = 1 } },
29+
{ value = { index = 3 } },
30+
{ value = { index = { primary = 4, secondary = 1 } } },
31+
{ value = { index = { primary = 4, secondary = 3 } } },
32+
}
33+
34+
-- When
35+
---@type WorkspaceEdit
36+
local got_workspace_edit = {}
37+
local got_offset_encoding = ""
38+
39+
local capture_fn = function(captured_workspace_edit, captured_offset_encoding)
40+
got_workspace_edit = captured_workspace_edit
41+
got_offset_encoding = captured_offset_encoding
42+
end
43+
local fn = diff.make_apply_func(input_workspace_edit, "utf16", capture_fn)
44+
fn(input_selected_entries)
45+
46+
-- Then
47+
local want = {
48+
documentChanges = {
49+
{ kind = "create", uri = "create-some-uri" },
50+
{ kind = "delete", uri = "delete-some-uri" },
51+
{
52+
textDocument = { uri = "edit-some-uri" },
53+
edits = {
54+
{ range = { start = { line = 1, character = 2 }, ["end"] = { line = 1, character = 10 } }, newText = "foo" },
55+
{ range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 12 } }, newText = "baz" },
56+
},
57+
},
58+
},
59+
}
60+
assert.same(want, got_workspace_edit)
61+
assert.equals("utf16", got_offset_encoding)
62+
end)
63+
64+
it("filters the workspace_edit changes correctly", function()
65+
-- Given
66+
---@type WorkspaceEdit
67+
local input_workspace_edit = {
68+
changes = {
69+
["edit-some-uri"] = {
70+
{ range = { start = { line = 1, character = 2 }, ["end"] = { line = 1, character = 10 } }, newText = "foo" },
71+
{ range = { start = { line = 2, character = 3 }, ["end"] = { line = 2, character = 11 } }, newText = "bar" },
72+
{ range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 12 } }, newText = "baz" },
73+
},
74+
},
75+
}
76+
local input_selected_entries = {
77+
{ value = { index = { primary = 1, secondary = 1 }, uri = "edit-some-uri" } },
78+
{ value = { index = { primary = 1, secondary = 3 }, uri = "edit-some-uri" } },
79+
}
80+
81+
-- When
82+
---@type WorkspaceEdit
83+
local got_workspace_edit = {}
84+
local got_offset_encoding = ""
85+
86+
local capture_fn = function(captured_workspace_edit, captured_offset_encoding)
87+
got_workspace_edit = captured_workspace_edit
88+
got_offset_encoding = captured_offset_encoding
89+
end
90+
local fn = diff.make_apply_func(input_workspace_edit, "utf16", capture_fn)
91+
fn(input_selected_entries)
92+
93+
-- Then
94+
local want = {
95+
changes = {
96+
["edit-some-uri"] = {
97+
{ range = { start = { line = 1, character = 2 }, ["end"] = { line = 1, character = 10 } }, newText = "foo" },
98+
{ range = { start = { line = 3, character = 4 }, ["end"] = { line = 3, character = 12 } }, newText = "baz" },
99+
},
100+
},
101+
}
102+
assert.same(want, got_workspace_edit)
103+
assert.equals("utf16", got_offset_encoding)
104+
end)
105+
end)

0 commit comments

Comments
 (0)
Please sign in to comment.