@fresh-editor/fresh-editor 0.2.12 → 0.2.13

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.
Files changed (51) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +10 -0
  3. package/package.json +1 -1
  4. package/plugins/audit_mode.ts +79 -58
  5. package/plugins/check-types.sh +1 -0
  6. package/plugins/clangd-lsp.ts +9 -6
  7. package/plugins/clangd_support.ts +12 -8
  8. package/plugins/code-tour.ts +15 -10
  9. package/plugins/config-schema.json +40 -3
  10. package/plugins/csharp_support.ts +15 -10
  11. package/plugins/css-lsp.ts +9 -6
  12. package/plugins/diagnostics_panel.ts +25 -18
  13. package/plugins/examples/README.md +1 -2
  14. package/plugins/examples/async_demo.ts +28 -28
  15. package/plugins/examples/bookmarks.ts +34 -32
  16. package/plugins/examples/buffer_query_demo.ts +20 -20
  17. package/plugins/examples/hello_world.ts +46 -10
  18. package/plugins/examples/virtual_buffer_demo.ts +16 -12
  19. package/plugins/find_references.ts +7 -5
  20. package/plugins/git_blame.ts +13 -9
  21. package/plugins/git_explorer.ts +9 -6
  22. package/plugins/git_find_file.ts +7 -5
  23. package/plugins/git_grep.ts +3 -2
  24. package/plugins/git_gutter.ts +15 -10
  25. package/plugins/git_log.ts +27 -18
  26. package/plugins/go-lsp.ts +9 -6
  27. package/plugins/html-lsp.ts +9 -6
  28. package/plugins/java-lsp.ts +9 -6
  29. package/plugins/json-lsp.ts +9 -6
  30. package/plugins/latex-lsp.ts +9 -6
  31. package/plugins/lib/finder.ts +1 -0
  32. package/plugins/lib/fresh.d.ts +139 -14
  33. package/plugins/live_grep.ts +3 -2
  34. package/plugins/markdown_compose.ts +33 -23
  35. package/plugins/markdown_source.ts +15 -10
  36. package/plugins/marksman-lsp.ts +9 -6
  37. package/plugins/merge_conflict.ts +33 -22
  38. package/plugins/odin-lsp.ts +9 -6
  39. package/plugins/path_complete.ts +3 -2
  40. package/plugins/pkg.ts +70 -48
  41. package/plugins/python-lsp.ts +9 -6
  42. package/plugins/rust-lsp.ts +102 -6
  43. package/plugins/search_replace.ts +32 -21
  44. package/plugins/templ-lsp.ts +9 -6
  45. package/plugins/test_i18n.ts +3 -2
  46. package/plugins/theme_editor.i18n.json +28 -14
  47. package/plugins/theme_editor.ts +1230 -495
  48. package/plugins/typescript-lsp.ts +9 -6
  49. package/plugins/vi_mode.ts +487 -297
  50. package/plugins/welcome.ts +9 -6
  51. package/plugins/zig-lsp.ts +9 -6
package/CHANGELOG.md CHANGED
@@ -1,5 +1,65 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.2.13
4
+
5
+ ### Features
6
+
7
+ * **Inline Diagnostics**: Diagnostic text displayed at end of lines, right-aligned, with version-aware staleness dropping. Disabled by default — enable "diagnostics inline text" in the Settings UI (#1175).
8
+
9
+ * **Hanging Line Wrap**: Wrapped continuation lines preserve the indentation of their parent logical line (#1169).
10
+
11
+ * **Theme Editor Redesign**: Virtual scrolling, mouse support, flicker-free inline styling. New "Inspect Theme at Cursor" command and Ctrl+Right-Click theme info popup.
12
+
13
+ * **Open File Jump**: `path:line[:col]` syntax in Open File prompt and Quick Open (#1081, #1149).
14
+
15
+ ### Improvements
16
+
17
+ * **Plugin API**: `registerHandler()` replacing `globalThis` pattern, `restartLspForLanguage`, process-limits for `registerLspServer`, async `reloadGrammars()`. Strict TypeScript across all plugins.
18
+
19
+ * **Load Plugin from Buffer**: Run and hot-reload plugins directly from an open buffer, with LSP support for plugin dev buffers.
20
+
21
+ * **Status Bar Toggle**: Command palette command and config option to show/hide the status bar.
22
+
23
+ * **LSP Environment Variables**: Pass environment variables to LSP server binaries via config (#1159).
24
+
25
+ * **LSP Language ID Overrides**: Configurable `language_id_overrides` in LSP server config.
26
+
27
+ * **Rust LSP Mode Switching**: Command palette action to switch between Full and Reduced Memory modes for rust-analyzer.
28
+
29
+ * **Signature Help Rendering**: Markdown rendering for signature help popups with hanging indent and paragraph spacing.
30
+
31
+ * **Non-Blocking Grammar Builds**: `SyntaxSet::build()` moved to a background thread. Buffered input events drained before render for CPU-constrained responsiveness.
32
+
33
+ * Disabled LSP start/restart commands for languages without LSP config (#1168).
34
+
35
+ ### Bug Fixes
36
+
37
+ * **LSP Bracket Paths**: Fixed LSP failing for file paths containing `[` or `]` (#953).
38
+
39
+ * **Search F3 Navigation**: Fixed F3 losing matches after viewport scroll (#1155).
40
+
41
+ * **Settings JSON Copy**: Fixed Ctrl+C not working in settings JSON editor (#1159).
42
+
43
+ * **Line Numbers on New Files**: Fixed line numbers showing when disabled in settings for newly opened files (#1181).
44
+
45
+ * **Client/Server Paste**: Fixed bracketed paste mode and terminal feature parity in client/server sessions (#1168).
46
+
47
+ * **Popup Selection**: Fixed popup text selection copying wrong text when lines wrap (#1170).
48
+
49
+ * **Suggestions Popup Border**: Fixed bottom border overwritten by status bar (#1174).
50
+
51
+ * **TSX/JSX Language ID**: Fixed wrong `languageId` sent to LSP for TSX/JSX files (#1174).
52
+
53
+ * **LSP Error Suppression**: Suppress ContentModified/ServerCancelled errors per LSP spec instead of logging as errors.
54
+
55
+ * **Semantic Tokens**: Skip degraded semantic token responses to preserve syntax highlighting.
56
+
57
+ * **Theme Save**: Fixed save failing when themes directory doesn't exist (#1180). Fixed saving incomplete theme files.
58
+
59
+ * **LSP Completion**: Fixed completion debounce, cleanup-on-disable, and popup positioning issues.
60
+
61
+ ---
62
+
3
63
  ## 0.2.12
4
64
 
5
65
  ### Features
package/README.md CHANGED
@@ -67,6 +67,7 @@ Or, pick your preferred method:
67
67
  | Debian/Ubuntu | [.deb](#debianubuntu-deb) |
68
68
  | Fedora/RHEL | [.rpm](#fedorarhelopensuse-rpm), [Terra](https://terra.fyralabs.com/) |
69
69
  | FreeBSD | [ports / pkg](https://www.freshports.org/editors/fresh) |
70
+ | Gentoo | [GURU](#gentoo-guru) |
70
71
  | Linux (any distro) | [AppImage](#appimage), [Flatpak](#flatpak) |
71
72
  | All platforms | [Pre-built binaries](#pre-built-binaries) |
72
73
  | npm | [npm / npx](#npm) |
@@ -142,6 +143,15 @@ curl -sL $(curl -s https://api.github.com/repos/sinelaw/fresh/releases/latest |
142
143
 
143
144
  Or download the `.rpm` file manually from the [releases page](https://github.com/sinelaw/fresh/releases).
144
145
 
146
+ ### Gentoo ([GURU](https://wiki.gentoo.org/wiki/Project:GURU))
147
+
148
+ Enable the repository as read in [Project:GURU/Information for End Users](https://wiki.gentoo.org/wiki/Project:GURU/Information_for_End_Users) then emerge the package:
149
+
150
+
151
+ ```bash
152
+ emerge --ask app-editors/fresh
153
+ ```
154
+
145
155
  ### AppImage
146
156
 
147
157
  Download the `.AppImage` file from the [releases page](https://github.com/sinelaw/fresh/releases) and run:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.2.12",
3
+ "version": "0.2.13",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -589,7 +589,7 @@ async function refreshReviewData() {
589
589
 
590
590
  // --- Actions ---
591
591
 
592
- globalThis.review_stage_hunk = async () => {
592
+ async function review_stage_hunk() {
593
593
  const props = editor.getTextPropertiesAtCursor(editor.getActiveBufferId());
594
594
  if (props.length > 0 && props[0].hunkId) {
595
595
  const id = props[0].hunkId as string;
@@ -598,9 +598,10 @@ globalThis.review_stage_hunk = async () => {
598
598
  if (h) h.status = 'staged';
599
599
  await updateReviewUI();
600
600
  }
601
- };
601
+ }
602
+ registerHandler("review_stage_hunk", review_stage_hunk);
602
603
 
603
- globalThis.review_discard_hunk = async () => {
604
+ async function review_discard_hunk() {
604
605
  const props = editor.getTextPropertiesAtCursor(editor.getActiveBufferId());
605
606
  if (props.length > 0 && props[0].hunkId) {
606
607
  const id = props[0].hunkId as string;
@@ -609,9 +610,10 @@ globalThis.review_discard_hunk = async () => {
609
610
  if (h) h.status = 'discarded';
610
611
  await updateReviewUI();
611
612
  }
612
- };
613
+ }
614
+ registerHandler("review_discard_hunk", review_discard_hunk);
613
615
 
614
- globalThis.review_undo_action = async () => {
616
+ async function review_undo_action() {
615
617
  const props = editor.getTextPropertiesAtCursor(editor.getActiveBufferId());
616
618
  if (props.length > 0 && props[0].hunkId) {
617
619
  const id = props[0].hunkId as string;
@@ -620,25 +622,29 @@ globalThis.review_undo_action = async () => {
620
622
  if (h) h.status = 'pending';
621
623
  await updateReviewUI();
622
624
  }
623
- };
625
+ }
626
+ registerHandler("review_undo_action", review_undo_action);
624
627
 
625
- globalThis.review_next_hunk = () => {
628
+ function review_next_hunk() {
626
629
  const bid = editor.getActiveBufferId();
627
630
  const props = editor.getTextPropertiesAtCursor(bid);
628
631
  let cur = -1;
629
632
  if (props.length > 0 && props[0].index !== undefined) cur = props[0].index as number;
630
633
  if (cur + 1 < state.hunks.length) editor.setBufferCursor(bid, state.hunks[cur + 1].byteOffset);
631
- };
634
+ }
635
+ registerHandler("review_next_hunk", review_next_hunk);
632
636
 
633
- globalThis.review_prev_hunk = () => {
637
+ function review_prev_hunk() {
634
638
  const bid = editor.getActiveBufferId();
635
639
  const props = editor.getTextPropertiesAtCursor(bid);
636
640
  let cur = state.hunks.length;
637
641
  if (props.length > 0 && props[0].index !== undefined) cur = props[0].index as number;
638
642
  if (cur - 1 >= 0) editor.setBufferCursor(bid, state.hunks[cur - 1].byteOffset);
639
- };
643
+ }
644
+ registerHandler("review_prev_hunk", review_prev_hunk);
640
645
 
641
- globalThis.review_refresh = () => { refreshReviewData(); };
646
+ function review_refresh() { refreshReviewData(); }
647
+ registerHandler("review_refresh", review_refresh);
642
648
 
643
649
  let activeDiffViewState: { lSplit: number, rSplit: number } | null = null;
644
650
 
@@ -659,7 +665,7 @@ function findLineForByte(lineByteOffsets: number[], topByte: number): number {
659
665
  return low;
660
666
  }
661
667
 
662
- globalThis.on_viewport_changed = (data: any) => {
668
+ function on_viewport_changed(data: any) {
663
669
  // This handler is now a no-op - scroll sync is handled by the core
664
670
  // using the anchor-based ScrollSyncGroup system.
665
671
  // Keeping the handler for backward compatibility if core sync fails.
@@ -681,7 +687,8 @@ globalThis.on_viewport_changed = (data: any) => {
681
687
  const targetByte = oldLineByteOffsets[Math.min(lineNum, oldLineByteOffsets.length - 1)];
682
688
  (editor as any).setSplitScroll(oldSplitId, targetByte);
683
689
  }
684
- };
690
+ }
691
+ registerHandler("on_viewport_changed", on_viewport_changed);
685
692
 
686
693
  /**
687
694
  * Represents an aligned line pair for side-by-side diff display
@@ -1017,7 +1024,7 @@ interface CompositeDiffState {
1017
1024
 
1018
1025
  let activeCompositeDiffState: CompositeDiffState | null = null;
1019
1026
 
1020
- globalThis.review_drill_down = async () => {
1027
+ async function review_drill_down() {
1021
1028
  const bid = editor.getActiveBufferId();
1022
1029
  const props = editor.getTextPropertiesAtCursor(bid);
1023
1030
  if (props.length > 0 && props[0].hunkId) {
@@ -1048,10 +1055,8 @@ globalThis.review_drill_down = async () => {
1048
1055
  const oldContent = gitShow.stdout;
1049
1056
 
1050
1057
  // Read new file content (use absolute path for readFile)
1051
- let newContent: string;
1052
- try {
1053
- newContent = await editor.readFile(absoluteFilePath);
1054
- } catch (e) {
1058
+ const newContent = await editor.readFile(absoluteFilePath);
1059
+ if (newContent === null) {
1055
1060
  editor.setStatus(editor.t("status.failed_new_version"));
1056
1061
  return;
1057
1062
  }
@@ -1185,7 +1190,8 @@ globalThis.review_drill_down = async () => {
1185
1190
 
1186
1191
  editor.setStatus(editor.t("status.diff_summary", { added: String(addedCount), removed: String(removedCount), modified: String(modifiedCount) }));
1187
1192
  }
1188
- };
1193
+ }
1194
+ registerHandler("review_drill_down", review_drill_down);
1189
1195
 
1190
1196
  // Define the diff-view mode - inherits from "normal" for all standard navigation/selection/copy
1191
1197
  // Only adds diff-specific keybindings (close, hunk navigation)
@@ -1237,7 +1243,7 @@ function getCurrentLineInfo(): PendingCommentInfo | null {
1237
1243
  // Pending prompt state for event-based prompt handling
1238
1244
  let pendingCommentInfo: PendingCommentInfo | null = null;
1239
1245
 
1240
- globalThis.review_add_comment = async () => {
1246
+ async function review_add_comment() {
1241
1247
  const info = getCurrentLineInfo();
1242
1248
  if (!info) {
1243
1249
  editor.setStatus(editor.t("status.no_hunk_selected"));
@@ -1257,10 +1263,11 @@ globalThis.review_add_comment = async () => {
1257
1263
  lineRef = `L${info.oldLine}`;
1258
1264
  }
1259
1265
  editor.startPrompt(editor.t("prompt.comment", { line: lineRef }), "review-comment");
1260
- };
1266
+ }
1267
+ registerHandler("review_add_comment", review_add_comment);
1261
1268
 
1262
1269
  // Prompt event handlers
1263
- globalThis.on_review_prompt_confirm = (args: { prompt_type: string; input: string }): boolean => {
1270
+ function on_review_prompt_confirm(args: { prompt_type: string; input: string }): boolean {
1264
1271
  if (args.prompt_type !== "review-comment") {
1265
1272
  return true; // Not our prompt
1266
1273
  }
@@ -1292,21 +1299,23 @@ globalThis.on_review_prompt_confirm = (args: { prompt_type: string; input: strin
1292
1299
  }
1293
1300
  pendingCommentInfo = null;
1294
1301
  return true;
1295
- };
1302
+ }
1303
+ registerHandler("on_review_prompt_confirm", on_review_prompt_confirm);
1296
1304
 
1297
- globalThis.on_review_prompt_cancel = (args: { prompt_type: string }): boolean => {
1305
+ function on_review_prompt_cancel(args: { prompt_type: string }): boolean {
1298
1306
  if (args.prompt_type === "review-comment") {
1299
1307
  pendingCommentInfo = null;
1300
1308
  editor.setStatus(editor.t("status.comment_cancelled"));
1301
1309
  }
1302
1310
  return true;
1303
- };
1311
+ }
1312
+ registerHandler("on_review_prompt_cancel", on_review_prompt_cancel);
1304
1313
 
1305
1314
  // Register prompt event handlers
1306
1315
  editor.on("prompt_confirmed", "on_review_prompt_confirm");
1307
1316
  editor.on("prompt_cancelled", "on_review_prompt_cancel");
1308
1317
 
1309
- globalThis.review_approve_hunk = async () => {
1318
+ async function review_approve_hunk() {
1310
1319
  const hunkId = getCurrentHunkId();
1311
1320
  if (!hunkId) return;
1312
1321
  const h = state.hunks.find(x => x.id === hunkId);
@@ -1315,9 +1324,10 @@ globalThis.review_approve_hunk = async () => {
1315
1324
  await updateReviewUI();
1316
1325
  editor.setStatus(editor.t("status.hunk_approved"));
1317
1326
  }
1318
- };
1327
+ }
1328
+ registerHandler("review_approve_hunk", review_approve_hunk);
1319
1329
 
1320
- globalThis.review_reject_hunk = async () => {
1330
+ async function review_reject_hunk() {
1321
1331
  const hunkId = getCurrentHunkId();
1322
1332
  if (!hunkId) return;
1323
1333
  const h = state.hunks.find(x => x.id === hunkId);
@@ -1326,9 +1336,10 @@ globalThis.review_reject_hunk = async () => {
1326
1336
  await updateReviewUI();
1327
1337
  editor.setStatus(editor.t("status.hunk_rejected"));
1328
1338
  }
1329
- };
1339
+ }
1340
+ registerHandler("review_reject_hunk", review_reject_hunk);
1330
1341
 
1331
- globalThis.review_needs_changes = async () => {
1342
+ async function review_needs_changes() {
1332
1343
  const hunkId = getCurrentHunkId();
1333
1344
  if (!hunkId) return;
1334
1345
  const h = state.hunks.find(x => x.id === hunkId);
@@ -1337,9 +1348,10 @@ globalThis.review_needs_changes = async () => {
1337
1348
  await updateReviewUI();
1338
1349
  editor.setStatus(editor.t("status.hunk_needs_changes"));
1339
1350
  }
1340
- };
1351
+ }
1352
+ registerHandler("review_needs_changes", review_needs_changes);
1341
1353
 
1342
- globalThis.review_question_hunk = async () => {
1354
+ async function review_question_hunk() {
1343
1355
  const hunkId = getCurrentHunkId();
1344
1356
  if (!hunkId) return;
1345
1357
  const h = state.hunks.find(x => x.id === hunkId);
@@ -1348,9 +1360,10 @@ globalThis.review_question_hunk = async () => {
1348
1360
  await updateReviewUI();
1349
1361
  editor.setStatus(editor.t("status.hunk_question"));
1350
1362
  }
1351
- };
1363
+ }
1364
+ registerHandler("review_question_hunk", review_question_hunk);
1352
1365
 
1353
- globalThis.review_clear_status = async () => {
1366
+ async function review_clear_status() {
1354
1367
  const hunkId = getCurrentHunkId();
1355
1368
  if (!hunkId) return;
1356
1369
  const h = state.hunks.find(x => x.id === hunkId);
@@ -1359,17 +1372,19 @@ globalThis.review_clear_status = async () => {
1359
1372
  await updateReviewUI();
1360
1373
  editor.setStatus(editor.t("status.hunk_status_cleared"));
1361
1374
  }
1362
- };
1375
+ }
1376
+ registerHandler("review_clear_status", review_clear_status);
1363
1377
 
1364
- globalThis.review_set_overall_feedback = async () => {
1378
+ async function review_set_overall_feedback() {
1365
1379
  const text = await editor.prompt(editor.t("prompt.overall_feedback"), state.overallFeedback || "");
1366
1380
  if (text !== null) {
1367
1381
  state.overallFeedback = text.trim();
1368
1382
  editor.setStatus(text.trim() ? editor.t("status.feedback_set") : editor.t("status.feedback_cleared"));
1369
1383
  }
1370
- };
1384
+ }
1385
+ registerHandler("review_set_overall_feedback", review_set_overall_feedback);
1371
1386
 
1372
- globalThis.review_export_session = async () => {
1387
+ async function review_export_session() {
1373
1388
  const cwd = editor.getCwd();
1374
1389
  const reviewDir = editor.pathJoin(cwd, ".review");
1375
1390
 
@@ -1440,9 +1455,10 @@ globalThis.review_export_session = async () => {
1440
1455
  const filePath = editor.pathJoin(reviewDir, "session.md");
1441
1456
  await editor.writeFile(filePath, md);
1442
1457
  editor.setStatus(editor.t("status.exported", { path: filePath }));
1443
- };
1458
+ }
1459
+ registerHandler("review_export_session", review_export_session);
1444
1460
 
1445
- globalThis.review_export_json = async () => {
1461
+ async function review_export_json() {
1446
1462
  const cwd = editor.getCwd();
1447
1463
  const reviewDir = editor.pathJoin(cwd, ".review");
1448
1464
  // writeFile creates parent directories
@@ -1476,9 +1492,10 @@ globalThis.review_export_json = async () => {
1476
1492
  const filePath = editor.pathJoin(reviewDir, "session.json");
1477
1493
  await editor.writeFile(filePath, JSON.stringify(session, null, 2));
1478
1494
  editor.setStatus(editor.t("status.exported", { path: filePath }));
1479
- };
1495
+ }
1496
+ registerHandler("review_export_json", review_export_json);
1480
1497
 
1481
- globalThis.start_review_diff = async () => {
1498
+ async function start_review_diff() {
1482
1499
  editor.setStatus(editor.t("status.generating"));
1483
1500
  editor.setContext("review-mode", true);
1484
1501
 
@@ -1497,27 +1514,31 @@ globalThis.start_review_diff = async () => {
1497
1514
  editor.setStatus(editor.t("status.review_summary", { count: String(state.hunks.length) }));
1498
1515
  editor.on("buffer_activated", "on_review_buffer_activated");
1499
1516
  editor.on("buffer_closed", "on_review_buffer_closed");
1500
- };
1517
+ }
1518
+ registerHandler("start_review_diff", start_review_diff);
1501
1519
 
1502
- globalThis.stop_review_diff = () => {
1520
+ function stop_review_diff() {
1503
1521
  state.reviewBufferId = null;
1504
1522
  editor.setContext("review-mode", false);
1505
1523
  editor.off("buffer_activated", "on_review_buffer_activated");
1506
1524
  editor.off("buffer_closed", "on_review_buffer_closed");
1507
1525
  editor.setStatus(editor.t("status.stopped"));
1508
- };
1526
+ }
1527
+ registerHandler("stop_review_diff", stop_review_diff);
1509
1528
 
1510
1529
 
1511
- globalThis.on_review_buffer_activated = (data: any) => {
1530
+ function on_review_buffer_activated(data: any) {
1512
1531
  if (data.buffer_id === state.reviewBufferId) refreshReviewData();
1513
- };
1532
+ }
1533
+ registerHandler("on_review_buffer_activated", on_review_buffer_activated);
1514
1534
 
1515
- globalThis.on_review_buffer_closed = (data: any) => {
1516
- if (data.buffer_id === state.reviewBufferId) globalThis.stop_review_diff();
1517
- };
1535
+ function on_review_buffer_closed(data: any) {
1536
+ if (data.buffer_id === state.reviewBufferId) stop_review_diff();
1537
+ }
1538
+ registerHandler("on_review_buffer_closed", on_review_buffer_closed);
1518
1539
 
1519
1540
  // Side-by-side diff for current file using composite buffers
1520
- globalThis.side_by_side_diff_current_file = async () => {
1541
+ async function side_by_side_diff_current_file() {
1521
1542
  const bid = editor.getActiveBufferId();
1522
1543
  const absolutePath = editor.getBufferPath(bid);
1523
1544
 
@@ -1613,10 +1634,8 @@ globalThis.side_by_side_diff_current_file = async () => {
1613
1634
  const oldContent = gitShow.stdout;
1614
1635
 
1615
1636
  // Read new file content (use absolute path for readFile)
1616
- let newContent: string;
1617
- try {
1618
- newContent = await editor.readFile(absolutePath);
1619
- } catch (e) {
1637
+ const newContent = await editor.readFile(absolutePath);
1638
+ if (newContent === null) {
1620
1639
  editor.setStatus(editor.t("status.failed_new_version"));
1621
1640
  return;
1622
1641
  }
@@ -1740,7 +1759,8 @@ globalThis.side_by_side_diff_current_file = async () => {
1740
1759
  const modifiedCount = Math.min(addedCount, removedCount);
1741
1760
 
1742
1761
  editor.setStatus(editor.t("status.diff_summary", { added: String(addedCount), removed: String(removedCount), modified: String(modifiedCount) }));
1743
- };
1762
+ }
1763
+ registerHandler("side_by_side_diff_current_file", side_by_side_diff_current_file);
1744
1764
 
1745
1765
  // Register Modes and Commands
1746
1766
  editor.registerCommand("%cmd.review_diff", "%cmd.review_diff_desc", "start_review_diff", null);
@@ -1760,7 +1780,7 @@ editor.registerCommand("%cmd.export_markdown", "%cmd.export_markdown_desc", "rev
1760
1780
  editor.registerCommand("%cmd.export_json", "%cmd.export_json_desc", "review_export_json", "review-mode");
1761
1781
 
1762
1782
  // Handler for when buffers are closed - cleans up scroll sync groups and composite buffers
1763
- globalThis.on_buffer_closed = (data: any) => {
1783
+ function on_buffer_closed(data: any) {
1764
1784
  // If one of the diff view buffers is closed, clean up the scroll sync group
1765
1785
  if (activeSideBySideState) {
1766
1786
  if (data.buffer_id === activeSideBySideState.oldBufferId ||
@@ -1787,7 +1807,8 @@ globalThis.on_buffer_closed = (data: any) => {
1787
1807
  activeCompositeDiffState = null;
1788
1808
  }
1789
1809
  }
1790
- };
1810
+ }
1811
+ registerHandler("on_buffer_closed", on_buffer_closed);
1791
1812
 
1792
1813
  editor.on("buffer_closed", "on_buffer_closed");
1793
1814
 
@@ -22,6 +22,7 @@ ERRORS=0
22
22
  for file in "${FILES[@]}"; do
23
23
  if ! npx -p typescript tsc \
24
24
  --noEmit \
25
+ --strict \
25
26
  --target esnext \
26
27
  --moduleResolution node \
27
28
  --lib esnext,dom \
@@ -54,7 +54,7 @@ let clangdLspError: {
54
54
  /**
55
55
  * Handle LSP server errors for C/C++
56
56
  */
57
- globalThis.on_clangd_lsp_server_error = function (
57
+ function on_clangd_lsp_server_error(
58
58
  data: LspServerErrorData
59
59
  ): void {
60
60
  // Only handle C/C++ language errors
@@ -79,7 +79,8 @@ globalThis.on_clangd_lsp_server_error = function (
79
79
  } else {
80
80
  editor.setStatus(`C/C++ LSP error: ${data.message}`);
81
81
  }
82
- };
82
+ }
83
+ registerHandler("on_clangd_lsp_server_error", on_clangd_lsp_server_error);
83
84
 
84
85
  // Register hook for LSP server errors
85
86
  editor.on("lsp_server_error", "on_clangd_lsp_server_error");
@@ -87,7 +88,7 @@ editor.on("lsp_server_error", "on_clangd_lsp_server_error");
87
88
  /**
88
89
  * Handle status bar click when there's a C/C++ LSP error
89
90
  */
90
- globalThis.on_clangd_lsp_status_clicked = function (
91
+ function on_clangd_lsp_status_clicked(
91
92
  data: LspStatusClickedData
92
93
  ): void {
93
94
  // Only handle C/C++ language clicks when there's an error
@@ -110,7 +111,8 @@ globalThis.on_clangd_lsp_status_clicked = function (
110
111
  { id: "dismiss", label: "Dismiss (ESC)" },
111
112
  ],
112
113
  });
113
- };
114
+ }
115
+ registerHandler("on_clangd_lsp_status_clicked", on_clangd_lsp_status_clicked);
114
116
 
115
117
  // Register hook for status bar clicks
116
118
  editor.on("lsp_status_clicked", "on_clangd_lsp_status_clicked");
@@ -118,7 +120,7 @@ editor.on("lsp_status_clicked", "on_clangd_lsp_status_clicked");
118
120
  /**
119
121
  * Handle action popup results for C/C++ LSP help
120
122
  */
121
- globalThis.on_clangd_lsp_action_result = function (
123
+ function on_clangd_lsp_action_result(
122
124
  data: ActionPopupResultData
123
125
  ): void {
124
126
  // Only handle our popup
@@ -160,7 +162,8 @@ globalThis.on_clangd_lsp_action_result = function (
160
162
  default:
161
163
  editor.debug(`clangd-lsp: Unknown action: ${data.action_id}`);
162
164
  }
163
- };
165
+ }
166
+ registerHandler("on_clangd_lsp_action_result", on_clangd_lsp_action_result);
164
167
 
165
168
  // Register hook for action popup results
166
169
  editor.on("action_popup_result", "on_clangd_lsp_action_result");
@@ -50,7 +50,7 @@ function setClangdStatus(message: string): void {
50
50
  editor.setStatus(message);
51
51
  }
52
52
 
53
- globalThis.clangdSwitchSourceHeader = async function(): Promise<void> {
53
+ async function clangdSwitchSourceHeader() : Promise<void> {
54
54
  const bufferId = editor.getActiveBufferId();
55
55
  const path = editor.getBufferPath(bufferId);
56
56
  if (!path) {
@@ -82,7 +82,8 @@ globalThis.clangdSwitchSourceHeader = async function(): Promise<void> {
82
82
  setClangdStatus(editor.t("status.switch_failed", { error: String(err) }));
83
83
  editor.debug(`clangdSwitchSourceHeader error: ${err}`);
84
84
  }
85
- };
85
+ }
86
+ registerHandler("clangdSwitchSourceHeader", clangdSwitchSourceHeader);
86
87
 
87
88
  const projectPanel = new PanelManager(editor, "Clangd project setup", "clangd-project-setup");
88
89
 
@@ -226,7 +227,7 @@ function analyzeProject(root: string | null) {
226
227
  return status;
227
228
  }
228
229
 
229
- globalThis.clangdProjectSetup = async function (): Promise<void> {
230
+ async function clangdProjectSetup() : Promise<void> {
230
231
  const projectRoot = detectProjectRoot();
231
232
  const summary = analyzeProject(projectRoot);
232
233
  const entries = summary.map((line) => ({
@@ -237,7 +238,8 @@ globalThis.clangdProjectSetup = async function (): Promise<void> {
237
238
  entries,
238
239
  ratio: 0.3,
239
240
  });
240
- };
241
+ }
242
+ registerHandler("clangdProjectSetup", clangdProjectSetup);
241
243
 
242
244
  editor.registerCommand(
243
245
  "%cmd.project_setup",
@@ -246,7 +248,7 @@ editor.registerCommand(
246
248
  null
247
249
  );
248
250
 
249
- globalThis.clangdOpenProjectConfig = function(): void {
251
+ function clangdOpenProjectConfig() : void {
250
252
  const bufferId = editor.getActiveBufferId();
251
253
  const targets = new Set<string>();
252
254
  const bufferPath = editor.getBufferPath(bufferId);
@@ -273,7 +275,8 @@ globalThis.clangdOpenProjectConfig = function(): void {
273
275
  if (!opened) {
274
276
  setClangdStatus(editor.t("status.config_not_found"));
275
277
  }
276
- };
278
+ }
279
+ registerHandler("clangdOpenProjectConfig", clangdOpenProjectConfig);
277
280
 
278
281
  editor.registerCommand(
279
282
  "%cmd.switch_source_header",
@@ -290,7 +293,7 @@ editor.registerCommand(
290
293
  );
291
294
 
292
295
 
293
- globalThis.onClangdCustomNotification = function(payload: {
296
+ function onClangdCustomNotification(payload: {
294
297
  language: string;
295
298
  method: string;
296
299
  params: Record<string, unknown> | null;
@@ -311,6 +314,7 @@ globalThis.onClangdCustomNotification = function(payload: {
311
314
  const usage = (payload.params as any).used ?? "unknown";
312
315
  editor.debug(`Clangd memory usage: ${usage}`);
313
316
  }
314
- };
317
+ }
318
+ registerHandler("onClangdCustomNotification", onClangdCustomNotification);
315
319
 
316
320
  editor.on("lsp/custom_notification", "onClangdCustomNotification");
@@ -123,7 +123,7 @@ interface ActionPopupResultData {
123
123
  action_id: string;
124
124
  }
125
125
 
126
- globalThis.tour_on_action_popup_result = function (data: ActionPopupResultData): void {
126
+ function tour_on_action_popup_result(data: ActionPopupResultData) : void {
127
127
  if (data.popup_id !== TOUR_POPUP_ID) return;
128
128
 
129
129
  switch (data.action_id) {
@@ -137,7 +137,8 @@ globalThis.tour_on_action_popup_result = function (data: ActionPopupResultData):
137
137
  exitTour();
138
138
  break;
139
139
  }
140
- };
140
+ }
141
+ registerHandler("tour_on_action_popup_result", tour_on_action_popup_result);
141
142
 
142
143
  // ============================================================================
143
144
  // Overlay Rendering
@@ -337,26 +338,30 @@ async function prevStep(): Promise<void> {
337
338
  // Command Handlers
338
339
  // ============================================================================
339
340
 
340
- globalThis.tour_load = async function (): Promise<void> {
341
+ async function tour_load() : Promise<void> {
341
342
  // Prompt for tour file
342
343
  const result = await editor.prompt("Enter tour file path:", ".fresh-tour.json");
343
344
 
344
345
  if (result) {
345
346
  await loadTour(result);
346
347
  }
347
- };
348
+ }
349
+ registerHandler("tour_load", tour_load);
348
350
 
349
- globalThis.tour_next = async function (): Promise<void> {
351
+ async function tour_next() : Promise<void> {
350
352
  await nextStep();
351
- };
353
+ }
354
+ registerHandler("tour_next", tour_next);
352
355
 
353
- globalThis.tour_prev = async function (): Promise<void> {
356
+ async function tour_prev() : Promise<void> {
354
357
  await prevStep();
355
- };
358
+ }
359
+ registerHandler("tour_prev", tour_prev);
356
360
 
357
- globalThis.tour_exit = function (): void {
361
+ function tour_exit() : void {
358
362
  exitTour();
359
- };
363
+ }
364
+ registerHandler("tour_exit", tour_exit);
360
365
 
361
366
  // ============================================================================
362
367
  // Registration