mdview2: горизонтальный скролл по типу блока + фиксы fence/HR/SDCC
Горизонтальный скролл (Фаза 5, финал): - Скроллим по ТИПУ, а не по длине: новый флаг IF_HSCROLL ставится только на код и таблицы; HR и границы fence (IF_NOWRAP без IF_HSCROLL) не двигаются. Блок едет целиком, включая строки короче 80. - Обход бага кодогенерации SDCC z80: `if (n!=g) g=n;` пишет (n-g) вместо n (SUB сравнения затирает A, store переиспользует испорченный A). Лечится записью viewport_x ДО сравнения. Минимальный репродьюсер и оба описания для трекера — в docs/bugs/sdcc-z80-cmp-store-a/ (воспроизводится на чистом sdcc 4.5, в т.ч. с --no-peep → это кодогенератор, не peephole). Рендеринг: - Отступленный fence (```c внутри списка) теперь распознаётся: is_fence_raw пропускает ведущие пробелы/табы; то же в рендере прячет строку-границу. - Строки-разделители (HR, ровно 80) больше не участвуют в скролле. Чистка: удалён мёртвый код (is_fence_delim, get_init_style[_raw], is_cont, seg_flags). Makefile (mdview/mdview2): iconv UTF-8→CP866 завершается ненулевым кодом при отбрасывании символов — игнорируем (|| true). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+65
-493
@@ -143,6 +143,9 @@ typedef struct idx_rec_s {
|
||||
#define IF_NOWRAP 0x02u /* строка не переносится (кодовый блок / HR / таблица) */
|
||||
#define IF_BLANK 0x04u /* визуально пустая строка */
|
||||
#define IF_CODE 0x08u /* тело fenced code-блока (verbatim-режим) */
|
||||
#define IF_HSCROLL 0x10u /* блок горизонтально скроллируется ЦЕЛИКОМ (код/таблица),
|
||||
* включая строки короче 80 — двигаются как единый блок.
|
||||
* НЕ ставится на HR и границы fence (им нечего прятать). */
|
||||
|
||||
/* ==================================================================
|
||||
* Фаза 1 (mdview2): директория и пул рендер-кэша
|
||||
@@ -502,15 +505,6 @@ static void idx_put(uint16_t idx, const idx_rec_t *r)
|
||||
bank_write(index_phys[page], off, r, INDEX_REC_SIZE);
|
||||
}
|
||||
|
||||
/* Чтение полей индекса для рендера и навигации (построчный доступ). */
|
||||
static uint8_t seg_flags(uint16_t idx)
|
||||
{
|
||||
idx_rec_t r;
|
||||
if (idx >= n_lines) return 0;
|
||||
idx_get(idx, &r);
|
||||
return r.flags;
|
||||
}
|
||||
|
||||
static uint32_t seg_off(uint16_t idx)
|
||||
{
|
||||
idx_rec_t r;
|
||||
@@ -519,16 +513,12 @@ static uint32_t seg_off(uint16_t idx)
|
||||
return r.off;
|
||||
}
|
||||
|
||||
static uint8_t is_cont(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_CONT) != 0); }
|
||||
static uint8_t is_nowrap(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_NOWRAP) != 0); }
|
||||
static uint8_t is_blank(uint16_t idx) { return (uint8_t)((seg_flags(idx) & IF_BLANK) != 0); }
|
||||
static uint8_t is_code_body(uint16_t idx){ return (uint8_t)((seg_flags(idx) & IF_CODE) != 0); }
|
||||
|
||||
/* Установка флагов всегда идёт в последний emit_seg (n_lines-1).
|
||||
* Используется near-копия cur_rec, чтобы не делать обратное чтение из банка. */
|
||||
static void set_nowrap_cur(void) { cur_rec.flags |= IF_NOWRAP; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_blank_cur(void) { cur_rec.flags |= IF_BLANK; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_code_cur(void) { cur_rec.flags |= IF_CODE; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
static void set_hscroll_cur(void){ cur_rec.flags |= IF_HSCROLL; idx_put((uint16_t)(n_lines - 1), &cur_rec); }
|
||||
|
||||
/* Загружает 16 текстовых атрибутов в палитры ink/paper/blink. */
|
||||
static void set_pallete(void) {
|
||||
@@ -653,20 +643,6 @@ static void unload_file(void)
|
||||
* Индексация строк
|
||||
* ================================================================== */
|
||||
|
||||
/* Возвращает 1, если сегмент idx — это старт логической строки,
|
||||
* и эта строка начинается с ``` (граница блока кода).
|
||||
* Сегменты-продолжения никогда не считаются такими границами. */
|
||||
static uint8_t is_fence_delim(uint16_t idx)
|
||||
{
|
||||
if (idx >= n_lines) return 0;
|
||||
if (is_cont(idx)) return 0;
|
||||
uint32_t off = seg_off(idx);
|
||||
if (off + 2 >= file_size) return 0;
|
||||
return (uint8_t)(fb(off) == '`' &&
|
||||
fb(off + 1) == '`' &&
|
||||
fb(off + 2) == '`');
|
||||
}
|
||||
|
||||
#define INIT_STYLE_PLAIN 0x0
|
||||
#define INIT_STYLE_BOLD 0x1
|
||||
#define INIT_STYLE_ITALIC 0x2
|
||||
@@ -680,25 +656,6 @@ static const uint8_t styles_map[] = {
|
||||
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE, ATTR_TEXT_STRIKE
|
||||
};
|
||||
|
||||
/* Возвращает стартовый атрибут сегмента из индексной записи. */
|
||||
static uint8_t get_init_style(uint16_t idx)
|
||||
{
|
||||
idx_rec_t r;
|
||||
if (idx >= n_lines) return styles_map[INIT_STYLE_PLAIN];
|
||||
idx_get(idx, &r);
|
||||
return styles_map[r.style & 7u];
|
||||
}
|
||||
|
||||
/* Возвращает «сырой» INIT_STYLE_* из индексной записи. */
|
||||
static uint8_t get_init_style_raw(uint16_t idx)
|
||||
{
|
||||
idx_rec_t r;
|
||||
if (idx >= n_lines) return INIT_STYLE_PLAIN;
|
||||
idx_get(idx, &r);
|
||||
return (uint8_t)(r.style & 7u);
|
||||
}
|
||||
|
||||
|
||||
/* Тип строки, который возвращает classify_line(). */
|
||||
#define LK_PLAIN 0
|
||||
#define LK_H1 1
|
||||
@@ -859,9 +816,13 @@ static uint8_t is_line_blank(uint32_t p)
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Быстрая проверка начала блока кода по маркеру ``` . */
|
||||
/* Быстрая проверка границы fenced-блока по маркеру ``` .
|
||||
* Ведущие пробелы/табы пропускаются — fence может быть с отступом
|
||||
* (например, ``` ```c ``` внутри элемента списка с отступом 2+ пробела).
|
||||
* Язык после бэктиков (```c, ```sh) игнорируется: достаточно трёх ` подряд. */
|
||||
static uint8_t is_fence_raw(uint32_t p)
|
||||
{
|
||||
while (p < file_size && (fb(p) == ' ' || fb(p) == '\t')) p++;
|
||||
if (p + 2 >= file_size) return 0;
|
||||
return (fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`');
|
||||
}
|
||||
@@ -1267,6 +1228,7 @@ static void index_lines(void)
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
set_code_cur();
|
||||
set_hscroll_cur(); /* код скроллится целым блоком */
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1285,6 +1247,7 @@ static void index_lines(void)
|
||||
if (is_table_raw(p)) {
|
||||
emit_seg(p, INIT_STYLE_PLAIN, CK_OTHER, 0);
|
||||
set_nowrap_cur();
|
||||
set_hscroll_cur(); /* таблица скроллится целым блоком */
|
||||
while (p < file_size && fb(p) != '\n') p++;
|
||||
if (p < file_size) p++;
|
||||
continue;
|
||||
@@ -1474,411 +1437,6 @@ static uint8_t handle_inline_marker(char ch, uint32_t *io_p, uint32_t seg_end,
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Сбрасывает накопленный "прогон" из *col символов одного атрибута
|
||||
* одним bios_writeattr() вместо посимвольных wrchar(). *need_place
|
||||
* лениво ставит BIOS-курсор перед первым выводом в текущей строке —
|
||||
* далее place продвигается сам (verified), повторный bios_set_place
|
||||
* не нужен пока *col растёт монотонно. */
|
||||
static void flush_run(uint8_t row, uint8_t *col, const char *buf,
|
||||
uint8_t len, uint8_t attr, uint8_t *need_place)
|
||||
{
|
||||
if (len == 0) return;
|
||||
/* *col уже указывает на конец прогона (вызывающий цикл инкрементит
|
||||
* col на каждый символ ещё во время накопления в буфер — это нужно
|
||||
* для верных проверок col < SCREEN_W до сброса). Поэтому начало
|
||||
* прогона на экране = *col - len, а не *col; и *col второй раз
|
||||
* наращивать НЕ нужно — он уже корректен. */
|
||||
if (*need_place) {
|
||||
bios_set_place(row, (uint8_t)(*col - len));
|
||||
*need_place = 0;
|
||||
}
|
||||
bios_writeattr(buf, len, attr);
|
||||
}
|
||||
|
||||
static void render_line(uint16_t line_idx, uint8_t row)
|
||||
{
|
||||
/* Один idx_get() на line_idx вместо ~13 отдельных is_blank/is_cont/
|
||||
* is_nowrap/is_code_body/seg_off/get_init_style* вызовов. Каждый из
|
||||
* них шёл через seg_flags()/seg_off() -> idx_get() -> bank_read(),
|
||||
* а bank_read() ДВАЖДЫ переключает банк окна 3 (на страницу индекса
|
||||
* и обратно) плюс memcpy — это оказалось главным узким местом
|
||||
* рендера (≈700+ переключений банка на кадр), а не сам вывод на
|
||||
* экран. Все нужные поля (off/flags/style) лежат в одной 8-байтовой
|
||||
* записи idx_rec_t — читаем её один раз и используем дальше. */
|
||||
uint8_t have_rec = (line_idx < n_lines);
|
||||
idx_rec_t rec;
|
||||
if (have_rec) idx_get(line_idx, &rec);
|
||||
|
||||
uint8_t col = 0;
|
||||
uint8_t base_attr = ATTR_TEXT;
|
||||
uint8_t is_code = (uint8_t)(have_rec && (rec.flags & IF_CODE));
|
||||
uint8_t cur_attr = is_code ? ATTR_TEXT_CODE
|
||||
: styles_map[(have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u];
|
||||
uint8_t emph = is_code ? EM_CODE
|
||||
: (uint8_t)((have_rec ? rec.style : INIT_STYLE_PLAIN) & 7u);
|
||||
uint8_t parse_inline = 1; /* 1: включён парсер выделений, 0: выводим код как есть */
|
||||
|
||||
if (have_rec && (rec.flags & IF_BLANK)) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
|
||||
return;
|
||||
}
|
||||
|
||||
if (have_rec) {
|
||||
uint32_t p = rec.off;
|
||||
uint32_t content_off = p;
|
||||
uint8_t cont = (uint8_t)((rec.flags & IF_CONT) != 0);
|
||||
/* Правая граница сегмента: начало следующего сегмента или конец файла. */
|
||||
uint32_t seg_end;
|
||||
if ((uint16_t)(line_idx + 1) < n_lines) {
|
||||
idx_rec_t rec_next;
|
||||
idx_get((uint16_t)(line_idx + 1), &rec_next);
|
||||
seg_end = rec_next.off;
|
||||
} else {
|
||||
seg_end = file_size;
|
||||
}
|
||||
if (seg_end > file_size) seg_end = file_size;
|
||||
uint8_t kind;
|
||||
uint8_t quote_stream = 0; /* 1: текущий сегмент относится к потоку цитаты */
|
||||
|
||||
/* Для сегмента-продолжения повторяем префикс/отступ
|
||||
* от первого не-CONT сегмента логической строки. */
|
||||
if (cont) {
|
||||
uint16_t first = line_idx;
|
||||
idx_rec_t rec_first = rec; /* rec.flags здесь уже содержит IF_CONT */
|
||||
/* Для любого сегмента-продолжения откатываемся к первому
|
||||
* НЕ-cont сегменту этой логической строки — один idx_get()
|
||||
* на шаг вместо двух (is_cont + повторный seg_off). */
|
||||
while (first > 0 && (rec_first.flags & IF_CONT)) {
|
||||
first--;
|
||||
idx_get(first, &rec_first);
|
||||
}
|
||||
uint32_t p_start = rec_first.off;
|
||||
uint32_t content_off = p_start;
|
||||
uint8_t first_kind = classify_line(p_start, &content_off);
|
||||
uint8_t indent = marker_visible_col(first_kind, p_start, content_off);
|
||||
if (first_kind == LK_QUOTE) {
|
||||
/* Для продолжения цитаты нужно помнить тип потока,
|
||||
* чтобы далее на newline корректно пропускать сырой `>`. */
|
||||
quote_stream = 1;
|
||||
uint8_t q = 0;
|
||||
while (q + 2 < content_off && q < SCREEN_W && fb(p_start + q) == ' ') q++;
|
||||
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
|
||||
if (q > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, q);
|
||||
col = (uint8_t)(col + q);
|
||||
}
|
||||
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
|
||||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||||
} else if (first_kind == LK_ULIST || first_kind == LK_OLIST) {
|
||||
uint8_t q = indent;
|
||||
if (q > (uint8_t)(SCREEN_W - col)) q = (uint8_t)(SCREEN_W - col);
|
||||
if (q > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, q);
|
||||
col = (uint8_t)(col + q);
|
||||
}
|
||||
}
|
||||
/* p/content_off уже равны rec.off — внутри cont-блока они не
|
||||
* менялись (там свои локальные тени с тем же именем),
|
||||
* пересчитывать через seg_off() заново не нужно. */
|
||||
}
|
||||
|
||||
/* Кодовый блок:
|
||||
* - строка-разделитель показывается как пустая;
|
||||
* - тело рисуется «как есть» с ATTR_TEXT_CODE без inline-парсинга.
|
||||
* is_fence_delim(line_idx) разворачено на месте: cont и p уже
|
||||
* известны, повторный idx_get()/is_cont() не нужен. */
|
||||
if (!cont && p + 2 < file_size &&
|
||||
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, (uint8_t)(SCREEN_W - col));
|
||||
return;
|
||||
}
|
||||
if (is_code) {
|
||||
base_attr = ATTR_TEXT_CODE;
|
||||
cur_attr = ATTR_TEXT_CODE;
|
||||
parse_inline = 0;
|
||||
kind = LK_PLAIN;
|
||||
quote_stream = 0;
|
||||
} else if (cont) {
|
||||
/* Строка-продолжение: префикс уже нарисован выше.
|
||||
* Содержимое рисуем как plain, без повторной классификации,
|
||||
* иначе начало слова могло бы ложно стать маркером. */
|
||||
kind = LK_PLAIN;
|
||||
} else {
|
||||
kind = classify_line(p, &content_off);
|
||||
if (kind == LK_QUOTE) quote_stream = 1;
|
||||
}
|
||||
|
||||
if (kind == LK_HR) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(0xC4, ATTR_HR, (uint8_t)(SCREEN_W - col));
|
||||
return;
|
||||
}
|
||||
if (kind >= LK_H1 && kind <= LK_H4) {
|
||||
switch (kind) {
|
||||
case LK_H1: base_attr = ATTR_TEXT_TITLE1; break;
|
||||
case LK_H2: base_attr = ATTR_TEXT_TITLE2; break;
|
||||
case LK_H3: base_attr = ATTR_TEXT_TITLE3; break;
|
||||
default: base_attr = ATTR_TEXT_TITLE4; break;
|
||||
}
|
||||
cur_attr = base_attr;
|
||||
p = content_off;
|
||||
} else if (kind == LK_ULIST) {
|
||||
/* Базовая поддержка вложенности: ведущие пробелы сохраняем,
|
||||
* затем рисуем маркер списка и пробел перед содержимым. */
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q + 2 < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
uint8_t nsp = (uint8_t)(q - p);
|
||||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||||
if (nsp > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||||
col = (uint8_t)(col + nsp);
|
||||
}
|
||||
if (col < SCREEN_W) wrchar(col++, row, 0x07, ATTR_LIST_MARKER); /* символ маркера списка */
|
||||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
} else if (kind == LK_OLIST) {
|
||||
/* Отступ -> пробелы; цифры и '.' / ')' рисуем как маркер;
|
||||
* завершающий пробел — обычным атрибутом текста. */
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
uint8_t nsp = (uint8_t)(q - p);
|
||||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||||
if (nsp > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||||
col = (uint8_t)(col + nsp);
|
||||
}
|
||||
/* Маркер (цифры/'.'/')') читается из fb() — это окно 3,
|
||||
* BIOS принимает указатели только из #4000..#BFFF, поэтому
|
||||
* сначала копируем во временный буфер на стеке (окно 2),
|
||||
* чанками по 8 байт (маркер почти всегда короче, но длинный
|
||||
* корректно не обрезается, просто шлётся несколькими
|
||||
* bios_writeattr вместо одного). */
|
||||
{
|
||||
char mkbuf[8];
|
||||
uint8_t first_chunk = 1;
|
||||
while (q < content_off - 1 && col < SCREEN_W) {
|
||||
uint8_t n = 0;
|
||||
while (q < content_off - 1 &&
|
||||
(uint8_t)(col + n) < SCREEN_W &&
|
||||
n < sizeof(mkbuf)) {
|
||||
mkbuf[n] = fb(q);
|
||||
q++;
|
||||
n++;
|
||||
}
|
||||
if (n == 0) break;
|
||||
if (first_chunk) { bios_set_place(row, col); first_chunk = 0; }
|
||||
bios_writeattr(mkbuf, n, ATTR_LIST_MARKER);
|
||||
col = (uint8_t)(col + n);
|
||||
}
|
||||
}
|
||||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
} else if (kind == LK_QUOTE) {
|
||||
/* Отступ -> пробелы; затем quote-префикс '│' (0xB3),
|
||||
* пробел и далее содержимое. */
|
||||
uint32_t q = p;
|
||||
uint32_t qmax = p + SCREEN_W;
|
||||
while (q + 1 < content_off && q < qmax && fb(q) == ' ') q++;
|
||||
uint8_t nsp = (uint8_t)(q - p);
|
||||
if (nsp > (uint8_t)(SCREEN_W - col)) nsp = (uint8_t)(SCREEN_W - col);
|
||||
if (nsp > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, nsp);
|
||||
col = (uint8_t)(col + nsp);
|
||||
}
|
||||
if (col < SCREEN_W) wrchar(col++, row, 0xB3, ATTR_QUOTE_MARKER);
|
||||
if (col < SCREEN_W) wrchar(col++, row, ' ', ATTR_TEXT);
|
||||
p = content_off;
|
||||
}
|
||||
|
||||
/* Эффективный горизонтальный сдвиг: применяется только к nowrap-строкам. */
|
||||
uint8_t effective_vx = (rec.flags & IF_NOWRAP) ? viewport_x : 0;
|
||||
/* cc — видимая колонка внутри области контента
|
||||
* (после фиксированного префикса строки). */
|
||||
uint8_t cc = 0;
|
||||
char prev_ch = ' '; /* используется в проверке границ emphasis */
|
||||
|
||||
/* Буферизация "ровного" прогона одного атрибута: символ и атрибут
|
||||
* внутри этого while переменные (emphasis/tab/soft-wrap), поэтому
|
||||
* весь цикл одним BIOS-вызовом не свернуть — но между сменами
|
||||
* атрибута почти всегда идёт протяжённый одноцветный текст, и
|
||||
* именно его стоит копить в runbuf и сбрасывать одним
|
||||
* bios_writeattr() вместо посимвольных wrchar().
|
||||
* runbuf вмещает целую строку экрана — переполнение невозможно,
|
||||
* т.к. весь цикл и так ограничен col < SCREEN_W. */
|
||||
char runbuf[SCREEN_W];
|
||||
uint8_t run_len = 0;
|
||||
uint8_t run_attr = cur_attr;
|
||||
uint8_t need_place = 1;
|
||||
|
||||
while (p < seg_end && col < SCREEN_W) {
|
||||
uint8_t soft_join = 0; /* 1: вставлен виртуальный пробел мягкой склейки, p уже сдвинут */
|
||||
char ch = fb(p);
|
||||
/* '\'
|
||||
* перед концом сегмента — это маркер wide-break:
|
||||
* поглощаем его без вывода (кроме verbatim code-body). */
|
||||
if (parse_inline && ch == '\\' && (p + 1) < seg_end) {
|
||||
char nb = fb(p + 1);
|
||||
if (nb == '\n' || nb == '\r') { p++; continue; }
|
||||
}
|
||||
/* Мягкий перенос (перевод строки внутри одного сегмента).
|
||||
* Рендер должен повторить логику индексатора:
|
||||
* - quote: пропустить сырой `> ` следующей строки;
|
||||
* - list/plain: схлопнуть перенос + ведущий отступ в 1 пробел. */
|
||||
if (parse_inline && (ch == '\n' || ch == '\r')) {
|
||||
uint32_t next = p + 1;
|
||||
if (quote_stream && next < seg_end) {
|
||||
uint32_t next_content = next;
|
||||
if (classify_line(next, &next_content) == LK_QUOTE) {
|
||||
/* Мягкая склейка quote-строк: `> ` не рендерим. */
|
||||
p = next_content;
|
||||
ch = ' ';
|
||||
soft_join = 1;
|
||||
} else {
|
||||
/* Защита: если внутри quote-сегмента встретился
|
||||
* не-цитатный перенос, работаем как обычная мягкая склейка. */
|
||||
while (next < seg_end) {
|
||||
char ws = fb(next);
|
||||
if (ws == ' ' || ws == '\t') next++;
|
||||
else break;
|
||||
}
|
||||
if (next > (p + 1)) {
|
||||
p = next;
|
||||
soft_join = 1;
|
||||
}
|
||||
ch = ' ';
|
||||
}
|
||||
} else {
|
||||
while (next < seg_end) {
|
||||
char ws = fb(next);
|
||||
if (ws == ' ' || ws == '\t') next++;
|
||||
else break;
|
||||
}
|
||||
if (next > (p + 1)) {
|
||||
/* Схлопываем перенос + отступ (ленивое продолжение). */
|
||||
p = next;
|
||||
soft_join = 1;
|
||||
}
|
||||
ch = ' ';
|
||||
}
|
||||
} else if (ch == '\n' || ch == '\r') {
|
||||
ch = ' ';
|
||||
}
|
||||
|
||||
/* inline-маркеры (**, ~~, *, _, `):
|
||||
* helper синхронизирует состояние emphasis и возвращает режим
|
||||
* обработки текущего токена (поглотить/вывести литерал/обычный символ). */
|
||||
if (parse_inline) {
|
||||
char lit_ch = 0;
|
||||
uint8_t mk = handle_inline_marker(ch, &p, seg_end, prev_ch, &emph, &lit_ch);
|
||||
if (mk == 1) {
|
||||
/* Атрибут меняется -> сбросить накопленный прогон
|
||||
* со СТАРЫМ атрибутом, прежде чем переключиться. */
|
||||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||||
run_len = 0;
|
||||
cur_attr = emph_to_attr(emph, base_attr);
|
||||
run_attr = cur_attr;
|
||||
continue;
|
||||
}
|
||||
if (mk == 2) {
|
||||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||||
runbuf[run_len++] = lit_ch;
|
||||
col++;
|
||||
}
|
||||
cc++;
|
||||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||||
runbuf[run_len++] = lit_ch;
|
||||
col++;
|
||||
}
|
||||
cc++;
|
||||
prev_ch = lit_ch;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (!soft_join) p++;
|
||||
if (ch == '\t') {
|
||||
/* Таб расширяется в координатах контента (cc).
|
||||
* Сначала сбросить накопленный текстовый прогон — таб
|
||||
* выводится отдельным bios_fillcharattr(), не через runbuf. */
|
||||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||||
run_len = 0;
|
||||
|
||||
uint8_t cc_start = cc;
|
||||
uint8_t tgt = (uint8_t)((cc & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
|
||||
uint8_t n_total = (uint8_t)(tgt - cc_start);
|
||||
uint8_t skip = (effective_vx > cc_start) ? (uint8_t)(effective_vx - cc_start) : 0;
|
||||
if (skip > n_total) skip = n_total;
|
||||
uint8_t want = (uint8_t)(n_total - skip);
|
||||
if (want > (uint8_t)(SCREEN_W - col)) want = (uint8_t)(SCREEN_W - col);
|
||||
if (want > 0) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', cur_attr, want);
|
||||
col = (uint8_t)(col + want);
|
||||
need_place = 0;
|
||||
}
|
||||
cc = (uint8_t)(cc_start + skip + want);
|
||||
prev_ch = ' ';
|
||||
} else {
|
||||
if (cc >= effective_vx && col < SCREEN_W && run_len < sizeof(runbuf)) {
|
||||
runbuf[run_len++] = ch;
|
||||
col++;
|
||||
}
|
||||
cc++;
|
||||
prev_ch = ch;
|
||||
}
|
||||
}
|
||||
flush_run(row, &col, runbuf, run_len, run_attr, &need_place);
|
||||
|
||||
/* Индикатор обрезки справа для nowrap-строки:
|
||||
* если после текущей позиции есть ещё видимый контент,
|
||||
* поверх последней колонки рисуем '>'. */
|
||||
if ((rec.flags & IF_NOWRAP) && col >= SCREEN_W) {
|
||||
uint32_t pp = p;
|
||||
uint32_t left_bound = rec.off;
|
||||
while (pp < seg_end) {
|
||||
char c = fb(pp);
|
||||
if (c == '\n' || c == '\r') break;
|
||||
if (c == '`') { pp++; continue; }
|
||||
if (c == '*' && (pp + 1) < seg_end && fb(pp + 1) == '*') {
|
||||
char pc = (pp > left_bound) ? fb(pp - 1) : ' ';
|
||||
char nc = (pp + 2 < seg_end) ? fb(pp + 2) : '\n';
|
||||
if (is_emph_flanked(pc, nc)) { pp += 2; continue; }
|
||||
/* Литерал `**` тоже считается видимым контентом справа. */
|
||||
} else if (c == '*' || c == '_') {
|
||||
char pc = (pp > left_bound) ? fb(pp - 1) : ' ';
|
||||
char nc = (pp + 1 < seg_end) ? fb(pp + 1) : '\n';
|
||||
if (is_emph_flanked(pc, nc)) { pp++; continue; }
|
||||
}
|
||||
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Индикатор скрытого контента слева при горизонтальном сдвиге. */
|
||||
if ((rec.flags & IF_NOWRAP) && viewport_x > 0) {
|
||||
wrchar(0, row, '<', ATTR_TRUNC);
|
||||
}
|
||||
}
|
||||
/* Дозаполняем остаток строки пробелами, чтобы стереть хвост
|
||||
* от предыдущего кадра рендера. */
|
||||
{
|
||||
uint8_t pad_attr = (parse_inline ? ATTR_TEXT : ATTR_TEXT_CODE);
|
||||
if (col < SCREEN_W) {
|
||||
bios_set_place(row, col);
|
||||
bios_fillcharattr(' ', pad_attr, (uint8_t)(SCREEN_W - col));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Пишет одну (char,attr) ячейку в локальный буфер кэш-строки. Молча
|
||||
* не пишет (и не даёт col перевалить за MAX_CACHE_LINE_LEN), если буфер
|
||||
* уже заполнен — длинные nowrap-строки просто обрезаются по канону,
|
||||
@@ -1983,9 +1541,10 @@ static void render_line_to_cache(uint16_t line_idx)
|
||||
}
|
||||
}
|
||||
|
||||
/* Разделитель fenced-блока — пустая строка в кэше (как IF_BLANK). */
|
||||
if (!cont && p + 2 < file_size &&
|
||||
fb(p) == '`' && fb(p + 1) == '`' && fb(p + 2) == '`') {
|
||||
/* Разделитель fenced-блока — пустая строка в кэше (как IF_BLANK).
|
||||
* is_fence_raw пропускает ведущие пробелы, поэтому отступленный ``` тоже
|
||||
* распознаётся (и язык ```c прячется вместе со всей строкой-границей). */
|
||||
if (!cont && is_fence_raw(p)) {
|
||||
out_rec.page = 0;
|
||||
out_rec.off = 0;
|
||||
out_rec.len = 0;
|
||||
@@ -2145,25 +1704,13 @@ static void render_line_to_cache(uint16_t line_idx)
|
||||
cache_dir_put(line_idx, &out_rec);
|
||||
}
|
||||
|
||||
/* Рендерит все 30 строк окна, начиная от top_line. ЖИВОЙ путь (парсинг
|
||||
* через fb()) — после Фазы 4 используется только для горизонтального
|
||||
* скролла (scroll_h(), Фаза 5 ещё не мигрировала его на кэш), для
|
||||
* вертикального скролла и обычной перерисовки см.
|
||||
* draw_viewport_from_cache() ниже. */
|
||||
static void render_viewport(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||||
render_line((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
|
||||
}
|
||||
}
|
||||
|
||||
/* Фаза 4 — cache-only draw path: рисует одну строку ИЗ КЭША, без
|
||||
* единого обращения к fb()/исходному файлу. rec.len==0 — пустая
|
||||
* строка, остаётся только фон. rec.len>SCREEN_W — nowrap-строка шире
|
||||
* экрана: показываем первые SCREEN_W ячеек (viewport_x=0, как и
|
||||
* остальные строки этим путём — горизонтальный скролл пока только
|
||||
* через scroll_h()/render_viewport(), см. Фазу 5) и индикатор обрезки
|
||||
* '>' в последней колонке. */
|
||||
/* Фаза 4-5 — cache-only draw path: рисует одну строку ИЗ КЭША, без
|
||||
* единого обращения к fb()/исходному файлу. Горизонтальный скролл для
|
||||
* nowrap-строк (Фаза 5) — это просто смещение начала среза на vx*2
|
||||
* байт ВНУТРИ ТОГО ЖЕ кэш-буфера (cache_reserve гарантирует, что вся
|
||||
* строка лежит в одной странице, так что off+vx*2 не пересекает
|
||||
* границу страницы); не nowrap-строки игнорируют viewport_x, как и
|
||||
* раньше в живом render_line() (там это называлось effective_vx). */
|
||||
static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
|
||||
{
|
||||
cache_rec_t rec;
|
||||
@@ -2172,12 +1719,24 @@ static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
|
||||
bios_set_place(row, 0);
|
||||
bios_fillcharattr(' ', ATTR_TEXT, SCREEN_W);
|
||||
|
||||
if (rec.len == 0) return;
|
||||
/* Горизонтальный сдвиг применяется ко ВСЕМ строкам hscroll-блока (код,
|
||||
* таблицы) — по ТИПУ, а не по длине: блок двигается целиком, включая
|
||||
* строки короче 80 (короткая строка, ушедшая за левый край, просто
|
||||
* показывает маркер '<' на пустом месте). HR и границы fence не имеют
|
||||
* IF_HSCROLL и не сдвигаются. */
|
||||
uint8_t vx = (rec.flags & IF_HSCROLL) ? viewport_x : 0;
|
||||
|
||||
uint8_t w = (rec.len > SCREEN_W) ? SCREEN_W : rec.len;
|
||||
win_rest(row, 0, 1, w, rec.page, rec.off);
|
||||
if (rec.len > SCREEN_W) {
|
||||
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
|
||||
if (rec.len > vx) {
|
||||
uint16_t remaining = (uint16_t)(rec.len - vx);
|
||||
uint8_t w = (remaining > SCREEN_W) ? SCREEN_W : (uint8_t)remaining;
|
||||
uint16_t off = (uint16_t)(rec.off + (uint16_t)vx * 2u);
|
||||
win_rest(row, 0, 1, w, rec.page, off);
|
||||
if (remaining > SCREEN_W) {
|
||||
wrchar(SCREEN_W - 1, row, '>', ATTR_TRUNC);
|
||||
}
|
||||
}
|
||||
if (vx > 0) {
|
||||
wrchar(0, row, '<', ATTR_TRUNC);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2185,7 +1744,7 @@ static void draw_line_from_cache(uint16_t line_idx, uint8_t row)
|
||||
* перерисовки видимой области после загрузки файла (Фаза 3-4). */
|
||||
static void draw_viewport_from_cache(void)
|
||||
{
|
||||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||||
for (uint8_t i = 0; i < VIEW_H - 1; i++) {
|
||||
draw_line_from_cache((uint16_t)(top_line + i), (uint8_t)(VIEW_TOP_ROW + i));
|
||||
}
|
||||
}
|
||||
@@ -2304,21 +1863,22 @@ static void scroll_down(uint16_t n)
|
||||
}
|
||||
|
||||
/* Горизонтальный сдвиг (только если в окне есть nowrap-строки).
|
||||
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране. */
|
||||
* Максимум сдвига ограничен самой широкой nowrap-строкой на экране.
|
||||
* Фаза 5: ширина берётся прямо из директории кэша (rec.len — уже
|
||||
* посчитанная реальная длина в ячейках после рендера), без единого
|
||||
* обращения к fb()/seg_off()/исходному файлу. */
|
||||
static void scroll_h(int8_t delta)
|
||||
{
|
||||
uint16_t maxw = 0;
|
||||
for (uint8_t i = 0; i < VIEW_H; i++) {
|
||||
uint16_t li = (uint16_t)(top_line + i);
|
||||
if (li >= n_lines) break;
|
||||
if (!is_nowrap(li)) continue;
|
||||
uint32_t a = seg_off(li);
|
||||
uint32_t b = seg_off((uint16_t)(li + 1));
|
||||
if (b > file_size) b = file_size;
|
||||
uint16_t w = (b > a) ? (uint16_t)(b - a) : 0; /* приблизительная видимая ширина */
|
||||
if (w > maxw) maxw = w;
|
||||
cache_rec_t rec;
|
||||
cache_dir_get(li, &rec);
|
||||
if (!(rec.flags & IF_HSCROLL)) continue; /* только код/таблицы */
|
||||
if (rec.len > maxw) maxw = rec.len; /* самая широкая строка блока */
|
||||
}
|
||||
if (maxw == 0) return; /* в окне нет nowrap-контента */
|
||||
if (maxw == 0) return; /* нет строк шире экрана */
|
||||
|
||||
/* Максимальный сдвиг = ширина за пределами экрана, в границах uint8. */
|
||||
uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
|
||||
@@ -2328,10 +1888,22 @@ static void scroll_h(int8_t delta)
|
||||
int16_t nx = (int16_t)viewport_x + delta;
|
||||
if (nx < 0) nx = 0;
|
||||
if (nx > (int16_t)max_vx) nx = max_vx;
|
||||
if ((uint8_t)nx != viewport_x) {
|
||||
viewport_x = (uint8_t)nx;
|
||||
render_viewport();
|
||||
}
|
||||
|
||||
/* ПОРЯДОК ВАЖЕН: записываем viewport_x ДО сравнения, а сравниваем уже
|
||||
* сохранённую копию old_vx с new_vx. Иначе SDCC генерирует для
|
||||
* `if (new_vx != viewport_x) viewport_x = new_vx;` такой код:
|
||||
* ld a, new_vx ; sub a,(viewport_x) ; jr Z,.. ; ld (viewport_x),a
|
||||
* — т.е. для записи переиспользует регистр A, испорченный вычитанием в
|
||||
* сравнении, и кладёт в viewport_x не new_vx, а (new_vx - old_vx).
|
||||
* (asm-дамп 2026-06-24 подтвердил; даёт 10-8=2, 10-2=8 — ровно
|
||||
* наблюдавшийся "прыжок" скролла.) Записав viewport_x первой, мы убираем
|
||||
* портящий sub с пути записи; сравнение ниже только решает, перерисовывать
|
||||
* ли экран. */
|
||||
uint8_t new_vx = (uint8_t)nx;
|
||||
uint8_t old_vx = viewport_x;
|
||||
viewport_x = new_vx;
|
||||
if (new_vx != old_vx)
|
||||
draw_viewport_from_cache();
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user