prfx.xyz

Как я исправил баг, который не смогли починить Microsoft

Типизация

У большинства современных языков программирования есть одна общая черта — они так или иначе работают с типами данных. При этом даже этот общий аспект большинства языков всё равно отличается между ними — типизация бывает статической, и динамической; сильной, и слабой; явной, и неявной. Сейчас разговор будет именно о языках с возможностью неявной типизации.

Что такое неявная типизация?

Вообще неявная типизация бывает и в языках со статической типизацией, и с динамической типизацией. Однако чаще всего при словах “неявная типизация” у читателей в голове всё же возникает динамическая типизация. И это можно понять — в языках со статической типизацией могут быть оба варианта, а в языках с динамической типизацией — только неявная типизация.

Однако! Неявная типизация это вполне популярная функция в современных языках программирования: auto в C++, let без явного указания типа в Rust, var в C#, итд.

1let x: u32 = 10;
2let y = x; // Тип `y` сам определяется как u32

Не всё так плохо, как может показаться!

У вас может возникнуть мысль, что это ужасно неудобно — разработчик экономит всего несколько символов, но за это ему придётся постоянно держать в голове типы переменных!

Но нет, к счастью, это не так, и многие современные IDE умеют рендерить так называемые inline hints, или inlay hints. Это призрачный текст, который рендерится схоже с обычными комментариями, вставленными посреди кода, и используется для разных функций, включая показывание типов этих переменных

1let x: u32 = 10;
2let y: u32 = x; // IDE автоматически определяет тип, и отображает его

Однако не всё так радужно. Если neovim или emacs рендерят inline hint’ы как обычный текст с другим стилем, то VSCode, которым я пользуюсь, рендерит их немного иначе1

Скриншот этого фрагмента кода из VSCode

Вероятнее всего вы уже заметили что что-то не так, но если нет — вот дополнительный скриншот, на котором помимо самого текста также отображается ширина шрифта

Скриншот этого фрагмента кода из VSCode, пробел после inline hint’а не моноширинный

Надеюсь в этот раз уже все заметили проблему, но если всё же нет — сразу после inline hint’а стоит пробел, длина которого не соответствует длине используемого моноширинного шрифта.

Поиск причины и решения. Успех?

Если вы не знали — VSCode — всего-лишь браузер. Если вам хочется вы даже можете использовать его как обычную веб-страницу. Поэтому недолго думая, я нашёл как открываются devtools, и быстро нашёл элемент, отвечающий за конкретный пробел после inline hint. Это был   со следующим CSS правилом:

1.dyn-rule-8-1 {
2    width: 5px;
3    display: inline-block;
4}

В идеале это решается установкой ширины 1ch.

1.dyn-rule-8-1 {
2    width: 1ch;
3    display: inline-block;
4}

Иии… Ура! Всё починилось!

Скриншот Rust кода из VSCode, больше никаких проблем

Но… Это было как-то просто? Не может же корпорация, с капитализацией в 3 триллиона долларов допустить такую простую ошибку?

Ну, вообще, может, и допустила. Да, это и в правду было ТАК просто. Но пока что это просто эксперимент, а не фикс — нужно сперва сделать так, чтобы мне не приходилось каждый раз редактировать CSS когда я пишу код.

Для этого я воспользовался расширением Custom UI Style, которое позволяет инжектить свои CSS правила прямо в рантайме. Я создал простое правило, которое ищет dyn-rule-, и инжектит в него width: 1ch !important. Финальный код выглядит так:

1span[class*="dyn-rule-"] {
2    width: 1ch !important
3}

Я загрузил его на свой сайт, импортировал его в конфиге Custom UI Style2, и проблема была решена!

Проблема не была решена…

А почему вообще VSCode использует такое странное название dyn-rule-*-*?

Ну, это сложно описать вкратце, да и я сам не очень хорошо знаком с внутренней архитектурой VSCode. Из того, что я понял — основная проблема в том, что внутренний рендерер Monaco просто не позволяет расширениям генерировать динамические CSS правила — они либо изначально запечены в расширение, либо проходят через какое-то внутреннее API, приводящее их к общему виду dyn-rule-$0-$1, где $0 — внутренний номер расширения, а $1 — порядковый номер конкретного запроса, сгенерировавшего это правило. Как можно предположить из названия — данным API пользуется не только наш language server, но и другие расширения. В моём случае это было расширение Color Highlight.

Сломанная каретка color picker’а

Я ожидал, что так и случится, просто не знал где именно. Но мне повезло — элемент, отображающий цвет, имеет свой отдельный CSS класс colorpicker-color-decoration, который я просто могу добавить в исключения:

1span[class*="dyn-rule-"]:not([class*="colorpicker-color-decoration"]) {
2    width: 1ch !important
3}

Корректная каретка color picker’а

Также, хотя я об этом не упомянал, я экспериментировал с разной шириной width, и почти при всех значениях начинал проявляться новый баг — при переходе со строки, где нет inline hint’а, на ту, где он есть — курсор двигался не просто вертикально, но и горизонтально — получалось диагональное смещение. Я не буду углубляться в причину этой проблемы, так как хотя бы примерное объяснение займёт столько же текста, как вся эта статья. Просто стоит отметить, что Monaco отвечает не только за генерацию CSS правил, но и за управление курсором. В этом плане мне очень повезло, что предыдущая ширина 5px и нынешняя 1ch под капотом Monaco округляются до одинакового оффсета курсора, и у меня не появилось проблем от замены одного на другое.3

Заключение

На самом деле это очень нишевая проблема, и сперва я даже сам не обращал на это внимания — всё же свою задачу — помощь разработчику — inline hint’ы решают, а то, что они рендерятся немного неправильно — уже второстепенное.

Если вы читали foot-note’ы — вы уже знаете, что проблема не конкретно в устройстве VSCode/Monaco или dyn-rule-*-*4, а в том, как расширение C/C++ использует фичу inline hint’ов. По какой-то причине он самостоятельно добавляет  , и затем задаёт ему width: 5px.

Так, получается, название лживое, и я пофиксил баг не в коде компании с капитализацией 3 триллиона долларов? И да, и нет — это официальное расширение от Microsoft, однако сам баг я всё же не пофиксил — всё, что я сделал — создал небольшую заплатку, которая просто делает вид, что всё хорошо, но сам баг всё ещё присутствует, просто глубже, чем я хотел бы погружаться на данный момент.


  1. На самом деле такое происходит не со всеми inline hint’ами. Конкретно с Rust’ом такой проблемы нет, однако по моему мнению Rust код более наглядно показывает суть проблемы, поэтому примеры показывают Rust код, хотя сам текст описывает поиск и решение проблемы в inline hint’ах C++. ↩︎

  2. Custom UI Style использует собственный JSON-образный синтаксис для репрезентации CSS правил, поэтому мне было проще загрузить обычный CSS файл на свой сайт, и просто импортировать его. ↩︎

  3. Если бы проблемы от подобного всё же возникли — можно было бы просто использовать width: 0ch, или другую соответствующую ширину, хоть это и не выглядело бы также красиво. ↩︎

  4. Сперва может показаться, что некая проблема в этом самом dyn-rule-*-* всё же есть. Однако VSCode не поддерживает нативное внедрение кастомных CSS правил, поэтому возможность задавать динамическим CSS правилам собственное имя — довольно нишевая идея, которая в реальности скорее принесёт только больше проблем, чем пользы — например, из-за потенциальной коллизии имён, а также расширенных затрат на разработку и поддержку. ↩︎

Reply to this post by email ↪