- The Problems
- The Solutions
- Installation
- Getting Started
- API
- Contribution
- Social
- Note to legacy Harpoon 1 users
- You're working on a codebase. medium, large, tiny, whatever. You find
yourself frequenting a small set of files and you are tired of using a fuzzy finder,
:bnext
&:bprev
are getting too repetitive, alternate file doesn't quite cut it, etc etc. - You want to execute some project specific commands, have any number of persistent terminals that can be easily navigated to, send commands to other tmux windows, or dream up your own custom action and execute with a single key
- Specify either by altering a ui or by adding via hot key files
- Unlimited lists and items within the lists
- neovim 0.8.0+ required
- install using your favorite plugin manager (i am using
packer
in this case)
use "nvim-lua/plenary.nvim" -- don't forget to add this one if you don't have it yet!
use {
"ThePrimeagen/harpoon",
branch = "harpoon2",
requires = { {"nvim-lua/plenary.nvim"} }
}
- install using lazy.nvim
{
"ThePrimeagen/harpoon",
branch = "harpoon2",
dependencies = { "nvim-lua/plenary.nvim" }
}
You will want to add your style of remaps and such to your neovim dotfiles with the shortcuts you like. My shortcuts are for me. Me alone. Which also means they are designed with dvorak in mind (My layout btw, I use dvorak btw).
it is a requirement to call harpoon:setup()
. This is required due to
autocmds setup.
Here is my basic setup
local harpoon = require("harpoon")
-- REQUIRED
harpoon:setup()
-- REQUIRED
vim.keymap.set("n", "<leader>a", function() harpoon:list():add() end)
vim.keymap.set("n", "<C-e>", function() harpoon.ui:toggle_quick_menu(harpoon:list()) end)
vim.keymap.set("n", "<C-h>", function() harpoon:list():select(1) end)
vim.keymap.set("n", "<C-t>", function() harpoon:list():select(2) end)
vim.keymap.set("n", "<C-n>", function() harpoon:list():select(3) end)
vim.keymap.set("n", "<C-s>", function() harpoon:list():select(4) end)
-- Toggle previous & next buffers stored within Harpoon list
vim.keymap.set("n", "<C-S-P>", function() harpoon:list():prev() end)
vim.keymap.set("n", "<C-S-N>", function() harpoon:list():next() end)
In order to use Telescope as a UI,
make sure to add telescope
to your dependencies and paste this following snippet into your configuration.
local harpoon = require('harpoon')
harpoon:setup({})
-- basic telescope configuration
local conf = require("telescope.config").values
local function toggle_telescope(harpoon_files)
local file_paths = {}
for _, item in ipairs(harpoon_files.items) do
table.insert(file_paths, item.value)
end
require("telescope.pickers").new({}, {
prompt_title = "Harpoon",
finder = require("telescope.finders").new_table({
results = file_paths,
}),
previewer = conf.file_previewer({}),
sorter = conf.generic_sorter({}),
}):find()
end
vim.keymap.set("n", "<C-e>", function() toggle_telescope(harpoon:list()) end,
{ desc = "Open harpoon window" })
You can define custom behavior of a harpoon list by providing your own calls.
Here is a simple example where i create a list named cmd
that takes the
current line in the editor and adds it to harpoon menu. When
list:select(...)
is called, we take the contents of the line and execute it
as a vim command
I don't think this is a great use of harpoon, but its meant to show how to add your own custom lists. You could imagine that a terminal list would be just as easy to create.
local harpoon = require("harpoon")
harpoon:setup({
-- Setting up custom behavior for a list named "cmd"
"cmd" = {
-- When you call list:add() this function is called and the return
-- value will be put in the list at the end.
--
-- which means same behavior for prepend except where in the list the
-- return value is added
--
-- @param possible_value string only passed in when you alter the ui manual
add = function(possible_value)
-- get the current line idx
local idx = vim.fn.line(".")
-- read the current line
local cmd = vim.api.nvim_buf_get_lines(0, idx - 1, idx, false)[1]
if cmd == nil then
return nil
end
return {
value = cmd,
context = { ... any data you want ... },
}
end,
--- This function gets invoked with the options being passed in from
--- list:select(index, <...options...>)
--- @param list_item {value: any, context: any}
--- @param list { ... }
--- @param option any
select = function(list_item, list, option)
-- WOAH, IS THIS HTMX LEVEL XSS ATTACK??
vim.cmd(list_item.value)
end
}
})
There is quite a bit of behavior you can configure via harpoon:setup()
settings
: is the global settings. as of now there isn't a global setting in use, but once we have some custom behavior i'll put them heredefault
: the default configuration for any list. it is simply a file harpoon[name] = HarpoonPartialConfigItem
: any named lists config. it will be merged withdefault
and override any behavior
HarpoonPartialConfigItem Definition
---@class HarpoonPartialConfigItem
---@field select_with_nil? boolean defaults to false
---@field encode? (fun(list_item: HarpoonListItem): string) | boolean
---@field decode? (fun(obj: string): any)
---@field display? (fun(list_item: HarpoonListItem): string)
---@field select? (fun(list_item?: HarpoonListItem, list: HarpoonList, options: any?): nil)
---@field equals? (fun(list_line_a: HarpoonListItem, list_line_b: HarpoonListItem): boolean)
---@field create_list_item? fun(config: HarpoonPartialConfigItem, item: any?): HarpoonListItem
---@field BufLeave? fun(evt: any, list: HarpoonList): nil
---@field VimLeavePre? fun(evt: any, list: HarpoonList): nil
---@field get_root_dir? fun(): string
Detailed Definitions
select_with_nil
: allows for a list to call select even if the provided item is nilencode
: how to encode the list item to the harpoon file. if encode isfalse
, then the list will not be saved to disk (think terminals)decode
: how to decode the listdisplay
: how to display the list item in the ui menuselect
: the action taken when selecting a list item. called fromlist:select(idx, options)
equals
: how to compare two list items for equalitycreate_list_item
: called whenlist:add()
orlist:prepend()
is called. called with an item, which will be a string, when adding through the ui menuBufLeave
: this function is called for every list on BufLeave. if you need custom behavior, this is the placeVimLeavePre
: this function is called for every list on VimLeavePre.get_root_dir
: used for creating relative paths. defaults tovim.loop.cwd()
Settings can alter the experience of harpoon
Definition
---@class HarpoonSettings
---@field save_on_toggle boolean defaults to false
---@field sync_on_ui_close boolean defaults to false
---@field key (fun(): string)
Descriptions
save_on_toggle
: any time the ui menu is closed then we will save the state back to the backing list, not to the fssync_on_ui_close
: any time the ui menu is closed then the state of the list will be sync'd back to the fskey
how the out list key is looked up. This can be useful when using worktrees and using git remote instead of file path
Defaults
settings = {
save_on_toggle = false,
sync_on_ui_close = false,
key = function()
return vim.loop.cwd()
end,
},
The 'extend' functionality can be used to add keymaps for opening files in splits & tabs.
harpoon:extend({
UI_CREATE = function(cx)
vim.keymap.set("n", "<C-v>", function()
harpoon.ui:select_menu_item({ vsplit = true })
end, { buffer = cx.bufnr })
vim.keymap.set("n", "<C-x>", function()
harpoon.ui:select_menu_item({ split = true })
end, { buffer = cx.bufnr })
vim.keymap.set("n", "<C-t>", function()
harpoon.ui:select_menu_item({ tabedit = true })
end, { buffer = cx.bufnr })
end,
})
TODO: Fill in the idea that we will emit out window information
This can help debug issues on other's computer. To get your debug log please do the following.
- open up a new instance of vim
- perform exact operation to cause bug
- execute vim command
:lua require("harpoon").logger:show()
and copy the buffer - paste the buffer as part of the bug creation
THIS PART OF THE DOCS NEEDS FILLING OUT
local harpoon = require("harpoon");
local extensions = require("harpoon.extensions");
harpoon:setup()
harpoon:extend(extensions.builtins.command_on_nav("foo bar"));
harpoon:extend(extensions.builtins.navigate_with_number());
This project is officially open source, not just public source. If you wish to contribute start with an issue and I am totally willing for PRs, but I will be very conservative on what I take. I don't want Harpoon solving specific issues, I want it to create the proper hooks to solve any problem
Running Tests
To run the tests make sure plenary is checked out in the parent directory of this repository, then run make test
.
For questions about Harpoon, there's a #harpoon channel on the Primeagen's Discord server.
Original Harpoon will remain in a frozen state and i will merge PRs in with no
code review for those that wish to remain on that. Harpoon 2 is significantly
better and allows for MUCH greater control. Please migrate to that (will
become master
within the next few months).