├── .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 ├── harpoon.png ├── 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 /.editorconfig: -------------------------------------------------------------------------------- 1 | # top-most EditorConfig file 2 | root = true 3 | 4 | # Unix-style newlines with a newline ending every file 5 | [*] 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 4 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: theprimeagen 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Found something wrong with Harpoon2? 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **WARNING** 11 | 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 12 | 13 | Please use `harpoon2` for branch 14 | --------------- 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What issue are you having that you need harpoon to solve?** 11 | 12 | **Why doesn't the current config help?** 13 | 14 | **What proposed api changes are you suggesting?** 15 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | format: 7 | name: Stylua 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - run: date +%W > weekly 12 | 13 | - name: Restore cache 14 | id: cache 15 | uses: actions/cache@v2 16 | with: 17 | path: | 18 | ~/.cargo/bin 19 | key: ${{ runner.os }}-cargo-${{ hashFiles('weekly') }} 20 | 21 | - name: Install 22 | if: steps.cache.outputs.cache-hit != 'true' 23 | run: cargo install stylua 24 | 25 | - name: Format 26 | run: stylua --check lua/ --config-path=.stylua.toml 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | name: Luacheck 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Setup 12 | run: | 13 | sudo apt-get update 14 | sudo apt-get install luarocks 15 | sudo luarocks install luacheck 16 | 17 | - name: Lint 18 | run: luacheck lua/ --globals vim 19 | -------------------------------------------------------------------------------- /.luacheckrc: -------------------------------------------------------------------------------- 1 | std = luajit 2 | cache = true 3 | codes = true 4 | 5 | globals = { 6 | "HarpoonConfig", 7 | "Harpoon_bufh", 8 | "Harpoon_win_id", 9 | "Harpoon_cmd_win_id", 10 | "Harpoon_cmd_bufh", 11 | } 12 | read_globals = { "vim" } 13 | -------------------------------------------------------------------------------- /.stylua.toml: -------------------------------------------------------------------------------- 1 | column_width = 80 2 | line_endings = "Unix" 3 | indent_type = "Spaces" 4 | indent_width = 4 5 | quote_style = "AutoPreferDouble" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 ThePrimeagen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | fmt: 2 | echo "===> Formatting" 3 | stylua lua/ --config-path=.stylua.toml 4 | 5 | lint: 6 | echo "===> Linting" 7 | luacheck lua/ --globals vim 8 | 9 | pr-ready: fmt lint 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | ## ⇁ HARPOON 2 4 | This is a deprecated and all future changes will be to the branch `harpoon2`. 5 | 6 | [Harpoon 2](https://github.com/ThePrimeagen/harpoon/tree/harpoon2) 7 | 8 | **STATUS**: Merging into mainline April 20th or June 9th (nice) 9 | 10 | ------------------------------- 11 | # Legacy Harpoon README 12 | 13 | # Harpoon 14 | ##### Getting you where you want with the fewest keystrokes. 15 | 16 | [![Lua](https://img.shields.io/badge/Lua-blue.svg?style=for-the-badge&logo=lua)](http://www.lua.org) 17 | [![Neovim](https://img.shields.io/badge/Neovim%200.5+-green.svg?style=for-the-badge&logo=neovim)](https://neovim.io) 18 |
19 | 20 | ![Harpoon](harpoon.png) 21 | -- image provided by **Bob Rust** 22 | 23 | ## ⇁ WIP 24 | This is not fully baked, though used by several people. If you experience any 25 | issues, see some improvement you think would be amazing, or just have some 26 | feedback for harpoon (or me), make an issue! 27 | 28 | 29 | ## ⇁ The Problems: 30 | 1. You're working on a codebase. medium, large, tiny, whatever. You find 31 | yourself frequenting a small set of files and you are tired of using a fuzzy finder, 32 | `:bnext` & `:bprev` are getting too repetitive, alternate file doesn't quite cut it, etc etc. 33 | 1. You want to execute some project specific commands or have any number of 34 | persistent terminals that can be easily navigated to. 35 | 36 | 37 | ## ⇁ The Solutions: 38 | 1. The ability to specify, or on the fly, mark and create persisting key strokes 39 | to go to the files you want. 40 | 1. Unlimited terminals and navigation. 41 | 42 | 43 | ## ⇁ Installation 44 | * neovim 0.5.0+ required 45 | * install using your favorite plugin manager (`vim-plug` in this example) 46 | ```vim 47 | Plug 'nvim-lua/plenary.nvim' " don't forget to add this one if you don't have it yet! 48 | Plug 'ThePrimeagen/harpoon' 49 | ``` 50 | 51 | ## ⇁ Harpooning 52 | here we'll explain how to wield the power of the harpoon: 53 | 54 | 55 | ### Marks 56 | you mark files you want to revisit later on 57 | ```lua 58 | :lua require("harpoon.mark").add_file() 59 | ``` 60 | 61 | ### File Navigation 62 | view all project marks with: 63 | ```lua 64 | :lua require("harpoon.ui").toggle_quick_menu() 65 | ``` 66 | you can go up and down the list, enter, delete or reorder. `q` and `` exit and save the menu 67 | 68 | you also can switch to any mark without bringing up the menu, use the below with the desired mark index 69 | ```lua 70 | :lua require("harpoon.ui").nav_file(3) -- navigates to file 3 71 | ``` 72 | you can also cycle the list in both directions 73 | ```lua 74 | :lua require("harpoon.ui").nav_next() -- navigates to next mark 75 | :lua require("harpoon.ui").nav_prev() -- navigates to previous mark 76 | ``` 77 | from the quickmenu, open a file in: 78 | a vertical split with control+v, 79 | a horizontal split with control+x, 80 | a new tab with control+t 81 | 82 | ### Terminal Navigation 83 | this works like file navigation except that if there is no terminal at the specified index 84 | a new terminal is created. 85 | ```lua 86 | lua require("harpoon.term").gotoTerminal(1) -- navigates to term 1 87 | ``` 88 | 89 | ### Commands to Terminals 90 | commands can be sent to any terminal 91 | ```lua 92 | lua require("harpoon.term").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 93 | ``` 94 | further more commands can be stored for later quick 95 | ```lua 96 | lua require('harpoon.cmd-ui').toggle_quick_menu() -- shows the commands menu 97 | lua require("harpoon.term").sendCommand(1, 1) -- sends command 1 to term 1 98 | ``` 99 | 100 | ### Tmux Support 101 | tmux is supported out of the box and can be used as a drop-in replacement to normal terminals 102 | by simply switching `'term' with 'tmux'` like so 103 | 104 | ```lua 105 | lua require("harpoon.tmux").gotoTerminal(1) -- goes to the first tmux window 106 | lua require("harpoon.tmux").sendCommand(1, "ls -La") -- sends ls -La to tmux window 1 107 | lua require("harpoon.tmux").sendCommand(1, 1) -- sends command 1 to tmux window 1 108 | ``` 109 | 110 | `sendCommand` and `goToTerminal` also accept any valid [tmux pane identifier](https://man7.org/linux/man-pages/man1/tmux.1.html#COMMANDS). 111 | ```lua 112 | lua require("harpoon.tmux").gotoTerminal("{down-of}") -- focus the pane directly below 113 | lua require("harpoon.tmux").sendCommand("%3", "ls") -- send a command to the pane with id '%3' 114 | ``` 115 | 116 | Once you switch to a tmux window you can always switch back to neovim, this is a 117 | little bash script that will switch to the window which is running neovim. 118 | 119 | In your `tmux.conf` (or anywhere you have keybinds), add this 120 | ```bash 121 | bind-key -r G run-shell "path-to-harpoon/harpoon/scripts/tmux/switch-back-to-nvim" 122 | ``` 123 | 124 | ### Telescope Support 125 | 1st register harpoon as a telescope extension 126 | ```lua 127 | require("telescope").load_extension('harpoon') 128 | ``` 129 | currently only marks are supported in telescope 130 | ``` 131 | :Telescope harpoon marks 132 | ``` 133 | 134 | ## ⇁ Configuration 135 | if configuring harpoon is desired it must be done through harpoons setup function 136 | ```lua 137 | require("harpoon").setup({ ... }) 138 | ``` 139 | 140 | ### Global Settings 141 | here are all the available global settings with their default values 142 | ```lua 143 | global_settings = { 144 | -- sets the marks upon calling `toggle` on the ui, instead of require `:w`. 145 | save_on_toggle = false, 146 | 147 | -- saves the harpoon file upon every change. disabling is unrecommended. 148 | save_on_change = true, 149 | 150 | -- sets harpoon to run the command immediately as it's passed to the terminal when calling `sendCommand`. 151 | enter_on_sendcmd = false, 152 | 153 | -- closes any tmux windows harpoon that harpoon creates when you close Neovim. 154 | tmux_autoclose_windows = false, 155 | 156 | -- filetypes that you want to prevent from adding to the harpoon list menu. 157 | excluded_filetypes = { "harpoon" }, 158 | 159 | -- set marks specific to each git branch inside git repository 160 | mark_branch = false, 161 | 162 | -- enable tabline with harpoon marks 163 | tabline = false, 164 | tabline_prefix = " ", 165 | tabline_suffix = " ", 166 | } 167 | ``` 168 | 169 | 170 | ### Preconfigured Terminal Commands 171 | to preconfigure terminal commands for later use 172 | ```lua 173 | projects = { 174 | -- Yes $HOME works 175 | ["$HOME/personal/vim-with-me/server"] = { 176 | term = { 177 | cmds = { 178 | "./env && npx ts-node src/index.ts" 179 | } 180 | } 181 | } 182 | } 183 | ``` 184 | 185 | ## ⇁ Logging 186 | - logs are written to `harpoon.log` within the nvim cache path (`:echo stdpath("cache")`) 187 | - available log levels are `trace`, `debug`, `info`, `warn`, `error`, or `fatal`. `warn` is default 188 | - log level can be set with `vim.g.harpoon_log_level` (must be **before** `setup()`) 189 | - launching nvim with `HARPOON_LOG=debug nvim` takes precedence over `vim.g.harpoon_log_level`. 190 | - invalid values default back to `warn`. 191 | 192 | ## ⇁ Others 193 | #### How do Harpoon marks differ from vim global marks 194 | they serve a similar purpose however harpoon marks differ in a few key ways: 195 | 1. They auto update their position within the file 196 | 1. They are saved _per project_. 197 | 1. They can be hand edited vs replaced (swapping is easier) 198 | 199 | #### The Motivation behind Harpoon terminals 200 | 1. I want to use the terminal since I can gF and gF to any errors arising 201 | from execution that are within the terminal that are not appropriate for 202 | something like dispatch. (not just running tests but perhaps a server that runs 203 | for X amount of time before crashing). 204 | 1. I want the terminal to be persistent and I can return to one of many terminals 205 | with some finger wizardry and reparse any of the execution information that was 206 | not necessarily error related. 207 | 1. I would like to have commands that can be tied to terminals and sent them 208 | without much thinking. Some sort of middle ground between vim-test and just 209 | typing them into a terminal (configuring netflix's television project isn't 210 | quite building and there are tons of ways to configure). 211 | 212 | #### Use a dynamic width for the Harpoon popup menu 213 | Sometimes the default width of `60` is not wide enough. 214 | The following example demonstrates how to configure a custom width by setting 215 | the menu's width relative to the current window's width. 216 | 217 | ```lua 218 | require("harpoon").setup({ 219 | menu = { 220 | width = vim.api.nvim_win_get_width(0) - 4, 221 | } 222 | }) 223 | ``` 224 | 225 | 226 | #### Tabline 227 | 228 | By default, the tabline will use the default theme of your theme. You can customize by editing the following highlights: 229 | 230 | * HarpoonInactive 231 | * HarpoonActive 232 | * HarpoonNumberActive 233 | * HarpoonNumberInactive 234 | 235 | Example to make it cleaner: 236 | 237 | ```lua 238 | vim.cmd('highlight! HarpoonInactive guibg=NONE guifg=#63698c') 239 | vim.cmd('highlight! HarpoonActive guibg=NONE guifg=white') 240 | vim.cmd('highlight! HarpoonNumberActive guibg=NONE guifg=#7aa2f7') 241 | vim.cmd('highlight! HarpoonNumberInactive guibg=NONE guifg=#7aa2f7') 242 | vim.cmd('highlight! TabLineFill guibg=NONE guifg=white') 243 | ``` 244 | 245 | Result: 246 | ![tabline](https://i.imgur.com/8i8mKJD.png) 247 | 248 | ## ⇁ Social 249 | For questions about Harpoon, there's a #harpoon channel on [the Primeagen's Discord](https://discord.gg/theprimeagen) server. 250 | * [Discord](https://discord.gg/theprimeagen) 251 | * [Twitch](https://www.twitch.tv/theprimeagen) 252 | * [Twitter](https://twitter.com/ThePrimeagen) 253 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### Manage A Mark 1.0 2 | * Logo 3 | * floating term / split term 4 | * TODO: Fill me in, that one really important thing.... 5 | * README.md 6 | 7 | ### Harpoon (upon requests) 8 | * Add hooks for vim so that someone can make it for me 9 | * ackshual tests. 10 | * interactive menu 11 | * cycle 12 | * make setup() callable more than once and just layer in the commands 13 | -------------------------------------------------------------------------------- /harpoon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ThePrimeagen/harpoon/1bc17e3e42ea3c46b33c0bbad6a880792692a1b3/harpoon.png -------------------------------------------------------------------------------- /lua/harpoon/cmd-ui.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local popup = require("plenary.popup") 3 | local utils = require("harpoon.utils") 4 | local log = require("harpoon.dev").log 5 | local term = require("harpoon.term") 6 | 7 | local M = {} 8 | 9 | Harpoon_cmd_win_id = nil 10 | Harpoon_cmd_bufh = nil 11 | 12 | local function close_menu(force_save) 13 | force_save = force_save or false 14 | local global_config = harpoon.get_global_settings() 15 | 16 | if global_config.save_on_toggle or force_save then 17 | require("harpoon.cmd-ui").on_menu_save() 18 | end 19 | 20 | vim.api.nvim_win_close(Harpoon_cmd_win_id, true) 21 | 22 | Harpoon_cmd_win_id = nil 23 | Harpoon_cmd_bufh = nil 24 | end 25 | 26 | local function create_window() 27 | log.trace("_create_window()") 28 | local config = harpoon.get_menu_config() 29 | local width = config.width or 60 30 | local height = config.height or 10 31 | local borderchars = config.borderchars 32 | or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 33 | local bufnr = vim.api.nvim_create_buf(false, false) 34 | 35 | local Harpoon_cmd_win_id, win = popup.create(bufnr, { 36 | title = "Harpoon Commands", 37 | highlight = "HarpoonWindow", 38 | line = math.floor(((vim.o.lines - height) / 2) - 1), 39 | col = math.floor((vim.o.columns - width) / 2), 40 | minwidth = width, 41 | minheight = height, 42 | borderchars = borderchars, 43 | }) 44 | 45 | vim.api.nvim_win_set_option( 46 | win.border.win_id, 47 | "winhl", 48 | "Normal:HarpoonBorder" 49 | ) 50 | 51 | return { 52 | bufnr = bufnr, 53 | win_id = Harpoon_cmd_win_id, 54 | } 55 | end 56 | 57 | local function get_menu_items() 58 | log.trace("_get_menu_items()") 59 | local lines = vim.api.nvim_buf_get_lines(Harpoon_cmd_bufh, 0, -1, true) 60 | local indices = {} 61 | 62 | for _, line in pairs(lines) do 63 | if not utils.is_white_space(line) then 64 | table.insert(indices, line) 65 | end 66 | end 67 | 68 | return indices 69 | end 70 | 71 | function M.toggle_quick_menu() 72 | log.trace("cmd-ui#toggle_quick_menu()") 73 | if 74 | Harpoon_cmd_win_id ~= nil 75 | and vim.api.nvim_win_is_valid(Harpoon_cmd_win_id) 76 | then 77 | close_menu() 78 | return 79 | end 80 | 81 | local win_info = create_window() 82 | local contents = {} 83 | local global_config = harpoon.get_global_settings() 84 | 85 | Harpoon_cmd_win_id = win_info.win_id 86 | Harpoon_cmd_bufh = win_info.bufnr 87 | 88 | for idx, cmd in pairs(harpoon.get_term_config().cmds) do 89 | contents[idx] = cmd 90 | end 91 | 92 | vim.api.nvim_win_set_option(Harpoon_cmd_win_id, "number", true) 93 | vim.api.nvim_buf_set_name(Harpoon_cmd_bufh, "harpoon-cmd-menu") 94 | vim.api.nvim_buf_set_lines(Harpoon_cmd_bufh, 0, #contents, false, contents) 95 | vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "filetype", "harpoon") 96 | vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "buftype", "acwrite") 97 | vim.api.nvim_buf_set_option(Harpoon_cmd_bufh, "bufhidden", "delete") 98 | vim.api.nvim_buf_set_keymap( 99 | Harpoon_cmd_bufh, 100 | "n", 101 | "q", 102 | "lua require('harpoon.cmd-ui').toggle_quick_menu()", 103 | { silent = true } 104 | ) 105 | vim.api.nvim_buf_set_keymap( 106 | Harpoon_cmd_bufh, 107 | "n", 108 | "", 109 | "lua require('harpoon.cmd-ui').toggle_quick_menu()", 110 | { silent = true } 111 | ) 112 | vim.api.nvim_buf_set_keymap( 113 | Harpoon_cmd_bufh, 114 | "n", 115 | "", 116 | "lua require('harpoon.cmd-ui').select_menu_item()", 117 | {} 118 | ) 119 | vim.cmd( 120 | string.format( 121 | "autocmd BufWriteCmd lua require('harpoon.cmd-ui').on_menu_save()", 122 | Harpoon_cmd_bufh 123 | ) 124 | ) 125 | if global_config.save_on_change then 126 | vim.cmd( 127 | string.format( 128 | "autocmd TextChanged,TextChangedI lua require('harpoon.cmd-ui').on_menu_save()", 129 | Harpoon_cmd_bufh 130 | ) 131 | ) 132 | end 133 | vim.cmd( 134 | string.format( 135 | "autocmd BufModifiedSet set nomodified", 136 | Harpoon_cmd_bufh 137 | ) 138 | ) 139 | end 140 | 141 | function M.select_menu_item() 142 | log.trace("cmd-ui#select_menu_item()") 143 | local cmd = vim.fn.line(".") 144 | close_menu(true) 145 | local answer = vim.fn.input("Terminal index (default to 1): ") 146 | if answer == "" then 147 | answer = "1" 148 | end 149 | local idx = tonumber(answer) 150 | if idx then 151 | term.sendCommand(idx, cmd) 152 | end 153 | end 154 | 155 | function M.on_menu_save() 156 | log.trace("cmd-ui#on_menu_save()") 157 | term.set_cmd_list(get_menu_items()) 158 | end 159 | 160 | return M 161 | -------------------------------------------------------------------------------- /lua/harpoon/dev.lua: -------------------------------------------------------------------------------- 1 | -- Don't include this file, we should manually include it via 2 | -- require("harpoon.dev").reload(); 3 | -- 4 | -- A quick mapping can be setup using something like: 5 | -- :nmap rr :lua require("harpoon.dev").reload() 6 | local M = {} 7 | 8 | function M.reload() 9 | require("plenary.reload").reload_module("harpoon") 10 | end 11 | 12 | local log_levels = { "trace", "debug", "info", "warn", "error", "fatal" } 13 | local function set_log_level() 14 | local log_level = vim.env.HARPOON_LOG or vim.g.harpoon_log_level 15 | 16 | for _, level in pairs(log_levels) do 17 | if level == log_level then 18 | return log_level 19 | end 20 | end 21 | 22 | return "warn" -- default, if user hasn't set to one from log_levels 23 | end 24 | 25 | local log_level = set_log_level() 26 | M.log = require("plenary.log").new({ 27 | plugin = "harpoon", 28 | level = log_level, 29 | }) 30 | 31 | local log_key = os.time() 32 | 33 | local function override(key) 34 | local fn = M.log[key] 35 | M.log[key] = function(...) 36 | fn(log_key, ...) 37 | end 38 | end 39 | 40 | for _, v in pairs(log_levels) do 41 | override(v) 42 | end 43 | 44 | function M.get_log_key() 45 | return log_key 46 | end 47 | 48 | return M 49 | -------------------------------------------------------------------------------- /lua/harpoon/init.lua: -------------------------------------------------------------------------------- 1 | local Path = require("plenary.path") 2 | local utils = require("harpoon.utils") 3 | local Dev = require("harpoon.dev") 4 | local log = Dev.log 5 | 6 | local config_path = vim.fn.stdpath("config") 7 | local data_path = vim.fn.stdpath("data") 8 | local user_config = string.format("%s/harpoon.json", config_path) 9 | local cache_config = string.format("%s/harpoon.json", data_path) 10 | 11 | local M = {} 12 | 13 | local the_primeagen_harpoon = vim.api.nvim_create_augroup( 14 | "THE_PRIMEAGEN_HARPOON", 15 | { clear = true } 16 | ) 17 | 18 | vim.api.nvim_create_autocmd({ "BufLeave", "VimLeave" }, { 19 | callback = function() 20 | require("harpoon.mark").store_offset() 21 | end, 22 | group = the_primeagen_harpoon, 23 | }) 24 | 25 | vim.api.nvim_create_autocmd("FileType", { 26 | pattern = "harpoon", 27 | group = the_primeagen_harpoon, 28 | 29 | callback = function() 30 | -- Open harpoon file choice in useful ways 31 | -- 32 | -- vertical split (control+v) 33 | vim.keymap.set("n", "", function() 34 | local curline = vim.api.nvim_get_current_line() 35 | local working_directory = vim.fn.getcwd() .. "/" 36 | vim.cmd("vs") 37 | vim.cmd("e " .. working_directory .. curline) 38 | end, { buffer = true, noremap = true, silent = true }) 39 | 40 | -- horizontal split (control+x) 41 | vim.keymap.set("n", "", function() 42 | local curline = vim.api.nvim_get_current_line() 43 | local working_directory = vim.fn.getcwd() .. "/" 44 | vim.cmd("sp") 45 | vim.cmd("e " .. working_directory .. curline) 46 | end, { buffer = true, noremap = true, silent = true }) 47 | 48 | -- new tab (control+t) 49 | vim.keymap.set("n", "", function() 50 | local curline = vim.api.nvim_get_current_line() 51 | local working_directory = vim.fn.getcwd() .. "/" 52 | vim.cmd("tabnew") 53 | vim.cmd("e " .. working_directory .. curline) 54 | end, { buffer = true, noremap = true, silent = true }) 55 | end, 56 | }) 57 | --[[ 58 | { 59 | projects = { 60 | ["/path/to/director"] = { 61 | term = { 62 | cmds = { 63 | } 64 | ... is there anything that could be options? 65 | }, 66 | mark = { 67 | marks = { 68 | } 69 | ... is there anything that could be options? 70 | } 71 | } 72 | }, 73 | ... high level settings 74 | } 75 | --]] 76 | HarpoonConfig = HarpoonConfig or {} 77 | 78 | -- tbl_deep_extend does not work the way you would think 79 | local function merge_table_impl(t1, t2) 80 | for k, v in pairs(t2) do 81 | if type(v) == "table" then 82 | if type(t1[k]) == "table" then 83 | merge_table_impl(t1[k], v) 84 | else 85 | t1[k] = v 86 | end 87 | else 88 | t1[k] = v 89 | end 90 | end 91 | end 92 | 93 | local function mark_config_key(global_settings) 94 | global_settings = global_settings or M.get_global_settings() 95 | if global_settings.mark_branch then 96 | return utils.branch_key() 97 | else 98 | return utils.project_key() 99 | end 100 | end 101 | 102 | local function merge_tables(...) 103 | log.trace("_merge_tables()") 104 | local out = {} 105 | for i = 1, select("#", ...) do 106 | merge_table_impl(out, select(i, ...)) 107 | end 108 | return out 109 | end 110 | 111 | local function ensure_correct_config(config) 112 | log.trace("_ensure_correct_config()") 113 | local projects = config.projects 114 | local mark_key = mark_config_key(config.global_settings) 115 | if projects[mark_key] == nil then 116 | log.debug("ensure_correct_config(): No config found for:", mark_key) 117 | projects[mark_key] = { 118 | mark = { marks = {} }, 119 | term = { 120 | cmds = {}, 121 | }, 122 | } 123 | end 124 | 125 | local proj = projects[mark_key] 126 | if proj.mark == nil then 127 | log.debug("ensure_correct_config(): No marks found for", mark_key) 128 | proj.mark = { marks = {} } 129 | end 130 | 131 | if proj.term == nil then 132 | log.debug( 133 | "ensure_correct_config(): No terminal commands found for", 134 | mark_key 135 | ) 136 | proj.term = { cmds = {} } 137 | end 138 | 139 | local marks = proj.mark.marks 140 | 141 | for idx, mark in pairs(marks) do 142 | if type(mark) == "string" then 143 | mark = { filename = mark } 144 | marks[idx] = mark 145 | end 146 | 147 | marks[idx].filename = utils.normalize_path(mark.filename) 148 | end 149 | 150 | return config 151 | end 152 | 153 | local function expand_dir(config) 154 | log.trace("_expand_dir(): Config pre-expansion:", config) 155 | 156 | local projects = config.projects or {} 157 | for k in pairs(projects) do 158 | local expanded_path = Path.new(k):expand() 159 | projects[expanded_path] = projects[k] 160 | if expanded_path ~= k then 161 | projects[k] = nil 162 | end 163 | end 164 | 165 | log.trace("_expand_dir(): Config post-expansion:", config) 166 | return config 167 | end 168 | 169 | function M.save() 170 | -- first refresh from disk everything but our project 171 | M.refresh_projects_b4update() 172 | 173 | log.trace("save(): Saving cache config to", cache_config) 174 | Path:new(cache_config):write(vim.fn.json_encode(HarpoonConfig), "w") 175 | end 176 | 177 | local function read_config(local_config) 178 | log.trace("_read_config():", local_config) 179 | return vim.json.decode(Path:new(local_config):read()) 180 | end 181 | 182 | -- 1. saved. Where do we save? 183 | function M.setup(config) 184 | log.trace("setup(): Setting up...") 185 | 186 | if not config then 187 | config = {} 188 | end 189 | 190 | local ok, u_config = pcall(read_config, user_config) 191 | 192 | if not ok then 193 | log.debug("setup(): No user config present at", user_config) 194 | u_config = {} 195 | end 196 | 197 | local ok2, c_config = pcall(read_config, cache_config) 198 | 199 | if not ok2 then 200 | log.debug("setup(): No cache config present at", cache_config) 201 | c_config = {} 202 | end 203 | 204 | local complete_config = merge_tables({ 205 | projects = {}, 206 | global_settings = { 207 | ["save_on_toggle"] = false, 208 | ["save_on_change"] = true, 209 | ["enter_on_sendcmd"] = false, 210 | ["tmux_autoclose_windows"] = false, 211 | ["excluded_filetypes"] = { "harpoon" }, 212 | ["mark_branch"] = false, 213 | ["tabline"] = false, 214 | ["tabline_suffix"] = " ", 215 | ["tabline_prefix"] = " ", 216 | }, 217 | }, expand_dir(c_config), expand_dir(u_config), expand_dir(config)) 218 | 219 | -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had 220 | -- an object for vim.loop.cwd() 221 | ensure_correct_config(complete_config) 222 | 223 | if complete_config.tabline then 224 | require("harpoon.tabline").setup(complete_config) 225 | end 226 | 227 | HarpoonConfig = complete_config 228 | 229 | log.debug("setup(): Complete config", HarpoonConfig) 230 | log.trace("setup(): log_key", Dev.get_log_key()) 231 | end 232 | 233 | function M.get_global_settings() 234 | log.trace("get_global_settings()") 235 | return HarpoonConfig.global_settings 236 | end 237 | 238 | -- refresh all projects from disk, except our current one 239 | function M.refresh_projects_b4update() 240 | log.trace( 241 | "refresh_projects_b4update(): refreshing other projects", 242 | cache_config 243 | ) 244 | -- save current runtime version of our project config for merging back in later 245 | local cwd = mark_config_key() 246 | local current_p_config = { 247 | projects = { 248 | [cwd] = ensure_correct_config(HarpoonConfig).projects[cwd], 249 | }, 250 | } 251 | 252 | -- erase all projects from global config, will be loaded back from disk 253 | HarpoonConfig.projects = nil 254 | 255 | -- this reads a stale version of our project but up-to-date versions 256 | -- of all other projects 257 | local ok2, c_config = pcall(read_config, cache_config) 258 | 259 | if not ok2 then 260 | log.debug( 261 | "refresh_projects_b4update(): No cache config present at", 262 | cache_config 263 | ) 264 | c_config = { projects = {} } 265 | end 266 | -- don't override non-project config in HarpoonConfig later 267 | c_config = { projects = c_config.projects } 268 | 269 | -- erase our own project, will be merged in from current_p_config later 270 | c_config.projects[cwd] = nil 271 | 272 | local complete_config = merge_tables( 273 | HarpoonConfig, 274 | expand_dir(c_config), 275 | expand_dir(current_p_config) 276 | ) 277 | 278 | -- There was this issue where the vim.loop.cwd() didn't have marks or term, but had 279 | -- an object for vim.loop.cwd() 280 | ensure_correct_config(complete_config) 281 | 282 | HarpoonConfig = complete_config 283 | log.debug("refresh_projects_b4update(): Complete config", HarpoonConfig) 284 | log.trace("refresh_projects_b4update(): log_key", Dev.get_log_key()) 285 | end 286 | 287 | function M.get_term_config() 288 | log.trace("get_term_config()") 289 | return ensure_correct_config(HarpoonConfig).projects[utils.project_key()].term 290 | end 291 | 292 | function M.get_mark_config() 293 | log.trace("get_mark_config()") 294 | return ensure_correct_config(HarpoonConfig).projects[mark_config_key()].mark 295 | end 296 | 297 | function M.get_menu_config() 298 | log.trace("get_menu_config()") 299 | return HarpoonConfig.menu or {} 300 | end 301 | 302 | -- should only be called for debug purposes 303 | function M.print_config() 304 | print(vim.inspect(HarpoonConfig)) 305 | end 306 | 307 | -- Sets a default config with no values 308 | M.setup() 309 | 310 | return M 311 | -------------------------------------------------------------------------------- /lua/harpoon/mark.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local utils = require("harpoon.utils") 3 | local log = require("harpoon.dev").log 4 | 5 | -- I think that I may have to organize this better. I am not the biggest fan 6 | -- of procedural all the things 7 | local M = {} 8 | local callbacks = {} 9 | 10 | -- I am trying to avoid over engineering the whole thing. We will likely only 11 | -- need one event emitted 12 | local function emit_changed() 13 | log.trace("_emit_changed()") 14 | 15 | local global_settings = harpoon.get_global_settings() 16 | 17 | if global_settings.save_on_change then 18 | harpoon.save() 19 | end 20 | 21 | if global_settings.tabline then 22 | vim.cmd("redrawt") 23 | end 24 | 25 | if not callbacks["changed"] then 26 | log.trace("_emit_changed(): no callbacks for 'changed', returning") 27 | return 28 | end 29 | 30 | for idx, cb in pairs(callbacks["changed"]) do 31 | log.trace( 32 | string.format( 33 | "_emit_changed(): Running callback #%d for 'changed'", 34 | idx 35 | ) 36 | ) 37 | cb() 38 | end 39 | end 40 | 41 | local function filter_empty_string(list) 42 | log.trace("_filter_empty_string()") 43 | local next = {} 44 | for idx = 1, #list do 45 | if list[idx] ~= "" then 46 | table.insert(next, list[idx].filename) 47 | end 48 | end 49 | 50 | return next 51 | end 52 | 53 | local function get_first_empty_slot() 54 | log.trace("_get_first_empty_slot()") 55 | for idx = 1, M.get_length() do 56 | local filename = M.get_marked_file_name(idx) 57 | if filename == "" then 58 | return idx 59 | end 60 | end 61 | 62 | return M.get_length() + 1 63 | end 64 | 65 | local function get_buf_name(id) 66 | log.trace("_get_buf_name():", id) 67 | if id == nil then 68 | return utils.normalize_path(vim.api.nvim_buf_get_name(0)) 69 | elseif type(id) == "string" then 70 | return utils.normalize_path(id) 71 | end 72 | 73 | local idx = M.get_index_of(id) 74 | if M.valid_index(idx) then 75 | return M.get_marked_file_name(idx) 76 | end 77 | -- 78 | -- not sure what to do here... 79 | -- 80 | return "" 81 | end 82 | 83 | local function create_mark(filename) 84 | local cursor_pos = vim.api.nvim_win_get_cursor(0) 85 | log.trace( 86 | string.format( 87 | "_create_mark(): Creating mark at row: %d, col: %d for %s", 88 | cursor_pos[1], 89 | cursor_pos[2], 90 | filename 91 | ) 92 | ) 93 | return { 94 | filename = filename, 95 | row = cursor_pos[1], 96 | col = cursor_pos[2], 97 | } 98 | end 99 | 100 | local function mark_exists(buf_name) 101 | log.trace("_mark_exists()") 102 | for idx = 1, M.get_length() do 103 | if M.get_marked_file_name(idx) == buf_name then 104 | log.debug("_mark_exists(): Mark exists", buf_name) 105 | return true 106 | end 107 | end 108 | 109 | log.debug("_mark_exists(): Mark doesn't exist", buf_name) 110 | return false 111 | end 112 | 113 | local function validate_buf_name(buf_name) 114 | log.trace("_validate_buf_name():", buf_name) 115 | if buf_name == "" or buf_name == nil then 116 | log.error( 117 | "_validate_buf_name(): Not a valid name for a mark,", 118 | buf_name 119 | ) 120 | error("Couldn't find a valid file name to mark, sorry.") 121 | return 122 | end 123 | end 124 | 125 | local function filter_filetype() 126 | local current_filetype = vim.bo.filetype 127 | local excluded_filetypes = harpoon.get_global_settings().excluded_filetypes 128 | 129 | if current_filetype == "harpoon" then 130 | log.error("filter_filetype(): You can't add harpoon to the harpoon") 131 | error("You can't add harpoon to the harpoon") 132 | return 133 | end 134 | 135 | if vim.tbl_contains(excluded_filetypes, current_filetype) then 136 | log.error( 137 | 'filter_filetype(): This filetype cannot be added or is included in the "excluded_filetypes" option' 138 | ) 139 | error( 140 | 'This filetype cannot be added or is included in the "excluded_filetypes" option' 141 | ) 142 | return 143 | end 144 | end 145 | 146 | function M.get_index_of(item, marks) 147 | log.trace("get_index_of():", item) 148 | if item == nil then 149 | log.error( 150 | "get_index_of(): Function has been supplied with a nil value." 151 | ) 152 | error( 153 | "You have provided a nil value to Harpoon, please provide a string rep of the file or the file idx." 154 | ) 155 | return 156 | end 157 | 158 | if type(item) == "string" then 159 | local relative_item = utils.normalize_path(item) 160 | if marks == nil then 161 | marks = harpoon.get_mark_config().marks 162 | end 163 | for idx = 1, M.get_length(marks) do 164 | if marks[idx] and marks[idx].filename == relative_item then 165 | return idx 166 | end 167 | end 168 | 169 | return nil 170 | end 171 | 172 | -- TODO move this to a "harpoon_" prefix or global config? 173 | if vim.g.manage_a_mark_zero_index then 174 | item = item + 1 175 | end 176 | 177 | if item <= M.get_length() and item >= 1 then 178 | return item 179 | end 180 | 181 | log.debug("get_index_of(): No item found,", item) 182 | return nil 183 | end 184 | 185 | function M.status(bufnr) 186 | log.trace("status()") 187 | local buf_name 188 | if bufnr then 189 | buf_name = vim.api.nvim_buf_get_name(bufnr) 190 | else 191 | buf_name = vim.api.nvim_buf_get_name(0) 192 | end 193 | 194 | local norm_name = utils.normalize_path(buf_name) 195 | local idx = M.get_index_of(norm_name) 196 | 197 | if M.valid_index(idx) then 198 | return "M" .. idx 199 | end 200 | return "" 201 | end 202 | 203 | function M.valid_index(idx, marks) 204 | log.trace("valid_index():", idx) 205 | if idx == nil then 206 | return false 207 | end 208 | 209 | local file_name = M.get_marked_file_name(idx, marks) 210 | return file_name ~= nil and file_name ~= "" 211 | end 212 | 213 | function M.add_file(file_name_or_buf_id) 214 | filter_filetype() 215 | local buf_name = get_buf_name(file_name_or_buf_id) 216 | log.trace("add_file():", buf_name) 217 | 218 | if M.valid_index(M.get_index_of(buf_name)) then 219 | -- we don't alter file layout. 220 | return 221 | end 222 | 223 | validate_buf_name(buf_name) 224 | 225 | local found_idx = get_first_empty_slot() 226 | harpoon.get_mark_config().marks[found_idx] = create_mark(buf_name) 227 | M.remove_empty_tail(false) 228 | emit_changed() 229 | end 230 | 231 | -- _emit_on_changed == false should only be used internally 232 | function M.remove_empty_tail(_emit_on_changed) 233 | log.trace("remove_empty_tail()") 234 | _emit_on_changed = _emit_on_changed == nil or _emit_on_changed 235 | local config = harpoon.get_mark_config() 236 | local found = false 237 | 238 | for i = M.get_length(), 1, -1 do 239 | local filename = M.get_marked_file_name(i) 240 | if filename ~= "" then 241 | return 242 | end 243 | 244 | if filename == "" then 245 | table.remove(config.marks, i) 246 | found = found or _emit_on_changed 247 | end 248 | end 249 | 250 | if found then 251 | emit_changed() 252 | end 253 | end 254 | 255 | function M.store_offset() 256 | log.trace("store_offset()") 257 | local ok, res = pcall(function() 258 | local marks = harpoon.get_mark_config().marks 259 | local buf_name = get_buf_name() 260 | local idx = M.get_index_of(buf_name, marks) 261 | if not M.valid_index(idx, marks) then 262 | return 263 | end 264 | 265 | local cursor_pos = vim.api.nvim_win_get_cursor(0) 266 | log.debug( 267 | string.format( 268 | "store_offset(): Stored row: %d, col: %d", 269 | cursor_pos[1], 270 | cursor_pos[2] 271 | ) 272 | ) 273 | marks[idx].row = cursor_pos[1] 274 | marks[idx].col = cursor_pos[2] 275 | end) 276 | 277 | if not ok then 278 | log.warn("store_offset(): Could not store offset:", res) 279 | end 280 | 281 | emit_changed() 282 | end 283 | 284 | function M.rm_file(file_name_or_buf_id) 285 | local buf_name = get_buf_name(file_name_or_buf_id) 286 | local idx = M.get_index_of(buf_name) 287 | log.trace("rm_file(): Removing mark at id", idx) 288 | 289 | if not M.valid_index(idx) then 290 | log.debug("rm_file(): No mark exists for id", file_name_or_buf_id) 291 | return 292 | end 293 | 294 | harpoon.get_mark_config().marks[idx] = create_mark("") 295 | M.remove_empty_tail(false) 296 | emit_changed() 297 | end 298 | 299 | function M.clear_all() 300 | harpoon.get_mark_config().marks = {} 301 | log.trace("clear_all(): Clearing all marks.") 302 | emit_changed() 303 | end 304 | 305 | --- ENTERPRISE PROGRAMMING 306 | function M.get_marked_file(idxOrName) 307 | log.trace("get_marked_file():", idxOrName) 308 | if type(idxOrName) == "string" then 309 | idxOrName = M.get_index_of(idxOrName) 310 | end 311 | return harpoon.get_mark_config().marks[idxOrName] 312 | end 313 | 314 | function M.get_marked_file_name(idx, marks) 315 | local mark 316 | if marks ~= nil then 317 | mark = marks[idx] 318 | else 319 | mark = harpoon.get_mark_config().marks[idx] 320 | end 321 | log.trace("get_marked_file_name():", mark and mark.filename) 322 | return mark and mark.filename 323 | end 324 | 325 | function M.get_length(marks) 326 | if marks == nil then 327 | marks = harpoon.get_mark_config().marks 328 | end 329 | log.trace("get_length()") 330 | return table.maxn(marks) 331 | end 332 | 333 | function M.set_current_at(idx) 334 | filter_filetype() 335 | local buf_name = get_buf_name() 336 | log.trace("set_current_at(): Setting id", idx, buf_name) 337 | local config = harpoon.get_mark_config() 338 | local current_idx = M.get_index_of(buf_name) 339 | 340 | -- Remove it if it already exists 341 | if M.valid_index(current_idx) then 342 | config.marks[current_idx] = create_mark("") 343 | end 344 | 345 | config.marks[idx] = create_mark(buf_name) 346 | 347 | for i = 1, M.get_length() do 348 | if not config.marks[i] then 349 | config.marks[i] = create_mark("") 350 | end 351 | end 352 | 353 | emit_changed() 354 | end 355 | 356 | function M.to_quickfix_list() 357 | log.trace("to_quickfix_list(): Sending marks to quickfix list.") 358 | local config = harpoon.get_mark_config() 359 | local file_list = filter_empty_string(config.marks) 360 | local qf_list = {} 361 | for idx = 1, #file_list do 362 | local mark = M.get_marked_file(idx) 363 | qf_list[idx] = { 364 | text = string.format("%d: %s", idx, file_list[idx]), 365 | filename = mark.filename, 366 | row = mark.row, 367 | col = mark.col, 368 | } 369 | end 370 | log.debug("to_quickfix_list(): qf_list:", qf_list) 371 | vim.fn.setqflist(qf_list) 372 | end 373 | 374 | function M.set_mark_list(new_list) 375 | log.trace("set_mark_list(): New list:", new_list) 376 | 377 | local config = harpoon.get_mark_config() 378 | 379 | for k, v in pairs(new_list) do 380 | if type(v) == "string" then 381 | local mark = M.get_marked_file(v) 382 | if not mark then 383 | mark = create_mark(v) 384 | end 385 | 386 | new_list[k] = mark 387 | end 388 | end 389 | 390 | config.marks = new_list 391 | emit_changed() 392 | end 393 | 394 | function M.toggle_file(file_name_or_buf_id) 395 | local buf_name = get_buf_name(file_name_or_buf_id) 396 | log.trace("toggle_file():", buf_name) 397 | 398 | validate_buf_name(buf_name) 399 | 400 | if mark_exists(buf_name) then 401 | M.rm_file(buf_name) 402 | print("Mark removed") 403 | log.debug("toggle_file(): Mark removed") 404 | else 405 | M.add_file(buf_name) 406 | print("Mark added") 407 | log.debug("toggle_file(): Mark added") 408 | end 409 | end 410 | 411 | function M.get_current_index() 412 | log.trace("get_current_index()") 413 | return M.get_index_of(vim.api.nvim_buf_get_name(0)) 414 | end 415 | 416 | function M.on(event, cb) 417 | log.trace("on():", event) 418 | if not callbacks[event] then 419 | log.debug("on(): no callbacks yet for", event) 420 | callbacks[event] = {} 421 | end 422 | 423 | table.insert(callbacks[event], cb) 424 | log.debug("on(): All callbacks:", callbacks) 425 | end 426 | 427 | return M 428 | -------------------------------------------------------------------------------- /lua/harpoon/tabline.lua: -------------------------------------------------------------------------------- 1 | local Dev = require("harpoon.dev") 2 | local log = Dev.log 3 | 4 | local M = {} 5 | 6 | local function get_color(group, attr) 7 | return vim.fn.synIDattr(vim.fn.synIDtrans(vim.fn.hlID(group)), attr) 8 | end 9 | 10 | 11 | local function shorten_filenames(filenames) 12 | local shortened = {} 13 | 14 | local counts = {} 15 | for _, file in ipairs(filenames) do 16 | local name = vim.fn.fnamemodify(file.filename, ":t") 17 | counts[name] = (counts[name] or 0) + 1 18 | end 19 | 20 | for _, file in ipairs(filenames) do 21 | local name = vim.fn.fnamemodify(file.filename, ":t") 22 | 23 | if counts[name] == 1 then 24 | table.insert(shortened, { filename = vim.fn.fnamemodify(name, ":t") }) 25 | else 26 | table.insert(shortened, { filename = file.filename }) 27 | end 28 | end 29 | 30 | return shortened 31 | end 32 | 33 | function M.setup(opts) 34 | function _G.tabline() 35 | local tabs = shorten_filenames(require('harpoon').get_mark_config().marks) 36 | local tabline = '' 37 | 38 | local index = require('harpoon.mark').get_index_of(vim.fn.bufname()) 39 | 40 | for i, tab in ipairs(tabs) do 41 | local is_current = i == index 42 | 43 | local label 44 | 45 | if tab.filename == "" or tab.filename == "(empty)" then 46 | label = "(empty)" 47 | is_current = false 48 | else 49 | label = tab.filename 50 | end 51 | 52 | 53 | if is_current then 54 | tabline = tabline .. 55 | '%#HarpoonNumberActive#' .. (opts.tabline_prefix or ' ') .. i .. ' %*' .. '%#HarpoonActive#' 56 | else 57 | tabline = tabline .. 58 | '%#HarpoonNumberInactive#' .. (opts.tabline_prefix or ' ') .. i .. ' %*' .. '%#HarpoonInactive#' 59 | end 60 | 61 | tabline = tabline .. label .. (opts.tabline_suffix or ' ') .. '%*' 62 | 63 | if i < #tabs then 64 | tabline = tabline .. '%T' 65 | end 66 | end 67 | 68 | return tabline 69 | end 70 | 71 | vim.opt.showtabline = 2 72 | 73 | vim.o.tabline = '%!v:lua.tabline()' 74 | 75 | vim.api.nvim_create_autocmd("ColorScheme", { 76 | group = vim.api.nvim_create_augroup("harpoon", { clear = true }), 77 | pattern = { "*" }, 78 | callback = function() 79 | local color = get_color('HarpoonActive', 'bg#') 80 | 81 | if (color == "" or color == nil) then 82 | vim.api.nvim_set_hl(0, "HarpoonInactive", { link = "Tabline" }) 83 | vim.api.nvim_set_hl(0, "HarpoonActive", { link = "TablineSel" }) 84 | vim.api.nvim_set_hl(0, "HarpoonNumberActive", { link = "TablineSel" }) 85 | vim.api.nvim_set_hl(0, "HarpoonNumberInactive", { link = "Tabline" }) 86 | end 87 | end, 88 | }) 89 | 90 | log.debug("setup(): Tabline Setup", opts) 91 | end 92 | 93 | return M 94 | -------------------------------------------------------------------------------- /lua/harpoon/term.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local log = require("harpoon.dev").log 3 | local global_config = harpoon.get_global_settings() 4 | 5 | local M = {} 6 | local terminals = {} 7 | 8 | local function create_terminal(create_with) 9 | if not create_with then 10 | create_with = ":terminal" 11 | end 12 | log.trace("term: _create_terminal(): Init:", create_with) 13 | local current_id = vim.api.nvim_get_current_buf() 14 | 15 | vim.cmd(create_with) 16 | local buf_id = vim.api.nvim_get_current_buf() 17 | local term_id = vim.b.terminal_job_id 18 | 19 | if term_id == nil then 20 | log.error("_create_terminal(): term_id is nil") 21 | -- TODO: Throw an error? 22 | return nil 23 | end 24 | 25 | -- Make sure the term buffer has "hidden" set so it doesn't get thrown 26 | -- away and cause an error 27 | vim.api.nvim_buf_set_option(buf_id, "bufhidden", "hide") 28 | 29 | -- Resets the buffer back to the old one 30 | vim.api.nvim_set_current_buf(current_id) 31 | return buf_id, term_id 32 | end 33 | 34 | local function find_terminal(args) 35 | log.trace("term: _find_terminal(): Terminal:", args) 36 | if type(args) == "number" then 37 | args = { idx = args } 38 | end 39 | local term_handle = terminals[args.idx] 40 | if not term_handle or not vim.api.nvim_buf_is_valid(term_handle.buf_id) then 41 | local buf_id, term_id = create_terminal(args.create_with) 42 | if buf_id == nil then 43 | error("Failed to find and create terminal.") 44 | return 45 | end 46 | 47 | term_handle = { 48 | buf_id = buf_id, 49 | term_id = term_id, 50 | } 51 | terminals[args.idx] = term_handle 52 | end 53 | return term_handle 54 | end 55 | 56 | local function get_first_empty_slot() 57 | log.trace("_get_first_empty_slot()") 58 | for idx, cmd in pairs(harpoon.get_term_config().cmds) do 59 | if cmd == "" then 60 | return idx 61 | end 62 | end 63 | return M.get_length() + 1 64 | end 65 | 66 | function M.gotoTerminal(idx) 67 | log.trace("term: gotoTerminal(): Terminal:", idx) 68 | local term_handle = find_terminal(idx) 69 | 70 | vim.api.nvim_set_current_buf(term_handle.buf_id) 71 | end 72 | 73 | function M.sendCommand(idx, cmd, ...) 74 | log.trace("term: sendCommand(): Terminal:", idx) 75 | local term_handle = find_terminal(idx) 76 | 77 | if type(cmd) == "number" then 78 | cmd = harpoon.get_term_config().cmds[cmd] 79 | end 80 | 81 | if global_config.enter_on_sendcmd then 82 | cmd = cmd .. "\n" 83 | end 84 | 85 | if cmd then 86 | log.debug("sendCommand:", cmd) 87 | vim.api.nvim_chan_send(term_handle.term_id, string.format(cmd, ...)) 88 | end 89 | end 90 | 91 | function M.clear_all() 92 | log.trace("term: clear_all(): Clearing all terminals.") 93 | for _, term in ipairs(terminals) do 94 | vim.api.nvim_buf_delete(term.buf_id, { force = true }) 95 | end 96 | terminals = {} 97 | end 98 | 99 | function M.get_length() 100 | log.trace("_get_length()") 101 | return table.maxn(harpoon.get_term_config().cmds) 102 | end 103 | 104 | function M.valid_index(idx) 105 | if idx == nil or idx > M.get_length() or idx <= 0 then 106 | return false 107 | end 108 | return true 109 | end 110 | 111 | function M.emit_changed() 112 | log.trace("_emit_changed()") 113 | if harpoon.get_global_settings().save_on_change then 114 | harpoon.save() 115 | end 116 | end 117 | 118 | function M.add_cmd(cmd) 119 | log.trace("add_cmd()") 120 | local found_idx = get_first_empty_slot() 121 | harpoon.get_term_config().cmds[found_idx] = cmd 122 | M.emit_changed() 123 | end 124 | 125 | function M.rm_cmd(idx) 126 | log.trace("rm_cmd()") 127 | if not M.valid_index(idx) then 128 | log.debug("rm_cmd(): no cmd exists for index", idx) 129 | return 130 | end 131 | table.remove(harpoon.get_term_config().cmds, idx) 132 | M.emit_changed() 133 | end 134 | 135 | function M.set_cmd_list(new_list) 136 | log.trace("set_cmd_list(): New list:", new_list) 137 | for k in pairs(harpoon.get_term_config().cmds) do 138 | harpoon.get_term_config().cmds[k] = nil 139 | end 140 | for k, v in pairs(new_list) do 141 | harpoon.get_term_config().cmds[k] = v 142 | end 143 | M.emit_changed() 144 | end 145 | 146 | return M 147 | -------------------------------------------------------------------------------- /lua/harpoon/test/manage-a-mark.lua: -------------------------------------------------------------------------------- 1 | -- TODO: Harpooned 2 | -- local Marker = require('harpoon.mark') 3 | -- local eq = assert.are.same 4 | -------------------------------------------------------------------------------- /lua/harpoon/test/manage_cmd_spec.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local term = require("harpoon.term") 3 | 4 | local function assert_table_equals(tbl1, tbl2) 5 | if #tbl1 ~= #tbl2 then 6 | assert(false, "" .. #tbl1 .. " != " .. #tbl2) 7 | end 8 | for i = 1, #tbl1 do 9 | if tbl1[i] ~= tbl2[i] then 10 | assert.equals(tbl1[i], tbl2[i]) 11 | end 12 | end 13 | end 14 | 15 | describe("basic functionalities", function() 16 | local emitted 17 | local cmds 18 | 19 | before_each(function() 20 | emitted = false 21 | cmds = {} 22 | harpoon.get_term_config = function() 23 | return { 24 | cmds = cmds, 25 | } 26 | end 27 | term.emit_changed = function() 28 | emitted = true 29 | end 30 | end) 31 | 32 | it("add_cmd for empty", function() 33 | term.add_cmd("cmake ..") 34 | local expected_result = { 35 | "cmake ..", 36 | } 37 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 38 | assert.equals(emitted, true) 39 | end) 40 | 41 | it("add_cmd for non_empty", function() 42 | term.add_cmd("cmake ..") 43 | term.add_cmd("make") 44 | term.add_cmd("ninja") 45 | local expected_result = { 46 | "cmake ..", 47 | "make", 48 | "ninja", 49 | } 50 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 51 | assert.equals(emitted, true) 52 | end) 53 | 54 | it("rm_cmd: removing a valid element", function() 55 | term.add_cmd("cmake ..") 56 | term.add_cmd("make") 57 | term.add_cmd("ninja") 58 | term.rm_cmd(2) 59 | local expected_result = { 60 | "cmake ..", 61 | "ninja", 62 | } 63 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 64 | assert.equals(emitted, true) 65 | end) 66 | 67 | it("rm_cmd: remove first element", function() 68 | term.add_cmd("cmake ..") 69 | term.add_cmd("make") 70 | term.add_cmd("ninja") 71 | term.rm_cmd(1) 72 | local expected_result = { 73 | "make", 74 | "ninja", 75 | } 76 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 77 | assert.equals(emitted, true) 78 | end) 79 | 80 | it("rm_cmd: remove last element", function() 81 | term.add_cmd("cmake ..") 82 | term.add_cmd("make") 83 | term.add_cmd("ninja") 84 | term.rm_cmd(3) 85 | local expected_result = { 86 | "cmake ..", 87 | "make", 88 | } 89 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 90 | assert.equals(emitted, true) 91 | end) 92 | 93 | it("rm_cmd: trying to remove invalid element", function() 94 | term.add_cmd("cmake ..") 95 | term.add_cmd("make") 96 | term.add_cmd("ninja") 97 | term.rm_cmd(5) 98 | local expected_result = { 99 | "cmake ..", 100 | "make", 101 | "ninja", 102 | } 103 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 104 | assert.equals(emitted, true) 105 | term.rm_cmd(0) 106 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 107 | term.rm_cmd(-1) 108 | assert_table_equals(harpoon.get_term_config().cmds, expected_result) 109 | end) 110 | 111 | it("get_length", function() 112 | term.add_cmd("cmake ..") 113 | term.add_cmd("make") 114 | term.add_cmd("ninja") 115 | assert.equals(term.get_length(), 3) 116 | end) 117 | 118 | it("valid_index", function() 119 | term.add_cmd("cmake ..") 120 | term.add_cmd("make") 121 | term.add_cmd("ninja") 122 | assert(term.valid_index(1)) 123 | assert(term.valid_index(2)) 124 | assert(term.valid_index(3)) 125 | assert(not term.valid_index(0)) 126 | assert(not term.valid_index(-1)) 127 | assert(not term.valid_index(4)) 128 | end) 129 | 130 | it("set_cmd_list", function() 131 | term.add_cmd("cmake ..") 132 | term.add_cmd("make") 133 | term.add_cmd("ninja") 134 | term.set_cmd_list({ "make uninstall", "make install" }) 135 | local expected_result = { 136 | "make uninstall", 137 | "make install", 138 | } 139 | assert_table_equals(expected_result, harpoon.get_term_config().cmds) 140 | end) 141 | end) 142 | -------------------------------------------------------------------------------- /lua/harpoon/tmux.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local log = require("harpoon.dev").log 3 | local global_config = harpoon.get_global_settings() 4 | local utils = require("harpoon.utils") 5 | 6 | local M = {} 7 | local tmux_windows = {} 8 | 9 | if global_config.tmux_autoclose_windows then 10 | local harpoon_tmux_group = vim.api.nvim_create_augroup( 11 | "HARPOON_TMUX", 12 | { clear = true } 13 | ) 14 | 15 | vim.api.nvim_create_autocmd("VimLeave", { 16 | callback = function() 17 | require("harpoon.tmux").clear_all() 18 | end, 19 | group = harpoon_tmux_group, 20 | }) 21 | end 22 | 23 | local function create_terminal() 24 | log.trace("tmux: _create_terminal())") 25 | 26 | local window_id 27 | 28 | -- Create a new tmux window and store the window id 29 | local out, ret, _ = utils.get_os_command_output({ 30 | "tmux", 31 | "new-window", 32 | "-P", 33 | "-F", 34 | "#{pane_id}", 35 | }, vim.loop.cwd()) 36 | 37 | if ret == 0 then 38 | window_id = out[1]:sub(2) 39 | end 40 | 41 | if window_id == nil then 42 | log.error("tmux: _create_terminal(): window_id is nil") 43 | return nil 44 | end 45 | 46 | return window_id 47 | end 48 | 49 | -- Checks if the tmux window with the given window id exists 50 | local function terminal_exists(window_id) 51 | log.trace("_terminal_exists(): Window:", window_id) 52 | 53 | local exists = false 54 | 55 | local window_list, _, _ = utils.get_os_command_output({ 56 | "tmux", 57 | "list-windows", 58 | }, vim.loop.cwd()) 59 | 60 | -- This has to be done this way because tmux has-session does not give 61 | -- updated results 62 | for _, line in pairs(window_list) do 63 | local window_info = utils.split_string(line, "@")[2] 64 | 65 | if string.find(window_info, string.sub(window_id, 2)) then 66 | exists = true 67 | end 68 | end 69 | 70 | return exists 71 | end 72 | 73 | local function find_terminal(args) 74 | log.trace("tmux: _find_terminal(): Window:", args) 75 | 76 | if type(args) == "string" then 77 | -- assume args is a valid tmux target identifier 78 | -- if invalid, the error returned by tmux will be thrown 79 | return { 80 | window_id = args, 81 | pane = true, 82 | } 83 | end 84 | 85 | if type(args) == "number" then 86 | args = { idx = args } 87 | end 88 | 89 | local window_handle = tmux_windows[args.idx] 90 | local window_exists 91 | 92 | if window_handle then 93 | window_exists = terminal_exists(window_handle.window_id) 94 | end 95 | 96 | if not window_handle or not window_exists then 97 | local window_id = create_terminal() 98 | 99 | if window_id == nil then 100 | error("Failed to find and create tmux window.") 101 | return 102 | end 103 | 104 | window_handle = { 105 | window_id = "%" .. window_id, 106 | } 107 | 108 | tmux_windows[args.idx] = window_handle 109 | end 110 | 111 | return window_handle 112 | end 113 | 114 | local function get_first_empty_slot() 115 | log.trace("_get_first_empty_slot()") 116 | for idx, cmd in pairs(harpoon.get_term_config().cmds) do 117 | if cmd == "" then 118 | return idx 119 | end 120 | end 121 | return M.get_length() + 1 122 | end 123 | 124 | function M.gotoTerminal(idx) 125 | log.trace("tmux: gotoTerminal(): Window:", idx) 126 | local window_handle = find_terminal(idx) 127 | 128 | local _, ret, stderr = utils.get_os_command_output({ 129 | "tmux", 130 | window_handle.pane and "select-pane" or "select-window", 131 | "-t", 132 | window_handle.window_id, 133 | }, vim.loop.cwd()) 134 | 135 | if ret ~= 0 then 136 | error("Failed to go to terminal." .. stderr[1]) 137 | end 138 | end 139 | 140 | function M.sendCommand(idx, cmd, ...) 141 | log.trace("tmux: sendCommand(): Window:", idx) 142 | local window_handle = find_terminal(idx) 143 | 144 | if type(cmd) == "number" then 145 | cmd = harpoon.get_term_config().cmds[cmd] 146 | end 147 | 148 | if global_config.enter_on_sendcmd then 149 | cmd = cmd .. "\n" 150 | end 151 | 152 | if cmd then 153 | log.debug("sendCommand:", cmd) 154 | 155 | local _, ret, stderr = utils.get_os_command_output({ 156 | "tmux", 157 | "send-keys", 158 | "-t", 159 | window_handle.window_id, 160 | string.format(cmd, ...), 161 | }, vim.loop.cwd()) 162 | 163 | if ret ~= 0 then 164 | error("Failed to send command. " .. stderr[1]) 165 | end 166 | end 167 | end 168 | 169 | function M.clear_all() 170 | log.trace("tmux: clear_all(): Clearing all tmux windows.") 171 | 172 | for _, window in pairs(tmux_windows) do 173 | -- Delete the current tmux window 174 | utils.get_os_command_output({ 175 | "tmux", 176 | "kill-window", 177 | "-t", 178 | window.window_id, 179 | }, vim.loop.cwd()) 180 | end 181 | 182 | tmux_windows = {} 183 | end 184 | 185 | function M.get_length() 186 | log.trace("_get_length()") 187 | return table.maxn(harpoon.get_term_config().cmds) 188 | end 189 | 190 | function M.valid_index(idx) 191 | if idx == nil or idx > M.get_length() or idx <= 0 then 192 | return false 193 | end 194 | return true 195 | end 196 | 197 | function M.emit_changed() 198 | log.trace("_emit_changed()") 199 | if harpoon.get_global_settings().save_on_change then 200 | harpoon.save() 201 | end 202 | end 203 | 204 | function M.add_cmd(cmd) 205 | log.trace("add_cmd()") 206 | local found_idx = get_first_empty_slot() 207 | harpoon.get_term_config().cmds[found_idx] = cmd 208 | M.emit_changed() 209 | end 210 | 211 | function M.rm_cmd(idx) 212 | log.trace("rm_cmd()") 213 | if not M.valid_index(idx) then 214 | log.debug("rm_cmd(): no cmd exists for index", idx) 215 | return 216 | end 217 | table.remove(harpoon.get_term_config().cmds, idx) 218 | M.emit_changed() 219 | end 220 | 221 | function M.set_cmd_list(new_list) 222 | log.trace("set_cmd_list(): New list:", new_list) 223 | for k in pairs(harpoon.get_term_config().cmds) do 224 | harpoon.get_term_config().cmds[k] = nil 225 | end 226 | for k, v in pairs(new_list) do 227 | harpoon.get_term_config().cmds[k] = v 228 | end 229 | M.emit_changed() 230 | end 231 | 232 | return M 233 | -------------------------------------------------------------------------------- /lua/harpoon/ui.lua: -------------------------------------------------------------------------------- 1 | local harpoon = require("harpoon") 2 | local popup = require("plenary.popup") 3 | local Marked = require("harpoon.mark") 4 | local utils = require("harpoon.utils") 5 | local log = require("harpoon.dev").log 6 | 7 | local M = {} 8 | 9 | Harpoon_win_id = nil 10 | Harpoon_bufh = nil 11 | 12 | -- We save before we close because we use the state of the buffer as the list 13 | -- of items. 14 | local function close_menu(force_save) 15 | force_save = force_save or false 16 | local global_config = harpoon.get_global_settings() 17 | 18 | if global_config.save_on_toggle or force_save then 19 | require("harpoon.ui").on_menu_save() 20 | end 21 | 22 | vim.api.nvim_win_close(Harpoon_win_id, true) 23 | 24 | Harpoon_win_id = nil 25 | Harpoon_bufh = nil 26 | end 27 | 28 | local function create_window() 29 | log.trace("_create_window()") 30 | local config = harpoon.get_menu_config() 31 | local width = config.width or 60 32 | local height = config.height or 10 33 | local borderchars = config.borderchars 34 | or { "─", "│", "─", "│", "╭", "╮", "╯", "╰" } 35 | local bufnr = vim.api.nvim_create_buf(false, false) 36 | 37 | local Harpoon_win_id, win = popup.create(bufnr, { 38 | title = "Harpoon", 39 | highlight = "HarpoonWindow", 40 | line = math.floor(((vim.o.lines - height) / 2) - 1), 41 | col = math.floor((vim.o.columns - width) / 2), 42 | minwidth = width, 43 | minheight = height, 44 | borderchars = borderchars, 45 | }) 46 | 47 | vim.api.nvim_win_set_option( 48 | win.border.win_id, 49 | "winhl", 50 | "Normal:HarpoonBorder" 51 | ) 52 | 53 | return { 54 | bufnr = bufnr, 55 | win_id = Harpoon_win_id, 56 | } 57 | end 58 | 59 | local function get_menu_items() 60 | log.trace("_get_menu_items()") 61 | local lines = vim.api.nvim_buf_get_lines(Harpoon_bufh, 0, -1, true) 62 | local indices = {} 63 | 64 | for _, line in pairs(lines) do 65 | if not utils.is_white_space(line) then 66 | table.insert(indices, line) 67 | end 68 | end 69 | 70 | return indices 71 | end 72 | 73 | function M.toggle_quick_menu() 74 | log.trace("toggle_quick_menu()") 75 | if Harpoon_win_id ~= nil and vim.api.nvim_win_is_valid(Harpoon_win_id) then 76 | close_menu() 77 | return 78 | end 79 | 80 | local curr_file = utils.normalize_path(vim.api.nvim_buf_get_name(0)) 81 | vim.cmd( 82 | string.format( 83 | "autocmd Filetype harpoon " 84 | .. "let path = '%s' | call clearmatches() | " 85 | -- move the cursor to the line containing the current filename 86 | .. "call search('\\V'.path.'\\$') | " 87 | -- add a hl group to that line 88 | .. "call matchadd('HarpoonCurrentFile', '\\V'.path.'\\$')", 89 | curr_file:gsub("\\", "\\\\") 90 | ) 91 | ) 92 | 93 | local win_info = create_window() 94 | local contents = {} 95 | local global_config = harpoon.get_global_settings() 96 | 97 | Harpoon_win_id = win_info.win_id 98 | Harpoon_bufh = win_info.bufnr 99 | 100 | for idx = 1, Marked.get_length() do 101 | local file = Marked.get_marked_file_name(idx) 102 | if file == "" then 103 | file = "(empty)" 104 | end 105 | contents[idx] = string.format("%s", file) 106 | end 107 | 108 | vim.api.nvim_win_set_option(Harpoon_win_id, "number", true) 109 | vim.api.nvim_buf_set_name(Harpoon_bufh, "harpoon-menu") 110 | vim.api.nvim_buf_set_lines(Harpoon_bufh, 0, #contents, false, contents) 111 | vim.api.nvim_buf_set_option(Harpoon_bufh, "filetype", "harpoon") 112 | vim.api.nvim_buf_set_option(Harpoon_bufh, "buftype", "acwrite") 113 | vim.api.nvim_buf_set_option(Harpoon_bufh, "bufhidden", "delete") 114 | vim.api.nvim_buf_set_keymap( 115 | Harpoon_bufh, 116 | "n", 117 | "q", 118 | "lua require('harpoon.ui').toggle_quick_menu()", 119 | { silent = true } 120 | ) 121 | vim.api.nvim_buf_set_keymap( 122 | Harpoon_bufh, 123 | "n", 124 | "", 125 | "lua require('harpoon.ui').toggle_quick_menu()", 126 | { silent = true } 127 | ) 128 | vim.api.nvim_buf_set_keymap( 129 | Harpoon_bufh, 130 | "n", 131 | "", 132 | "lua require('harpoon.ui').select_menu_item()", 133 | {} 134 | ) 135 | vim.cmd( 136 | string.format( 137 | "autocmd BufWriteCmd lua require('harpoon.ui').on_menu_save()", 138 | Harpoon_bufh 139 | ) 140 | ) 141 | if global_config.save_on_change then 142 | vim.cmd( 143 | string.format( 144 | "autocmd TextChanged,TextChangedI lua require('harpoon.ui').on_menu_save()", 145 | Harpoon_bufh 146 | ) 147 | ) 148 | end 149 | vim.cmd( 150 | string.format( 151 | "autocmd BufModifiedSet set nomodified", 152 | Harpoon_bufh 153 | ) 154 | ) 155 | vim.cmd( 156 | "autocmd BufLeave ++nested ++once silent lua require('harpoon.ui').toggle_quick_menu()" 157 | ) 158 | end 159 | 160 | function M.select_menu_item() 161 | local idx = vim.fn.line(".") 162 | close_menu(true) 163 | M.nav_file(idx) 164 | end 165 | 166 | function M.on_menu_save() 167 | log.trace("on_menu_save()") 168 | Marked.set_mark_list(get_menu_items()) 169 | end 170 | 171 | local function get_or_create_buffer(filename) 172 | local buf_exists = vim.fn.bufexists(filename) ~= 0 173 | if buf_exists then 174 | return vim.fn.bufnr(filename) 175 | end 176 | 177 | return vim.fn.bufadd(filename) 178 | end 179 | 180 | function M.nav_file(id) 181 | log.trace("nav_file(): Navigating to", id) 182 | local idx = Marked.get_index_of(id) 183 | if not Marked.valid_index(idx) then 184 | log.debug("nav_file(): No mark exists for id", id) 185 | return 186 | end 187 | 188 | local mark = Marked.get_marked_file(idx) 189 | local filename = vim.fs.normalize(mark.filename) 190 | local buf_id = get_or_create_buffer(filename) 191 | local set_row = not vim.api.nvim_buf_is_loaded(buf_id) 192 | 193 | local old_bufnr = vim.api.nvim_get_current_buf() 194 | 195 | vim.api.nvim_set_current_buf(buf_id) 196 | vim.api.nvim_buf_set_option(buf_id, "buflisted", true) 197 | if set_row and mark.row and mark.col then 198 | vim.api.nvim_win_set_cursor(0, { mark.row, mark.col }) 199 | log.debug( 200 | string.format( 201 | "nav_file(): Setting cursor to row: %d, col: %d", 202 | mark.row, 203 | mark.col 204 | ) 205 | ) 206 | end 207 | 208 | local old_bufinfo = vim.fn.getbufinfo(old_bufnr) 209 | if type(old_bufinfo) == "table" and #old_bufinfo >= 1 then 210 | old_bufinfo = old_bufinfo[1] 211 | local no_name = old_bufinfo.name == "" 212 | local one_line = old_bufinfo.linecount == 1 213 | local unchanged = old_bufinfo.changed == 0 214 | if no_name and one_line and unchanged then 215 | vim.api.nvim_buf_delete(old_bufnr, {}) 216 | end 217 | end 218 | end 219 | 220 | function M.location_window(options) 221 | local default_options = { 222 | relative = "editor", 223 | style = "minimal", 224 | width = 30, 225 | height = 15, 226 | row = 2, 227 | col = 2, 228 | } 229 | options = vim.tbl_extend("keep", options, default_options) 230 | 231 | local bufnr = options.bufnr or vim.api.nvim_create_buf(false, true) 232 | local win_id = vim.api.nvim_open_win(bufnr, true, options) 233 | 234 | return { 235 | bufnr = bufnr, 236 | win_id = win_id, 237 | } 238 | end 239 | 240 | function M.notification(text) 241 | local win_stats = vim.api.nvim_list_uis()[1] 242 | local win_width = win_stats.width 243 | 244 | local prev_win = vim.api.nvim_get_current_win() 245 | 246 | local info = M.location_window({ 247 | width = 20, 248 | height = 2, 249 | row = 1, 250 | col = win_width - 21, 251 | }) 252 | 253 | vim.api.nvim_buf_set_lines( 254 | info.bufnr, 255 | 0, 256 | 5, 257 | false, 258 | { "!!! Notification", text } 259 | ) 260 | vim.api.nvim_set_current_win(prev_win) 261 | 262 | return { 263 | bufnr = info.bufnr, 264 | win_id = info.win_id, 265 | } 266 | end 267 | 268 | function M.close_notification(bufnr) 269 | vim.api.nvim_buf_delete(bufnr) 270 | end 271 | 272 | function M.nav_next() 273 | log.trace("nav_next()") 274 | local current_index = Marked.get_current_index() 275 | local number_of_items = Marked.get_length() 276 | 277 | if current_index == nil then 278 | current_index = 1 279 | else 280 | current_index = current_index + 1 281 | end 282 | 283 | if current_index > number_of_items then 284 | current_index = 1 285 | end 286 | M.nav_file(current_index) 287 | end 288 | 289 | function M.nav_prev() 290 | log.trace("nav_prev()") 291 | local current_index = Marked.get_current_index() 292 | local number_of_items = Marked.get_length() 293 | 294 | if current_index == nil then 295 | current_index = number_of_items 296 | else 297 | current_index = current_index - 1 298 | end 299 | 300 | if current_index < 1 then 301 | current_index = number_of_items 302 | end 303 | 304 | M.nav_file(current_index) 305 | end 306 | 307 | return M 308 | -------------------------------------------------------------------------------- /lua/harpoon/utils.lua: -------------------------------------------------------------------------------- 1 | local Path = require("plenary.path") 2 | local data_path = vim.fn.stdpath("data") 3 | local Job = require("plenary.job") 4 | 5 | local M = {} 6 | 7 | M.data_path = data_path 8 | 9 | function M.project_key() 10 | return vim.loop.cwd() 11 | end 12 | 13 | function M.branch_key() 14 | local branch 15 | 16 | -- use tpope's fugitive for faster branch name resolution if available 17 | if vim.fn.exists("*FugitiveHead") == 1 then 18 | branch = vim.fn["FugitiveHead"]() 19 | -- return "HEAD" for parity with `git rev-parse` in detached head state 20 | if #branch == 0 then 21 | branch = "HEAD" 22 | end 23 | else 24 | -- `git branch --show-current` requires Git v2.22.0+ so going with more 25 | -- widely available command 26 | branch = M.get_os_command_output({ 27 | "git", 28 | "rev-parse", 29 | "--abbrev-ref", 30 | "HEAD", 31 | })[1] 32 | end 33 | 34 | if branch then 35 | return vim.loop.cwd() .. "-" .. branch 36 | else 37 | return M.project_key() 38 | end 39 | end 40 | 41 | function M.normalize_path(item) 42 | return Path:new(item):make_relative(M.project_key()) 43 | end 44 | 45 | function M.get_os_command_output(cmd, cwd) 46 | if type(cmd) ~= "table" then 47 | print("Harpoon: [get_os_command_output]: cmd has to be a table") 48 | return {} 49 | end 50 | local command = table.remove(cmd, 1) 51 | local stderr = {} 52 | local stdout, ret = Job 53 | :new({ 54 | command = command, 55 | args = cmd, 56 | cwd = cwd, 57 | on_stderr = function(_, data) 58 | table.insert(stderr, data) 59 | end, 60 | }) 61 | :sync() 62 | return stdout, ret, stderr 63 | end 64 | 65 | function M.split_string(str, delimiter) 66 | local result = {} 67 | for match in (str .. delimiter):gmatch("(.-)" .. delimiter) do 68 | table.insert(result, match) 69 | end 70 | return result 71 | end 72 | 73 | function M.is_white_space(str) 74 | return str:gsub("%s", "") == "" 75 | end 76 | 77 | return M 78 | -------------------------------------------------------------------------------- /lua/telescope/_extensions/harpoon.lua: -------------------------------------------------------------------------------- 1 | local has_telescope, telescope = pcall(require, "telescope") 2 | 3 | if not has_telescope then 4 | error("harpoon.nvim requires nvim-telescope/telescope.nvim") 5 | end 6 | 7 | return telescope.register_extension({ 8 | exports = { 9 | marks = require("telescope._extensions.marks"), 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /lua/telescope/_extensions/marks.lua: -------------------------------------------------------------------------------- 1 | local action_state = require("telescope.actions.state") 2 | local action_utils = require("telescope.actions.utils") 3 | local entry_display = require("telescope.pickers.entry_display") 4 | local finders = require("telescope.finders") 5 | local pickers = require("telescope.pickers") 6 | local conf = require("telescope.config").values 7 | local harpoon = require("harpoon") 8 | local harpoon_mark = require("harpoon.mark") 9 | 10 | local function prepare_results(list) 11 | local next = {} 12 | for idx = 1, #list do 13 | if list[idx].filename ~= "" then 14 | list[idx].index = idx 15 | table.insert(next, list[idx]) 16 | end 17 | end 18 | 19 | return next 20 | end 21 | 22 | local generate_new_finder = function() 23 | return finders.new_table({ 24 | results = prepare_results(harpoon.get_mark_config().marks), 25 | entry_maker = function(entry) 26 | local line = entry.filename .. ":" .. entry.row .. ":" .. entry.col 27 | local displayer = entry_display.create({ 28 | separator = " - ", 29 | items = { 30 | { width = 2 }, 31 | { width = 50 }, 32 | { remaining = true }, 33 | }, 34 | }) 35 | local make_display = function() 36 | return displayer({ 37 | tostring(entry.index), 38 | line, 39 | }) 40 | end 41 | return { 42 | value = entry, 43 | ordinal = line, 44 | display = make_display, 45 | lnum = entry.row, 46 | col = entry.col, 47 | filename = entry.filename, 48 | } 49 | end, 50 | }) 51 | end 52 | 53 | local delete_harpoon_mark = function(prompt_bufnr) 54 | local confirmation = vim.fn.input( 55 | string.format("Delete current mark(s)? [y/n]: ") 56 | ) 57 | if 58 | string.len(confirmation) == 0 59 | or string.sub(string.lower(confirmation), 0, 1) ~= "y" 60 | then 61 | print(string.format("Didn't delete mark")) 62 | return 63 | end 64 | 65 | local selection = action_state.get_selected_entry() 66 | harpoon_mark.rm_file(selection.filename) 67 | 68 | local function get_selections() 69 | local results = {} 70 | action_utils.map_selections(prompt_bufnr, function(entry) 71 | table.insert(results, entry) 72 | end) 73 | return results 74 | end 75 | 76 | local selections = get_selections() 77 | for _, current_selection in ipairs(selections) do 78 | harpoon_mark.rm_file(current_selection.filename) 79 | end 80 | 81 | local current_picker = action_state.get_current_picker(prompt_bufnr) 82 | current_picker:refresh(generate_new_finder(), { reset_prompt = true }) 83 | end 84 | 85 | local move_mark_up = function(prompt_bufnr) 86 | local selection = action_state.get_selected_entry() 87 | local length = harpoon_mark.get_length() 88 | 89 | if selection.index == length then 90 | return 91 | end 92 | 93 | local mark_list = harpoon.get_mark_config().marks 94 | 95 | table.remove(mark_list, selection.index) 96 | table.insert(mark_list, selection.index + 1, selection.value) 97 | 98 | local current_picker = action_state.get_current_picker(prompt_bufnr) 99 | current_picker:refresh(generate_new_finder(), { reset_prompt = true }) 100 | end 101 | 102 | local move_mark_down = function(prompt_bufnr) 103 | local selection = action_state.get_selected_entry() 104 | if selection.index == 1 then 105 | return 106 | end 107 | local mark_list = harpoon.get_mark_config().marks 108 | table.remove(mark_list, selection.index) 109 | table.insert(mark_list, selection.index - 1, selection.value) 110 | local current_picker = action_state.get_current_picker(prompt_bufnr) 111 | current_picker:refresh(generate_new_finder(), { reset_prompt = true }) 112 | end 113 | 114 | return function(opts) 115 | opts = opts or {} 116 | 117 | pickers.new(opts, { 118 | prompt_title = "harpoon marks", 119 | finder = generate_new_finder(), 120 | sorter = conf.generic_sorter(opts), 121 | previewer = conf.grep_previewer(opts), 122 | attach_mappings = function(_, map) 123 | map("i", "", delete_harpoon_mark) 124 | map("n", "", delete_harpoon_mark) 125 | 126 | map("i", "", move_mark_up) 127 | map("n", "", move_mark_up) 128 | 129 | map("i", "", move_mark_down) 130 | map("n", "", move_mark_down) 131 | return true 132 | end, 133 | }):find() 134 | end 135 | -------------------------------------------------------------------------------- /scripts/tmux/switch-back-to-nvim: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Make sure tmux is running 4 | tmux_running=$(pgrep tmux) 5 | 6 | if [[ -z $TMUX ]] && [[ -z $tmux_running ]]; then 7 | echo "tmux needs to be running" 8 | exit 1 9 | fi 10 | 11 | # Switch to a window called nvim in tmux - if it exists 12 | session_name=$(tmux display-message -p "#S") 13 | 14 | tmux switch-client -t "$session_name:nvim" 15 | --------------------------------------------------------------------------------