1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
|
local util = require("quicker.util")
local M = {}
---@class (exact) quicker.QFContext
---@field num_before integer
---@field num_after integer
---@class (exact) quicker.ExpandOpts
---@field before? integer Number of lines of context to show before the line (default 2)
---@field after? integer Number of lines of context to show after the line (default 2)
---@field add_to_existing? boolean
---@field loclist_win? integer
---@param item QuickFixItem
---@param new_text string
local function update_item_text_keep_diagnostics(item, new_text)
-- If this is an "error" item, replace the text with the source line and store that text
-- in the user data so we can add it as virtual text later
if item.type ~= "" and not vim.endswith(new_text, item.text) then
local user_data = util.get_user_data(item)
if not user_data.error_text then
user_data.error_text = item.text
item.user_data = user_data
end
end
item.text = new_text
end
---@param opts? quicker.ExpandOpts
function M.expand(opts)
opts = opts or {}
if not opts.loclist_win and util.get_win_type(0) == "l" then
opts.loclist_win = vim.api.nvim_get_current_win()
end
local qf_list
if opts.loclist_win then
qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
else
qf_list = vim.fn.getqflist({ all = 0 })
end
local winid = qf_list.winid
if not winid then
vim.notify("Cannot find quickfix window", vim.log.levels.ERROR)
return
end
local ctx = qf_list.context or {}
if type(ctx) ~= "table" then
-- If the quickfix had a non-table context, we're going to have to overwrite it
ctx = {}
end
---@type quicker.QFContext
local quicker_ctx = ctx.quicker
if not quicker_ctx then
quicker_ctx = { num_before = 0, num_after = 0 }
ctx.quicker = quicker_ctx
end
local curpos = vim.api.nvim_win_get_cursor(winid)[1]
local cur_item = qf_list.items[curpos]
local newpos
-- calculate the number of lines to show before and after the current line
local num_before = opts.before or 2
if opts.add_to_existing then
num_before = num_before + quicker_ctx.num_before
end
num_before = math.max(0, num_before)
quicker_ctx.num_before = num_before
local num_after = opts.after or 2
if opts.add_to_existing then
num_after = num_after + quicker_ctx.num_after
end
num_after = math.max(0, num_after)
quicker_ctx.num_after = num_after
local items = {}
---@type nil|QuickFixItem
local prev_item
---@param i integer
---@return nil|QuickFixItem
local function get_next_item(i)
local item = qf_list.items[i]
for j = i + 1, #qf_list.items do
local next_item = qf_list.items[j]
-- Next valid item that is on a different line (since we dedupe same-line items)
if
next_item.valid == 1 and (item.bufnr ~= next_item.bufnr or item.lnum ~= next_item.lnum)
then
return next_item
end
end
end
for i, item in ipairs(qf_list.items) do
(function()
---@cast item QuickFixItem
if item.valid == 0 or item.bufnr == 0 then
return
end
if not vim.api.nvim_buf_is_loaded(item.bufnr) then
vim.fn.bufload(item.bufnr)
end
local overlaps_previous = false
local header_type = "hard"
local low = math.max(0, item.lnum - 1 - num_before)
if prev_item then
if prev_item.bufnr == item.bufnr then
-- If this is the second match on the same line, skip this item
if prev_item.lnum == item.lnum then
return
end
header_type = "soft"
if prev_item.lnum + num_after >= low then
low = math.min(item.lnum - 1, prev_item.lnum + num_after)
overlaps_previous = true
end
end
end
local high = item.lnum + num_after
local next_item = get_next_item(i)
if next_item then
if next_item.bufnr == item.bufnr and next_item.lnum <= high then
high = next_item.lnum - 1
end
end
local item_start_idx = #items
local lines = vim.api.nvim_buf_get_lines(item.bufnr, low, high, false)
for j, line in ipairs(lines) do
if j + low == item.lnum then
update_item_text_keep_diagnostics(item, line)
table.insert(items, item)
else
table.insert(items, {
bufnr = item.bufnr,
lnum = low + j,
text = line,
valid = 0,
user_data = { lnum = low + j },
})
end
if cur_item.bufnr == item.bufnr and cur_item.lnum == low + j then
newpos = #items
end
end
-- Add the header to the first item in this sequence, if one is needed
if prev_item and not overlaps_previous then
local first_item = items[item_start_idx + 1]
if first_item then
first_item.user_data = first_item.user_data or {}
first_item.user_data.header = header_type
end
end
prev_item = item
end)()
if i == curpos and not newpos then
newpos = #items
end
end
if opts.loclist_win then
vim.fn.setloclist(
opts.loclist_win,
{},
"r",
{ items = items, title = qf_list.title, context = ctx }
)
else
vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = ctx })
end
pcall(vim.api.nvim_win_set_cursor, qf_list.winid, { newpos, 0 })
end
---@class (exact) quicker.CollapseArgs
---@field loclist_win? integer
---
function M.collapse(opts)
opts = opts or {}
if not opts.loclist_win and util.get_win_type(0) == "l" then
opts.loclist_win = vim.api.nvim_get_current_win()
end
local curpos = vim.api.nvim_win_get_cursor(0)[1]
local qf_list
if opts.loclist_win then
qf_list = vim.fn.getloclist(opts.loclist_win, { all = 0 })
else
qf_list = vim.fn.getqflist({ all = 0 })
end
local items = {}
local last_item
for i, item in ipairs(qf_list.items) do
if item.valid == 1 then
if item.user_data then
-- Clear the header, if present
item.user_data.header = nil
end
table.insert(items, item)
if i <= curpos then
last_item = #items
end
end
end
vim.tbl_filter(function(item)
return item.valid == 1
end, qf_list.items)
local ctx = qf_list.context or {}
if type(ctx) == "table" then
local quicker_ctx = ctx.quicker
if quicker_ctx then
quicker_ctx = { num_before = 0, num_after = 0 }
ctx.quicker = quicker_ctx
end
end
if opts.loclist_win then
vim.fn.setloclist(
opts.loclist_win,
{},
"r",
{ items = items, title = qf_list.title, context = qf_list.context }
)
else
vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
end
if qf_list.winid then
if last_item then
vim.api.nvim_win_set_cursor(qf_list.winid, { last_item, 0 })
end
end
end
---@param opts? quicker.ExpandOpts
function M.toggle(opts)
opts = opts or {}
local ctx
if opts.loclist_win then
ctx = vim.fn.getloclist(opts.loclist_win, { context = 0 }).context
else
ctx = vim.fn.getqflist({ context = 0 }).context
end
if
type(ctx) == "table"
and ctx.quicker
and (ctx.quicker.num_before > 0 or ctx.quicker.num_after > 0)
then
M.collapse()
else
M.expand(opts)
end
end
---@class (exact) quicker.RefreshOpts
---@field keep_diagnostics? boolean If a line has a diagnostic type, keep the original text and display it as virtual text after refreshing from source.
---@param loclist_win? integer
---@param opts? quicker.RefreshOpts
function M.refresh(loclist_win, opts)
opts = vim.tbl_extend("keep", opts or {}, { keep_diagnostics = true })
if not loclist_win then
local ok, qf = pcall(vim.fn.getloclist, 0, { filewinid = 0 })
if ok and qf.filewinid and qf.filewinid ~= 0 then
loclist_win = qf.filewinid
end
end
local qf_list
if loclist_win then
qf_list = vim.fn.getloclist(loclist_win, { all = 0 })
else
qf_list = vim.fn.getqflist({ all = 0 })
end
local items = {}
for _, item in ipairs(qf_list.items) do
if item.bufnr ~= 0 and item.lnum ~= 0 then
if not vim.api.nvim_buf_is_loaded(item.bufnr) then
vim.fn.bufload(item.bufnr)
end
local line = vim.api.nvim_buf_get_lines(item.bufnr, item.lnum - 1, item.lnum, false)[1]
if line then
if opts.keep_diagnostics then
update_item_text_keep_diagnostics(item, line)
else
item.text = line
end
table.insert(items, item)
end
else
table.insert(items, item)
end
end
if loclist_win then
vim.fn.setloclist(
loclist_win,
{},
"r",
{ items = items, title = qf_list.title, context = qf_list.context }
)
else
vim.fn.setqflist({}, "r", { items = items, title = qf_list.title, context = qf_list.context })
end
end
return M
|