Маппинги: leader, which-key и здравые привычки

Рейтинг: 55.2% · 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

Маппинги: leader, which-key и здравые привычки

Сообщение 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. Шпаргалка-справочник: все ключевые команды одним списком
Конфигурация из главы 20 дала вам разумные дефолты: модульный init.lua, нужные опции, автокоманды. Но настоящая сила персонального Vim начинается там, где вы перестаёте подстраиваться под редактор и начинаете подгонять его под свои пальцы. Маппинг (mapping, переназначение клавиш) - это инструмент, которым вы превращаете повторяющиеся последовательности нажатий в один аккорд, навешиваете действия плагинов на удобные клавиши и строите свой личный "командный язык" поверх грамматики Vim.

Маппинги - обоюдоострый инструмент. Грамотно выстроенная раскладка экономит тысячи нажатий в день и снимает когнитивную нагрузку. Бездумная - ломает мышечную память, конфликтует с плагинами и оставляет вас с конфигом, в котором через полгода вы сами не разберётесь. Эта глава о том, как делать первое и не делать второго: про разницу map и noremap, режимные варианты, современный vim.keymap.set в Lua, клавишу <leader>, альтернативы для выхода из режима вставки, дискаверабилити через which-key.nvim и принципы, которые удержат вашу раскладку здравой.

Зачем вообще переназначать клавиши

Vim из коробки уже невероятно эффективен - его грамматика "оператор + движение" (глава 05) даёт сотни команд из десятка примитивов. Так зачем маппинги?
  • Действия плагинов и LSP не имеют дефолтных клавиш под ваш вкус. vim.lsp.buf.code_action, открытие fuzzy-поиска, git-операции - всё это нужно куда-то повесить.
  • Длинные команды вроде :nohlsearch<CR> хочется сократить до одного нажатия.
  • Эргономика: Esc далеко, точка с запятой ; под мизинцем простаивает, а <leader> открывает целое второе измерение клавиатуры.
  • Единая мышечная память между Neovim, IdeaVim и браузером (подробнее в главе 31) строится именно на согласованных маппингах.
Ключевой принцип, к которому мы вернёмся не раз: маппинг должен добавлять возможности, а не отнимать привычные. Каждый раз, переопределяя дефолтную клавишу, вы платите за это тем, что встроенная функция станет недоступна - и эта плата должна быть осознанной.

map против noremap: всегда noremap

Это самое важное правило главы, поэтому начнём с него. В классическом Vimscript есть две группы команд переназначения:
  • map (и режимные nmap, imap, vmap, xmap) - создают рекурсивный маппинг. Правая часть снова проходит через таблицу маппингов.
  • noremap (nnoremap, inoremap, vnoremap, xnoremap) - создают нерекурсивный маппинг. Правая часть интерпретируется как встроенные команды, минуя пользовательские переназначения.
Почему это критично, покажет классический пример. Допустим, вы поменяли местами поведение j и gj (движение по визуальным строкам в обёрнутом тексте):

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

" ПЛОХО: рекурсивный map
nmap j gj
Теперь j означает gj. Но gj в правой части тоже пройдёт через маппинги. Если позже кто-то (или плагин) замапит g, поведение поедет. А в худшем случае вы получите бесконечную рекурсию: nmap j gj при наличии nmap g something_with_j зациклится, и Vim выдаст E223: recursive mapping.

Нерекурсивный вариант свободен от этой ловушки:

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

" ХОРОШО: nnoremap - правая часть это всегда встроенные j/g
nnoremap j gj
nnoremap k gk
Здесь gj гарантированно означает встроенное движение по экранным строкам, что бы вы ни намапили дальше.
Подвох с count. Даже нерекурсивный nnoremap j gj не идеален: он ломает движение со счётчиком. Команда 5j обычно прыгает ровно на 5 логических строк (это привычно для навигации по relativenumber), но с маппингом j -> gj она прыгнет на 5 визуальных строк, и на длинных обёрнутых строках курсор окажется не там, где вы ждёте. Каноничный рецепт - сделать маппинг условным через expr: при явном счётчике вести себя как обычный j, а без счётчика - как gj. Как это написать, разберём ниже в разделе про expr.
Правило простое: по умолчанию пишите noremap/nnoremap/inoremap/... Рекурсивный map нужен в редких случаях - обычно когда вы сознательно хотите задействовать в правой части другой свой маппинг или маппинг плагина (например, цепочка поверх <Plug>-маппингов, которые многие плагины предоставляют именно для этого). Если вы не уверены, нужна ли вам рекурсия, - она вам не нужна. Берите noremap.
Исключение, которое подтверждает правило: <Plug>-маппинги. Многие плагины экспортируют действия вида <Plug>(SomeAction), которые сами по себе ничего не делают, пока вы их не привяжете. Для них нужен именно рекурсивный map, потому что <Plug> обязан пройти через таблицу маппингов, чтобы развернуться в реальную команду. Это единственный массовый легитимный случай nmap/xmap.
Режимные варианты: nnoremap, inoremap, vnoremap, xnoremap

Маппинг всегда привязан к режиму. Одна и та же клавиша в normal-режиме и в insert-режиме - это два совершенно разных действия, и Vim это строго различает.

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

Команда   |  Режим             |  Когда применять
----------+--------------------+-----------------------------------------------------------------
nnoremap  |  Normal            |  Большинство ваших маппингов: команды, движения, вызовы действий
inoremap  |  Insert            |  Хоткеи внутри набора текста (escape-альтернативы, автопары)
vnoremap  |  Visual + Select   |  Действия над выделением (но захватывает и Select-режим)
xnoremap  |  Visual только     |  Действия над визуальным выделением, не трогая Select
snoremap  |  Select только     |  Редко; Select-режим используют сниппеты
cnoremap  |  Command-line      |  Хоткеи в строке : (навигация, вставка)
tnoremap  |  Terminal          |  Хоткеи во встроенном терминале Neovim
onoremap  |  Operator-pending  |  Кастомные движения/текстовые объекты для операторов
Тонкий, но важный момент - vnoremap против xnoremap. Команда vnoremap действует и в Visual, и в Select-режиме. Select-режим - это тот, в котором печать символа заменяет выделение (его активно используют движки сниппетов). Если вы мапите что-то для визуального выделения и не хотите случайно сломать поведение сниппетов, точнее использовать xnoremap - он покрывает только настоящий Visual-режим (v, V, Ctrl-v). На практике: для действий над выделенным текстом предпочитайте xnoremap, оставляя vnoremap для случаев, где вы сознательно хотите захватить и Select.

Маленький, но полезный стартовый пример - перемещение выделенных строк вверх/вниз:

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

" Двигаем выделение вниз/вверх и переиндентируем (gv сохраняет выделение)
xnoremap J :m '>+1<CR>gv=gv
xnoremap K :m '<-2<CR>gv=gv
vim.keymap.set в Lua: современный способ

В конфиге 2026 года на init.lua (глава 20) маппинги пишут не Vimscript-командами, а через Lua-функцию vim.keymap.set. Её сигнатура:

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

vim.keymap.set(mode, lhs, rhs, opts)
  • mode - строка режима ('n', 'i', 'v', 'x', 't'...) или таблица режимов {'n', 'x'}.
  • lhs - клавиша(и), которые нажимаем.
  • rhs - строка с командами или Lua-функция.
  • opts - таблица опций.
Главное преимущество перед Vimscript: rhs может быть прямо Lua-функцией, без обёрток :lua ...<CR>. И ещё одно: vim.keymap.set нерекурсивен по умолчанию (noremap = true), то есть здравое поведение встроено - отдельный "noremap-вариант" не нужен.

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

-- Очистить подсветку поиска
vim.keymap.set('n', '<leader>h', '<cmd>nohlsearch<CR>', { desc = 'Снять подсветку поиска' })

-- Вызов Lua-функции напрямую
vim.keymap.set('n', '<leader>w', function()
  vim.cmd.write()
  vim.notify('Сохранено')
end, { desc = 'Сохранить файл' })

-- Один маппинг сразу для normal и visual
vim.keymap.set({ 'n', 'x' }, '<leader>y', '"+y', { desc = 'Yank в системный буфер' })
Обратите внимание на <cmd>...<CR> вместо :...<CR>. Запись через <cmd> не переключает редактор в command-line режим, работает в любом режиме одинаково и не сбивает выделение в visual - это рекомендуемая форма для вызова Ex-команд из маппинга.

Поле desc: маппинги, которые сами себя документируют

Самая важная опция в opts - это desc, человекочитаемое описание. Оно решает сразу две задачи:
  1. Появляется в выводе :map (и удобнее всего - в :verbose map <lhs>), помогая вам же разобраться в своём конфиге.
  2. Автоматически подхватывается which-key.nvim (о нём ниже) - вам не нужно отдельно регистрировать описания, достаточно desc в самом маппинге.
Поэтому правило 2026 года: каждому осмысленному маппингу - свой desc. Это превращает раскладку в самодокументируемую.

Другие полезные опции:

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

Опция                   |  Значение
------------------------+--------------------------------------------------------------
desc                    |  Описание (для which-key и :map)
silent = true           |  Не эхоить команду в command-line
buffer = 0 (или bufnr)  |  Маппинг только для текущего/конкретного буфера
expr = true             |  rhs - выражение, его результат используется как набор клавиш
remap = true            |  Сделать маппинг рекурсивным (для <Plug>-целей)
nowait = true           |  Не ждать продолжения, если есть более длинный маппинг
Буфер-локальные маппинги (buffer) особенно важны для LSP: их вешают в автокоманде LspAttach, чтобы клавиши действовали только там, где есть языковой сервер (подробнее про LSP - в главе 25). Аналогично ftplugin-маппинги навешивают на конкретный тип файла.

Удалить маппинг можно через vim.keymap.del(mode, lhs, opts) - это пригодится, когда дистрибутив вроде LazyVim уже навесил что-то, что вам мешает.

expr = true: маппинг, который вычисляет свою правую часть

Опция expr стоит особняком и заслуживает отдельного примера, потому что это нетривиальный, но очень полезный случай. Обычно rhs - это фиксированный набор клавиш. С expr = true всё иначе: rhs становится выражением (Vimscript-строкой или, в Lua, функцией, возвращающей строку), и Vim сначала вычисляет это выражение, а результат уже подставляет как набор клавиш. То есть маппинг сам решает, во что развернуться, в зависимости от контекста.

Классический пример - тот самый умный j/k, чинящий проблему со счётчиком из раздела про noremap. Идея: если пользователь набрал явный счётчик (v:count не ноль, например 5j), ведём себя как обычный j (прыжок по логическим строкам); если счётчика нет - как gj (по визуальным):

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

vim.keymap.set('n', 'j', "v:count == 0 ? 'gj' : 'j'", { expr = true, desc = 'Вниз: gj без счётчика, j со счётчиком' })
vim.keymap.set('n', 'k', "v:count == 0 ? 'gk' : 'k'", { expr = true, desc = 'Вверх: gk без счётчика, k со счётчиком' })
Теперь j без числа аккуратно идёт по обёрнутым строкам, а 5j снова прыгает ровно на 5 логических строк - счётчик не сломан. Обратите внимание: expr-маппинг по умолчанию остаётся нерекурсивным, так что 'gj'/'j' в результате - это встроенные движения.

Второй частый сценарий - умный <CR> в insert-режиме для плагинов автодополнения/автопар: подтвердить выбранный пункт меню, если оно открыто, иначе вставить обычный перевод строки. Логику пишут как Lua-функцию, возвращающую нужную последовательность клавиш:

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

vim.keymap.set('i', '<CR>', function()
  -- если открыто меню автодополнения (pum) - подтверждаем выбор
  if vim.fn.pumvisible() == 1 then
    return '<C-y>'
  end
  return '<CR>'
end, { expr = true, desc = 'Умный Enter: подтвердить меню или перенос строки' })
Тонкость expr-маппингов: внутри выражения нельзя менять состояние редактора напрямую (двигать курсор, переключать режим) - функция должна быть "чистой" и лишь возвращать клавиши, которые Vim затем выполнит. Если в Lua-функции для expr вам нужны спецклавиши в возвращаемой строке (<C-y>, <CR> и т.п.), их корректно собирает vim.api.nvim_replace_termcodes или передача опции replace_keycodes = true (по умолчанию включена для expr-маппингов в vim.keymap.set).
<leader> и <localleader>: второе измерение клавиатуры

Клавиш на клавиатуре конечное число, и почти все заняты грамматикой Vim. <leader> (лидер) решает эту проблему элегантно: это клавиша-префикс, после которой открывается целое отдельное пространство ваших личных команд. Нажали <leader>, затем ещё одну-две клавиши - и сработало ваше действие, не конфликтуя с встроенными.

По умолчанию <leader> - это обратный слеш \, что неудобно. Современный стандарт (kickstart.nvim, LazyVim) - пробел (<Space>): он большой, под обоими большими пальцами, и в normal-режиме почти бесполезен (двигает курсор вправо, близко к l).

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

vim.g.mapleader = ' '
vim.g.maplocalleader = ' '
Критически важно: эти две строки должны стоять в самом начале init.lua, до загрузки плагинов (до require('lazy')...). Причина: когда плагин при загрузке регистрирует свои keys = { '<leader>x' }, он подставляет текущее значение лидера. Если вы зададите mapleader после загрузки плагинов, они "запомнят" старый \, и ваши leader-маппинги "не сработают". Это едва ли не самая частая ошибка новичков. Подробнее про порядок загрузки - в главе 23.
После этого <leader> в маппингах разворачивается в пробел:

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

vim.keymap.set('n', '<leader>w', '<cmd>write<CR>', { desc = 'Сохранить' })
-- теперь "пробел w" сохраняет файл
<localleader> - для маппингов конкретного типа файла

<localleader> (локальный лидер) - это второй префикс, предназначенный для буфер-локальных маппингов, специфичных для типа файла. Логика разделения такая:
  • <leader> - глобальные действия (поиск файлов, git, переключение буферов).
  • <localleader> - действия, осмысленные только в этом языке: "скомпилировать LaTeX", "запустить Python-клетку", "выполнить блок в R".
Их можно задать одинаковыми (часто оба = пробел), но удобнее развести: например, <leader> - пробел, <localleader> - запятая ,. Тогда не возникает двусмысленности в ftplugin-маппингах.

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

vim.g.mapleader = ' '
vim.g.maplocalleader = ','

-- В ftplugin/tex.lua:
vim.keymap.set('n', '<localleader>b', '<cmd>!latexmk %<CR>', { buffer = true, desc = 'Собрать LaTeX' })
Альтернативы Esc: jk, Ctrl-[

Esc - клавиша, которую в Vim жмут постоянно (выход из insert-режима, отмена операций), но на современных клавиатурах она вынесена далеко в угол. Существуют два классических подхода сократить путь.

Ctrl-[ - буквально тот же Esc

Малоизвестный факт: Ctrl-[ посылает ровно тот же кейкод, что и Esc (escape, 0x1b). Это не маппинг, не настройка - это свойство терминала, работающее везде и всегда, без задержек.

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

Режим вставки -> нажимаем Ctrl-[ -> вышли в normal, мгновенно
Многим, кто привыкает держать пальцы на home-row, Ctrl-[ оказывается удобнее Esc и не требует вообще никакой конфигурации. Минус - нужна привычка к комбинации с Ctrl.

jk (или jj) - маппинг-аккорд

Популярный приём - замапить редкую буквенную последовательность на выход:

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

vim.keymap.set('i', 'jk', '<Esc>', { desc = 'Выход из insert' })
Идея в том, что jk почти не встречается внутри реального текста, а пальцы на home-row. Но у наивного маппинга есть подводный камень - лаг. После нажатия j Neovim вынужден ждать timeoutlen (по умолчанию ровно 1000 мс, часто урезают до 200-300 мс), чтобы понять: это одиночный j для печати буквы или начало jk. Пока он ждёт, символ j не появляется. При быстром наборе это раздражает, а слова с настоящим jk (редко, но бывает) набираются неуклюже.

Решений два:
  1. Не мапить jk вообще, а использовать Ctrl-[ - нулевая задержка, ничего настраивать не надо. Это самый "дешёвый" путь.
  2. Поставить better-escape.nvim - плагин детектит комбо jk без опоры на timeoutlen: одиночный j печатается мгновенно, а быстрое jk срабатывает как Esc. Это снимает компромисс между лагом и удобством.
И отдельное предупреждение: не урезайте timeoutlen слишком агрессивно ради jk. Если поставить timeoutlen = 100, вы не будете успевать дожимать многоклавишные leader-последовательности (<leader>gp и т.п.) - Vim сбросит ожидание раньше, чем вы нажмёте вторую клавишу. Баланс обычно около 300-500 мс, а проблему лага jk лучше решать better-escape.nvim или переходом на Ctrl-[.

which-key.nvim: дискаверабилити вместо запоминания

Когда у вас три десятка leader-маппингов, держать их все в голове невозможно. which-key.nvim решает это: после нажатия <leader> (или любого префикса) он показывает всплывающее окно со списком доступных продолжений и их описаний. Вы нажимаете пробел - и видите: f -> find, g -> git, l -> lsp... Это превращает раскладку из "надо помнить" в "можно подсмотреть".

which-key тесно интегрирован с vim.keymap.set: он автоматически берёт desc из ваших маппингов. Если вы аккуратно проставляли desc, отдельная регистрация не нужна - попап заполнится сам.

Минимальная установка через lazy.nvim (глава 23):

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

{
  'folke/which-key.nvim',
  event = 'VeryLazy',
  opts = {},
}
Чтобы попап красиво группировал префиксы, можно задать им имена. В свежем API which-key это делают через таблицу spec:

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

{
  'folke/which-key.nvim',
  event = 'VeryLazy',
  opts = {
    spec = {
      { '<leader>f', group = 'find/files' },
      { '<leader>g', group = 'git' },
      { '<leader>l', group = 'lsp' },
      { '<leader>b', group = 'buffers' },
    },
  },
}
Если вы используете дистрибутив (LazyVim) и какой-то его дефолтный маппинг мешает или вы хотите спрятать его из попапа, лишнее прячут через spec с hidden = true, а удаляют полностью через vim.keymap.del.

Принципы здравой раскладки

Маппинги легко превратить в свалку. Несколько принципов удержат их в порядке.

Не ломайте дефолты бездумно

Каждая встроенная клавиша Vim - это потенциально полезная команда. Прежде чем переназначить s, H, x или другую дефолтную клавишу, спросите: что я теряю? s - это "substitute character" (часто его переопределяют под surround/flash, но осознанно). Ctrl-a/Ctrl-x - инкремент/декремент числа, незаменимы. Хорошая привычка: по умолчанию вешать новое на <leader>-пространство, а дефолтные клавиши трогать только когда есть веская причина и вы точно знаете, чем жертвуете.

Особенно осторожно - с клавишами, которые ядро Neovim уже заняло под полезное. Например, в Neovim 0.11+ из коробки работают LSP-маппинги grn (rename), gra (code action), grr (references), gri (implementation), gO (document symbol), K (hover) - переопределять их вручную, дублируя то, что уже есть, обычно лишнее (см. главу 25).

Мнемоника

Клавиша должна напоминать о действии. <leader>f -> find, <leader>g -> git, <leader>w -> write, <leader>q -> quit. Когда раскладка мнемонична, вы запоминаете её один раз и не путаетесь. Произвольные сочетания вроде "<leader>x для git-blame" забудутся через неделю.

Группировка по префиксам

Складывайте родственные действия под общий префикс: всё про git - под <leader>g (<leader>gs status, <leader>gp push, <leader>gb blame), всё про поиск - под <leader>f. Это даёт две выгоды: проще помнить (думаете "git" -> жмёте g -> смотрите варианты) и which-key красиво показывает их сгруппированными.

Согласованность между окружениями

Если вы работаете в Neovim, IdeaVim и браузере с Vimium, держите ключевые маппинги (особенно <leader> и часто используемые команды) одинаковыми. Единая мышечная память дороже локальной оптимизации под каждую среду. Подробнее про "Vim повсюду" - в главе 31.

Отладка конфликтов: :verbose map и <unique>

Рано или поздно маппинг "не работает" - а на деле его кто-то перебил: другой ваш маппинг, плагин или дистрибутив. Главный инструмент диагностики - :verbose map <lhs>. Он показывает не только во что развёрнут маппинг (включая его desc), но и из какого файла и с какой строки он задан (Last set from ...). Это сразу отвечает на вопрос "кто перебил мой <leader>gg":

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

:verbose nmap <leader>gg
Команды без аргумента (:nmap, :map) выводят всю таблицу режима - полезно, чтобы окинуть взглядом, что вообще навешано (в том числе буфер-локальные маппинги и <Plug>-цепочки плагинов, у которых тоже видно desc, если он задан).

Чтобы не перебить чужой маппинг молча, во Vimscript есть модификатор <unique>: с ним команда map завершится ошибкой, если такой lhs уже занят, вместо тихой перезаписи.

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

nnoremap <unique> <leader>x <cmd>echo "mine"<CR>
В Lua у vim.keymap.set прямого аналога <unique> нет - там перезапись всегда тихая, поэтому привычка периодически проверять :verbose map особенно ценна. Если же вы сознательно снимаете чужой маппинг, делайте это явно через vim.keymap.del, а не надеясь на порядок загрузки.

И ещё одна практическая мелочь про дискаверабилити: which-key подхватывает desc не только у глобальных, но и у буфер-локальных маппингов (тех, что навешаны с buffer), так что LSP- и ftplugin-действия тоже появляются в попапе - но только в том буфере, где они активны. А вот "голые" <Plug>-цели без desc в попапе выглядят неинформативно: если строите цепочку поверх <Plug>, давайте именно своему финальному маппингу осмысленный desc.

Полезные стартовые маппинги

Вот набор разумных маппингов, с которых можно начать свой конфиг. Все - нерекурсивные, с desc, на Lua.

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

local map = vim.keymap.set

-- Снять подсветку поиска
map('n', '<leader>h', '<cmd>nohlsearch<CR>', { desc = 'Снять подсветку' })

-- Сохранить / выйти
map('n', '<leader>w', '<cmd>write<CR>', { desc = 'Сохранить' })
map('n', '<leader>q', '<cmd>quit<CR>', { desc = 'Закрыть окно' })

-- Системный буфер обмена (явно, без тотального unnamedplus; см. главу 08)
map({ 'n', 'x' }, '<leader>y', '"+y', { desc = 'Yank в системный буфер' })
map({ 'n', 'x' }, '<leader>p', '"+p', { desc = 'Paste из системного буфера' })

-- Вставить поверх выделения, не теряя регистр (см. главу 08)
map('x', '<leader>P', '"_dP', { desc = 'Заменить выделение без потери буфера' })

-- Удаление в чёрную дыру, не засоряя регистр
map({ 'n', 'x' }, '<leader>d', '"_d', { desc = 'Удалить в blackhole' })

-- Центрировать экран при прыжках по результатам поиска
map('n', 'n', 'nzzzv', { desc = 'Следующий результат + центр' })
map('n', 'N', 'Nzzzv', { desc = 'Предыдущий результат + центр' })

-- Центрировать при полуэкранных прыжках
map('n', '<C-d>', '<C-d>zz')
map('n', '<C-u>', '<C-u>zz')

-- Перемещение выделенных строк
map('x', 'J', ":m '>+1<CR>gv=gv", { desc = 'Сдвинуть выделение вниз' })
map('x', 'K', ":m '<-2<CR>gv=gv", { desc = 'Сдвинуть выделение вверх' })

-- Сохранять отступ при повторной вставке в visual без сдвига курсора
map('x', '<', '<gv')
map('x', '>', '>gv')

-- Навигация по окнам без Ctrl-w (см. главу 13)
map('n', '<C-h>', '<C-w>h', { desc = 'Окно слева' })
map('n', '<C-j>', '<C-w>j', { desc = 'Окно снизу' })
map('n', '<C-k>', '<C-w>k', { desc = 'Окно сверху' })
map('n', '<C-l>', '<C-w>l', { desc = 'Окно справа' })

-- Выход из терминального режима Neovim
map('t', '<C-\\><C-n>', '<C-\\><C-n>', { desc = 'Выход из terminal-режима' })
Несколько пояснений к выбору:
  • nzzzv (n + zz центрирует + zv раскрывает свёртку) держит следующий найденный результат по центру экрана с открытой свёрткой - глаза не прыгают.
  • "+y/"+p дают явный контроль над системным буфером, не вешая глобально clipboard=unnamedplus, чтобы не гонять каждый yank через системный буфер/провайдер (нюансы OSC52 и работы по SSH - в главе 08).
  • <leader>P ("_dP) - вставка поверх выделения, при которой ваш регистр не затирается удалённым текстом.
Лучшие практики 2026
  • vim.keymap.set с desc для каждого маппинга. Это нерекурсивно по умолчанию, принимает Lua-функции напрямую, а desc делает раскладку самодокументируемой и автоматически кормит which-key. Старые nnoremap/inoremap в Vimscript остаются валидными, но в init.lua пишите на Lua.
  • Лидер - пробел, задан первым. vim.g.mapleader = ' ' и vim.g.maplocalleader - самые первые строки init.lua, до плагинов. Это убирает самую частую поломку leader-маппингов.
  • which-key.nvim как слой дискаверабилити. Группируйте маппинги по логическим префиксам (<leader>f find, <leader>g git, <leader>l lsp) и давайте группам имена через spec. Не регистрируйте описания руками - пусть desc делает это за вас.
  • Escape: Ctrl-[ или better-escape.nvim. Наивный inoremap jk <Esc> даёт лаг из-за ожидания timeoutlen. Ctrl-[ - буквально Esc без задержки и без конфигурации; better-escape.nvim детектит jk без таймаута. Не урезайте timeoutlen агрессивно - сломаете leader-последовательности.
  • Опирайтесь на встроенные дефолты Neovim 0.11+. LSP-маппинги (grn, gra, grr, gri, gO, K) уже есть из коробки - не дублируйте их вручную (глава 25). Меньше своего кода - меньше конфликтов.
  • Буфер-локальные маппинги через buffer/LspAttach/ftplugin. Языко-специфичные действия не должны висеть глобально. Вешайте их с buffer = true в автокоманде LspAttach или в ftplugin/<ft>.lua, а под локальные - <localleader>.
  • Минимально трогайте дефолты. Новое вешайте в <leader>-пространство. Переопределяя встроенную клавишу, осознавайте, что теряете.
Частые ошибки
  • mapleader задан после загрузки плагинов. Плагины захватывают старый \, leader-маппинги "молча не работают". Лечится переносом vim.g.mapleader в самое начало init.lua.
  • Использование рекурсивного map/nmap по привычке. Ведёт к неожиданному поведению и в пределе к E223: recursive mapping. По умолчанию - noremap/vim.keymap.set (он уже нерекурсивен).
  • Лаг от inoremap jk <Esc>. Каждый j тормозит на timeoutlen. Решение - Ctrl-[ или better-escape.nvim, а не урезание timeoutlen.
  • Слепое урезание timeoutlen. Ломает многоклавишные leader-цепочки: не успеваете дожать вторую клавишу до сброса.
  • vnoremap там, где нужен xnoremap. vnoremap цепляет ещё и Select-режим и может конфликтовать с поведением сниппетов. Для действий над выделением точнее xnoremap.
  • Маппинги без desc. Раскладка становится непрозрачной, which-key показывает пустые строки, через полгода вы сами не помните, что делает <leader>x.
  • Ручное дублирование дефолтных LSP-маппингов 0.11+. grn/gra/grr/K уже встроены - повторное навешивание создаёт конфликты и лишний код.
  • Использование :...<CR> вместо <cmd>...<CR>. Старая форма переключает в command-line режим и может сбивать выделение в visual; <cmd> работает чище во всех режимах.
  • nnoremap j gj без expr. Простой маппинг j -> gj ломает движение со счётчиком: 5j идёт по визуальным строкам вместо логических. Каноничное решение - expr-маппинг v:count == 0 ? 'gj' : 'j'.
  • Молчаливо перебитый маппинг. Если ваш маппинг "не сработал", его, скорее всего, перебил плагин или дистрибутив. Найдите виновника через :verbose map <lhs> (покажет Last set from), а не гадайте.
Упражнения
  1. Перенесите лидер. Если ещё не сделали - поставьте vim.g.mapleader = ' ' первой строкой init.lua, перезапустите Neovim и проверьте, что <Space> стал лидером (например, через <leader>w -> сохранение). Сознательно поломайте: переместите строку mapleader в конец файла и убедитесь, что leader-маппинги перестали работать. Верните обратно.
  1. Поставьте which-key.nvim через lazy.nvim, добавьте 5-6 leader-маппингов с разными префиксами (<leader>f, <leader>g, <leader>b) и осмысленными desc. Нажмите <Space> и убедитесь, что попап показывает ваши описания. Дайте префиксам имена-группы через spec.
  1. Сравните три способа выхода из insert. Поработайте поочерёдно с Esc, Ctrl-[ и наивным inoremap jk <Esc>. Прочувствуйте лаг у jk. Затем установите better-escape.nvim и убедитесь, что лаг исчез. Выберите подход, который вам ближе.
  1. Соберите git-группу. Под префиксом <leader>g навесьте хотя бы три осмысленных маппинга (пусть пока это заглушки вроде <cmd>echo "status"<CR>), все с desc, и убедитесь, что which-key группирует их под именем "git". Позже (глава 29) замените заглушки реальными git-действиями.
  1. Найдите рекурсию. Намеренно напишите nmap j gj и nmap g <something with j>, спровоцируйте E223, затем перепишите всё на nnoremap/vim.keymap.set и убедитесь, что проблема ушла. Это закрепит понимание, зачем нужен noremap.
  1. Буфер-локальный маппинг. Создайте ftplugin/markdown.lua (или Lua-аналог) с маппингом <localleader>p, который делает что-то осмысленное только для Markdown (например, <cmd>set wrap!<CR>). Откройте .md и обычный файл, убедитесь, что маппинг есть только в первом.
  1. Почините счётчик через expr. Включите set wrap на длинном абзаце. Сначала повесьте наивный vim.keymap.set('n', 'j', 'gj') и убедитесь, что 5j ведёт себя не так, как ожидается. Затем перепишите на expr-вариант v:count == 0 ? 'gj' : 'j' и проверьте, что и j, и 5j снова работают правильно.
  1. Найдите, кто перебил маппинг. Повесьте <leader>x на одно действие, затем где-то ниже в конфиге - на другое (имитируя плагин). Выполните :verbose nmap <leader>x и по строке Last set from найдите, какой из них победил.
Главное из главы
  • Всегда noremap (в Lua - vim.keymap.set, он нерекурсивен по умолчанию). Рекурсивный map - только для <Plug>-маппингов.
  • Маппинги режимные: nnoremap/inoremap/xnoremap/... Для действий над выделением предпочитайте xnoremap, а не vnoremap.
  • vim.keymap.set(mode, lhs, rhs, opts) принимает Lua-функции напрямую; desc обязателен - он документирует раскладку и кормит which-key.
  • <leader> - пробел, заданный первой строкой init.lua до плагинов. <localleader> - для буфер-локальных, языко-специфичных действий.
  • Выход из insert: Ctrl-[ (буквально Esc, без задержки) или better-escape.nvim; наивный jk лагает, а урезать timeoutlen нельзя.
  • which-key.nvim делает раскладку обнаружимой; группируйте по мнемоничным префиксам.
  • expr = true позволяет маппингу вычислять свою правую часть: канон - умный j/k (v:count == 0 ? 'gj' : 'j'), чинящий счётчик, и умный <CR>. Конфликты ищите через :verbose map <lhs>.
  • Не ломайте дефолты бездумно: вешайте новое в <leader>-пространство и не дублируйте встроенные LSP-маппинги Neovim 0.11+.
👍 ❤️2 🔥2 😄 🤔3
✔ Лучший ответ сформирован автоматически — ronane
Roman_Vim писал(а):маппинг должен добавлять возможности, а не отнимать привычные дошло почему у меня всё разъезжалось. я по молодости переопределил s под surround и потом полгода не мог понять куда делся substitute. теперь только в leader пространство вешаю
Перейти к ответу →
Аватара пользователя
gregory
Сообщения: 1
Зарегистрирован: 21 май 2026, 02:26

Re: Маппинги: leader, which-key и здравые привычки

Сообщение gregory »

вот про Ctrl-[ реально не знал, всю жизнь мучался с jk и лагом. оказалось это буквально тот же эскейп и работает сразу без всякого конфига. перешёл, пальцы уже привыкли
👍1 ❤️ 🔥 😄 🤔
Аватара пользователя
ronane
Сообщения: 1
Зарегистрирован: 03 июн 2026, 13:51

Re: Маппинги: leader, which-key и здравые привычки

Сообщение ronane »

✔ Лучший ответ — сформирован автоматически
Roman_Vim писал(а):маппинг должен добавлять возможности, а не отнимать привычные
дошло почему у меня всё разъезжалось. я по молодости переопределил s под surround и потом полгода не мог понять куда делся substitute. теперь только в leader пространство вешаю
👍 ❤️ 🔥 😄 🤔
Аватара пользователя
mucous
Сообщения: 1
Зарегистрирован: 06 июн 2026, 16:04

Re: Маппинги: leader, which-key и здравые привычки

Сообщение mucous »

а у меня умный j через expr не заводился сначала, 5j всё равно по визуальным прыгал. оказалось я remap_keycodes руками выключил где то выше в конфиге. убрал - заработало v:count == 0 ? 'gj' : 'j'
👍1 ❤️1 🔥 😄 🤔
Аватара пользователя
sritter
Сообщения: 1
Зарегистрирован: 11 май 2026, 14:11

Re: Маппинги: leader, which-key и здравые привычки

Сообщение sritter »

спасибо за verbose map, реально лайфхак. полдня искал кто перебивает leader gg, оказался лейзивим со своим дефолтом. del и норм
👍 ❤️1 🔥 😄 🤔
Аватара пользователя
pacheco
Сообщения: 1
Зарегистрирован: 25 май 2026, 03:48

Re: Маппинги: leader, which-key и здравые привычки

Сообщение pacheco »

делюсь, держу localleader на запятой а leader на пробеле. поначалу думал зачем разводить, но в ftplugin для тс файлов сразу понятно что глобальное а что локальное, путаницы ноль
👍1 ❤️ 🔥 😄 🤔
Ответить
← Предыдущая глава
Конфигурация с нуля: init.lua и разумные дефолты 2026
Следующая глава →
Скриптинг: основы Vimscript и Lua для Neovim

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

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

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

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

Сейчас этот форум просматривают: Amazon [Bot] и 1 гость