├── LICENSE
├── README.md
└── lua
└── neomarks.lua
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Luca Saccarola
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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > **Important**
2 | > All the credits for the idea for the plugin goes to [ThePrimegean][1] and his
3 | > plugin [harpoon][2]. I highly suggest you to watch is [vimconf video][3] to
4 | > understand the usage of this plugin.
5 |
6 | > **Warning**
7 | > This plugin is not stable. Is expected changes in the API. If you experience bugs open an issue
8 |
9 |
10 |
11 | # Neomarks
12 |
13 | A new take on vim marks.
14 |
15 |
16 |
17 | ## Table of contents
18 |
19 | * [Goals](#goals)
20 | * [Non Goals](#non-goals)
21 | * [Why](#why)
22 | * [Installation](#installation)
23 | * [Setup](#setup)
24 | * [Branch specific marks](#branch-specific-marks)
25 | * [Roadmap](#roadmap)
26 | * [UI Mappings](#ui-mappings)
27 |
28 | ## Goals
29 |
30 | * No opt-out dependencies
31 | * Take advantage of native neovim features
32 |
33 | ## Non Goals
34 |
35 | * Feature compatible with harpoon
36 | * Support anything other than marking file
37 |
38 | ## Why
39 |
40 | [Harpoon][2] is great and all but as a lot of features that I don't really need
41 | and depends on `plenary.nvim`. This plugin focus on minimalism, do the minimum
42 | set of features to be usable and use only neovim standard functions.
43 |
44 | ## Installation
45 |
46 | Using your favorite Package manager:
47 |
48 | ```
49 | "saccarosium/neomarks"
50 | ```
51 |
52 | Put it directly in your config:
53 |
54 | ```sh
55 | curl https://raw.githubusercontent.com/saccarosium/neomarks/main/lua/neomarks.lua -o "${XDG_CONFIG_HOME:-$HOME/.config}"/nvim/lua/neomarks.lua
56 | ```
57 |
58 | ## Setup
59 |
60 | Call the `setup` function (the following are the defaults):
61 |
62 | ```lua
63 | require("neomarks").setup({
64 | storagefile = vim.fn.stdpath('data') .. "/neomarks.json",
65 | menu = {
66 | title = "Neomarks",
67 | title_pos = "center",
68 | border = "rounded",
69 | width = 60,
70 | height = 10,
71 | }
72 | })
73 | ```
74 |
75 | Now you can remap as you wish the following functions:
76 |
77 | ```lua
78 | require("neomarks").mark_file() -- Mark file
79 | require("neomarks").menu_toggle() -- Toggle the UI
80 | require("neomarks").jump_to() -- Jump to specific index
81 | ```
82 |
83 | ### Branch specific marks
84 |
85 | > **Note**
86 | > For enabling branch specific files you need to: or install some sort of git integration plugin, that exposes a function to get the current branch name, or build a function on your own. It is preferable to achive this using a plugin. Some options are: [gitsigns.nvim][4] or [vim-fugitive][5].
87 |
88 | To enable the feature you need to pass a function that returns the current branch name.
89 |
90 | ```lua
91 | git_branch = vim.fn["FugitiveHead"], -- For vim-fugitive
92 | git_branch = function() return vim.api.nvim_buf_get_var(0, "gitsigns_head") end, -- For gitsigns.nvim
93 | git_branch = function() ... end, -- For custom function that returns branch name
94 | ```
95 |
96 | ## Roadmap
97 |
98 | - [x] Support branch specific marks
99 | - [ ] Mark specific buffer symbol using tree-sitter
100 |
101 | ## UI Mappings
102 |
103 | | Keys | Action |
104 | | :--- | :----- |
105 | | ``, `e`, `E` | edit file under the cursor |
106 | | ``, ``, `q` | close UI |
107 |
108 |
109 | [1]: https://github.com/ThePrimeagen
110 | [2]: https://github.com/ThePrimeagen/harpoon
111 | [3]: https://www.youtube.com/watch?v=Qnos8aApa9g
112 | [4]: https://github.com/lewis6991/gitsigns.nvim
113 | [5]: https://github.com/tpope/vim-fugitive
114 |
--------------------------------------------------------------------------------
/lua/neomarks.lua:
--------------------------------------------------------------------------------
1 | local uv = vim.loop
2 | local autocmd = vim.api.nvim_create_autocmd
3 |
4 | Options = {
5 | storagefile = vim.fn.stdpath('data') .. "/neomarks.json",
6 | git_branch = nil,
7 | menu = {
8 | width = 60,
9 | height = 10,
10 | border = "rounded",
11 | title = "Neomarks",
12 | title_pos = "center",
13 | },
14 | }
15 |
16 | Storage = {}
17 | Marks = {}
18 | Menu = nil
19 |
20 | -- UTILS: {{{
21 |
22 | local function path_sep()
23 | if jit then
24 | local os = string.lower(jit.os)
25 | return os ~= "windows" and "/" or "\\"
26 | else
27 | return package.config:sub(1, 1)
28 | end
29 | end
30 |
31 | local function make_absolute(path)
32 | local cwd = uv.cwd() .. path_sep()
33 | path = cwd .. path
34 | return path
35 | end
36 |
37 | local function make_relative(path)
38 | local cwd = uv.cwd()
39 | if path == cwd then
40 | path = "."
41 | else
42 | if path:sub(1, #cwd) == cwd then
43 | path = path:sub(#cwd + 2, -1)
44 | end
45 | end
46 | return path
47 | end
48 |
49 | local function create_float()
50 | local buf = vim.api.nvim_create_buf(false, true)
51 | local win = vim.api.nvim_open_win(buf, true, {
52 | title = Options.menu.title,
53 | title_pos = Options.menu.title_pos,
54 | relative = "editor",
55 | border = Options.menu.border,
56 | width = Options.menu.width,
57 | height = Options.menu.height,
58 | row = math.floor(((vim.o.lines - Options.menu.height) / 2) - 1),
59 | col = math.floor((vim.o.columns - Options.menu.width) / 2),
60 | })
61 |
62 | vim.api.nvim_win_set_option(win, "winhl", "Normal:Normal")
63 | vim.api.nvim_buf_set_option(buf, "filetype", "neomarks")
64 | vim.api.nvim_buf_set_option(buf, "bufhidden", "delete")
65 |
66 | assert(buf and win, "Couldn't create menu correctly")
67 |
68 | return win, buf
69 | end
70 |
71 | -- }}}
72 | -- STORAGE: {{{
73 |
74 | local function storage_get()
75 | local cwd = uv.cwd()
76 | if Options.git_branch then
77 | cwd = cwd .. ":" .. Options.git_branch()
78 | end
79 | Storage[cwd] = Storage[cwd] or {}
80 | return Storage[cwd]
81 | end
82 |
83 | local function storage_save()
84 | for k, v in pairs(Storage) do
85 | if vim.tbl_isempty(v) then
86 | Storage[k] = nil
87 | end
88 | end
89 | local file = uv.fs_open(Options.storagefile, "w", 438)
90 | if not file then
91 | error("Couldn't save to storagefile")
92 | end
93 | local ok, result = pcall(vim.json.encode, Storage)
94 | if not ok then
95 | error(result)
96 | end
97 | assert(uv.fs_write(file, result))
98 | assert(uv.fs_close(file))
99 | end
100 |
101 | local function storage_load()
102 | local file = uv.fs_open(Options.storagefile, "r", 438)
103 | if not file then
104 | return
105 | end
106 | local stat = assert(uv.fs_fstat(file))
107 | local data = assert(uv.fs_read(file, stat.size, 0))
108 | assert(uv.fs_close(file))
109 | local ok, result = pcall(vim.json.decode, data)
110 | Storage = ok and result or {}
111 | end
112 |
113 | -- }}}
114 | -- MARK: {{{
115 |
116 | local function mark_new(file)
117 | return {
118 | file = file or vim.api.nvim_buf_get_name(0),
119 | buffer = vim.api.nvim_get_current_buf(),
120 | pos = vim.api.nvim_win_get_cursor(0),
121 | }
122 | end
123 |
124 | local function mark_get(file)
125 | file = file or vim.api.nvim_buf_get_name(0)
126 | for _, mark in ipairs(Marks) do
127 | if mark.file == file then
128 | return mark
129 | end
130 | end
131 | return nil
132 | end
133 |
134 | local function mark_update_pos(mark)
135 | mark.pos = vim.api.nvim_win_get_cursor(0)
136 | end
137 |
138 | local function mark_update_current_pos()
139 | local mark = mark_get()
140 | if not mark then
141 | return
142 | end
143 | mark_update_pos(mark)
144 | end
145 |
146 | local function mark_follow(mark)
147 | assert(mark, "Mark not valid")
148 | local buf_valid = vim.fn.buflisted(mark.buffer) == 1
149 | local buf_name = buf_valid and vim.api.nvim_buf_get_name(mark.buffer)
150 | if buf_valid and buf_name == mark.file then
151 | vim.cmd.buffer(mark.buffer)
152 | else
153 | vim.cmd.edit(mark.file)
154 | end
155 | vim.api.nvim_win_set_cursor(0, mark.pos)
156 | end
157 |
158 | -- }}}
159 | -- MENU: {{{
160 |
161 | local function menu_get_items()
162 | local lines = vim.api.nvim_buf_get_lines(Menu.buf, 0, -1, true)
163 | for i, line in ipairs(lines) do
164 | if line == "" or line:gsub("%s", "") == "" then
165 | table.remove(lines, i)
166 | else
167 | lines[i] = make_absolute(line)
168 | end
169 | end
170 | return lines
171 | end
172 |
173 | local function menu_save_items()
174 | local res = {}
175 | for _, file in ipairs(menu_get_items()) do
176 | local mark = mark_get(file)
177 | res[#res + 1] = mark or mark_new(file)
178 | end
179 | Storage[uv.cwd()] = res
180 | Marks = storage_get()
181 | end
182 |
183 | local function menu_select_item()
184 | local line = vim.api.nvim_get_current_line()
185 | local file = make_absolute(line)
186 | local mark = mark_get(file)
187 | if not mark then
188 | return
189 | end
190 | mark_follow(mark)
191 | end
192 |
193 | local function menu_close()
194 | menu_save_items()
195 | vim.api.nvim_win_close(Menu.win, true)
196 | Menu = nil
197 | end
198 |
199 | local function menu_open()
200 | local win, buf = create_float()
201 |
202 | for k, v in pairs({
203 | ["a"] = [[]],
204 | ["o"] = [[]],
205 | ["i"] = [[]],
206 | ["c"] = [[]],
207 | ["e"] = menu_select_item,
208 | ["q"] = menu_close,
209 | [""] = menu_close,
210 | [""] = menu_close,
211 | [""] = menu_select_item,
212 | }) do
213 | -- This is done so I can write only the lower case verison of a letter
214 | -- and remap also the upper case version.
215 | local upper = string.byte(k) - 32
216 | vim.keymap.set('n', k, v, { buffer = buf })
217 | if upper >= 65 or upper <= 122 then
218 | vim.keymap.set('n', string.char(upper), v, { buffer = buf })
219 | end
220 | end
221 |
222 | autocmd("BufLeave", { once = true, callback = menu_close, })
223 |
224 | Menu = {
225 | buf = buf,
226 | win = win,
227 | }
228 | end
229 |
230 | local function menu_populate()
231 | local files = {}
232 | for _, mark in ipairs(Marks) do
233 | table.insert(files, make_relative(mark.file))
234 | end
235 | vim.api.nvim_buf_set_lines(Menu.buf, 0, -1, false, files)
236 | end
237 |
238 | -- }}}
239 |
240 | local M = {}
241 |
242 | function M.setup(opts)
243 | storage_load()
244 | Marks = storage_get()
245 | Options = vim.tbl_deep_extend("force", Options, opts or {})
246 | local group = vim.api.nvim_create_augroup("Neomarks", {})
247 | autocmd("DirChanged", { group = group, callback = function() Marks = storage_get() end })
248 | autocmd("BufLeave", { group = group, callback = mark_update_current_pos, })
249 | autocmd("VimLeave", { group = group, callback = storage_save, })
250 | end
251 |
252 | function M.mark_file()
253 | if Options.git_branch then
254 | Marks = storage_get()
255 | end
256 | local mark = mark_get()
257 | if mark then
258 | return
259 | end
260 | table.insert(Marks, mark_new())
261 | end
262 |
263 | function M.menu_toggle()
264 | if Menu then
265 | menu_close()
266 | else
267 | if Options.git_branch then
268 | Marks = storage_get()
269 | end
270 | menu_open()
271 | menu_populate()
272 | end
273 | end
274 |
275 | function M.jump_to(idx)
276 | if Options.git_branch then
277 | Marks = storage_get()
278 | end
279 | mark_update_current_pos()
280 | local mark = Marks[idx]
281 | if not mark then
282 | return
283 | end
284 | mark_follow(mark)
285 | end
286 |
287 | return M
288 |
289 | -- vim: foldmethod=marker
290 |
--------------------------------------------------------------------------------