馃惢 minimal ui2 fuzzy finder for Neovim
codeberg.org/comfysage/artio.nvim
1local cmdline = require("vim._core.ui2.cmdline")
2local ui2 = require("vim._core.ui2")
3
4local _log = {}
5local _loglevel = vim.log.levels.ERROR
6---@private
7---@param msgwrapped { [1]: string, [2]: string? }
8---@param level? integer
9local function logadd(msgwrapped, level)
10 level = level or vim.log.levels.DEBUG
11 if level < _loglevel then
12 return
13 end
14 _log[#_log + 1] = msgwrapped
15end
16---@private
17---@param msg string
18local function logdebug(msg)
19 logadd({ msg .. "\n" })
20end
21---@private
22---@param msg string
23---@param v any
24local function logdbg(msg, v)
25 logdebug(string.format("%s: %s\n", msg, (vim.inspect(v))))
26end
27---@private
28---@param msg string
29local function logerror(msg)
30 logadd({ msg .. "\n", "ErrorMsg" }, vim.log.levels.ERROR)
31end
32
33local prompt_hl_id = vim.api.nvim_get_hl_id_by_name("ArtioPrompt")
34
35---@class artio.View
36---@field picker artio.Picker
37---@field closed boolean
38---@field opts table<'win'|'buf'|'g',table<string,any>>
39---@field marks table<string|integer, integer>
40---@field win artio.View.win
41---@field preview_win integer
42local View = {}
43View.__index = View
44
45---@param picker artio.Picker
46function View:new(picker)
47 ---@diagnostic disable-next-line: undefined-field
48 if picker.log_level then
49 ---@diagnostic disable-next-line: undefined-field
50 _loglevel = picker.log_level
51 end
52
53 return setmetatable({
54 picker = picker,
55 closed = false,
56 opts = {},
57 marks = {},
58 win = {
59 height = 1,
60 },
61 }, View)
62end
63
64---@class artio.View.win
65---@field height integer
66
67local prompthl_id = -1
68
69--- gets updated before draw
70local before_draw_tick = 0
71--- gets updated after changedtick event
72local last_draw_tick = 0
73local function get_changedtick()
74 return vim.api.nvim_buf_get_changedtick(ui2.bufs.cmd)
75end
76
77local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset.
78local promptlen = 0 -- Current length of the last line in the prompt.
79local promptidx = 0
80--- Concatenate content chunks and set the text for the current row in the cmdline buffer.
81---
82---@param content CmdContent
83---@param prompt string
84function View:setprompttext(content, prompt)
85 local lines = {} ---@type string[]
86 for line in (prompt .. "\n"):gmatch("(.-)\n") do
87 lines[#lines + 1] = vim.fn.strtrans(line)
88 end
89
90 local promptstr = lines[#lines]
91 promptlen = #lines[#lines]
92
93 cmdbuff = ""
94 for _, chunk in ipairs(content) do
95 cmdbuff = cmdbuff .. chunk[2]
96 end
97 lines[#lines] = ("%s%s"):format(promptstr, vim.fn.strtrans(cmdbuff))
98
99 self:promptpos()
100 self:setlines(promptidx, promptidx + 1, lines)
101 if vim.fn.prompt_getprompt(ui2.bufs.cmd) ~= promptstr then
102 vim.fn.prompt_setprompt(ui2.bufs.cmd, promptstr)
103 end
104 vim.schedule(function()
105 local ok, result = pcall(vim.api.nvim_buf_set_mark, ui2.bufs.cmd, ":", promptidx + 1, promptlen, {})
106 if not ok then
107 logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result))
108 return
109 end
110 end)
111end
112
113--- Set the cmdline buffer text and cursor position.
114---
115---@param content CmdContent
116---@param pos? integer
117---@param firstc string
118---@param prompt string
119---@param indent integer
120---@param level integer
121---@param hl_id integer
122function View:show(content, pos, firstc, prompt, indent, level, hl_id)
123 cmdline.level, cmdline.indent = level, indent
124 if cmdline.highlighter and cmdline.highlighter.active then
125 cmdline.highlighter.active[ui2.bufs.cmd] = nil
126 end
127 if ui2.msg.cmd.msg_row ~= -1 then
128 ui2.msg.msg_clear()
129 end
130 ui2.msg.virt.last = { {}, {}, {}, {} }
131
132 self:clear()
133 prompthl_id = hl_id
134
135 local cmd_text = ""
136 for _, chunk in ipairs(content) do
137 cmd_text = cmd_text .. chunk[2]
138 end
139
140 self:showmatches()
141
142 self:setprompttext(content, ("%s%s%s"):format(firstc, prompt, (" "):rep(indent)))
143 self:updatecursor(pos)
144
145 self:updatewinheight()
146
147 self:drawprompt()
148 self:hlselect()
149end
150
151--- Set the 'cmdheight' and cmdline window height. Reposition message windows.
152---
153---@param win integer Cmdline window in the current tabpage.
154---@param hide boolean Whether to hide or show the window.
155---@param height integer (Text)height of the cmdline window.
156function View:win_config(win, hide, height)
157 if ui2.cmdheight == 0 and vim.api.nvim_win_get_config(win).hide ~= hide then
158 vim.api.nvim_win_set_config(win, { hide = hide, height = not hide and height or nil })
159 elseif vim.api.nvim_win_get_height(win) ~= height then
160 vim.api.nvim_win_set_height(win, height)
161 end
162
163 if not hide and self.picker.win.hidestatusline then
164 height = 0
165 end
166
167 if vim.o.cmdheight ~= height then
168 -- Avoid moving the cursor with 'splitkeep' = "screen", and altering the user
169 -- configured value with noautocmd.
170 vim._with({ noautocmd = true, o = { splitkeep = "screen" } }, function()
171 vim.o.cmdheight = height
172 end)
173 ui2.msg.set_pos()
174 end
175
176 if self.preview_win and vim.api.nvim_win_is_valid(self.preview_win) then
177 vim.api.nvim_win_set_config(self.preview_win, self:previewconfig())
178 end
179end
180
181---@param predicted? integer The predicted height of the cmdline window
182function View:updatewinheight(predicted)
183 local height = math.max(1, predicted or vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all)
184 height = math.min(height, self.win.height)
185 self:win_config(ui2.wins.cmd, false, height)
186end
187
188function View:saveview()
189 self.save = vim.fn.winsaveview()
190 self.prevwin = vim.api.nvim_get_current_win()
191end
192
193function View:restoreview()
194 vim.api.nvim_set_current_win(self.prevwin)
195 vim.fn.winrestview(self.save)
196end
197
198local ext_winhl = "Search:,CurSearch:,IncSearch:"
199
200---@param restore? boolean
201function View:setopts(restore)
202 local opts = {
203 win = {
204 eventignorewin = "all,-FileType,-InsertCharPre,-TextChangedI,-CursorMovedI",
205 winhighlight = "Normal:ArtioNormal," .. ext_winhl,
206 signcolumn = "no",
207 wrap = false,
208 },
209 buf = {
210 filetype = "artio-picker",
211 buftype = "prompt",
212 autocomplete = false,
213 },
214 g = {
215 showmode = false,
216 showcmd = false,
217 },
218 }
219
220 for level, o in pairs(opts) do
221 self.opts[level] = self.opts[level] or {}
222 local props = {
223 scope = level == "g" and "global" or "local",
224 buf = level == "buf" and ui2.bufs.cmd or nil,
225 win = level == "win" and ui2.wins.cmd or nil,
226 }
227
228 for name, value in pairs(o) do
229 if restore then
230 vim.api.nvim_set_option_value(name, self.opts[level][name], props)
231 else
232 self.opts[level][name] = vim.api.nvim_get_option_value(name, props)
233 vim.api.nvim_set_option_value(name, value, props)
234 end
235 end
236 end
237end
238
239local maxlistheight = 1 -- Max height of the matches list (`self.win.height - 1`)
240
241function View:on_resized()
242 logdebug("on_resized")
243
244 if self.picker.win.height > 1 then
245 self.win.height = self.picker.win.height
246 else
247 self.win.height = vim.o.lines * self.picker.win.height
248 end
249 self.win.height = math.max(math.ceil(self.win.height), 1)
250
251 maxlistheight = math.max(self.win.height - 1, 1)
252end
253
254function View:open()
255 if not self.picker then
256 return
257 end
258 _log = nil
259 _log = {}
260
261 ui2.check_targets()
262
263 vim.schedule(function()
264 self.augroup = vim.api.nvim_create_augroup("@artio.view", { clear = true })
265
266 vim.api.nvim_create_autocmd("CmdlineLeave", {
267 group = self.augroup,
268 once = true,
269 callback = function()
270 self:close()
271 end,
272 })
273
274 vim.api.nvim_create_autocmd("ModeChanged", {
275 group = self.augroup,
276 callback = function(ev)
277 if string.match(ev.match, "^i:") then
278 self:close()
279 end
280 end,
281 })
282
283 vim.api.nvim_create_autocmd({ "VimResized", "WinEnter" }, {
284 group = self.augroup,
285 callback = function()
286 self:on_resized()
287 end,
288 })
289
290 vim.api.nvim_create_autocmd("WinEnter", {
291 group = self.augroup,
292 callback = function()
293 self:update(true)
294 end,
295 })
296
297 vim.api.nvim_create_autocmd("TextChangedI", {
298 group = self.augroup,
299 buffer = ui2.bufs.cmd,
300 callback = function()
301 self:update()
302 end,
303 })
304
305 vim.api.nvim_create_autocmd("CursorMovedI", {
306 group = self.augroup,
307 buffer = ui2.bufs.cmd,
308 callback = function()
309 self:updatecursor()
310 end,
311 })
312 end)
313
314 cmdline.prompt = false
315 cmdline.srow = 0
316 cmdline.indent = 1
317 cmdline.level = 1
318
319 self:saveview()
320
321 -- initial render
322 self:trigger_show()
323
324 vim._with({ noautocmd = true }, function()
325 vim.api.nvim_set_current_win(ui2.wins.cmd)
326 end)
327
328 self:setopts()
329
330 -- start insert *before* registering events
331 self:updatecursor(#cmdbuff)
332 vim._with({ noautocmd = true }, function()
333 vim.cmd.startinsert({ bang = true })
334 end)
335
336 -- trigger after registering events
337 vim.schedule(function()
338 vim._with({ win = ui2.wins.cmd, wo = { eventignorewin = "" } }, function()
339 vim.api.nvim_exec_autocmds("WinEnter", {})
340 end)
341 end)
342end
343
344function View:close()
345 if self.closed then
346 return
347 end
348 self:closepreview()
349 vim.schedule(function()
350 pcall(vim.api.nvim_del_augroup_by_id, self.augroup)
351 pcall(vim.api.nvim_buf_detach, ui2.bufs.cmd)
352
353 vim.cmd.stopinsert()
354
355 -- prepare state
356 self:setopts(true)
357
358 -- reset state
359 self:clear()
360 cmdline.srow = 0
361 cmdline.erow = 0
362
363 -- restore ui
364 self:hide()
365 self:restoreview()
366 vim.cmd.redraw()
367
368 self.closed = true
369
370 self.picker:close()
371
372 vim.api.nvim_echo(_log, true, {})
373 end)
374end
375
376function View:hide()
377 vim.fn.clearmatches(ui2.wins.cmd) -- Clear matchparen highlights.
378 vim.api.nvim_win_set_cursor(ui2.wins.cmd, { 1, 0 })
379 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
380
381 local clear = vim.schedule_wrap(function(was_prompt)
382 -- Avoid clearing prompt window when it is re-entered before the next event
383 -- loop iteration. E.g. when a non-choice confirm button is pressed.
384 if was_prompt and not cmdline.prompt then
385 pcall(function()
386 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, 0, -1, false, {})
387 vim.api.nvim_buf_set_lines(ui2.bufs.dialog, 0, -1, false, {})
388 vim.api.nvim_win_set_config(ui2.wins.dialog, { hide = true })
389 vim.on_key(nil, ui2.msg.dialog_on_key)
390 end)
391 end
392 -- Messages emitted as a result of a typed command are treated specially:
393 -- remember if the cmdline was used this event loop iteration.
394 -- NOTE: Message event callbacks are themselves scheduled, so delay two iterations.
395 vim.schedule(function()
396 cmdline.level = -1
397 end)
398 end)
399 clear(cmdline.prompt)
400
401 cmdline.prompt, cmdline.level = false, 0
402 self:win_config(ui2.wins.cmd, true, ui2.cmdheight)
403end
404
405function View:trigger_show()
406 logdebug("trigger_show")
407 local input
408 if self.picker.live then
409 input = self.picker.liveinput
410 else
411 input = self.picker.input
412 end
413 self:show({ { 0, input } }, -1, "", self.picker.prompttext, cmdline.indent, cmdline.level, prompt_hl_id)
414end
415
416---@param force? boolean
417function View:update(force)
418 if not force and before_draw_tick < last_draw_tick and before_draw_tick == get_changedtick() - 1 then
419 logdebug("update (skip-redraw)")
420 return self:drawprompt()
421 end
422
423 logdebug("update")
424
425 local text = vim.api.nvim_get_current_line()
426 text = text:sub(promptlen + 1)
427
428 if self.picker.live then
429 self.picker.liveinput = text
430 else
431 self.picker.input = text
432 end
433
434 vim.schedule(coroutine.wrap(function()
435 logdebug("getmatches")
436 self.picker:getmatches()
437
438 if self.closed then
439 return
440 end
441
442 vim.schedule_wrap(self.trigger_show)(self)
443 end))
444end
445
446local curpos = { 0, 0 } -- Last drawn cursor position. absolute
447---@param pos? integer relative to prompt
448function View:updatecursor(pos)
449 logdebug("updatecursor")
450
451 self:promptpos()
452
453 if not pos or pos < 0 then
454 local cursorpos = vim.api.nvim_win_get_cursor(ui2.wins.cmd)
455 pos = cursorpos[2] - promptlen
456 end
457
458 -- set cursor pos to *at least* the prompt length
459 curpos[2] = math.max(curpos[2], promptlen)
460
461 if curpos[1] == promptidx + 1 and curpos[2] == promptlen + pos then
462 return
463 end
464
465 if pos < 0 then
466 -- reset to last known position
467 pos = curpos[2] - promptlen
468 end
469
470 curpos[1], curpos[2] = promptidx + 1, promptlen + pos
471
472 vim._with({ noautocmd = true }, function()
473 local ok, _ = pcall(vim.api.nvim_win_set_cursor, ui2.wins.cmd, curpos)
474 if not ok then
475 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2]))
476 end
477 end)
478end
479
480local srow = 0
481
482function View:clear()
483 srow = self.picker.opts.bottom and 0 or 1
484 cmdline.erow = srow
485 self:setlines(0, -1, {})
486end
487
488function View:promptpos()
489 promptidx = self.picker.opts.bottom and cmdline.erow or 0
490end
491
492function View:setlines(posstart, posend, lines)
493 -- update winheight to prevent wrong scroll when increasing from 1
494 local diff = #lines - (posend - posstart)
495 if diff ~= 0 then
496 local height = vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all
497 local predicted = height + diff
498 self:updatewinheight(predicted)
499 end
500
501 before_draw_tick = get_changedtick()
502 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, posstart, posend, false, lines)
503 last_draw_tick = get_changedtick()
504end
505
506local view_ns = vim.api.nvim_create_namespace("@artio.view.ns")
507local ext_priority = {
508 prompt = 1,
509 info = 2,
510 select = 4,
511 marker = 8,
512 hl = 16,
513 icon = 32,
514 match = 64,
515}
516
517---@param id? string|integer
518---@param line integer 0-based
519---@param col integer 0-based
520---@param opts vim.api.keyset.set_extmark
521---@return integer
522function View:mark(id, line, col, opts)
523 if id and self.marks[id] then
524 vim._with({ noautocmd = true }, function()
525 vim.api.nvim_buf_del_extmark(ui2.bufs.cmd, view_ns, self.marks[id])
526 end)
527 self.marks[id] = nil
528 end
529
530 opts.hl_mode = "combine"
531 opts.invalidate = true
532
533 local ok, result
534 vim._with({ noautocmd = true }, function()
535 ok, result = pcall(vim.api.nvim_buf_set_extmark, ui2.bufs.cmd, view_ns, line, col, opts)
536 end)
537 if not ok then
538 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result))
539 return -1
540 end
541
542 if id and result >= 0 then
543 self.marks[id] = result
544 end
545
546 return result
547end
548
549---@param p artio.Picker
550---@param info 'index'|'list'|string
551---@return string
552local function getpromptinfo(p, info)
553 if info == "index" then
554 return ("[%d]"):format(p.idx)
555 elseif info == "list" then
556 return ("(%d/%d)"):format(#p.matches, #p.items)
557 end
558 return ""
559end
560
561function View:drawprompt()
562 logdebug("drawprompt")
563
564 self:promptpos()
565 if promptlen > 0 and prompthl_id > 0 then
566 self:mark("prompthl", promptidx, 0, { hl_group = prompthl_id, end_col = promptlen, priority = ext_priority.prompt })
567 self:mark("promptinfo", promptidx, 0, {
568 virt_text = {
569 {
570 table.concat(
571 vim
572 .iter(self.picker.opts.infolist)
573 :map(function(info)
574 return getpromptinfo(self.picker, info)
575 end)
576 :totable(),
577 " "
578 ),
579 "InfoText",
580 },
581 },
582 virt_text_pos = "eol_right_align",
583 priority = ext_priority.info,
584 })
585 end
586end
587
588local offset = 0
589
590function View:updateoffset()
591 self.picker:fix()
592 if self.picker.idx == 0 then
593 offset = 0
594 return
595 end
596
597 local _offset = self.picker.idx - maxlistheight
598 if _offset > offset then
599 offset = _offset
600 elseif self.picker.idx <= offset then
601 offset = self.picker.idx - 1
602 end
603
604 offset = math.min(math.max(0, offset), math.max(0, #self.picker.matches - maxlistheight))
605end
606
607local icon_pad = 2
608
609function View:showmatches()
610 local indent = vim.fn.strdisplaywidth(self.picker.opts.pointer) + 1
611 local prefix = (" "):rep(indent)
612 local icon_pad_str = (" "):rep(icon_pad)
613
614 self:updateoffset()
615
616 local lines = {} ---@type string[]
617 local hls = {}
618 local icons = {} ---@type ([string, string]|false)[]
619 local custom_hls = {} ---@type (artio.Picker.hl[]|false)[]
620 local marks = {} ---@type boolean[]
621 for i = 1 + offset, math.min(#self.picker.matches, maxlistheight + offset) do
622 local match = self.picker.matches[i]
623 local item = self.picker.items[match[1]]
624
625 local icon, icon_hl = item.icon, item.icon_hl
626 if not (icon and icon_hl) and vim.is_callable(self.picker.get_icon) then
627 icon, icon_hl = self.picker.get_icon(item)
628 item.icon, item.icon_hl = icon, icon_hl
629 end
630 icons[#icons + 1] = icon and { icon, icon_hl } or false
631 icon = icon and ("%s%s"):format(item.icon, icon_pad_str) or ""
632
633 local hl = item.hls
634 if not hl and vim.is_callable(self.picker.hl_item) then
635 hl = self.picker.hl_item(item)
636 item.hls = hl
637 end
638 custom_hls[#custom_hls + 1] = hl or false
639
640 marks[#marks + 1] = self.picker.marked[item.id] or false
641
642 lines[#lines + 1] = ("%s%s%s"):format(prefix, icon, item.text)
643 hls[#hls + 1] = match[2]
644 end
645
646 if not self.picker.opts.shrink then
647 for _ = 1, (maxlistheight - #lines) do
648 lines[#lines + 1] = ""
649 end
650 end
651 self:setlines(srow, cmdline.erow, lines)
652 cmdline.erow = srow + #lines
653
654 for i = 1, #lines do
655 local has_icon = icons[i] and icons[i][1] and true
656 local icon_indent = has_icon and (#icons[i][1] + icon_pad) or 0
657
658 if has_icon and icons[i][2] then
659 self:mark(nil, srow + i - 1, indent, {
660 end_col = indent + icon_indent,
661 hl_group = icons[i][2],
662 priority = ext_priority.icon,
663 })
664 end
665
666 local line_hls = custom_hls[i]
667 if line_hls then
668 for j = 1, #line_hls do
669 local hl = line_hls[j]
670 self:mark(nil, srow + i - 1, indent + icon_indent + hl[1][1], {
671 end_col = indent + icon_indent + hl[1][2],
672 hl_group = hl[2],
673 priority = ext_priority.hl,
674 })
675 end
676 end
677
678 if marks[i] then
679 self:mark(nil, srow + i - 1, indent - 1, {
680 virt_text = { { self.picker.opts.marker, "ArtioMark" } },
681 virt_text_pos = "overlay",
682 priority = ext_priority.marker,
683 })
684 self:mark(nil, srow + i - 1, 0, {
685 hl_group = "ArtioMarkLine",
686 hl_eol = true,
687 end_row = srow + i,
688 end_col = 0,
689
690 priority = ext_priority.marker,
691 })
692 end
693
694 if hls[i] then
695 for j = 1, #hls[i] do
696 local col = indent + icon_indent + hls[i][j]
697 self:mark(nil, srow + i - 1, col, {
698 hl_group = "ArtioMatch",
699 end_col = col + 1,
700 priority = ext_priority.match,
701 })
702 end
703 end
704 end
705end
706
707function View:hlselect()
708 self:softupdatepreview()
709
710 self.picker:fix()
711 local idx = self.picker.idx
712 if idx == 0 then
713 return
714 end
715
716 self:updateoffset()
717 local row = math.max(0, math.min(srow + (idx - offset), cmdline.erow) - 1)
718
719 self:mark("hlselect", row, 0, {
720 virt_text = { { self.picker.opts.pointer, "ArtioPointer" } },
721 virt_text_pos = "overlay",
722
723 hl_group = "ArtioSel",
724 hl_eol = true,
725 end_row = row + 1,
726 end_col = 0,
727
728 priority = ext_priority.select,
729 })
730end
731
732function View:togglepreview()
733 if self.preview_win then
734 self:closepreview()
735 return
736 end
737
738 self:updatepreview()
739end
740
741---@return {buf?:integer, pos?:[integer,integer], pos_end?:[integer,integer]}?
742function View:openpreview()
743 if self.picker.idx == 0 then
744 return
745 end
746
747 local match = self.picker.matches[self.picker.idx]
748 local item = self.picker.items[match[1]]
749
750 if not item or not (self.picker.preview_item and vim.is_callable(self.picker.preview_item)) then
751 return
752 end
753
754 return self.picker.preview_item(item.v)
755end
756
757function View:previewconfig()
758 local previewopts = self.picker.win.preview_opts
759 and vim.is_callable(self.picker.win.preview_opts)
760 and self.picker.win.preview_opts(self)
761 local cmdheight = vim.api.nvim_win_get_height(ui2.wins.cmd)
762
763 local winborder = previewopts and previewopts.border or vim.o.winborder
764 return vim.tbl_extend("force", {
765 relative = "editor",
766 focusable = false,
767 width = vim.o.columns,
768 height = self.win.height,
769 col = 0,
770 row = vim.o.lines
771 - (self.win.height + cmdheight)
772 - ((winborder == "none" or winborder == "") and 0 or 2)
773 - (self.picker.win.hidestatusline and 0 or 1),
774 }, previewopts or {})
775end
776
777function View:updatepreview()
778 local pr = self:openpreview()
779 if not pr or not pr.buf then
780 return
781 end
782
783 if not self.preview_win then
784 self.preview_win = vim.api.nvim_open_win(pr.buf, false, self:previewconfig())
785 else
786 vim.api.nvim_win_set_buf(self.preview_win, pr.buf)
787 end
788
789 vim._with({ win = self.preview_win, noautocmd = true }, function()
790 vim.api.nvim_set_option_value("previewwindow", true, { scope = "local" })
791 vim.api.nvim_set_option_value("eventignorewin", "all,-FileType", { scope = "local" })
792
793 local sameline = pr.pos ~= nil and pr.pos_end == nil or pr.pos_end[1] == pr.pos[1]
794 vim.api.nvim_set_option_value("cursorline", sameline, { scope = "local" })
795
796 if pr.pos then
797 vim.api.nvim_win_set_cursor(self.preview_win, pr.pos)
798 end
799 end)
800end
801
802function View:softupdatepreview()
803 if self.picker.idx == 0 then
804 self:closepreview()
805 end
806
807 if not self.preview_win then
808 return
809 end
810
811 self:updatepreview()
812end
813
814function View:closepreview()
815 if not self.preview_win then
816 return
817 end
818
819 vim.api.nvim_win_close(self.preview_win, true)
820 self.preview_win = nil
821end
822
823return View