sessionizer/src/workspaces.lua
2026-02-03 11:42:52 -05:00

215 lines
5.1 KiB
Lua

local wezterm = require("wezterm")
local state = require("plugins.sessionizer.src.state")
local command_builder = require("plugins.sessionizer.src.command_builder")
local utils = require("plugins.sessionizer.src.utils")
local notify = require("plugins.sessionizer.src.notify")
local M = {}
local _scan_cache = {}
local _meta_lookup = {}
local CONTROL_PATTERN = "[%z\r\n]"
local function deep_copy(list)
local copy = {}
for i, v in ipairs(list or {}) do
local project_copy = {}
for k, val in pairs(v) do
project_copy[k] = val
end
copy[i] = project_copy
end
return copy
end
local function clear_scan_cache()
for k in pairs(_scan_cache) do
_scan_cache[k] = nil
end
for k in pairs(_meta_lookup) do
_meta_lookup[k] = nil
end
end
local function cache_list(list)
state.cached_directories = list
state.cached_checksum = utils.checksum(list)
end
local function scan_base(base)
local key = tostring(base.path) .. ":" .. tostring(base.max_depth or "default")
if _scan_cache[key] then
if state.profiler_enabled then
wezterm.log_info(("[sessionizer] scan_base %s: cache_hit=true"):format(tostring(base.path)))
end
return _scan_cache[key]
end
return utils.profile("scan_base " .. tostring(base.path), function()
local paths = utils.resolve_glob_all(base.path)
local cmd = command_builder.build_cmd(base)
local backend = cmd[1] or ""
local ok, out = utils.retry_command(cmd, 3, 200)
if not ok then
local message = "Failed to scan: " .. tostring(base.path)
wezterm.log_error(message)
state.last_scan_error = message
return {}
end
local choices = {}
local out_lines = 0
local skipped_control = 0
for _, line in ipairs(wezterm.split_by_newlines(out)) do
out_lines = out_lines + 1
if line ~= "" then
if line:find(CONTROL_PATTERN) then
skipped_control = skipped_control + 1
wezterm.log_error("[sessionizer] Skipping path with control characters: " .. line)
else
local id = line
local label = line:gsub(wezterm.home_dir, "~")
label = label:gsub(CONTROL_PATTERN, "")
table.insert(choices, {
id = id,
label = label,
})
_meta_lookup[id] = {
workspace = utils.basename(line),
title = utils.basename(line),
path = line,
}
end
end
end
_scan_cache[key] = choices
state.last_scan_error = nil
if state.profiler_enabled then
wezterm.log_info((
"[sessionizer] scan_base %s: cache_hit=false backend=%s glob_paths=%d lines=%d dirs=%d skipped_control=%d"
):format(
tostring(base.path),
tostring(backend),
#paths,
out_lines,
#choices,
skipped_control
))
end
return choices
end)
end
local function perform_scan()
return utils.profile("workspace_scan", function()
local list = {}
local seen = {}
local last_error = nil
local bases = #state.project_base
local total_dirs = 0
for _, base in ipairs(state.project_base) do
local ok, scanned = pcall(scan_base, base)
if ok and scanned then
total_dirs = total_dirs + #scanned
for _, folder in ipairs(scanned) do
if folder.id and not seen[folder.id] then
table.insert(list, folder)
seen[folder.id] = true
end
end
else
last_error = "Failed to scan project base: " .. tostring(base.path)
wezterm.log_error(last_error)
notify.show(last_error)
end
end
cache_list(list)
state.last_scan_error = last_error
if state.profiler_enabled then
wezterm.log_info((
"[sessionizer] workspace_scan: bases=%d total_dirs=%d unique_dirs=%d"
):format(bases, total_dirs, #list))
end
return list
end)
end
local function finish_scan(ok, result)
if not ok then
local message = "[sessionizer] Workspace scan failed: " .. tostring(result)
wezterm.log_error(message)
state.last_scan_error = message
notify.show(message)
end
state.scan_in_progress = false
if state.scan_pending then
state.scan_pending = false
M.refresh_async()
end
end
--- Kick off a background refresh of project directories without blocking the UI.
function M.refresh_async()
if state.scan_in_progress then
state.scan_pending = true
return
end
if state.config_changed then
clear_scan_cache()
state.config_changed = false
end
if #state.project_base == 0 then
cache_list({})
state.last_scan_error = "No project bases configured"
return
end
state.scan_in_progress = true
wezterm.time.call_after(0, function()
local ok, result = pcall(perform_scan)
finish_scan(ok, result)
end)
end
--- Get a list of cached project directories. Triggers a refresh if cache is empty/stale.
function M.all_dirs()
if state.cached_directories and utils.checksum(state.cached_directories) == state.cached_checksum then
return deep_copy(state.cached_directories)
end
if not state.scan_in_progress then
M.refresh_async()
end
return {}
end
--- Get cached project directories without triggering a refresh.
function M.cached_dirs()
if state.cached_directories and utils.checksum(state.cached_directories) == state.cached_checksum then
return deep_copy(state.cached_directories)
end
return {}
end
function M.get_metadata(id)
return _meta_lookup[id]
end
function M.clear_scan_cache()
clear_scan_cache()
end
return M