🐻 minimal ui2 fuzzy finder for Neovim codeberg.org/comfysage/artio.nvim
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(view): async and redraw logic

robin c0cc4634 53688b0a

+163 -58
+163 -58
lua/artio/view.lua
··· 1 1 local cmdline = require("vim._extui.cmdline") 2 2 local ext = require("vim._extui.shared") 3 3 4 + local _log = {} 5 + local _loglevel = vim.log.levels.ERROR 6 + ---@private 7 + ---@param msgwrapped { [1]: string, [2]: string? } 8 + ---@param level? integer 9 + local 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 15 + end 16 + ---@private 17 + ---@param msg string 18 + local function logdebug(msg) 19 + logadd({ msg .. "\n" }) 20 + end 21 + ---@private 22 + ---@param msg string 23 + ---@param v any 24 + local function logdbg(msg, v) 25 + logdebug(string.format("%s: %s\n", msg, (vim.inspect(v)))) 26 + end 27 + ---@private 28 + ---@param msg string 29 + local function logerror(msg) 30 + logadd({ msg .. "\n", "ErrorMsg" }, vim.log.levels.ERROR) 31 + end 32 + 4 33 local prompt_hl_id = vim.api.nvim_get_hl_id_by_name("ArtioPrompt") 5 34 6 35 --- Set the 'cmdheight' and cmdline window height. Reposition message windows. ··· 28 57 ---@field picker artio.Picker 29 58 ---@field closed boolean 30 59 ---@field opts table<'win'|'buf'|'g',table<string,any>> 60 + ---@field marks table<string|integer, integer> 31 61 ---@field win artio.View.win 32 62 ---@field preview_win integer 33 63 local View = {} ··· 35 65 36 66 ---@param picker artio.Picker 37 67 function View:new(picker) 68 + ---@diagnostic disable-next-line: undefined-field 69 + if picker.log_level then 70 + ---@diagnostic disable-next-line: undefined-field 71 + _loglevel = picker.log_level 72 + end 73 + 38 74 return setmetatable({ 39 75 picker = picker, 40 76 closed = false, 41 77 opts = {}, 78 + marks = {}, 42 79 win = { 43 - height = 0, 80 + height = 1, 44 81 }, 45 82 }, View) 46 83 end ··· 50 87 51 88 local prompthl_id = -1 52 89 90 + --- gets updated before draw 91 + local before_draw_tick = 0 92 + --- gets updated after changedtick event 93 + local last_draw_tick = 0 94 + local function get_changedtick() 95 + return vim.api.nvim_buf_get_changedtick(ext.bufs.cmd) 96 + end 97 + 53 98 local cmdbuff = "" ---@type string Stored cmdline used to calculate translation offset. 54 99 local promptlen = 0 -- Current length of the last line in the prompt. 55 100 local promptidx = 0 ··· 71 116 cmdbuff = cmdbuff .. chunk[2] 72 117 end 73 118 lines[#lines] = ("%s%s"):format(promptstr, vim.fn.strtrans(cmdbuff)) 119 + 120 + self:promptpos() 74 121 self:setlines(promptidx, promptidx + 1, lines) 75 122 vim.fn.prompt_setprompt(ext.bufs.cmd, promptstr) 76 123 vim.schedule(function() 77 124 local ok, result = pcall(vim.api.nvim_buf_set_mark, ext.bufs.cmd, ":", promptidx + 1, 0, {}) 78 125 if not ok then 79 - vim.notify(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result), vim.log.levels.ERROR) 126 + logerror(("Failed to set mark %d:%d\n\t%s"):format(promptidx, promptlen, result)) 80 127 return 81 128 end 82 129 end) ··· 102 149 ext.msg.virt.last = { {}, {}, {}, {} } 103 150 104 151 self:clear() 152 + prompthl_id = hl_id 105 153 106 154 local cmd_text = "" 107 155 for _, chunk in ipairs(content) do 108 156 cmd_text = cmd_text .. chunk[2] 109 157 end 110 158 111 - self.picker:getmatches(cmd_text) 112 159 self:showmatches() 113 160 114 - self:promptpos() 115 161 self:setprompttext(content, ("%s%s%s"):format(firstc, prompt, (" "):rep(indent))) 116 162 self:updatecursor(pos) 117 163 118 - local height = math.max(1, vim.api.nvim_win_text_height(ext.wins.cmd, {}).all) 119 - height = math.min(height, self.win.height) 120 - win_config(ext.wins.cmd, false, height) 164 + self:updatewinheight() 121 165 122 - prompthl_id = hl_id 123 166 self:drawprompt() 124 167 self:hlselect() 168 + end 169 + 170 + ---@param predicted? integer The predicted height of the cmdline window 171 + function View:updatewinheight(predicted) 172 + local height = math.max(1, predicted or vim.api.nvim_win_text_height(ext.wins.cmd, {}).all) 173 + height = math.min(height, self.win.height) 174 + win_config(ext.wins.cmd, false, height) 125 175 end 126 176 127 177 function View:saveview() ··· 176 226 end 177 227 end 178 228 179 - local maxlistheight = 0 -- Max height of the matches list (`self.win.height - 1`) 229 + local maxlistheight = 1 -- Max height of the matches list (`self.win.height - 1`) 180 230 181 231 function View:on_resized() 232 + logdebug("on_resized") 233 + 182 234 if self.picker.win.height > 1 then 183 235 self.win.height = self.picker.win.height 184 236 else ··· 186 238 end 187 239 self.win.height = math.max(math.ceil(self.win.height), 1) 188 240 189 - maxlistheight = self.win.height - 1 241 + maxlistheight = math.max(self.win.height - 1, 1) 190 242 end 191 243 192 244 function View:open() 193 245 if not self.picker then 194 246 return 195 247 end 248 + _log = nil 249 + _log = {} 196 250 197 251 ext.check_targets() 198 252 199 253 self.prev_show = cmdline.cmdline_show 200 254 201 255 vim.schedule(function() 256 + self.augroup = vim.api.nvim_create_augroup("artio:group", { clear = true }) 257 + 202 258 vim.api.nvim_create_autocmd({ "CmdlineLeave", "ModeChanged" }, { 203 259 group = self.augroup, 204 260 once = true, ··· 207 263 end, 208 264 }) 209 265 210 - vim.api.nvim_create_autocmd("VimResized", { 266 + vim.api.nvim_create_autocmd({ "VimResized", "WinEnter" }, { 211 267 group = self.augroup, 212 268 callback = function() 213 269 self:on_resized() 214 270 end, 215 271 }) 216 272 273 + vim.api.nvim_create_autocmd("WinEnter", { 274 + group = self.augroup, 275 + callback = function() 276 + self:update(true) 277 + end, 278 + }) 279 + 217 280 vim.api.nvim_create_autocmd("TextChangedI", { 218 281 group = self.augroup, 219 282 buffer = ext.bufs.cmd, ··· 231 294 }) 232 295 end) 233 296 234 - self:on_resized() 235 - 236 297 cmdline.cmdline_show = function(...) 237 298 return self:show(...) 238 299 end 239 300 301 + cmdline.indent = 1 302 + cmdline.level = 0 303 + 240 304 self:saveview() 241 305 242 - cmdline.cmdline_show( 243 - { self.picker.defaulttext and { 0, self.picker.defaulttext } or nil }, 244 - -1, 245 - "", 246 - self.picker.prompttext, 247 - 1, 248 - 0, 249 - prompt_hl_id 250 - ) 306 + -- initial render 307 + self:trigger_show() 251 308 252 309 vim._with({ noautocmd = true }, function() 253 310 vim.api.nvim_set_current_win(ext.wins.cmd) ··· 255 312 256 313 self:setopts() 257 314 258 - vim.schedule(function() 259 - self:clear() 260 - self:updatecursor() 261 - end) 262 - 315 + -- start insert *before* registering events 316 + self:updatecursor() 263 317 vim._with({ noautocmd = true }, function() 264 318 vim.cmd.startinsert() 265 319 end) 266 320 267 - vim._with({ win = ext.wins.cmd, wo = { eventignorewin = "" } }, function() 268 - vim.api.nvim_exec_autocmds("WinEnter", {}) 321 + -- trigger after registering events 322 + vim.schedule(function() 323 + vim._with({ win = ext.wins.cmd, wo = { eventignorewin = "" } }, function() 324 + vim.api.nvim_exec_autocmds("WinEnter", {}) 325 + end) 269 326 end) 270 327 end 271 328 ··· 277 334 self:closepreview() 278 335 vim.schedule(function() 279 336 pcall(vim.api.nvim_del_augroup_by_id, self.augroup) 337 + pcall(vim.api.nvim_buf_detach, ext.bufs.cmd) 280 338 281 339 vim.cmd.stopinsert() 282 340 ··· 296 354 self.closed = true 297 355 298 356 self.picker:close() 357 + 358 + vim.api.nvim_echo(_log, true, {}) 299 359 end) 300 360 end 301 361 ··· 328 388 win_config(ext.wins.cmd, true, ext.cmdheight) 329 389 end 330 390 331 - function View:update() 391 + function View:trigger_show() 392 + logdebug("trigger_show") 393 + cmdline.cmdline_show( 394 + { { 0, self.picker.input } }, 395 + -1, 396 + "", 397 + self.picker.prompttext, 398 + cmdline.indent, 399 + cmdline.level, 400 + prompt_hl_id 401 + ) 402 + end 403 + 404 + ---@param force? boolean 405 + function View:update(force) 406 + if not force and before_draw_tick < last_draw_tick and before_draw_tick == get_changedtick() - 1 then 407 + logdebug("update (skip-redraw)") 408 + return self:drawprompt() 409 + end 410 + 411 + logdebug("update") 412 + 332 413 local text = vim.api.nvim_get_current_line() 333 414 text = text:sub(promptlen + 1) 334 415 335 - cmdline.cmdline_show({ { 0, text } }, -1, "", self.picker.prompttext, cmdline.indent, cmdline.level, prompt_hl_id) 416 + self.picker.input = text 417 + 418 + vim.schedule(coroutine.wrap(function() 419 + logdebug("getmatches") 420 + self.picker:getmatches() 421 + 422 + if self.closed then 423 + return 424 + end 425 + 426 + vim.schedule_wrap(self.trigger_show)(self) 427 + end)) 336 428 end 337 429 338 430 local curpos = { 0, 0 } -- Last drawn cursor position. absolute 339 431 ---@param pos? integer relative to prompt 340 432 function View:updatecursor(pos) 433 + logdebug("updatecursor") 434 + 341 435 self:promptpos() 342 436 343 437 if not pos or pos < 0 then ··· 361 455 vim._with({ noautocmd = true }, function() 362 456 local ok, _ = pcall(vim.api.nvim_win_set_cursor, ext.wins.cmd, curpos) 363 457 if not ok then 364 - vim.notify(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2]), vim.log.levels.ERROR) 458 + logerror(("Failed to set cursor %d:%d"):format(curpos[1], curpos[2])) 365 459 end 366 460 end) 367 461 end 368 462 369 463 function View:clear() 370 464 cmdline.srow = self.picker.opts.bottom and 0 or 1 371 - cmdline.erow = 0 465 + cmdline.erow = cmdline.srow 372 466 self:setlines(0, -1, {}) 373 467 end 374 468 ··· 377 471 end 378 472 379 473 function View:setlines(posstart, posend, lines) 380 - vim._with({ noautocmd = true }, function() 381 - vim.api.nvim_buf_set_lines(ext.bufs.cmd, posstart, posend, false, lines) 382 - end) 474 + -- update winheight to prevent wrong scroll when increasing from 1 475 + local diff = #lines - (posend - posstart) 476 + if diff ~= 0 then 477 + local height = vim.api.nvim_win_text_height(ext.wins.cmd, {}).all 478 + local predicted = height + diff 479 + self:updatewinheight(predicted) 480 + end 481 + 482 + before_draw_tick = get_changedtick() 483 + vim.api.nvim_buf_set_lines(ext.bufs.cmd, posstart, posend, false, lines) 484 + last_draw_tick = get_changedtick() 383 485 end 384 486 385 487 local view_ns = vim.api.nvim_create_namespace("artio:view:ns") ··· 393 495 match = 64, 394 496 } 395 497 498 + ---@param id? string|integer 396 499 ---@param line integer 0-based 397 500 ---@param col integer 0-based 398 501 ---@param opts vim.api.keyset.set_extmark 399 502 ---@return integer 400 - function View:mark(line, col, opts) 503 + function View:mark(id, line, col, opts) 504 + if id and self.marks[id] then 505 + vim._with({ noautocmd = true }, function() 506 + vim.api.nvim_buf_del_extmark(ext.bufs.cmd, view_ns, self.marks[id]) 507 + end) 508 + self.marks[id] = nil 509 + end 510 + 401 511 opts.hl_mode = "combine" 402 512 opts.invalidate = true 403 513 ··· 406 516 ok, result = pcall(vim.api.nvim_buf_set_extmark, ext.bufs.cmd, view_ns, line, col, opts) 407 517 end) 408 518 if not ok then 409 - vim.notify(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result), vim.log.levels.ERROR) 519 + logerror(("Failed to add extmark %d:%d\n\t%s"):format(line, col, result)) 410 520 return -1 411 521 end 412 522 523 + if id and result >= 0 then 524 + self.marks[id] = result 525 + end 526 + 413 527 return result 414 528 end 415 529 416 530 function View:drawprompt() 531 + logdebug("drawprompt") 532 + 533 + self:promptpos() 417 534 if promptlen > 0 and prompthl_id > 0 then 418 - self:mark(promptidx, 0, { hl_group = prompthl_id, end_col = promptlen, priority = ext_priority.prompt }) 419 - self:mark(promptidx, 0, { 535 + self:mark("prompthl", promptidx, 0, { hl_group = prompthl_id, end_col = promptlen, priority = ext_priority.prompt }) 536 + self:mark("promptinfo", promptidx, 0, { 420 537 virt_text = { 421 538 { 422 539 ("[%d] (%d/%d)"):format(self.picker.idx, #self.picker.matches, #self.picker.items), ··· 492 609 lines[#lines + 1] = "" 493 610 end 494 611 end 495 - cmdline.erow = cmdline.srow + #lines 496 612 self:setlines(cmdline.srow, cmdline.erow, lines) 613 + cmdline.erow = cmdline.srow + #lines 497 614 498 615 for i = 1, #lines do 499 616 local has_icon = icons[i] and icons[i][1] and true 500 617 local icon_indent = has_icon and (#icons[i][1] + icon_pad) or 0 501 618 502 619 if has_icon and icons[i][2] then 503 - self:mark(cmdline.srow + i - 1, indent, { 620 + self:mark(nil, cmdline.srow + i - 1, indent, { 504 621 end_col = indent + icon_indent, 505 622 hl_group = icons[i][2], 506 623 priority = ext_priority.icon, ··· 511 628 if line_hls then 512 629 for j = 1, #line_hls do 513 630 local hl = line_hls[j] 514 - self:mark(cmdline.srow + i - 1, indent + icon_indent + hl[1][1], { 631 + self:mark(nil, cmdline.srow + i - 1, indent + icon_indent + hl[1][1], { 515 632 end_col = indent + icon_indent + hl[1][2], 516 633 hl_group = hl[2], 517 634 priority = ext_priority.hl, ··· 520 637 end 521 638 522 639 if marks[i] then 523 - self:mark(cmdline.srow + i - 1, indent - 1, { 640 + self:mark(nil, cmdline.srow + i - 1, indent - 1, { 524 641 virt_text = { { self.picker.opts.marker, "ArtioMarker" } }, 525 642 virt_text_pos = "overlay", 526 643 priority = ext_priority.marker, ··· 530 647 if hls[i] then 531 648 for j = 1, #hls[i] do 532 649 local col = indent + icon_indent + hls[i][j] 533 - self:mark(cmdline.srow + i - 1, col, { 650 + self:mark(nil, cmdline.srow + i - 1, col, { 534 651 hl_group = "ArtioMatch", 535 652 end_col = col + 1, 536 653 priority = ext_priority.match, ··· 541 658 end 542 659 543 660 function View:hlselect() 544 - if self.select_ext then 545 - vim._with({ noautocmd = true }, function() 546 - vim.api.nvim_buf_del_extmark(ext.bufs.cmd, view_ns, self.select_ext) 547 - end) 548 - end 549 - 550 661 self:softupdatepreview() 551 662 552 663 self.picker:fix() ··· 557 668 558 669 self:updateoffset() 559 670 local row = math.max(0, math.min(cmdline.srow + (idx - offset), cmdline.erow) - 1) 560 - if row == promptidx then 561 - return 562 - end 563 671 564 - local extid = self:mark(row, 0, { 672 + self:mark("hlselect", row, 0, { 565 673 virt_text = { { self.picker.opts.pointer, "ArtioPointer" } }, 566 674 virt_text_pos = "overlay", 567 675 ··· 572 680 573 681 priority = ext_priority.select, 574 682 }) 575 - if extid ~= -1 then 576 - self.select_ext = extid 577 - end 578 683 end 579 684 580 685 function View:togglepreview()