├── .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 | [](http://www.lua.org)
17 | [](https://neovim.io)
18 |
19 |
20 | 
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 | 
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 |
--------------------------------------------------------------------------------