# scope.nvim: giving each Neovim tab its own set of buffers

> scope.nvim is a small Neovim plugin that turns tabs into scopes, so that :bnext and :bprev cycle only through the buffers belonging to the current tab instead of the whole buffer list. It ships six lifecycle hooks, a :ScopeMoveBuf command for relocating a buffer, a Telescope extension for browsing every tab's buffers, and experimental session support for nvim-possession and resession.nvim.

**tiagovla/scope.nvim** — Revolutionize Your Neovim Tab Workflow: Introducing Enhanced Tab Scoping!

- Repository: https://github.com/tiagovla/scope.nvim
- Stars: 544 · Forks: 25
- Language: Lua
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/tiagovla-scope-nvim

## The problem it solves, and what a tab scope actually is

scope.nvim is a Neovim plugin built around one idea: a tab should hold its own set of buffers, and the buffer navigation commands should respect that.

The problem is the familiar one. Neovim has one global buffer list. Every tab shows a window on some buffer, and the buffer list holds them all, so :bnext and :bprev walk through every buffer you have open anywhere, including the ones belonging to tabs you are not looking at. Open four tabs, and those commands become a way to lose track of where you are rather than a way to move around.

The plugin's answer is scoped buffers. Navigating with :bnext and :bprev within a tab moves only through the buffers of that tab, and the tab arrangement stays clean rather than turning into a single crowded list.

The important thing to grasp is the scope of the change. This is not a new buffer manager and it does not replace whatever you use to open files. It changes the behaviour of buffer cycling so that it is tab-aware. That distinction matters for how you adopt it, because a plugin that redefines :bnext can conflict with a buffer management plugin that also redefines :bnext, and the two have to agree on what the command means.

The plugin is written in Lua, which is the native language of Neovim configuration, so it loads without a compiled step and without a runtime dependency. The repository is small, with the top level holding a LICENSE, the project description, a `.stylua.toml` and a `lua/` directory, and the whole plugin lives in that one directory. The formatting configuration is there because the project asks contributors to review formatting with stylua.

It is MIT licensed, the repository is not archived, the default branch is main, and the last push was on 2026-10-12. There are no GitHub releases, so there is no version to pin, and the plugin is installed by repository name through a package manager.

## Installing it and the setup call

Installation goes through a package manager, and the documentation gives the lazy.nvim form:

```lua
{ "tiagovla/scope.nvim", config = true }
```

The `config = true` option tells lazy.nvim to call the plugin's setup function automatically, which is the lazy.nvim convention for a plugin that needs configuration. Without it you would have to require the module yourself.

The setup call itself takes an options table, and the simplest form is:

```lua
-- init.lua
require("scope").setup({})
```

The empty table is worth pausing on, because it tells you something about the design. There is no required configuration, no list of tabs to define, no keymaps to bind. The plugin installs its behaviour when required and you configure it by adding to the options table.

That is a small plugin in the most literal sense. The repository contains a LICENSE, the project description, a formatting configuration file and one `lua/` directory, so there is no build step, no lockfile, no dependency manifest and nothing to compile. It is a plugin you can read end to end in a sitting, which is a meaningful property for something that changes how a core command behaves.

The package manager is not specified beyond lazy.nvim, and the project description does not document installation for other managers. If you use packer.nvim or a different lazy.nvim layout, the equivalent is a repository specification with a setup call, and the setup function name is fixed at `scope.setup`.

One more thing worth noting is what is not in the project description. There is no mention of a compatibility table for Neovim versions, no mention of a minimum Neovim version, and no changelog. For a plugin that modifies buffer navigation behaviour, that is a mild risk, because a Neovim release that changes buffer internals would affect it and you would find out from a broken command rather than from a compatibility note.

## The six lifecycle hooks, and the buffer move command

The extension point of the plugin is a set of six hooks, and the list is complete.

There is `pre_tab_enter` and `post_tab_enter`, which run before and after entering a tab. There is `pre_tab_leave` and `post_tab_leave`, which run before and after leaving a tab. And there is `pre_tab_close` and `post_tab_close`, which run before and after closing a tab.

The documentation gives the configuration shape with the `pre_tab_enter` example:

```lua
-- init.lua
require("scope").setup({
    hooks = {
        pre_tab_enter = function()
            -- Your custom logic to run before entering a tab
        end,
    },
})
```

Six hooks covering three tab events, each with a before and an after, is a complete enough model that you can do almost anything a tab plugin needs: run a formatter on save when a tab is entered, restore a working directory when you switch to a project tab, or trigger a preview window on leave.

The pairing of before and after is what makes the hooks usable for stateful behaviour. A `pre_tab_leave` hook that stores the current window position and a `post_tab_enter` hook that restores it gives you per-tab cursor memory, which is one of those small things that makes a multi-tab setup feel right. With only a single on-tab-enter callback you would have to invent your own state tracking to do the same.

The one command the plugin adds is `:ScopeMoveBuf <tab_nr>`, which moves the current buffer to the specified tab. There is a subtlety in its behaviour that the documentation spells out. If the tab number is omitted or invalid, the plugin prompts for a tab number. And if the current buffer is the only buffer in the current tab, it is copied to the target rather than moved, to retain the layout, otherwise it is moved.

That copy-instead-of-move rule is the kind of detail that would otherwise be discovered the hard way. Moving the only buffer out of a tab leaves an empty tab behind, which breaks the mental model of a tab as a workspace, so the plugin keeps the tab alive with a copy and lets you close it yourself.

The command table shows the parameter is a tab number, so `:ScopeMoveBuf 2` moves the buffer to the second tab.

## The Telescope extension and the experimental session support

Two extensions sit on top of the core behaviour, and both are aimed at people who have particular plugins in their setup.

The Telescope extension shows all buffers from all tabs, which is the one view that scoped buffers deliberately deny you inside a single tab. The setup is one line:

```lua
-- init.lua
    require("telescope").load_extension("scope")
```

After that the picker is available with:

```
:Telescope scope buffers
```

The name of the picker is `scope buffers`, and it is what makes this plugin useful with Telescope, because a Telescope buffer picker normally shows the global list, which is exactly the confusing list the plugin exists to partition.

The session support is marked experimental, and it exists because scoped state does not survive a session file by default. When you write a session in Neovim, the tab pages and the buffers are recorded, but the association between a buffer and the tab it belongs to is plugin state rather than Neovim state, so a plain session restore loses the scoping.

The setup for that case requires a specific session option:

```lua
-- init.lua
vim.opt.sessionoptions = { -- required
    "buffers",
    "tabpages",
    "globals",
}
require("scope").setup({})
```

With that in place, two commands handle the plugin's own state: `:ScopeLoadState` loads the plugin's state as a global variable from a session file, and `:ScopeSaveState` saves it as a global variable in a session file. The globals entry in sessionoptions is the mechanism, since it is what lets the state travel in the session file at all.

The documentation also gives the API endpoints for anyone who would rather handle the state by hand: `require("scope.session").serialize_state()` and `require("scope.session").deserialize_state(state)`.

Two session managers are covered. For nvim-possession, the plugin is listed as a dependency and the save and post hooks call the two commands:

```lua
{
    "gennaro-tedesco/nvim-possession",
    lazy = false,
    dependencies = {
        {
            "tiagovla/scope.nvim",
            lazy = false,
            config = true,
        },
    },
    config = function()
        require("nvim-possession").setup({
            autoload = true,
            autoswitch = {
                enable = true,
            },
            save_hook = function()
                vim.cmd([[ScopeSaveState]]) -- Scope.nvim saving
            end,
            post_hook = function()
                vim.cmd([[ScopeLoadState]]) -- Scope.nvim loading
            end,
        })
    end,
},
```

The resession.nvim route is different in an interesting way. The description says it works without :mksession under the hood, and the configuration overrides the buffer filter and registers the plugin as an extension:

```lua
{
    "stevearc/resession.nvim",
    lazy = false,
    dependencies = {
        {
            "tiagovla/scope.nvim",
            lazy = false,
            config = true,
        },
    },
    opts = {
        -- override default filter
        buf_filter = function(bufnr)
            local buftype = vim.bo[bufnr].buftype
            if buftype == 'help' then
              return true
            end
            if buftype ~= "" and buftype ~= "acwrite" then
              return false
            end
            if vim.api.nvim_buf_get_name(bufnr) == "" then
              return false
            end

            -- this is required, since the default filter skips nobuflisted buffers
            return true
        end,
        extensions = { scope = {} }, -- add scope.nvim extension
    }
},
```

The comment inside that filter is the one to notice. The default resession filter skips nobuflisted buffers, and scope.nvim's state depends on those buffers being saved, so the filter has to be overridden or the restored state will be missing entries. That is a coupling between two plugins that no amount of reading either one's own documentation would reveal, and it is the sort of thing that makes the experimental label honest.

## Where scope.nvim is the wrong choice

The first limit is that this is a modification to a core command rather than a replacement for buffer management. If you already use a plugin that redefines :bnext, you now have two plugins claiming the same behaviour, and the result depends on load order. The plugin does not document how it interacts with buffer management plugins, so a conflict is something you discover rather than something you can check before installing.

The second limit is that the multi-window case is not addressed in the documentation. The description is about tabs, and the hooks are all about tab lifecycle. A window inside a tab is a different dimension, and nothing indicates whether the scoping is per tab or per window. If your workflow relies on splits within a tab, that is a question the project description does not answer.

The third is the session support, which is marked experimental. It requires changing your global sessionoptions, it depends on a global variable travelling in the session file, and it needs a specific filter override in resession.nvim. Each of those is a small thing, but together they mean the session integration is the least settled part of the plugin and the most likely to break when either side changes.

The fourth is that there is no compatibility information and no version history. No GitHub releases, no minimum Neovim version, no changelog, and the last push was on 2026-10-12. A plugin that touches buffer internals without stating which Neovim versions it supports is a plugin you will need to check after a Neovim upgrade.

Compare that with not using a plugin at all and living with the global buffer list. The difference in approach is that scope.nvim adds a layer of state, the buffer-to-tab association, that Neovim does not have, and every feature that depends on that state has to be re-established when the state is rebuilt, which is what the session extension is doing. If you use one or two tabs, the global list is not a problem. If you use six and switch between them constantly, the plugin removes a real source of friction for the price of the state you now own.

## Licence, contributing and what to check before you install it

scope.nvim is released under the MIT licence, with the LICENSE file in the repository root. MIT is one of the most permissive terms, with no copyleft obligation and no requirement beyond keeping the copyright notice, and for a Lua plugin that most people vendor into their own configuration, that removes the usual licensing question entirely.

The project asks for pull requests and reminds contributors to review the formatting with stylua, and the `.stylua.toml` file at the top level is what defines the style the project expects. That is a small thing, but a formatting configuration checked into the repository is the sign of a project that cares about consistent diffs, which matters more in a small codebase where every line is read often.

The repository layout is as small as it gets. A LICENSE, the project description, the formatting configuration and a `lua/` directory. There is no test directory in the top level listing, no CI configuration beyond what is in `.github/`, and no documentation directory separate from the project description. The documentation is the description, which is thorough about the hooks, the command and the extensions, and silent about everything else.

What to check before you install it is a short list. First, find out whether the buffer navigation commands you already use are provided by another plugin, because that is the most likely source of a conflict and the project description will not tell you. Second, work out how many tabs you actually keep open, because the plugin earns its keep above two and does nothing for one.

Third, if you use a session manager, look at the experimental session support and the resession filter override, and test it with a throwaway session file rather than discovering the problem when a real session fails to restore. The nvim-possession route is the simpler of the two because the plugin is listed as a dependency and the save and post hooks call the two commands, but both routes depend on a global variable surviving a session write.

Fourth, load the Telescope extension and try the `scope buffers` picker. If you already live in Telescope, that picker is the feature that makes the whole thing worth it, because it gives you a cross-tab view that the scoped cycling deliberately takes away.

## Conclusion

scope.nvim fits a Neovim user who works across several tabs at once and finds that :bnext and :bprev jump into buffers belonging to other tabs, and it fits someone with a Telescope-heavy workflow who wants one picker across all tabs. It does not fit a user who has not settled on a buffer management plugin already, since it is a modification of buffer cycling behaviour rather than a replacement for it, and it does not fit a user whose session manager conflicts with the experimental session extension. Before you install it, read the hooks list, because the six lifecycle callbacks are the extension point, and if you use a session manager, test the :ScopeSaveState and :ScopeLoadState pair with a scratch session file first.

## FAQ

### What does scope.nvim do in Neovim?

It introduces scoped buffers so that :bnext and :bprev cycle only through the buffers belonging to the current tab, instead of walking the global buffer list. The result is that each tab behaves as its own workspace rather than sharing one crowded list of buffers.

### How do I install and configure scope.nvim?

With lazy.nvim the specification is `{ "tiagovla/scope.nvim", config = true }`, and the setup call is `require("scope").setup({})`. The options table takes a `hooks` key, where you can define the tab lifecycle callbacks, and no configuration is required for the core behaviour.

### Which hooks does scope.nvim provide?

Six, covering three tab events with a before and an after each: pre_tab_enter, post_tab_enter, pre_tab_leave, post_tab_leave, pre_tab_close and post_tab_close. They are passed in the `hooks` table of the setup options.

### How does the :ScopeMoveBuf command work?

It moves the current buffer to the specified tab, prompting for a tab number if the argument is omitted or invalid. If the current buffer is the only buffer in the current tab it is copied to the target rather than moved, so the tab layout is retained, otherwise it is moved.

### How does scope.nvim work with session managers?

The session support is experimental and requires `vim.opt.sessionoptions` to include buffers, tabpages and globals. Two commands, :ScopeSaveState and :ScopeLoadState, save and restore the plugin's state as a global variable, with integration examples for nvim-possession and resession.nvim, and the API endpoints serialize_state() and deserialize_state(state) for manual handling.

### What licence is scope.nvim released under?

The plugin is released under the MIT licence, with the licence text in the LICENSE file at the repository root. The repository publishes no GitHub releases and the last push was on 2026-10-12, and the project asks contributors to review formatting with stylua.

## Sources

- [Issues](https://github.com/tiagovla/scope.nvim/issues)
- [License: MIT](https://github.com/tiagovla/scope.nvim/blob/main/LICENSE)
- [README](https://github.com/tiagovla/scope.nvim/blob/main/README.md)
- [tiagovla/scope.nvim on GitHub](https://github.com/tiagovla/scope.nvim)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/tiagovla-scope-nvim
