# ssr.nvim: structural search and replace for Neovim with tree-sitter patterns

> ssr.nvim is a Neovim plugin that searches and replaces code by AST shape rather than by regular expression, using tree-sitter to parse the pattern and a float window to edit it. It suits editors without a language server that can do structural replacement, and its own documentation says to prefer the LSP implementation when one exists.

**cshuaimin/ssr.nvim** — Treesitter based structural search and replace plugin for Neovim.

- Repository: https://github.com/cshuaimin/ssr.nvim
- Stars: 1,010 · Forks: 13
- Language: Lua
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/cshuaimin-ssr-nvim

## What structural search buys you over a regular expression

ssr.nvim is a structural search and replace plugin for Neovim, and it points at the JetBrains documentation for structural search as the concept it implements. The link is the clearest statement of intent: this is the idea of searching for the shape of code rather than for a string of characters.

The difference is concrete. A regular expression that matches a function call will also match a call inside a comment, a string literal, or a variable with a similar name, and it will not match a call written across two lines. A structural pattern describes a node in the syntax tree, so a match is a match by construction.

The plugin is written in Lua, is MIT licensed, and is small, with a repository that holds a lua directory, a tests directory and a stylua configuration. There are no tagged releases in the project, and the last push to the repository was on 2026-04-06.

The scope is worth stating early, because the plugin itself is explicit about it. The README says ssr.nvim performs searching and replacing at the AST level and does not understand code as much as LSP servers do, and that you should use the language server's structural search and replace implementation if possible. What follows describes what it does do well, which is the case where you have no such server.

## Installing with packer.nvim and binding the key

The installation is a packer.nvim declaration for cshuaimin/ssr.nvim, with the module name given as ssr. The README notes that calling setup is optional, so the declaration can be as short as the plugin name and the module, and the configuration shown later in this article is the fuller form the same README shows.

After installation you add a mapping to open the plugin. The mapping covers normal and visual modes, and the command it calls is the open function on the module.

```lua
vim.keymap.set({ "n", "x" }, "<leader>sr", function() require("ssr").open() end)
```

The visual mode entry is not incidental. The usage instructions say that if you are not sure what structure you want to search for, you can select a region instead of putting the cursor on it, and a mapping bound only in normal mode would make that path impossible. The mapping in the README binds the leader key sequence to ssr and passes no arguments, so the plugin reads the visual selection or the cursor position depending on the mode.

The plugin is invoked through require("ssr").open(), the same function the mapping above calls.

## The setup options: window geometry and the keymap table

The configuration is optional, but when you provide it, you get control over the float window and over the keys. The example in the README shows the full set.

```lua
    require("ssr").setup {
      border = "rounded",
      min_width = 50,
      min_height = 5,
      max_width = 120,
      max_height = 25,
      adjust_window = true,
      keymaps = {
        close = "q",
        next_match = "n",
        prev_match = "N",
        replace_confirm = "<cr>",
        replace_all = "<leader><cr>",
      },
    }
```

The geometry options are given as a minimum and a maximum width and height rather than a fixed size, and adjust_window is set to true. Combined, those read as a window that opens at a sensible size and grows if the pattern needs more room, up to a ceiling.

The border option takes a Neovim border style, and rounded is the value used in the example.

The keymap table is the more interesting part, because it separates the two replacement actions. close is q. next_match is n and prev_match is N, which deliberately reuse the normal and capital N movement keys so the muscle memory of searching a buffer carries over. replace_confirm is enter, which selects which match to replace, and replace_all is the leader key followed by enter, which is the destructive one.

## Writing the pattern: a dollar sign turns an identifier into a wildcard

The workflow starts with the cursor. You put the cursor on the structure you want to search for and replace, and then open the plugin with the leader key sequence. If the placeholder code that appears in the float window is not what you intended, you exit with q and select the region instead, which gives the plugin a different piece of code to work from.

What appears in the window is placeholder search code derived from the code under the cursor, and you can edit it. The editing that matters is the wildcard syntax. A wildcard is an identifier that starts with a dollar sign, like $name. A $name wildcard in the search pattern will match any AST node, and $name in the replacement refers to what it matched.

That reference behaviour is what makes the tool more than a search. A pattern that captures an argument and puts it back in a different position is a refactoring, and it is the same capability that the JetBrains documentation describes.

Replacement happens with two keys. The leader key followed by enter replaces all matches in the current buffer, and plain enter chooses which match to replace, so you step through candidates and accept the ones you want.

The buffer scope is worth noticing. The README describes replacing in the current buffer, which is a deliberate limit compared with a project wide search, and it keeps the operation fast and predictable.

## Why the pattern is parsed in its original context

There is a short section on context, and it explains the most interesting design decision in the plugin.

ssr.nvim parses the search pattern to syntax trees to perform structural searching. But directly parsing code without context is not accurate. The example given is a TypeScript function argument written as foo: number, which will be parsed as a label without context.

That is a real ambiguity. The same three characters mean different things depending on where they appear, and a parser that has never seen the surrounding file has to guess. Left to guess, the plugin would produce a pattern that does not match the code you actually wrote.

The solution is that ssr.nvim parses the pattern in its original context, which is to say it re-parses the pattern using the surrounding code as the context rather than parsing the pattern as a standalone fragment. This is why the cursor position matters when you open the plugin, and why the escape hatch is to select a region rather than to place the cursor.

The practical consequence for a user is that the plugin depends on you pointing it at the right place. The float window is your feedback loop, showing the placeholder code the plugin inferred, and the q key plus a region selection is the recovery when it inferred wrongly.

## The alternative: the language server's own implementation

The README names the alternative directly, and it is worth spending a paragraph on it because the difference is in the depth of understanding rather than in the interface.

A language server that implements structural search and replace works from the same semantic model it uses for completion, diagnostics and go to definition. It knows which identifier is a local variable and which is a property access, it knows the type of an expression, and it can tell an argument from a keyword argument in a call that looks identical to a definition.

ssr.nvim works at the AST level, and the plugin says so. The tree-sitter grammar knows the shape of the code and it knows the language, but it does not do name resolution, type inference or scope analysis. The consequence is that a pattern which is ambiguous at the syntax level can match more than you expected, or fewer.

So the choice comes down to what is running in your editor already. If you have a language server for the language you are working in, and it offers structural search, the plugin explicitly recommends using that. ssr.nvim earns its place in the other cases, a language with no server, a server without the feature, or a situation where you want structural replacement to work without the server running.

The plugin is also honest about the state of the project, with no tagged releases, and the last push on 2026-04-06.

## Conclusion

Adopt ssr.nvim if you edit code in a language that has no language server with structural search, or if you want structural replacement to work without a running LSP, because the plugin parses the pattern with tree-sitter and matches on AST shape. Do not adopt it where a language server offers the same feature, since the project states plainly that it does not understand code as much as LSP servers do and recommends their implementation instead. Before you rely on it, check the last commit, because the last push to the repository was on 2026-04-06 and there are no tagged releases to track.

## FAQ

### How do I install ssr.nvim in Neovim?

The README shows a packer.nvim declaration for cshuaimin/ssr.nvim with module = "ssr", and notes that calling setup is optional. You then add a mapping such as vim.keymap.set({ "n", "x" }, "<leader>sr", function() require("ssr").open() end).

### How do I write a wildcard in an ssr.nvim pattern?

A wildcard is an identifier that starts with a dollar sign, like $name. In the search pattern it matches any AST node, and in the replacement it refers to whatever it matched.

### Why is my ssr.nvim search pattern wrong when I open it?

The cursor position matters, because the plugin infers the placeholder code from the structure under the cursor. The README says that if the placeholder code is not correct, exit with q and select the region instead, because the plugin parses the pattern in its original context rather than as a standalone fragment.

### When should I not use ssr.nvim?

The README states that ssr.nvim performs searching and replacing at the AST level and does not understand code as much as LSP servers do, and recommends using the language server's own structural search and replace implementation if possible.

## Sources

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

---

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