/marlin.nvim

Smooth sailing through buffers of interest in neovim

Primary LanguageLuaMIT LicenseMIT

marlin.nvim

Smooth sailing between buffers of interest

Persistent and extensible jumps across project buffers of interest with ease.

Setup

Example using the lazy plugin manager

{
    "desdic/marlin.nvim",
    opts = {},
    config = function(_, opts)
        local marlin = require("marlin")
        marlin.setup(opts)

        local keymap = vim.keymap.set
        keymap("n", "<Leader>fa", function() marlin.add() end, {  desc = "add file" })
        keymap("n", "<Leader>fd", function() marlin.remove() end, {  desc = "remove file" })
        keymap("n", "<Leader>fx", function() marlin.remove_all() end, {  desc = "remove all for current project" })
        keymap("n", "<Leader>f]", function() marlin.move_up() end, {  desc = "move up" })
        keymap("n", "<Leader>f[", function() marlin.move_down() end, {  desc = "move down" })
        keymap("n", "<Leader>fs", function() marlin.sort() end, {  desc = "sort" })
        keymap("n", "<Leader>fn", function() marlin.next() end, {  desc = "open next index" })
        keymap("n", "<Leader>fp", function() marlin.prev() end, {  desc = "open previous index" })
        keymap("n", "<Leader><Leader>", function() marlin.toggle() end, {  desc = "toggle cur/last open index" })

        for index = 1,4 do
            keymap("n", "<Leader>"..index, function() marlin.open(index) end, {  desc = "goto "..index })
        end
    end
}

If you want to restore the 'session' it can be done via autocmd like

vim.api.nvim_create_autocmd("VimEnter", {
    group = vim.api.nvim_create_augroup("restore_marlin", { clear = true }),
    callback = function()
        -- If nvim has an argument like file(s) we skip the restore
        if next(vim.fn.argv()) ~= nil then
            return
        end
        require("marlin").open_all()
    end,
    nested = true,
})

Default configuration

local default = {
    patterns = { ".git", ".svn" }, -- look for root of project
    datafile = vim.fn.stdpath("data") .. "/marlin.json", -- location of data file
    open_callback = callbacks.change_buffer -- default way to open buffer
    sorter = sorter.by_buffer -- sort by bufferid
    save_cursor_location = true
    suppress = {
        missing_root = false -- don't give warning on project root not found
    }
}

Easy integration with most status lines

Example with lualine

return {
    "nvim-lualine/lualine.nvim",
    config = function()
        local marlin = require("marlin")

        local marlin_component = function()
            local indexes = marlin.num_indexes()
            if indexes == 0 then
                return ""
            end
            local cur_index = marlin.cur_index()

            return "" .. cur_index .. "/" .. indexes
        end

        require("lualine").setup({
            ...
            sections = {
                ...
                lualine_c = { marlin_component },
                ...
            },
        })
    end

Extending behaviour

marlin.callbacks has a few options like

  • change_buffer (which does what it says, default)
  • use_split (if file is already open in a split switch to it)

But its possible to change the open_call function to get the behaviour you want. If you want to open new buffers in a vsplit you can

    open_callback = function(bufnr, _)
        vim.cmd("vsplit")
        vim.api.nvim_set_current_buf(bufnr)
    end,

Or if want to add an options to open_index that switches to the buffer if already open in a split

    open_callback = function(bufnr, opts)
        if opts.use_split then
            local wins = vim.api.nvim_tabpage_list_wins(0)
            for _, win in ipairs(wins) do
                local winbufnr = vim.api.nvim_win_get_buf(win)

                if winbufnr == bufnr then
                    vim.api.nvim_set_current_win(win)
                    return
                end
            end
        end

        vim.api.nvim_set_current_buf(bufnr)
    end,

Sorting also has a few options like

  • by_buffer sorts by buffer id (The order they where opened in)
  • by_name (Sorts by path+filename)

But they can also be change if you want to write your own sorter.

Choice is yours

But there is no UI

Correct. I'm not planning on creating a UI but if you really want one you can easily create it.

Example using telescope

    local mindex = 0
    local generate_finder = function()
        mindex = 0
        return require("telescope.finders").new_table({
            results = require("marlin").get_indexes(),
            entry_maker = function(entry)
                mindex = mindex + 1
                return {
                    value = entry,
                    ordinal = mindex .. ":" .. entry.filename,
                    lnum = entry.row,
                    col = entry.col + 1,
                    filename = entry.filename,
                    display = mindex .. ":" .. entry.filename .. ":" .. entry.row .. ":" .. entry.col,
                }
            end,
        })
    end

    vim.keymap.set("n", "<Leader>fx", function()
        local conf = require("telescope.config").values
        local action_state = require("telescope.actions.state")

        require("telescope.pickers")
            .new({}, {
                prompt_title = "Marlin",
                finder = generate_finder(),
                previewer = conf.grep_previewer({}),
                sorter = conf.generic_sorter({}),
                attach_mappings = function(_, map)
                    map("i", "<c-d>", function(bufnr)
                        local current_picker = action_state.get_current_picker(bufnr)
                        current_picker:delete_selection(function(selection)
                            require("marlin").remove(selection.filename)
                        end)
                    end)
                    map("i", "+", function(bufnr)
                        local current_picker = action_state.get_current_picker(bufnr)
                        local selection = current_picker:get_selection()
                        require("marlin").move_up(selection.filename)
                        current_picker:refresh(generate_finder(), {})
                    end)
                    map("i", "-", function(bufnr)
                        local current_picker = action_state.get_current_picker(bufnr)
                        local selection = current_picker:get_selection()
                        require("marlin").move_down(selection.filename)
                        current_picker:refresh(generate_finder(), {})
                    end)
                    return true
                end,
            })
            :find()
    end, { desc = "Telescope marlin" })

Why yet another ..

When I first saw harpoon I was immediately hooked but I missed a few key features.

  • I use splits and wanted to have it jump to the buffer and not replace the current one.
  • I wanted persistent jumps per project and not per directory.

Like anyone else missing a feature I created a patch but it seems that many other did the same.

Issues/Feature request

FAQ might have what you are looking, but pull request are also welcome.

Credits

Credit goes to ThePrimeagen for the idea.