@tmustier/pi-files-widget 0.1.16 → 0.1.17
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/CHANGELOG.md +12 -0
- package/README.md +4 -0
- package/TODO.md +4 -4
- package/file-viewer.ts +35 -20
- package/index.ts +8 -4
- package/input-utils.ts +19 -10
- package/package.json +1 -1
- package/utils.ts +4 -0
- package/viewer.ts +91 -12
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this extension will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [Unreleased]
|
|
6
|
+
|
|
7
|
+
## [0.1.17] - 2026-04-19
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- Make the inline comment editor multiline with wrapped footer rendering, `Enter` for a new line, and `Ctrl+Enter`/`Ctrl+D` to send.
|
|
11
|
+
- Add an `m` toggle for rendered vs raw Markdown in the viewer, and fall back to raw mode before line-based search or selection.
|
|
12
|
+
- Show a sent/queued confirmation toast after returning an inline comment to the agent.
|
|
13
|
+
|
|
14
|
+
### Thanks
|
|
15
|
+
- Thanks to avg8888 in the Pi Discord for surfacing the comment editor and Markdown review issues fixed in this release.
|
|
16
|
+
|
|
5
17
|
## [0.1.16] - 2026-04-19
|
|
6
18
|
|
|
7
19
|
### Fixed
|
package/README.md
CHANGED
|
@@ -113,10 +113,13 @@ If missing, `/review` or `/diff` will show a clear install prompt.
|
|
|
113
113
|
- `PgUp/PgDn`: page up/down
|
|
114
114
|
- `g/G`: top/bottom
|
|
115
115
|
- `d`: toggle diff (tracked files only)
|
|
116
|
+
- `m`: toggle rendered/raw view for Markdown files
|
|
116
117
|
- `/`: search (type to search)
|
|
117
118
|
- `n` / `N`: next/prev match
|
|
118
119
|
- `v`: select mode (line selection)
|
|
119
120
|
- `c`: comment on selected lines (inline prompt)
|
|
121
|
+
- `Enter`: new line in the comment editor
|
|
122
|
+
- `Ctrl+Enter` or `Ctrl+D`: send the comment (`Alt+Enter` also works when supported)
|
|
120
123
|
- `]` / `[`: next/prev changed file
|
|
121
124
|
- `+` / `-`: increase/decrease viewer height
|
|
122
125
|
- `q`: back to browser
|
|
@@ -124,6 +127,7 @@ If missing, `/review` or `/diff` will show a clear install prompt.
|
|
|
124
127
|
## Notes
|
|
125
128
|
|
|
126
129
|
- Untracked files show as `[UNTRACKED]` and open in normal view.
|
|
130
|
+
- Searching in rendered Markdown switches to raw mode first, and selecting from rendered Markdown first switches you back to raw so line-based matches and comments stay aligned with the source file.
|
|
127
131
|
- Folder LOCs are shown only when the folder is collapsed (expanded folders would duplicate counts).
|
|
128
132
|
- Line counts load asynchronously; the header shows activity while counts are computed.
|
|
129
133
|
- Large non-git folders load progressively and may show `[partial]` while loading in safe mode.
|
package/TODO.md
CHANGED
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
### Markdown Rendering
|
|
47
47
|
- [x] Detect `glow` availability
|
|
48
48
|
- [x] Shell out to `glow` for .md files
|
|
49
|
-
- [
|
|
49
|
+
- [x] Toggle between rendered and raw (`m`)
|
|
50
50
|
- [x] Fallback to syntax-highlighted raw
|
|
51
51
|
|
|
52
52
|
### Diff View
|
|
@@ -66,15 +66,15 @@
|
|
|
66
66
|
|
|
67
67
|
### Comment Dialog
|
|
68
68
|
- [x] `c` to open comment input
|
|
69
|
-
- [
|
|
70
|
-
- [x] `Enter`
|
|
69
|
+
- [x] Multi-line text input
|
|
70
|
+
- [x] `Enter` for newline and `Ctrl+Enter` / `Ctrl+D` to confirm
|
|
71
71
|
- [x] `Esc` to cancel
|
|
72
72
|
|
|
73
73
|
### Send to Agent
|
|
74
74
|
- [x] Format message with file path, line range, code snippet, comment
|
|
75
75
|
- [x] Use `pi.sendUserMessage()` with `deliverAs: "followUp"`
|
|
76
76
|
- [x] Handle case when agent is idle vs streaming
|
|
77
|
-
- [
|
|
77
|
+
- [x] Show confirmation notification
|
|
78
78
|
|
|
79
79
|
## Phase 4: tuicr Integration (Optional)
|
|
80
80
|
|
package/file-viewer.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { execSync } from "node:child_process";
|
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
|
|
5
5
|
import { isGitRepo } from "./git";
|
|
6
|
-
import { hasCommand, stripLeadingEmptyLines } from "./utils";
|
|
6
|
+
import { hasCommand, isMarkdownPath, stripLeadingEmptyLines } from "./utils";
|
|
7
7
|
|
|
8
8
|
const DIFF_CONTENT_PREFIXES = new Set(["+", "-", " "]);
|
|
9
9
|
|
|
@@ -152,14 +152,20 @@ function wrapDeltaLines(lines: string[], width: number): string[] {
|
|
|
152
152
|
return wrapped;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
export interface LoadedFileContent {
|
|
156
|
+
lines: string[];
|
|
157
|
+
renderedMarkdown: boolean;
|
|
158
|
+
}
|
|
159
|
+
|
|
155
160
|
export function loadFileContent(
|
|
156
161
|
filePath: string,
|
|
157
162
|
cwd: string,
|
|
158
163
|
diffMode: boolean,
|
|
159
164
|
hasChanges: boolean,
|
|
160
|
-
width?: number
|
|
161
|
-
|
|
162
|
-
|
|
165
|
+
width?: number,
|
|
166
|
+
renderMarkdown = true
|
|
167
|
+
): LoadedFileContent {
|
|
168
|
+
const isMarkdown = isMarkdownPath(filePath);
|
|
163
169
|
const termWidth = width || process.stdout.columns || 80;
|
|
164
170
|
|
|
165
171
|
try {
|
|
@@ -195,7 +201,7 @@ export function loadFileContent(
|
|
|
195
201
|
}
|
|
196
202
|
|
|
197
203
|
if (!diffOutput.trim()) {
|
|
198
|
-
return ["No diff available - file may be untracked or unchanged"];
|
|
204
|
+
return { lines: ["No diff available - file may be untracked or unchanged"], renderedMarkdown: false };
|
|
199
205
|
}
|
|
200
206
|
|
|
201
207
|
if (hasCommand("delta")) {
|
|
@@ -211,23 +217,23 @@ export function loadFileContent(
|
|
|
211
217
|
stdio: ["pipe", "pipe", "pipe"],
|
|
212
218
|
}
|
|
213
219
|
);
|
|
214
|
-
return wrapDeltaLines(stripLeadingEmptyLines(deltaOutput.split("\n")), termWidth);
|
|
220
|
+
return { lines: wrapDeltaLines(stripLeadingEmptyLines(deltaOutput.split("\n")), termWidth), renderedMarkdown: false };
|
|
215
221
|
} catch {
|
|
216
222
|
// Fall back to raw diff
|
|
217
223
|
}
|
|
218
224
|
}
|
|
219
225
|
|
|
220
|
-
return wrapDiffLines(stripLeadingEmptyLines(diffOutput.split("\n")), termWidth);
|
|
226
|
+
return { lines: wrapDiffLines(stripLeadingEmptyLines(diffOutput.split("\n")), termWidth), renderedMarkdown: false };
|
|
221
227
|
} catch (e: any) {
|
|
222
|
-
return [`Diff error: ${e.message}`];
|
|
228
|
+
return { lines: [`Diff error: ${e.message}`], renderedMarkdown: false };
|
|
223
229
|
}
|
|
224
230
|
}
|
|
225
231
|
|
|
226
|
-
if (isMarkdown && hasCommand("glow")) {
|
|
232
|
+
if (isMarkdown && renderMarkdown && hasCommand("glow")) {
|
|
227
233
|
try {
|
|
228
234
|
const output = execSync(`glow -s dark -w ${termWidth} "${filePath}"`, { encoding: "utf-8", timeout: 10000 });
|
|
229
235
|
if (output.trim()) {
|
|
230
|
-
return stripLeadingEmptyLines(output.split("\n"));
|
|
236
|
+
return { lines: stripLeadingEmptyLines(output.split("\n")), renderedMarkdown: true };
|
|
231
237
|
}
|
|
232
238
|
} catch {
|
|
233
239
|
// Fall through to bat
|
|
@@ -236,16 +242,22 @@ export function loadFileContent(
|
|
|
236
242
|
|
|
237
243
|
if (hasCommand("bat")) {
|
|
238
244
|
try {
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
245
|
+
return {
|
|
246
|
+
lines: execSync(
|
|
247
|
+
`bat --style=numbers --color=always --paging=never --wrap=auto --terminal-width=${termWidth} "${filePath}"`,
|
|
248
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
249
|
+
).split("\n"),
|
|
250
|
+
renderedMarkdown: false,
|
|
251
|
+
};
|
|
243
252
|
} catch {
|
|
244
253
|
try {
|
|
245
|
-
return
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
254
|
+
return {
|
|
255
|
+
lines: execSync(
|
|
256
|
+
`bat --style=numbers --color=always --paging=never --terminal-width=${termWidth} "${filePath}"`,
|
|
257
|
+
{ encoding: "utf-8", timeout: 10000 }
|
|
258
|
+
).split("\n"),
|
|
259
|
+
renderedMarkdown: false,
|
|
260
|
+
};
|
|
249
261
|
} catch {
|
|
250
262
|
// Fall through to raw file read
|
|
251
263
|
}
|
|
@@ -253,8 +265,11 @@ export function loadFileContent(
|
|
|
253
265
|
}
|
|
254
266
|
|
|
255
267
|
const raw = readFileSync(filePath, "utf-8");
|
|
256
|
-
return
|
|
268
|
+
return {
|
|
269
|
+
lines: raw.split("\n").map((line, i) => `${String(i + 1).padStart(4)} │ ${line}`),
|
|
270
|
+
renderedMarkdown: false,
|
|
271
|
+
};
|
|
257
272
|
} catch (e: any) {
|
|
258
|
-
return [`Error loading file: ${e.message}`];
|
|
273
|
+
return { lines: [`Error loading file: ${e.message}`], renderedMarkdown: false };
|
|
259
274
|
}
|
|
260
275
|
}
|
package/index.ts
CHANGED
|
@@ -41,10 +41,14 @@ export default function editorExtension(pi: ExtensionAPI): void {
|
|
|
41
41
|
};
|
|
42
42
|
|
|
43
43
|
const requestComment = (payload: { relPath: string; lineRange: string; ext: string; selectedText: string }, comment: string) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
const message = formatCommentMessage(payload, comment);
|
|
45
|
+
if (ctx.isIdle()) {
|
|
46
|
+
pi.sendUserMessage(message);
|
|
47
|
+
ctx.ui.notify(`Comment sent to agent for ${payload.relPath} (${payload.lineRange})`, "success");
|
|
48
|
+
} else {
|
|
49
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
50
|
+
ctx.ui.notify(`Comment queued for agent for ${payload.relPath} (${payload.lineRange})`, "info");
|
|
51
|
+
}
|
|
48
52
|
};
|
|
49
53
|
|
|
50
54
|
const requestRender = () => tui.requestRender();
|
package/input-utils.ts
CHANGED
|
@@ -4,17 +4,22 @@ const CONTROL_CHARS = /[\u0000-\u0008\u000B-\u001F\u007F]/g;
|
|
|
4
4
|
const BRACKETED_PASTE_START = "\u001b[200~";
|
|
5
5
|
const BRACKETED_PASTE_END = "\u001b[201~";
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
interface SanitizeTextInputOptions {
|
|
8
|
+
preserveNewlines?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sanitizeTextInput(data: string, options: SanitizeTextInputOptions = {}): string {
|
|
8
12
|
const normalized = decodeKittyPrintable(data) ?? data;
|
|
9
13
|
if (!normalized || normalized.includes("\u001b")) {
|
|
10
14
|
return "";
|
|
11
15
|
}
|
|
12
16
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
.replace(/\
|
|
16
|
-
.replace(/\t/g, " ")
|
|
17
|
-
|
|
17
|
+
const withNewlines = normalized.replace(/\r\n?/g, "\n");
|
|
18
|
+
const withoutTabs = options.preserveNewlines
|
|
19
|
+
? withNewlines.replace(/\t/g, " ")
|
|
20
|
+
: withNewlines.replace(/\n/g, " ").replace(/\t/g, " ");
|
|
21
|
+
|
|
22
|
+
return withoutTabs.replace(CONTROL_CHARS, "");
|
|
18
23
|
}
|
|
19
24
|
|
|
20
25
|
function getPendingStartSuffix(data: string): string {
|
|
@@ -33,7 +38,11 @@ export interface TextInputBuffer {
|
|
|
33
38
|
reset(): void;
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
41
|
+
interface TextInputBufferOptions {
|
|
42
|
+
preserveNewlines?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function createTextInputBuffer(options: TextInputBufferOptions = {}): TextInputBuffer {
|
|
37
46
|
let isInPaste = false;
|
|
38
47
|
let pasteBuffer = "";
|
|
39
48
|
let pendingStart = "";
|
|
@@ -52,14 +61,14 @@ export function createTextInputBuffer(): TextInputBuffer {
|
|
|
52
61
|
const pendingSuffix = getPendingStartSuffix(combined);
|
|
53
62
|
const completeText = pendingSuffix ? combined.slice(0, combined.length - pendingSuffix.length) : combined;
|
|
54
63
|
pendingStart = pendingSuffix;
|
|
55
|
-
return sanitizeTextInput(completeText);
|
|
64
|
+
return sanitizeTextInput(completeText, options);
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
const beforePaste = combined.slice(0, startIndex);
|
|
59
68
|
const afterStart = combined.slice(startIndex + BRACKETED_PASTE_START.length);
|
|
60
69
|
isInPaste = true;
|
|
61
70
|
pasteBuffer = "";
|
|
62
|
-
return sanitizeTextInput(beforePaste) + push(afterStart);
|
|
71
|
+
return sanitizeTextInput(beforePaste, options) + push(afterStart);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
74
|
pasteBuffer += combined;
|
|
@@ -73,7 +82,7 @@ export function createTextInputBuffer(): TextInputBuffer {
|
|
|
73
82
|
isInPaste = false;
|
|
74
83
|
pasteBuffer = "";
|
|
75
84
|
|
|
76
|
-
return sanitizeTextInput(pastedText) + push(remaining);
|
|
85
|
+
return sanitizeTextInput(pastedText, options) + push(remaining);
|
|
77
86
|
};
|
|
78
87
|
|
|
79
88
|
return {
|
package/package.json
CHANGED
package/utils.ts
CHANGED
|
@@ -17,6 +17,10 @@ export function isIgnoredStatus(status?: string): boolean {
|
|
|
17
17
|
return status === "!" || status === "!!";
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
export function isMarkdownPath(path: string): boolean {
|
|
21
|
+
return path.toLowerCase().endsWith(".md");
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
export function stripLeadingEmptyLines(lines: string[]): string[] {
|
|
21
25
|
let startIdx = 0;
|
|
22
26
|
while (startIdx < lines.length && !lines[startIdx].trim()) {
|
package/viewer.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Theme } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { Key, matchesKey, truncateToWidth } from "@mariozechner/pi-tui";
|
|
2
|
+
import { Key, matchesKey, truncateToWidth, wrapTextWithAnsi } from "@mariozechner/pi-tui";
|
|
3
3
|
import { readFileSync, statSync } from "node:fs";
|
|
4
4
|
import { relative } from "node:path";
|
|
5
5
|
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
} from "./constants";
|
|
12
12
|
import { loadFileContent } from "./file-viewer";
|
|
13
13
|
import type { FileNode } from "./types";
|
|
14
|
-
import { isUntrackedStatus } from "./utils";
|
|
14
|
+
import { isMarkdownPath, isUntrackedStatus } from "./utils";
|
|
15
15
|
import { createTextInputBuffer } from "./input-utils";
|
|
16
16
|
|
|
17
|
+
const COMMENT_EDITOR_MAX_VISIBLE_LINES = 4;
|
|
18
|
+
|
|
17
19
|
export interface CommentPayload {
|
|
18
20
|
relPath: string;
|
|
19
21
|
lineRange: string;
|
|
@@ -34,6 +36,7 @@ interface ViewerState {
|
|
|
34
36
|
rawContent: string;
|
|
35
37
|
scroll: number;
|
|
36
38
|
diffMode: boolean;
|
|
39
|
+
renderMarkdown: boolean;
|
|
37
40
|
mode: ViewerMode;
|
|
38
41
|
selectStart: number;
|
|
39
42
|
selectEnd: number;
|
|
@@ -61,7 +64,8 @@ export function createViewer(
|
|
|
61
64
|
theme: Theme,
|
|
62
65
|
requestComment: (payload: CommentPayload, comment: string) => void
|
|
63
66
|
): ViewerController {
|
|
64
|
-
const
|
|
67
|
+
const searchInput = createTextInputBuffer();
|
|
68
|
+
const commentInput = createTextInputBuffer({ preserveNewlines: true });
|
|
65
69
|
|
|
66
70
|
const state: ViewerState = {
|
|
67
71
|
file: null,
|
|
@@ -69,6 +73,7 @@ export function createViewer(
|
|
|
69
73
|
rawContent: "",
|
|
70
74
|
scroll: 0,
|
|
71
75
|
diffMode: false,
|
|
76
|
+
renderMarkdown: true,
|
|
72
77
|
mode: "normal",
|
|
73
78
|
selectStart: 0,
|
|
74
79
|
selectEnd: 0,
|
|
@@ -81,6 +86,31 @@ export function createViewer(
|
|
|
81
86
|
height: DEFAULT_VIEWER_HEIGHT,
|
|
82
87
|
};
|
|
83
88
|
|
|
89
|
+
function isMarkdownFile(): boolean {
|
|
90
|
+
return !!state.file && isMarkdownPath(state.file.path);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isRenderedMarkdownMode(): boolean {
|
|
94
|
+
return isMarkdownFile() && !state.diffMode && state.renderMarkdown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function switchMarkdownToRaw(): boolean {
|
|
98
|
+
if (!isRenderedMarkdownMode()) return false;
|
|
99
|
+
state.renderMarkdown = false;
|
|
100
|
+
const width = state.lastRenderWidth || process.stdout.columns || 80;
|
|
101
|
+
reloadContent(width);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function toggleMarkdownMode(): void {
|
|
106
|
+
if (!isMarkdownFile() || state.diffMode) return;
|
|
107
|
+
state.renderMarkdown = !state.renderMarkdown;
|
|
108
|
+
state.lastRenderWidth = 0;
|
|
109
|
+
resetSearch();
|
|
110
|
+
setMode("normal");
|
|
111
|
+
clampScroll();
|
|
112
|
+
}
|
|
113
|
+
|
|
84
114
|
function resetSearch(): void {
|
|
85
115
|
state.searchQuery = "";
|
|
86
116
|
state.searchMatches = [];
|
|
@@ -98,7 +128,8 @@ export function createViewer(
|
|
|
98
128
|
|
|
99
129
|
function setMode(mode: ViewerMode): void {
|
|
100
130
|
if (mode !== state.mode) {
|
|
101
|
-
|
|
131
|
+
searchInput.reset();
|
|
132
|
+
commentInput.reset();
|
|
102
133
|
}
|
|
103
134
|
|
|
104
135
|
state.mode = mode;
|
|
@@ -146,7 +177,9 @@ export function createViewer(
|
|
|
146
177
|
if (!state.file) return;
|
|
147
178
|
refreshRawContent();
|
|
148
179
|
const hasChanges = !!state.file.gitStatus;
|
|
149
|
-
|
|
180
|
+
const result = loadFileContent(state.file.path, cwd, state.diffMode, hasChanges, width, state.renderMarkdown);
|
|
181
|
+
state.content = result.lines;
|
|
182
|
+
state.renderMarkdown = result.renderedMarkdown;
|
|
150
183
|
state.lastRenderWidth = width;
|
|
151
184
|
clampScroll();
|
|
152
185
|
if (state.searchQuery) {
|
|
@@ -235,6 +268,8 @@ export function createViewer(
|
|
|
235
268
|
header += theme.fg("dim", " [UNTRACKED]");
|
|
236
269
|
} else if (state.diffMode) {
|
|
237
270
|
header += theme.fg("warning", " [DIFF]");
|
|
271
|
+
} else if (isMarkdownFile()) {
|
|
272
|
+
header += theme.fg("accent", state.renderMarkdown ? " [RENDERED]" : " [RAW]");
|
|
238
273
|
}
|
|
239
274
|
if (state.mode === "select" || state.mode === "comment") {
|
|
240
275
|
header += theme.fg("accent", ` [SELECT ${state.selectStart + 1}-${state.selectEnd + 1}]`);
|
|
@@ -264,6 +299,38 @@ export function createViewer(
|
|
|
264
299
|
return truncateToWidth(header, width);
|
|
265
300
|
}
|
|
266
301
|
|
|
302
|
+
function renderCommentEditor(width: number): string[] {
|
|
303
|
+
const contentWidth = Math.max(1, width - 2);
|
|
304
|
+
const wrappedLines: string[] = [];
|
|
305
|
+
const logicalLines = state.commentText.split("\n");
|
|
306
|
+
|
|
307
|
+
for (const line of logicalLines) {
|
|
308
|
+
if (line.length === 0) {
|
|
309
|
+
wrappedLines.push("");
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
wrappedLines.push(...wrapTextWithAnsi(line, contentWidth));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (wrappedLines.length === 0) {
|
|
316
|
+
wrappedLines.push("");
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const lastIndex = wrappedLines.length - 1;
|
|
320
|
+
wrappedLines[lastIndex] = `${wrappedLines[lastIndex]}█`;
|
|
321
|
+
|
|
322
|
+
const overflow = Math.max(0, wrappedLines.length - COMMENT_EDITOR_MAX_VISIBLE_LINES);
|
|
323
|
+
const visibleLines = wrappedLines.slice(-COMMENT_EDITOR_MAX_VISIBLE_LINES);
|
|
324
|
+
if (overflow > 0 && visibleLines.length > 0) {
|
|
325
|
+
visibleLines[0] = `…${visibleLines[0]}`;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return [
|
|
329
|
+
truncateToWidth(theme.fg("accent", "Comment:"), width),
|
|
330
|
+
...visibleLines.map(line => truncateToWidth(` ${line}`, width)),
|
|
331
|
+
];
|
|
332
|
+
}
|
|
333
|
+
|
|
267
334
|
function renderFooter(width: number): string[] {
|
|
268
335
|
const lines: string[] = [];
|
|
269
336
|
const pct = state.content.length > 0
|
|
@@ -271,23 +338,23 @@ export function createViewer(
|
|
|
271
338
|
: 0;
|
|
272
339
|
|
|
273
340
|
if (state.mode === "comment") {
|
|
274
|
-
|
|
275
|
-
lines.push(truncateToWidth(prompt, width));
|
|
341
|
+
lines.push(...renderCommentEditor(width));
|
|
276
342
|
lines.push(theme.fg("borderMuted", "─".repeat(width)));
|
|
277
343
|
}
|
|
278
344
|
|
|
279
345
|
let help: string;
|
|
280
346
|
if (state.mode === "comment") {
|
|
281
|
-
help = theme.fg("dim", "Enter: send Esc: cancel");
|
|
347
|
+
help = theme.fg("dim", "Enter: newline Ctrl+Enter/Ctrl+D: send Esc: cancel");
|
|
282
348
|
} else if (state.mode === "select") {
|
|
283
349
|
help = theme.fg("dim", "j/k: extend c: comment Esc: cancel");
|
|
284
350
|
} else if (state.mode === "search") {
|
|
285
351
|
help = theme.fg("dim", "Type to search Enter: confirm Esc: cancel");
|
|
286
352
|
} else {
|
|
287
353
|
const isUntracked = state.file && isUntrackedStatus(state.file.gitStatus);
|
|
354
|
+
const markdownHelp = isMarkdownFile() && !state.diffMode ? "m: raw/render " : "";
|
|
288
355
|
help = theme.fg(
|
|
289
356
|
"dim",
|
|
290
|
-
`j/k: scroll /: search n/N: next/prev match []: files ${state.file?.gitStatus && !isUntracked ? "d: diff " : ""}q: back ${pct}%`
|
|
357
|
+
`j/k: scroll /: search n/N: next/prev match ${markdownHelp}[]: files ${state.file?.gitStatus && !isUntracked ? "d: diff " : ""}q: back ${pct}%`
|
|
291
358
|
);
|
|
292
359
|
}
|
|
293
360
|
lines.push(truncateToWidth(help, width));
|
|
@@ -308,6 +375,7 @@ export function createViewer(
|
|
|
308
375
|
state.file = file;
|
|
309
376
|
state.scroll = 0;
|
|
310
377
|
state.diffMode = !!file.gitStatus && !isUntrackedStatus(file.gitStatus);
|
|
378
|
+
state.renderMarkdown = isMarkdownPath(file.path);
|
|
311
379
|
setMode("normal");
|
|
312
380
|
state.content = [];
|
|
313
381
|
state.lastRenderWidth = 0;
|
|
@@ -323,6 +391,7 @@ export function createViewer(
|
|
|
323
391
|
state.file = null;
|
|
324
392
|
state.content = [];
|
|
325
393
|
state.rawContent = "";
|
|
394
|
+
state.renderMarkdown = true;
|
|
326
395
|
state.lastLoadedMtimeMs = null;
|
|
327
396
|
setMode("normal");
|
|
328
397
|
},
|
|
@@ -363,19 +432,21 @@ export function createViewer(
|
|
|
363
432
|
if (!state.file) return { type: "none" };
|
|
364
433
|
|
|
365
434
|
if (state.mode === "comment") {
|
|
366
|
-
if (matchesKey(data,
|
|
435
|
+
if (matchesKey(data, "ctrl+enter") || matchesKey(data, "ctrl+d") || matchesKey(data, "alt+enter")) {
|
|
367
436
|
const comment = state.commentText.trim();
|
|
368
437
|
if (comment) {
|
|
369
438
|
sendComment(comment);
|
|
370
439
|
} else {
|
|
371
440
|
setMode("normal");
|
|
372
441
|
}
|
|
442
|
+
} else if (matchesKey(data, Key.enter) || matchesKey(data, "shift+enter")) {
|
|
443
|
+
state.commentText += "\n";
|
|
373
444
|
} else if (matchesKey(data, Key.escape)) {
|
|
374
445
|
setMode("normal");
|
|
375
446
|
} else if (matchesKey(data, Key.backspace)) {
|
|
376
447
|
state.commentText = state.commentText.slice(0, -1);
|
|
377
448
|
} else {
|
|
378
|
-
const text =
|
|
449
|
+
const text = commentInput.push(data);
|
|
379
450
|
if (text) {
|
|
380
451
|
state.commentText += text;
|
|
381
452
|
}
|
|
@@ -392,7 +463,7 @@ export function createViewer(
|
|
|
392
463
|
state.searchQuery = state.searchQuery.slice(0, -1);
|
|
393
464
|
updateSearchMatches();
|
|
394
465
|
} else {
|
|
395
|
-
const text =
|
|
466
|
+
const text = searchInput.push(data);
|
|
396
467
|
if (text) {
|
|
397
468
|
state.searchQuery += text;
|
|
398
469
|
updateSearchMatches();
|
|
@@ -415,6 +486,7 @@ export function createViewer(
|
|
|
415
486
|
return { type: "none" };
|
|
416
487
|
}
|
|
417
488
|
if (matchesKey(data, "/") && state.mode !== "select") {
|
|
489
|
+
switchMarkdownToRaw();
|
|
418
490
|
setMode("search");
|
|
419
491
|
return { type: "none" };
|
|
420
492
|
}
|
|
@@ -474,7 +546,14 @@ export function createViewer(
|
|
|
474
546
|
state.scroll = 0;
|
|
475
547
|
return { type: "none" };
|
|
476
548
|
}
|
|
549
|
+
if (matchesKey(data, "m") && state.mode !== "select" && state.mode !== "comment") {
|
|
550
|
+
toggleMarkdownMode();
|
|
551
|
+
return { type: "none" };
|
|
552
|
+
}
|
|
477
553
|
if (matchesKey(data, "v") && state.mode !== "select") {
|
|
554
|
+
if (switchMarkdownToRaw()) {
|
|
555
|
+
return { type: "none" };
|
|
556
|
+
}
|
|
478
557
|
state.mode = "select";
|
|
479
558
|
state.selectStart = state.scroll;
|
|
480
559
|
state.selectEnd = state.scroll;
|