snacks-file-browser

 avatar
unknown
lua
19 days ago
7.7 kB
10
Indexable
---@class catdaddy.util.snacks
local M = {}

local uv = vim.uv or vim.loop

--- Global variable to store the current working directory
---@type string
M.cwd = ""

--- Global variable to store the current buffer file
--- This is used to track the current buffer file when opening the file browser
---@type string
M.current_buf_file = ""

---Get the current working directory of the buffer
---@return string
local function get_cwd_path()
	if package.loaded["oil"] then
		local oil = require("oil")
		if oil.get_current_dir then
			local oil_dir = oil.get_current_dir()
			if oil_dir then
				return vim.fn.fnamemodify(oil_dir, ":h")
			end
		end
	end

	local buf_path = vim.api.nvim_buf_get_name(0)
	if buf_path == "" then
		return uv.cwd()
	end
	return vim.fn.isdirectory(buf_path) == 1 and buf_path or vim.fn.fnamemodify(buf_path, ":h")
end

---Locate a target file in the picker list and scroll to it
---@param picker snacks.Picker
---@param target_file string
local function locate_file(picker, target_file)
	vim.schedule(function()
		local items_lookup = {}
		for idx, item in ipairs(picker.list.items) do
			items_lookup[item.file] = idx
		end
		local target_idx = items_lookup[target_file]
		if target_idx then
			picker.list:view(target_idx)
		end
	end)
end

--- Set the picker current working directory (cwd) and reload the picker
--- This function is used to set the new working directory and refresh the picker.
---@param picker snacks.Picker
---@param new_cwd string
function M.set_cwd(picker, new_cwd)
	local resolved_cwd = uv.fs_realpath(new_cwd) or new_cwd
	if resolved_cwd and vim.fn.isdirectory(resolved_cwd) == 1 then
		M.cwd = resolved_cwd
		picker:set_cwd(M.cwd)
		picker.opts.title = " File Browser ( " .. M.cwd .. " )"
		picker.input:set("", "")
		picker:find()
	end
end

---Wait for finder and matcher tasks to complete before locating target file
---@param picker snacks.Picker
---@param target_file string
local function wait_for_tasks_and_locate(picker, target_file)
	local finder_done = false
	local matcher_done = false

	local function check()
		if finder_done and matcher_done then
			locate_file(picker, target_file)
		end
	end

	if picker.finder and picker.finder.task then
		picker.finder.task:on("done", function()
			finder_done = true
			check()
		end)
	end

	if picker.matcher and picker.matcher.task then
		picker.matcher.task:on("done", function()
			matcher_done = true
			check()
		end)
	end
end

--- Open the file browser picker with specified actions and keys
---@param opts? table  -- Optional configuration table
function M.file_browser(opts)
	opts = opts or {}
	M.cwd = opts.cwd or get_cwd_path()
	M.current_buf_file = vim.fs.basename(vim.api.nvim_buf_get_name(0))

	-- Configure the picker to use the actions and keys from options or defaults
	local picker = Snacks.picker.files({
		on_show = function(self)
			if M.current_buf_file ~= "" then
				wait_for_tasks_and_locate(self, M.current_buf_file)
			end
		end,
		cwd = M.cwd,
		cmd = "fd",
		args = opts.args or {
			"--hidden",
			"--follow",
			"--max-depth=1",
			"--type=d",
			"--color=never",
			"-E",
			".git",
		},
		title = " File Browser ( " .. M.cwd .. " )",
		actions = opts.actions or {
			confirm = {
				action = function(self, selected)
					if not selected or selected.score == 0 then
						local new_path = vim.fs.joinpath(M.cwd, self:filter().pattern)
						self:close()
						vim.schedule(function()
							vim.cmd.edit(new_path)
						end)
						return
					end

					local file = vim.fs.joinpath(M.cwd, selected.file)
					if vim.fn.isdirectory(file) == 1 then
						M.set_cwd(self, file)
					else
						self:close()
						vim.schedule(function()
							vim.cmd.edit(file)
						end)
					end
				end,
			},
			navigate_parent = {
				action = function(self)
					local parent = vim.fs.dirname(M.cwd)
					if parent and parent ~= M.cwd then
						M.set_cwd(self, parent)
					end
				end,
			},
			change_directory = {
				action = function(self, selected)
					if selected then
						M.set_cwd(self, vim.fs.joinpath(M.cwd, selected.file))
						vim.cmd("tcd " .. vim.fn.fnameescape(M.cwd))
					end
				end,
			},
			copy_path = {
				action = function(_, selected)
					if selected then
						local file_name = vim.fn.fnamemodify(vim.fs.joinpath(M.cwd, selected.file), ":p")
						-- Copies to system clipboard
						vim.fn.setreg("+", file_name)
						Snacks.notify("Copied: " .. file_name, { level = "info" })
					end
				end,
			},
			delete = {
				action = function(self, selected)
					if selected then
						local file = vim.fs.joinpath(M.cwd, selected.file)
						vim.ui.input({ prompt = "Delete " .. selected.file .. "? [y/N] " }, function(confirm)
							if confirm == "y" then
								local success = vim.fn.delete(file, "rf") == 0
								if success then
									Snacks.notify("Deleted: " .. file, { level = "info" })
									self:find() -- Refresh the picker
								else
									Snacks.notify("Delete failed: " .. vim.v.exception, { level = "error" })
								end
							end
						end)
					end
				end,
			},
			rename = {
				action = function(self, selected)
					if selected then
						local old_path = vim.fs.joinpath(M.cwd, selected.file)
						vim.ui.input(
							{ prompt = "Rename " .. selected.file .. " to: ", completion = "file" },
							function(new_name)
								if new_name and new_name ~= "" and new_name ~= selected.file then
									local new_path = vim.fs.joinpath(M.cwd, new_name)
									local success, err = os.rename(old_path, new_path)
									if success then
										Snacks.notify("Renamed to: " .. new_name, { level = "info" })
										self:find() -- Refresh the picker
									else
										Snacks.notify("Rename failed: " .. err, { level = "error" })
									end
								end
							end
						)
					end
				end,
			},

			new = {
				action = function(self)
					vim.ui.input({ prompt = "New file/folder: ", completion = "file" }, function(new_name)
						if new_name and new_name ~= "" then
							local new_path = vim.fs.joinpath(M.cwd, new_name)
							if new_name:sub(-1) == "/" then
								vim.fn.mkdir(new_path, "p")
							else
								local parent_dir = vim.fn.fnamemodify(new_path, ":h")
								vim.fn.mkdir(parent_dir, "p")
								local file = io.open(new_path, "w")
								if not file then
									Snacks.notify("Failed to create file: " .. new_path, { level = "error" })
									return
								end
								file:close()
								Snacks.notify("Created: " .. new_path, { level = "info" })
							end
							self:find()
							local target_file = vim.fn.fnamemodify(new_name, ":t")
							Snacks.notify("New file/folder: " .. target_file, { level = "info" })
							wait_for_tasks_and_locate(self, target_file)
						end
					end)
				end,
			},
		},

		win = {
			input = {
				keys = opts.keys or {
					["<BS>"] = { "navigate_parent", mode = { "n" } },
					["<M-BS>"] = { "navigate_parent", mode = { "i" } },
					["<C-d>"] = { "change_directory", mode = { "i", "n" } },
					["<M-y>"] = { "copy_path", mode = { "i" } },
					["<M-r>"] = { "rename", mode = { "i" } },
					["<M-d>"] = { "delete", mode = { "i" } },
					["<M-n>"] = { "new", mode = { "i" } },
					["y"] = { "copy_path", mode = { "n" } },
					["r"] = { "rename", mode = { "n" } },
					["d"] = { "delete", mode = { "n" } },
					["N"] = { "new", mode = { "n" } },
				},
			},
		},

		layout = opts.layout or {
			preview = "main",
			preset = "vscode",
		},
	})

	-- If the initial directory is invalid, close the picker and show an error message
	uv.fs_stat(M.cwd, function(err, stat)
		if err or not stat then
			vim.schedule(function()
				picker:close()
				Snacks.notify(("Invalid initial directory: %s"):format(M.cwd), { level = "error" })
			end)
		end
	end)
end

return M
Editor is loading...
Leave a Comment