Skip to content

Commit e410cf7

Browse files
committed
feat(ui): use floating window for CursorSpinner
- Refactor CursorSpinner to render its spinner animation using a minimal-style floating window at the cursor position instead of an extmark-based virtual text overlay.
1 parent 9646053 commit e410cf7

File tree

1 file changed

+52
-10
lines changed

1 file changed

+52
-10
lines changed

lua/opencode/quick_chat/spinner.lua

Lines changed: 52 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
local config = require('opencode.config')
22
local Timer = require('opencode.ui.timer')
33

4+
---@class Timer
5+
---@field start function
6+
---@field stop function
7+
---@field is_running function
8+
49
---@class CursorSpinner
510
---@field buf integer
611
---@field row integer
@@ -11,6 +16,8 @@ local Timer = require('opencode.ui.timer')
1116
---@field timer Timer|nil
1217
---@field active boolean
1318
---@field frames string[]
19+
---@field float_win integer|nil
20+
---@field float_buf integer|nil
1421
local CursorSpinner = {}
1522
CursorSpinner.__index = CursorSpinner
1623

@@ -24,27 +31,54 @@ function CursorSpinner.new(buf, row, col)
2431
self.current_frame = 1
2532
self.timer = nil
2633
self.active = true
34+
self.float_win = nil
35+
self.float_buf = nil
2736

28-
self.frames = config.values.ui.loading_animation and config.values.ui.loading_animation.frames
37+
self.frames = config.ui.loading_animation and config.ui.loading_animation.frames
2938
or { '', '', '', '', '', '', '', '', '', '' }
3039

40+
self:create_float()
3141
self:render()
3242
self:start_timer()
3343
return self
3444
end
3545

36-
function CursorSpinner:render()
46+
function CursorSpinner:create_float()
3747
if not self.active or not vim.api.nvim_buf_is_valid(self.buf) then
3848
return
3949
end
4050

41-
local frame = ' ' .. self.frames[self.current_frame]
42-
self.extmark_id = vim.api.nvim_buf_set_extmark(self.buf, self.ns_id, self.row, self.col, {
43-
id = self.extmark_id,
44-
virt_text = { { frame .. ' ', 'Comment' } },
45-
virt_text_pos = 'overlay',
46-
right_gravity = false,
47-
})
51+
self.float_buf = vim.api.nvim_create_buf(false, true)
52+
53+
local win_config = self:get_float_config()
54+
55+
self.float_win = vim.api.nvim_open_win(self.float_buf, false, win_config)
56+
57+
vim.api.nvim_set_option_value('winhl', 'Normal:Comment', { win = self.float_win })
58+
vim.api.nvim_set_option_value('wrap', false, { win = self.float_win })
59+
end
60+
61+
function CursorSpinner:get_float_config()
62+
return {
63+
relative = 'cursor',
64+
width = 3,
65+
height = 1,
66+
row = 0,
67+
col = 2, -- 2 columns to the right of cursor
68+
style = 'minimal',
69+
border = 'rounded',
70+
focusable = false,
71+
zindex = 1000,
72+
}
73+
end
74+
75+
function CursorSpinner:render()
76+
if not self.active or not self.float_buf or not vim.api.nvim_buf_is_valid(self.float_buf) then
77+
return
78+
end
79+
80+
local frame = ' ' .. self.frames[self.current_frame] .. ' '
81+
vim.api.nvim_buf_set_lines(self.float_buf, 0, -1, false, { frame })
4882
end
4983

5084
function CursorSpinner:next_frame()
@@ -53,7 +87,7 @@ end
5387

5488
function CursorSpinner:start_timer()
5589
self.timer = Timer.new({
56-
interval = 100, -- 10 FPS like the main loading animation
90+
interval = 100, -- 10 FPS
5791
on_tick = function()
5892
if not self.active then
5993
return false
@@ -79,6 +113,14 @@ function CursorSpinner:stop()
79113
self.timer = nil
80114
end
81115

116+
if self.float_win and vim.api.nvim_win_is_valid(self.float_win) then
117+
pcall(vim.api.nvim_win_close, self.float_win, true)
118+
end
119+
120+
if self.float_buf and vim.api.nvim_buf_is_valid(self.float_buf) then
121+
pcall(vim.api.nvim_buf_delete, self.float_buf, { force = true })
122+
end
123+
82124
if self.extmark_id and vim.api.nvim_buf_is_valid(self.buf) then
83125
pcall(vim.api.nvim_buf_del_extmark, self.buf, self.ns_id, self.extmark_id)
84126
end

0 commit comments

Comments
 (0)