Текстовые объекты: оперируем структурой, а не символами

Рейтинг: 64.6% · 12 голосов
Исчерпывающий курс по Vim и Neovim: модальное редактирование и грамматика (операторы, движения, текстовые объекты, точка-команда), регистры, метки, макросы, поиск и :substitute, :global, буферы/окна/quickfix, визуальный режим, конфигурация на Lua, маппинги, плагины (lazy.nvim), LSP, автодополнение (blink.cmp), Treesitter, fuzzy-поиск, Git, эргономика и антипаттерны. 34 главы, актуально на 2026 (Vim 9.1, Neovim 0.11/0.12).
Ответить
Аватара пользователя
Roman_Vim
Сообщения: 34
Зарегистрирован: 11 май 2026, 05:31

Текстовые объекты: оперируем структурой, а не символами

Сообщение Roman_Vim »

Оглавление курса (34)
  1. Философия Vim: модальное редактирование и почему это актуально в 2026
  2. Установка и выживание: первый запуск, режимы, как выйти
  3. Перемещение по тексту: движения как основа эффективности
  4. Базовое редактирование: вставка, удаление, копирование
  5. Грамматика Vim: операторы + движения = команды
  6. Текстовые объекты: оперируем структурой, а не символами (вы здесь)
  7. Точка-команда и повторяемость: "один штрих, чтобы повторить"
  8. Регистры: продвинутая работа с буфером обмена
  9. Метки и прыжки: навигация по истории перемещений
  10. Поиск: /, ?, *, # и магия паттернов
  11. Замена :substitute и регулярные выражения Vim
  12. Команда :global - оружие массового редактирования
  13. Буферы, окна и вкладки: организация рабочего пространства
  14. Quickfix и location list: пакетная работа по всему проекту
  15. Навигация по файлам: netrw, fuzzy-поиск и проекты
  16. Макросы: автоматизация повторяющихся правок
  17. Визуальный режим и блочное редактирование
  18. Трюки режима вставки
  19. Свёртки, автодополнение и аббревиатуры (встроенные)
  20. Конфигурация с нуля: init.lua и разумные дефолты 2026
  21. Маппинги: leader, which-key и здравые привычки
  22. Скриптинг: основы Vimscript и Lua для Neovim
  23. Менеджеры плагинов: lazy.nvim и lazy-loading
  24. Neovim в 2026: чем отличается и почему выбирают
  25. LSP в Neovim: умные возможности языка
  26. Автодополнение 2026: blink.cmp против nvim-cmp
  27. Treesitter: точная подсветка и структурное редактирование
  28. Fuzzy-поиск: Telescope, fzf-lua и snacks.picker
  29. Git внутри редактора: fugitive, gitsigns и lazygit
  30. Дистрибутивы, отладка и терминал: быстрый старт и интеграции
  31. Vim повсюду: IDE, браузер и shell
  32. Эргономика и антипаттерны: как не навредить себе и не застрять
  33. Рабочие рецепты, тренажёры и план обучения
  34. Шпаргалка-справочник: все ключевые команды одним списком
Зачем это нужно

В предыдущей главе мы вывели формулу грамматики Vim: оператор + движение = команда (подробнее в главе 05). Движения отвечают на вопрос "куда идёт курсор": dw удаляет от курсора до начала следующего слова, d$ - до конца строки. Это мощно, но у движений есть врождённый недостаток: они привязаны к текущей позиции курсора.

Представьте, что курсор стоит в середине слова parameter, и вам нужно удалить это слово целиком. С движениями придётся сначала прыгнуть в начало (b), потом удалить (dw) - две операции, и результат зависит от того, где именно вы стояли. А если слово внутри кавычек, скобок или HTML-тега? Движениями вы будете считать символы и промахиваться.

Текстовые объекты решают эту проблему радикально. Вместо "отсюда и докуда" они описывают смысловую единицу текста целиком: слово, предложение, абзац, содержимое скобок, строку в кавычках, тело функции. Курсор может стоять в любой точке объекта - результат будет одинаковым. Именно это делает текстовые объекты предсказуемыми и идеально совместимыми с точкой-командой (., подробнее в главе 07).

Если движения - это "глаголы плюс направления", то текстовые объекты - это существительные, описывающие структуру кода и текста. Освоив их, вы перестаёте думать символами и начинаете думать конструкциями языка.

Концепция: модификаторы i и a

Текстовый объект состоит из двух частей: модификатора и типа объекта.

Модификаторов всего два:
  • i - inner (внутренний): сам объект без окружающих ограничителей и без прилегающих пробелов.
  • a - around (вокруг, "весь"): объект вместе с ограничителями (скобки, кавычки, теги) или вместе с прилегающим пробелом.
Текстовые объекты не работают в normal-режиме сами по себе - это не движения, на которые можно "прыгнуть". Они применяются либо после оператора (d, c, y, gU, gq и т. д.), либо в visual-режиме для расширения выделения. То есть iw - это не команда, а diw, ciw, yiw, viw - команды.

Мнемоника проста: читайте команду как фразу. ci( - change inner parens, "изменить содержимое скобок". da" - delete around quotes, "удалить строку в кавычках вместе с кавычками". yip - yank inner paragraph, "скопировать абзац".

Почему текстовые объекты мощнее движений
  1. Курсоронезависимость. diw удаляет слово одинаково, где бы курсор ни стоял внутри него. dw зависит от позиции.
  2. Предсказуемый dot-repeat. Поскольку результат не зависит от курсора, повтор через . ведёт себя стабильно. Команды на движениях (dw, d$) повторяются непредсказуемо именно из-за привязки к позиции.
  3. Работа со структурой. Объекты вроде i(, it, ip знают про вложенность скобок, теги и границы абзацев - то, что движениями вручную не воспроизвести.
Это прямое следствие философии композируемости: вы учите N операторов и M объектов и получаете NxM команд. Знание работает множителем, а не суммой.

Полная карта текстовых объектов

Слова, предложения, абзацы

Код: Выделить всё

Объект  |  Что выделяет
--------+---------------------------------------------------------------------------------
iw      |  слово (inner word) без окружающих пробелов
aw      |  слово вместе с одним прилегающим пробелом (around word)
iW      |  WORD - последовательность непробельных символов (например, foo-bar.baz целиком)
aW      |  WORD вместе с прилегающим пробелом
is      |  предложение (inner sentence)
as      |  предложение вместе с пробелом после него
ip      |  абзац (inner paragraph) - блок непустых строк
ap      |  абзац вместе с прилегающими пустыми строками
Разница w против W: "слово" (w) разрывается на пунктуации - foo-bar это три слова (foo, -, bar). "WORD" (W) разрывается только пробелами - foo-bar это один WORD. Когда нужно схватить user@example.com или /path/to/file целиком - берите iW.

Тонкость абзацев и пустых строк. Для Vim "абзац" - это блок строк, отделённый пустыми строками. Поведение ip/ap на границах часто удивляет:
  • ap забирает абзац вместе с пустыми строками после него; если пустых строк после нет (например, абзац в конце файла), то прихватит пустые строки перед ним.
  • ip сам по себе на пустой строке выделяет не текст, а блок прилегающих пустых строк - то есть dip, стоя на пустой строке, удалит всю пачку пустых строк, а не текст вокруг. На непустой строке ip выделяет блок непустых строк без обрамляющих пустых.
Поэтому, если dap/dip дали неожиданный результат, проверьте, не стоял ли курсор на пустой строке и есть ли пустые строки только с одной стороны абзаца.

Пары: скобки, кавычки, теги

Код: Выделить всё

Объект  |  Синонимы     |  Что выделяет
--------+---------------+----------------------------------------
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: b - bracket (круглая скобка), B - фигурная (как "Block"). Поэтому dib == di(, а diB == di{. Любую из парных клавиш можно набирать с открывающего или закрывающего символа - di( и di) эквивалентны.

Для парных объектов важна вложенность: 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        <- удалено слово И прилегающий пробел
Правило: iw - только текст, aw - текст с пробелом, чтобы не оставалось "дырки" из двойных пробелов. Когда удаляете слово из предложения - почти всегда нужен daw. Когда меняете слово на другое (c) - нужен ciw, чтобы не съесть пробел перед следующим словом.

ci" - изменить строку в кавычках

Код: Выделить всё

было:    const title = "old va|lue";
нажимаем: ci"
стало:   const title = "|";   (режим вставки, содержимое стёрто)
Обратите внимание: курсор стоял внутри кавычек, и не важно где. Более того, Vim умеет находить кавычки, даже если курсор стоит перед ними на той же строке - он прыгнет к ближайшей паре кавычек справа. Это удобная особенность именно кавычек.

ci( против di(

Код: Выделить всё

было:    result = calculate(|a, b, c);
нажимаем: ci(
стало:   result = calculate(|);   (вставка, аргументы стёрты)

Код: Выделить всё

было:    result = calculate(|a, b, c);
нажимаем: di(
стало:   result = calculate();    (содержимое удалено, курсор внутри скобок)
c оставляет вас в режиме вставки готовым печатать новые аргументы; d просто удаляет. Сравните с da(:

Код: Выделить всё

было:    result = calculate(|a, b, c);
нажимаем: da(
стало:   result = calculate;   (удалены скобки и всё внутри, имя функции осталось)
Обратите внимание: da( сносит только пару скобок и их содержимое, а окружающий идентификатор (имя вызова calculate) остаётся нетронутым - объект a( не захватывает прилегающее слово. Именно поэтому da( удобен, когда нужно убрать список аргументов вместе со скобками, не трогая имя функции.

cit - переписать содержимое тега

Код: Выделить всё

было:    <span class="badge">old|text</span>
нажимаем: cit
стало:   <span class="badge">|</span>   (вставка)
А dat удалит весь элемент <span ...>...</span> целиком.

Композиция с любыми операторами

Грамматика универсальна - объекты сочетаются не только с d/c/y:

Код: Выделить всё

gUiw   -> слово в ВЕРХНИЙ регистр
gqip   -> переформатировать (reflow) абзац по textwidth
>i{    -> увеличить отступ содержимого блока { }
=i{    -> автоотступ содержимого блока
yi"    -> скопировать строку из кавычек
vi(    -> выделить содержимое скобок (визуально)
Один раз выученный объект i{ мгновенно работает со всеми операторами, которые вы знаете. В этом и есть множитель грамматики.

Лучшие практики 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 и последовательного применения из коробки.
  • объекты для вызовов функций, аргументов и тегов без отдельных плагинов.
Практический минимум - добавить mini.ai в lazy.nvim (про менеджеры плагинов см. главу 23):

Код: Выделить всё

{
  "nvim-mini/mini.ai",
  event = "VeryLazy",
  opts = {},
}
Внимание: возможная коллизия клавиш. mini.ai заводит свои мапы an/in/al/il в visual- и operator-режимах. В ядре Neovim таких дефолтных мапов нет, поэтому ничего "встроенного" вы не теряете. Но если вы сами (или другой плагин) повесили на an/in инкрементальное выделение узлов Treesitter - например, через ручной keymap к модулю incremental_selection из nvim-treesitter, - мапы конфликтуют. Тогда выберите другие клавиши для next/last-объектов mini.ai либо для своего инкрементального выделения, чтобы они не перекрывались.
Для точности по синтаксису - Treesitter-объекты

Regex- и Lua-pattern-объекты (включая встроенные спеки mini.ai) не отличают код от строки или комментария и иногда срабатывают ложно. Когда нужна AST-точность - выделить функцию, класс, параметр строго по дереву разбора - есть два пути:
  1. nvim-treesitter-textobjects (ветка main, требует Neovim 0.12, статус experimental): модули select, move, swap, lsp_interop. Даёт объекты af/if (функция), ac/ic (класс), параметры, а также repeatable-moves через ; и , (как нативные f/t) и обмен узлами AST.
  1. mini.ai с MiniAi.gen_spec.treesitter() - получить и широту mini.ai, и точность AST в одном плагине, не подключая второй.
Типичные AST-объекты:

Код: Выделить всё

Объект   |  Что выделяет
---------+------------------------------------------
af / if  |  функция целиком / её тело
ac / ic  |  класс целиком / его тело
aa / ia  |  аргумент (параметр) с разделителем / без
Помните: nvim-treesitter-textobjects официально experimental и завязан на статус Treesitter в ядре Neovim. Базовая подсветка Treesitter в Neovim 0.12 уже в ядре (подробнее в главе 27), но текстовые объекты остаются отдельным плагином.
Vim против Neovim
  • В чистом 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 есть - выберите для одной из сторон другие клавиши.
Упражнения
  1. Базовая пара i/a. Возьмите строку set("key", value, opts). Поставьте курсор в любое место внутри скобок и выполните di(, затем отмените (u) и выполните da(. Сравните результат. Повторите с ci( - обратите внимание, что вы оказываетесь в режиме вставки.
  1. Слово против WORD. В строке email: user@example.com, попробуйте diw и diW, стоя на example. Объясните себе разницу в захваченном тексте.
  1. Кавычки на расстоянии. На строке const msg = "hello world"; поставьте курсор в самое начало строки (на c) и выполните ci". Убедитесь, что Vim сам нашёл кавычки справа. Это работает только для кавычек - проверьте, что для ( так не выйдет без плагина.
  1. Теги. В <div class="card"><p>текст</p></div> встаньте на слово текст. Выполните cit, затем dit, затем dat (каждый раз отменяя через u). Проследите, что меняется: содержимое, внутренний тег, весь элемент.
  1. Грамматика-множитель. На абзаце текста выполните по очереди: gUiw (слово в верхний регистр), gqip (reflow абзаца), >i{ (если вы в блоке кода). Убедитесь, что один и тот же объект работает с разными операторами.
  1. (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 о точке-команде и повторяемости, где текстовые объекты раскрываются на полную.
👍4 ❤️2 🔥2 😄 🤔
Аватара пользователя
kernelpro
Сообщения: 1
Зарегистрирован: 11 май 2026, 10:29

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение kernelpro »

ci" это просто магия, реально не важно где курсор внутри кавычек стоит. раньше я мышкой выделял значение и стирал, теперь cit/ci" и всё. жаль раньше не знал
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
semyon8
Сообщения: 1
Зарегистрирован: 06 июн 2026, 14:44

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение semyon8 »

вопрос про граничный случай: а если у меня dip стоя на пустой строке, оно реально пачку пустых строк сожрёт? у меня сегодня так и вышло, думал баг, потом дошло что курсор был на пустой. в уроке про это есть, проглядел сначала
👍 ❤️1 🔥1 😄 🤔1
Аватара пользователя
moonbeam
Сообщения: 1
Зарегистрирован: 24 май 2026, 02:40

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение moonbeam »

Roman_Vim писал(а):курсор может стоять в любой точке объекта - результат будет одинаковым
вот это и есть главная мысль. перешёл с cw на ciw именно ради точки. cw + . прыгал куда попало, а ciw + . идёт по словам ровно
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
svelte22
Сообщения: 1
Зарегистрирован: 17 май 2026, 11:24

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение svelte22 »

поделюсь своим затыком: пытался diW схватить user@example.com, а у меня запятая в конце тоже улетала. оказалось я строку с запятой брал, W же только на пробелах рвётся. так что aW осторожнее
👍 ❤️ 🔥1 😄 🤔1
Аватара пользователя
cor3077
Сообщения: 1
Зарегистрирован: 13 май 2026, 06:59

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение cor3077 »

наконец понял зачем b и B в скобках. dib это di(, diB это di{. мнемоника bracket/Block, теперь хоть запомню
👍1 ❤️1 🔥 😄 🤔
Аватара пользователя
guru_vasya
Сообщения: 2
Зарегистрирован: 19 май 2026, 01:47

Re: Текстовые объекты: оперируем структурой, а не символами

Сообщение guru_vasya »

спс, про vi( перед тем как делать di( - золото. примеряешь выделение, видишь границы, потом уже оператор. особенно с тегами vat где непонятно что захватит
👍 ❤️2 🔥 😄 🤔
Ответить
← Предыдущая глава
Грамматика Vim: операторы + движения = команды
Следующая глава →
Точка-команда и повторяемость: "один штрих, чтобы повторить"

Все главы курса «Vim и Neovim: модальное редактирование от основ до современного Neovim»

Поделиться темой: ✈ Telegram VK

Вернуться в «Vim и Neovim: модальное редактирование от основ до Neovim 2026»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость