mdview2: фоновая сборка второго набора кодировки в паузах между клавишами; версия v1.0 (b3)

- индексатор порезан на резюмируемые шаги index_begin/index_step/index_finish;
  межшаговое состояние в статиках модуля, в docset_t не входит
- bg_build_start/bg_step: второй набор (UTF-8 при 8-битном первичном и
  наоборот) строится в idle главного цикла; холдаун после клавиш, спиннер
  погашен (g_bg_building) — фон незаметен
- F8 до готовности докручивает начатое фоном (ветка resume в build_doc),
  а не строит заново; общий setup вынесен в doc_setup
- кодировка в статус-баре показывается сразу (детект/F8), не дожидаясь
  конца индексации
- побочный фикс: UTF-конвертация впереди проверки останова — >4КБ абзац
  больше не обрывает конвертацию остатка
- README.md (новый, v1.0 b3), CHANGELOG.md; дискета: README/DEMO/CHANGES
  в трёх кодировках (пути автодетекта)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-05 19:33:34 +03:00
parent ae23d2dea2
commit 46553f4e07
8 changed files with 484 additions and 89 deletions
+98 -52
View File
@@ -1,7 +1,7 @@
/*
* mdview2_index.c — парсер/индексатор markdown (сердце приложения).
*
* Единственный проход по исходному файлу (index_lines): классификация строк
* Единственный проход по исходному файлу (index_begin/index_step): классификация строк
* (classify_line, is_*_raw), сборка абзацев с переносом (scan_join_stream),
* inline-разметка (inline_marker/inline_scan) и эмиссия готовых (char,attr)-
* ячеек в рендер-кэш (gc_put → seg_commit_cells → cache_*). Ведёт индекс
@@ -669,31 +669,88 @@ static uint32_t scan_join_stream(uint32_t q, uint32_t para_start, uint8_t col,
/* Отрисовка таблиц (row_end/table_*) — в модуле mdview2_table.c. */
/* Строит индекс сегментов рендера по загруженному файлу. */
void index_lines(void)
/* ================== Резюмируемый проход индексации =======================
* Бывший монолит index_lines() порезан на begin/step/finish, чтобы фоновая
* сборка второго набора (bg_step ядра) могла выполнять индексацию порциями
* в паузах между клавишами. Всё межшаговое состояние — в статиках модуля
* (idx_p/idx_in_block ниже + g_cells/cur_rec/g_trunc_* выше); оно НЕ входит
* в docset_t и переживает свапы наборов, потому что обработчики клавиш
* видимого набора индексатор не трогают. */
static uint32_t idx_p; /* позиция чтения исходника между шагами */
static uint8_t idx_in_block; /* внутри fenced code-блока */
static uint8_t idx_active; /* 1 = проход начат и ещё не финализирован */
/* Начинает новый проход (сброс позиции/буферов/причин обрыва). */
void index_begin(void)
{
uint8_t in_block = 0;
idx_p = 0;
idx_in_block = 0;
n_lines = 0;
index_truncated = 0;
g_trunc_cause = 0;
g_trunc_line = 0;
cur_seg_off = 0xFFFFFFFFu; /* пока не эмитили ни одного сегмента */
if (file_size == 0) return;
g_ncells = 0;
g_ncells_at_space = 0;
g_skip_flush = 0;
idx_active = 1;
}
uint32_t p = 0;
while (p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause) {
/* Хвост прохода: финальный флаш ячеек + строка-обрыв (бывший финал index_lines). */
static void index_finish(void)
{
idx_active = 0;
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
* комментарий) — последняя эмитированная строка файла так и остаётся
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
* склейки/классификации ~ один абзац). file_size при этом = доступная
* сконвертированная часть и растёт по ходу. */
if (g_utf_building && (uint32_t)(p + CONV_MARGIN) > file_size)
utf_convert_more((uint32_t)(p + CONV_MARGIN));
/* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по
* `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном
* состоянии (для code-block — один emit_seg на строку, эта ветка не
* достигается). Признак — остановились, а файл не кончился; g_abort и
* исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */
if (!g_trunc_cause && !g_abort && idx_p < file_size) {
g_trunc_cause = TRUNC_LINES;
g_trunc_line = (uint16_t)(max_lines - 1);
}
if (g_loading)
progress_tick(); /* первый экран + статус + скролл по готовым данным */
else if ((n_lines & 15) == 0)
spinner_tick();
/* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца
* клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку
* (место есть: n_lines < max_lines). Приоритет ниже content/lines —
* те означают, что обрезали ещё раньше, их сообщение точнее. */
if (!g_trunc_cause && !g_abort && g_file_clamped) {
g_trunc_cause = TRUNC_FILE;
g_trunc_line = n_lines;
}
/* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort):
* документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение
* рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что
* и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */
if (g_trunc_cause && !g_abort) {
cache_rec_t r;
r.page = 0; r.off = 0; r.len = 0;
r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause;
r.pad[0] = 0; r.pad[1] = 0;
cache_dir_put(g_trunc_line, &r);
n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */
}
}
/* Одна итерация бывшего главного цикла index_lines(): классифицирует и
* эмитит очередной блок исходника (параграф/строку кода/таблицу целиком).
* do{}while(0) сохраняет исходные `continue` тела как выход из итерации
* с записью локальной p обратно в idx_p. */
static void index_iter(void)
{
if (g_loading)
progress_tick(); /* первый экран + статус + скролл по готовым данным */
else if (!g_bg_building && (n_lines & 15) == 0)
spinner_tick();
uint32_t p = idx_p;
do {
/* Пустая строка — разделитель параграфов.
* В индекс добавляем не более одной пустой экранной строки подряд. */
if (is_line_blank(p)) {
@@ -708,13 +765,13 @@ void index_lines(void)
/* ---- fenced code-блок (граница или тело) ---- */
if (is_fence_raw(p)) {
in_block = !in_block;
idx_in_block = !idx_in_block;
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP);
p = next_line(p);
continue;
}
if (in_block) {
if (idx_in_block) {
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
set_cur_flags(IF_NOWRAP | IF_CODE | IF_HSCROLL); /* код скроллится целым блоком */
{
@@ -926,43 +983,32 @@ void index_lines(void)
p = next_line(q);
}
}
}
/* emit_seg() флашит ячейки с отставанием на одну строку (см. её
* комментарий) — последняя эмитированная строка файла так и остаётся
* незафлашенной, её ячейки ещё в g_cells; коммитим здесь. */
if (n_lines > 0) seg_commit_cells((uint16_t)(n_lines - 1));
} while (0);
idx_p = p;
}
/* Лимит строк ловим ЗДЕСЬ, а не в emit_seg: главный цикл выходит по
* `n_lines < max_lines` ДО того, как emit_seg будет вызван в переполненном
* состоянии (для code-block — один emit_seg на строку, эта ветка не
* достигается). Признак — остановились, а файл не кончился; g_abort и
* исчерпание контента (TRUNC_CONTENT) уже обработаны/исключены. */
if (!g_trunc_cause && !g_abort && p < file_size) {
g_trunc_cause = TRUNC_LINES;
g_trunc_line = (uint16_t)(max_lines - 1);
}
/* Выполняет до budget итераций индексации. Возврат: 1 — проход завершён
* (индекс финализирован index_finish), 0 — есть ещё работа.
* Конвертация UTF-8 держится впереди позиции чтения ЗДЕСЬ, до проверки
* условия останова: пока исходник не сконвертирован до конца, idx_p не
* может «догнать» file_size и оборвать проход преждевременно. */
uint8_t index_step(uint8_t budget)
{
if (!idx_active) return 1;
while (budget-- > 0) {
/* UTF-8: держим конвертацию впереди позиции чтения (включая look-ahead
* склейки/классификации ~ один абзац). file_size при этом = доступная
* сконвертированная часть и растёт по ходу. */
if (g_utf_building && (uint32_t)(idx_p + CONV_MARGIN) > file_size)
utf_convert_more((uint32_t)(idx_p + CONV_MARGIN));
/* Файл был обрезан до 256 КБ при загрузке, а индексация дошла до конца
* клампнутых данных без исчерпания контента/строк → ДОПИСЫВАЕМ строку
* (место есть: n_lines < max_lines). Приоритет ниже content/lines —
* те означают, что обрезали ещё раньше, их сообщение точнее. */
if (!g_trunc_cause && !g_abort && g_file_clamped) {
g_trunc_cause = TRUNC_FILE;
g_trunc_line = n_lines;
}
/* Обрыв по контенту/лимиту строк (не путать с пользовательским g_abort):
* документ заканчиваем строкой-сообщением на g_trunc_line. Сообщение
* рисуется вживую (IF_TRUNC_MSG) — место в контент-кэше ему не нужно, что
* и снимает тупик «кэш полон, а сообщение тоже надо закэшировать». */
if (g_trunc_cause && !g_abort) {
cache_rec_t r;
r.page = 0; r.off = 0; r.len = 0;
r.flags = IF_TRUNC_MSG; r.reserved = g_trunc_cause;
r.pad[0] = 0; r.pad[1] = 0;
cache_dir_put(g_trunc_line, &r);
n_lines = (uint16_t)(g_trunc_line + 1); /* документ кончается на строке-обрыве */
if (!(idx_p < file_size && n_lines < max_lines && !g_abort && !g_trunc_cause)) {
index_finish();
return 1;
}
index_iter();
}
return 0;
}
#define EM_NONE 0
#define EM_BOLD 1