minimal extui fuzzy finder for neovim
at main 819 lines 21 kB view raw
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:group", { 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() 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 curpos[2] = math.max(curpos[2], promptlen) 459 460 if curpos[1] == promptidx + 1 and curpos[2] == promptlen + pos then 461 return 462 end 463 464 if pos < 0 then 465 -- reset to last known position 466 pos = curpos[2] - promptlen 467 end 468 469 curpos[1], curpos[2] = promptidx + 1, promptlen + pos 470 471 vim._with({ noautocmd = true }, function() 472 local ok, _ = pcall(vim.api.nvim_win_set_cursor, ui2.wins.cmd, curpos) 473 if not ok then 474 logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2])) 475 end 476 end) 477end 478 479local srow = 0 480 481function View:clear() 482 srow = self.picker.opts.bottom and 0 or 1 483 cmdline.erow = srow 484 self:setlines(0, -1, {}) 485end 486 487function View:promptpos() 488 promptidx = self.picker.opts.bottom and cmdline.erow or 0 489end 490 491function View:setlines(posstart, posend, lines) 492 -- update winheight to prevent wrong scroll when increasing from 1 493 local diff = #lines - (posend - posstart) 494 if diff ~= 0 then 495 local height = vim.api.nvim_win_text_height(ui2.wins.cmd, {}).all 496 local predicted = height + diff 497 self:updatewinheight(predicted) 498 end 499 500 before_draw_tick = get_changedtick() 501 vim.api.nvim_buf_set_lines(ui2.bufs.cmd, posstart, posend, false, lines) 502 last_draw_tick = get_changedtick() 503end 504 505local view_ns = vim.api.nvim_create_namespace("artio:view:ns") 506local ext_priority = { 507 prompt = 1, 508 info = 2, 509 select = 4, 510 marker = 8, 511 hl = 16, 512 icon = 32, 513 match = 64, 514} 515 516---@param id? string|integer 517---@param line integer 0-based 518---@param col integer 0-based 519---@param opts vim.api.keyset.set_extmark 520---@return integer 521function View:mark(id, line, col, opts) 522 if id and self.marks[id] then 523 vim._with({ noautocmd = true }, function() 524 vim.api.nvim_buf_del_extmark(ui2.bufs.cmd, view_ns, self.marks[id]) 525 end) 526 self.marks[id] = nil 527 end 528 529 opts.hl_mode = "combine" 530 opts.invalidate = true 531 532 local ok, result 533 vim._with({ noautocmd = true }, function() 534 ok, result = pcall(vim.api.nvim_buf_set_extmark, ui2.bufs.cmd, view_ns, line, col, opts) 535 end) 536 if not ok then 537 logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result)) 538 return -1 539 end 540 541 if id and result >= 0 then 542 self.marks[id] = result 543 end 544 545 return result 546end 547 548---@param p artio.Picker 549---@param info 'index'|'list'|string 550---@return string 551local function getpromptinfo(p, info) 552 if info == "index" then 553 return ("[%d]"):format(p.idx) 554 elseif info == "list" then 555 return ("(%d/%d)"):format(#p.matches, #p.items) 556 end 557 return "" 558end 559 560function View:drawprompt() 561 logdebug("drawprompt") 562 563 self:promptpos() 564 if promptlen > 0 and prompthl_id > 0 then 565 self:mark("prompthl", promptidx, 0, { hl_group = prompthl_id, end_col = promptlen, priority = ext_priority.prompt }) 566 self:mark("promptinfo", promptidx, 0, { 567 virt_text = { 568 { 569 table.concat( 570 vim 571 .iter(self.picker.opts.infolist) 572 :map(function(info) 573 return getpromptinfo(self.picker, info) 574 end) 575 :totable(), 576 " " 577 ), 578 "InfoText", 579 }, 580 }, 581 virt_text_pos = "eol_right_align", 582 priority = ext_priority.info, 583 }) 584 end 585end 586 587local offset = 0 588 589function View:updateoffset() 590 self.picker:fix() 591 if self.picker.idx == 0 then 592 offset = 0 593 return 594 end 595 596 local _offset = self.picker.idx - maxlistheight 597 if _offset > offset then 598 offset = _offset 599 elseif self.picker.idx <= offset then 600 offset = self.picker.idx - 1 601 end 602 603 offset = math.min(math.max(0, offset), math.max(0, #self.picker.matches - maxlistheight)) 604end 605 606local icon_pad = 2 607 608function View:showmatches() 609 local indent = vim.fn.strdisplaywidth(self.picker.opts.pointer) + 1 610 local prefix = (" "):rep(indent) 611 local icon_pad_str = (" "):rep(icon_pad) 612 613 self:updateoffset() 614 615 local lines = {} ---@type string[] 616 local hls = {} 617 local icons = {} ---@type ([string, string]|false)[] 618 local custom_hls = {} ---@type (artio.Picker.hl[]|false)[] 619 local marks = {} ---@type boolean[] 620 for i = 1 + offset, math.min(#self.picker.matches, maxlistheight + offset) do 621 local match = self.picker.matches[i] 622 local item = self.picker.items[match[1]] 623 624 local icon, icon_hl = item.icon, item.icon_hl 625 if not (icon and icon_hl) and vim.is_callable(self.picker.get_icon) then 626 icon, icon_hl = self.picker.get_icon(item) 627 item.icon, item.icon_hl = icon, icon_hl 628 end 629 icons[#icons + 1] = icon and { icon, icon_hl } or false 630 icon = icon and ("%s%s"):format(item.icon, icon_pad_str) or "" 631 632 local hl = item.hls 633 if not hl and vim.is_callable(self.picker.hl_item) then 634 hl = self.picker.hl_item(item) 635 item.hls = hl 636 end 637 custom_hls[#custom_hls + 1] = hl or false 638 639 marks[#marks + 1] = self.picker.marked[item.id] or false 640 641 lines[#lines + 1] = ("%s%s%s"):format(prefix, icon, item.text) 642 hls[#hls + 1] = match[2] 643 end 644 645 if not self.picker.opts.shrink then 646 for _ = 1, (maxlistheight - #lines) do 647 lines[#lines + 1] = "" 648 end 649 end 650 self:setlines(srow, cmdline.erow, lines) 651 cmdline.erow = srow + #lines 652 653 for i = 1, #lines do 654 local has_icon = icons[i] and icons[i][1] and true 655 local icon_indent = has_icon and (#icons[i][1] + icon_pad) or 0 656 657 if has_icon and icons[i][2] then 658 self:mark(nil, srow + i - 1, indent, { 659 end_col = indent + icon_indent, 660 hl_group = icons[i][2], 661 priority = ext_priority.icon, 662 }) 663 end 664 665 local line_hls = custom_hls[i] 666 if line_hls then 667 for j = 1, #line_hls do 668 local hl = line_hls[j] 669 self:mark(nil, srow + i - 1, indent + icon_indent + hl[1][1], { 670 end_col = indent + icon_indent + hl[1][2], 671 hl_group = hl[2], 672 priority = ext_priority.hl, 673 }) 674 end 675 end 676 677 if marks[i] then 678 self:mark(nil, srow + i - 1, indent - 1, { 679 virt_text = { { self.picker.opts.marker, "ArtioMark" } }, 680 virt_text_pos = "overlay", 681 priority = ext_priority.marker, 682 }) 683 self:mark(nil, srow + i - 1, 0, { 684 hl_group = "ArtioMarkLine", 685 hl_eol = true, 686 end_row = srow + i, 687 end_col = 0, 688 689 priority = ext_priority.marker, 690 }) 691 end 692 693 if hls[i] then 694 for j = 1, #hls[i] do 695 local col = indent + icon_indent + hls[i][j] 696 self:mark(nil, srow + i - 1, col, { 697 hl_group = "ArtioMatch", 698 end_col = col + 1, 699 priority = ext_priority.match, 700 }) 701 end 702 end 703 end 704end 705 706function View:hlselect() 707 self:softupdatepreview() 708 709 self.picker:fix() 710 local idx = self.picker.idx 711 if idx == 0 then 712 return 713 end 714 715 self:updateoffset() 716 local row = math.max(0, math.min(srow + (idx - offset), cmdline.erow) - 1) 717 718 self:mark("hlselect", row, 0, { 719 virt_text = { { self.picker.opts.pointer, "ArtioPointer" } }, 720 virt_text_pos = "overlay", 721 722 hl_group = "ArtioSel", 723 hl_eol = true, 724 end_row = row + 1, 725 end_col = 0, 726 727 priority = ext_priority.select, 728 }) 729end 730 731function View:togglepreview() 732 if self.preview_win then 733 self:closepreview() 734 return 735 end 736 737 self:updatepreview() 738end 739 740---@return integer 741---@return fun(win: integer)? 742function View:openpreview() 743 if self.picker.idx == 0 then 744 return -1 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 -1 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 width = vim.o.columns, 767 height = self.win.height, 768 col = 0, 769 row = vim.o.lines 770 - (self.win.height + cmdheight) 771 - ((winborder == "none" or winborder == "") and 0 or 2) 772 - (self.picker.win.hidestatusline and 0 or 1), 773 }, previewopts or {}) 774end 775 776function View:updatepreview() 777 local buf, on_win = self:openpreview() 778 if buf < 0 then 779 return 780 end 781 782 if not self.preview_win then 783 self.preview_win = vim.api.nvim_open_win(buf, false, self:previewconfig()) 784 else 785 vim.api.nvim_win_set_buf(self.preview_win, buf) 786 end 787 788 vim._with({ win = self.preview_win, noautocmd = true }, function() 789 vim.api.nvim_set_option_value("previewwindow", true, { scope = "local" }) 790 vim.api.nvim_set_option_value("eventignorewin", "all,-FileType", { scope = "local" }) 791 end) 792 793 if on_win and vim.is_callable(on_win) then 794 on_win(self.preview_win) 795 end 796end 797 798function View:softupdatepreview() 799 if self.picker.idx == 0 then 800 self:closepreview() 801 end 802 803 if not self.preview_win then 804 return 805 end 806 807 self:updatepreview() 808end 809 810function View:closepreview() 811 if not self.preview_win then 812 return 813 end 814 815 vim.api.nvim_win_close(self.preview_win, true) 816 self.preview_win = nil 817end 818 819return View