Skip to content

Принцип работы режима сокрытия: почему расчёт ширины так важен

Чему вы научитесь после изучения

  • Поймёте, как работает режим сокрытия в OpenCode
  • Узнаете, почему обычные инструменты форматирования дают сбой при неверном выравнивании в режиме сокрытия
  • Освоите алгоритм расчёта ширины плагина (три шага)
  • Поймёте роль Bun.stringWidth

Ваша текущая проблема

Вы пишете код в OpenCode, и AI сгенерировал красивую таблицу:

markdown
| Поле | Тип | Описание |
|---|--- | ---|
| **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-разметки нужно "спрятать" содержимое блоков кода.

Реализация в коде

typescript
// Шаг 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-разметки

Почему

В режиме сокрытия **жирный** отображается как жирный, *курсив* — как курсив. При расчёте ширины эти символы нужно удалить.

Реализация в коде

typescript
// Шаг 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](url) → alt
    .replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1 ($2)") // [text](url) → text (url)
}

Почему используется цикл while?

Для обработки вложенного синтаксиса. Например, ***жирный курсив***:

Раунд 1: ***жирный курсив*** → **жирный курсив** (удалены внешние ***)
Раунд 2: **жирный курсив** → *жирный курсив* (удалены **)
Раунд 3: *жирный курсив* → жирный курсив (удалены *)
Раунд 4: жирный курсив = жирный курсив (без изменений, выход из цикла)
Обработка изображений и ссылок
  • Изображения ![alt](url): OpenCode показывает только alt-текст, поэтому заменяются на alt
  • Ссылки [text](url): отображаются как text (url), сохраняя информацию об URL

Шаг 3: Восстановление блоков кода + расчёт ширины

Почему

Содержимое блоков кода нужно вернуть на место, а затем рассчитать финальную отображаемую ширину с помощью Bun.stringWidth.

Реализация в коде

typescript
// Шаг 3: Восстановление содержимого блоков кода
visualText = visualText.replace(/\x00CODE(\d+)\x00/g, (match, index) => {
  return codeBlocks[parseInt(index)]
})

return Bun.stringWidth(visualText)

Почему используется Bun.stringWidth?

Bun.stringWidth корректно вычисляет:

Тип символовПримерКоличество символовОтображаемая ширина
ASCIIabc33
Китайские你好24 (каждый занимает 2 клетки)
Эмодзи😀12 (занимает 2 клетки)
Нулевой шириныa\u200Bb32 (нулевой ширины не занимает место)

Обычный 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.ts151-159
Защита блоков кодаindex.ts168-173
Удаление Markdown-разметкиindex.ts175-188
Восстановление блоков кодаindex.ts190-193
Вызов Bun.stringWidthindex.ts195

Ключевые функции:

  • calculateDisplayWidth(): Вход с кэшированием расчёта ширины
  • getStringWidth(): Основной алгоритм — удаление Markdown-разметки и расчёт отображаемой ширины

Ключевые константы:

  • \x00CODE{n}\x00: Формат плейсхолдера для блоков кода