mdview: multiline list items with lazy continuation and blank-line grouping

- List items (UL/OL) can now span multiple source lines: non-marker
  lines are joined into the current item as lazy continuation.
- Continuation-line leading indentation is trimmed before joining so
  wrapped item text is separated by a single space.
- A single blank line between adjacent list markers is suppressed
  (same visual list), while 2+ blank lines still produce a separator.

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
2026-06-07 19:13:02 +03:00
parent 394ee3a2cd
commit 982af12710
+160 -8
View File
@@ -841,15 +841,167 @@ static void index_lines(void)
continue;
}
/* ---- List item: single line, not merged ---- */
/* ---- List item: may span multiple source lines ---- */
if (kind == LK_ULIST || kind == LK_OLIST) {
uint8_t col = marker_visible_col(kind, p, content_off);
emit_seg(p, INIT_STYLE_PLAIN, CK_LIST, 0);
uint32_t line_end = content_off;
while (line_end < file_size && fb(line_end) != '\n') line_end++;
(void)inline_scan(content_off, line_end, col, CK_LIST, INIT_STYLE_PLAIN);
while (p < file_size && fb(p) != '\n') p++;
if (p < file_size) p++;
uint8_t col = marker_visible_col(kind, p, content_off);
uint8_t line_style = INIT_STYLE_PLAIN;
uint32_t item_start = p;
uint32_t q = content_off;
uint32_t last_space = 0xFFFFFFFFu;
char prev_ch = ' ';
uint8_t seg_col = col;
emit_seg(p, line_style, CK_LIST, 0);
while (q < file_size && n_lines < max_lines) {
char ch = fb(q);
uint8_t soft_break = 0;
if (ch == '\n') {
uint32_t next = q + 1;
if (next >= file_size) break;
/* Blank line ends the current list item. */
if (is_line_blank(next)) break;
/* Block boundaries also end the item. */
if (is_fence_raw(next) || is_hr_raw(next) || is_table_raw(next)) break;
/* Next list marker starts a new item; same for quote/header/hr. */
uint32_t next_content = next;
uint8_t next_kind = classify_line(next, &next_content);
if (next_kind == LK_ULIST || next_kind == LK_OLIST ||
next_kind == LK_QUOTE ||
(next_kind >= LK_H1 && next_kind <= LK_H4) ||
next_kind == LK_HR) {
break;
}
/* Lazy continuation: join the next non-marker line with
* one space; trim its leading indentation first. */
while (next < file_size) {
char ws = fb(next);
if (ws == ' ' || ws == '\t') next++;
else break;
}
q = next;
soft_break = 1;
ch = ' ';
}
if (ch == '`') {
if (line_style == INIT_STYLE_CODE) line_style = INIT_STYLE_PLAIN;
else line_style = INIT_STYLE_CODE;
if (!soft_break) q++;
continue;
}
if (ch == '*' && (q + 1) < file_size && fb(q + 1) == '*') {
char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = INIT_STYLE_BOLD;
if (!soft_break) q += 2; else q++;
continue;
} else if (line_style == INIT_STYLE_BOLD && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN;
if (!soft_break) q += 2; else q++;
continue;
}
}
seg_col += 2;
if (!soft_break) q += 2; else q++;
prev_ch = '*'; continue;
}
if (ch == '~' && (q + 1) < file_size && fb(q + 1) == '~') {
char next_ch = ((q + 2) < file_size) ? fb(q + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = INIT_STYLE_STRIKE;
if (!soft_break) q += 2; else q++;
continue;
} else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN;
if (!soft_break) q += 2; else q++;
continue;
}
}
seg_col += 2;
if (!soft_break) q += 2; else q++;
prev_ch = '~'; continue;
}
if (ch == '*' || ch == '_') {
char next_ch = ((q + 1) < file_size) ? fb(q + 1) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
uint8_t new_style = (ch == '*') ? INIT_STYLE_ITALIC : INIT_STYLE_UNDER;
if (line_style == INIT_STYLE_PLAIN && ws_or_eol(prev_ch)) {
line_style = new_style;
if (!soft_break) q++; else q++;
continue;
} else if (line_style == new_style && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN;
if (!soft_break) q++; else q++;
continue;
}
}
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ch; continue;
}
if (ch == '\t') {
uint8_t tgt = (uint8_t)((seg_col & (uint8_t)~(TAB_STOP - 1)) + TAB_STOP);
if (tgt > SCREEN_W) tgt = SCREEN_W;
seg_col = tgt;
if (!soft_break) q++; else q++;
prev_ch = ' '; continue;
}
if (ch == ' ') {
if (soft_break) last_space = q;
else last_space = q + 1;
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ' ';
} else {
seg_col++;
if (!soft_break) q++; else q++;
prev_ch = ch;
}
if (seg_col >= SCREEN_W) {
uint32_t wrap_at = (last_space != 0xFFFFFFFFu && last_space > cur_seg_off)
? last_space : q;
if (wrap_at >= file_size || wrap_at == cur_seg_off) break;
emit_seg(wrap_at, line_style, CK_LIST, 1);
seg_col = col; last_space = 0xFFFFFFFFu; prev_ch = ' '; q = wrap_at;
}
}
/* Move to start of the next physical line after the item. */
if (q < file_size) {
while (q < file_size && fb(q) != '\n') q++;
if (q < file_size) q++;
}
/* One blank line between adjacent list markers is suppressed
* (same list). Two+ blank lines keep a visible separator. */
if (q < file_size && is_line_blank(q)) {
uint32_t t = q;
uint8_t blanks = 0;
while (t < file_size && is_line_blank(t)) {
while (t < file_size && fb(t) != '\n') t++;
if (t < file_size) t++;
blanks++;
if (blanks >= 2) break;
}
if (blanks == 1 && t < file_size) {
uint32_t d = t;
uint8_t nk = classify_line(t, &d);
if (nk == LK_ULIST || nk == LK_OLIST) {
p = t; /* suppress the single blank row */
continue;
}
}
}
p = q;
if (p < item_start) p = item_start;
continue;
}