Принцип работы режима сокрытия: почему расчёт ширины так важен
Чему вы научитесь после изучения
- Поймёте, как работает режим сокрытия в OpenCode
- Узнаете, почему обычные инструменты форматирования дают сбой при неверном выравнивании в режиме сокрытия
- Освоите алгоритм расчёта ширины плагина (три шага)
- Поймёте роль
Bun.stringWidth
Ваша текущая проблема
Вы пишете код в OpenCode, и AI сгенерировал красивую таблицу:
| Поле | Тип | Описание |
|---|--- | ---|
| **name** | string | Имя пользователя |
| age | number | Возраст |В представлении исходного кода всё выглядит ровно. Но при переключении в режим предпросмотра таблица сдвигается:
| Поле | Тип | Описание |
|---|--- | ---|
| name | string | Имя пользователя | ← Почему короче?
| age | number | Возраст |В чём проблема? Режим сокрытия.
Что такое режим сокрытия
OpenCode по умолчанию включает режим сокрытия (Concealment Mode), который скрывает символы Markdown-разметки при рендеринге:
| Исходный код | Отображение в режиме сокрытия |
|---|---|
**жирный** | жирный (4 символа) |
*курсив* | курсив (6 символов) |
~~зачёркнутый~~ | зачёркнутый (11 символов) |
`код` | код (4 символа + фоновый цвет) |
Преимущества режима сокрытия
Позволяет сосредоточиться на содержании, а не отвлекаться на кучу ** и * символов.
Почему обычные инструменты форматирования дают сбой
Обычные инструменты форматирования таблиц при расчёте ширины считают **name** как 8 символов:
** n a m e ** = 8 символовНо в режиме сокрытия пользователь видит name — всего 4 символа.
Результат: инструмент форматирования выравнивает по 8 символам, но пользователь видит 4 символа — таблица, естественно, сдвигается.
Ключевая идея: расчёт "отображаемой ширины", а не "длины символов"
Ключевая идея плагина: рассчитывать ширину, которую видит пользователь, а не количество символов исходного кода.
Алгоритм делится на три шага:
Шаг 1: Защита блоков кода (символы внутри блоков кода не стрипаются)
Шаг 2: Стриппинг Markdown-разметки (**、*、~~ и т.д.)
Шаг 3: Расчёт финальной ширины с помощью Bun.stringWidthПрактика: понимание трёхшагового алгоритма
Шаг 1: Защита блоков кода
Почему
Markdown-разметка внутри строчного кода (в обратных кавычках) является "литералом" — пользователь увидит **bold** — все 8 символов, а не bold — 4 символа.
Поэтому перед стриппингом Markdown-разметки нужно "спрятать" содержимое блоков кода.
Реализация в коде
// Шаг 1: Извлечение и защита строчного кода
const codeBlocks: string[] = []
let textWithPlaceholders = text.replace(/`(.+?)`/g, (match, content) => {
codeBlocks.push(content)
return `\x00CODE${codeBlocks.length - 1}\x00`
})Как работает
| Вход | После обработки | Массив codeBlocks |
|---|---|---|
`**bold**` | \x00CODE0\x00 | ["**bold**"] |
`a` and `b` | \x00CODE0\x00 and \x00CODE1\x00 | ["a", "b"] |
Специальные плейсхолдеры вида \x00CODE0\x00 заменяют блоки кода, чтобы при последующем стриппинге Markdown-разметки они не пострадали.
Шаг 2: Стриппинг Markdown-разметки
Почему
В режиме сокрытия **жирный** отображается как жирный, *курсив* — как курсив. При расчёте ширины эти символы нужно удалить.
Реализация в коде
// Шаг 2: Стриппинг Markdown-разметки из не-кодовых частей
let visualText = textWithPlaceholders
let previousText = ""
while (visualText !== previousText) {
previousText = visualText
visualText = visualText
.replace(/\*\*\*(.+?)\*\*\*/g, "$1") // ***жирный курсив*** → текст
.replace(/\*\*(.+?)\*\*/g, "$1") // **жирный** → жирный
.replace(/\*(.+?)\*/g, "$1") // *курсив* → курсив
.replace(/~~(.+?)~~/g, "$1") // ~~зачёркнутый~~ → зачёркнутый
.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "$1") //  → alt
.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}Почему используется цикл while?
Для обработки вложенного синтаксиса. Например, ***жирный курсив***:
Раунд 1: ***жирный курсив*** → **жирный курсив** (удалены внешние ***)
Раунд 2: **жирный курсив** → *жирный курсив* (удалены **)
Раунд 3: *жирный курсив* → жирный курсив (удалены *)
Раунд 4: жирный курсив = жирный курсив (без изменений, выход из цикла)Обработка изображений и ссылок
- Изображения
: OpenCode показывает только alt-текст, поэтому заменяются наalt - Ссылки
[text](url): отображаются какtext (url), сохраняя информацию об URL
Шаг 3: Восстановление блоков кода + расчёт ширины
Почему
Содержимое блоков кода нужно вернуть на место, а затем рассчитать финальную отображаемую ширину с помощью Bun.stringWidth.
Реализация в коде
// Шаг 3: Восстановление содержимого блоков кода
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
return codeBlocks[parseInt(index)]
})
return Bun.stringWidth(visualText)Почему используется Bun.stringWidth?
Bun.stringWidth корректно вычисляет:
| Тип символов | Пример | Количество символов | Отображаемая ширина |
|---|---|---|---|
| ASCII | abc | 3 | 3 |
| Китайские | 你好 | 2 | 4 (каждый занимает 2 клетки) |
| Эмодзи | 😀 | 1 | 2 (занимает 2 клетки) |
| Нулевой ширины | a\u200Bb | 3 | 2 (нулевой ширины не занимает место) |
Обычный text.length может только подсчитывать количество символов, но не справляется с этими особыми случаями.
Полный пример
Предположим, содержимое ячейки: **`code`** and *text*
Шаг 1: Защита блоков кода
Вход: **`code`** and *text*
Выход: **\x00CODE0\x00** and *text*
codeBlocks = ["code"]Шаг 2: Стриппинг Markdown-разметки
Раунд 1: **\x00CODE0\x00** and *text* → \x00CODE0\x00 and text
Раунд 2: без изменений, выходШаг 3: Восстановление блоков кода + расчёт ширины
После восстановления: code and text
Ширина: Bun.stringWidth("code and text") = 13В итоге плагин выравнивает эту ячейку по ширине 13 символов, а не по 22 символам исходного кода.
Контрольные вопросы
После прохождения этого урока вы должны уметь отвечать:
- [ ] Какие символы скрывает режим сокрытия? (Ответ:
**,*,~~и другие символы Markdown-разметки) - [ ] Зачем сначала защищать блоки кода? (Ответ: символы внутри блоков кода являются литералами и не должны быть удалены)
- [ ] Почему для удаления символов используется цикл while? (Ответ: для обработки вложенного синтаксиса, например
***жирный курсив***) - [ ] Чем
Bun.stringWidthлучшеtext.length? (Ответ: корректно вычисляет отображаемую ширину китайских символов, эмодзи и нулевой ширины)
Предостережения
Распространённые заблуждения
Заблуждение: ** внутри блоков кода тоже будет удалено
Факт: Нет. Плагин сначала защищает содержимое блоков кода с помощью плейсхолдеров, затем удаляет символы из остальной части и только потом восстанавливает блоки кода.
Таким образом, `**bold**` имеет ширину 8 (как **bold**), а не 4 (как bold).
Резюме урока
| Шаг | Назначение | Ключевой код |
|---|---|---|
| Защита блоков кода | Предотвращение случайного удаления символов внутри блоков кода | text.replace(/\(.+?)`/g, ...)` |
| Удаление Markdown | Вычисление фактического отображаемого содержимого в режиме сокрытия | Множественные замены регулярных выражений |
| Расчёт ширины | Обработка специальных символов: китайские, эмодзи и т.д. | Bun.stringWidth() |
Анонс следующего урока
Следующий урок — Спецификация таблиц.
Вы узнаете:
- Какие таблицы можно форматировать
- 4 правила валидации таблиц
- Как избежать ошибки "недопустимая таблица"
Приложение: Справка по исходному коду
Нажмите, чтобы развернуть справку по исходному коду
Обновлено: 2026-01-26
| Функция | Путь к файлу | Номера строк |
|---|---|---|
| Вход расчёта отображаемой ширины | index.ts | 151-159 |
| Защита блоков кода | index.ts | 168-173 |
| Удаление Markdown-разметки | index.ts | 175-188 |
| Восстановление блоков кода | index.ts | 190-193 |
| Вызов Bun.stringWidth | index.ts | 195 |
Ключевые функции:
calculateDisplayWidth(): Вход с кэшированием расчёта шириныgetStringWidth(): Основной алгоритм — удаление Markdown-разметки и расчёт отображаемой ширины
Ключевые константы:
\x00CODE{n}\x00: Формат плейсхолдера для блоков кода