@fresh-editor/fresh-editor 0.2.22 → 0.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -37,6 +37,11 @@ function setGlobalComposeEnabled(value: boolean): void {
37
37
  interface TableWidthInfo {
38
38
  maxW: number[];
39
39
  allocated: number[];
40
+ // True iff this row is the markdown source separator (`|---|---|---|`) — the
41
+ // border code uses this to avoid drawing a duplicate `├─┼─┤` next to it.
42
+ // Optional for backwards-compat with persisted view states from older
43
+ // sessions.
44
+ isSourceSep?: boolean;
40
45
  }
41
46
 
42
47
  // Helper: check whether the active split has compose mode for this buffer
@@ -90,6 +95,161 @@ const HTML_ENTITY_MAP: Record<string, string> = {
90
95
  laquo: "\u00AB", raquo: "\u00BB", ensp: "\u2002", emsp: "\u2003", thinsp: "\u2009",
91
96
  };
92
97
 
98
+ // =============================================================================
99
+ // Table border virtual lines (top/bottom + inter-row separators)
100
+ // =============================================================================
101
+ //
102
+ // Markdown tables source-encode only an underline-style separator between the
103
+ // header and the first data row. In compose mode we already conceal the
104
+ // pipe characters into Unicode box-drawing (`│`, `├`, `┼`, `┤`). This module
105
+ // adds the *missing* visual frame: a `┌─┬─┐` top border above the header,
106
+ // `├─┼─┤` separators between consecutive data rows (so each row reads as a
107
+ // distinct cell), and a `└─┴─┘` bottom border below the last row.
108
+ //
109
+ // Implementation:
110
+ //
111
+ // * Borders are virtual lines (no source bytes), keyed per-line via a
112
+ // unique namespace `md-tb-${lineNumber}`. The namespace lets us
113
+ // clear+rebuild borders for one row without disturbing other tables.
114
+ // * "First/last/source-separator" classification is derived from the
115
+ // cached widthMap (a row is "known" iff it has a TableWidthInfo entry).
116
+ // This is cheap and stable across scrolls because widthMap accumulates.
117
+ // * Border column widths come from the same `allocated` widths used by
118
+ // processLineConceals, so the borders line up exactly with the cell
119
+ // conceals.
120
+
121
+ /** Build a horizontal table border line of the given style for a row. */
122
+ function buildTableBorderLine(
123
+ allocated: number[],
124
+ left: string,
125
+ mid: string,
126
+ right: string,
127
+ ): string {
128
+ // Each cell render is `│ <text padded to allocated[i] - 2> │` (2 chars of
129
+ // inside padding). The matching border slot must therefore be
130
+ // `allocated[i]` wide of `─` characters between the corner/junction marks.
131
+ const parts: string[] = [];
132
+ for (let i = 0; i < allocated.length; i++) {
133
+ const fill = "─".repeat(Math.max(1, allocated[i]));
134
+ parts.push(fill);
135
+ }
136
+ return left + parts.join(mid) + right;
137
+ }
138
+
139
+ /** True if `lineContent` looks like a markdown table separator row. */
140
+ function isTableSeparatorContent(lineContent: string): boolean {
141
+ return /^\|[-:\s|]+\|$/.test(lineContent.trim());
142
+ }
143
+
144
+ /** Re-emit the table border virtual lines for the given table-row group.
145
+ *
146
+ * Detects the group's first/last visible rows by consulting `widthMap`
147
+ * (which is updated by `processTableAlignment` before this runs). A row at
148
+ * `lineNumber - 1` or `lineNumber + 1` that is *not* in `widthMap` is treated
149
+ * as the boundary of the table's visible extent.
150
+ */
151
+ function processTableBorders(
152
+ bufferId: number,
153
+ lines: Array<{
154
+ line_number: number;
155
+ byte_start: number;
156
+ byte_end: number;
157
+ content: string;
158
+ }>,
159
+ widthMap: Map<number, TableWidthInfo>,
160
+ ): void {
161
+ // Use theme keys (resolved at render time so the borders follow theme
162
+ // changes — same pattern as addOverlay's fg/bg options).
163
+ //
164
+ // * fg → editor.fg (the default document foreground, matching the
165
+ // concealed `│` / `─` glyphs inside row text so the virtual
166
+ // `┌─┬─┐` / `├─┼─┤` / `└─┴─┘` frame doesn't create a visible seam
167
+ // where it meets the in-text borders)
168
+ // * bg → editor.bg (matches the document background so the borders
169
+ // blend in rather than carving an opaque slab through the page)
170
+ const borderOptions = { fg: "editor.fg", bg: "editor.bg" };
171
+
172
+ for (const line of lines) {
173
+ const ns = `md-tb-${line.line_number}`;
174
+ // Always start by clearing this row's previous borders (handles
175
+ // edits that removed/widened the row, scrolls that change the
176
+ // first/last classification, etc.).
177
+ editor.clearVirtualTextNamespace(bufferId, ns);
178
+
179
+ const trimmed = line.content.trim();
180
+ const isTableRow = trimmed.startsWith("|") || trimmed.endsWith("|");
181
+ if (!isTableRow) continue;
182
+
183
+ const widthInfo = widthMap.get(line.line_number);
184
+ if (!widthInfo || widthInfo.allocated.length === 0) continue;
185
+
186
+ const allocated = widthInfo.allocated;
187
+ // Prefer the cached flag (set by processTableAlignment from the source
188
+ // text of this exact row); fall back to a regex check in case this row
189
+ // was loaded from a persisted view state without the flag.
190
+ const isSourceSep = widthInfo.isSourceSep === true
191
+ || isTableSeparatorContent(line.content);
192
+
193
+ const prevIsTable = widthMap.has(line.line_number - 1);
194
+ const nextIsTable = widthMap.has(line.line_number + 1);
195
+
196
+ // Top border: only above the very first known row of the table.
197
+ // ┌─┬─┐ — opens the frame above the header.
198
+ if (!prevIsTable) {
199
+ editor.addVirtualLine(
200
+ bufferId,
201
+ line.byte_start,
202
+ buildTableBorderLine(allocated, "┌", "┬", "┐"),
203
+ borderOptions,
204
+ true, // above
205
+ ns,
206
+ 0,
207
+ );
208
+ }
209
+
210
+ // Inter-row separator: between consecutive *data* rows.
211
+ //
212
+ // Skip if either side is the source separator row (`|---|---|---|`)
213
+ // because the source already provides `├─┼─┤` there via conceals —
214
+ // adding another above/below would draw two adjacent separator lines.
215
+ //
216
+ // Drawn ABOVE the current row when its predecessor is also a (non-
217
+ // source-separator) table row, so each row owns the separator that
218
+ // appears above it.
219
+ const prevInfo = widthMap.get(line.line_number - 1);
220
+ const prevIsSourceSep = prevInfo?.isSourceSep === true;
221
+ if (prevIsTable && !isSourceSep && !prevIsSourceSep) {
222
+ editor.addVirtualLine(
223
+ bufferId,
224
+ line.byte_start,
225
+ buildTableBorderLine(allocated, "├", "┼", "┤"),
226
+ borderOptions,
227
+ true, // above
228
+ ns,
229
+ 1,
230
+ );
231
+ }
232
+
233
+ // Bottom border: only below the last known row of the table.
234
+ // └─┴─┘ — closes the frame. Anchor at the END of the row's bytes
235
+ // (one before the trailing newline) and place "below".
236
+ if (!nextIsTable) {
237
+ // byte_end points just past the newline; anchor at last byte of
238
+ // the row content so the virtual line renders directly under it.
239
+ const anchor = Math.max(line.byte_start, line.byte_end - 1);
240
+ editor.addVirtualLine(
241
+ bufferId,
242
+ anchor,
243
+ buildTableBorderLine(allocated, "└", "┴", "┘"),
244
+ borderOptions,
245
+ false, // below
246
+ ns,
247
+ 0,
248
+ );
249
+ }
250
+ }
251
+ }
252
+
93
253
  // =============================================================================
94
254
  // Block-based parser for hanging indent support
95
255
  // =============================================================================
@@ -1375,18 +1535,25 @@ function processTableAlignment(
1375
1535
  if (allocGrew(widthMap.get(ln)!)) { needsRefresh = true; break; }
1376
1536
  }
1377
1537
 
1378
- // Store merged widths for all lines in the group AND propagate
1379
- // back to adjacent cached lines so they pick up wider columns
1380
- // without needing to be re-delivered by lines_changed.
1381
- const info: TableWidthInfo = { maxW: merged, allocated };
1538
+ // Store merged widths for each line in the group. We tag the source
1539
+ // separator row (`|---|---|---|`) so the border renderer can skip
1540
+ // drawing a duplicate `├─┼─┤` adjacent to it (the source separator is
1541
+ // already concealed into one). Each line gets its own info object so
1542
+ // the per-row `isSourceSep` flag is independent.
1382
1543
  for (const line of group) {
1383
- widthMap.set(line.line_number, info);
1544
+ const isSep = /^\|[-:\s|]+\|$/.test(line.content.trim());
1545
+ widthMap.set(line.line_number, { maxW: merged, allocated, isSourceSep: isSep });
1384
1546
  }
1547
+ // Adjacent cached lines (already-processed neighbours of this group)
1548
+ // need their `allocated` updated but should keep their existing
1549
+ // `isSourceSep` flag — they were classified when they were processed.
1385
1550
  for (let ln = firstLine - 1; widthMap.has(ln); ln--) {
1386
- widthMap.set(ln, info);
1551
+ const prev = widthMap.get(ln)!;
1552
+ widthMap.set(ln, { maxW: merged, allocated, isSourceSep: prev.isSourceSep });
1387
1553
  }
1388
1554
  for (let ln = lastLine + 1; widthMap.has(ln); ln++) {
1389
- widthMap.set(ln, info);
1555
+ const prev = widthMap.get(ln)!;
1556
+ widthMap.set(ln, { maxW: merged, allocated, isSourceSep: prev.isSourceSep });
1390
1557
  }
1391
1558
  }
1392
1559
 
@@ -1426,6 +1593,15 @@ function onMarkdownLinesChanged(data: {
1426
1593
  processLineSoftBreaks(data.buffer_id, line.content, line.byte_start, line.byte_end, cursors, line.line_number);
1427
1594
  }
1428
1595
 
1596
+ // Add/refresh table border virtual lines (top/bottom + inter-row separators).
1597
+ // Runs AFTER processTableAlignment so the widthMap reflects the latest
1598
+ // allocated widths, and AFTER processLineConceals so the borders we draw
1599
+ // line up with the cell pipes the conceals produce.
1600
+ const widthMapForBorders = getTableWidths(data.buffer_id);
1601
+ if (widthMapForBorders) {
1602
+ processTableBorders(data.buffer_id, data.lines, widthMapForBorders);
1603
+ }
1604
+
1429
1605
  if (tableWidthsGrew) {
1430
1606
  editor.refreshLines(data.buffer_id);
1431
1607
  }