{"id":29903613,"url":"https://github.com/kampanaut/cavediver","last_synced_at":"2026-04-18T04:02:18.547Z","repository":{"id":303352483,"uuid":"1015173798","full_name":"kampanaut/cavediver","owner":"kampanaut","description":"A novel approach on file navigation context management in Neovim buffers. ","archived":false,"fork":false,"pushed_at":"2026-03-06T11:46:02.000Z","size":5719,"stargazers_count":5,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-03-06T15:45:44.871Z","etag":null,"topics":["bufferline","lua","neovim","nvim","nvim-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/kampanaut.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,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-07-07T05:45:43.000Z","updated_at":"2026-03-06T11:46:06.000Z","dependencies_parsed_at":"2025-07-07T07:20:18.904Z","dependency_job_id":"ffeab16d-b415-4cf0-886c-aa666f6658f3","html_url":"https://github.com/kampanaut/cavediver","commit_stats":null,"previous_names":["kampanaut/cavediver"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/kampanaut/cavediver","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kampanaut%2Fcavediver","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kampanaut%2Fcavediver/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kampanaut%2Fcavediver/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kampanaut%2Fcavediver/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kampanaut","download_url":"https://codeload.github.com/kampanaut/cavediver/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kampanaut%2Fcavediver/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31955919,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-18T00:39:45.007Z","status":"online","status_checked_at":"2026-04-18T02:00:07.018Z","response_time":103,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"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":["bufferline","lua","neovim","nvim","nvim-plugin"],"created_at":"2025-08-01T17:01:31.138Z","updated_at":"2026-04-18T04:02:18.539Z","avatar_url":"https://github.com/kampanaut.png","language":"Lua","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Cavediver\n\n## Introduction\n\nThis is a **navigation context manager system**. Let me explain what that is: have you ever felt lost after unconsciously jumping to files trying to get around the codebase just because you forgot to be conscious about it? Like some jumps earlier you should have remembered your current filename, but you were too busy thinking about the problem at hand not the filename itself. And also sometimes you don't want to mess up your current jumping setup, like, at some point you have decided to just swap between files A and B, with A being your current file, and now you have to go to C. In traditional neovim, B would have been forgotten, and now you can only jump between A and C. It's fine if you remember the full filename of B, what if you forgot? And now it gets annoying to try remember it and then it just interrupts your flow, trying to go back to only swap between A and B. \n\nThis is the kind of problem this plugin solves for you. We introduce mainly **Triquetra buffer system**. This is the novel part of the project. In a way because of how the triquetra buffers are presented in the UI, this gives you at least a semblance of your current jumping setup. And I have designed this in a way, that you can always restore the secondary buffer and the ternary buffer so you can always get back to what once was. There are other neat features as well, just read the **How to Use** section below.\n\n### Functions\n\n- **Triquetra Buffer System**: Each window maintains current/secondary/ternary buffer relationships with a primary buffer\n- **Session Persistence**: Buffer relationships survive restarts through hash-based file identification  \n- **Cycling Mode**: Explore buffers without losing your working context\n- **Context Preservation**: Remembers where buffers came from for restoration\n- **Buffer Lifecycle**: Closed buffers reopen when accessed through relationships\n- **Visual Feedback**: Color-coded bufferline and winbar show buffer relationships\n- **Per-Window Tracking**: Independent buffer relationships for each window\n\n## New Feature\n\n1. **Multi-version primary buffer**\n    You can backtrack now your previous primary buffer. Every time you set a new buffer as a primary, it is put in the first of a queue (primary_buffers). The first one is always the primary, the others are the \"previous\".\n    - You can now then manage it with a window pop-up, it is editable like that in harpoon window. \n        - Delete-yank and then paste it in another place to reorder... \n        - Or just delete, to really remove it.\n        - Type up a filereadable filepath and just save.\n            - The system validates if it is readable. If it is readable, it registers it.\n            - If not, then vim.notify() an error, nothing is saved.\n        - If you press enter while under a line (in normal mode), that line will be put on top which will be promoted as the current primary buffer.\n    - The default keybind `\u003cm-;\u003e\u003cm-f\u003e` will now just always select the first element `primary_buffers[1]`\n    - If you disable primary buffer, you can still use the pop-up window to set a primary buffer from the queue, but `\u003cm-;\u003e\u003cm-f\u003e` won't do anything at all, until you set it to active again..\n    - You can open the popup floating window with `\u003cm-;\u003eF`\n\n\n## Demo\nThis is a short demo. It just covers how the triquetra buffers update for all sorts of jumps. The system automatically handles all cases, to make jumps and backtracking consistent. This buffers update only updates at BufEnter and non-cycling mode.\n\nYou can also see that the two windows have their own history. You can turn this off. \n\n![Image](https://github.com/user-attachments/assets/deb30ab4-d049-437d-bbd5-8dfe5c21bd47)\n\n## Requirements\n\n- resession.nvim\n- nvim-cokeline\n- harpoon (harpoon feature still coupled with the plugin, working on togglable option)\n- openssl (system installed, used for hashing file names and  directories)\n\n## Installation\n\nThis works for all plugin managers.\n\n### lazy.nvim (An example)\n\n```lua\n{\n    \"kampanaut/cavediver\",\n    -- dir = \"~/Projects/nvim/cavediver\",\n    config = true\n        dependencies = {\n            \"ThePrimeagen/harpoon\",\n            -- resession.nvim and nvim-cokeline, requires this plugin.\n        }\n}\n```\n\n### Resession.nvim setup\n\nYou have to add a hook for the session manager, to allow this plugin to have persistence. \nThis cavediver plugin saves the session state in your neovim's `data` directory by default.\n\n```lua\n{\n    \"stevearc/resession.nvim\",\n    lazy = false,\n    config = function()\n        local resession = require(\"resession\")\n        resession.setup( {\n            buf_filter = function(bufnr)\n                local buftype = vim.bo[bufnr].buftype\n                if vim.bo[bufnr].filetype == \"image_nvim\" or vim.bo[bufnr].filetype == \"oil\" then -- if you incorporated oil.nvim for example.\n                    return true\n                elseif buftype ~= \"\" then\n                    return false\n                elseif vim.api.nvim_get_option_value(\"buflisted\", { buf = bufnr }) == false then\n                    return false\n                else\n                    return resession.default_buf_filter(bufnr)\n                end\n            end,\n            -- your configurations....\n        })\n        resession.add_hook(\"pre_save\", function()\n            require('cavediver').save_session(vim.loop.cwd())\n        end)\n\n        resession.add_hook(\"post_load\", function()\n            require('cavediver').load_session(vim.loop.cwd())\n        end)\n    end,\n    dependencies = {\n        \"kampanaut/cavediver\"\n    }\n},\n```\n\n### nvim-cokeline setup\n\nThe cavediver plugin can work without the bufferline, but it would be nice to visualise the \nvisit history of all your browsers in a bufferline, so that you can now how you can end up \nto your current buffer.\n\n```lua \n{\n    \"willothy/nvim-cokeline\",\n    config = function()\n        -- update_bufferline_state()\n\n        require('cokeline').setup({\n            show_if_buffers_are_at_least = 2,\n            fill_hl = 'Normal',\n            buffers = {\n                -- A function to filter out unwanted buffers. Takes a buffer table as a\n                -- parameter (see the following section for more infos) and has to return\n                -- either `true` or `false`.\n                -- default: `false`.\n                ---@type false | fun(buf: Buffer):boolean\n                filter_valid = function(buffer)\n                    return require('cavediver').bufferline.bufnr_is_displayed(buffer.number)\n                end,\n\n                -- A looser version of `filter_valid`, use this function if you still\n                -- want the `cokeline-{switch,focus}-{prev,next}` mappings to work for\n                -- these buffers without displaying them in your bufferline.\n                -- default: `false`.\n                ---@type false | fun(buf: Buffer):boolean\n                filter_visible = false,\n\n                -- Which buffer to focus when a buffer is deleted, `prev` focuses the\n                -- buffer to the left of the deleted one while `next` focuses the one the\n                -- right.\n                -- default: 'next'.\n                focus_on_delete = 'prev',\n\n                -- If set to `last` new buffers are added to the end of the bufferline,\n                -- if `next` they are added next to the current buffer.\n                -- if set to `directory` buffers are sorted by their full path.\n                -- if set to `number` buffers are sorted by bufnr, as in default Neovim\n                -- default: 'last'.\n                ---@type 'last' | 'next' | 'directory' | 'number' | fun(a: Buffer, b: Buffer):boolean\n                new_buffers_position = function(buffer_a, buffer_b)\n                    local compare = require('cavediver').bufferline.compare_buffers(buffer_a.number, buffer_b.number)\n                    return compare\n                end,\n\n                -- If true, right clicking a buffer will close it\n                -- The close button will still work normally\n                -- Default: true\n                ---@type boolean\n                delete_on_right_click = true,\n            },\n            mappings = {\n                disable_mouse = false,\n                cycle_prev_next = true\n            },\n            pick = {\n                use_filename = false,\n                letters = \"jfkdlsa;bvncmurieowpqyt\"\n            },\n            default_hl = {\n                bg = function(buffer)\n                    return require('cavediver').bufferline.get_theme_bg_color(buffer.is_focused)\n                end,\n                fg = function(buffer)\n                    return require('cavediver').bufferline.get_theme_fg_color(buffer.number, buffer.is_focused)\n                end,\n                bold = function(buffer)\n                    return require('cavediver').bufferline.get_buffer_bold(buffer.number)\n                end,\n            },\n            history = {\n                enabled = false,\n            },\n            tabs = {\n                placement = \"right\",\n                components = require('cavediver').bufferline.cokeline.create_tab_components()\n                \n            },\n            components = require('cavediver').bufferline.cokeline.create_buffer_components()\n        })\n    end\n    dependencies = {\n        \"nvim-lua/plenary.nvim\", -- Required for v0.4.0+\n        \"nvim-tree/nvim-web-devicons\", -- If you want devicons\n        \"stevearc/resession.nvim\", -- Optional, for persistent history\n        \"kampanaut/cavediver\"\n    },\n}\n```\n\n## Configuration Defaults\n\nYou can make your own theme as well, make your own color markers for each buffer type.\n\nYou can start by just modifying the base colors\n\n```lua\nlocal defaults = {\n    session_dir = vim.fn.stdpath(\"data\") .. \"/cavediver/sessions/\",\n    bufferline = {\n        history_view = \"global\" -- \"global\" | \"window\"\n    },\n    colors = {\n        base = {\n            focused = {\n                fg = \"#D5C4A1\",\n                bg = \"#151515\",\n            },\n            unfocused = {\n                fg = \"#504945\",\n                bg = \"#151515\"\n            },\n        },\n        current = {\n            focused = {\n                filename = \"#A9B665\",\n                bufnr = \"#A9B665\",\n            },\n            unfocused = {\n                filename = \"#504945\",\n                bufnr = \"#504945\",\n            }\n        },\n        secondary = {\n            focused = {\n                filename = \"#A9B665\",\n                bufnr = \"#D8A657\"\n            },\n            unfocused = {\n                filename = \"#504945\",\n                bufnr = \"#504945\",\n            },\n        },\n        ternary = {\n            focused = {\n                filename = \"#8d965e\",\n                bufnr = \"#8d965e\"\n            },\n            unfocused = {\n                filename = \"#504945\",\n                bufnr = \"#504945\",\n            },\n        },\n        primary = {\n            active = \"#FFAF00\",\n            inactive = \"#504945\",\n            unfocused = \"#504945\"\n        },\n        harpooned = {\n            unfocused = {\n                fg = \"#5d6699\",\n            }\n        },\n        both_way_arrow = {\n            focused = \"#5F87AF\",\n            unfocused = \"#5F87AF\"\n        },\n        cokeline = {\n            is_picking_close = \"#a05959\",\n            is_picking_focus = \"#d7d7a5\",\n            bg = {\n                detached = {\n                    focused = \"#2B2A33\",\n                    unfocused = \"#020202\",\n                },\n                attached = \"#151515\"\n            },\n            fg = {\n                focused = \"#988E75\",\n                unfocused = \"#6c6c6c\"\n            },\n        }\n    },\n    keymaps = {\n        -- Buffer lifecycle\n        reopen_last_closed = \"\u003cM-S-w\u003e\",\n\n        -- Core triquetra operations\n        toggle_secondary = \"\u003cM-f\u003e\", -- Current ↔ Secondary toggle\n        toggle_ternary = \"\u003cM-S-f\u003e\", -- Current ↔ Ternary toggle\n        jump_to_primary = \"\u003cM-;\u003e\u003cM-f\u003e\", -- Bring primary to current\n        restore_ternary = \"\u003cM-;\u003e\u003cM-r\u003e\", -- Restore ternary relationship\n        restore_secondary = \"\u003cM-;\u003e\u003cM-t\u003e\", -- Restore secondary relationship\n\n        -- Window navigation\n        toggle_window = \"\u003cM-,\u003e\", -- Toggle between current and previous window\n\n        -- Cycling mode navigation\n        cycle_left = \"\u003cM-n\u003e\", -- During cycling mode\n        cycle_right = \"\u003cM-m\u003e\", -- During cycling mode\n        cycle_select = \"\u003cM-v\u003e\", -- Quit cycling mode and attach history with cycling buffer\n\n        -- Primary buffer management\n        toggle_primary = \"\u003cM-;\u003e\u003cM-c\u003e\", -- Toggle current buffer as primary\n        set_primary = \"\u003cM-;\u003e\u003cM-x\u003e\",\n        close_buffer = \"\u003cM-.\u003e\",\n    },\n    cleanup_interval = 8000,    -- seconds\n    winbar_refresh_interval = 140 -- milliseconds\n}\n```\n\n## How to use\n\n### Window Triquetras\n\nThe cavediver plugin tracks windows showing regular file buffers. When a window switches to a non-regular buffer then it will clean the triquetra associated to that window. Each window has their own tiquetra. \n\nA window triquetra is a data structure that stores the window's triquetra slots: current buffer, ternary buffer, secondary buffer, and primary buffer. Each type of slot has their own means of updating themselves.\n\nWinbar UI:\n```text\n⦉32⦊ [7] history/init.lua    ⟦9⟧ window/routines.lua\n```\n\n- `⦉32⦊` is the ternary buffer's buffer number.\n- `[7] history/init.lua` is the current buffer.\n- `⟦9⟧ window/routines.lua` is the secondary buffer.\n\n**Ternary slot**\n\nThe ternary slot updates every time you jump to a new buffer. The current buffer you had will now become the ternary buffer when you jump to a new buffer that is not the secondary buffer. And the new buffer will become the current buffer. It's your previous buffer, basically. But it won't get updated when you select a buffer during cycling mode, via `\u003cm-v\u003e`.\n\nEverytime you jump, if there is a ternary buffer, the system records your current buffer's ternary buffer, before the current buffer is replaced. So when you go back to that current buffer and want to bring back what was once before, you can do it with `\u003cm-;\u003e\u003cm-r\u003e`.\n\n**Secondary slot**\n\nThe secondary slot only updates when you say so. This is your stow slot. If you have a buffer you want to keep before jumping, this is your way. To jump to the secondary buffer, simply press `\u003cm-f\u003e`. The current buffer will become the secondary buffer.\n\nEvery time you jump to secondary the system also records the current buffers, secondary – just the same as ternary's way but with keys `\u003cm-;\u003e\u003cm-t\u003e`\n\n**Primary Slot**\n\nThe primary buffer is the window's \"mark.\"\nSo if you have a buffer of interest that you don't want to lose your hands on, then simply make it a primary buffer, so that no matter how much you kept jumping, you can bring it back to your reach with `\u003cm-;\u003e\u003cm-f\u003e`\n\nAlso take a look at the **New feature** section above as I had extended this feature as well.\n\n**Notes**\n\nThese only works for regular file buffers. The cavediver plugin, only tracks regular file buffers. Makes it easy to deal with cross-session persistence. \n\nA buffer cannot occur more than once in a window triquetra.\n\n## Bufferline\n\nAs you can see the buffers shown in the bufferline are color coded for better experience and it is sorted by buffer visits. The most recent appears on the left. This is really helpful on trying to know how you ended up with your current buffer and not solely depending on the `\u003cc-i\u003e` \u0026 `\u003cc-o\u003e`. \n\nThere is an option `bufferline.history_view`, and this dictates whether to sort the buffers with the global visit history or the current window's own visit history. \n\n**bufferline.history_view = \"window\"**\n\nWindow-specific history is a recent addition and is really nice on seeing the bufferline not being cluttered with your previous window's series of buffer jumps. \n\n### Cycling\n\n`\u003cm-n\u003e` and `\u003cm-m\u003e` calls cokeline's cycle function and this makes the whole cavediver plugin be put into \"cycling\" mode. You will see the change of mode with the colors of the triquetra winbar and cokeline bufferline. \n\nYou will notice that no matter how you cycle, the current slot buffer doesn't change. It is because during cycling mode, the cavediver plugin has stopped tracking visits. You need to do `\u003cm-v\u003e` to select the buffer you are currently and quit cycling mode. \n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkampanaut%2Fcavediver","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkampanaut%2Fcavediver","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkampanaut%2Fcavediver/lists"}