Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Next Next commit
feat(integrations): add snacks.explorer file explorer support (file-s…
…end)

This PR aims to add an equivalent "ClaudeCodeSend" integration for
snacks.explorer, as what already exists for nvim-tree, neo-tree and
oil.nvim

Snacks.explorer seems to have become the default LazyVim file explorer,
and has gained a fair bit of traction.

- Add snacks_picker_list filetype detection across the codebase
- Implement _get_snacks_explorer_selection() to handle file selection
- Support both individual selection and current file fallback
- Handle visual mode for snacks.explorer in visual commands
- Add comprehensive test coverage for the new integration
  • Loading branch information
andresthor committed Jul 3, 2025
commit 00aa785b42b886819f1d3d63f7093fa52121b277
1 change: 1 addition & 0 deletions lua/claudecode/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -611,6 +611,7 @@ function M._create_commands()
local is_tree_buffer = current_ft == "NvimTree"
or current_ft == "neo-tree"
or current_ft == "oil"
or current_ft == "snacks_picker_list"
or string.match(current_bufname, "neo%-tree")
or string.match(current_bufname, "NvimTree")

Expand Down
60 changes: 59 additions & 1 deletion lua/claudecode/integrations.lua
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
-- Tree integration module for ClaudeCode.nvim
-- Handles detection and selection of files from nvim-tree, neo-tree, and oil.nvim
-- Handles detection and selection of files from nvim-tree, neo-tree, oil.nvim and snacks.explorer
-- @module claudecode.integrations
local M = {}

Expand All @@ -16,6 +16,8 @@ function M.get_selected_files_from_tree()
return M._get_neotree_selection()
elseif current_ft == "oil" then
return M._get_oil_selection()
elseif current_ft == "snacks_picker_list" then
return M._get_snacks_explorer_selection()
else
return nil, "Not in a supported tree buffer (current filetype: " .. current_ft .. ")"
end
Expand Down Expand Up @@ -261,4 +263,60 @@ function M._get_oil_selection()
return {}, "No file found under cursor"
end

--- Get selected files from snacks.explorer
--- Uses the picker API to get the current selection
--- @return table files List of file paths
--- @return string|nil error Error message if operation failed
function M._get_snacks_explorer_selection()
local snacks_ok, snacks = pcall(require, "snacks")
if not snacks_ok or not snacks.picker then
return {}, "snacks.nvim not available"
end

-- Get the current explorer picker
local explorers = snacks.picker.get({ source = "explorer" })
if not explorers or #explorers == 0 then
return {}, "No active snacks.explorer found"
end

-- Get the first (and likely only) explorer instance
local explorer = explorers[1]
if not explorer then
return {}, "No active snacks.explorer found"
end

local files = {}

-- Check if there are selected items
local selected = explorer:selected({ fallback = false })
if selected and #selected > 0 then
-- Process selected items
for _, item in ipairs(selected) do
-- Try different possible fields for file path
local file_path = item.file or item.path or (item.item and item.item.file) or (item.item and item.item.path)
if file_path and file_path ~= "" then
table.insert(files, file_path)
end
end
if #files > 0 then
return files, nil
end
end

-- Fall back to current item under cursor
local current = explorer:current({ resolve = true })
if current then
-- Try different possible fields for file path
local file_path = current.file
or current.path
or (current.item and current.item.file)
or (current.item and current.item.path)
if file_path and file_path ~= "" then
return { file_path }, nil
end
end

return {}, "No file found under cursor"
end

return M
1 change: 1 addition & 0 deletions lua/claudecode/tools/open_file.lua
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ local function find_main_editor_window()
or filetype == "oil"
or filetype == "aerial"
or filetype == "tagbar"
or filetype == "snacks_picker_list"
)
then
is_suitable = false
Expand Down
23 changes: 22 additions & 1 deletion lua/claudecode/visual_commands.lua
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ function M.get_visual_range()
end

--- Check if we're in a tree buffer and get the tree state
--- @return table|nil, string|nil tree_state, tree_type ("neo-tree" or "nvim-tree")
--- @return table|nil, string|nil tree_state, tree_type ("neo-tree", "nvim-tree", "oil", or "snacks-explorer")
function M.get_tree_state()
local current_ft = "" -- Default fallback
local current_win = 0 -- Default fallback
Expand Down Expand Up @@ -181,6 +181,16 @@ function M.get_tree_state()
end

return oil, "oil"
elseif current_ft == "snacks_picker_list" then
local snacks_success, snacks = pcall(require, "snacks")
if not snacks_success or not snacks.picker then
return nil, nil
end

local explorers = snacks.picker.get({ source = "explorer" })
if explorers and #explorers > 0 then
return explorers[1], "snacks-explorer"
end
else
return nil, nil
end
Expand Down Expand Up @@ -381,6 +391,17 @@ function M.get_files_from_visual_selection(visual_data)
end
end
end
elseif tree_type == "snacks-explorer" then
-- For snacks.explorer, we need to handle visual selection differently
-- since it's a picker and doesn't have a traditional tree structure
local integrations = require("claudecode.integrations")
local selected_files, error = integrations._get_snacks_explorer_selection()

if not error and selected_files and #selected_files > 0 then
for _, file in ipairs(selected_files) do
table.insert(files, file)
end
end
end

return files, nil
Expand Down
225 changes: 225 additions & 0 deletions tests/unit/snacks_explorer_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
local helpers = require("tests.helpers.setup")
local integrations = require("claudecode.integrations")

describe("snacks.explorer integration", function()
before_each(function()
helpers.setup()
end)

after_each(function()
helpers.cleanup()
end)

describe("_get_snacks_explorer_selection", function()
it("should return error when snacks.nvim is not available", function()
-- Mock require to fail for snacks
local original_require = _G.require
_G.require = function(module)
if module == "snacks" then
error("Module not found")
end
return original_require(module)
end

local files, err = integrations._get_snacks_explorer_selection()
assert.are.same({}, files)
assert.equals("snacks.nvim not available", err)

-- Restore original require
_G.require = original_require
end)

it("should return error when no explorer picker is active", function()
-- Mock snacks module
local mock_snacks = {
picker = {
get = function()
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations._get_snacks_explorer_selection()
assert.are.same({}, files)
assert.equals("No active snacks.explorer found", err)

package.loaded["snacks"] = nil
end)

it("should return selected files from snacks.explorer", function()
-- Mock snacks module with explorer picker
local mock_explorer = {
selected = function(self, opts)
return {
{ file = "/path/to/file1.lua" },
{ file = "/path/to/file2.lua" },
}
end,
current = function(self, opts)
return { file = "/path/to/current.lua" }
end,
}

local mock_snacks = {
picker = {
get = function(opts)
if opts.source == "explorer" then
return { mock_explorer }
end
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations._get_snacks_explorer_selection()
assert.is_nil(err)
assert.are.same({ "/path/to/file1.lua", "/path/to/file2.lua" }, files)

package.loaded["snacks"] = nil
end)

it("should fall back to current file when no selection", function()
-- Mock snacks module with explorer picker
local mock_explorer = {
selected = function(self, opts)
return {}
end,
current = function(self, opts)
return { file = "/path/to/current.lua" }
end,
}

local mock_snacks = {
picker = {
get = function(opts)
if opts.source == "explorer" then
return { mock_explorer }
end
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations._get_snacks_explorer_selection()
assert.is_nil(err)
assert.are.same({ "/path/to/current.lua" }, files)

package.loaded["snacks"] = nil
end)

it("should handle empty file paths", function()
-- Mock snacks module with empty file paths
local mock_explorer = {
selected = function(self, opts)
return {
{ file = "" },
{ file = "/valid/path.lua" },
{ file = nil },
}
end,
current = function(self, opts)
return { file = "" }
end,
}

local mock_snacks = {
picker = {
get = function(opts)
if opts.source == "explorer" then
return { mock_explorer }
end
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations._get_snacks_explorer_selection()
assert.is_nil(err)
assert.are.same({ "/valid/path.lua" }, files)

package.loaded["snacks"] = nil
end)

it("should try alternative fields for file path", function()
-- Mock snacks module with different field names
local mock_explorer = {
selected = function(self, opts)
return {
{ path = "/path/from/path.lua" },
{ item = { file = "/path/from/item.file.lua" } },
{ item = { path = "/path/from/item.path.lua" } },
}
end,
current = function(self, opts)
return { path = "/current/from/path.lua" }
end,
}

local mock_snacks = {
picker = {
get = function(opts)
if opts.source == "explorer" then
return { mock_explorer }
end
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations._get_snacks_explorer_selection()
assert.is_nil(err)
assert.are.same({
"/path/from/path.lua",
"/path/from/item.file.lua",
"/path/from/item.path.lua",
}, files)

package.loaded["snacks"] = nil
end)
end)

describe("get_selected_files_from_tree", function()
it("should detect snacks_picker_list filetype", function()
vim.bo.filetype = "snacks_picker_list"

-- Mock snacks module
local mock_explorer = {
selected = function(self, opts)
return {}
end,
current = function(self, opts)
return { file = "/test/file.lua" }
end,
}

local mock_snacks = {
picker = {
get = function(opts)
if opts.source == "explorer" then
return { mock_explorer }
end
return {}
end,
},
}

package.loaded["snacks"] = mock_snacks

local files, err = integrations.get_selected_files_from_tree()
assert.is_nil(err)
assert.are.same({ "/test/file.lua" }, files)

package.loaded["snacks"] = nil
end)
end)
end)