{"id":13476186,"url":"https://github.com/leath-dub/snipe.nvim","last_synced_at":"2025-03-27T02:31:45.053Z","repository":{"id":250500647,"uuid":"834635581","full_name":"leath-dub/snipe.nvim","owner":"leath-dub","description":"Efficient targetted menu built for fast buffer navigation","archived":false,"fork":false,"pushed_at":"2024-07-30T23:29:35.000Z","size":50,"stargazers_count":97,"open_issues_count":3,"forks_count":4,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-07-31T01:08:10.622Z","etag":null,"topics":["neovim-lua-plugin","neovim-plugin"],"latest_commit_sha":null,"homepage":"","language":"Lua","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/leath-dub.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-07-27T22:04:17.000Z","updated_at":"2024-07-30T23:29:38.000Z","dependencies_parsed_at":"2024-07-27T23:38:06.617Z","dependency_job_id":"810baa1f-835c-4c1c-a808-d5474cc17713","html_url":"https://github.com/leath-dub/snipe.nvim","commit_stats":null,"previous_names":["leath-dub/snipe.nvim"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leath-dub%2Fsnipe.nvim","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leath-dub%2Fsnipe.nvim/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leath-dub%2Fsnipe.nvim/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/leath-dub%2Fsnipe.nvim/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/leath-dub","download_url":"https://codeload.github.com/leath-dub/snipe.nvim/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":213382449,"owners_count":15578723,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["neovim-lua-plugin","neovim-plugin"],"created_at":"2024-07-31T16:01:27.441Z","updated_at":"2025-03-27T02:31:45.045Z","avatar_url":"https://github.com/leath-dub.png","language":"Lua","funding_links":[],"categories":["Lua"],"sub_categories":[],"readme":"# Snipe.nvim\n\nEfficient targetted menu built for fast buffer navigation\n\n![recording](https://github.com/user-attachments/assets/a0804e7f-5a04-4e5c-9274-e5eab7a36dc7)\n\n![20241028_17h05m16s_grim](https://github.com/user-attachments/assets/48757fd7-dcd1-42c8-8385-02b43bcd0bdc)\nAlso useful as a general menu (see [`vim.ui.select` wrapper](#vimuiselect-wrapper)).\n\n## Description\n\n`Snipe.nvim` is selection menu that can accept any list of items\nand present a user interface with quick minimal character navigation\nhints to select exactly what you want. It is not flashy it is just\nfast !\n\n## Motivation\n\nIf you ever find yourself in a tangle of buffers scrabbling to find\nyour way back to where you came from, this plugin can help ! Maybe\nyou use harpoon, this is great for project files, but what if you\nwant a fast fallback for when you're in someone else's project. Maybe\nyou use telescope, but that can feel inconsistent and visually\ndistracting. This is why I made this, because I wanted a [Vimium-like](https://github.com/philc/vimium)\nway of hopping around a large amount of buffers\n\n## Usage\n\nFor `lazy.nvim`:\n\n```lua\n{\n  \"leath-dub/snipe.nvim\",\n  keys = {\n    {\"gb\", function () require(\"snipe\").open_buffer_menu() end, desc = \"Open Snipe buffer menu\"}\n  },\n  opts = {}\n}\n```\n\nFor `packadd` (builtin package manager), clone the repo into `$HOME/.config/nvim/pack/snipe/opt/snipe.nvim` and add this to your configuration:\n\n```lua\nvim.cmd.packadd \"snipe.nvim\"\nlocal snipe = require(\"snipe\")\nsnipe.setup()\nvim.keymap.set(\"n\", \"gb\", snipe.open_buffer_menu)\n```\n\n## Options\n\nYou can pass in a table of options to the `setup` function, here are the default options:\n\n```lua\n{\n  ui = {\n    ---@type integer\n    max_height = -1, -- -1 means dynamic height\n    -- Where to place the ui window\n    -- Can be any of \"topleft\", \"bottomleft\", \"topright\", \"bottomright\", \"center\", \"cursor\" (sets under the current cursor pos)\n    ---@type \"topleft\"|\"bottomleft\"|\"topright\"|\"bottomright\"|\"center\"|\"cursor\"\n    position = \"topleft\",\n    -- Override options passed to `nvim_open_win`\n    -- Be careful with this as snipe will not validate\n    -- anything you override here. See `:h nvim_open_win`\n    -- for config options\n    ---@type vim.api.keyset.win_config\n    open_win_override = {\n      -- title = \"My Window Title\",\n      border = \"single\", -- use \"rounded\" for rounded border\n    },\n\n    -- Preselect the currently open buffer\n    ---@type boolean\n    preselect_current = false,\n\n    -- Set a function to preselect the currently open buffer\n    -- E.g, `preselect = require(\"snipe\").preselect_by_classifier(\"#\")` to\n    -- preselect alternate buffer (see :h ls and look at the \"Indicators\")\n    ---@type nil|fun(buffers: snipe.Buffer[]): number\n    preselect = nil, -- function (bs: Buffer[] [see lua/snipe/buffer.lua]) -\u003e int (index)\n\n    -- Changes how the items are aligned: e.g. \"\u003ctag\u003e foo    \" vs \"\u003ctag\u003e    foo\"\n    -- Can be \"left\", \"right\" or \"file-first\"\n    -- NOTE: \"file-first\" puts the file name first and then the directory name\n    ---@type \"left\"|\"right\"|\"file-first\"\n    text_align = \"left\",\n\n    -- Provide custom buffer list format\n    -- Available options:\n    --  \"filename\" - basename of the buffer\n    --  \"directory\" - buffer parent directory path\n    --  \"icon\" - icon for buffer filetype from \"mini.icons\" or \"nvim-web-devicons\"\n    --  string - any string, will be inserted as is\n    --  fun(buffer_object): string,string - function that takes snipe.Buffer object as an argument\n    --    and returns a string to be inserted and optional highlight group name\n    -- buffer_format = { \"-\u003e\", \"icon\", \"filename\", \"\", \"directory\", function(buf)\n    --   if vim.fn.isdirectory(vim.api.nvim_buf_get_name(buf.id)) == 1 then\n    --     return \" \", \"SnipeText\"\n    --   end\n    -- end },\n  },\n  hints = {\n    -- Charaters to use for hints (NOTE: make sure they don't collide with the navigation keymaps)\n    ---@type string\n    dictionary = \"sadflewcmpghio\",\n  },\n  navigate = {\n    -- When the list is too long it is split into pages\n    -- `[next|prev]_page` options allow you to navigate\n    -- this list\n    next_page = \"J\",\n    prev_page = \"K\",\n\n    -- You can also just use normal navigation to go to the item you want\n    -- this option just sets the keybind for selecting the item under the\n    -- cursor\n    under_cursor = \"\u003ccr\u003e\",\n\n    -- In case you changed your mind, provide a keybind that lets you\n    -- cancel the snipe and close the window.\n    ---@type string|string[]\n    cancel_snipe = \"\u003cesc\u003e\",\n\n    -- Close the buffer under the cursor\n    -- Remove \"j\" and \"k\" from your dictionary to navigate easier to delete\n    -- NOTE: Make sure you don't use the character below on your dictionary\n    close_buffer = \"D\",\n\n    -- Open buffer in vertical split\n    open_vsplit = \"V\",\n\n    -- Open buffer in split, based on `vim.opt.splitbelow`\n    open_split = \"H\",\n\n    -- Change tag manually\n    change_tag = \"C\",\n  },\n  -- The default sort used for the buffers\n  -- Can be any of:\n  --  \"last\" - sort buffers by last accessed\n  --  \"default\" - sort buffers by its number\n  --  fun(bs:snipe.Buffer[]):snipe.Buffer[] - custom sort function, should accept a list of snipe.Buffer[] as an argument and return sorted list of snipe.Buffer[]\n  sort = \"default\",\n}\n```\n\n## More Details\n\n### Projects using `snipe.nvim`\n\n* [snipe-lsp](https://github.com/kungfusheep/snipe-lsp.nvim) - navigate LSP\nsymbols using a snipe menu\n* [snipe-spell](https://github.com/kungfusheep/snipe-spell.nvim) - use snipe as\nui menu to builtin vim spell checking\n* [snipe-marks](https://github.com/nicholasxjy/snipe-marks.nvim) - navigate\nmarks using snipe\n\n### Use `snipe` as a `vim.ui.select` wrapper\n\nSnipe nvim can act as your `vim.ui.select` menu, which is what is used for \"code actions\" in LSP\namong other things. You can set this up like so:\n\n```lua\nlocal snipe = require(\"snipe\")\nsnipe.ui_select_menu = require(\"snipe.menu\"):new { position = \"center\" }\nsnipe.ui_select_menu:add_new_buffer_callback(function (m)\n  vim.keymap.set(\"n\", \"\u003cesc\u003e\", function ()\n    m:close()\n  end, { nowait = true, buffer = m.buf })\nend)\nvim.ui.select = snipe.ui_select;\n```\n\nThis makes `vim.ui.select` menus open in the center, with `\u003cesc\u003e` to cancel.\n\n### Development\n\nThe older API, as I am sure contributors are aware, was shite! The new API is\nbased on creating a `Menu` which is just a state object mostly just maintaining\na buffer, a window and what page you are on. There is no longer a concept of\n`generator`/`producer` functions, each call to `open` on the window just\naccepts a list of items to show. All of the old global config was implemented\nmuch easier using this api. A minimal example of a menu is the following:\n\n```lua\nlocal Menu = require(\"snipe.menu\")\nlocal menu = Menu:new {\n  -- Per-menu configuration (does not affect global configuration)\n  position = \"center\"\n}\n\n-- The items to snipe is just an array\n-- Be careful how you reference the array in closures though\n-- if you have the items table created inside a closure\n-- as uncommented when setting the open keymap a few lines down,\n-- this means that the items array will change every trigger and\n-- can be an outdated capture in sub-closures.\nlocal items = { \"foo\", \"bar\", \"baz\" }\n\nvim.keymap.set(\"n\", \"gb\", function()\n  -- local items = { ... }\n\n  -- This method allows you to add `n' callbacks to be\n  -- triggered whenever a new buffer is created.\n  -- A new buffer is only ever created if it is somehow\n  -- externally removed or at normal startup. The reason\n  -- For this system is so that you can update any buffer local\n  -- keymaps and alike to work for the new buffer.\n  menu:add_new_buffer_callback(function(m)\n    -- `m` is a reference to the menu, prefer referencing it via this (i.e. not through your menu variable) !\n\n    -- Keymaps like \"open in split\" etc can be put in here\n    print(\"I dont want any other keymaps X( !\")\n  end)\n\n  menu:open(items, function (m, i)\n    -- Prefer accessing items on the menu itself (m.items not items) !\n    print(\"You selected: \" .. m.items[i])\n    print(\"You are hovering over: \" .. m.items[m:hovered()])\n    -- Close the menu\n    m:close()\n    -- You can also call `reopen` for things like navigating\n    -- between pages when the window can stay open and just\n    -- needs to be updated.\n  end, function (item)\n    -- Format function means you don't just have to pass a list of strings\n    -- you get to format each item as you choose.\n    return item\n  end, 10 -- the item to preselect, if it is out of bounds of the currently shown page\n          -- it is ignored\n  )\nend)\n```\n\n### Examples\n\n#### File browser\n\n```lua\nlocal uv = vim.uv or vim.loop\nlocal menu\nlocal items\n\nlocal prev = uv.cwd()\nlocal curr = prev\n\nlocal function new_dir(dir_name)\n  prev = curr\n\n  local cwd = uv.cwd()\n\n  if uv.fs_realpath(dir_name) == cwd then\n    dir_name = cwd\n  end\n\n  local dir = uv.fs_opendir(dir_name)\n  if dir == nil then\n    return\n  end\n\n  items = {}\n  while true do\n    local ent = dir:readdir()\n    if not ent then\n      break\n    end\n\n    if dir_name ~= uv.cwd() then\n      ent[1].name = dir_name .. \"/\" .. ent[1].name\n    end\n\n    table.insert(items, ent[1])\n  end\n  dir:closedir()\n\n  curr = dir_name\nend\n\nlocal function set_keymaps(m)\n  local nav_next = function()\n    m:goto_next_page()\n    m:reopen()\n  end\n\n  local nav_prev = function()\n    m:goto_prev_page()\n    m:reopen()\n  end\n\n  vim.keymap.set(\"n\", \"J\", nav_next, { nowait = true, buffer = m.buf })\n  vim.keymap.set(\"n\", \"K\", nav_prev, { nowait = true, buffer = m.buf })\n  vim.keymap.set(\"n\", \"\u003cesc\u003e\", function()\n    m:close()\n  end, { nowait = true, buffer = m.buf })\n  vim.keymap.set(\"n\", \"-\", function()\n    new_dir(prev)\n    m.items = items\n    m:reopen()\n  end)\n  vim.keymap.set(\"n\", \"..\", function()\n    local cwd = uv.fs_realpath(uv.cwd())\n    local cur = uv.fs_realpath(vim.fs.dirname(m.items[1].name))\n    local dir = uv.fs_realpath(vim.fs.dirname(cur))\n\n    if uv.fs_realpath(dir) == cwd then\n      new_dir(cwd)\n      m.items = items\n      m:reopen()\n      return\n    end\n\n    local tot = 0\n    local matched = 0\n    local fail = false\n    local unmatched = \"\"\n\n    local cwd_it = cwd:gmatch(\"[^/]+\")\n    local dir_it = dir:gmatch(\"[^/]+\")\n    local dual_it = function()\n      return cwd_it(), dir_it()\n    end\n\n    for cwd_d, dir_d in dual_it do\n      if cwd_d == dir_d and not fail then\n        matched = matched + 1\n      else\n        if not fail then\n          unmatched = unmatched .. (dir_d or \"\")\n        else\n          unmatched = unmatched .. \"/\" .. (dir_d or \"\")\n        end\n        fail = true\n      end\n      tot = tot + 1\n    end\n\n    if tot == matched then\n      unmatched = dir:gsub(cwd, \"\")\n    end\n\n    unmatched = unmatched:gsub(\"^/\", \"\")\n    unmatched = unmatched:gsub(\"/$\", \"\")\n    unmatched = unmatched == \"/\" and \"\" or unmatched\n\n    -- how many \"..\" is determined by tot - matched\n    local backs = tot - matched\n    local pfx = (\"../\"):rep(backs):gsub(\"/$\", \"\")\n\n    new_dir(pfx .. unmatched)\n    m.items = items\n    m:reopen()\n  end, { nowait = true, buffer = m.buf })\nend\n\nlocal function open_file_manager()\n  if menu == nil then\n    menu = require(\"snipe.menu\"):new { position = \"bottomleft\" }\n    menu:add_new_buffer_callback(set_keymaps)\n  end\n\n  new_dir(uv.cwd())\n  menu:open(items, function(m, i)\n    if m.items[i].type == \"directory\" then\n      new_dir(m.items[i].name)\n      m.items = items\n      m:reopen()\n    else\n      m:close()\n      vim.cmd.edit(m.items[i].name)\n    end\n  end, function (item)\n  if item.type == \"directory\" then\n    return item.name .. \"/\"\n  end\n  return item.name end)\nend\n\nvim.keymap.set(\"n\", \"cd\", open_file_manager)\n```\n\n#### Modal Buffer menu\n\nThe following code has a single menu that has different actions on the selected\nitem depending on what keybind you open it with (`\u003cleader\u003eo` or `\u003cleader\u003ed`):\n\n```lua\nlocal menu = require(\"snipe.menu\"):new()\nlocal items\n\n-- Other default mappings can be set here too\nlocal function set_keymaps(m)\n  vim.keymap.set(\"n\", \"\u003cesc\u003e\", function()\n    m:close()\n  end, { nowait = true, buffer = m.buf })\nend\nmenu:add_new_buffer_callback(set_keymaps)\n\nvim.keymap.set(\"n\", \"\u003cleader\u003eo\", function()\n  items = require(\"snipe.buffer\").get_buffers()\n  menu.config.open_win_override.title = \"Snipe [Open]\"\n  menu:open(items, function(m, i)\n    m:close()\n    vim.api.nvim_set_current_buf(m.items[i].id)\n  end, function (item) return item.name end)\nend)\n\nvim.keymap.set(\"n\", \"\u003cleader\u003ed\", function()\n  items = require(\"snipe.buffer\").get_buffers()\n  menu.config.open_win_override.title = \"Snipe [Delete]\"\n  menu:open(items, function(m, i)\n    local bufnr = m.items[i].id\n    -- I have to hack switch back to main window, otherwise currently background focused\n    -- window cannot be deleted when focused on a floating window\n    local current_tabpage = vim.api.nvim_get_current_tabpage()\n    local root_win = vim.api.nvim_tabpage_list_wins(current_tabpage)[1]\n    vim.api.nvim_set_current_win(root_win)\n    vim.api.nvim_buf_delete(bufnr, { force = true })\n    vim.api.nvim_set_current_win(m.win)\n    table.remove(m.items, i)\n    m:reopen()\n  end, function (item) return item.name end)\nend)\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleath-dub%2Fsnipe.nvim","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fleath-dub%2Fsnipe.nvim","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fleath-dub%2Fsnipe.nvim/lists"}