Каждый русскоязычный разработчик хотя бы раз набирал ghbdtn. Это «привет» с неверной раскладкой: те же шесть физических клавиш, прочитанные другой таблицей символов. Задача выглядит простой: собрать соответствие, применить, готово. Но перед конвертацией возникает вопрос, на который у самого текста ответа нет. Была ли это ошибка?
ghbdtn может быть опечаткой, а может быть валидным идентификатором, префиксом хеша, веткой в git или куском пароля. Нажатия клавиш в обоих случаях одинаковы. Нужная информация никогда не лежала в буфере обмена, она была в голове человека в момент набора и не сохранилась. Автор плагина keyboard-magic для IntelliJ IDEA, публикующийся на Habr как i_alakey, разобрал этот тупик в статье «Раскладку нельзя определить. Можно только сделать ошибку обратимой». На момент проверки Habr показывал у публикации отметку «6 часов назад»; абсолютная дата в доступном тексте не указана. Материал представляет собой инженерный разбор личного проекта, а не исследование с выборкой или измерением точности.
Автор начинает с того, что отбрасывает три напрашивающихся решения. Словарь внутри IDE не выдерживает первого же запуска: большая часть текста в редакторе вообще не является словами ни на одном языке. Частоты биграмм отлично работают на целом абзаце, но ничего не дают на фрагменте из четырёх-шести символов, а именно такие куски и приходится переключать. Порог уверенности порождает самое неприятное поведение для горячей клавиши: инструмент иногда срабатывает, а иногда молча не делает ничего. Вывод автор формулирует без смягчений: все три подхода превращают неразрешимый вопрос в догадку, а затем эту догадку прячут.
Вместо этого плагин не спрашивает, была ли ошибка. Он задаёт вопрос, у которого есть ответ: каким алфавитом написан выделенный текст? Детектор нарочито прост. Он считает кириллические и латинские буквы, победившее большинство определяет направление, при ничьей и на пустом множестве выбирается английская раскладка. Пунктуация и цифры игнорируются, потому что они есть в обеих раскладках и не несут сигнала о намерении. Главное свойство, на котором держится дизайн, — инволюция: повторное применение операции должно вернуть исходный текст. Формально это записывается как switch(switch(x)) == x.
Здесь появляется тонкость, которую стоит разобрать отдельно. Автор связывает инволюцию со свойством таблицы, а не алгоритма. Таблица строится от физической клавиатуры: восемь строк, каждая задаёт соответствие ряда клавиш без Shift и с Shift, например qwertyuiop[]\ соответствует йцукенгшщзхъ\. Каждая физическая клавиша даёт две пары символов, и поскольку источник правды — клавиши, а не символы, получающиеся отображения являются биекцией по построению. Обе стороны собираются одним циклом из одних и тех же рядов, поэтому разъехаться не могут. Охранная строка require(enRow.length == ruRow.length) превращает ошибку выравнивания рядов в падение на инициализации класса вместо тихой порчи текста. Автор утверждает, что пересобрал таблицу из исходника: 94 символа в каждую сторону, каждый round-trip-ится, без исключений.
Но заявленная универсальная гарантия switch(switch(x)) == x не следует из описанного алгоритма выбора направления. При равном числе латинских и кириллических букв детектор возвращает EN и до, и после преобразования. Возьмём строку, где букв двух алфавитов поровну: после первого нажатия ничья сохраняется, второе нажатие снова выбирает EN, и обратного преобразования не происходит. Это не ошибка таблицы, а ограничение правила выбора направления. README проекта обещает, что повторное нажатие переводит фрагмент обратно, и одновременно описывает выбор по большинству и фолбэк EN для фрагментов без букв; эти два утверждения несовместимы для ничьих. Показанного теста на round-trip для каждого отображённого символа недостаточно, чтобы проверить свойство на всех строках. Нужен тест, включающий смешанные строки с равным числом букв двух алфавитов и непустые строки без букв, на которых преобразование меняет символы.
Самое интересное следствие поклавишного отображения автор показывает на пунктуации. ghbdtn, vbh! превращается в приветб мир!, а ghbdtn? vbh! — в привет, мир!. Запятая в двух раскладках не является одной и той же клавишей. В русской раскладке запятая набирается клавишей, на которой в английской живёт вопросительный знак, а английская запятая соответствует русской букве «б». Таблица является биекцией над клавишами, а не над смыслами. Сопоставление двух запятых друг другу сломало бы инволюцию, отняв гарантию round-trip у вопросительного знака и буквы «б». Автор называет такой обмен плохим: свойство, выполняющееся всегда, меняется на интуицию, выполняющуюся иногда.
Настоящие сложности проекта оказались не в отображении символов, а в редакторе. Слово под кареткой определяется расширением в обе стороны от позиции курсора, поэтому каретка, касающаяся любого края слова, это слово выбирает. Работает и сразу после набора, и при клике перед словом. Символы слова включают буквы, цифры и подчёркивание, но исключают пунктуацию. Поэтому end; конвертируется как end и сохраняет точку с запятой, что правильно в исходниках, но ,jkm («боль») от голой каретки превращается только в оль: ведущая запятая в слово не входит. Такое выделяют руками. Автор называет дефолт, верный в коде и слегка неверный в прозе, правильным для плагина, живущего в IDE.
Две каретки внутри одного слова попадают в один и тот же диапазон, и без защиты слово конвертировалось бы дважды, то есть возвращалось бы к исходному виду. Автор добавляет множество уже обработанных диапазонов, чтобы действие не выглядело пустым. Пакет правок применяется справа налево, потому что правка на смещении 100 делает недействительными все смещения после неё. В текущем коде замены случайно оказались одинаковой длины, так что сработал бы и прямой порядок, но автор настаивает на корректности по построению, а не по совпадению. Диапазоны собираются до входа в действие записи, потому что каретки нужно читать из документа, который под тобой не меняется.
Отдельно выделен один Ctrl+Z. Все изменения происходят внутри одной команды редактирования, поэтому откат занимает один шаг. Автор описывает это как вопрос доверия: исправляющий инструмент, откат которого требует пяти нажатий, перестаёт вызывать доверие сразу после первого случая, когда пользователь откатил три конвертации из пяти и сидит разбирается, в каком он состоянии. Для выделенного фрагмента плагин восстанавливает выделение после конвертации, чтобы следующее нажатие снова применилось к тому же тексту. Без этого математическая гарантия существует, но пользователь до неё не дотягивается.
Автор также описывает три практические ловушки платформы IntelliJ. Версия Kotlin API должна совпадать с той, которую везёт IDE, а не с той, что стоит у разработчика: IDEA 2022.2 поставляется со стандартной библиотекой Kotlin 1.6, и пиннинг apiVersion = 1.6 превращает NoSuchMethodError в чужой IDE в ошибку компиляции на своей машине. Параметр platformLocalPath не работает под C:\Program Files, потому что gradle-intellij-plugin кладёт рядом с IDE Ivy-дескриптор, а каталог недоступен на запись. Дефолтный хоткей Ctrl+Shift+R уже занят командой Replace in Files, а Cmd+Shift+R на macOS — командой Run, поэтому IDEA показывает диалог выбора. README документирует конфликт и объясняет, как перевесить сочетание.
В конце автор ставит вопрос, на который сам отвечает неуверенно. IDE знает больше, чем буфер: PSI-дерево, находится ли каретка в строковом литерале или идентификаторе, язык проекта. Контекстно-зависимая версия, которая конвертирует внутри строк и комментариев и никогда внутри идентификаторов, была бы права чаще. Но она перестаёт быть инволюцией, на которую можно опереться, и превращается в инструмент, ведущий себя по-разному в зависимости от того, где стоит каретка. Автор прямо признаёт: «держусь я за это некрепко».
Важно не принять этот инженерный блог за эмпирическое исследование. В источнике нет данных о тестировании с пользователями, измерений точности эвристик или сопоставления с вероятностной детекцией. Преимущество дешёвого отката сформулировано как проектный тезис: угадывать хорошо — интересная инженерная задача, но инструмент, правый в 95% случаев и молча неправый в 5%, оптимизирует частоту правильности, тогда как приятность определяется ценой ошибки. Это аргумент о дизайне взаимодействия, а не доказанное превосходство одного подхода. Показательный контраст — Punto Switcher, который с 2001 года делает противоположный выбор: работает в фоновом режиме, проводит статистический анализ последовательностей вводимых символов и, если сочетание нетипично для текущего языка, переключает раскладку, стирает напечатанное эмуляцией Backspace и вводит текст заново. Эта программа принадлежит «Яндексу» с 2008 года и бесплатна для некоммерческого использования, как указано в её описании в русской Википедии. Оба подхода решают одну проблему, но по-разному распределяют ошибку: один прячет неверную догадку, другой делает её обратимой одним нажатием.
Для читателя, который сталкивается с неоднозначностью в собственных проектах — будь то автозамена, нормализация данных или любая эвристика над пользовательским вводом, — урок здесь не в конкретном плагине. Когда намерение восстановить нельзя, можно перестать его выводить и сделать операцию гарантированно обратимой. Но даже это решение требует проверки на всех входах, включая ничьи, которые кажутся экзотикой до первого же смешанного идентификатора. Таблица может быть биекцией, а вся система всё равно не быть инволюцией, если направление выбирается правилом с фолбэком. Свойство, о котором заявляют как о гарантии, надо проверять не только на отдельных символах, но и на границах правил. Похожий урок о том, как легко принять удобное правило за доказанную закономерность, можно увидеть в разборе округлённых процентов, сумма которых даёт 100,1%: видимая аккуратность таблицы не отменяет необходимости проверять краевые случаи.
Источники и литература
- Раскладку нельзя определить. Можно только сделать ошибку обратимой / Хабр — авторское мнение; доступный текст страницы
- GitHub - ialakey/keyboard-magic: IntelliJ IDEA plugin that fixes text typed in the wrong keyboard layout (RU/EN) with a single shortcut. · GitHub — первичный документ; доступный текст страницы
- Раскладку нельзя определить. Можно только сделать ошибку обратимой / Комментарии / Хабр — тип публикации не установлен; доступный текст страницы
- keyboard-magic/README.md at main · ialakey/keyboard-magic · GitHub — тип публикации не установлен; доступный текст страницы
- Punto Switcher — Википедия — тип публикации не установлен; доступный текст страницы

Фото: cottonbro studio / Pexels