Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions lua/mason-core/spinner.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
local EventEmitter = require "mason-core.EventEmitter"
local settings = require "mason.settings"
local uv = vim.uv

local event = EventEmitter:new()

---@class Spinner
---@field id string
---@field private enabled boolean
---@field private active integer
---@field private index integer
local Spinner = {}
Spinner.__index = Spinner

---@type string[]
local texts = settings.current.ui.icons.spinner_texts

---@type integer
local texts_length = #texts

---@type uv.uv_timer_t|nil
local timer = nil

---@type table<string, fun()>
local ticks = {}

---@type integer spinner refresh interval
local INTERVAL = 80

local function start_tick(id, cb)
if not ticks[id] then
ticks[id] = cb
end

if timer then
return
end

timer = uv.new_timer()
assert(timer, "Failed to create spinner timer")
timer:start(
0,
INTERVAL,
vim.schedule_wrap(function()
vim.iter(ticks):each(function(_, f)
if f then
f()
end
end)
-- combine all spinners refresh event into one.
event:emit "change"
end)
)
end

local function stop_tick(id)
ticks[id] = nil
if next(ticks) == nil and timer then
timer:stop()
timer:close()
timer = nil
end
end

---Create a new spinner.
---
---@return Spinner
local function new(id)
return setmetatable({
id = id,
enabled = false,
active = 0,
index = 1,
}, Spinner)
end

---Start spinner.
function Spinner:start()
self.active = self.active + 1
if self.enabled then
return
end

self.enabled = true
--- refresh ui immediately.
event:emit "change"

start_tick(self.id, function()
self.index = (self.index % texts_length) + 1
end)
end

---Stop spinner.
function Spinner:stop()
self.active = self.active - 1
if not self.enabled or self.active > 0 then
return
end

stop_tick(self.id)

self.enabled = false
self.active = 0

event:emit "change"
end

function Spinner:__tostring()
return self.enabled and texts[self.index] or ""
end

---@type table<string, Spinner>
local instances = setmetatable({}, {
__index = function(self, k)
local v = rawget(self, k)
if v == nil then
v = new(k)
rawset(self, k, v)
end
return v
end,
})

return {
event = event,
instances = instances,
}
3 changes: 3 additions & 0 deletions lua/mason/settings.lua
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,9 @@ local DEFAULT_SETTINGS = {
---@since 1.0.0
-- The list icon to use for packages that are not installed.
package_uninstalled = "◍",
---@since 2.2.2
---Spinner frame texts
spinner_texts = { "⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏" },
},

keymaps = {
Expand Down
14 changes: 7 additions & 7 deletions lua/mason/ui/components/main/package_list.lua
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ local Ui = require "mason-core.ui"
local _ = require "mason-core.functional"
local p = require "mason.ui.palette"
local settings = require "mason.settings"
local spinner = require "mason-core.spinner"

local JsonSchema = require "mason.ui.components.json-schema"

Expand Down Expand Up @@ -205,15 +206,14 @@ local function Installed(state)
local registry_count = #state.info.registries
local text
if registry_count > 1 then
text = p.Comment(
is_all_registries_installed and ("updating %d registries "):format(registry_count)
or ("installing %d registries "):format(registry_count)
)
text = is_all_registries_installed and ("updating %d registries "):format(registry_count)
or ("installing %d registries "):format(registry_count)
else
text = p.Comment(is_all_registries_installed and "updating registry " or "installing registry ")
text = is_all_registries_installed and "updating registry " or "installing registry "
end
text = string.format("%s%s ", text, spinner.instances.registries)
return Ui.VirtualTextNode {
text,
p.Comment(text),
styling(
("%-4s"):format(math.floor(state.info.registry_update.percentage_complete * 100) .. "%")
),
Expand Down Expand Up @@ -270,7 +270,7 @@ local function InstallingPackageComponent(pkg, state)
{
pkg_state.has_failed and p.error(settings.current.ui.icons.package_uninstalled)
or p.highlight(settings.current.ui.icons.package_pending),
p.none(" " .. pkg.name),
p.none(string.format(" %s %s", pkg.name, spinner.instances[pkg.name])),
current_state,
pkg_state.latest_spawn and p.Comment((" $ %s"):format(pkg_state.latest_spawn)) or p.none "",
},
Expand Down
13 changes: 13 additions & 0 deletions lua/mason/ui/instance.lua
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ local display = require "mason-core.ui.display"
local notify = require "mason-core.notify"
local registry = require "mason-registry"
local settings = require "mason.settings"
local spinner = require "mason-core.spinner"

local Header = require "mason.ui.components.header"
local Help = require "mason.ui.components.help"
Expand Down Expand Up @@ -229,8 +230,10 @@ local function setup_handle(handle)
if handle.state == "QUEUED" then
mutate_package_grouping(handle.package, "queued", true)
elseif handle.state == "ACTIVE" then
spinner.instances[handle.package.name]:start()
mutate_package_grouping(handle.package, "installing", true)
elseif handle.state == "CLOSED" then
spinner.instances[handle.package.name]:stop()
mutate_state(function(state)
state.packages.states[handle.package.name].is_terminated = false
end)
Expand Down Expand Up @@ -605,6 +608,7 @@ local function setup_package(pkg)
pkg:on("install:handle", setup_handle)

pkg:on("install:success", function()
spinner.instances[pkg.name]:stop()
vim.schedule(function()
notify(("%s was successfully installed."):format(pkg.name))
end)
Expand All @@ -621,6 +625,7 @@ local function setup_package(pkg)
"install:failed",
---@param handle InstallHandle
function(handle)
spinner.instances[pkg.name]:stop()
if handle.is_terminated then
-- If installation was explicitly terminated - restore to "pristine" state
mutate_state(function(state)
Expand Down Expand Up @@ -682,6 +687,7 @@ local function setup_packages(packages)
end

registry:on("update:failed", function(errors)
spinner.instances.registries:stop()
mutate_state(function(state)
state.info.registry_update.percentage_complete = 0
state.info.registry_update.in_progress = false
Expand All @@ -690,6 +696,7 @@ registry:on("update:failed", function(errors)
end)

registry:on("update:success", function()
spinner.instances.registries:stop()
setup_packages(registry.get_all_packages())
update_registry_info()
check_new_package_versions()
Expand All @@ -708,6 +715,7 @@ registry:on("update:success", function()
end)

registry:on("update:start", function()
spinner.instances.registries:start()
mutate_state(function(state)
state.packages.outdated_packages = {}
state.info.registry_update.error = nil
Expand All @@ -716,6 +724,11 @@ registry:on("update:start", function()
end)
end)

spinner.event:on("change", function()
-- just refresh UI, nothing change.
mutate_state(function(state) end)
end)

registry:on("update:progress", function(finished, all)
mutate_state(function(state)
state.info.registry_update.percentage_complete = #finished / #all
Expand Down
66 changes: 66 additions & 0 deletions tests/mason-core/spinner_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
local spinner = require "mason-core.spinner"
local stub = require "luassert.stub"
local eq = assert.are.same

---@diagnostic disable: invisible
---@diagnostic disable: undefined-field

describe("spinner", function()
before_each(function()
spinner.instances.sp = nil
stub(vim.uv, "new_timer").returns {
start = function(_, _, _, callback)
callback()
end,
stop = function(_) end,
close = function(_) end,
}
stub(vim, "schedule_wrap").invokes(function(fn)
return fn
end)
end)

it("empty default", function()
local sp = spinner.instances.sp
eq(false, sp.enabled)
eq("", tostring(sp))
end)

it("start spinner", function()
local sp = spinner.instances.sp
stub(vim.uv, "new_timer").returns {
start = function(_, _, _, callback)
callback()
eq(true, sp.enabled)
eq(true, tostring(sp) ~= "")

callback()
eq(true, sp.enabled)
eq(true, tostring(sp) ~= "")
end,
stop = function(_) end,
close = function(_) end,
}
sp:start()
end)

it("remain start if call times start() > stop()", function()
local sp = spinner.instances.sp

sp:start()
eq(true, sp.enabled)
sp:start()
eq(true, sp.enabled)

sp:stop()
-- remain enable
eq(true, sp.enabled)

sp:stop()
eq(false, sp.enabled)

-- remian stop
sp:stop()
eq(false, sp.enabled)
end)
end)