@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 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
- - [ ] Toggle between rendered and raw (`m` key?)
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
- - [ ] Multi-line text input
70
- - [x] `Enter` or `Ctrl+Enter` to confirm
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
- - [ ] Show confirmation notification
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
- ): string[] {
162
- const isMarkdown = filePath.endsWith(".md");
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 execSync(
240
- `bat --style=numbers --color=always --paging=never --wrap=auto --terminal-width=${termWidth} "${filePath}"`,
241
- { encoding: "utf-8", timeout: 10000 }
242
- ).split("\n");
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 execSync(
246
- `bat --style=numbers --color=always --paging=never --terminal-width=${termWidth} "${filePath}"`,
247
- { encoding: "utf-8", timeout: 10000 }
248
- ).split("\n");
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 raw.split("\n").map((line, i) => `${String(i + 1).padStart(4)} │ ${line}`);
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
- pi.sendUserMessage(formatCommentMessage(payload, comment), {
45
- deliverAs: "followUp",
46
- streamingBehavior: "followUp" as any,
47
- } as any);
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
- function sanitizeTextInput(data: string): string {
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
- return normalized
14
- .replace(/\r\n?/g, "\n")
15
- .replace(/\n/g, " ")
16
- .replace(/\t/g, " ")
17
- .replace(CONTROL_CHARS, "");
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
- export function createTextInputBuffer(): TextInputBuffer {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tmustier/pi-files-widget",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "In-terminal file browser and viewer for Pi.",
5
5
  "license": "MIT",
6
6
  "author": "Thomas Mustier",
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 textInput = createTextInputBuffer();
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
- textInput.reset();
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
- state.content = loadFileContent(state.file.path, cwd, state.diffMode, hasChanges, width);
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
- const prompt = theme.fg("accent", `Comment: ${state.commentText}█`);
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, Key.enter)) {
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 = textInput.push(data);
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 = textInput.push(data);
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;