@fresh-editor/fresh-editor 0.2.12 → 0.2.14

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 +72 -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
@@ -16,6 +16,23 @@
16
16
  * Plugins must call this at the top of their file to get a scoped editor object.
17
17
  */
18
18
  declare function getEditor(): EditorAPI;
19
+ /**
20
+ * Register a function as a named handler on the global scope.
21
+ *
22
+ * Handler functions registered this way can be referenced by name in
23
+ * `editor.registerCommand()`, `editor.on()`, and mode keybindings.
24
+ *
25
+ * The `fn` parameter is typed as `Function` because the runtime passes
26
+ * different argument shapes depending on the caller: command handlers
27
+ * receive no arguments, event handlers receive an event-specific data
28
+ * object (e.g. `{ buffer_id: number }`), and prompt handlers receive
29
+ * `{ prompt_type: string, input: string }`. Type-annotate your handler
30
+ * parameters to match the event you are handling.
31
+ *
32
+ * @param name - Handler name (referenced by registerCommand, on, etc.)
33
+ * @param fn - The handler function
34
+ */
35
+ declare function registerHandler(name: string, fn: Function): void;
19
36
  /** Handle for a cancellable async operation */
20
37
  interface ProcessHandle<T> extends PromiseLike<T> {
21
38
  /** Promise that resolves to the result when complete */
@@ -36,6 +53,14 @@ type TextPropertyEntry = {
36
53
  * Optional properties attached to this text (e.g., file path, line number)
37
54
  */
38
55
  properties?: Record<string, unknown>;
56
+ /**
57
+ * Optional whole-entry styling
58
+ */
59
+ style?: Partial<OverlayOptions>;
60
+ /**
61
+ * Optional sub-range styling within this entry
62
+ */
63
+ inlineOverlays?: Array<InlineOverlay>;
39
64
  };
40
65
  type TsCompositeLayoutConfig = {
41
66
  /**
@@ -45,7 +70,7 @@ type TsCompositeLayoutConfig = {
45
70
  /**
46
71
  * Width ratios for side-by-side (e.g., [0.5, 0.5])
47
72
  */
48
- ratios: Array<number> | null;
73
+ ratios?: Array<number>;
49
74
  /**
50
75
  * Show separator between panes
51
76
  */
@@ -53,7 +78,7 @@ type TsCompositeLayoutConfig = {
53
78
  /**
54
79
  * Spacing for stacked layout
55
80
  */
56
- spacing: number | null;
81
+ spacing?: number;
57
82
  };
58
83
  type TsCompositeSourceConfig = {
59
84
  /**
@@ -78,19 +103,19 @@ type TsCompositePaneStyle = {
78
103
  * Background color for added lines (RGB)
79
104
  * Using [u8; 3] instead of (u8, u8, u8) for better rquickjs_serde compatibility
80
105
  */
81
- addBg: [number, number, number] | null;
106
+ addBg?: [number, number, number];
82
107
  /**
83
108
  * Background color for removed lines (RGB)
84
109
  */
85
- removeBg: [number, number, number] | null;
110
+ removeBg?: [number, number, number];
86
111
  /**
87
112
  * Background color for modified lines (RGB)
88
113
  */
89
- modifyBg: [number, number, number] | null;
114
+ modifyBg?: [number, number, number];
90
115
  /**
91
116
  * Gutter style: "line-numbers", "diff-markers", "both", or "none"
92
117
  */
93
- gutterStyle: string | null;
118
+ gutterStyle?: string;
94
119
  };
95
120
  type TsCompositeHunk = {
96
121
  /**
@@ -158,11 +183,11 @@ type LayoutHints = {
158
183
  /**
159
184
  * Optional compose width for centering/wrapping
160
185
  */
161
- composeWidth: number | null;
186
+ composeWidth?: number;
162
187
  /**
163
188
  * Optional column guides for aligned tables
164
189
  */
165
- columnGuides: Array<number> | null;
190
+ columnGuides?: Array<number>;
166
191
  };
167
192
  type ViewTokenWire = {
168
193
  /**
@@ -386,6 +411,20 @@ type FormatterPackConfig = {
386
411
  */
387
412
  args: Array<string>;
388
413
  };
414
+ type ProcessLimitsPackConfig = {
415
+ /**
416
+ * Maximum memory usage as percentage of total system memory (null = no limit)
417
+ */
418
+ maxMemoryPercent: number | null;
419
+ /**
420
+ * Maximum CPU usage as percentage of total CPU (null = no limit)
421
+ */
422
+ maxCpuPercent: number | null;
423
+ /**
424
+ * Enable resource limiting
425
+ */
426
+ enabled: boolean | null;
427
+ };
389
428
  type TerminalResult = {
390
429
  /**
391
430
  * The created buffer ID (for use with setSplitBuffer, etc.)
@@ -431,6 +470,61 @@ type CursorInfo = {
431
470
  end: number;
432
471
  } | null;
433
472
  };
473
+ type OverlayOptions = {
474
+ /**
475
+ * Foreground color - RGB array or theme key string
476
+ */
477
+ fg?: OverlayColorSpec | null;
478
+ /**
479
+ * Background color - RGB array or theme key string
480
+ */
481
+ bg?: OverlayColorSpec | null;
482
+ /**
483
+ * Whether to render with underline
484
+ */
485
+ underline: boolean;
486
+ /**
487
+ * Whether to render in bold
488
+ */
489
+ bold: boolean;
490
+ /**
491
+ * Whether to render in italic
492
+ */
493
+ italic: boolean;
494
+ /**
495
+ * Whether to render with strikethrough
496
+ */
497
+ strikethrough: boolean;
498
+ /**
499
+ * Whether to extend background color to end of line
500
+ */
501
+ extendToLineEnd: boolean;
502
+ /**
503
+ * Optional URL for OSC 8 terminal hyperlinks.
504
+ * When set, the overlay text becomes a clickable hyperlink in terminals
505
+ * that support OSC 8 escape sequences.
506
+ */
507
+ url?: string | null;
508
+ };
509
+ type OverlayColorSpec = [number, number, number] | string;
510
+ type InlineOverlay = {
511
+ /**
512
+ * Start byte offset within the entry's text
513
+ */
514
+ start: number;
515
+ /**
516
+ * End byte offset within the entry's text (exclusive)
517
+ */
518
+ end: number;
519
+ /**
520
+ * Styling options for this range
521
+ */
522
+ style: Partial<OverlayOptions>;
523
+ /**
524
+ * Optional properties for this sub-range (e.g., click target metadata)
525
+ */
526
+ properties?: Record<string, any>;
527
+ };
434
528
  type BackgroundProcessResult = {
435
529
  /**
436
530
  * Unique process ID for later reference
@@ -621,6 +715,10 @@ type LspServerPackConfig = {
621
715
  * LSP initialization options
622
716
  */
623
717
  initializationOptions: Record<string, unknown> | null;
718
+ /**
719
+ * Process resource limits (memory and CPU)
720
+ */
721
+ processLimits: ProcessLimitsPackConfig | null;
624
722
  };
625
723
  type SpawnResult = {
626
724
  /**
@@ -683,10 +781,16 @@ interface EditorAPI {
683
781
  copyToClipboard(text: string): void;
684
782
  setClipboard(text: string): void;
685
783
  /**
686
- * Register a command - reads plugin name from __pluginName__ global
687
- * context is optional - can be omitted, null, undefined, or a string
784
+ * Register a command in the command palette (Ctrl+P).
785
+ *
786
+ * Usually you should omit `context` so the command is always visible.
787
+ * If provided, the command is **hidden** unless your plugin has activated
788
+ * that context with `editor.setContext(name, true)` or the focused buffer's
789
+ * virtual mode (from `defineMode()`) matches. This is for plugin-defined
790
+ * contexts only (e.g. `"tour-active"`, `"review-mode"`), not built-in
791
+ * editor modes.
688
792
  */
689
- registerCommand(name: string, description: string, handlerName: string, context?: unknown): boolean;
793
+ registerCommand(name: string, description: string, handlerName: string, context?: string | null): boolean;
690
794
  /**
691
795
  * Unregister a command by name
692
796
  */
@@ -899,6 +1003,10 @@ interface EditorAPI {
899
1003
  */
900
1004
  reloadThemes(): void;
901
1005
  /**
1006
+ * Reload theme registry and apply a theme atomically
1007
+ */
1008
+ reloadAndApplyTheme(themeName: string): void;
1009
+ /**
902
1010
  * Register a TextMate grammar file for a language
903
1011
  * The grammar will be pending until reload_grammars() is called
904
1012
  */
@@ -912,10 +1020,11 @@ interface EditorAPI {
912
1020
  */
913
1021
  registerLspServer(language: string, config: LspServerPackConfig): boolean;
914
1022
  /**
915
- * Reload the grammar registry to apply registered grammars
916
- * Call this after registering one or more grammars
1023
+ * Reload the grammar registry to apply registered grammars (async)
1024
+ * Call this after registering one or more grammars.
1025
+ * Returns a Promise that resolves when the grammar rebuild completes.
917
1026
  */
918
- reloadGrammars(): void;
1027
+ reloadGrammars(): Promise<void>;
919
1028
  /**
920
1029
  * Get config directory path
921
1030
  */
@@ -941,6 +1050,18 @@ interface EditorAPI {
941
1050
  */
942
1051
  deleteTheme(name: string): boolean;
943
1052
  /**
1053
+ * Get theme data (JSON) by name from the in-memory cache
1054
+ */
1055
+ getThemeData(name: string): unknown;
1056
+ /**
1057
+ * Save a theme file to the user themes directory, returns the saved path
1058
+ */
1059
+ saveThemeFile(name: string, content: string): string;
1060
+ /**
1061
+ * Check if a user theme file exists
1062
+ */
1063
+ themeFileExists(name: string): boolean;
1064
+ /**
944
1065
  * Get file stat information
945
1066
  */
946
1067
  fileStat(path: string): unknown;
@@ -1223,6 +1344,10 @@ interface EditorAPI {
1223
1344
  */
1224
1345
  disableLspForLanguage(language: string): boolean;
1225
1346
  /**
1347
+ * Restart LSP server for a specific language
1348
+ */
1349
+ restartLspForLanguage(language: string): boolean;
1350
+ /**
1226
1351
  * Set the workspace root URI for a specific language's LSP server
1227
1352
  * This allows plugins to specify project roots (e.g., directory containing .csproj)
1228
1353
  */
@@ -75,7 +75,7 @@ async function searchWithRipgrep(query: string): Promise<GrepMatch[]> {
75
75
  }
76
76
 
77
77
  // Start live grep
78
- globalThis.start_live_grep = function (): void {
78
+ function start_live_grep() : void {
79
79
  finder.prompt({
80
80
  title: editor.t("prompt.live_grep"),
81
81
  source: {
@@ -85,7 +85,8 @@ globalThis.start_live_grep = function (): void {
85
85
  minQueryLength: 2,
86
86
  },
87
87
  });
88
- };
88
+ }
89
+ registerHandler("start_live_grep", start_live_grep);
89
90
 
90
91
  // Register command
91
92
  editor.registerCommand(
@@ -390,7 +390,7 @@ function enableMarkdownCompose(bufferId: number): void {
390
390
  editor.setLineWrap(bufferId, null, true);
391
391
 
392
392
  // Set layout hints for centered margins
393
- editor.setLayoutHints(bufferId, null, { composeWidth: config.composeWidth });
393
+ editor.setLayoutHints(bufferId, null, { composeWidth: config.composeWidth ?? undefined });
394
394
 
395
395
  // Trigger a refresh so lines_changed hooks fire for visible content
396
396
  editor.refreshLines(bufferId);
@@ -421,7 +421,7 @@ function disableMarkdownCompose(bufferId: number): void {
421
421
  }
422
422
 
423
423
  // Toggle markdown compose mode for current buffer
424
- globalThis.markdownToggleCompose = function(): void {
424
+ function markdownToggleCompose() : void {
425
425
  const bufferId = editor.getActiveBufferId();
426
426
  const info = editor.getBufferInfo(bufferId);
427
427
 
@@ -442,7 +442,8 @@ globalThis.markdownToggleCompose = function(): void {
442
442
  editor.refreshLines(bufferId);
443
443
  editor.setStatus(editor.t("status.compose_on"));
444
444
  }
445
- };
445
+ }
446
+ registerHandler("markdownToggleCompose", markdownToggleCompose);
446
447
 
447
448
  /**
448
449
  * Extract text content from incoming tokens
@@ -1360,7 +1361,7 @@ function processTableAlignment(
1360
1361
  }
1361
1362
 
1362
1363
  // lines_changed: called for newly visible or invalidated lines
1363
- globalThis.onMarkdownLinesChanged = function(data: {
1364
+ function onMarkdownLinesChanged(data: {
1364
1365
  buffer_id: number;
1365
1366
  lines: Array<{
1366
1367
  line_number: number;
@@ -1394,7 +1395,8 @@ globalThis.onMarkdownLinesChanged = function(data: {
1394
1395
  if (tableWidthsGrew) {
1395
1396
  editor.refreshLines(data.buffer_id);
1396
1397
  }
1397
- };
1398
+ }
1399
+ registerHandler("onMarkdownLinesChanged", onMarkdownLinesChanged);
1398
1400
 
1399
1401
  // after_insert: no-op for conceals/overlays.
1400
1402
  // The edit automatically invalidates seen_byte_ranges for affected lines,
@@ -1402,7 +1404,7 @@ globalThis.onMarkdownLinesChanged = function(data: {
1402
1404
  // handles clearing and rebuilding atomically.
1403
1405
  // Marker-based positions auto-adjust with buffer edits, so existing conceals
1404
1406
  // remain visually correct until lines_changed rebuilds them.
1405
- globalThis.onMarkdownAfterInsert = function(data: {
1407
+ function onMarkdownAfterInsert(data: {
1406
1408
  buffer_id: number;
1407
1409
  position: number;
1408
1410
  text: string;
@@ -1411,10 +1413,11 @@ globalThis.onMarkdownAfterInsert = function(data: {
1411
1413
  }): void {
1412
1414
  if (!isComposingInAnySplit(data.buffer_id)) return;
1413
1415
  editor.debug(`[mc] after_insert: pos=${data.position} text="${data.text.replace(/\n/g,'\\n')}" affected=${data.affected_start}..${data.affected_end}`);
1414
- };
1416
+ }
1417
+ registerHandler("onMarkdownAfterInsert", onMarkdownAfterInsert);
1415
1418
 
1416
1419
  // after_delete: no-op for conceals/overlays (same reasoning as after_insert).
1417
- globalThis.onMarkdownAfterDelete = function(data: {
1420
+ function onMarkdownAfterDelete(data: {
1418
1421
  buffer_id: number;
1419
1422
  start: number;
1420
1423
  end: number;
@@ -1424,10 +1427,11 @@ globalThis.onMarkdownAfterDelete = function(data: {
1424
1427
  }): void {
1425
1428
  if (!isComposingInAnySplit(data.buffer_id)) return;
1426
1429
  editor.debug(`[mc] after_delete: start=${data.start} end=${data.end} deleted="${data.deleted_text.replace(/\n/g,'\\n')}" affected_start=${data.affected_start} deleted_len=${data.deleted_len}`);
1427
- };
1430
+ }
1431
+ registerHandler("onMarkdownAfterDelete", onMarkdownAfterDelete);
1428
1432
 
1429
1433
  // cursor_moved: update cursor-aware reveal/conceal for old and new cursor lines
1430
- globalThis.onMarkdownCursorMoved = function(data: {
1434
+ function onMarkdownCursorMoved(data: {
1431
1435
  buffer_id: number;
1432
1436
  cursor_id: number;
1433
1437
  old_position: number;
@@ -1445,7 +1449,8 @@ globalThis.onMarkdownCursorMoved = function(data: {
1445
1449
  // auto-expose is span-level (cursor entering/leaving an emphasis or link
1446
1450
  // span within the same line must toggle its syntax markers).
1447
1451
  editor.refreshLines(data.buffer_id);
1448
- };
1452
+ }
1453
+ registerHandler("onMarkdownCursorMoved", onMarkdownCursorMoved);
1449
1454
 
1450
1455
  // view_transform_request is no longer needed — soft wrapping is handled by
1451
1456
  // marker-based soft breaks (computed in lines_changed), and layout hints
@@ -1453,12 +1458,13 @@ globalThis.onMarkdownCursorMoved = function(data: {
1453
1458
  // caused by the async view_transform round-trip.
1454
1459
 
1455
1460
  // Handle buffer close events - clean up compose mode tracking
1456
- globalThis.onMarkdownBufferClosed = function(data: { buffer_id: number }): void {
1461
+ function onMarkdownBufferClosed(data: { buffer_id: number }) : void {
1457
1462
  // View state is cleaned up automatically when the buffer is removed from keyed_states
1458
- };
1463
+ }
1464
+ registerHandler("onMarkdownBufferClosed", onMarkdownBufferClosed);
1459
1465
 
1460
1466
  // viewport_changed: recalculate table column widths on terminal resize
1461
- globalThis.onMarkdownViewportChanged = function(data: {
1467
+ function onMarkdownViewportChanged(data: {
1462
1468
  split_id: number;
1463
1469
  buffer_id: number;
1464
1470
  top_byte: number;
@@ -1485,12 +1491,13 @@ globalThis.onMarkdownViewportChanged = function(data: {
1485
1491
  setTableWidths(data.buffer_id, bufWidths);
1486
1492
  }
1487
1493
  editor.refreshLines(data.buffer_id);
1488
- };
1494
+ }
1495
+ registerHandler("onMarkdownViewportChanged", onMarkdownViewportChanged);
1489
1496
 
1490
1497
  // Re-enable compose mode for buffers restored from a saved session.
1491
1498
  // The Rust side restores ViewMode::Compose and compose_width, but the plugin
1492
1499
  // needs to re-apply line numbers, line wrap, and layout hints when activated.
1493
- globalThis.onMarkdownBufferActivated = function(data: { buffer_id: number }): void {
1500
+ function onMarkdownBufferActivated(data: { buffer_id: number }) : void {
1494
1501
  const bufferId = data.buffer_id;
1495
1502
 
1496
1503
  const info = editor.getBufferInfo(bufferId);
@@ -1505,7 +1512,8 @@ globalThis.onMarkdownBufferActivated = function(data: { buffer_id: number }): vo
1505
1512
  }
1506
1513
  enableMarkdownCompose(bufferId);
1507
1514
  }
1508
- };
1515
+ }
1516
+ registerHandler("onMarkdownBufferActivated", onMarkdownBufferActivated);
1509
1517
 
1510
1518
  // Register hooks
1511
1519
  editor.on("lines_changed", "onMarkdownLinesChanged");
@@ -1519,7 +1527,7 @@ editor.on("prompt_confirmed", "onMarkdownComposeWidthConfirmed");
1519
1527
  editor.on("buffer_activated", "onMarkdownBufferActivated");
1520
1528
 
1521
1529
  // Set compose width command - starts interactive prompt
1522
- globalThis.markdownSetComposeWidth = function(): void {
1530
+ function markdownSetComposeWidth() : void {
1523
1531
  const currentValue = config.composeWidth === null ? "None" : String(config.composeWidth);
1524
1532
  editor.startPromptWithInitial(editor.t("prompt.compose_width"), "markdown-compose-width", currentValue);
1525
1533
  editor.setPromptInputSync(true);
@@ -1527,10 +1535,11 @@ globalThis.markdownSetComposeWidth = function(): void {
1527
1535
  { text: "None", description: editor.t("suggestion.none") },
1528
1536
  { text: "120", description: editor.t("suggestion.default") },
1529
1537
  ]);
1530
- };
1538
+ }
1539
+ registerHandler("markdownSetComposeWidth", markdownSetComposeWidth);
1531
1540
 
1532
1541
  // Handle compose width prompt confirmation
1533
- globalThis.onMarkdownComposeWidthConfirmed = function(args: {
1542
+ function onMarkdownComposeWidthConfirmed(args: {
1534
1543
  prompt_type: string;
1535
1544
  input: string;
1536
1545
  }): void {
@@ -1543,7 +1552,7 @@ globalThis.onMarkdownComposeWidthConfirmed = function(args: {
1543
1552
 
1544
1553
  const bufferId = editor.getActiveBufferId();
1545
1554
  if (isComposing(bufferId)) {
1546
- editor.setLayoutHints(bufferId, null, { composeWidth: null });
1555
+ editor.setLayoutHints(bufferId, null, {});
1547
1556
  editor.refreshLines(bufferId);
1548
1557
  }
1549
1558
  return;
@@ -1557,13 +1566,14 @@ globalThis.onMarkdownComposeWidthConfirmed = function(args: {
1557
1566
  // Re-process active buffer if in compose mode
1558
1567
  const bufferId = editor.getActiveBufferId();
1559
1568
  if (isComposing(bufferId)) {
1560
- editor.setLayoutHints(bufferId, null, { composeWidth: config.composeWidth });
1569
+ editor.setLayoutHints(bufferId, null, { composeWidth: config.composeWidth ?? undefined });
1561
1570
  editor.refreshLines(bufferId); // Trigger soft break recomputation
1562
1571
  }
1563
1572
  } else {
1564
1573
  editor.setStatus(editor.t("status.invalid_width"));
1565
1574
  }
1566
- };
1575
+ }
1576
+ registerHandler("onMarkdownComposeWidthConfirmed", onMarkdownComposeWidthConfirmed);
1567
1577
 
1568
1578
  // Register commands
1569
1579
  editor.registerCommand(
@@ -148,7 +148,7 @@ async function readRestOfLine(bufferId: number, cursorPos: number): Promise<stri
148
148
  // Enter handler: auto-continue list items or match indentation
149
149
  // ---------------------------------------------------------------------------
150
150
 
151
- globalThis.md_src_enter = async function (): Promise<void> {
151
+ async function md_src_enter() : Promise<void> {
152
152
  const bufferId = editor.getActiveBufferId();
153
153
  if (!bufferId) {
154
154
  editor.executeAction("insert_newline");
@@ -203,13 +203,14 @@ globalThis.md_src_enter = async function (): Promise<void> {
203
203
  }
204
204
  }
205
205
  editor.insertAtCursor("\n" + indent);
206
- };
206
+ }
207
+ registerHandler("md_src_enter", md_src_enter);
207
208
 
208
209
  // ---------------------------------------------------------------------------
209
210
  // Tab handler: indent + cycle bullet on blank list items, else insert spaces
210
211
  // ---------------------------------------------------------------------------
211
212
 
212
- globalThis.md_src_tab = async function (): Promise<void> {
213
+ async function md_src_tab() : Promise<void> {
213
214
  const bufferId = editor.getActiveBufferId();
214
215
  if (!bufferId) {
215
216
  editor.insertAtCursor(" ".repeat(TAB_SIZE));
@@ -249,13 +250,14 @@ globalThis.md_src_tab = async function (): Promise<void> {
249
250
 
250
251
  // Default: insert spaces
251
252
  editor.insertAtCursor(" ".repeat(TAB_SIZE));
252
- };
253
+ }
254
+ registerHandler("md_src_tab", md_src_tab);
253
255
 
254
256
  // ---------------------------------------------------------------------------
255
257
  // Shift+Tab handler: de-indent + reverse-cycle bullet on blank list items
256
258
  // ---------------------------------------------------------------------------
257
259
 
258
- globalThis.md_src_shift_tab = async function (): Promise<void> {
260
+ async function md_src_shift_tab() : Promise<void> {
259
261
  const bufferId = editor.getActiveBufferId();
260
262
  if (!bufferId) {
261
263
  editor.executeAction("dedent_selection");
@@ -300,7 +302,8 @@ globalThis.md_src_shift_tab = async function (): Promise<void> {
300
302
 
301
303
  // Default: fall through to built-in dedent
302
304
  editor.executeAction("dedent_selection");
303
- };
305
+ }
306
+ registerHandler("md_src_shift_tab", md_src_shift_tab);
304
307
 
305
308
  // ---------------------------------------------------------------------------
306
309
  // Mode definition
@@ -340,13 +343,15 @@ function updateMarkdownMode(): void {
340
343
  }
341
344
  }
342
345
 
343
- globalThis.md_src_on_buffer_activated = function (): void {
346
+ function md_src_on_buffer_activated() : void {
344
347
  updateMarkdownMode();
345
- };
348
+ }
349
+ registerHandler("md_src_on_buffer_activated", md_src_on_buffer_activated);
346
350
 
347
- globalThis.md_src_on_language_changed = function (): void {
351
+ function md_src_on_language_changed() : void {
348
352
  updateMarkdownMode();
349
- };
353
+ }
354
+ registerHandler("md_src_on_language_changed", md_src_on_language_changed);
350
355
 
351
356
  editor.on("buffer_activated", "md_src_on_buffer_activated");
352
357
  editor.on("language_changed", "md_src_on_language_changed");
@@ -22,7 +22,7 @@ interface ActionPopupResultData {
22
22
  const INSTALL_URL = "https://github.com/artempyanykh/marksman#how-to-install";
23
23
  let markdownLspError: { serverCommand: string; message: string } | null = null;
24
24
 
25
- globalThis.on_markdown_lsp_server_error = function (data: LspServerErrorData): void {
25
+ function on_markdown_lsp_server_error(data: LspServerErrorData) : void {
26
26
  if (data.language !== "markdown") return;
27
27
  markdownLspError = { serverCommand: data.server_command, message: data.message };
28
28
  if (data.error_type === "not_found") {
@@ -30,10 +30,11 @@ globalThis.on_markdown_lsp_server_error = function (data: LspServerErrorData): v
30
30
  } else {
31
31
  editor.setStatus(`Markdown LSP error: ${data.message}`);
32
32
  }
33
- };
33
+ }
34
+ registerHandler("on_markdown_lsp_server_error", on_markdown_lsp_server_error);
34
35
  editor.on("lsp_server_error", "on_markdown_lsp_server_error");
35
36
 
36
- globalThis.on_markdown_lsp_status_clicked = function (data: LspStatusClickedData): void {
37
+ function on_markdown_lsp_status_clicked(data: LspStatusClickedData) : void {
37
38
  if (data.language !== "markdown" || !markdownLspError) return;
38
39
  editor.showActionPopup({
39
40
  id: "marksman-lsp-help",
@@ -45,10 +46,11 @@ globalThis.on_markdown_lsp_status_clicked = function (data: LspStatusClickedData
45
46
  { id: "dismiss", label: "Dismiss (ESC)" },
46
47
  ],
47
48
  });
48
- };
49
+ }
50
+ registerHandler("on_markdown_lsp_status_clicked", on_markdown_lsp_status_clicked);
49
51
  editor.on("lsp_status_clicked", "on_markdown_lsp_status_clicked");
50
52
 
51
- globalThis.on_markdown_lsp_action_result = function (data: ActionPopupResultData): void {
53
+ function on_markdown_lsp_action_result(data: ActionPopupResultData) : void {
52
54
  if (data.popup_id !== "marksman-lsp-help") return;
53
55
  switch (data.action_id) {
54
56
  case "copy_url":
@@ -61,5 +63,6 @@ globalThis.on_markdown_lsp_action_result = function (data: ActionPopupResultData
61
63
  markdownLspError = null;
62
64
  break;
63
65
  }
64
- };
66
+ }
67
+ registerHandler("on_markdown_lsp_action_result", on_markdown_lsp_action_result);
65
68
  editor.on("action_popup_result", "on_markdown_lsp_action_result");