Repository: ThePrimeagen/harpoon Branch: master Commit: 1bc17e3e42ea Files: 26 Total size: 67.5 KB Directory structure: gitextract_l5y7hm4o/ ├── .editorconfig ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── feature_request.md │ └── workflows/ │ ├── format.yml │ └── lint.yml ├── .luacheckrc ├── .stylua.toml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── lua/ │ ├── harpoon/ │ │ ├── cmd-ui.lua │ │ ├── dev.lua │ │ ├── init.lua │ │ ├── mark.lua │ │ ├── tabline.lua │ │ ├── term.lua │ │ ├── test/ │ │ │ ├── manage-a-mark.lua │ │ │ └── manage_cmd_spec.lua │ │ ├── tmux.lua │ │ ├── ui.lua │ │ └── utils.lua │ └── telescope/ │ └── _extensions/ │ ├── harpoon.lua │ └── marks.lua └── scripts/ └── tmux/ └── switch-back-to-nvim ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # top-most EditorConfig file root = true # Unix-style newlines with a newline ending every file [*] end_of_line = lf indent_style = space indent_size = 4 insert_final_newline = true ================================================ FILE: .github/FUNDING.yml ================================================ github: theprimeagen ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Found something wrong with Harpoon2? title: '' labels: '' assignees: '' --- **WARNING** If this is about Harpoon1, the issue will be closed. All support and everything of harpoon1 will be frozen on `master` until 4/20 or 6/9 and then harpoon2 will become master Please use `harpoon2` for branch --------------- ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **What issue are you having that you need harpoon to solve?** **Why doesn't the current config help?** **What proposed api changes are you suggesting?** ================================================ FILE: .github/workflows/format.yml ================================================ name: Format on: [push, pull_request] jobs: format: name: Stylua runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - run: date +%W > weekly - name: Restore cache id: cache uses: actions/cache@v2 with: path: | ~/.cargo/bin key: ${{ runner.os }}-cargo-${{ hashFiles('weekly') }} - name: Install if: steps.cache.outputs.cache-hit != 'true' run: cargo install stylua - name: Format run: stylua --check lua/ --config-path=.stylua.toml ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: [push, pull_request] jobs: lint: name: Luacheck runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Setup run: | sudo apt-get update sudo apt-get install luarocks sudo luarocks install luacheck - name: Lint run: luacheck lua/ --globals vim ================================================ FILE: .luacheckrc ================================================ std = luajit cache = true codes = true globals = { "HarpoonConfig", "Harpoon_bufh", "Harpoon_win_id", "Harpoon_cmd_win_id", "Harpoon_cmd_bufh", } read_globals = { "vim" } ================================================ FILE: .stylua.toml ================================================ column_width = 80 line_endings = "Unix" indent_type = "Spaces" indent_width = 4 quote_style = "AutoPreferDouble" ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 ThePrimeagen Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ fmt: echo "===> Formatting" stylua lua/ --config-path=.stylua.toml lint: echo "===> Linting" luacheck lua/ --globals vim pr-ready: fmt lint ================================================ FILE: README.md ================================================
## ⇁ HARPOON 2 This is a deprecated and all future changes will be to the branch `harpoon2`. [Harpoon 2](https://github.com/ThePrimeagen/harpoon/tree/harpoon2) **STATUS**: Merging into mainline April 20th or June 9th (nice) ------------------------------- # Legacy Harpoon README # Harpoon ##### Getting you where you want with the fewest keystrokes. [![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org) [![Neovim](https://img.shields.io/badge/Neovim%200.5+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io)
![Harpoon](harpoon.png) -- image provided by **Bob Rust** ## ⇁ WIP This is not fully baked, though used by several people. If you experience any issues, see some improvement you think would be amazing, or just have some feedback for harpoon (or me), make an issue! ## ⇁ The Problems: 1. 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. 1. You want to execute some project specific commands or have any number of persistent terminals that can be easily navigated to. ## ⇁ The Solutions: 1. The ability to specify, or on the fly, mark and create persisting key strokes to go to the files you want. 1. Unlimited terminals and navigation. ## ⇁ Installation * neovim 0.5.0+ required * install using your favorite plugin manager (`vim-plug` in this example) ```vim Plug 'nvim-lua/plenary.nvim' " don't forget to add this one if you don't have it yet! Plug 'ThePrimeagen/harpoon' ``` ## ⇁ Harpooning here we'll explain how to wield the power of the harpoon: ### Marks you mark files you want to revisit later on ```lua :lua require("harpoon.mark").add_file() ``` ### File Navigation view all project marks with: ```lua :lua require("harpoon.ui").toggle_quick_menu() ``` you can go up and down the list, enter, delete or reorder. `q` and `` exit and save the menu you also can switch to any mark without bringing up the menu, use the below with the desired mark index ```lua :lua require("harpoon.ui").nav_file(3) -- navigates to file 3 ``` you can also cycle the list in both directions ```lua :lua require("harpoon.ui").nav_next() -- navigates to next mark :lua require("harpoon.ui").nav_prev() -- navigates to previous mark ``` from the quickmenu, open a file in: a vertical split with control+v, a horizontal split with control+x, a new tab with control+t ### Terminal Navigation this works like file navigation except that if there is no terminal at the specified index a new terminal is created. ```lua lua require("harpoon.term").gotoTerminal(1) -- navigates to term 1 ``` ### Commands to Terminals commands can be sent to any terminal ```lua lua require("harpoon.term").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 ``` further more commands can be stored for later quick ```lua lua require('harpoon.cmd-ui').toggle_quick_menu() -- shows the commands menu lua require("harpoon.term").sendCommand(1, 1) -- sends command 1 to term 1 ``` ### Tmux Support tmux is supported out of the box and can be used as a drop-in replacement to normal terminals by simply switching `'term' with 'tmux'` like so ```lua lua require("harpoon.tmux").gotoTerminal(1) -- goes to the first tmux window lua require("harpoon.tmux").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 lua require("harpoon.tmux").sendCommand(1, 1) -- sends command 1 to tmux window 1 ``` `sendCommand` and `goToTerminal` also accept any valid [tmux pane identifier](https://man7.org/linux/man-pages/man1/tmux.1.html#COMMANDS). ```lua lua require("harpoon.tmux").gotoTerminal("{down-of}") -- focus the pane directly below lua require("harpoon.tmux").sendCommand("%3", "ls") -- send a command to the pane with id '%3' ``` Once you switch to a tmux window you can always switch back to neovim, this is a little bash script that will switch to the window which is running neovim. In your `tmux.conf` (or anywhere you have keybinds), add this ```bash bind-key -r G run-shell "path-to-harpoon/harpoon/scripts/tmux/switch-back-to-nvim" ``` ### Telescope Support 1st register harpoon as a telescope extension ```lua require("telescope").load_extension('harpoon') ``` currently only marks are supported in telescope ``` :Telescope harpoon marks ``` ## ⇁ Configuration if configuring harpoon is desired it must be done through harpoons setup function ```lua require("harpoon").setup({ ... }) ``` ### Global Settings here are all the available global settings with their default values ```lua global_settings = { -- sets the marks upon calling `toggle` on the ui, instead of require `:w`. save_on_toggle = false, -- saves the harpoon file upon every change. disabling is unrecommended. save_on_change = true, -- sets harpoon to run the command immediately as it's passed to the terminal when calling `sendCommand`. enter_on_sendcmd = false, -- closes any tmux windows harpoon that harpoon creates when you close Neovim. tmux_autoclose_windows = false, -- filetypes that you want to prevent from adding to the harpoon list menu. excluded_filetypes = { "harpoon" }, -- set marks specific to each git branch inside git repository mark_branch = false, -- enable tabline with harpoon marks tabline = false, tabline_prefix = " ", tabline_suffix = " ", } ``` ### Preconfigured Terminal Commands to preconfigure terminal commands for later use ```lua projects = { -- Yes $HOME works ["$HOME/personal/vim-with-me/server"] = { term = { cmds = { "./env && npx ts-node src/index.ts" } } } } ``` ## ⇁ Logging - logs are written to `harpoon.log` within the nvim cache path (`:echo stdpath("cache")`) - available log levels are `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. `warn` is default - log level can be set with `vim.g.harpoon_log_level` (must be **before** `setup()`) - launching nvim with `HARPOON_LOG=debug nvim` takes precedence over `vim.g.harpoon_log_level`. - invalid values default back to `warn`. ## ⇁ Others #### How do Harpoon marks differ from vim global marks they serve a similar purpose however harpoon marks differ in a few key ways: 1. They auto update their position within the file 1. They are saved _per project_. 1. They can be hand edited vs replaced (swapping is easier) #### The Motivation behind Harpoon terminals 1. I want to use the terminal since I can gF and gF to any errors arising from execution that are within the terminal that are not appropriate for something like dispatch. (not just running tests but perhaps a server that runs for X amount of time before crashing). 1. I want the terminal to be persistent and I can return to one of many terminals with some finger wizardry and reparse any of the execution information that was not necessarily error related. 1. I would like to have commands that can be tied to terminals and sent them without much thinking. Some sort of middle ground between vim-test and just typing them into a terminal (configuring netflix's television project isn't quite building and there are tons of ways to configure). #### Use a dynamic width for the Harpoon popup menu Sometimes the default width of `60` is not wide enough. The following example demonstrates how to configure a custom width by setting the menu's width relative to the current window's width. ```lua require("harpoon").setup({ menu = { width = vim.api.nvim_win_get_width(0) - 4, } }) ``` #### Tabline By default, the tabline will use the default theme of your theme. You can customize by editing the following highlights: * HarpoonInactive * HarpoonActive * HarpoonNumberActive * HarpoonNumberInactive Example to make it cleaner: ```lua vim.cmd('highlight! HarpoonInactive guibg=NONE guifg=#63698c') vim.cmd('highlight! HarpoonActive guibg=NONE guifg=white') vim.cmd('highlight! HarpoonNumberActive guibg=NONE guifg=#7aa2f7') vim.cmd('highlight! HarpoonNumberInactive guibg=NONE guifg=#7aa2f7') vim.cmd('highlight! TabLineFill guibg=NONE guifg=white') ``` Result: ![tabline](https://i.imgur.com/8i8mKJD.png) ## ⇁ Social For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. * [Discord](https://discord.gg/theprimeagen) * [Twitch](https://www.twitch.tv/theprimeagen) * [Twitter](https://twitter.com/ThePrimeagen) ================================================ FILE: TODO.md ================================================ ### Manage A Mark 1.0 * Logo * floating term / split term * TODO: Fill me in, that one really important thing.... * README.md ### Harpoon (upon requests) * Add hooks for vim so that someone can make it for me * ackshual tests. * interactive menu * cycle * make setup() callable more than once and just layer in the commands ================================================ FILE: lua/harpoon/cmd-ui.lua ================================================ local harpoon = require("harpoon") local popup = require("plenary.popup") local utils = require("harpoon.utils") local log = require("harpoon.dev").log local term = require("harpoon.term") local M = {} Harpoon_cmd_win_id = nil Harpoon_cmd_bufh = nil local function close_menu(force_save) force_save = force_save or false local global_config = harpoon.get_global_settings() if global_config.save_on_toggle or force_save then require("harpoon.cmd-ui").on_menu_save() end vim.api.nvim_win_close(Harpoon_cmd_win_id, true) Harpoon_cmd_win_id = nil Harpoon_cmd_bufh = nil end local function create_window() log.trace("_create_window()") local config = harpoon.get_menu_config() local width = config.width or 60 local height = config.height or 10 local borderchars = config.borderchars or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } local bufnr = vim.api.nvim_create_buf(false, false) local Harpoon_cmd_win_id, win = popup.create(bufnr, { title = "Harpoon Commands", highlight = "HarpoonWindow", line = math.floor(((vim.o.lines - height) / 2) - 1), col = math.floor((vim.o.columns - width) / 2), minwidth = width, minheight = height, borderchars = borderchars, }) vim.api.nvim_win_set_option( win.border.win_id, "winhl", "Normal:HarpoonBorder" ) return { bufnr = bufnr, win_id = Harpoon_cmd_win_id, } end local function get_menu_items() log.trace("_get_menu_items()") local lines = vim.api.nvim_buf_get_lines(Harpoon_cmd_bufh, 0, -1, true) local indices = {} for _, line in pairs(lines) do if not utils.is_white_space(line) then table.insert(indices, line) end end return indices end function M.toggle_quick_menu() log.trace("cmd-ui#toggle_quick_menu()") if Harpoon_cmd_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_cmd_win_id) then close_menu() return end local win_info = create_window() local contents = {} local global_config = harpoon.get_global_settings() Harpoon_cmd_win_id = win_info.win_id Harpoon_cmd_bufh = win_info.bufnr for idx, cmd in pairs(harpoon.get_term_config().cmds) do contents[idx] = cmd end vim.api.nvim_win_set_option(Harpoon_cmd_win_id, "number", true) vim.api.nvim_buf_set_name(Harpoon_cmd_bufh, "harpoon-cmd-menu") vim.api.nvim_buf_set_lines(Harpoon_cmd_bufh, 0, #contents, false, contents) vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "filetype", "harpoon") vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "buftype", "acwrite") vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "bufhidden", "delete") vim.api.nvim_buf_set_keymap( Harpoon_cmd_bufh, "n", "q", "lua require('harpoon.cmd-ui').toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( Harpoon_cmd_bufh, "n", "", "lua require('harpoon.cmd-ui').toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( Harpoon_cmd_bufh, "n", "", "lua require('harpoon.cmd-ui').select_menu_item()", {} ) vim.cmd( string.format( "autocmd BufWriteCmd lua require('harpoon.cmd-ui').on_menu_save()", Harpoon_cmd_bufh ) ) if global_config.save_on_change then vim.cmd( string.format( "autocmd TextChanged,TextChangedI lua require('harpoon.cmd-ui').on_menu_save()", Harpoon_cmd_bufh ) ) end vim.cmd( string.format( "autocmd BufModifiedSet set nomodified", Harpoon_cmd_bufh ) ) end function M.select_menu_item() log.trace("cmd-ui#select_menu_item()") local cmd = vim.fn.line(".") close_menu(true) local answer = vim.fn.input("Terminal index (default to 1): ") if answer == "" then answer = "1" end local idx = tonumber(answer) if idx then term.sendCommand(idx, cmd) end end function M.on_menu_save() log.trace("cmd-ui#on_menu_save()") term.set_cmd_list(get_menu_items()) end return M ================================================ FILE: lua/harpoon/dev.lua ================================================ -- Don't include this file, we should manually include it via -- require("harpoon.dev").reload(); -- -- A quick mapping can be setup using something like: -- :nmap rr :lua require("harpoon.dev").reload() local M = {} function M.reload() require("plenary.reload").reload_module("harpoon") end local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" } local function set_log_level() local log_level = vim.env.HARPOON_LOG or vim.g.harpoon_log_level for _, level in pairs(log_levels) do if level == log_level then return log_level end end return "warn" -- default, if user hasn't set to one from log_levels end local log_level = set_log_level() M.log = require("plenary.log").new({ plugin = "harpoon", level = log_level, }) local log_key = os.time() local function override(key) local fn = M.log[key] M.log[key] = function(...) fn(log_key, ...) end end for _, v in pairs(log_levels) do override(v) end function M.get_log_key() return log_key end return M ================================================ FILE: lua/harpoon/init.lua ================================================ local Path = require("plenary.path") local utils = require("harpoon.utils") local Dev = require("harpoon.dev") local log = Dev.log local config_path = vim.fn.stdpath("config") local data_path = vim.fn.stdpath("data") local user_config = string.format("%s/harpoon.json", config_path) local cache_config = string.format("%s/harpoon.json", data_path) local M = {} local the_primeagen_harpoon = vim.api.nvim_create_augroup( "THE_PRIMEAGEN_HARPOON", { clear = true } ) vim.api.nvim_create_autocmd({ "BufLeave", "VimLeave" }, { callback = function() require("harpoon.mark").store_offset() end, group = the_primeagen_harpoon, }) vim.api.nvim_create_autocmd("FileType", { pattern = "harpoon", group = the_primeagen_harpoon, callback = function() -- Open harpoon file choice in useful ways -- -- vertical split (control+v) vim.keymap.set("n", "", function() local curline = vim.api.nvim_get_current_line() local working_directory = vim.fn.getcwd() .. "/" vim.cmd("vs") vim.cmd("e " .. working_directory .. curline) end, { buffer = true, noremap = true, silent = true }) -- horizontal split (control+x) vim.keymap.set("n", "", function() local curline = vim.api.nvim_get_current_line() local working_directory = vim.fn.getcwd() .. "/" vim.cmd("sp") vim.cmd("e " .. working_directory .. curline) end, { buffer = true, noremap = true, silent = true }) -- new tab (control+t) vim.keymap.set("n", "", function() local curline = vim.api.nvim_get_current_line() local working_directory = vim.fn.getcwd() .. "/" vim.cmd("tabnew") vim.cmd("e " .. working_directory .. curline) end, { buffer = true, noremap = true, silent = true }) end, }) --[[ { projects = { ["/path/to/director"] = { term = { cmds = { } ... is there anything that could be options? }, mark = { marks = { } ... is there anything that could be options? } } }, ... high level settings } --]] HarpoonConfig = HarpoonConfig or {} -- tbl_deep_extend does not work the way you would think local function merge_table_impl(t1, t2) for k, v in pairs(t2) do if type(v) == "table" then if type(t1[k]) == "table" then merge_table_impl(t1[k], v) else t1[k] = v end else t1[k] = v end end end local function mark_config_key(global_settings) global_settings = global_settings or M.get_global_settings() if global_settings.mark_branch then return utils.branch_key() else return utils.project_key() end end local function merge_tables(...) log.trace("_merge_tables()") local out = {} for i = 1, select("#", ...) do merge_table_impl(out, select(i, ...)) end return out end local function ensure_correct_config(config) log.trace("_ensure_correct_config()") local projects = config.projects local mark_key = mark_config_key(config.global_settings) if projects[mark_key] == nil then log.debug("ensure_correct_config(): No config found for:", mark_key) projects[mark_key] = { mark = { marks = {} }, term = { cmds = {}, }, } end local proj = projects[mark_key] if proj.mark == nil then log.debug("ensure_correct_config(): No marks found for", mark_key) proj.mark = { marks = {} } end if proj.term == nil then log.debug( "ensure_correct_config(): No terminal commands found for", mark_key ) proj.term = { cmds = {} } end local marks = proj.mark.marks for idx, mark in pairs(marks) do if type(mark) == "string" then mark = { filename = mark } marks[idx] = mark end marks[idx].filename = utils.normalize_path(mark.filename) end return config end local function expand_dir(config) log.trace("_expand_dir(): Config pre-expansion:", config) local projects = config.projects or {} for k in pairs(projects) do local expanded_path = Path.new(k):expand() projects[expanded_path] = projects[k] if expanded_path ~= k then projects[k] = nil end end log.trace("_expand_dir(): Config post-expansion:", config) return config end function M.save() -- first refresh from disk everything but our project M.refresh_projects_b4update() log.trace("save(): Saving cache config to", cache_config) Path:new(cache_config):write(vim.fn.json_encode(HarpoonConfig), "w") end local function read_config(local_config) log.trace("_read_config():", local_config) return vim.json.decode(Path:new(local_config):read()) end -- 1. saved. Where do we save? function M.setup(config) log.trace("setup(): Setting up...") if not config then config = {} end local ok, u_config = pcall(read_config, user_config) if not ok then log.debug("setup(): No user config present at", user_config) u_config = {} end local ok2, c_config = pcall(read_config, cache_config) if not ok2 then log.debug("setup(): No cache config present at", cache_config) c_config = {} end local complete_config = merge_tables({ projects = {}, global_settings = { ["save_on_toggle"] = false, ["save_on_change"] = true, ["enter_on_sendcmd"] = false, ["tmux_autoclose_windows"] = false, ["excluded_filetypes"] = { "harpoon" }, ["mark_branch"] = false, ["tabline"] = false, ["tabline_suffix"] = " ", ["tabline_prefix"] = " ", }, }, expand_dir(c_config), expand_dir(u_config), expand_dir(config)) -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had -- an object for vim.loop.cwd() ensure_correct_config(complete_config) if complete_config.tabline then require("harpoon.tabline").setup(complete_config) end HarpoonConfig = complete_config log.debug("setup(): Complete config", HarpoonConfig) log.trace("setup(): log_key", Dev.get_log_key()) end function M.get_global_settings() log.trace("get_global_settings()") return HarpoonConfig.global_settings end -- refresh all projects from disk, except our current one function M.refresh_projects_b4update() log.trace( "refresh_projects_b4update(): refreshing other projects", cache_config ) -- save current runtime version of our project config for merging back in later local cwd = mark_config_key() local current_p_config = { projects = { [cwd] = ensure_correct_config(HarpoonConfig).projects[cwd], }, } -- erase all projects from global config, will be loaded back from disk HarpoonConfig.projects = nil -- this reads a stale version of our project but up-to-date versions -- of all other projects local ok2, c_config = pcall(read_config, cache_config) if not ok2 then log.debug( "refresh_projects_b4update(): No cache config present at", cache_config ) c_config = { projects = {} } end -- don't override non-project config in HarpoonConfig later c_config = { projects = c_config.projects } -- erase our own project, will be merged in from current_p_config later c_config.projects[cwd] = nil local complete_config = merge_tables( HarpoonConfig, expand_dir(c_config), expand_dir(current_p_config) ) -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had -- an object for vim.loop.cwd() ensure_correct_config(complete_config) HarpoonConfig = complete_config log.debug("refresh_projects_b4update(): Complete config", HarpoonConfig) log.trace("refresh_projects_b4update(): log_key", Dev.get_log_key()) end function M.get_term_config() log.trace("get_term_config()") return ensure_correct_config(HarpoonConfig).projects[utils.project_key()].term end function M.get_mark_config() log.trace("get_mark_config()") return ensure_correct_config(HarpoonConfig).projects[mark_config_key()].mark end function M.get_menu_config() log.trace("get_menu_config()") return HarpoonConfig.menu or {} end -- should only be called for debug purposes function M.print_config() print(vim.inspect(HarpoonConfig)) end -- Sets a default config with no values M.setup() return M ================================================ FILE: lua/harpoon/mark.lua ================================================ local harpoon = require("harpoon") local utils = require("harpoon.utils") local log = require("harpoon.dev").log -- I think that I may have to organize this better. I am not the biggest fan -- of procedural all the things local M = {} local callbacks = {} -- I am trying to avoid over engineering the whole thing. We will likely only -- need one event emitted local function emit_changed() log.trace("_emit_changed()") local global_settings = harpoon.get_global_settings() if global_settings.save_on_change then harpoon.save() end if global_settings.tabline then vim.cmd("redrawt") end if not callbacks["changed"] then log.trace("_emit_changed(): no callbacks for 'changed', returning") return end for idx, cb in pairs(callbacks["changed"]) do log.trace( string.format( "_emit_changed(): Running callback #%d for 'changed'", idx ) ) cb() end end local function filter_empty_string(list) log.trace("_filter_empty_string()") local next = {} for idx = 1, #list do if list[idx] ~= "" then table.insert(next, list[idx].filename) end end return next end local function get_first_empty_slot() log.trace("_get_first_empty_slot()") for idx = 1, M.get_length() do local filename = M.get_marked_file_name(idx) if filename == "" then return idx end end return M.get_length() + 1 end local function get_buf_name(id) log.trace("_get_buf_name():", id) if id == nil then return utils.normalize_path(vim.api.nvim_buf_get_name(0)) elseif type(id) == "string" then return utils.normalize_path(id) end local idx = M.get_index_of(id) if M.valid_index(idx) then return M.get_marked_file_name(idx) end -- -- not sure what to do here... -- return "" end local function create_mark(filename) local cursor_pos = vim.api.nvim_win_get_cursor(0) log.trace( string.format( "_create_mark(): Creating mark at row: %d, col: %d for %s", cursor_pos[1], cursor_pos[2], filename ) ) return { filename = filename, row = cursor_pos[1], col = cursor_pos[2], } end local function mark_exists(buf_name) log.trace("_mark_exists()") for idx = 1, M.get_length() do if M.get_marked_file_name(idx) == buf_name then log.debug("_mark_exists(): Mark exists", buf_name) return true end end log.debug("_mark_exists(): Mark doesn't exist", buf_name) return false end local function validate_buf_name(buf_name) log.trace("_validate_buf_name():", buf_name) if buf_name == "" or buf_name == nil then log.error( "_validate_buf_name(): Not a valid name for a mark,", buf_name ) error("Couldn't find a valid file name to mark, sorry.") return end end local function filter_filetype() local current_filetype = vim.bo.filetype local excluded_filetypes = harpoon.get_global_settings().excluded_filetypes if current_filetype == "harpoon" then log.error("filter_filetype(): You can't add harpoon to the harpoon") error("You can't add harpoon to the harpoon") return end if vim.tbl_contains(excluded_filetypes, current_filetype) then log.error( 'filter_filetype(): This filetype cannot be added or is included in the "excluded_filetypes" option' ) error( 'This filetype cannot be added or is included in the "excluded_filetypes" option' ) return end end function M.get_index_of(item, marks) log.trace("get_index_of():", item) if item == nil then log.error( "get_index_of(): Function has been supplied with a nil value." ) error( "You have provided a nil value to Harpoon, please provide a string rep of the file or the file idx." ) return end if type(item) == "string" then local relative_item = utils.normalize_path(item) if marks == nil then marks = harpoon.get_mark_config().marks end for idx = 1, M.get_length(marks) do if marks[idx] and marks[idx].filename == relative_item then return idx end end return nil end -- TODO move this to a "harpoon_" prefix or global config? if vim.g.manage_a_mark_zero_index then item = item + 1 end if item <= M.get_length() and item >= 1 then return item end log.debug("get_index_of(): No item found,", item) return nil end function M.status(bufnr) log.trace("status()") local buf_name if bufnr then buf_name = vim.api.nvim_buf_get_name(bufnr) else buf_name = vim.api.nvim_buf_get_name(0) end local norm_name = utils.normalize_path(buf_name) local idx = M.get_index_of(norm_name) if M.valid_index(idx) then return "M" .. idx end return "" end function M.valid_index(idx, marks) log.trace("valid_index():", idx) if idx == nil then return false end local file_name = M.get_marked_file_name(idx, marks) return file_name ~= nil and file_name ~= "" end function M.add_file(file_name_or_buf_id) filter_filetype() local buf_name = get_buf_name(file_name_or_buf_id) log.trace("add_file():", buf_name) if M.valid_index(M.get_index_of(buf_name)) then -- we don't alter file layout. return end validate_buf_name(buf_name) local found_idx = get_first_empty_slot() harpoon.get_mark_config().marks[found_idx] = create_mark(buf_name) M.remove_empty_tail(false) emit_changed() end -- _emit_on_changed == false should only be used internally function M.remove_empty_tail(_emit_on_changed) log.trace("remove_empty_tail()") _emit_on_changed = _emit_on_changed == nil or _emit_on_changed local config = harpoon.get_mark_config() local found = false for i = M.get_length(), 1, -1 do local filename = M.get_marked_file_name(i) if filename ~= "" then return end if filename == "" then table.remove(config.marks, i) found = found or _emit_on_changed end end if found then emit_changed() end end function M.store_offset() log.trace("store_offset()") local ok, res = pcall(function() local marks = harpoon.get_mark_config().marks local buf_name = get_buf_name() local idx = M.get_index_of(buf_name, marks) if not M.valid_index(idx, marks) then return end local cursor_pos = vim.api.nvim_win_get_cursor(0) log.debug( string.format( "store_offset(): Stored row: %d, col: %d", cursor_pos[1], cursor_pos[2] ) ) marks[idx].row = cursor_pos[1] marks[idx].col = cursor_pos[2] end) if not ok then log.warn("store_offset(): Could not store offset:", res) end emit_changed() end function M.rm_file(file_name_or_buf_id) local buf_name = get_buf_name(file_name_or_buf_id) local idx = M.get_index_of(buf_name) log.trace("rm_file(): Removing mark at id", idx) if not M.valid_index(idx) then log.debug("rm_file(): No mark exists for id", file_name_or_buf_id) return end harpoon.get_mark_config().marks[idx] = create_mark("") M.remove_empty_tail(false) emit_changed() end function M.clear_all() harpoon.get_mark_config().marks = {} log.trace("clear_all(): Clearing all marks.") emit_changed() end --- ENTERPRISE PROGRAMMING function M.get_marked_file(idxOrName) log.trace("get_marked_file():", idxOrName) if type(idxOrName) == "string" then idxOrName = M.get_index_of(idxOrName) end return harpoon.get_mark_config().marks[idxOrName] end function M.get_marked_file_name(idx, marks) local mark if marks ~= nil then mark = marks[idx] else mark = harpoon.get_mark_config().marks[idx] end log.trace("get_marked_file_name():", mark and mark.filename) return mark and mark.filename end function M.get_length(marks) if marks == nil then marks = harpoon.get_mark_config().marks end log.trace("get_length()") return table.maxn(marks) end function M.set_current_at(idx) filter_filetype() local buf_name = get_buf_name() log.trace("set_current_at(): Setting id", idx, buf_name) local config = harpoon.get_mark_config() local current_idx = M.get_index_of(buf_name) -- Remove it if it already exists if M.valid_index(current_idx) then config.marks[current_idx] = create_mark("") end config.marks[idx] = create_mark(buf_name) for i = 1, M.get_length() do if not config.marks[i] then config.marks[i] = create_mark("") end end emit_changed() end function M.to_quickfix_list() log.trace("to_quickfix_list(): Sending marks to quickfix list.") local config = harpoon.get_mark_config() local file_list = filter_empty_string(config.marks) local qf_list = {} for idx = 1, #file_list do local mark = M.get_marked_file(idx) qf_list[idx] = { text = string.format("%d: %s", idx, file_list[idx]), filename = mark.filename, row = mark.row, col = mark.col, } end log.debug("to_quickfix_list(): qf_list:", qf_list) vim.fn.setqflist(qf_list) end function M.set_mark_list(new_list) log.trace("set_mark_list(): New list:", new_list) local config = harpoon.get_mark_config() for k, v in pairs(new_list) do if type(v) == "string" then local mark = M.get_marked_file(v) if not mark then mark = create_mark(v) end new_list[k] = mark end end config.marks = new_list emit_changed() end function M.toggle_file(file_name_or_buf_id) local buf_name = get_buf_name(file_name_or_buf_id) log.trace("toggle_file():", buf_name) validate_buf_name(buf_name) if mark_exists(buf_name) then M.rm_file(buf_name) print("Mark removed") log.debug("toggle_file(): Mark removed") else M.add_file(buf_name) print("Mark added") log.debug("toggle_file(): Mark added") end end function M.get_current_index() log.trace("get_current_index()") return M.get_index_of(vim.api.nvim_buf_get_name(0)) end function M.on(event, cb) log.trace("on():", event) if not callbacks[event] then log.debug("on(): no callbacks yet for", event) callbacks[event] = {} end table.insert(callbacks[event], cb) log.debug("on(): All callbacks:", callbacks) end return M ================================================ FILE: lua/harpoon/tabline.lua ================================================ local Dev = require("harpoon.dev") local log = Dev.log local M = {} local function get_color(group, attr) return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), attr) end local function shorten_filenames(filenames) local shortened = {} local counts = {} for _, file in ipairs(filenames) do local name = vim.fn.fnamemodify(file.filename, ":t") counts[name] = (counts[name] or 0) + 1 end for _, file in ipairs(filenames) do local name = vim.fn.fnamemodify(file.filename, ":t") if counts[name] == 1 then table.insert(shortened, { filename = vim.fn.fnamemodify(name, ":t") }) else table.insert(shortened, { filename = file.filename }) end end return shortened end function M.setup(opts) function _G.tabline() local tabs = shorten_filenames(require('harpoon').get_mark_config().marks) local tabline = '' local index = require('harpoon.mark').get_index_of(vim.fn.bufname()) for i, tab in ipairs(tabs) do local is_current = i == index local label if tab.filename == "" or tab.filename == "(empty)" then label = "(empty)" is_current = false else label = tab.filename end if is_current then tabline = tabline .. '%#HarpoonNumberActive#' .. (opts.tabline_prefix or ' ') .. i .. ' %*' .. '%#HarpoonActive#' else tabline = tabline .. '%#HarpoonNumberInactive#' .. (opts.tabline_prefix or ' ') .. i .. ' %*' .. '%#HarpoonInactive#' end tabline = tabline .. label .. (opts.tabline_suffix or ' ') .. '%*' if i < #tabs then tabline = tabline .. '%T' end end return tabline end vim.opt.showtabline = 2 vim.o.tabline = '%!v:lua.tabline()' vim.api.nvim_create_autocmd("ColorScheme", { group = vim.api.nvim_create_augroup("harpoon", { clear = true }), pattern = { "*" }, callback = function() local color = get_color('HarpoonActive', 'bg#') if (color == "" or color == nil) then vim.api.nvim_set_hl(0, "HarpoonInactive", { link = "Tabline" }) vim.api.nvim_set_hl(0, "HarpoonActive", { link = "TablineSel" }) vim.api.nvim_set_hl(0, "HarpoonNumberActive", { link = "TablineSel" }) vim.api.nvim_set_hl(0, "HarpoonNumberInactive", { link = "Tabline" }) end end, }) log.debug("setup(): Tabline Setup", opts) end return M ================================================ FILE: lua/harpoon/term.lua ================================================ local harpoon = require("harpoon") local log = require("harpoon.dev").log local global_config = harpoon.get_global_settings() local M = {} local terminals = {} local function create_terminal(create_with) if not create_with then create_with = ":terminal" end log.trace("term: _create_terminal(): Init:", create_with) local current_id = vim.api.nvim_get_current_buf() vim.cmd(create_with) local buf_id = vim.api.nvim_get_current_buf() local term_id = vim.b.terminal_job_id if term_id == nil then log.error("_create_terminal(): term_id is nil") -- TODO: Throw an error? return nil end -- Make sure the term buffer has "hidden" set so it doesn't get thrown -- away and cause an error vim.api.nvim_buf_set_option(buf_id, "bufhidden", "hide") -- Resets the buffer back to the old one vim.api.nvim_set_current_buf(current_id) return buf_id, term_id end local function find_terminal(args) log.trace("term: _find_terminal(): Terminal:", args) if type(args) == "number" then args = { idx = args } end local term_handle = terminals[args.idx] if not term_handle or not vim.api.nvim_buf_is_valid(term_handle.buf_id) then local buf_id, term_id = create_terminal(args.create_with) if buf_id == nil then error("Failed to find and create terminal.") return end term_handle = { buf_id = buf_id, term_id = term_id, } terminals[args.idx] = term_handle end return term_handle end local function get_first_empty_slot() log.trace("_get_first_empty_slot()") for idx, cmd in pairs(harpoon.get_term_config().cmds) do if cmd == "" then return idx end end return M.get_length() + 1 end function M.gotoTerminal(idx) log.trace("term: gotoTerminal(): Terminal:", idx) local term_handle = find_terminal(idx) vim.api.nvim_set_current_buf(term_handle.buf_id) end function M.sendCommand(idx, cmd, ...) log.trace("term: sendCommand(): Terminal:", idx) local term_handle = find_terminal(idx) if type(cmd) == "number" then cmd = harpoon.get_term_config().cmds[cmd] end if global_config.enter_on_sendcmd then cmd = cmd .. "\n" end if cmd then log.debug("sendCommand:", cmd) vim.api.nvim_chan_send(term_handle.term_id, string.format(cmd, ...)) end end function M.clear_all() log.trace("term: clear_all(): Clearing all terminals.") for _, term in ipairs(terminals) do vim.api.nvim_buf_delete(term.buf_id, { force = true }) end terminals = {} end function M.get_length() log.trace("_get_length()") return table.maxn(harpoon.get_term_config().cmds) end function M.valid_index(idx) if idx == nil or idx > M.get_length() or idx <= 0 then return false end return true end function M.emit_changed() log.trace("_emit_changed()") if harpoon.get_global_settings().save_on_change then harpoon.save() end end function M.add_cmd(cmd) log.trace("add_cmd()") local found_idx = get_first_empty_slot() harpoon.get_term_config().cmds[found_idx] = cmd M.emit_changed() end function M.rm_cmd(idx) log.trace("rm_cmd()") if not M.valid_index(idx) then log.debug("rm_cmd(): no cmd exists for index", idx) return end table.remove(harpoon.get_term_config().cmds, idx) M.emit_changed() end function M.set_cmd_list(new_list) log.trace("set_cmd_list(): New list:", new_list) for k in pairs(harpoon.get_term_config().cmds) do harpoon.get_term_config().cmds[k] = nil end for k, v in pairs(new_list) do harpoon.get_term_config().cmds[k] = v end M.emit_changed() end return M ================================================ FILE: lua/harpoon/test/manage-a-mark.lua ================================================ -- TODO: Harpooned -- local Marker = require('harpoon.mark') -- local eq = assert.are.same ================================================ FILE: lua/harpoon/test/manage_cmd_spec.lua ================================================ local harpoon = require("harpoon") local term = require("harpoon.term") local function assert_table_equals(tbl1, tbl2) if #tbl1 ~= #tbl2 then assert(false, "" .. #tbl1 .. " != " .. #tbl2) end for i = 1, #tbl1 do if tbl1[i] ~= tbl2[i] then assert.equals(tbl1[i], tbl2[i]) end end end describe("basic functionalities", function() local emitted local cmds before_each(function() emitted = false cmds = {} harpoon.get_term_config = function() return { cmds = cmds, } end term.emit_changed = function() emitted = true end end) it("add_cmd for empty", function() term.add_cmd("cmake ..") local expected_result = { "cmake ..", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) end) it("add_cmd for non_empty", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") local expected_result = { "cmake ..", "make", "ninja", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) end) it("rm_cmd: removing a valid element", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") term.rm_cmd(2) local expected_result = { "cmake ..", "ninja", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) end) it("rm_cmd: remove first element", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") term.rm_cmd(1) local expected_result = { "make", "ninja", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) end) it("rm_cmd: remove last element", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") term.rm_cmd(3) local expected_result = { "cmake ..", "make", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) end) it("rm_cmd: trying to remove invalid element", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") term.rm_cmd(5) local expected_result = { "cmake ..", "make", "ninja", } assert_table_equals(harpoon.get_term_config().cmds, expected_result) assert.equals(emitted, true) term.rm_cmd(0) assert_table_equals(harpoon.get_term_config().cmds, expected_result) term.rm_cmd(-1) assert_table_equals(harpoon.get_term_config().cmds, expected_result) end) it("get_length", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") assert.equals(term.get_length(), 3) end) it("valid_index", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") assert(term.valid_index(1)) assert(term.valid_index(2)) assert(term.valid_index(3)) assert(not term.valid_index(0)) assert(not term.valid_index(-1)) assert(not term.valid_index(4)) end) it("set_cmd_list", function() term.add_cmd("cmake ..") term.add_cmd("make") term.add_cmd("ninja") term.set_cmd_list({ "make uninstall", "make install" }) local expected_result = { "make uninstall", "make install", } assert_table_equals(expected_result, harpoon.get_term_config().cmds) end) end) ================================================ FILE: lua/harpoon/tmux.lua ================================================ local harpoon = require("harpoon") local log = require("harpoon.dev").log local global_config = harpoon.get_global_settings() local utils = require("harpoon.utils") local M = {} local tmux_windows = {} if global_config.tmux_autoclose_windows then local harpoon_tmux_group = vim.api.nvim_create_augroup( "HARPOON_TMUX", { clear = true } ) vim.api.nvim_create_autocmd("VimLeave", { callback = function() require("harpoon.tmux").clear_all() end, group = harpoon_tmux_group, }) end local function create_terminal() log.trace("tmux: _create_terminal())") local window_id -- Create a new tmux window and store the window id local out, ret, _ = utils.get_os_command_output({ "tmux", "new-window", "-P", "-F", "#{pane_id}", }, vim.loop.cwd()) if ret == 0 then window_id = out[1]:sub(2) end if window_id == nil then log.error("tmux: _create_terminal(): window_id is nil") return nil end return window_id end -- Checks if the tmux window with the given window id exists local function terminal_exists(window_id) log.trace("_terminal_exists(): Window:", window_id) local exists = false local window_list, _, _ = utils.get_os_command_output({ "tmux", "list-windows", }, vim.loop.cwd()) -- This has to be done this way because tmux has-session does not give -- updated results for _, line in pairs(window_list) do local window_info = utils.split_string(line, "@")[2] if string.find(window_info, string.sub(window_id, 2)) then exists = true end end return exists end local function find_terminal(args) log.trace("tmux: _find_terminal(): Window:", args) if type(args) == "string" then -- assume args is a valid tmux target identifier -- if invalid, the error returned by tmux will be thrown return { window_id = args, pane = true, } end if type(args) == "number" then args = { idx = args } end local window_handle = tmux_windows[args.idx] local window_exists if window_handle then window_exists = terminal_exists(window_handle.window_id) end if not window_handle or not window_exists then local window_id = create_terminal() if window_id == nil then error("Failed to find and create tmux window.") return end window_handle = { window_id = "%" .. window_id, } tmux_windows[args.idx] = window_handle end return window_handle end local function get_first_empty_slot() log.trace("_get_first_empty_slot()") for idx, cmd in pairs(harpoon.get_term_config().cmds) do if cmd == "" then return idx end end return M.get_length() + 1 end function M.gotoTerminal(idx) log.trace("tmux: gotoTerminal(): Window:", idx) local window_handle = find_terminal(idx) local _, ret, stderr = utils.get_os_command_output({ "tmux", window_handle.pane and "select-pane" or "select-window", "-t", window_handle.window_id, }, vim.loop.cwd()) if ret ~= 0 then error("Failed to go to terminal." .. stderr[1]) end end function M.sendCommand(idx, cmd, ...) log.trace("tmux: sendCommand(): Window:", idx) local window_handle = find_terminal(idx) if type(cmd) == "number" then cmd = harpoon.get_term_config().cmds[cmd] end if global_config.enter_on_sendcmd then cmd = cmd .. "\n" end if cmd then log.debug("sendCommand:", cmd) local _, ret, stderr = utils.get_os_command_output({ "tmux", "send-keys", "-t", window_handle.window_id, string.format(cmd, ...), }, vim.loop.cwd()) if ret ~= 0 then error("Failed to send command. " .. stderr[1]) end end end function M.clear_all() log.trace("tmux: clear_all(): Clearing all tmux windows.") for _, window in pairs(tmux_windows) do -- Delete the current tmux window utils.get_os_command_output({ "tmux", "kill-window", "-t", window.window_id, }, vim.loop.cwd()) end tmux_windows = {} end function M.get_length() log.trace("_get_length()") return table.maxn(harpoon.get_term_config().cmds) end function M.valid_index(idx) if idx == nil or idx > M.get_length() or idx <= 0 then return false end return true end function M.emit_changed() log.trace("_emit_changed()") if harpoon.get_global_settings().save_on_change then harpoon.save() end end function M.add_cmd(cmd) log.trace("add_cmd()") local found_idx = get_first_empty_slot() harpoon.get_term_config().cmds[found_idx] = cmd M.emit_changed() end function M.rm_cmd(idx) log.trace("rm_cmd()") if not M.valid_index(idx) then log.debug("rm_cmd(): no cmd exists for index", idx) return end table.remove(harpoon.get_term_config().cmds, idx) M.emit_changed() end function M.set_cmd_list(new_list) log.trace("set_cmd_list(): New list:", new_list) for k in pairs(harpoon.get_term_config().cmds) do harpoon.get_term_config().cmds[k] = nil end for k, v in pairs(new_list) do harpoon.get_term_config().cmds[k] = v end M.emit_changed() end return M ================================================ FILE: lua/harpoon/ui.lua ================================================ local harpoon = require("harpoon") local popup = require("plenary.popup") local Marked = require("harpoon.mark") local utils = require("harpoon.utils") local log = require("harpoon.dev").log local M = {} Harpoon_win_id = nil Harpoon_bufh = nil -- We save before we close because we use the state of the buffer as the list -- of items. local function close_menu(force_save) force_save = force_save or false local global_config = harpoon.get_global_settings() if global_config.save_on_toggle or force_save then require("harpoon.ui").on_menu_save() end vim.api.nvim_win_close(Harpoon_win_id, true) Harpoon_win_id = nil Harpoon_bufh = nil end local function create_window() log.trace("_create_window()") local config = harpoon.get_menu_config() local width = config.width or 60 local height = config.height or 10 local borderchars = config.borderchars or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } local bufnr = vim.api.nvim_create_buf(false, false) local Harpoon_win_id, win = popup.create(bufnr, { title = "Harpoon", highlight = "HarpoonWindow", line = math.floor(((vim.o.lines - height) / 2) - 1), col = math.floor((vim.o.columns - width) / 2), minwidth = width, minheight = height, borderchars = borderchars, }) vim.api.nvim_win_set_option( win.border.win_id, "winhl", "Normal:HarpoonBorder" ) return { bufnr = bufnr, win_id = Harpoon_win_id, } end local function get_menu_items() log.trace("_get_menu_items()") local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true) local indices = {} for _, line in pairs(lines) do if not utils.is_white_space(line) then table.insert(indices, line) end end return indices end function M.toggle_quick_menu() log.trace("toggle_quick_menu()") if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then close_menu() return end local curr_file = utils.normalize_path(vim.api.nvim_buf_get_name(0)) vim.cmd( string.format( "autocmd Filetype harpoon " .. "let path = '%s' | call clearmatches() | " -- move the cursor to the line containing the current filename .. "call search('\\V'.path.'\\$') | " -- add a hl group to that line .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", curr_file:gsub("\\", "\\\\") ) ) local win_info = create_window() local contents = {} local global_config = harpoon.get_global_settings() Harpoon_win_id = win_info.win_id Harpoon_bufh = win_info.bufnr for idx = 1, Marked.get_length() do local file = Marked.get_marked_file_name(idx) if file == "" then file = "(empty)" end contents[idx] = string.format("%s", file) end vim.api.nvim_win_set_option(Harpoon_win_id, "number", true) vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu") vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents) vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon") vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite") vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete") vim.api.nvim_buf_set_keymap( Harpoon_bufh, "n", "q", "lua require('harpoon.ui').toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( Harpoon_bufh, "n", "", "lua require('harpoon.ui').toggle_quick_menu()", { silent = true } ) vim.api.nvim_buf_set_keymap( Harpoon_bufh, "n", "", "lua require('harpoon.ui').select_menu_item()", {} ) vim.cmd( string.format( "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", Harpoon_bufh ) ) if global_config.save_on_change then vim.cmd( string.format( "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", Harpoon_bufh ) ) end vim.cmd( string.format( "autocmd BufModifiedSet set nomodified", Harpoon_bufh ) ) vim.cmd( "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" ) end function M.select_menu_item() local idx = vim.fn.line(".") close_menu(true) M.nav_file(idx) end function M.on_menu_save() log.trace("on_menu_save()") Marked.set_mark_list(get_menu_items()) end local function get_or_create_buffer(filename) local buf_exists = vim.fn.bufexists(filename) ~= 0 if buf_exists then return vim.fn.bufnr(filename) end return vim.fn.bufadd(filename) end function M.nav_file(id) log.trace("nav_file(): Navigating to", id) local idx = Marked.get_index_of(id) if not Marked.valid_index(idx) then log.debug("nav_file(): No mark exists for id", id) return end local mark = Marked.get_marked_file(idx) local filename = vim.fs.normalize(mark.filename) local buf_id = get_or_create_buffer(filename) local set_row = not vim.api.nvim_buf_is_loaded(buf_id) local old_bufnr = vim.api.nvim_get_current_buf() vim.api.nvim_set_current_buf(buf_id) vim.api.nvim_buf_set_option(buf_id, "buflisted", true) if set_row and mark.row and mark.col then vim.api.nvim_win_set_cursor(0, { mark.row, mark.col }) log.debug( string.format( "nav_file(): Setting cursor to row: %d, col: %d", mark.row, mark.col ) ) end local old_bufinfo = vim.fn.getbufinfo(old_bufnr) if type(old_bufinfo) == "table" and #old_bufinfo >= 1 then old_bufinfo = old_bufinfo[1] local no_name = old_bufinfo.name == "" local one_line = old_bufinfo.linecount == 1 local unchanged = old_bufinfo.changed == 0 if no_name and one_line and unchanged then vim.api.nvim_buf_delete(old_bufnr, {}) end end end function M.location_window(options) local default_options = { relative = "editor", style = "minimal", width = 30, height = 15, row = 2, col = 2, } options = vim.tbl_extend("keep", options, default_options) local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) local win_id = vim.api.nvim_open_win(bufnr, true, options) return { bufnr = bufnr, win_id = win_id, } end function M.notification(text) local win_stats = vim.api.nvim_list_uis()[1] local win_width = win_stats.width local prev_win = vim.api.nvim_get_current_win() local info = M.location_window({ width = 20, height = 2, row = 1, col = win_width - 21, }) vim.api.nvim_buf_set_lines( info.bufnr, 0, 5, false, { "!!! Notification", text } ) vim.api.nvim_set_current_win(prev_win) return { bufnr = info.bufnr, win_id = info.win_id, } end function M.close_notification(bufnr) vim.api.nvim_buf_delete(bufnr) end function M.nav_next() log.trace("nav_next()") local current_index = Marked.get_current_index() local number_of_items = Marked.get_length() if current_index == nil then current_index = 1 else current_index = current_index + 1 end if current_index > number_of_items then current_index = 1 end M.nav_file(current_index) end function M.nav_prev() log.trace("nav_prev()") local current_index = Marked.get_current_index() local number_of_items = Marked.get_length() if current_index == nil then current_index = number_of_items else current_index = current_index - 1 end if current_index < 1 then current_index = number_of_items end M.nav_file(current_index) end return M ================================================ FILE: lua/harpoon/utils.lua ================================================ local Path = require("plenary.path") local data_path = vim.fn.stdpath("data") local Job = require("plenary.job") local M = {} M.data_path = data_path function M.project_key() return vim.loop.cwd() end function M.branch_key() local branch -- use tpope's fugitive for faster branch name resolution if available if vim.fn.exists("*FugitiveHead") == 1 then branch = vim.fn["FugitiveHead"]() -- return "HEAD" for parity with `git rev-parse` in detached head state if #branch == 0 then branch = "HEAD" end else -- `git branch --show-current` requires Git v2.22.0+ so going with more -- widely available command branch = M.get_os_command_output({ "git", "rev-parse", "--abbrev-ref", "HEAD", })[1] end if branch then return vim.loop.cwd() .. "-" .. branch else return M.project_key() end end function M.normalize_path(item) return Path:new(item):make_relative(M.project_key()) end function M.get_os_command_output(cmd, cwd) if type(cmd) ~= "table" then print("Harpoon: [get_os_command_output]: cmd has to be a table") return {} end local command = table.remove(cmd, 1) local stderr = {} local stdout, ret = Job :new({ command = command, args = cmd, cwd = cwd, on_stderr = function(_, data) table.insert(stderr, data) end, }) :sync() return stdout, ret, stderr end function M.split_string(str, delimiter) local result = {} for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do table.insert(result, match) end return result end function M.is_white_space(str) return str:gsub("%s", "") == "" end return M ================================================ FILE: lua/telescope/_extensions/harpoon.lua ================================================ local has_telescope, telescope = pcall(require, "telescope") if not has_telescope then error("harpoon.nvim requires nvim-telescope/telescope.nvim") end return telescope.register_extension({ exports = { marks = require("telescope._extensions.marks"), }, }) ================================================ FILE: lua/telescope/_extensions/marks.lua ================================================ local action_state = require("telescope.actions.state") local action_utils = require("telescope.actions.utils") local entry_display = require("telescope.pickers.entry_display") local finders = require("telescope.finders") local pickers = require("telescope.pickers") local conf = require("telescope.config").values local harpoon = require("harpoon") local harpoon_mark = require("harpoon.mark") local function prepare_results(list) local next = {} for idx = 1, #list do if list[idx].filename ~= "" then list[idx].index = idx table.insert(next, list[idx]) end end return next end local generate_new_finder = function() return finders.new_table({ results = prepare_results(harpoon.get_mark_config().marks), entry_maker = function(entry) local line = entry.filename .. ":" .. entry.row .. ":" .. entry.col local displayer = entry_display.create({ separator = " - ", items = { { width = 2 }, { width = 50 }, { remaining = true }, }, }) local make_display = function() return displayer({ tostring(entry.index), line, }) end return { value = entry, ordinal = line, display = make_display, lnum = entry.row, col = entry.col, filename = entry.filename, } end, }) end local delete_harpoon_mark = function(prompt_bufnr) local confirmation = vim.fn.input( string.format("Delete current mark(s)? [y/n]: ") ) if string.len(confirmation) == 0 or string.sub(string.lower(confirmation), 0, 1) ~= "y" then print(string.format("Didn't delete mark")) return end local selection = action_state.get_selected_entry() harpoon_mark.rm_file(selection.filename) local function get_selections() local results = {} action_utils.map_selections(prompt_bufnr, function(entry) table.insert(results, entry) end) return results end local selections = get_selections() for _, current_selection in ipairs(selections) do harpoon_mark.rm_file(current_selection.filename) end local current_picker = action_state.get_current_picker(prompt_bufnr) current_picker:refresh(generate_new_finder(), { reset_prompt = true }) end local move_mark_up = function(prompt_bufnr) local selection = action_state.get_selected_entry() local length = harpoon_mark.get_length() if selection.index == length then return end local mark_list = harpoon.get_mark_config().marks table.remove(mark_list, selection.index) table.insert(mark_list, selection.index + 1, selection.value) local current_picker = action_state.get_current_picker(prompt_bufnr) current_picker:refresh(generate_new_finder(), { reset_prompt = true }) end local move_mark_down = function(prompt_bufnr) local selection = action_state.get_selected_entry() if selection.index == 1 then return end local mark_list = harpoon.get_mark_config().marks table.remove(mark_list, selection.index) table.insert(mark_list, selection.index - 1, selection.value) local current_picker = action_state.get_current_picker(prompt_bufnr) current_picker:refresh(generate_new_finder(), { reset_prompt = true }) end return function(opts) opts = opts or {} pickers.new(opts, { prompt_title = "harpoon marks", finder = generate_new_finder(), sorter = conf.generic_sorter(opts), previewer = conf.grep_previewer(opts), attach_mappings = function(_, map) map("i", "", delete_harpoon_mark) map("n", "", delete_harpoon_mark) map("i", "", move_mark_up) map("n", "", move_mark_up) map("i", "", move_mark_down) map("n", "", move_mark_down) return true end, }):find() end ================================================ FILE: scripts/tmux/switch-back-to-nvim ================================================ #!/usr/bin/env bash # Make sure tmux is running tmux_running=$(pgrep tmux) if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then echo "tmux needs to be running" exit 1 fi # Switch to a window called nvim in tmux - if it exists session_name=$(tmux display-message -p "#S") tmux switch-client -t "$session_name:nvim"