├── 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 | --------------------------------------------------------------------------------