revspec 0.8.3 → 0.8.4
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.
- package/package.json +1 -1
- package/src/tui/app.ts +17 -4
- package/src/tui/help.ts +1 -0
- package/src/tui/pager.ts +45 -6
- package/src/tui/ui/markdown.ts +1 -1
package/package.json
CHANGED
package/src/tui/app.ts
CHANGED
|
@@ -108,6 +108,12 @@ export async function runTui(
|
|
|
108
108
|
rootBox.add(bottomBar.box);
|
|
109
109
|
renderer.root.add(rootBox);
|
|
110
110
|
|
|
111
|
+
// Wrap mode state
|
|
112
|
+
let wrapEnabled = false;
|
|
113
|
+
function currentWrapWidth(): number {
|
|
114
|
+
return wrapEnabled ? renderer.width : 0;
|
|
115
|
+
}
|
|
116
|
+
|
|
111
117
|
// 7. Initial render
|
|
112
118
|
function refreshPager(): void {
|
|
113
119
|
// Spec mutation guard
|
|
@@ -118,7 +124,7 @@ export async function runTui(
|
|
|
118
124
|
}
|
|
119
125
|
} catch {}
|
|
120
126
|
|
|
121
|
-
buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds);
|
|
127
|
+
buildPagerNodes(pager.lineNode, state, searchQuery, state.unreadThreadIds, currentWrapWidth());
|
|
122
128
|
buildTopBar(topBar, specFile, state, state.unreadCount(), specMtimeChanged);
|
|
123
129
|
// Don't overwrite transient messages (welcome hint, warnings) during navigation
|
|
124
130
|
if (!messageTimer) {
|
|
@@ -171,7 +177,7 @@ export async function runTui(
|
|
|
171
177
|
// Map visual row back to spec line number (for H/M/L)
|
|
172
178
|
function visualRowToSpecLine(targetRow: number): number {
|
|
173
179
|
for (let i = 0; i < state.specLines.length; i++) {
|
|
174
|
-
const row = i + countExtraVisualLines(state.specLines, i);
|
|
180
|
+
const row = i + countExtraVisualLines(state.specLines, i, currentWrapWidth());
|
|
175
181
|
if (row >= targetRow) return i + 1;
|
|
176
182
|
}
|
|
177
183
|
return state.lineCount;
|
|
@@ -219,7 +225,7 @@ export async function runTui(
|
|
|
219
225
|
// Helper: scroll pager to ensure cursor line is visible
|
|
220
226
|
function ensureCursorVisible(): void {
|
|
221
227
|
// Map spec line to visual row, accounting for table border extra lines
|
|
222
|
-
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
|
|
228
|
+
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1, currentWrapWidth());
|
|
223
229
|
const cursorRow = state.cursorLine - 1 + extra;
|
|
224
230
|
const viewportHeight = Math.max(1, renderer.height - 2); // minus top + bottom bar
|
|
225
231
|
|
|
@@ -254,6 +260,13 @@ export async function runTui(
|
|
|
254
260
|
exitTui(resolve, "session-end");
|
|
255
261
|
return "exit";
|
|
256
262
|
}
|
|
263
|
+
// :wrap — toggle line wrapping
|
|
264
|
+
if (cmd === "wrap") {
|
|
265
|
+
wrapEnabled = !wrapEnabled;
|
|
266
|
+
refreshPager();
|
|
267
|
+
showTransient(wrapEnabled ? "Line wrap on" : "Line wrap off", "info");
|
|
268
|
+
return "stay";
|
|
269
|
+
}
|
|
257
270
|
// :{N} — jump to line number
|
|
258
271
|
const lineNum = parseInt(cmd, 10);
|
|
259
272
|
if (!isNaN(lineNum) && lineNum > 0) {
|
|
@@ -636,7 +649,7 @@ export async function runTui(
|
|
|
636
649
|
refreshPager();
|
|
637
650
|
break;
|
|
638
651
|
case "center-cursor": {
|
|
639
|
-
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1);
|
|
652
|
+
const extra = countExtraVisualLines(state.specLines, state.cursorLine - 1, currentWrapWidth());
|
|
640
653
|
const cursorRow = state.cursorLine - 1 + extra;
|
|
641
654
|
const halfView = Math.floor(pageSize() / 2);
|
|
642
655
|
pager.scrollBox.scrollTo(Math.max(0, cursorRow - halfView));
|
package/src/tui/help.ts
CHANGED
package/src/tui/pager.ts
CHANGED
|
@@ -53,13 +53,33 @@ export function buildPagerContent(state: ReviewState, searchQuery?: string | nul
|
|
|
53
53
|
* Inline markdown is parsed and styled per line.
|
|
54
54
|
* Line numbers and thread hints are dimmed.
|
|
55
55
|
*/
|
|
56
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Word-wrap a string at the given width, breaking at word boundaries.
|
|
58
|
+
*/
|
|
59
|
+
function wordWrap(text: string, width: number): string[] {
|
|
60
|
+
if (width <= 0 || text.length <= width) return [text];
|
|
61
|
+
const lines: string[] = [];
|
|
62
|
+
let remaining = text;
|
|
63
|
+
while (remaining.length > width) {
|
|
64
|
+
let breakAt = remaining.lastIndexOf(" ", width);
|
|
65
|
+
if (breakAt <= 0) breakAt = width; // no space found — hard break
|
|
66
|
+
lines.push(remaining.slice(0, breakAt));
|
|
67
|
+
remaining = remaining.slice(breakAt).replace(/^ /, ""); // trim leading space on continuation
|
|
68
|
+
}
|
|
69
|
+
if (remaining.length > 0) lines.push(remaining);
|
|
70
|
+
return lines;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, searchQuery?: string | null, unreadThreadIds?: ReadonlySet<string>, wrapWidth?: number): void {
|
|
57
74
|
lineNode.clear();
|
|
58
75
|
|
|
59
76
|
// Calculate dynamic gutter width based on total line count
|
|
60
77
|
const numWidth = Math.max(String(state.lineCount).length, 3);
|
|
61
|
-
|
|
62
|
-
|
|
78
|
+
const gutterWidth = 2 + numWidth + 2; // prefix(1) + indicator(1) + numWidth + spaces(2)
|
|
79
|
+
// Blank gutter for table borders and wrap continuations
|
|
80
|
+
const gutterBlank = " ".repeat(gutterWidth);
|
|
81
|
+
// Available content width for wrapping
|
|
82
|
+
const contentWidth = wrapWidth && wrapWidth > gutterWidth ? wrapWidth - gutterWidth : 0;
|
|
63
83
|
|
|
64
84
|
// Pre-scan for table blocks so we can calculate column widths
|
|
65
85
|
const tableBlocks = new Map<number, TableBlock>();
|
|
@@ -179,6 +199,17 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
|
|
|
179
199
|
lineNode.add(TextNodeRenderable.fromString(part, { fg: theme.text, bg: isCursor ? theme.backgroundElement : undefined }));
|
|
180
200
|
}
|
|
181
201
|
}
|
|
202
|
+
} else if (contentWidth > 0 && specText.length > contentWidth) {
|
|
203
|
+
// Wrap long lines — first chunk gets markdown, continuations get blank gutter + markdown
|
|
204
|
+
const chunks = wordWrap(specText, contentWidth);
|
|
205
|
+
const segments = parseMarkdownLine(chunks[0]);
|
|
206
|
+
addSegments(lineNode, segments, theme.text, isCursor ? theme.backgroundElement : undefined);
|
|
207
|
+
for (let c = 1; c < chunks.length; c++) {
|
|
208
|
+
lineNode.add(TextNodeRenderable.fromString("\n", {}));
|
|
209
|
+
lineNode.add(TextNodeRenderable.fromString(gutterBlank, { fg: theme.textDim }));
|
|
210
|
+
const contSegments = parseMarkdownLine(chunks[c]);
|
|
211
|
+
addSegments(lineNode, contSegments, theme.text, isCursor ? theme.backgroundElement : undefined);
|
|
212
|
+
}
|
|
182
213
|
} else {
|
|
183
214
|
// Parse and render inline markdown
|
|
184
215
|
const segments = parseMarkdownLine(specText);
|
|
@@ -195,10 +226,14 @@ export function buildPagerNodes(lineNode: TextRenderable, state: ReviewState, se
|
|
|
195
226
|
// --- Visual row offset calculation ---
|
|
196
227
|
|
|
197
228
|
/**
|
|
198
|
-
* Count extra visual lines (table borders) before a given spec line index.
|
|
229
|
+
* Count extra visual lines (table borders + word wrap) before a given spec line index.
|
|
199
230
|
* Used to map spec line numbers to actual visual rows in the rendered content.
|
|
200
231
|
*/
|
|
201
|
-
export function countExtraVisualLines(specLines: string[], cursorIndex: number): number {
|
|
232
|
+
export function countExtraVisualLines(specLines: string[], cursorIndex: number, wrapWidth?: number): number {
|
|
233
|
+
const numWidth = Math.max(String(specLines.length).length, 3);
|
|
234
|
+
const gutterWidth = 2 + numWidth + 2;
|
|
235
|
+
const contentWidth = wrapWidth && wrapWidth > gutterWidth ? wrapWidth - gutterWidth : 0;
|
|
236
|
+
|
|
202
237
|
let extra = 0;
|
|
203
238
|
let i = 0;
|
|
204
239
|
while (i < specLines.length) {
|
|
@@ -215,6 +250,10 @@ export function countExtraVisualLines(specLines: string[], cursorIndex: number):
|
|
|
215
250
|
if (cursorIndex >= tableEnd) extra++;
|
|
216
251
|
continue;
|
|
217
252
|
}
|
|
253
|
+
// Word wrap: count extra continuation lines
|
|
254
|
+
if (contentWidth > 0 && i < cursorIndex && specLines[i].length > contentWidth) {
|
|
255
|
+
extra += wordWrap(specLines[i], contentWidth).length - 1;
|
|
256
|
+
}
|
|
218
257
|
i++;
|
|
219
258
|
}
|
|
220
259
|
return extra;
|
|
@@ -243,7 +282,7 @@ export function createPager(renderer: CliRenderer): PagerComponents {
|
|
|
243
282
|
width: "100%",
|
|
244
283
|
flexGrow: 1,
|
|
245
284
|
scrollY: true,
|
|
246
|
-
scrollX:
|
|
285
|
+
scrollX: false,
|
|
247
286
|
backgroundColor: theme.base,
|
|
248
287
|
});
|
|
249
288
|
|
package/src/tui/ui/markdown.ts
CHANGED
|
@@ -82,7 +82,7 @@ export function parseMarkdownLine(line: string): StyledSegment[] {
|
|
|
82
82
|
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
|
|
83
83
|
if (headingMatch) {
|
|
84
84
|
const level = headingMatch[1].length;
|
|
85
|
-
const color = level <= 2 ? theme.blue : theme.mauve;
|
|
85
|
+
const color = level <= 2 ? theme.blue : level === 3 ? theme.mauve : theme.textMuted;
|
|
86
86
|
// Parse inline markdown within heading text
|
|
87
87
|
const inner = parseInlineMarkdown(headingMatch[2]);
|
|
88
88
|
return inner.map((s) => ({
|