+10
.editorconfig
+10
.editorconfig
+1
.gitignore
+1
.gitignore
···
1
+
.mise.local.toml
+21
LICENSE
+21
LICENSE
···
1
+
MIT License
2
+
3
+
Copyright (c) 2025 Eric Richards
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.
+44
README.md
+44
README.md
···
1
+
# javelin.nvim
2
+
3
+
Yet Another Harpoon Clone.
4
+
5
+
A Neovim plugin to quick switch between files, essentially a simplified version of harpoon.
6
+
One main difference is `use_git_root` which will make the lists per project, rather than purely
7
+
based on the current directory.
8
+
9
+
## Dependencies
10
+
11
+
```
12
+
folke/snacks.nvim
13
+
```
14
+
15
+
## Configuration
16
+
17
+
```lua
18
+
{
19
+
use_git_root = true,
20
+
-- Passthrough options to Snacks.win
21
+
menu = {
22
+
width = 0.6,
23
+
height = 8,
24
+
title_pos = "center",
25
+
border = "rounded",
26
+
keys = {
27
+
["<CR>"] = "goto",
28
+
q = "close",
29
+
["<Esc>"] = "close",
30
+
},
31
+
},
32
+
}
33
+
```
34
+
35
+
## Commands
36
+
37
+
- `:Javelin open` - Open popup menu, list of javelin'd items
38
+
- `:Javelin add` - Add current buffer to list
39
+
40
+
## Functions
41
+
42
+
- `require("javelin").open()` - Open popup menu, list of javelin'd items
43
+
- `require("javelin").add()` - Add current buffer to list
44
+
- `require("javelin").select(num)` - Open buffer at `num` position in the list
+37
cliff.toml
+37
cliff.toml
···
1
+
[changelog]
2
+
# https://keats.github.io/tera/docs/#introduction
3
+
body = """
4
+
{% for group, commits in commits | group_by(attribute="group") %}
5
+
=== {{ group | striptags | trim | upper_first }}
6
+
{% for commit in commits %}
7
+
- {% if commit.scope %}*({{ commit.scope }})* {% endif %}\
8
+
{% if commit.breaking %}[**breaking**] {% endif %}\
9
+
{{ commit.message | upper_first }}\
10
+
{% endfor %}
11
+
{% endfor %}\n
12
+
"""
13
+
# remove the leading and trailing s
14
+
trim = true
15
+
16
+
[git]
17
+
conventional_commits = true
18
+
filter_unconventional = true
19
+
split_commits = false
20
+
21
+
# Just things end users will care about
22
+
commit_parsers = [
23
+
{ message = "^feat", group = "<!-- 0 -->🚀 Features" },
24
+
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
25
+
{ message = "^doc", group = "<!-- 3 -->📚 Documentation" },
26
+
{ message = "^perf", group = "<!-- 4 -->⚡ Performance" },
27
+
{ message = "^style", group = "<!-- 5 -->🎨 Styling" },
28
+
{ message = "^test", group = "<!-- 6 -->🧪 Testing" },
29
+
{ message = "^chore|^ci|^refactor", skip = true },
30
+
{ body = ".*security", group = "<!-- 8 -->🛡️ Security" },
31
+
{ message = "^revert", group = "<!-- 9 -->◀️ Revert" },
32
+
{ message = ".*", group = "<!-- 10 -->💼 Other" },
33
+
]
34
+
# filter out the commits that are not matched by commit parsers
35
+
filter_commits = false
36
+
topo_order = false
37
+
sort_commits = "oldest"
+2
doc.sh
+2
doc.sh
doc/.gitkeep
doc/.gitkeep
This is a binary file and will not be displayed.
+64
doc/javelin.nvim.txt
+64
doc/javelin.nvim.txt
···
1
+
*javelin.nvim.txt* For Neovim >= 0.10 Last change: 2025 March 28
2
+
3
+
==============================================================================
4
+
Table of Contents *javelin.nvim-table-of-contents*
5
+
6
+
1. javelin.nvim |javelin.nvim-javelin.nvim|
7
+
- Dependencies |javelin.nvim-javelin.nvim-dependencies|
8
+
- Configuration |javelin.nvim-javelin.nvim-configuration|
9
+
- Commands |javelin.nvim-javelin.nvim-commands|
10
+
- Functions |javelin.nvim-javelin.nvim-functions|
11
+
12
+
==============================================================================
13
+
1. javelin.nvim *javelin.nvim-javelin.nvim*
14
+
15
+
Yet Another Harpoon Clone.
16
+
17
+
A Neovim plugin to quick switch between files, essentially a simplified version
18
+
of harpoon. One main difference is `use_git_root` which will make the lists per
19
+
project, rather than purely based on the current directory.
20
+
21
+
22
+
DEPENDENCIES *javelin.nvim-javelin.nvim-dependencies*
23
+
24
+
>
25
+
folke/snacks.nvim
26
+
<
27
+
28
+
29
+
CONFIGURATION *javelin.nvim-javelin.nvim-configuration*
30
+
31
+
>lua
32
+
{
33
+
use_git_root = true,
34
+
-- Passthrough options to Snacks.win
35
+
menu = {
36
+
width = 0.6,
37
+
height = 8,
38
+
title_pos = "center",
39
+
border = "rounded",
40
+
keys = {
41
+
["<CR>"] = "goto",
42
+
q = "close",
43
+
["<Esc>"] = "close",
44
+
},
45
+
},
46
+
}
47
+
<
48
+
49
+
50
+
COMMANDS *javelin.nvim-javelin.nvim-commands*
51
+
52
+
- `:Javelin open` - Open popup menu, list of javelin’d items
53
+
- `:Javelin add` - Add current buffer to list
54
+
55
+
56
+
FUNCTIONS *javelin.nvim-javelin.nvim-functions*
57
+
58
+
- `require("javelin").open()` - Open popup menu, list of javelin’d items
59
+
- `require("javelin").add()` - Add current buffer to list
60
+
- `require("javelin").select(num)` - Open buffer at `num` position in the list
61
+
62
+
Generated by panvimdoc <https://github.com/kdheepak/panvimdoc>
63
+
64
+
vim:tw=78:ts=8:noet:ft=help:norl:
+4
lazy.lua
+4
lazy.lua
+134
lua/javelin/data.lua
+134
lua/javelin/data.lua
···
1
+
---@module 'snacks'
2
+
3
+
local log = require("javelin.log")
4
+
5
+
local Data = {}
6
+
7
+
---@type JavelinConfig
8
+
local options
9
+
10
+
local data_dir
11
+
local data_cache = {}
12
+
13
+
---@param path string
14
+
---@return string
15
+
local function hash_path(path)
16
+
return vim.fn.sha256(vim.fn.resolve(path))
17
+
end
18
+
19
+
---@param root_path string
20
+
local function get_data_file(root_path)
21
+
return vim.fn.resolve(data_dir .. "/" .. hash_path(root_path))
22
+
end
23
+
24
+
---@param root_path string
25
+
---@param path string
26
+
---@return string
27
+
local function normalize_path(root_path, path)
28
+
root_path = root_path .. "/"
29
+
if vim.startswith(path, root_path) then
30
+
return path:sub(#root_path + 1)
31
+
end
32
+
33
+
return path
34
+
end
35
+
36
+
---@param root_path string
37
+
---@param list string[]
38
+
---@return string[]
39
+
local function normalize_list(root_path, list)
40
+
local new_list = {}
41
+
for _, path in ipairs(list) do
42
+
if path ~= "" then
43
+
table.insert(new_list, path)
44
+
end
45
+
end
46
+
47
+
for i, path in ipairs(new_list) do
48
+
new_list[i] = normalize_path(root_path, path)
49
+
end
50
+
51
+
return new_list
52
+
end
53
+
54
+
---@param root_path string
55
+
local function load(root_path)
56
+
local data_file = get_data_file(root_path)
57
+
if not vim.uv.fs_stat(data_file) then
58
+
return {}
59
+
end
60
+
61
+
return normalize_list(root_path, vim.fn.readfile(data_file))
62
+
end
63
+
64
+
---@param root_path string
65
+
local function write(root_path)
66
+
vim.fn.writefile(Data.get_list(root_path), get_data_file(root_path))
67
+
end
68
+
69
+
---@param root_path string
70
+
---@param path string
71
+
local function add_path(root_path, path)
72
+
local list = Data.get_list(root_path)
73
+
table.insert(list, normalize_path(root_path, vim.fn.fnamemodify(path, ":~")))
74
+
write(root_path)
75
+
end
76
+
77
+
---@param root_path string
78
+
---@return string[]
79
+
function Data.get_list(root_path)
80
+
local hash = hash_path(root_path)
81
+
if data_cache[hash] == nil then
82
+
data_cache[hash] = load(root_path)
83
+
end
84
+
85
+
return data_cache[hash]
86
+
end
87
+
88
+
---@return string[]
89
+
function Data.get_current()
90
+
return Data.get_list(Data.get_root())
91
+
end
92
+
93
+
function Data.add_current()
94
+
local filename = vim.api.nvim_buf_get_name(0)
95
+
local buftype = vim.api.nvim_get_option_value("buftype", { buf = 0 })
96
+
97
+
if filename ~= "" and buftype == "" then
98
+
add_path(Data.get_root(), filename)
99
+
else
100
+
log.warn("Cannot add this buffer")
101
+
end
102
+
end
103
+
104
+
---@param root_path string
105
+
---@param lines string[]
106
+
function Data.update(root_path, lines)
107
+
data_cache[hash_path(root_path)] = normalize_list(root_path, lines)
108
+
write(root_path)
109
+
end
110
+
111
+
---@return string
112
+
function Data.get_root()
113
+
local root = vim.fn.getcwd()
114
+
115
+
if options.use_git_root then
116
+
local git_root = Snacks.git.get_root(root)
117
+
if git_root then
118
+
root = git_root
119
+
end
120
+
end
121
+
122
+
return vim.fn.fnamemodify(root, ":~")
123
+
end
124
+
125
+
function Data.setup(opt)
126
+
options = opt
127
+
128
+
data_dir = vim.fn.resolve(vim.fn.stdpath("data") .. "/javelin")
129
+
if not vim.uv.fs_stat(data_dir) then
130
+
vim.uv.fs_mkdir(data_dir, tonumber("755", 8))
131
+
end
132
+
end
133
+
134
+
return Data
+75
lua/javelin/init.lua
+75
lua/javelin/init.lua
···
1
+
local ui = require("javelin.ui")
2
+
local data = require("javelin.data")
3
+
4
+
local Javelin = {}
5
+
6
+
---@class JavelinConfig
7
+
local defaults = {
8
+
use_git_root = true,
9
+
-- Passthrough options to Snacks.win
10
+
menu = {
11
+
width = 0.6,
12
+
height = 8,
13
+
title_pos = "center",
14
+
border = "rounded",
15
+
keys = {
16
+
["<CR>"] = "goto",
17
+
q = "close",
18
+
["<Esc>"] = "close",
19
+
},
20
+
},
21
+
}
22
+
23
+
function Javelin.open()
24
+
return ui.open()
25
+
end
26
+
27
+
---@param index number
28
+
function Javelin.select(index)
29
+
return ui.select(index)
30
+
end
31
+
32
+
function Javelin.add()
33
+
return data.add_current()
34
+
end
35
+
36
+
local function create_user_command()
37
+
local commands = {
38
+
open = Javelin.open,
39
+
add = Javelin.add,
40
+
}
41
+
42
+
vim.api.nvim_create_user_command("Javelin", function(args)
43
+
local cmd = vim.trim(args.args or "")
44
+
if cmd == "" then
45
+
commands.open()
46
+
elseif commands[cmd] then
47
+
commands[cmd]()
48
+
end
49
+
end, {
50
+
desc = "Javelin",
51
+
nargs = "?",
52
+
complete = function(_, line)
53
+
if line:match("^%s*Javelin %w+ ") then
54
+
return {}
55
+
end
56
+
local prefix = line:match("^%s*Javelin (%w*)") or ""
57
+
return vim.tbl_filter(function(key)
58
+
return key:find(prefix) == 1
59
+
end, vim.tbl_keys(commands))
60
+
end,
61
+
})
62
+
end
63
+
64
+
---@param opt? JavelinConfig
65
+
function Javelin.setup(opt)
66
+
---@type JavelinConfig
67
+
local options = vim.tbl_deep_extend("force", {}, defaults, opt or {})
68
+
69
+
create_user_command()
70
+
71
+
ui.setup(options)
72
+
data.setup(options)
73
+
end
74
+
75
+
return Javelin
+15
lua/javelin/log.lua
+15
lua/javelin/log.lua
···
1
+
local Log = {}
2
+
3
+
function Log.info(msg)
4
+
return Snacks.notify.info(msg, { title = "javelin.nvim" })
5
+
end
6
+
7
+
function Log.warn(msg)
8
+
return Snacks.notify.warn(msg, { title = "javelin.nvim" })
9
+
end
10
+
11
+
function Log.error(msg)
12
+
return Snacks.notify.error(msg, { title = "javelin.nvim" })
13
+
end
14
+
15
+
return Log
+88
lua/javelin/ui.lua
+88
lua/javelin/ui.lua
···
1
+
---@module 'snacks'
2
+
3
+
local data = require("javelin.data")
4
+
local log = require("javelin.log")
5
+
6
+
local UI = {}
7
+
8
+
---@type JavelinConfig
9
+
local options
10
+
11
+
local function open_buf(file)
12
+
-- nvim 0.11 only command
13
+
if vim.fn.exists("*isabsolutepath") then
14
+
if not vim.fn.isabsolutepath(file) then
15
+
file = data.get_root() .. "/" .. file
16
+
end
17
+
else
18
+
if file:sub(1, 1) ~= "/" and file:sub(1, 1) ~= "~" then
19
+
file = data.get_root() .. "/" .. file
20
+
end
21
+
end
22
+
23
+
local bufnr = vim.fn.bufadd(vim.fn.expand(file))
24
+
vim.fn.bufload(bufnr)
25
+
vim.api.nvim_set_option_value("buflisted", true, {
26
+
buf = bufnr,
27
+
})
28
+
vim.api.nvim_set_current_buf(bufnr)
29
+
end
30
+
31
+
---@param index number
32
+
function UI.select(index)
33
+
local file = data.get_current()[index]
34
+
if file ~= nil then
35
+
open_buf(file)
36
+
end
37
+
end
38
+
39
+
function UI.open()
40
+
local root = data.get_root()
41
+
local initial_text = data.get_list(root)
42
+
Snacks.win.new({
43
+
text = initial_text,
44
+
title = root,
45
+
title_pos = options.menu.title_pos,
46
+
width = options.menu.width,
47
+
height = options.menu.height,
48
+
border = options.menu.border,
49
+
keys = options.menu.keys,
50
+
fixbuf = true,
51
+
wo = {
52
+
number = true,
53
+
},
54
+
bo = {
55
+
modifiable = true,
56
+
readonly = false,
57
+
buftype = "nofile",
58
+
bufhidden = "wipe",
59
+
},
60
+
actions = {
61
+
["goto"] = function(self)
62
+
local file = vim.api.nvim_get_current_line()
63
+
self:close()
64
+
open_buf(file)
65
+
end,
66
+
},
67
+
on_close = function(self)
68
+
if self.buf then
69
+
local lines = self:lines()
70
+
if #lines == 1 and lines[1] == "" then
71
+
lines = {}
72
+
end
73
+
if not vim.deep_equal(initial_text, lines) then
74
+
data.update(root, self:lines())
75
+
log.info("Javelin updated.")
76
+
end
77
+
else
78
+
log.error("No buf on win")
79
+
end
80
+
end,
81
+
})
82
+
end
83
+
84
+
function UI.setup(opt)
85
+
options = opt
86
+
end
87
+
88
+
return UI