Created
June 28, 2025 00:17
-
-
Save fira42073/d46969c8321f70536c63be01666ff0f7 to your computer and use it in GitHub Desktop.
harpoon extensions
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local Path = require("plenary.path") | |
local pathutil = {} | |
function pathutil.normalize_path(buf_name, root) return Path:new(buf_name):make_relative(root) end | |
-- Function to get the module path from go.mod | |
local function get_go_mod_path() | |
local cwd = vim.loop.cwd() -- Current working directory | |
local go_mod_path = Path:new(cwd, "go.mod") | |
-- Read the go.mod file | |
if go_mod_path:exists() then | |
local go_mod_contents = go_mod_path:read() | |
-- Find the module line | |
for line in go_mod_contents:gmatch("[^\r\n]+") do | |
if line:match("^module ") then | |
-- Extract the module path | |
return line:sub(8) -- Skip the 'module ' prefix | |
end | |
end | |
end | |
return nil | |
end | |
local function get_folder_path(buf_name) | |
local path = Path:new(buf_name) -- Create a Path object | |
return path:parent():absolute() -- Get the absolute path of the parent directory | |
end | |
-- Function to get the full path including module and file path | |
function pathutil.get_full_path_to_go_package() | |
local module_path = get_go_mod_path() | |
if not module_path then | |
print("Error: go.mod file not found or module path not defined") | |
return | |
end | |
local buf_name = vim.api.nvim_buf_get_name(0) -- Get the current buffer's file path | |
local root = vim.loop.cwd() -- Current working directory | |
local relative_path = pathutil.normalize_path(get_folder_path(buf_name), root) | |
-- Combine module path with relative path | |
local full_path = module_path .. "/" .. relative_path | |
return full_path | |
end | |
return pathutil |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local stringutil = {} | |
function stringutil.split_filepath_with_num_and_prefix(input) | |
-- Match the optional bookmark comment and the filename:num | |
local bookmark_comment, filename, num = input:match("^(.-)%s*=>%s*(.-):(%d+)$") | |
-- If the match fails, it means there was no '=>', so try the simpler pattern | |
if not bookmark_comment then | |
filename, num = input:match("^(.-):(%d+)$") | |
if not num then return "", input, "" end | |
-- Convert the matched number to an actual number type | |
num = tonumber(num) | |
-- Return the parts | |
return "", filename, num | |
end | |
return bookmark_comment, filename, num | |
end | |
function stringutil.split_filepath_with_num(input) | |
-- Match the string and the number at the end | |
local str, num = input:match("^(.-):(%d+)$") | |
-- Convert the matched number to an actual number type | |
num = tonumber(num) | |
-- Return the parts | |
return str, num | |
end | |
function stringutil.construct_filepath_with_num(filepath, num) | |
-- If num is provided, concatenate it with a colon | |
if num then return filepath .. ":" .. tostring(num) end | |
return filepath | |
end | |
return stringutil |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local tables = {} | |
function tables.merge(t1, t2) | |
local result = {} | |
for k, v in pairs(t1) do | |
result[k] = v | |
end | |
for k, v in pairs(t2) do | |
result[k] = v | |
end | |
return result | |
end | |
-- function to get deep clone of passed table | |
function tables.clone(obj, seen) | |
-- Handle non-tables and previously-seen tables. | |
if type(obj) ~= "table" then return obj end | |
if seen and seen[obj] then return seen[obj] end | |
-- New table; mark it as seen an copy recursively. | |
local s = seen or {} | |
local res = {} | |
s[obj] = res | |
for k, v in next, obj do | |
res[tables.clone(k, s)] = tables.clone(v, s) | |
end | |
return setmetatable(res, getmetatable(obj)) | |
end | |
return tables |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
local harpoon = require("harpoon") | |
local tables = require("action.tables") | |
local stringutil = require("action.stringutil") | |
local pathutil = require("action.pathutil") | |
local opts = { noremap = true } | |
local DEFAULT_LIST = "bookmarks" | |
local BUFFERS_LIST = "buffers" | |
harpoon:setup({ | |
settings = { | |
save_on_toggle = false, | |
sync_on_ui_close = true, | |
}, | |
-------------------------------- | |
-- -- | |
-- Bookmarks -- | |
-- -- | |
-------------------------------- | |
bookmarks = { | |
create_list_item = function(_, input) | |
local list_entry = "" | |
if input == nil then | |
-- Get the current file path and normalize to relative | |
local root_dir = vim.loop.cwd() | |
-- Get the current line idx | |
local idx = vim.fn.line(".") | |
local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_get_current_buf()) | |
local bufname_rel = pathutil.normalize_path(bufname, root_dir) | |
list_entry = stringutil.construct_filepath_with_num(bufname_rel, idx) | |
else | |
-- input is from the window commit | |
list_entry = input | |
end | |
-- if input is not nill ask for comment | |
if input == nil then | |
local comment = "" | |
local symbol_name = "" | |
local ft = vim.api.nvim_get_option_value("filetype", {}) | |
if ft == "go" then | |
local fn = require("go.ts.go").get_func_method_node_at_pos() | |
if not (fn == nil or fn == "") then symbol_name = fn.name end | |
end | |
-- Ask for bookmark comment | |
comment = vim.fn.input("Bookmark comment (press <Enter> to leave empty): ", symbol_name) | |
if comment ~= "" then list_entry = comment .. " => " .. list_entry end | |
end | |
return { | |
value = list_entry, -- comment => filepath:linenum | |
context = {}, | |
} | |
end, | |
select = function(list_item, _, _) | |
local bm_comment, bufname_rel, num = stringutil.split_filepath_with_num_and_prefix(list_item.value) | |
vim.print("Bookmark: " .. (bm_comment ~= "" and bm_comment or "<No comment>")) | |
-- Get the current buffer ID if the file is already open | |
local buf_id = vim.fn.bufnr(bufname_rel) | |
-- If the buffer is not loaded, load it with :badd, which doesn't require saving | |
if buf_id == -1 then | |
vim.cmd("badd " .. bufname_rel) | |
buf_id = vim.fn.bufnr(bufname_rel) | |
end | |
-- Navigate to the buffer | |
vim.cmd("buffer " .. buf_id) | |
-- Move to the desired line number, if provided | |
if num then vim.fn.setpos(".", { buf_id, tonumber(num), 0, 0 }) end | |
end, | |
}, | |
-------------------------------- | |
-- -- | |
-- Buffer manager -- | |
-- -- | |
-------------------------------- | |
buffers = { | |
select = function(list_item, _, _) | |
-- there are no comments in the buffer list, so first argument is empty | |
local _, bufname_rel, buf_id = stringutil.split_filepath_with_num_and_prefix(list_item.value) | |
vim.print("Buffer: " .. bufname_rel) | |
-- Navigate to the buffer | |
vim.cmd("buffer " .. buf_id) | |
end, | |
}, | |
}) | |
-------------------------------- | |
-- -- | |
-- Bookmarks -- | |
-- -- | |
-------------------------------- | |
local bookmark_ui_toggle_opts = { | |
["title"] = "Bookmarks", | |
["height_in_lines"] = 20, | |
} | |
-- Harpoon a file | |
vim.keymap.set( | |
"n", | |
"<A-a>", | |
function() harpoon:list(DEFAULT_LIST):add() end, | |
tables.merge(opts, { desc = "Harpoon add" }) | |
) | |
-- Toggle Harpoon menu | |
vim.keymap.set( | |
"n", | |
"<leader>m", | |
function() harpoon.ui:toggle_quick_menu(harpoon:list(DEFAULT_LIST), bookmark_ui_toggle_opts) end, | |
tables.merge(opts, { desc = "Harpoon toggle" }) | |
) | |
-- set <leader>h1-9 to navigate to the corresponding file | |
for i = 1, 9 do | |
vim.keymap.set( | |
"n", | |
"<leader>h" .. i, | |
function() harpoon:list(DEFAULT_LIST):select(i) end, | |
tables.merge(opts, { desc = "Harpoon open file number" }) | |
) | |
end | |
-- Toggle previous & next buffers stored within Harpoon list | |
vim.keymap.set( | |
"n", | |
"<C-S-O>", | |
function() harpoon:list(DEFAULT_LIST):prev() end, | |
tables.merge(opts, { desc = "Harpoon previous" }) | |
) | |
vim.keymap.set( | |
"n", | |
"<C-S-I>", | |
function() harpoon:list(DEFAULT_LIST):next() end, | |
tables.merge(opts, { desc = "Harpoon next" }) | |
) | |
-------------------------------- | |
-- -- | |
-- Buffer manager -- | |
-- -- | |
-------------------------------- | |
local buffer_ui_toggle_opts = { | |
["title"] = "Buffer Manager", | |
["height_in_lines"] = 20, | |
} | |
-- Open menu and search | |
-- Alt + m | |
vim.keymap.set({ "t", "n" }, "<A-m>", function() | |
harpoon.ui:toggle_quick_menu(harpoon:list(BUFFERS_LIST), buffer_ui_toggle_opts) | |
-- wait for the menu to open | |
vim.defer_fn(function() vim.fn.feedkeys("/") end, 50) | |
end, tables.merge(opts, { desc = "Buffer manager menu search" })) | |
-- Toggle buffer manager menu | |
vim.keymap.set( | |
"n", | |
"<leader>bm", | |
function() harpoon.ui:toggle_quick_menu(harpoon:list(BUFFERS_LIST), buffer_ui_toggle_opts) end, | |
tables.merge(opts, { desc = "Buffer manager toggle" }) | |
) | |
-- set <leader>h1-9 to navigate to the corresponding file | |
for i = 1, 9 do | |
vim.keymap.set( | |
"n", | |
"<leader>b" .. i, | |
function() harpoon:list(BUFFERS_LIST):select(i) end, | |
tables.merge(opts, { desc = "Buffer manager open file number" }) | |
) | |
end | |
-- Toggle previous & next buffers stored within Harpoon list | |
vim.keymap.set( | |
"n", | |
"<A-[>", | |
function() harpoon:list(BUFFERS_LIST):prev() end, | |
tables.merge(opts, { desc = "Buffer manager previous" }) | |
) | |
vim.keymap.set( | |
"n", | |
"<A-]>", | |
function() harpoon:list(BUFFERS_LIST):next() end, | |
tables.merge(opts, { desc = "Buffer manager next" }) | |
) | |
-- Keep buffer manager up to date (incl autocmd) | |
local function add_open_buffers_to_harpoon() | |
-- List all buffer IDs | |
local bufs = vim.api.nvim_list_bufs() | |
local root_dir = vim.loop.cwd() | |
harpoon:list(BUFFERS_LIST):clear() | |
for _, buf in ipairs(bufs) do | |
-- Check if buffer is valid and listed | |
if vim.api.nvim_buf_is_valid(buf) and vim.api.nvim_get_option_value("buflisted", { buf = buf }) then | |
local bufname = vim.api.nvim_buf_get_name(buf) | |
local bufname_rel = "" | |
if bufname and bufname ~= "" then bufname_rel = pathutil.normalize_path(bufname, root_dir) end | |
-- Using buffer id as buf num | |
local buf_num = buf | |
local list_entry = stringutil.construct_filepath_with_num(bufname_rel, buf_num) | |
-- Refresh list completely | |
harpoon:list(BUFFERS_LIST):add({ | |
value = list_entry, | |
context = {}, | |
}) | |
end | |
end | |
end | |
vim.api.nvim_create_autocmd({ "BufAdd", "BufDelete", "BufEnter", "BufLeave", "BufFilePost", "BufNew", "BufNewFile" }, { | |
pattern = "*", | |
callback = function() vim.defer_fn(add_open_buffers_to_harpoon, 100) end, | |
}) | |
-- Make buffer manager writeable | |
local function update_open_buffers(_) | |
local harpoon_bufs = harpoon:list(BUFFERS_LIST):display() | |
local keep = {} | |
-- Extract buffer numbers from Harpoon entries like ":4" or "path/to/file.go:6" | |
for _, entry in ipairs(harpoon_bufs) do | |
-- Match :<number> or <path>:<number> | |
local buf_num = entry:match("^:%s*(%d+)$") or entry:match(":(%d+)$") | |
if buf_num then keep[tonumber(buf_num)] = true end | |
end | |
-- Go through all buffers and close any not in the keep list | |
for _, buf in ipairs(vim.api.nvim_list_bufs()) do | |
local should_keep = keep[buf] | |
local is_valid = vim.api.nvim_buf_is_valid(buf) | |
local is_listed = vim.api.nvim_get_option_value("buflisted", { buf = buf }) | |
local is_loaded = vim.api.nvim_buf_is_loaded(buf) | |
if is_valid and is_listed and is_loaded and not should_keep then vim.api.nvim_buf_delete(buf, { force = true }) end | |
end | |
end | |
harpoon:extend({ | |
REMOVE = update_open_buffers, | |
LIST_CHANGE = update_open_buffers, | |
}) | |
-- Sync and load harpoon files to cwd | |
local function hash_path(path) | |
local real = vim.fn.resolve(vim.fn.fnamemodify(path, ":p")) | |
return vim.fn.sha256(real) | |
end | |
local function get_harpoon_file() | |
local dir = vim.fn.stdpath("data") .. "/harpoon" | |
local file = dir .. "/" .. hash_path(vim.fn.getcwd()) .. ".json" | |
vim.fn.mkdir(vim.fn.fnamemodify(file, ":h"), "p") | |
return file | |
end | |
local function harpoonSave() | |
local src_filepath = get_harpoon_file() | |
local src = io.open(src_filepath, "r") | |
if not src then | |
vim.notify("Cannot open harpoon file: " .. src_filepath, vim.log.levels.ERROR) | |
return | |
end | |
local content = src:read("*a") -- read entire file | |
src:close() | |
local dest_filepath = vim.loop.cwd() .. "/.harpoon.json" | |
local dest = io.open(dest_filepath, "w") | |
if not dest then | |
vim.notify("Cannot open harpoon file: " .. dest_filepath, vim.log.levels.ERROR) | |
return | |
end | |
dest:write(content) -- read entire file | |
dest:close() | |
vim.notify("Harpoon saved: " .. dest_filepath) | |
end | |
local function harpoonLoad() | |
local filepath = vim.loop.cwd() .. "/.harpoon.json" | |
local file = io.open(filepath, "r") | |
if not file then | |
vim.notify("Cannot open harpoon file: " .. filepath, vim.log.levels.ERROR) | |
return | |
end | |
local content = file:read("*a") -- read entire file | |
file:close() | |
local parsed = vim.json.decode(content) | |
if not parsed then | |
vim.notify("Failed to decode harpoon JSON", vim.log.levels.ERROR) | |
return | |
end | |
local project = nil | |
for key, value in pairs(parsed) do | |
if key ~= "bookmarks" then | |
project = value | |
break | |
end | |
end | |
if not project then | |
vim.notify("No valid project entry found in harpoon file", vim.log.levels.WARN) | |
return | |
end | |
harpoon:list(DEFAULT_LIST):clear() | |
local entries = project[DEFAULT_LIST] or {} | |
for _, encoded in ipairs(entries) do | |
local decoded = vim.json.decode(encoded) | |
if decoded and decoded.value then | |
harpoon:list(DEFAULT_LIST):add({ | |
value = decoded.value, | |
context = decoded.context or {}, | |
}) | |
end | |
end | |
vim.notify("Harpoon bookmarks and buffers loaded.") | |
end | |
vim.api.nvim_create_user_command("HarpoonSave", function() harpoonSave() end, {}) | |
vim.api.nvim_create_user_command("HarpoonLoad", function() harpoonLoad() end, {}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment