autocmds

Source: autocmds · updated 2026-06-11 · 🔒 secret gist

Synced verbatim from gist.github.com/bl9.

-- Tabline that shows ws_name tab-local variable when set, else falls back to
-- the buffer name. Used by LoadWorkspace to label tabs by folder name.
_G.WsTabline = function()
  local s = ""
  local ntabs = vim.fn.tabpagenr("$")
  for i = 1, ntabs do
    local label = vim.fn.gettabvar(i, "ws_name", "")
    if label == "" then
      local bufs = vim.fn.tabpagebuflist(i)
      label = vim.fn.fnamemodify(vim.fn.bufname(bufs[1]), ":t")
      if label == "" then label = "[No Name]" end
    end
    if i == vim.fn.tabpagenr() then
      s = s .. "%#TabLineSel# " .. i .. ":" .. label .. " "
    else
      s = s .. "%#TabLine# " .. i .. ":" .. label .. " "
    end
  end
  return s .. "%#TabLineFill#"
end
vim.opt.tabline = "%!v:lua.WsTabline()"
vim.opt.showtabline = 2

-- Cycle tabs with Alt-l / Alt-h
vim.keymap.set("n", "<A-l>", "<cmd>tabnext<cr>", { desc = "Next tab" })
vim.keymap.set("n", "<A-h>", "<cmd>tabprev<cr>", { desc = "Prev tab" })

-- Fuzzy-pick a tab with <leader>T (Alt+t unreliable on macOS)
vim.keymap.set("n", "<leader>T", function()
  local tabs = {}
  for i = 1, vim.fn.tabpagenr("$") do
    local name = vim.fn.gettabvar(i, "ws_name", "")
    if name == "" then
      local bufs = vim.fn.tabpagebuflist(i)
      name = vim.fn.fnamemodify(vim.fn.bufname(bufs[1]), ":t")
      if name == "" then name = "[No Name]" end
    end
    table.insert(tabs, { text = i .. ": " .. name, idx = i })
  end
  Snacks.picker.pick({
    title = "Tabs",
    items = tabs,
    format = function(item) return { { item.text } } end,
    confirm = function(picker, item)
      picker:close()
      if item then vim.cmd("tabn " .. item.idx) end
    end,
  })
end, { desc = "Pick tab" })

local function open_vscode_workspace(workspace_file)
  local f = io.open(workspace_file, "r")
  if not f then
    vim.notify("Could not open workspace file: " .. workspace_file, vim.log.levels.ERROR)
    return
  end
  local content = f:read("*a")
  f:close()

  local ok, ws = pcall(vim.json.decode, content)
  if not ok or not ws.folders then
    vim.notify("Failed to parse workspace file", vim.log.levels.ERROR)
    return
  end

  local ws_dir = vim.fn.fnamemodify(workspace_file, ":p:h")
  local paths = {}

  for _, folder in ipairs(ws.folders) do
    local path = folder.path
    if not vim.startswith(path, "/") then
      path = vim.fn.simplify(ws_dir .. "/" .. path)
    end
    if vim.fn.isdirectory(path) == 0 then
      vim.notify("Skipping missing directory: " .. path, vim.log.levels.WARN)
    else
      table.insert(paths, path)
    end
  end

  if #paths == 0 then return end

  for i, path in ipairs(paths) do
    local name = vim.fn.fnamemodify(path, ":t")
    if i == 1 then
      vim.cmd("tcd " .. vim.fn.fnameescape(path))
    else
      vim.cmd("tabnew")
      vim.cmd("tcd " .. vim.fn.fnameescape(path))
    end
    vim.t.ws_name = name
  end

  -- Open tab 1's explorer immediately; lazily open the rest on first TabEnter.
  -- Opening multiple Snacks pickers in the same event loop causes BufWinEnter
  -- nesting (E218) due to snacks' internal nvim_win_set_buf autocmd cascade.
  vim.cmd("tabfirst")

  local pending = {}
  for i = 2, #paths do
    pending[i] = paths[i]
  end

  local aug = vim.api.nvim_create_augroup("LoadWorkspaceExplorers", { clear = true })
  vim.api.nvim_create_autocmd("TabEnter", {
    group = aug,
    callback = function()
      local tabnr = vim.fn.tabpagenr()
      if pending[tabnr] then
        local p = pending[tabnr]
        pending[tabnr] = nil
        vim.schedule(function()
          Snacks.explorer.open({ cwd = p })
        end)
        -- Clean up autocmd once all tabs have been visited
        if vim.tbl_isempty(pending) then
          vim.api.nvim_del_augroup_by_id(aug)
        end
      end
    end,
  })

  -- Open the first tab's explorer last so it ends up focused
  vim.schedule(function()
    Snacks.explorer.open({ cwd = paths[1] })
  end)
end

vim.api.nvim_create_user_command("LoadWorkspace", function(opts)
  open_vscode_workspace(opts.args)
end, { nargs = 1, complete = "file" })