В предыдущей главе мы вывели формулу грамматики Vim: оператор + движение = команда (подробнее в главе 05). Движения отвечают на вопрос "куда идёт курсор": dw удаляет от курсора до начала следующего слова, d$ - до конца строки. Это мощно, но у движений есть врождённый недостаток: они привязаны к текущей позиции курсора.
Представьте, что курсор стоит в середине слова parameter, и вам нужно удалить это слово целиком. С движениями придётся сначала прыгнуть в начало (b), потом удалить (dw) - две операции, и результат зависит от того, где именно вы стояли. А если слово внутри кавычек, скобок или HTML-тега? Движениями вы будете считать символы и промахиваться.
Текстовые объекты решают эту проблему радикально. Вместо "отсюда и докуда" они описывают смысловую единицу текста целиком: слово, предложение, абзац, содержимое скобок, строку в кавычках, тело функции. Курсор может стоять в любой точке объекта - результат будет одинаковым. Именно это делает текстовые объекты предсказуемыми и идеально совместимыми с точкой-командой (., подробнее в главе 07).
Если движения - это "глаголы плюс направления", то текстовые объекты - это существительные, описывающие структуру кода и текста. Освоив их, вы перестаёте думать символами и начинаете думать конструкциями языка.
Концепция: модификаторы i и a
Текстовый объект состоит из двух частей: модификатора и типа объекта.
Модификаторов всего два:
- i - inner (внутренний): сам объект без окружающих ограничителей и без прилегающих пробелов.
- a - around (вокруг, "весь"): объект вместе с ограничителями (скобки, кавычки, теги) или вместе с прилегающим пробелом.
Мнемоника проста: читайте команду как фразу. ci( - change inner parens, "изменить содержимое скобок". da" - delete around quotes, "удалить строку в кавычках вместе с кавычками". yip - yank inner paragraph, "скопировать абзац".
Почему текстовые объекты мощнее движений
- Курсоронезависимость. diw удаляет слово одинаково, где бы курсор ни стоял внутри него. dw зависит от позиции.
- Предсказуемый dot-repeat. Поскольку результат не зависит от курсора, повтор через . ведёт себя стабильно. Команды на движениях (dw, d$) повторяются непредсказуемо именно из-за привязки к позиции.
- Работа со структурой. Объекты вроде i(, it, ip знают про вложенность скобок, теги и границы абзацев - то, что движениями вручную не воспроизвести.
Полная карта текстовых объектов
Слова, предложения, абзацы
Код: Выделить всё
Объект | Что выделяет
--------+---------------------------------------------------------------------------------
iw | слово (inner word) без окружающих пробелов
aw | слово вместе с одним прилегающим пробелом (around word)
iW | WORD - последовательность непробельных символов (например, foo-bar.baz целиком)
aW | WORD вместе с прилегающим пробелом
is | предложение (inner sentence)
as | предложение вместе с пробелом после него
ip | абзац (inner paragraph) - блок непустых строк
ap | абзац вместе с прилегающими пустыми строками
Тонкость абзацев и пустых строк. Для Vim "абзац" - это блок строк, отделённый пустыми строками. Поведение ip/ap на границах часто удивляет:
- ap забирает абзац вместе с пустыми строками после него; если пустых строк после нет (например, абзац в конце файла), то прихватит пустые строки перед ним.
- ip сам по себе на пустой строке выделяет не текст, а блок прилегающих пустых строк - то есть dip, стоя на пустой строке, удалит всю пачку пустых строк, а не текст вокруг. На непустой строке ip выделяет блок непустых строк без обрамляющих пустых.
Пары: скобки, кавычки, теги
Код: Выделить всё
Объект | Синонимы | Что выделяет
--------+---------------+----------------------------------------
i( a( | i) a) ib ab | круглые скобки ( ... )
i{ a{ | i} a} iB aB | фигурные скобки { ... }
i[ a[ | i] a] | квадратные скобки [ ... ]
i< a< | i> a> | угловые скобки < ... >
i" a" | - | двойные кавычки " ... "
i' a' | - | одинарные кавычки ' ... '
i a | - | обратные кавычки ...
it at | - | XML/HTML-тег (tag), включая содержимое
Для парных объектов важна вложенность: Vim ищет ближайшую охватывающую пару. Со счётчиком можно выйти на уровень выше. Возьмём f(g(x)) с курсором внутри x: v2i( выделит содержимое внешних скобок (g(x)), а для удаления - d2i(. Поведение count нагляднее всего изучать через visual (v2i( сначала покажет границы), поэтому и демонстрировать его лучше так - а уже потом применять оператор.
Тег-объекты it/at работают только там, где есть теги (HTML, XML, JSX). cit меняет содержимое тега, не трогая сами теги, а dat сносит элемент целиком вместе с открывающим и закрывающим тегами. it включает в содержимое и пробелы с переводами строк внутри тега - то есть для <p>\n текст\n</p> объект it захватит весь внутренний текст с отступами, а не только слово.
Вложенные одноимённые теги. На структуре <div><div>x</div></div> команда dit/dat всегда берёт ближайший охватывающий тег относительно курсора (внутренний <div>). Чтобы выйти к внешнему элементу, примените команду повторно (после первого dit курсор окажется во внешнем теге) или используйте счётчик - 2dit/2dat поднимается на тег наружу. Это та же логика вложенности, что и у скобок.
Практика: "было -> нажимаем -> стало"
Ниже | обозначает позицию курсора.
diw против daw
Код: Выделить всё
было: foo |bar baz
нажимаем: diw
стало: foo | baz <- остаётся 3 пробела (1 после foo + 2 перед baz), Vim их не схлопывает
Код: Выделить всё
было: foo |bar baz
нажимаем: daw
стало: foo |baz <- удалено слово И прилегающий пробел
ci" - изменить строку в кавычках
Код: Выделить всё
было: const title = "old va|lue";
нажимаем: ci"
стало: const title = "|"; (режим вставки, содержимое стёрто)
ci( против di(
Код: Выделить всё
было: result = calculate(|a, b, c);
нажимаем: ci(
стало: result = calculate(|); (вставка, аргументы стёрты)
Код: Выделить всё
было: result = calculate(|a, b, c);
нажимаем: di(
стало: result = calculate(); (содержимое удалено, курсор внутри скобок)
Код: Выделить всё
было: result = calculate(|a, b, c);
нажимаем: da(
стало: result = calculate; (удалены скобки и всё внутри, имя функции осталось)
cit - переписать содержимое тега
Код: Выделить всё
было: <span class="badge">old|text</span>
нажимаем: cit
стало: <span class="badge">|</span> (вставка)
Композиция с любыми операторами
Грамматика универсальна - объекты сочетаются не только с d/c/y:
Код: Выделить всё
gUiw -> слово в ВЕРХНИЙ регистр
gqip -> переформатировать (reflow) абзац по textwidth
>i{ -> увеличить отступ содержимого блока { }
=i{ -> автоотступ содержимого блока
yi" -> скопировать строку из кавычек
vi( -> выделить содержимое скобок (визуально)
Лучшие практики 2026
Предпочитайте text objects движениям для повторяемых правок
Если правка должна повторяться через ., выбирайте объект, а не движение. ciw + . пройдётся по словам предсказуемо; cw + . будет зависеть от того, куда встал курсор. Это базовое правило повторяемого редактирования (подробнее про . в главе 07).
Изучайте и отлаживайте объекты через visual-режим
Прежде чем применять деструктивный оператор (d, c), сомнительный объект стоит "примерить" в visual-режиме. Наберите vi(, vi", vit, vap - Vim подсветит ровно то, что захватит объект, ничего не меняя. Увидели верные границы - выходите (<Esc>) и выполняйте di(/ci(; границы не те (не та глубина вложенности, курсор на пустой строке, не тот тег) - поправьте счётчик или позицию. Это лучший способ и учить новые объекты, и отлаживать неожиданный результат: сначала v-версия для проверки выделения, потом оператор. Особенно помогает с count (v2i() и тегами (vat), где границы неочевидны.
Расширяйте грамматику плагином mini.ai (Neovim)
Встроенных объектов хватает на 80% задач, но исторически их не хватало для "редактирования на расстоянии" и для нестандартных разделителей. Эталоном расширенных объектов долго был wellle/targets.vim: он добавил поиск ближайшего объекта вперёд/назад, объекты на разделителях (i,, i., i:) и счётчики для пар.
В Neovim 2026 эту роль нативно закрывает mini.ai (из организации nvim-mini; старые пути echasnovski/* остались как редиректы). Он не просто расширяет встроенные a/i, но добавляет:
- next/last-варианты - an/in (around/inside next) и al/il (around/inside last). Теперь cin( меняет содержимое следующих скобок, даже если курсор ещё не внутри них.
- search_method - стратегию поиска объекта: cover, cover_or_next (по умолчанию), cover_or_prev, cover_or_nearest, next, previous, nearest.
- движения к границам - g[ и g] (goto_left/goto_right) прыгают к началу/концу текущего объекта.
- поддержку dot-repeat, v:count и последовательного применения из коробки.
- объекты для вызовов функций, аргументов и тегов без отдельных плагинов.
Код: Выделить всё
{
"nvim-mini/mini.ai",
event = "VeryLazy",
opts = {},
}
Для точности по синтаксису - Treesitter-объектыВнимание: возможная коллизия клавиш. mini.ai заводит свои мапы an/in/al/il в visual- и operator-режимах. В ядре Neovim таких дефолтных мапов нет, поэтому ничего "встроенного" вы не теряете. Но если вы сами (или другой плагин) повесили на an/in инкрементальное выделение узлов Treesitter - например, через ручной keymap к модулю incremental_selection из nvim-treesitter, - мапы конфликтуют. Тогда выберите другие клавиши для next/last-объектов mini.ai либо для своего инкрементального выделения, чтобы они не перекрывались.
Regex- и Lua-pattern-объекты (включая встроенные спеки mini.ai) не отличают код от строки или комментария и иногда срабатывают ложно. Когда нужна AST-точность - выделить функцию, класс, параметр строго по дереву разбора - есть два пути:
- nvim-treesitter-textobjects (ветка main, требует Neovim 0.12, статус experimental): модули select, move, swap, lsp_interop. Даёт объекты af/if (функция), ac/ic (класс), параметры, а также repeatable-moves через ; и , (как нативные f/t) и обмен узлами AST.
- mini.ai с MiniAi.gen_spec.treesitter() - получить и широту mini.ai, и точность AST в одном плагине, не подключая второй.
Код: Выделить всё
Объект | Что выделяет
---------+------------------------------------------
af / if | функция целиком / её тело
ac / ic | класс целиком / его тело
aa / ia | аргумент (параметр) с разделителем / без
Vim против NeovimПомните: nvim-treesitter-textobjects официально experimental и завязан на статус Treesitter в ядре Neovim. Базовая подсветка Treesitter в Neovim 0.12 уже в ядре (подробнее в главе 27), но текстовые объекты остаются отдельным плагином.
- В чистом Vim (в том числе Vim 9.1) нет нативного Treesitter - для расширенных объектов берите wellle/targets.vim. На серверах, где нельзя ставить много софта, встроенных iw/i(/it обычно достаточно.
- В Neovim предпочитайте mini.ai (+ опционально Treesitter-спеки): нативный Lua, dot-repeat, next/last и count из коробки.
- Пытаться использовать объект как движение. iw в normal-режиме просто переключит в insert-режим (i - вставка, w - буква). Объект работает только после оператора или в visual-режиме: diw, viw.
- Путать i и a при удалении слова. diw оставляет двойной пробел в предложении; для удаления слова "начисто" нужен daw. А вот при change берите ciw, чтобы не съесть пробел перед следующим словом.
- Путать w и W. Хотите схватить foo-bar.baz или URL целиком - это iW/aW, а не iw.
- Забывать про вложенность скобок. Внутри f(g(x)) команда di( удалит содержимое ближайшей охватывающей пары. Чтобы выйти на уровень наружу - используйте счётчик: d2i( (а проверить границы заранее удобно через v2i().
- Ждать, что it/at сработают вне разметки. Тег-объекты осмысленны только в HTML/XML/JSX.
- Полагаться на regex-объекты там, где нужен синтаксис. mini.ai по pattern-спекам может "зацепить" скобку внутри строки или комментария. Для надёжного выделения функций/аргументов используйте gen_spec.treesitter() или nvim-treesitter-textobjects.
- Ставить nvim-treesitter-textobjects без указания ветки. В 2026 нужна ветка main и Neovim 0.12; старая master заморожена. Несоответствие версии даёт жёсткую ошибку.
- Случайно перебить своё инкрементальное выделение мапами an/in из mini.ai. Это не дефолт ядра и не LSP, а коллизия с вашим собственным (или плагинным) keymap инкрементального выделения Treesitter. Если такой keymap есть - выберите для одной из сторон другие клавиши.
- Базовая пара i/a. Возьмите строку set("key", value, opts). Поставьте курсор в любое место внутри скобок и выполните di(, затем отмените (u) и выполните da(. Сравните результат. Повторите с ci( - обратите внимание, что вы оказываетесь в режиме вставки.
- Слово против WORD. В строке email: user@example.com, попробуйте diw и diW, стоя на example. Объясните себе разницу в захваченном тексте.
- Кавычки на расстоянии. На строке const msg = "hello world"; поставьте курсор в самое начало строки (на c) и выполните ci". Убедитесь, что Vim сам нашёл кавычки справа. Это работает только для кавычек - проверьте, что для ( так не выйдет без плагина.
- Теги. В <div class="card"><p>текст</p></div> встаньте на слово текст. Выполните cit, затем dit, затем dat (каждый раз отменяя через u). Проследите, что меняется: содержимое, внутренний тег, весь элемент.
- Грамматика-множитель. На абзаце текста выполните по очереди: gUiw (слово в верхний регистр), gqip (reflow абзаца), >i{ (если вы в блоке кода). Убедитесь, что один и тот же объект работает с разными операторами.
- (Neovim) next-объект. Установите mini.ai. Поставьте курсор перед строкой с несколькими скобками и выполните cin( - измените содержимое следующих скобок, не наводя на них курсор вручную. Затем повторите правку через ..
- Текстовый объект = модификатор (i/a) + тип. i - внутреннее содержимое, a - вместе с ограничителями/пробелом.
- Объекты применяются после оператора (diw, ci", yap) или в visual-режиме (vi(), но не как самостоятельные команды.
- Главное преимущество над движениями - курсоронезависимость и, как следствие, предсказуемый dot-repeat.
- Запоминайте не комбо, а паттерн: любой оператор x любой объект. Это множитель эффективности.
- Ключевые объекты: iw/aw, iW/aW, is/as, ip/ap, пары i(/i{/i[/i< (и синонимы ib/iB), кавычки i"/i'/i`, теги it/at.
- В 2026 расширяйте грамматику: в Neovim - mini.ai (next/last, search_method, count, dot-repeat) и Treesitter-объекты (af/ac/aa) для AST-точности; в чистом Vim - targets.vim.
- Дальше - глава 07 о точке-команде и повторяемости, где текстовые объекты раскрываются на полную.