From 5613a9060ddbc3491c593e976d2c4f7f6673d1e2 Mon Sep 17 00:00:00 2001 From: OxKahi Date: Mon, 17 Nov 2025 23:37:25 +0800 Subject: [PATCH 1/2] add dir bookmarks --- lua/arrow/buffer_ui.lua | 28 ++- lua/arrow/init.lua | 62 +++++- lua/arrow/integration/icons.lua | 43 ++-- lua/arrow/persist.lua | 123 ++++++++++- lua/arrow/ui.lua | 352 +++++++++++++++++++++++--------- 5 files changed, 487 insertions(+), 121 deletions(-) diff --git a/lua/arrow/buffer_ui.lua b/lua/arrow/buffer_ui.lua index 4c10d8f..d128983 100644 --- a/lua/arrow/buffer_ui.lua +++ b/lua/arrow/buffer_ui.lua @@ -4,6 +4,7 @@ local preview_buffers = {} local persist = require("arrow.buffer_persist") local config = require("arrow.config") +local namespace = vim.api.nvim_create_namespace("arrow_buffers") local lastRow = 0 local has_current_line = false @@ -92,10 +93,10 @@ function M.spawn_preview_window(buffer, index, bookmark, bookmark_count) extra_title = "(Current)" end - vim.api.nvim_win_set_option(win, "scrolloff", 999) + vim.api.nvim_set_option_value("scrolloff", 999, { win = win }) vim.api.nvim_win_set_cursor(win, { bookmark.line, 0 }) vim.api.nvim_win_set_config(win, { title = displayIndex .. " " .. extra_title }) - vim.api.nvim_win_set_option(win, "number", true) + vim.api.nvim_set_option_value("number", true, { win = win }) table.insert(preview_buffers, { buffer = buffer, win = win, index = index }) @@ -104,7 +105,7 @@ function M.spawn_preview_window(buffer, index, bookmark, bookmark_count) local shift = ctx_config.line_shift_down local win_view = vim.fn.winsaveview() - vim.api.nvim_win_set_option(win, "scrolloff", 0) + vim.api.nvim_set_option_value("scrolloff", 0, { win = win }) vim.fn.winrestview({ topline = win_view.topline - shift }) local ok, _ = pcall(require, "treesitter-context") @@ -163,16 +164,22 @@ local function go_to_window() end local function render_highlights(buffer) - vim.api.nvim_buf_clear_namespace(buffer, -1, 0, -1) + vim.api.nvim_buf_clear_namespace(buffer, namespace, 0, -1) local buffer_line_count = vim.api.nvim_buf_line_count(buffer) for i = 0, buffer_line_count - 1 do - vim.api.nvim_buf_add_highlight(buffer, -1, "ArrowFileIndex", i, 0, 3) + vim.api.nvim_buf_set_extmark(buffer, namespace, i, 0, { + end_col = 3, + hl_group = "ArrowFileIndex", + }) -- if line contains Delete Mode if string.match(vim.fn.getline(i + 1), "Delete Mode") and delete_mode then - vim.api.nvim_buf_add_highlight(buffer, -1, "ArrowDeleteMode", i, 0, 3) + vim.api.nvim_buf_set_extmark(buffer, namespace, i, 0, { + end_col = 3, + hl_group = "ArrowDeleteMode", + }) end end end @@ -224,8 +231,8 @@ local function toggle_delete_mode(action_buffer) else delete_mode = true - current_highlight = vim.api.nvim_get_hl_by_name("FloatBorder", true) - local arrow_delete_mode = vim.api.nvim_get_hl_by_name("ArrowDeleteMode", true) + current_highlight = vim.api.nvim_get_hl(0, { name = "FloatBorder" }) + local arrow_delete_mode = vim.api.nvim_get_hl(0, { name = "ArrowDeleteMode" }) vim.api.nvim_set_hl(0, "FloatBorder", { fg = arrow_delete_mode.bg or "red" }) end @@ -355,8 +362,7 @@ function M.spawn_action_windows(call_buffer, bookmarks, line_nr, col_nr, call_wi local menuKeymapOpts = { noremap = true, silent = true, buffer = actions_buffer, nowait = true } - vim.api.nvim_buf_set_option(actions_buffer, "modifiable", true) - + vim.api.nvim_set_option_value("modifiable", true, { buf = actions_buffer }) vim.api.nvim_buf_set_lines(actions_buffer, 0, -1, false, lines) vim.keymap.set("n", config.getState("leader_key"), function() @@ -485,7 +491,7 @@ function M.openMenu(bufnr) M.spawn_preview_window(opt[1], opt[2], opt[3], #bookmarks) end - M.spawn_action_windows(bufnr, bookmarks, line_nr, col_nr, cur_win) + M.spawn_action_windows(bufnr, bookmarks, line_nr, col_nr) end return M diff --git a/lua/arrow/init.lua b/lua/arrow/init.lua index 33b112c..caece8a 100644 --- a/lua/arrow/init.lua +++ b/lua/arrow/init.lua @@ -16,6 +16,9 @@ function M.setup(opts) vim.cmd("highlight default link ArrowCurrentFile SpecialChar") vim.cmd("highlight default link ArrowAction Character") vim.cmd("highlight default link ArrowDeleteMode DiagnosticError") + vim.cmd("highlight default link ArrowSplitMode Character") + vim.cmd("highlight default link ArrowFileBorder FloatBorder") + vim.cmd("highlight default link ArrowDirBorder FloatBorder") opts = opts or {} @@ -35,6 +38,7 @@ function M.setup(opts) remove = "x", next_item = "]", prev_item = "[", + toggle_bookmark_type = "", } local default_window_config = { @@ -85,13 +89,32 @@ function M.setup(opts) config.setState("global_bookmarks", opts.global_bookmarks or false) config.setState("relative_path", opts.relative_path or false) config.setState("separate_save_and_remove", opts.separate_save_and_remove or false) + config.setState("icon_provider", opts.icon_provider or "web_dev_icons") + + -- Directory bookmarks configuration + local default_dir_bookmark_config = { + toggle_key = "", + open_action = function(_, _) + print("no dir action found") + end, + } + config.setState( + "dir_bookmark_config", + utils.join_two_keys_tables(default_dir_bookmark_config, opts.dir_bookmark_config or {}) + ) + config.setState("current_bookmark_type", "file") config.setState("save_key", save_keys[opts.save_key] or save_keys.cwd) config.setState("save_key_name", opts.save_key or "cwd") config.setState("save_key_cached", config.getState("save_key")()) if leader_key then - vim.keymap.set("n", leader_key, ui.openMenu, { noremap = true, silent = true, nowait = true, desc = "Arrow File Mappings" }) + vim.keymap.set( + "n", + leader_key, + ui.openMenu, + { noremap = true, silent = true, nowait = true, desc = "Arrow File Mappings" } + ) end if buffer_leader_key then @@ -180,10 +203,46 @@ function M.setup(opts) "reset", } + local default_dir_full_path_list = { + "src", + "lib", + "library", + "utils", + "bin", + "cmd", + "docs", + "doc", + "tests", + "specs", + "configs", + "config", + "settings", + "images", + "themes", + "assets", + "static", + "public", + "scripts", + "examples", + "modules", + "development", + "dev", + "lua", + "tools", + ".github", + ".vscode", + ".config", + } + config.setState("mappings", utils.join_two_keys_tables(default_mappings, opts.mappings or {})) config.setState("full_path_list", utils.join_two_arrays(default_full_path_list, opts.full_path_list or {})) + config.setState( + "dir_full_path_list", + utils.join_two_arrays(default_dir_full_path_list, opts.dir_full_path_list or {}) + ) persist.load_cache_file() + persist.load_dir_cache_file() vim.api.nvim_create_augroup("arrow", { clear = true }) @@ -191,6 +250,7 @@ function M.setup(opts) callback = function() git.refresh_git_branch() persist.load_cache_file() + persist.load_dir_cache_file() config.setState("save_key_cached", config.getState("save_key")()) end, desc = "load cache file on DirChanged", diff --git a/lua/arrow/integration/icons.lua b/lua/arrow/integration/icons.lua index 81d4ebb..caf7998 100644 --- a/lua/arrow/integration/icons.lua +++ b/lua/arrow/integration/icons.lua @@ -1,37 +1,52 @@ +local config = require("arrow.config") + local M = {} local get_icon_from_web_dev_icons = function(file_name) local webdevicons = require("nvim-web-devicons") - local extension = vim.fn.fnamemodify(file_name, ":e") - local icon, hl_group = webdevicons.get_icon(file_name, extension, { default = true }) + if vim.fn.isdirectory(file_name) == 1 then + return "H", "Normal" + else + local extension = vim.fn.fnamemodify(file_name, ":e") + local icon, hl_group = webdevicons.get_icon(file_name, extension, { default = true }) - return icon, hl_group + return icon, hl_group + end end local get_icon_from_mini = function(file_name) local icons = require("mini.icons") - return icons.get("extension", file_name) + if vim.fn.isdirectory(file_name) == 1 then + return icons.get("directory", file_name) + else + return icons.get("extension", file_name) + end end --- Gets file icon from either `nvim-web-devicons` or `mini.icons`. --- @param file_name string M.get_file_icon = function(file_name) - if vim.fn.isdirectory(file_name) == 1 then - return "", "Normal" - end + local provider = config.getState("icon_provider") - local use_web_dev_icons = pcall(require, "nvim-web-devicons") - local use_mini_icons = pcall(require, "mini.icons") + if provider == "web_dev_icons" then + local use_web_dev_icons = pcall(require, "nvim-web-devicons") - if not (use_web_dev_icons or use_mini_icons) then - error("No icon provider found", vim.log.levels.ERROR) - end + if not use_web_dev_icons then + error("No icon provider found", vim.log.levels.ERROR) + end - if use_web_dev_icons then return get_icon_from_web_dev_icons(file_name) end - return get_icon_from_mini(file_name) + if provider == "mini" then + local use_mini_icons = pcall(require, "mini.icons") + + if not use_mini_icons then + error("No icon provider found", vim.log.levels.ERROR) + end + + return get_icon_from_mini(file_name) + end end return M diff --git a/lua/arrow/persist.lua b/lua/arrow/persist.lua index 7922cfd..6bb06d9 100644 --- a/lua/arrow/persist.lua +++ b/lua/arrow/persist.lua @@ -9,15 +9,16 @@ local function save_key() return "global" end + local save_key_cached = config.getState("save_key_cached") or vim.fn.getcwd() if config.getState("separate_by_branch") then local branch = git.refresh_git_branch() if branch then - return utils.normalize_path_to_filename(config.getState("save_key_cached") .. "-" .. branch) + return utils.normalize_path_to_filename(save_key_cached .. "-" .. branch) end end - return utils.normalize_path_to_filename(config.getState("save_key_cached")) + return utils.normalize_path_to_filename(save_key_cached) end local function cache_file_path() @@ -32,6 +33,26 @@ local function cache_file_path() return save_path .. "/" .. save_key() end +local function dir_cache_file_path() + local save_path_func = config.getState("save_path") + local save_path + + if save_path_func then + save_path = save_path_func() + else + -- Fallback if save_path is not configured + save_path = vim.fn.stdpath("cache") .. "/arrow" + end + + save_path = save_path:gsub("/$", "") + + if vim.fn.isdirectory(save_path) == 0 then + vim.fn.mkdir(save_path, "p") + end + + return save_path .. "/" .. save_key() .. "_dir" +end + local function notify() vim.api.nvim_exec_autocmds("User", { pattern = "ArrowUpdate", @@ -39,6 +60,85 @@ local function notify() end vim.g.arrow_filenames = {} +vim.g.arrow_dir_bookmarks = {} + +function M.save_dir(directory) + if not M.is_dir_saved(directory) then + local new_table = vim.g.arrow_dir_bookmarks + table.insert(new_table, directory) + vim.g.arrow_dir_bookmarks = new_table + + M.cache_dir_file() + M.load_dir_cache_file() + end + notify() +end + +function M.remove_dir(directory) + local index = M.is_dir_saved(directory) + if index then + local new_table = vim.g.arrow_dir_bookmarks + table.remove(new_table, index) + vim.g.arrow_dir_bookmarks = new_table + + M.cache_dir_file() + M.load_dir_cache_file() + end + notify() +end + +function M.toggle_dir(directory) + git.refresh_git_branch() + + directory = directory or vim.fn.getcwd() + + local index = M.is_dir_saved(directory) + if index then + M.remove_dir(directory) + else + M.save_dir(directory) + end + notify() +end + +function M.clear_dir() + vim.g.arrow_dir_bookmarks = {} + M.cache_dir_file() + M.load_dir_cache_file() + notify() +end + +function M.is_dir_saved(directory) + for i, dir in ipairs(vim.g.arrow_dir_bookmarks) do + if dir == directory then + return i + end + end + return nil +end + +function M.load_dir_cache_file() + local cache_path = dir_cache_file_path() + + if vim.fn.filereadable(cache_path) == 0 then + vim.g.arrow_dir_bookmarks = {} + + return + end + + local success, data = pcall(vim.fn.readfile, cache_path) + if success then + vim.g.arrow_dir_bookmarks = data + else + vim.g.arrow_dir_bookmarks = {} + end +end + +function M.cache_dir_file() + local content = vim.fn.join(vim.g.arrow_dir_bookmarks, "\n") + local lines = vim.fn.split(content, "\n") + vim.fn.writefile(lines, dir_cache_file_path()) +end function M.save(filename) if not M.is_saved(filename) then @@ -146,6 +246,19 @@ function M.go_to(index) end end +function M.go_to_dir(index) + local directory = vim.g.arrow_dir_bookmarks[index] + + if not directory then + return + end + + local dir_bookmark_config = config.getState("dir_bookmark_config") + if dir_bookmark_config and dir_bookmark_config.open_action then + dir_bookmark_config.open_action(directory, vim.b.filename) + end +end + function M.next() git.refresh_git_branch() @@ -178,10 +291,11 @@ function M.previous() M.go_to(previous_index) end -function M.open_cache_file() +function M.open_cache_file(type) git.refresh_git_branch() + local bookmark_type = type or "file" - local cache_path = cache_file_path() + local cache_path = bookmark_type == "dir" and dir_cache_file_path() or cache_file_path() local cache_content if vim.fn.filereadable(cache_path) == 0 then @@ -242,6 +356,7 @@ function M.open_cache_file() local updated_content = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) vim.fn.writefile(updated_content, cache_path) M.load_cache_file() + M.load_dir_cache_file() end, }) diff --git a/lua/arrow/ui.lua b/lua/arrow/ui.lua index fa8b82c..2b8005e 100644 --- a/lua/arrow/ui.lua +++ b/lua/arrow/ui.lua @@ -6,17 +6,25 @@ local utils = require("arrow.utils") local git = require("arrow.git") local icons = require("arrow.integration.icons") +local namespace = vim.api.nvim_create_namespace("arrow_files") local fileNames = {} local to_highlight = {} local current_index = 0 +local actionMenuIndent = 3 +---@return {key: string, text: string}[] local function getActionsMenu() local mappings = config.getState("mappings") + local bookmark_type = config.getState("current_bookmark_type") - if #vim.g.arrow_filenames == 0 then + local bookmark_list = bookmark_type == "dir" and vim.g.arrow_dir_bookmarks or vim.g.arrow_filenames + local remove_text = bookmark_type == "dir" and "Remove Current Folder" or "Remove Current File" + local save_text = bookmark_type == "dir" and "Save Current Folder" or "Save Current File" + + if #bookmark_list == 0 then return { - string.format("%s Save File", mappings.toggle), + { key = mappings.toggle, text = string.format("%s %s", mappings.toggle, save_text) }, } end @@ -25,24 +33,53 @@ local function getActionsMenu() local separate_save_and_remove = config.getState("separate_save_and_remove") local return_mappings = { - string.format("%s Edit Arrow File", mappings.edit), - string.format("%s Clear All Items", mappings.clear_all_items), - string.format("%s Delete Mode", mappings.delete_mode), - string.format("%s Open Vertical", mappings.open_vertical), - string.format("%s Open Horizontal", mappings.open_horizontal), - string.format("%s Next Item", mappings.next_item), - string.format("%s Prev Item", mappings.prev_item), - string.format("%s Quit", mappings.quit), + { key = mappings.edit, text = string.format("%s Edit Arrow File", mappings.edit) }, + { key = mappings.clear_all_items, text = string.format("%s Clear All Items", mappings.clear_all_items) }, + { key = mappings.delete_mode, text = string.format("%s Delete Mode", mappings.delete_mode) }, + -- { key = mappings.toggle_bookmark_type, text = string.format("%s Switch Mode", mappings.toggle_bookmark_type) }, + { key = mappings.next_item, text = string.format("%s Next Item", mappings.next_item) }, + { key = mappings.prev_item, text = string.format("%s Prev Item", mappings.prev_item) }, + { key = mappings.quit, text = string.format("%s Quit", mappings.quit) }, } + -- Add split options only for file bookmarks + if bookmark_type ~= "dir" then + table.insert( + return_mappings, + 4, + { key = mappings.open_vertical, text = string.format("%s Open Vertical", mappings.open_vertical) } + ) + table.insert( + return_mappings, + 5, + { key = mappings.open_horizontal, text = string.format("%s Open Horizontal", mappings.open_horizontal) } + ) + end + if separate_save_and_remove then - table.insert(return_mappings, 1, string.format("%s Remove Current File", mappings.remove)) - table.insert(return_mappings, 1, string.format("%s Save Current File", mappings.toggle)) + table.insert( + return_mappings, + 1, + { key = mappings.remove, text = string.format("%s %s", mappings.remove, remove_text) } + ) + table.insert( + return_mappings, + 1, + { key = mappings.toggle, text = string.format("%s %s", mappings.toggle, save_text) } + ) else if already_saved == true then - table.insert(return_mappings, 1, string.format("%s Remove Current File", mappings.toggle)) + table.insert( + return_mappings, + 1, + { key = mappings.toggle, text = string.format("%s %s", mappings.toggle, remove_text) } + ) else - table.insert(return_mappings, 1, string.format("%s Save Current File", mappings.toggle)) + table.insert( + return_mappings, + 1, + { key = mappings.toggle, text = string.format("%s %s", mappings.toggle, save_text) } + ) end end @@ -51,14 +88,13 @@ end local function format_file_names(file_names) local full_path_list = config.getState("full_path_list") + local dir_full_path_list = config.getState("dir_full_path_list") local formatted_names = {} -- Table to store occurrences of file names (tail) local name_occurrences = {} for _, full_path in ipairs(file_names) do - local tail = vim.fn.fnamemodify(full_path, ":t:r") -- Get the file name without extension - if vim.fn.isdirectory(full_path) == 1 then local parsed_path = full_path @@ -75,6 +111,7 @@ local function format_file_names(file_names) name_occurrences[folder_name] = { full_path } end else + local tail = vim.fn.fnamemodify(full_path, ":t:r") -- Get the file name without extension if not name_occurrences[tail] then name_occurrences[tail] = { full_path } else @@ -83,6 +120,7 @@ local function format_file_names(file_names) end end + -- print(vim.inspect(name_occurrences)) for _, full_path in ipairs(file_names) do local tail = vim.fn.fnamemodify(full_path, ":t:r") local tail_with_extension = vim.fn.fnamemodify(full_path, ":t") @@ -103,7 +141,11 @@ local function format_file_names(file_names) local location = vim.fn.fnamemodify(full_path, ":h:h") - if #name_occurrences[folder_name] > 1 or config.getState("always_show_path") then + if + #name_occurrences[folder_name] > 1 + or config.getState("always_show_path") + or vim.tbl_contains(dir_full_path_list, folder_name) + then table.insert(formatted_names, string.format("%s . %s", folder_name .. "/", location)) else table.insert(formatted_names, string.format("%s", folder_name .. "/")) @@ -132,7 +174,6 @@ local function format_file_names(file_names) table.insert(formatted_names, string.format("%s . %s", tail_with_extension, display_path)) end end - return formatted_names end @@ -142,13 +183,24 @@ local function closeMenu() vim.api.nvim_win_close(win, true) end +-- function to refresh the menu window config on switch +local function refreshMenu() + local win = vim.fn.win_getid() + local window_config = M.getWindowConfig() + vim.api.nvim_win_set_config(win, window_config) +end + local function renderBuffer(buffer) - vim.api.nvim_buf_set_option(buffer, "modifiable", true) + vim.api.nvim_set_option_value("modifiable", true, { buf = buffer }) local show_icons = config.getState("show_icons") local buf = buffer or vim.api.nvim_get_current_buf() local lines = { "" } + local bookmark_type = config.getState("current_bookmark_type") + local bookmark_list = bookmark_type == "dir" and vim.g.arrow_dir_bookmarks or vim.g.arrow_filenames + fileNames = bookmark_list + local formattedFleNames = format_file_names(fileNames) to_highlight = {} @@ -159,11 +211,11 @@ local function renderBuffer(buffer) displayIndex = config.getState("index_keys"):sub(i, i) - vim.api.nvim_buf_add_highlight(buf, -1, "ArrowDeleteMode", i + 3, 0, -1) + -- vim.api.nvim_buf_add_highlight(buf, -1, "ArrowDeleteMode", i + 3, 0, -1) local parsed_filename = fileNames[i] - if fileNames[i]:sub(1, 2) == "./" then + if fileNames[i] and fileNames[i]:sub(1, 2) == "./" then parsed_filename = fileNames[i]:sub(3) end @@ -187,8 +239,9 @@ local function renderBuffer(buffer) end -- Add a separator - if #vim.g.arrow_filenames == 0 then - table.insert(lines, " No files yet.") + local empty_text = bookmark_type == "dir" and " No directories yet." or " No files yet." + if #bookmark_list == 0 then + table.insert(lines, empty_text) end table.insert(lines, "") @@ -198,15 +251,15 @@ local function renderBuffer(buffer) -- Add actions to the menu if not (config.getState("hide_handbook")) then for _, action in ipairs(actionsMenu) do - table.insert(lines, " " .. action) + table.insert(lines, string.rep(" ", actionMenuIndent) .. action.text) end end table.insert(lines, "") vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) - vim.api.nvim_buf_set_option(buf, "modifiable", false) - vim.api.nvim_buf_set_option(buf, "buftype", "nofile") + vim.api.nvim_set_option_value("modifiable", false, { buf = buf }) + vim.api.nvim_set_option_value("buftype", "nofile", { buf = buf }) end -- Function to create the menu buffer with a list format @@ -224,95 +277,149 @@ local function render_highlights(buffer) local actionsMenu = getActionsMenu() local mappings = config.getState("mappings") - vim.api.nvim_buf_clear_namespace(buffer, -1, 0, -1) + vim.api.nvim_buf_clear_namespace(buffer, namespace, 0, -1) local menuBuf = buffer or vim.api.nvim_get_current_buf() - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowCurrentFile", current_index, 0, -1) + -- vim.api.nvim_set_hl(0, "FloatBorder", borderHighlight) + + vim.api.nvim_buf_set_extmark(menuBuf, namespace, current_index, 0, { + hl_eol = true, + hl_group = "ArrowCurrentFile", + }) + --------------------------------------------- + -- setting highlights for file/dir indexes -- + --------------------------------------------- for i, _ in ipairs(fileNames) do - if vim.b.arrow_current_mode == "delete_mode" then - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowDeleteMode", i, 3, 4) - else - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowFileIndex", i, 3, 4) + local line = vim.api.nvim_buf_get_lines(menuBuf, i, i + 1, false)[1] + if line and #line >= 4 then + if vim.b.arrow_current_mode == "delete_mode" then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i, 3, { + end_col = 4, + hl_group = "ArrowDeleteMode", + }) + elseif vim.b.arrow_current_mode == "vertical_mode" or vim.b.arrow_current_mode == "horizontal_mode" then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i, 3, { + end_col = 4, + hl_group = "ArrowSplitMode", + }) + else + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i, 3, { + end_col = 4, + hl_group = "ArrowFileIndex", + }) + end end end + ---------------------------------- + -- setting highlights for icons -- + ---------------------------------- if config.getState("show_icons") then for k, v in pairs(to_highlight) do - vim.api.nvim_buf_add_highlight(menuBuf, -1, v, k, 5, 8) - end - end - - for i = #fileNames + 3, #fileNames + #actionsMenu + 3 do - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowAction", i - 1, 3, 4) - end - - -- Find the line containing "d - Delete Mode" - local deleteModeLine = -1 - local verticalModeLine = -1 - local horizontalModelLine = -1 - - for i, action in ipairs(actionsMenu) do - if action:find(mappings.delete_mode .. " Delete Mode") then - deleteModeLine = i - 1 - end - - if action:find(mappings.open_vertical .. " Open Vertical") then - verticalModeLine = i - 1 - end - - if action:find(mappings.open_horizontal .. " Open Horizontal") then - horizontalModelLine = i - 1 + local line = vim.api.nvim_buf_get_lines(menuBuf, k, k + 1, false)[1] + if line and #line >= 8 then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, k, 5, { + end_col = 8, + hl_group = v, + }) + end end end - if deleteModeLine >= 0 then - if vim.b.arrow_current_mode == "delete_mode" then - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowDeleteMode", #fileNames + deleteModeLine + 2, 0, -1) - end - end + ---------------------------------------------------- + -- setting highlights for action menu description -- + ---------------------------------------------------- + -- compensate for the empty file/dir text line when there is no files + -- default 3 spacings + filenames count + local topSpacing = #fileNames == 0 and 4 or 3 + local lineCountTilActions = #fileNames + topSpacing + for i = lineCountTilActions, #actionsMenu + lineCountTilActions do + local actionMenuIndex = i - lineCountTilActions + 1 + if actionMenuIndex <= #actionsMenu then + local action = actionsMenu[actionMenuIndex] + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i - 1, actionMenuIndent, { + end_col = actionMenuIndent + #action.key, + hl_group = "ArrowAction", + }) + + local nextStartCol = actionMenuIndent + #action.key + if vim.b.arrow_current_mode == "delete_mode" and action.key == mappings.delete_mode then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i - 1, nextStartCol, { + end_col = nextStartCol + #action.text - 1, + hl_group = "ArrowDeleteMode", + }) + end - if verticalModeLine >= 0 then - if vim.b.arrow_current_mode == "vertical_mode" then - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowAction", #fileNames + verticalModeLine + 2, 0, -1) - end - end + if vim.b.arrow_current_mode == "vertical_mode" and action.key == mappings.open_vertical then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i - 1, nextStartCol, { + end_col = nextStartCol + #action.text - 1, + hl_group = "ArrowSplitMode", + }) + end - if horizontalModelLine >= 0 then - if vim.b.arrow_current_mode == "horizontal_mode" then - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowAction", #fileNames + horizontalModelLine + 2, 0, -1) + if vim.b.arrow_current_mode == "horizontal_mode" and action.key == mappings.open_horizontal then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, i - 1, nextStartCol, { + end_col = nextStartCol + #action.text - 1, + hl_group = "ArrowSplitMode", + }) + end end end + --------------------------------------- + -- setting highlights for file paths -- + --------------------------------------- local pattern = " %. .-$" local line_number = 1 - while line_number <= #fileNames + 1 do local line_content = vim.api.nvim_buf_get_lines(menuBuf, line_number - 1, line_number, false)[1] - local match_start, match_end = string.find(line_content, pattern) - if match_start then - vim.api.nvim_buf_add_highlight(menuBuf, -1, "ArrowAction", line_number - 1, match_start - 1, match_end) + if line_content then + local match_start, match_end = string.find(line_content, pattern) + if match_start then + vim.api.nvim_buf_set_extmark(menuBuf, namespace, line_number - 1, match_start - 1, { + end_col = match_end, + hl_group = "ArrowAction", + }) + end end line_number = line_number + 1 end + + -- setting conditional border highlight TODO + -- local bookmark_type = config.getState("current_bookmark_type") + -- local borderHighlightKey = bookmark_type == "dir" and "ArrowDirBorder" or "ArrowFileBorder" + -- local borderHighlight = vim.api.nvim_get_hl(0, { name = borderHighlightKey }) + -- print(vim.inspect(borderHighlight)) + -- if borderHighlight.fg then + -- vim.api.nvim_set_hl(namespace, "FloatBorder", { + -- fg = borderHighlight.fg, + -- }) + -- end end --- Function to open the selected file +-- Function to open the selected file or directory function M.openFile(fileNumber) - local fileName = vim.g.arrow_filenames[fileNumber] + local bookmark_type = config.getState("current_bookmark_type") + local fileName = bookmark_type == "dir" and vim.g.arrow_dir_bookmarks[fileNumber] + or vim.g.arrow_filenames[fileNumber] if vim.b.arrow_current_mode == "delete_mode" then - persist.remove(fileName) + if bookmark_type == "dir" then + persist.remove_dir(fileName) + else + persist.remove(fileName) + end - fileNames = vim.g.arrow_filenames + fileNames = bookmark_type == "dir" and vim.g.arrow_dir_bookmarks or vim.g.arrow_filenames renderBuffer(vim.api.nvim_get_current_buf()) render_highlights(vim.api.nvim_get_current_buf()) else if not fileName then - print("Invalid file number") + print("Invalid " .. bookmark_type .. " number") return end @@ -322,17 +429,35 @@ function M.openFile(fileNumber) fileName = vim.fn.fnameescape(fileName) if vim.b.arrow_current_mode == "" or not vim.b.arrow_current_mode then - action = config.getState("open_action") + if bookmark_type == "dir" then + local dir_config = config.getState("dir_bookmark_config") + action = dir_config and dir_config.open_action + else + action = config.getState("open_action") + end elseif vim.b.arrow_current_mode == "vertical_mode" then - action = config.getState("vertical_action") + if bookmark_type ~= "dir" then + action = config.getState("vertical_action") + else + local dir_config = config.getState("dir_bookmark_config") + action = dir_config and dir_config.open_action + end elseif vim.b.arrow_current_mode == "horizontal_mode" then - action = config.getState("horizontal_action") + if bookmark_type ~= "dir" then + action = config.getState("horizontal_action") + else + local dir_config = config.getState("dir_bookmark_config") + action = dir_config and dir_config.open_action + end end closeMenu() vim.api.nvim_exec_autocmds("User", { pattern = "ArrowOpenFile" }) - if + if bookmark_type == "dir" then + -- For directory bookmarks, just pass the directory path + action(fileName, vim.b.filename) + elseif config.getState("global_bookmarks") == true or config.getState("save_key_name") == "cwd" or config.getState("save_key_name") == "git_root_bare" @@ -348,10 +473,13 @@ function M.getWindowConfig() local show_handbook = not (config.getState("hide_handbook")) local parsedFileNames = format_file_names(fileNames) local separate_save_and_remove = config.getState("separate_save_and_remove") + local bookmark_type = config.getState("current_bookmark_type") + local mappings = config.getState("mappings") + local actionsMenu = getActionsMenu() local max_width = 0 if show_handbook then - max_width = 13 + max_width = 15 if separate_save_and_remove then max_width = max_width + 2 end @@ -366,7 +494,7 @@ function M.getWindowConfig() local height = #fileNames + 2 if show_handbook then - height = height + 10 + height = height + #actionsMenu if separate_save_and_remove then height = height + 1 end @@ -377,9 +505,14 @@ function M.getWindowConfig() height = height, row = math.ceil((vim.o.lines - height) / 2), col = math.ceil((vim.o.columns - width) / 2), + title = bookmark_type == "dir" and " directories " or " files ", + title_pos = "left", + footer = string.format(" %s switch ", mappings.toggle_bookmark_type), + footer_pos = "center", } - local is_empty = #vim.g.arrow_filenames == 0 + local bookmark_list = bookmark_type == "dir" and vim.g.arrow_dir_bookmarks or vim.g.arrow_filenames + local is_empty = #bookmark_list == 0 if is_empty and show_handbook then current_config.height = 5 @@ -410,6 +543,9 @@ end function M.openMenu(bufnr) git.refresh_git_branch() + -- Always default to file bookmarks when opening the menu + -- config.setState("current_bookmark_type", type or "file") + local call_buffer = bufnr or vim.api.nvim_get_current_buf() if vim.g.arrow_filenames == 0 then @@ -454,32 +590,56 @@ function M.openMenu(bufnr) vim.keymap.set("n", mappings.quit, closeMenu, menuKeymapOpts) vim.keymap.set("n", mappings.edit, function() closeMenu() - persist.open_cache_file() + local bookmark_type = config.getState("current_bookmark_type") + persist.open_cache_file(bookmark_type) end, menuKeymapOpts) if separate_save_and_remove then vim.keymap.set("n", mappings.toggle, function() + local bookmark_type = config.getState("current_bookmark_type") filename = filename or utils.get_current_buffer_path() - - persist.save(filename) + if bookmark_type == "dir" then + local current_dir = vim.fn.fnamemodify(filename, ":h") .. "/" + persist.save_dir(current_dir) + else + persist.save(filename) + end closeMenu() end, menuKeymapOpts) vim.keymap.set("n", mappings.remove, function() + local bookmark_type = config.getState("current_bookmark_type") filename = filename or utils.get_current_buffer_path() - persist.remove(filename) + if bookmark_type == "dir" then + local current_dir = vim.fn.fnamemodify(filename, ":h") .. "/" + persist.remove_dir(current_dir) + else + persist.remove(filename) + end closeMenu() end, menuKeymapOpts) else vim.keymap.set("n", mappings.toggle, function() - persist.toggle(filename) + local bookmark_type = config.getState("current_bookmark_type") + if bookmark_type == "dir" then + local current_file = utils.get_current_buffer_path() + local current_dir = vim.fn.fnamemodify(current_file, ":h") .. "/" + persist.toggle_dir(current_dir) + else + persist.toggle(filename) + end closeMenu() end, menuKeymapOpts) end vim.keymap.set("n", mappings.clear_all_items, function() - persist.clear() + local bookmark_type = config.getState("current_bookmark_type") + if bookmark_type == "dir" then + persist.clear_dir() + else + persist.clear() + end closeMenu() end, menuKeymapOpts) @@ -506,6 +666,16 @@ function M.openMenu(bufnr) render_highlights(menuBuf) end, menuKeymapOpts) + vim.keymap.set("n", mappings.toggle_bookmark_type, function() + local current_type = config.getState("current_bookmark_type") + local new_type = current_type == "file" and "dir" or "file" + config.setState("current_bookmark_type", new_type) + + renderBuffer(menuBuf) + render_highlights(menuBuf) + refreshMenu() + end, menuKeymapOpts) + vim.keymap.set("n", mappings.open_vertical, function() if vim.b.arrow_current_mode == "vertical_mode" then vim.b.arrow_current_mode = "" From ba05f6e823e7909afb72d199856363951d2069f1 Mon Sep 17 00:00:00 2001 From: OxKahi Date: Mon, 17 Nov 2025 23:49:52 +0800 Subject: [PATCH 2/2] fix readme and cleanup some code --- README.md | 9 +++++++++ lua/arrow/init.lua | 1 - 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b1e0d64..b068c5c 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,10 @@ return { }, opts = { show_icons = true, + icon_provider = 'nvim-web-devicons', -- or 'mini.icons' leader_key = ';', -- Recommended to be a single key buffer_leader_key = 'm', -- Per Buffer Mappings + } } ``` @@ -81,6 +83,7 @@ Just press the leader_key set on setup and follow you heart. (Is that easy) remove = "x", -- only used if separate_save_and_remove is true next_item = "]", prev_item = "[" + toggle_bookmark_type = '', -- toggle between file and dir bookmark }, custom_actions = { open = function(target_file_name, current_file_name) end, -- target_file_name = file selected to be open, current_file_name = filename from where this was called @@ -105,6 +108,12 @@ Just press the leader_key set on setup and follow you heart. (Is that easy) zindex = 10, --default 50 treesitter_context = nil, -- it can be { line_shift_down = 2 }, currently not usable, for detail see https://github.com/otavioschwanck/arrow.nvim/pull/43#issue-2236320268 }, + dir_bookmark_config = { + -- required to open dir bookmarks + open_action = function(dir_path, _) -- action to take when opening a dir in an oil floating window + require('oil').open_float(dir_path) + end, + }, separate_save_and_remove = false, -- if true, will remove the toggle and create the save/remove keymaps. leader_key = ";", save_key = "cwd", -- what will be used as root to save the bookmarks. Can be also `git_root` and `git_root_bare`. diff --git a/lua/arrow/init.lua b/lua/arrow/init.lua index caece8a..1df717a 100644 --- a/lua/arrow/init.lua +++ b/lua/arrow/init.lua @@ -93,7 +93,6 @@ function M.setup(opts) -- Directory bookmarks configuration local default_dir_bookmark_config = { - toggle_key = "", open_action = function(_, _) print("no dir action found") end,