Vim Text Object: Semantic Editing
Updates:
- 2026-04-24: Use the native plugin manager with the
vim.pack.addAPI (since Neovim v0.12)
Intro
You probably do not know what the text-object is in Vim/Neovim. However, you may use it in your daily life. For instance, when you are writing code, you may want to change the arguments of a function. Take the following code as an example, let’s say you want to change the function call to bar(3, 2, 1), and the cursor currently stays on the ,
def foo():
...
res = bar(1, 2, 3)
# ^
# The position of your cursor(on `,`)
...
How will you achieve this goal? If you are familiar with Vim/Neovim, you will likely instinctively type ci) or ci(, and then you can quickly type in the new arguments. The i( or i) here are so-called text-object.
Bad practice: Use F( to jump to previous ( and then use x to delete char one by one
Built-in text-object
Check :h text-objects for more information
Simply put, text-object is a textual entity corresponding to a certain area of text, and this area of text contains structure within it. Therefore, they can be treated as a single text-object, which brings convenience to many operations.
The built-in text-object is represented as two-letter combinations:
- The first letter is either
iorairefers to text-object itself, andaalso contains the next/previous whitespace or paired symbols(e.g.{}, (), ...)- Mnemonic:
i = (i)nside,a = (a)round
- The second letter has two possibilities
- One of the paired symbols
- Paired symbols:
{}, (), [], '', "", <tag></tag>. Note that<tag></tag>is represented ast. For example, if you want to delete thefoobarin<tag>foobar</tag>, you can put the cursor within the tag and typedit - e.g.
a{=(around) {, it contains all the text within{}(inclusive)
- Paired symbols:
- (w)ord, (s)entences, (p)aragraphs
- Each one of them can be used with
iora-iw, aw, is, as, ip, ap
- Each one of them can be used with
- One of the paired symbols
Syntax-aware text-object
Previously, we mentioned that the text-object is structured in some way. It reminds me of the code, which is highly structured text.
Occasionally, we want to delete the entire function body and rewrite it. If the function body is enclosed in {}, then typically we can use ci{ to accomplish this task. However, care must be taken regarding the cursor’s position to ensure the {} of the function body is closet to the cursor, which can be somewhat annoying.
The function body in Rust is enclosed in {}. To delete the entire function body and rewrite it, we can place the cursor on the i in if, and then type ci{
fn fib(n: i32) -> i32 {
// The position of your cursor(on `i`)
// v
if n == 1 || n == 2 {
1
} else {
fib(n - 1) + fib(n-2)
}
}
But what if the function body is not enclosed in {}? For example, the Python code uses indentation to recognize the function body.
def fib(n: int):
if n == 1 or n == 2:
return 1
return fib(n - 1) + fib(n - 2)
Is there a special text-object that corresponds to the function body, considering that the code is highly structured? That is what the magic plugin - nvim-treesitter-textobjects does for us. By analyzing the syntax of code, it defines various text-object such as functions, classes, loops, etc.
If you want to use the new vim.pack API since Neovim v0.12 to configure your Neovim. You may refer to the tutorial I wrote before.
You may refer to my configuration if you also use the vim.pack API. The following code shows how to install the nvim-treesitter-textobjects plugin.
vim.pack.add({
{ src = "https://github.com/nvim-treesitter/nvim-treesitter" },
{ src = "https://github.com/nvim-treesitter/nvim-treesitter-textobjects" },
})
If you want to know what the @function.outer means here. You may refer to my previous post, where the tree-sitter and its query mechanism are explained.
I will just put the most relevant configuration code here.
vim.keymap.set({ "x", "o" }, "af", function()
require("nvim-treesitter-textobjects.select").select_textobject("@function.outer", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "if", function()
require("nvim-treesitter-textobjects.select").select_textobject("@function.inner", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "ac", function()
require("nvim-treesitter-textobjects.select").select_textobject("@class.outer", "textobjects")
end)
vim.keymap.set({ "x", "o" }, "ic", function()
require("nvim-treesitter-textobjects.select").select_textobject("@class.inner", "textobjects")
end)
Based on my personal needs, I add 4 text-object(af, if, ac, ic) to operate on function, class, and loop. You can find a syntax-aware text-object in the official README.md file.
Now we can use dif to delete the entire function body in any position within the function body :)
Wrap-up
The text-object organizes the text into objects, allowing us to operate them conveniently. With the help of nvim-treesitter-textobjects, we can operate on various programming language constructs🍺