-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpopup.lua
More file actions
264 lines (236 loc) · 7.88 KB
/
popup.lua
File metadata and controls
264 lines (236 loc) · 7.88 KB
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
local config = require("peekstack.config")
local fs = require("peekstack.util.fs")
local render = require("peekstack.ui.render")
local diagnostics_ui = require("peekstack.ui.diagnostics")
local keymaps = require("peekstack.ui.keymaps")
local notify = require("peekstack.util.notify")
local M = {}
--- Maximum number of lines to copy from the source buffer into a popup.
--- Files smaller than this are copied in full; larger files are windowed
--- around the target line to avoid blocking on huge buffers.
local MAX_VIEWPORT_LINES = 500
---Compute the line range to copy from source buffer.
---@param source_bufnr integer
---@param target_line integer 0-indexed target line
---@return integer start_line 0-indexed inclusive
---@return integer end_line 0-indexed exclusive (-1 means all)
---@return integer line_offset lines skipped from the start
local function compute_viewport(source_bufnr, target_line)
local total = vim.api.nvim_buf_line_count(source_bufnr)
if total <= MAX_VIEWPORT_LINES then
return 0, -1, 0
end
local half = math.floor(MAX_VIEWPORT_LINES / 2)
local start_line = math.max(0, target_line - half)
local end_line = math.min(total, start_line + MAX_VIEWPORT_LINES)
if end_line - start_line < MAX_VIEWPORT_LINES then
start_line = math.max(0, end_line - MAX_VIEWPORT_LINES)
end
return start_line, end_line, start_line
end
---@param bufnr integer
---@param source_bufnr integer
---@param opts? table
local function configure_popup_buffer(bufnr, source_bufnr, opts)
fs.configure_buffer(bufnr)
vim.bo[bufnr].filetype = vim.bo[source_bufnr].filetype
local editable = config.get().ui.popup.editable
if opts and opts.editable ~= nil then
editable = opts.editable
end
vim.bo[bufnr].modifiable = editable
vim.bo[bufnr].readonly = not editable
end
---@type integer
local next_id = 1
---@return { winid: integer, bufnr: integer, row: integer, col: integer }
local function capture_origin()
local winid = vim.api.nvim_get_current_win()
local bufnr = vim.api.nvim_win_get_buf(winid)
local cursor = vim.api.nvim_win_get_cursor(winid)
local is_popup = vim.w[winid].peekstack_popup_id ~= nil
return {
winid = winid,
bufnr = bufnr,
row = cursor[1],
col = cursor[2],
is_popup = is_popup,
}
end
---@param winid integer
---@param location PeekstackLocation
---@param line_offset? integer lines skipped from the start of the source buffer
local function set_cursor(winid, location, line_offset)
local line = (location.range.start.line or 0) + 1 - (line_offset or 0)
local col = (location.range.start.character or 0)
pcall(vim.api.nvim_win_set_cursor, winid, { math.max(1, line), col })
end
---Resolve buffer_mode from opts or config.
---@param opts table
---@return "copy"|"source"
local function resolve_buffer_mode(opts)
if opts.buffer_mode then
return opts.buffer_mode
end
return config.get().ui.popup.buffer_mode or "copy"
end
---@param winid integer
---@return { winid: integer, bufnr: integer, row: integer, col: integer, is_popup: boolean }
local function capture_origin_from_win(winid)
local bufnr = vim.api.nvim_win_get_buf(winid)
local cursor = vim.api.nvim_win_get_cursor(winid)
local is_popup = vim.w[winid].peekstack_popup_id ~= nil
return {
winid = winid,
bufnr = bufnr,
row = cursor[1],
col = cursor[2],
is_popup = is_popup,
}
end
---@param location PeekstackLocation
---@param opts? { buffer_mode?: "copy"|"source", title?: string|PeekstackTitleChunk[], editable?: boolean, ephemeral?: boolean, origin_winid?: integer, parent_popup_id?: integer }
---@return PeekstackPopupModel?
function M.create(location, opts)
opts = opts or {}
local origin = capture_origin()
if opts.origin_winid and vim.api.nvim_win_is_valid(opts.origin_winid) then
origin = capture_origin_from_win(opts.origin_winid)
end
local origin_is_popup = false
if
origin.is_popup == true
and vim.api.nvim_buf_is_valid(origin.bufnr)
and vim.bo[origin.bufnr].buftype == "nofile"
and vim.bo[origin.bufnr].bufhidden == "wipe"
then
origin_is_popup = true
elseif vim.api.nvim_buf_is_valid(origin.bufnr) then
local ft = vim.bo[origin.bufnr].filetype
if ft == "peekstack-stack" or ft == "peekstack-stack-help" then
origin_is_popup = true
end
end
local buffer_mode = resolve_buffer_mode(opts)
opts.buffer_mode = buffer_mode
local ok_buf, fname = pcall(fs.uri_to_fname, location.uri)
if not ok_buf or not fname then
notify.warn("Failed to resolve file: " .. tostring(location.uri))
return nil
end
local source_bufnr = vim.fn.bufadd(fname)
if source_bufnr == 0 then
notify.warn("Failed to add buffer: " .. fname)
return nil
end
local ok_load = pcall(vim.fn.bufload, source_bufnr)
if not ok_load then
notify.warn("Failed to load buffer: " .. fname)
return nil
end
local bufnr
local line_offset = 0
if buffer_mode == "source" then
-- Source mode: use the real buffer directly
bufnr = source_bufnr
else
-- Copy mode (default): create scratch buffer with copied lines
bufnr = vim.api.nvim_create_buf(false, true)
vim.bo[bufnr].modifiable = true
vim.bo[bufnr].readonly = false
local target_line = location.range.start.line or 0
local vp_start, vp_end, vp_offset = compute_viewport(source_bufnr, target_line)
line_offset = vp_offset
local ok_lines, lines = pcall(vim.api.nvim_buf_get_lines, source_bufnr, vp_start, vp_end, false)
if not ok_lines then
notify.warn("Failed to read buffer contents: " .. fname)
return nil
end
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
configure_popup_buffer(bufnr, source_bufnr, opts)
end
local ok_win, winid, win_opts = pcall(render.open, bufnr, location, opts)
if not ok_win or not winid then
if buffer_mode ~= "source" and vim.api.nvim_buf_is_valid(bufnr) then
pcall(vim.api.nvim_buf_delete, bufnr, { force = true })
end
notify.warn("Failed to open popup window")
return nil
end
local title = nil
local title_chunks = nil
if win_opts.title ~= nil then
if type(win_opts.title) == "table" then
title_chunks = win_opts.title
end
title = render.title_text(win_opts.title)
if title == "" then
title = nil
title_chunks = nil
end
end
if opts.title and opts.title ~= "" then
win_opts.title = opts.title
win_opts.title_pos = "center"
pcall(vim.api.nvim_win_set_config, winid, win_opts)
title = render.title_text(opts.title)
title_chunks = nil
if title == "" then
title = nil
end
end
set_cursor(winid, location, line_offset)
local id = next_id
next_id = next_id + 1
local popup = {
id = id,
bufnr = bufnr,
source_bufnr = source_bufnr,
winid = winid,
location = location,
origin = {
winid = origin.winid,
bufnr = origin.bufnr,
row = origin.row,
col = origin.col,
},
origin_bufnr = origin.bufnr,
origin_is_popup = origin_is_popup,
parent_popup_id = opts.parent_popup_id,
title = title,
title_chunks = title_chunks,
pinned = false,
buffer_mode = buffer_mode,
line_offset = line_offset,
created_at = os.time(),
last_active_at = vim.uv.now(),
ephemeral = opts.ephemeral or false,
win_opts = win_opts,
}
keymaps.apply_popup(popup)
vim.b[bufnr].peekstack_popup_id = id
vim.w[winid].peekstack_popup_id = id
popup.diagnostics = diagnostics_ui.decorate(popup)
return popup
end
---@param popup PeekstackPopupModel
---@return boolean
function M.focus(popup)
if popup.winid and vim.api.nvim_win_is_valid(popup.winid) then
vim.api.nvim_set_current_win(popup.winid)
return true
end
return false
end
---@param popup PeekstackPopupModel
function M.close(popup)
diagnostics_ui.clear(popup.diagnostics)
if popup.winid and vim.api.nvim_win_is_valid(popup.winid) then
vim.api.nvim_win_close(popup.winid, true)
end
end
--- Reset next_id (for testing).
function M._reset()
next_id = 1
end
return M