mdview: wide-break '\\', strikethrough, horizontal scroll bounds + '<'

- A trailing backslash before a newline now forces an in-paragraph line
  break (like two trailing spaces); render consumes the marker (non-code).
- Add ~~strikethrough~~ inline style (INIT_STYLE_STRIKE / EM_STRIKE),
  parsed in inline_scan, the paragraph merger and render, mirroring **.
- Horizontal pan is bounded by the widest nowrap segment on screen, and a
  '<' indicator marks hidden content off the left edge.

Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
2026-06-07 11:55:19 +03:00
parent ca5f30b332
commit 47c9cd326a
+98 -11
View File
@@ -93,6 +93,7 @@ static const uint8_t md_pallete[16 * 4] = {
#define ATTR_TEXT_ITALIC COLOR(COLOR_LIGHTGREEN, COLOR_BLUE)
#define ATTR_TEXT_UNDERSORE COLOR(COLOR_LIGHTMAGENTA, COLOR_BLUE)
#define ATTR_TEXT_CODE COLOR(COLOR_WHITE, COLOR_BLUE)
#define ATTR_TEXT_STRIKE COLOR(COLOR_DARKGRAY, COLOR_BLUE)
#define ATTR_LIST_MARKER COLOR(COLOR_LIGHTCYAN, COLOR_BLUE)
#define ATTR_QUOTE_MARKER COLOR(COLOR_CYAN, COLOR_BLUE)
#define ATTR_HR COLOR(COLOR_LIGHTGRAY, COLOR_BLUE)
@@ -435,9 +436,11 @@ static uint8_t is_fence_delim(uint16_t idx)
#define INIT_STYLE_ITALIC 0x2
#define INIT_STYLE_UNDER 0x3
#define INIT_STYLE_CODE 0x4
#define INIT_STYLE_STRIKE 0x5
/* Indexed by init_style & 7; order must match INIT_STYLE_* / EM_* below. */
static const uint8_t styles_map[] = {
ATTR_TEXT, ATTR_TEXT_BOLD, ATTR_TEXT_ITALIC, ATTR_TEXT_UNDERSORE, ATTR_TEXT_CODE
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)
@@ -705,6 +708,17 @@ static uint8_t inline_scan(uint32_t q, uint32_t q_end, uint8_t col,
}
seg_col += 2; q += 2; prev_ch = '*'; continue;
}
if (ch == '~' && (q + 1) < q_end && fb(q + 1) == '~') {
char next_ch = ((q + 2) < q_end) ? 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; q += 2; continue;
} else if (line_style == INIT_STYLE_STRIKE && ws_or_eol_or_delim(next_ch)) {
line_style = INIT_STYLE_PLAIN; q += 2; continue;
}
}
seg_col += 2; q += 2; prev_ch = '~'; continue;
}
if (ch == '*' || ch == '_') {
char next_ch = ((q + 1) < q_end) ? fb(q + 1) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
@@ -854,13 +868,19 @@ static void index_lines(void)
if (classify_line(next, &dummy) != LK_PLAIN) break;
if (is_line_blank(next)) break;
/* Hard break? */
/* Wide break? Two trailing spaces/tabs OR a trailing
* backslash before the newline force a line break inside
* the paragraph (the inline style is preserved). */
uint8_t is_hard = 0;
if (q >= p + 2) {
if (q >= p + 1) {
char b1 = fb(q - 1);
char b2 = fb(q - 2);
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
if (b1 == '\\') {
is_hard = 1;
} else if (q >= p + 2) {
char b2 = fb(q - 2);
if ((b1 == ' ' || b1 == '\t') && (b2 == ' ' || b2 == '\t'))
is_hard = 1;
}
}
if (is_hard) {
@@ -899,6 +919,23 @@ static void index_lines(void)
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)) {
@@ -960,6 +997,7 @@ static void index_lines(void)
#define EM_ITALIC 2
#define EM_UNDER 3
#define EM_CODE 4
#define EM_STRIKE 5
static void render_line(uint16_t line_idx, uint8_t row)
{
@@ -1089,6 +1127,13 @@ static void render_line(uint16_t line_idx, uint8_t row)
char prev_ch = ' '; /* for `_` flanking check */
while (p < seg_end && col < SCREEN_W) {
char ch = fb(p);
/* A backslash immediately before this segment's end-of-line is a
* wide-break marker (index_lines split here) — consume it without
* drawing. Not applied to verbatim code bodies. */
if (parse_inline && ch == '\\' && (p + 1) < seg_end) {
char nb = fb(p + 1);
if (nb == '\n' || nb == '\r') { p++; continue; }
}
/* Soft break (newline inside a paragraph segment) — render as a
* single space. Hard breaks are already separate segments. */
if (ch == '\n' || ch == '\r') ch = ' ';
@@ -1125,6 +1170,27 @@ static void render_line(uint16_t line_idx, uint8_t row)
prev_ch = '*';
continue;
}
/* `~~` (strikethrough) — same flanking rule as `**` */
if (ch == '~' && (p + 1) < seg_end && fb(p + 1) == '~') {
char next_ch = ((p + 2) < seg_end) ? fb(p + 2) : '\n';
if (is_emph_flanked(prev_ch, next_ch)) {
if (emph == EM_NONE && ws_or_eol(prev_ch)) {
emph = EM_STRIKE; cur_attr = ATTR_TEXT_STRIKE;
p += 2; continue;
} else if (emph == EM_STRIKE && ws_or_eol_or_delim(next_ch)) {
emph = EM_NONE; cur_attr = base_attr;
p += 2; continue;
}
}
/* literal `~~` — render both tildes */
p += 2;
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '~', cur_attr);
cc++;
if (cc >= effective_vx && col < SCREEN_W) wrchar(col++, row, '~', cur_attr);
cc++;
prev_ch = '~';
continue;
}
/* `*` (italic) and `_` (underline) — same flanking rule */
if (ch == '*' || ch == '_') {
char next_ch = ((p + 1) < seg_end) ? fb(p + 1) : '\n';
@@ -1197,6 +1263,12 @@ static void render_line(uint16_t line_idx, uint8_t row)
break;
}
}
/* Left-edge indicator: when panned right, show that nowrap content
* is hidden off the left side. Overlays column 0. */
if (is_nowrap(line_idx) && viewport_x > 0) {
wrchar(0, row, '<', ATTR_TRUNC);
}
}
/* Pad rest of the row so previous render content is wiped. For code-
* body rows use the code attribute so the block extends to the right
@@ -1306,17 +1378,32 @@ static void scroll_down(uint16_t n)
}
}
/* Horizontal pan (only when viewport contains nowrap lines). */
/* Horizontal pan (only when the viewport contains nowrap lines). The pan
* is bounded by the widest nowrap segment currently on screen, so the user
* cannot scroll past the end of the longest code/table line. */
static void scroll_h(int8_t delta)
{
uint8_t any_nowrap = 0;
uint16_t maxw = 0;
for (uint8_t i = 0; i < VIEW_H; i++) {
if (is_nowrap((uint16_t)(top_line + i))) { any_nowrap = 1; break; }
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; /* ~visible width */
if (w > maxw) maxw = w;
}
if (!any_nowrap) return;
if (maxw == 0) return; /* no nowrap content in view */
/* Max pan = width beyond the screen, capped to the uint8 pan range. */
uint16_t over = (maxw > SCREEN_W) ? (uint16_t)(maxw - SCREEN_W) : 0;
if (over > 248u) over = 248u;
uint8_t max_vx = (uint8_t)over;
int16_t nx = (int16_t)viewport_x + delta;
if (nx < 0) nx = 0;
if (nx > 240) nx = 240;
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();