@fresh-editor/fresh-editor 0.2.5 → 0.2.12

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
@@ -1,5 +1,151 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.2.12
4
+
5
+ ### Features
6
+
7
+ * **Auto-Close Config**: Separate `auto_close` toggle (default: true) to independently control bracket/quote auto-close, skip-over, and pair deletion — previously coupled to `auto_indent`. Per-language overrides via `languages.<lang>.auto_close` (#1144).
8
+
9
+ * **Surround Selection**: Typing an opening delimiter with a selection wraps it instead of replacing it (e.g. selecting `hello` and typing `(` produces `(hello)`). Controlled by `auto_surround` config with per-language overrides.
10
+
11
+ * **Smart Quote Suppression**: Quotes typed inside an existing string no longer auto-close, preventing unwanted doubled quotes (#1142).
12
+
13
+ * **Read-Only Mode**: Files without write permission and library/toolchain paths (rustup, /usr/include, /nix/store, Homebrew Cellar, .nuget, Xcode SDKs) automatically open as read-only. New "Toggle Read Only" command to override. Status bar shows `[RO]` indicator.
14
+
15
+ ### Bug Fixes
16
+
17
+ * **Multi-Cursor Enter**: Fixed Enter key in markdown mode only inserting a newline at the last cursor, ignoring secondary cursors. Falls back to built-in insert_newline when multiple cursors are active (#1140).
18
+
19
+ * **Multi-Cursor Position Drift**: Fixed cursors with no events during bulk edits (e.g. Delete at end of buffer) drifting to stale positions. Uses saturating arithmetic to prevent overflow with overlapping selections (#1140).
20
+
21
+ ### Improvements
22
+
23
+ * **Log Noise Reduction**: Disabled span close events (~90% of log volume) by default and moved 12 high-frequency log sites to trace level. Typical log size reduced from ~266MB to ~5-10MB. Set `FRESH_LOG_SPANS=1` to re-enable (#1154).
24
+
25
+ ### Internal
26
+
27
+ * Added multi-cursor shadow model property-based tests with random operation sequences across 2-3 cursors.
28
+ * Added e2e regression tests for multi-cursor Enter with auto_indent, Ctrl+D selection, tree-sitter, and markdown grammar.
29
+
30
+ ---
31
+
32
+ ## 0.2.11
33
+
34
+ ### Features
35
+
36
+ * **Whitespace Indicators**: Granular control over whitespace visibility — configure space (·) and tab (→) indicators independently for leading, inner, and trailing positions. Master toggle, per-language overrides, and a new `whitespace_indicator_fg` theme color.
37
+
38
+ * **Indent-Based Code Folding**: Code folding now works in large file mode and for files without LSP folding ranges, using indentation analysis as a fallback. Fold from any line within a block (not just the header). Unified byte-offset pipeline for consistent gutter indicators.
39
+
40
+ * **Session Open-File Enhancements**: `--wait` flag blocks the CLI until the user dismisses a popup or closes the buffer — enables use as `git core.editor`. Range selection syntax (`file:L-EL`, `file:L:C-EL:EC`) and hover messages (`file:L@"markdown msg"`) for annotated file opening. Auto-attaches a client when `open-file` starts a new session.
41
+
42
+ * **GUI: macOS Native Integration** (experimental): Native menu bar with dynamic when/checkbox conditions, Cmd keybindings (`macos-gui` keymap), app icon, and `.app` bundle resources. Menu tracking detection prevents state mutations from causing menu jumps.
43
+
44
+ * **Platform Icons**: Application icons for Windows `.exe`, Linux `.deb`/`.rpm` packages, and macOS app bundles.
45
+
46
+ ### Bug Fixes
47
+
48
+ * **Bracket Highlight Hanging on Large Files**: Bracket matching now caps scanning at 1MB and uses 16KB bulk reads instead of byte-at-a-time, preventing hangs on large files.
49
+
50
+ * **Markdown Plugin Activation**: Plugin now activates based on buffer language (not just file extension), fixing cases where `Set Language` to markdown didn't enable smart editing (#1117). Reverse bullet cycling on Shift+Tab now works correctly (#1116).
51
+
52
+ * **Settings UI**: Fixed Save button mouse click not closing the dialog. Fixed Reset button not showing confirmation dialog. Fixed Discard dialog persisting on reopen.
53
+
54
+ * **Active Tab Styling Bleed**: Fixed active tab border color bleeding through dropdown menus.
55
+
56
+ * **Cursor Corruption on Tab Click**: Fixed hardware cursor appearing at wrong position when clicking a tab in an inactive split.
57
+
58
+ * **Comment Delimiter Colors**: Fixed comment delimiter characters (e.g. `//`) using the wrong color in syntax highlighting.
59
+
60
+ * **Scroll Events Routing**: Fixed mouse scroll events going to the file explorer panel regardless of mouse position.
61
+
62
+ * **File Explorer Border**: Fixed hover/drag bugs on the file explorer resize border.
63
+
64
+ * **Windows Named Pipe Crash**: Fixed crash in `Server::handle_new_connection` on Windows.
65
+
66
+ * **Bar/Underline Cursor Invisible**: Fixed bar and underline cursor styles being invisible on characters due to REVERSED modifier creating a block-like highlight (#851).
67
+
68
+ * **Wrapped Line Viewport Scroll**: Fixed viewport scroll limit counting logical lines instead of visual rows, causing erratic scrolling, skipped wrapped rows, and stuck End key with line wrap enabled (#1147).
69
+
70
+ * **Search on Large Files**: Fixed multi-GB memory consumption, O(N²) offset accumulation, and search scan never completing when capped at max matches. Chunked incremental search, viewport-only overlays, and 100K match cap (#1146).
71
+
72
+ * **macOS Menu Hover Jump**: Fixed menu bar jumping to leftmost menu during hover by using `WaitUntil` instead of `Poll` and caching menu item states.
73
+
74
+ ### Improvements
75
+
76
+ * Status log and warning log buffers are now read-only.
77
+ * Replaced `buffer_modified` JS plugin with native Rust diff indicators, eliminating JS↔Rust round-trips on every edit/scroll.
78
+
79
+ ### Internal
80
+
81
+ * Folding system refactored to use byte offsets instead of line numbers for gutter indicators, fixing consistency issues in large file mode.
82
+ * Unified fold indicator pipeline shared between LSP-based and indent-based folding.
83
+ * Fixed Nix build: include PNG files in source filter for GUI icon resources.
84
+
85
+ ---
86
+
87
+ ## 0.2.9
88
+
89
+ ### Features
90
+
91
+ * **Code Folding**: Fold/unfold code blocks via LSP foldingRange. Per-view fold state, gutter indicators for collapsed ranges, fold-aware scrolling. Toggle via command palette ("Toggle Fold") (#900). Thanks @asukaminato0721 !
92
+
93
+ * **Large File Line Numbers**: Large files show byte offsets in gutter/status bar until scanned. On-demand parallel line index scanning (via Ctrl+G prompt or "Scan Line Index" command) gives exact line numbers with progress indicator. Remote scanning counts newlines server-side without data transfer.
94
+
95
+ * **Markdown Source Editing**: New plugin for smart Markdown source-mode editing — auto-continues list items on Enter (bullets, ordered lists, checkboxes), removes empty markers, Tab indents + cycles bullet style (#1095).
96
+
97
+ * **GUI mode - can run without terminal** (highly experimental): GPU-accelerated windowed mode via winit + wgpu. Build with `--features gui` and run with `--gui` to try it.
98
+
99
+ ### Improvements
100
+
101
+ * **Smart Backspace Dedent**: Backspace in leading whitespace removes one indent unit (tab_size spaces or 1 tab) instead of a single character.
102
+
103
+ * **Diagnostics Panel**: Up/Down now scrolls the editor to preview diagnostic location. Enter jumps and focuses the editor.
104
+
105
+ * **Glob Patterns in Language Config**: `filenames` field now supports glob patterns (`*.conf`, `*rc`, `/etc/**/rc.*`) for extensionless file detection (#1083).
106
+
107
+ * Disabled single-quote auto-close in Markdown files (interferes with apostrophes).
108
+
109
+ ### Bug Fixes
110
+
111
+ * **Auto-Indent**: Fixed `tab_size` setting ignored for auto-indent; fixed indent level lost on normal statement lines; fixed Go auto-dedent using spaces instead of tabs (#1068); fixed Python nested indent after consecutive `:` lines (#1069).
112
+
113
+ * **File Explorer Dotfiles**: Fixed dotfiles always visible regardless of "Show hidden files" toggle. Config `show_hidden`/`show_gitignored` now applied on init (#1079).
114
+
115
+ * **LSP Toggle Desync**: Fixed state corruption when toggling LSP off/on — now sends `didClose` so re-enable gets fresh `didOpen` (#952).
116
+
117
+ * **LSP Client Capabilities**: Now advertises all supported capabilities including `publishDiagnostics`, enabling diagnostics from strict servers like pyright (#1006).
118
+
119
+ * **LSP Status Indicator**: Fixed status bar indicator disappearing after each request completion (#952).
120
+
121
+ * **Set Language**: Fixed command storing display name instead of canonical ID, breaking LSP config lookups (#1078).
122
+
123
+ * **Escape Sequences in Client Mode**: Fixed mouse codes, Shift+Tab, and standalone ESC not working in `fresh -a` attach mode (#1089).
124
+
125
+ * **Client Mode Terminal Reset**: Fixed terminal not fully restored on exit in client mode (#1089).
126
+
127
+ * **Ctrl+End with Line Wrap**: Fixed viewport not scrolling to trailing empty line; fixed Down arrow not reaching it (#992).
128
+
129
+ * **Diagnostics Panel Windows Paths**: Fixed file URIs not decoded properly on Windows (#1071).
130
+
131
+ * **Debug Keyboard Dialog**: Fixed not capturing keys in client/server mode (#1089).
132
+
133
+ ### Performance
134
+
135
+ * Replaced linear span lookups in syntax highlighting with O(1) amortized cursor.
136
+ * Eliminated JSON round-trip and JS re-parsing in plugin hook dispatch (~16% CPU reduction).
137
+ * Path-copying PieceTree mutations with structural diff via `Arc::ptr_eq` — O(edit regions) instead of O(all leaves).
138
+ * Viewport-aware filtering and batch API for large file gutter indicators (~780K IPC commands → ~50 per edit).
139
+
140
+ ### Internal
141
+
142
+ * Update flake.nix to rust 1.92.0
143
+ * Split GUI backend into separate `fresh-gui` crate.
144
+ * Unified language detection with `DetectedLanguage` struct and single `apply_language()` mutation point.
145
+ * CI now runs clippy with `--all-features` to lint GUI code.
146
+
147
+ ---
148
+
3
149
  ## 0.2.5
4
150
 
5
151
  ### Features
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A modern, full-featured terminal text editor, **with zero configuration**. Familiar keybindings, mouse support, and IDE-level features — no learning curve required.
4
4
 
5
- [Official Website](https://sinelaw.github.io/fresh/) &nbsp;·&nbsp; [Documentation](https://getfresh.dev/docs) &nbsp;·&nbsp; [Discord](https://discord.gg/GpNWqMUH) &nbsp;·&nbsp; [Contributing](#contributing)
5
+ [Official Website](https://sinelaw.github.io/fresh/) &nbsp;·&nbsp; [Documentation](https://getfresh.dev/docs) &nbsp;·&nbsp; [Discord](https://discord.gg/gqGh3K4uW3) &nbsp;·&nbsp; [Contributing](#contributing)
6
6
 
7
7
  **[Quick Install](#installation):** &nbsp; `curl https://raw.githubusercontent.com/sinelaw/fresh/refs/heads/master/scripts/install.sh | sh`
8
8
 
@@ -47,7 +47,7 @@ See more feature demos: [Editing](https://getfresh.dev/docs/blog/editing) (searc
47
47
  | **Views & Layout** | split panes, line numbers, line wrap, backgrounds, markdown preview |
48
48
  | **Language Server (LSP)** | go to definition, references, hover, code actions, rename, diagnostics, autocompletion |
49
49
  | **Productivity** | command palette, menu bar, keyboard macros, git log, diagnostics panel |
50
- | **Extensibility** | TypeScript plugins (sandboxed Deno), color highlighter, TODO highlighter, merge conflicts, path complete, keymaps |
50
+ | **Extensibility** | TypeScript plugins (sandboxed QuickJS), color highlighter, TODO highlighter, merge conflicts, path complete, keymaps |
51
51
  | **Internationalization** | Multiple language support (see [`locales/`](locales/)), plugin translation system |
52
52
 
53
53
  ## Installation
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.2.5",
3
+ "version": "0.2.12",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
package/plugins/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Plugins
2
2
 
3
- This directory contains production-ready plugins for the editor. Plugins are written in **TypeScript** and run in a sandboxed Deno environment. They are automatically loaded when the editor starts.
3
+ This directory contains production-ready plugins for the editor. Plugins are written in **TypeScript** and run in a sandboxed QuickJS environment (transpiled via oxc_transformer). They are automatically loaded when the editor starts.
4
4
 
5
5
  ## Available Plugins
6
6
 
@@ -36,22 +36,14 @@ function detectLanguage(path: string): string | null {
36
36
  }
37
37
 
38
38
  function pathToFileUri(path: string): string {
39
- let normalized = path.replace(/\\/g, "/");
40
- if (!normalized.startsWith("/")) {
41
- normalized = "/" + normalized;
42
- }
43
- return "file://" + encodeURI(normalized);
39
+ return editor.pathToFileUri(path);
44
40
  }
45
41
 
46
42
  function fileUriToPath(uri: string): string {
47
43
  if (!uri.startsWith("file://")) {
48
44
  return uri;
49
45
  }
50
- let path = decodeURI(uri.substring("file://".length));
51
- if (path.startsWith("/") && path.length > 2 && path[2] === ":") {
52
- path = path.substring(1);
53
- }
54
- return path;
46
+ return editor.fileUriToPath(uri) || uri;
55
47
  }
56
48
 
57
49
  function setClangdStatus(message: string): void {
@@ -41,8 +41,17 @@
41
41
  "use_terminal_bg": false,
42
42
  "cursor_style": "default",
43
43
  "rulers": [],
44
+ "whitespace_show": true,
45
+ "whitespace_spaces_leading": false,
46
+ "whitespace_spaces_inner": false,
47
+ "whitespace_spaces_trailing": false,
48
+ "whitespace_tabs_leading": true,
49
+ "whitespace_tabs_inner": true,
50
+ "whitespace_tabs_trailing": true,
44
51
  "tab_size": 4,
45
52
  "auto_indent": true,
53
+ "auto_close": true,
54
+ "auto_surround": true,
46
55
  "scroll_offset": 3,
47
56
  "default_line_ending": "lf",
48
57
  "trim_trailing_whitespace_on_save": false,
@@ -72,6 +81,7 @@
72
81
  "highlight_context_bytes": 10000,
73
82
  "large_file_threshold_bytes": 1048576,
74
83
  "estimated_line_length": 80,
84
+ "read_concurrency": 64,
75
85
  "file_tree_poll_interval_ms": 3000
76
86
  }
77
87
  },
@@ -278,6 +288,48 @@
278
288
  "default": [],
279
289
  "x-section": "Display"
280
290
  },
291
+ "whitespace_show": {
292
+ "description": "Master toggle for whitespace indicator visibility.\nWhen disabled, no whitespace indicators (·, →) are shown regardless\nof the per-position settings below.\nDefault: true",
293
+ "type": "boolean",
294
+ "default": true,
295
+ "x-section": "Whitespace"
296
+ },
297
+ "whitespace_spaces_leading": {
298
+ "description": "Show space indicators (·) for leading whitespace (indentation).\nLeading whitespace is everything before the first non-space character on a line.\nDefault: false",
299
+ "type": "boolean",
300
+ "default": false,
301
+ "x-section": "Whitespace"
302
+ },
303
+ "whitespace_spaces_inner": {
304
+ "description": "Show space indicators (·) for inner whitespace (between words/tokens).\nInner whitespace is spaces between the first and last non-space characters.\nDefault: false",
305
+ "type": "boolean",
306
+ "default": false,
307
+ "x-section": "Whitespace"
308
+ },
309
+ "whitespace_spaces_trailing": {
310
+ "description": "Show space indicators (·) for trailing whitespace.\nTrailing whitespace is everything after the last non-space character on a line.\nDefault: false",
311
+ "type": "boolean",
312
+ "default": false,
313
+ "x-section": "Whitespace"
314
+ },
315
+ "whitespace_tabs_leading": {
316
+ "description": "Show tab indicators (→) for leading tabs (indentation).\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
317
+ "type": "boolean",
318
+ "default": true,
319
+ "x-section": "Whitespace"
320
+ },
321
+ "whitespace_tabs_inner": {
322
+ "description": "Show tab indicators (→) for inner tabs (between words/tokens).\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
323
+ "type": "boolean",
324
+ "default": true,
325
+ "x-section": "Whitespace"
326
+ },
327
+ "whitespace_tabs_trailing": {
328
+ "description": "Show tab indicators (→) for trailing tabs.\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
329
+ "type": "boolean",
330
+ "default": true,
331
+ "x-section": "Whitespace"
332
+ },
281
333
  "tab_size": {
282
334
  "description": "Number of spaces per tab character",
283
335
  "type": "integer",
@@ -292,6 +344,18 @@
292
344
  "default": true,
293
345
  "x-section": "Editing"
294
346
  },
347
+ "auto_close": {
348
+ "description": "Automatically close brackets, parentheses, and quotes when typing.\nWhen enabled, typing an opening delimiter like `(`, `[`, `{`, `\"`, `'`, or `` ` ``\nwill automatically insert the matching closing delimiter.\nAlso enables skip-over (moving past existing closing delimiters) and\npair deletion (deleting both delimiters when backspacing between them).\nDefault: true",
349
+ "type": "boolean",
350
+ "default": true,
351
+ "x-section": "Editing"
352
+ },
353
+ "auto_surround": {
354
+ "description": "Automatically surround selected text with matching pairs when typing\nan opening delimiter. When enabled and text is selected, typing `(`, `[`,\n`{`, `\"`, `'`, or `` ` `` wraps the selection instead of replacing it.\nDefault: true",
355
+ "type": "boolean",
356
+ "default": true,
357
+ "x-section": "Editing"
358
+ },
295
359
  "scroll_offset": {
296
360
  "description": "Minimum lines to keep visible above/below cursor when scrolling",
297
361
  "type": "integer",
@@ -490,6 +554,14 @@
490
554
  "default": 80,
491
555
  "x-section": "Performance"
492
556
  },
557
+ "read_concurrency": {
558
+ "description": "Maximum number of concurrent filesystem read requests.\nUsed during line-feed scanning and other bulk I/O operations.\nHigher values improve throughput, especially for remote filesystems.\nDefault: 64",
559
+ "type": "integer",
560
+ "format": "uint",
561
+ "minimum": 0,
562
+ "default": 64,
563
+ "x-section": "Performance"
564
+ },
493
565
  "file_tree_poll_interval_ms": {
494
566
  "description": "Poll interval in milliseconds for refreshing expanded directories in the file explorer.\nDirectory modification times are checked at this interval to detect new/deleted files.\nLower values detect changes faster but use more CPU.\nDefault: 3000ms (3 seconds)",
495
567
  "type": "integer",
@@ -702,7 +774,8 @@
702
774
  "default",
703
775
  "emacs",
704
776
  "vscode",
705
- "macos"
777
+ "macos",
778
+ "macos-gui"
706
779
  ]
707
780
  },
708
781
  "LanguageConfig": {
@@ -743,6 +816,22 @@
743
816
  "type": "boolean",
744
817
  "default": true
745
818
  },
819
+ "auto_close": {
820
+ "description": "Whether to auto-close brackets, parentheses, and quotes for this language.\nIf not specified (`null`), falls back to the global `editor.auto_close` setting.",
821
+ "type": [
822
+ "boolean",
823
+ "null"
824
+ ],
825
+ "default": null
826
+ },
827
+ "auto_surround": {
828
+ "description": "Whether to auto-surround selected text with matching pairs for this language.\nIf not specified (`null`), falls back to the global `editor.auto_surround` setting.",
829
+ "type": [
830
+ "boolean",
831
+ "null"
832
+ ],
833
+ "default": null
834
+ },
746
835
  "highlighter": {
747
836
  "description": "Preferred highlighter backend (auto, tree-sitter, or textmate)",
748
837
  "$ref": "#/$defs/HighlighterPreference",
@@ -186,7 +186,7 @@ globalThis.on_csharp_file_open = async function (data: AfterFileOpenData): Promi
186
186
  configuredProjectRoots.add(projectRoot);
187
187
 
188
188
  // Convert path to file:// URI
189
- const rootUri = `file://${projectRoot}`;
189
+ const rootUri = editor.pathToFileUri(projectRoot);
190
190
  editor.debug(`csharp_support: Setting LSP root URI to ${rootUri}`);
191
191
  editor.setLspRootUri("csharp", rootUri);
192
192
  }
@@ -13,7 +13,7 @@
13
13
  * - syncWithEditor for bidirectional cursor sync
14
14
  */
15
15
 
16
- import { Finder, createLiveProvider, getRelativePath, type FinderProvider } from "./lib/finder.ts";
16
+ import { Finder, createLiveProvider, type FinderProvider } from "./lib/finder.ts";
17
17
 
18
18
  const editor = getEditor();
19
19
 
@@ -49,41 +49,42 @@ function severityToString(severity: number): "error" | "warning" | "info" | "hin
49
49
  }
50
50
  }
51
51
 
52
- // Convert URI to file path
52
+ // Convert file URI to file path using the editor's built-in URI handling
53
53
  function uriToPath(uri: string): string {
54
- if (uri.startsWith("file://")) {
55
- return uri.slice(7);
54
+ if (!uri.startsWith("file://")) {
55
+ return uri;
56
56
  }
57
- return uri;
57
+ return editor.fileUriToPath(uri) || uri;
58
58
  }
59
59
 
60
60
  // Get diagnostics based on current filter
61
61
  function getDiagnostics(): DiagnosticItem[] {
62
62
  const diagnostics = editor.getAllDiagnostics();
63
63
 
64
- // Get active file URI for filtering
65
- let activeUri: string | null = null;
64
+ // Get active file path for filtering
65
+ let activePath: string | null = null;
66
66
  if (sourceBufferId !== null) {
67
67
  const path = editor.getBufferPath(sourceBufferId);
68
68
  if (path) {
69
- activeUri = "file://" + path;
69
+ activePath = path.replace(/\\/g, "/");
70
70
  }
71
71
  }
72
72
 
73
- // Filter diagnostics
74
- const filterUri = showAllFiles ? null : activeUri;
75
- const filtered = filterUri
76
- ? diagnostics.filter((d) => d.uri === filterUri)
77
- : diagnostics;
73
+ // Filter diagnostics by comparing decoded paths (avoids URI encoding mismatches)
74
+ const filtered = showAllFiles || !activePath
75
+ ? diagnostics
76
+ : diagnostics.filter((d) => uriToPath(d.uri).replace(/\\/g, "/") === activePath);
78
77
 
79
78
  // Sort by file, then line, then severity
80
79
  filtered.sort((a, b) => {
81
80
  // File comparison
82
81
  if (a.uri !== b.uri) {
83
82
  // Active file first
84
- if (activeUri) {
85
- if (a.uri === activeUri) return -1;
86
- if (b.uri === activeUri) return 1;
83
+ if (activePath) {
84
+ const aPath = uriToPath(a.uri).replace(/\\/g, "/");
85
+ const bPath = uriToPath(b.uri).replace(/\\/g, "/");
86
+ if (aPath === activePath) return -1;
87
+ if (bPath === activePath) return 1;
87
88
  }
88
89
  return a.uri < b.uri ? -1 : 1;
89
90
  }
@@ -124,15 +125,7 @@ const finder = new Finder<DiagnosticItem>(editor, {
124
125
  }),
125
126
  groupBy: "file",
126
127
  syncWithEditor: true,
127
- onSelect: (d) => {
128
- const displayPath = getRelativePath(editor, d.file);
129
- editor.setStatus(
130
- editor.t("status.jumped_to", {
131
- file: displayPath,
132
- line: String(d.line),
133
- })
134
- );
135
- },
128
+ navigateOnCursorMove: true,
136
129
  });
137
130
 
138
131
  // Get title based on current filter state
@@ -225,6 +218,11 @@ globalThis.on_diagnostics_buffer_activated = function (data: {
225
218
  }): void {
226
219
  if (!isOpen) return;
227
220
 
221
+ // Skip virtual buffers (e.g. the diagnostics panel itself) — they have no
222
+ // file path and would clear the filtered diagnostics list.
223
+ const path = editor.getBufferPath(data.buffer_id);
224
+ if (!path) return;
225
+
228
226
  // Update source buffer
229
227
  sourceBufferId = data.buffer_id;
230
228
 
@@ -396,7 +396,7 @@ globalThis.onGitGutterAfterSave = function (args: {
396
396
 
397
397
  // Note: Git diff compares the file on disk, not the in-memory buffer.
398
398
  // Line indicators automatically track position changes via byte-position markers.
399
- // A full re-diff happens on save. For unsaved changes, see buffer_modified plugin.
399
+ // A full re-diff happens on save. Unsaved changes are shown natively by the editor.
400
400
 
401
401
  /**
402
402
  * Handle buffer closed - cleanup state
@@ -112,6 +112,9 @@ export interface FinderConfig<T> {
112
112
 
113
113
  /** Panel-specific: sync cursor with editor */
114
114
  syncWithEditor?: boolean;
115
+
116
+ /** Panel-specific: navigate source split when cursor moves (preview without focus change) */
117
+ navigateOnCursorMove?: boolean;
115
118
  }
116
119
 
117
120
  /**
@@ -410,6 +413,7 @@ export class Finder<T> {
410
413
  maxResults: 100,
411
414
  groupBy: "none",
412
415
  syncWithEditor: false,
416
+ navigateOnCursorMove: false,
413
417
  ...config,
414
418
  };
415
419
 
@@ -1016,6 +1020,24 @@ export class Finder<T> {
1016
1020
  self.editor.setStatus(
1017
1021
  `Item ${itemIndex + 1}/${self.panelState.items.length}`
1018
1022
  );
1023
+
1024
+ // Navigate source split to show the item's location (without focus change)
1025
+ if (self.config.navigateOnCursorMove) {
1026
+ const entry = self.panelState.entries[itemIndex];
1027
+ if (
1028
+ entry.location &&
1029
+ self.panelState.sourceSplitId !== null &&
1030
+ self.panelState.splitId !== null
1031
+ ) {
1032
+ self.editor.openFileInSplit(
1033
+ self.panelState.sourceSplitId,
1034
+ entry.location.file,
1035
+ entry.location.line,
1036
+ entry.location.column
1037
+ );
1038
+ self.editor.focusSplit(self.panelState.splitId);
1039
+ }
1040
+ }
1019
1041
  }
1020
1042
  };
1021
1043
 
@@ -138,6 +138,10 @@ type ViewportInfo = {
138
138
  */
139
139
  topByte: number;
140
140
  /**
141
+ * Line number of the first visible line (None when line index unavailable, e.g. large file before scan)
142
+ */
143
+ topLine: number | null;
144
+ /**
141
145
  * Left column offset (horizontal scroll)
142
146
  */
143
147
  leftColumn: number;
@@ -260,7 +264,7 @@ type BufferInfo = {
260
264
  view_mode: string;
261
265
  /**
262
266
  * True if any split showing this buffer has compose mode enabled.
263
- * Plugins should use this (not view_mode) to decide whether to maintain
267
+ * Plugins should use this (not `view_mode`) to decide whether to maintain
264
268
  * decorations, since decorations live on the buffer and are filtered
265
269
  * per-split at render time.
266
270
  */
@@ -269,6 +273,10 @@ type BufferInfo = {
269
273
  * Compose width (if set), from the active split's view state
270
274
  */
271
275
  compose_width: number | null;
276
+ /**
277
+ * The detected language for this buffer (e.g., "rust", "markdown", "text")
278
+ */
279
+ language: string;
272
280
  };
273
281
  type JsDiagnostic = {
274
282
  /**
@@ -836,6 +844,18 @@ interface EditorAPI {
836
844
  */
837
845
  pathIsAbsolute(path: string): boolean;
838
846
  /**
847
+ * Convert a file:// URI to a local file path.
848
+ * Handles percent-decoding and Windows drive letters.
849
+ * Returns an empty string if the URI is not a valid file URI.
850
+ */
851
+ fileUriToPath(uri: string): string;
852
+ /**
853
+ * Convert a local file path to a file:// URI.
854
+ * Handles Windows drive letters and special characters.
855
+ * Returns an empty string if the path cannot be converted.
856
+ */
857
+ pathToFileUri(path: string): string;
858
+ /**
839
859
  * Get the UTF-8 byte length of a JavaScript string.
840
860
  *
841
861
  * JS strings are UTF-16 internally, so `str.length` returns the number of
@@ -1147,6 +1167,10 @@ interface EditorAPI {
1147
1167
  */
1148
1168
  setLineIndicator(bufferId: number, line: number, namespace: string, symbol: string, r: number, g: number, b: number, priority: number): boolean;
1149
1169
  /**
1170
+ * Batch set line indicators in the gutter
1171
+ */
1172
+ setLineIndicators(bufferId: number, lines: number[], namespace: string, symbol: string, r: number, g: number, b: number, priority: number): boolean;
1173
+ /**
1150
1174
  * Clear line indicators in a namespace
1151
1175
  */
1152
1176
  clearLineIndicators(bufferId: number, namespace: string): boolean;