@fresh-editor/fresh-editor 0.2.4 → 0.2.11

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,153 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.2.11
4
+
5
+ ### Features
6
+
7
+ * **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.
8
+
9
+ * **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.
10
+
11
+ * **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.
12
+
13
+ * **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.
14
+
15
+ * **Platform Icons**: Application icons for Windows `.exe`, Linux `.deb`/`.rpm` packages, and macOS app bundles.
16
+
17
+ ### Bug Fixes
18
+
19
+ * **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.
20
+
21
+ * **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).
22
+
23
+ * **Settings UI**: Fixed Save button mouse click not closing the dialog. Fixed Reset button not showing confirmation dialog. Fixed Discard dialog persisting on reopen.
24
+
25
+ * **Active Tab Styling Bleed**: Fixed active tab border color bleeding through dropdown menus.
26
+
27
+ * **Cursor Corruption on Tab Click**: Fixed hardware cursor appearing at wrong position when clicking a tab in an inactive split.
28
+
29
+ * **Comment Delimiter Colors**: Fixed comment delimiter characters (e.g. `//`) using the wrong color in syntax highlighting.
30
+
31
+ * **Scroll Events Routing**: Fixed mouse scroll events going to the file explorer panel regardless of mouse position.
32
+
33
+ * **File Explorer Border**: Fixed hover/drag bugs on the file explorer resize border.
34
+
35
+ * **Windows Named Pipe Crash**: Fixed crash in `Server::handle_new_connection` on Windows.
36
+
37
+ * **Bar/Underline Cursor Invisible**: Fixed bar and underline cursor styles being invisible on characters due to REVERSED modifier creating a block-like highlight (#851).
38
+
39
+ * **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).
40
+
41
+ * **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).
42
+
43
+ * **macOS Menu Hover Jump**: Fixed menu bar jumping to leftmost menu during hover by using `WaitUntil` instead of `Poll` and caching menu item states.
44
+
45
+ ### Improvements
46
+
47
+ * Status log and warning log buffers are now read-only.
48
+ * Replaced `buffer_modified` JS plugin with native Rust diff indicators, eliminating JS↔Rust round-trips on every edit/scroll.
49
+
50
+ ### Internal
51
+
52
+ * Folding system refactored to use byte offsets instead of line numbers for gutter indicators, fixing consistency issues in large file mode.
53
+ * Unified fold indicator pipeline shared between LSP-based and indent-based folding.
54
+ * Fixed Nix build: include PNG files in source filter for GUI icon resources.
55
+
56
+ ---
57
+
58
+ ## 0.2.9
59
+
60
+ ### Features
61
+
62
+ * **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 !
63
+
64
+ * **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.
65
+
66
+ * **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).
67
+
68
+ * **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.
69
+
70
+ ### Improvements
71
+
72
+ * **Smart Backspace Dedent**: Backspace in leading whitespace removes one indent unit (tab_size spaces or 1 tab) instead of a single character.
73
+
74
+ * **Diagnostics Panel**: Up/Down now scrolls the editor to preview diagnostic location. Enter jumps and focuses the editor.
75
+
76
+ * **Glob Patterns in Language Config**: `filenames` field now supports glob patterns (`*.conf`, `*rc`, `/etc/**/rc.*`) for extensionless file detection (#1083).
77
+
78
+ * Disabled single-quote auto-close in Markdown files (interferes with apostrophes).
79
+
80
+ ### Bug Fixes
81
+
82
+ * **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).
83
+
84
+ * **File Explorer Dotfiles**: Fixed dotfiles always visible regardless of "Show hidden files" toggle. Config `show_hidden`/`show_gitignored` now applied on init (#1079).
85
+
86
+ * **LSP Toggle Desync**: Fixed state corruption when toggling LSP off/on — now sends `didClose` so re-enable gets fresh `didOpen` (#952).
87
+
88
+ * **LSP Client Capabilities**: Now advertises all supported capabilities including `publishDiagnostics`, enabling diagnostics from strict servers like pyright (#1006).
89
+
90
+ * **LSP Status Indicator**: Fixed status bar indicator disappearing after each request completion (#952).
91
+
92
+ * **Set Language**: Fixed command storing display name instead of canonical ID, breaking LSP config lookups (#1078).
93
+
94
+ * **Escape Sequences in Client Mode**: Fixed mouse codes, Shift+Tab, and standalone ESC not working in `fresh -a` attach mode (#1089).
95
+
96
+ * **Client Mode Terminal Reset**: Fixed terminal not fully restored on exit in client mode (#1089).
97
+
98
+ * **Ctrl+End with Line Wrap**: Fixed viewport not scrolling to trailing empty line; fixed Down arrow not reaching it (#992).
99
+
100
+ * **Diagnostics Panel Windows Paths**: Fixed file URIs not decoded properly on Windows (#1071).
101
+
102
+ * **Debug Keyboard Dialog**: Fixed not capturing keys in client/server mode (#1089).
103
+
104
+ ### Performance
105
+
106
+ * Replaced linear span lookups in syntax highlighting with O(1) amortized cursor.
107
+ * Eliminated JSON round-trip and JS re-parsing in plugin hook dispatch (~16% CPU reduction).
108
+ * Path-copying PieceTree mutations with structural diff via `Arc::ptr_eq` — O(edit regions) instead of O(all leaves).
109
+ * Viewport-aware filtering and batch API for large file gutter indicators (~780K IPC commands → ~50 per edit).
110
+
111
+ ### Internal
112
+
113
+ * Update flake.nix to rust 1.92.0
114
+ * Split GUI backend into separate `fresh-gui` crate.
115
+ * Unified language detection with `DetectedLanguage` struct and single `apply_language()` mutation point.
116
+ * CI now runs clippy with `--all-features` to lint GUI code.
117
+
118
+ ---
119
+
120
+ ## 0.2.5
121
+
122
+ ### Features
123
+
124
+ * **Persistent Auto-Save**: New `auto_save_enabled` config option (default: false) to automatically save modified buffers to their original file at a configurable interval (`auto_save_interval_secs`, default: 30s) (#542)
125
+
126
+ * **Smart Home**: Home key now uses smart home behavior by default, toggling between the first non-whitespace character and column 0. On soft-wrapped lines, smart home respects visual line boundaries instead of jumping to the physical line start (#1064).
127
+
128
+ ### Bug Fixes
129
+
130
+ * **Diff View Scrollbar**: Fixed scrollbar click-to-jump and thumb drag not working in side-by-side diff views. Composite buffer views now use row-based scrolling via CompositeViewState.
131
+
132
+ * **Terminal Bracket Paste**: Fixed pasted text going into the editor buffer instead of the terminal PTY when in terminal mode (#1056).
133
+
134
+ * **LSP did_open Reliability**: Fixed buffer being incorrectly marked as LSP-opened when the did_open send fails, which prevented retry and could corrupt server document state.
135
+
136
+ * **Remote Editing Data Loss**: Fixed intermittent data loss when loading large files via SSH remote editing on macOS. The bounded channel now uses backpressure instead of silently dropping data when the buffer overflows (#1059).
137
+
138
+ ### Configuration
139
+
140
+ * Renamed `auto_save_interval_secs` (recovery) to `auto_recovery_save_interval_secs` to distinguish it from the new persistent auto-save feature. Added `auto_recovery_save_interval_secs` config option (default: 2s).
141
+
142
+ ### Internal
143
+
144
+ * Introduced typed `LeafId` and `ContainerId` wrappers around `SplitId` to enforce leaf-vs-container constraints at compile time.
145
+ * Enabled `#![deny(clippy::let_underscore_must_use)]` crate-wide; all ignored `Result` values now have explicit annotations or proper error handling.
146
+ * Made `request_completion` and `request_signature_help` infallible, removing dead `Result` return types.
147
+ * Added CONTRIBUTING.md with development guidelines.
148
+
149
+ ---
150
+
3
151
  ## 0.2.4
4
152
 
5
153
  ### 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/)  ·  [Documentation](https://getfresh.dev/docs)  ·  [Discord](https://discord.gg/qUutBj9t)  ·  [Contributing](#contributing)
5
+ [Official Website](https://sinelaw.github.io/fresh/)  ·  [Documentation](https://getfresh.dev/docs)  ·  [Discord](https://discord.gg/gqGh3K4uW3)  ·  [Contributing](#contributing)
6
6
 
7
7
  **[Quick Install](#installation):**   `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
@@ -240,30 +240,7 @@ cargo build --release
240
240
 
241
241
  ## Contributing
242
242
 
243
- Thanks for contributing!
244
-
245
- 1. **Reproduce Before Fixing**: Always include a test case that reproduces the bug (fails) without the fix, and passes with the fix. This ensures the issue is verified and prevents future regressions.
246
-
247
- 2. **E2E Tests for New Flows**: Any new user flow or feature must include an end-to-end (e2e) test. E2E tests send keyboard/mouse events and examines the final rendered output, do not examine internal state.
248
-
249
- 3. **No timeouts or time-sensitive tests**: Use "semantic waiting" (waiting for specific state changes/events) instead of fixed timers to ensure test stability. Wait indefinitely, don't put timeouts inside tests (cargo nextest will timeout externally).
250
-
251
- 4. **Test isolation**: Tests should run in parallel. Use the internal clipboard mode in tests to isolate them from the host system and prevent flakiness in CI. Same for other external resources (temp files, etc. should all be isolated between tests, under a per-test temporary workdir).
252
-
253
- 5. **Required Formatting**: All code must be formatted with `cargo fmt` before submission. PRs that fail formatting checks will not be merged.
254
-
255
- 6. **Cross-Platform Consistency**: Avoid hard-coding newline or CRLF related logic, consider the buffer mode.
256
-
257
- 7. **LSP**: Ensure LSP interactions follow the correct lifecycle (e.g., `didOpen` must always precede other requests to avoid server-side errors). Use the appropriate existing helpers for this pattern.
258
-
259
- 8. **Regenerate plugin types and schemas**: After modifying the plugin API or config types:
260
- - **TypeScript definitions** (`plugins/lib/fresh.d.ts`): Auto-generated from Rust types with `#[derive(TS)]`. Run: `cargo test -p fresh-plugin-runtime write_fresh_dts_file -- --ignored`
261
- - **JSON schemas** (`plugins/config-schema.json`, `plugins/schemas/theme.schema.json`): Auto-generated from Rust types with `#[derive(JsonSchema)]`. Run: `./scripts/gen_schema.sh`
262
- - **Package schema** (`plugins/schemas/package.schema.json`): Manually maintained - edit directly when adding new language pack fields
263
-
264
- 9. **Type check plugins**: Run `crates/fresh-editor/plugins/check-types.sh` (requires `tsc`)
265
-
266
- **Tip**: You can use tmux + send-keys + render-pane to script ad-hoc tests on the UI, for example when trying to reproduce an issue.
243
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
267
244
 
268
245
  ## Privacy
269
246
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.2.4",
3
+ "version": "0.2.11",
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,6 +41,13 @@
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,
46
53
  "scroll_offset": 3,
@@ -58,8 +65,10 @@
58
65
  "mouse_hover_enabled": true,
59
66
  "mouse_hover_delay_ms": 500,
60
67
  "double_click_time_ms": 500,
68
+ "auto_save_enabled": false,
69
+ "auto_save_interval_secs": 30,
61
70
  "recovery_enabled": true,
62
- "auto_save_interval_secs": 2,
71
+ "auto_recovery_save_interval_secs": 2,
63
72
  "auto_revert_poll_interval_ms": 2000,
64
73
  "keyboard_disambiguate_escape_codes": true,
65
74
  "keyboard_report_event_types": false,
@@ -70,6 +79,7 @@
70
79
  "highlight_context_bytes": 10000,
71
80
  "large_file_threshold_bytes": 1048576,
72
81
  "estimated_line_length": 80,
82
+ "read_concurrency": 64,
73
83
  "file_tree_poll_interval_ms": 3000
74
84
  }
75
85
  },
@@ -276,6 +286,48 @@
276
286
  "default": [],
277
287
  "x-section": "Display"
278
288
  },
289
+ "whitespace_show": {
290
+ "description": "Master toggle for whitespace indicator visibility.\nWhen disabled, no whitespace indicators (·, →) are shown regardless\nof the per-position settings below.\nDefault: true",
291
+ "type": "boolean",
292
+ "default": true,
293
+ "x-section": "Whitespace"
294
+ },
295
+ "whitespace_spaces_leading": {
296
+ "description": "Show space indicators (·) for leading whitespace (indentation).\nLeading whitespace is everything before the first non-space character on a line.\nDefault: false",
297
+ "type": "boolean",
298
+ "default": false,
299
+ "x-section": "Whitespace"
300
+ },
301
+ "whitespace_spaces_inner": {
302
+ "description": "Show space indicators (·) for inner whitespace (between words/tokens).\nInner whitespace is spaces between the first and last non-space characters.\nDefault: false",
303
+ "type": "boolean",
304
+ "default": false,
305
+ "x-section": "Whitespace"
306
+ },
307
+ "whitespace_spaces_trailing": {
308
+ "description": "Show space indicators (·) for trailing whitespace.\nTrailing whitespace is everything after the last non-space character on a line.\nDefault: false",
309
+ "type": "boolean",
310
+ "default": false,
311
+ "x-section": "Whitespace"
312
+ },
313
+ "whitespace_tabs_leading": {
314
+ "description": "Show tab indicators (→) for leading tabs (indentation).\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
315
+ "type": "boolean",
316
+ "default": true,
317
+ "x-section": "Whitespace"
318
+ },
319
+ "whitespace_tabs_inner": {
320
+ "description": "Show tab indicators (→) for inner tabs (between words/tokens).\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
321
+ "type": "boolean",
322
+ "default": true,
323
+ "x-section": "Whitespace"
324
+ },
325
+ "whitespace_tabs_trailing": {
326
+ "description": "Show tab indicators (→) for trailing tabs.\nCan be overridden per-language via `show_whitespace_tabs` in language config.\nDefault: true",
327
+ "type": "boolean",
328
+ "default": true,
329
+ "x-section": "Whitespace"
330
+ },
279
331
  "tab_size": {
280
332
  "description": "Number of spaces per tab character",
281
333
  "type": "integer",
@@ -388,14 +440,28 @@
388
440
  "default": 500,
389
441
  "x-section": "Mouse"
390
442
  },
443
+ "auto_save_enabled": {
444
+ "description": "Whether to enable persistent auto-save (save to original file on disk).\nWhen enabled, modified buffers are saved to their original file path\nat a configurable interval.\nDefault: false",
445
+ "type": "boolean",
446
+ "default": false,
447
+ "x-section": "Recovery"
448
+ },
449
+ "auto_save_interval_secs": {
450
+ "description": "Interval in seconds for persistent auto-save.\nModified buffers are saved to their original file at this interval.\nOnly effective when auto_save_enabled is true.\nDefault: 30 seconds",
451
+ "type": "integer",
452
+ "format": "uint32",
453
+ "minimum": 0,
454
+ "default": 30,
455
+ "x-section": "Recovery"
456
+ },
391
457
  "recovery_enabled": {
392
458
  "description": "Whether to enable file recovery (Emacs-style auto-save)\nWhen enabled, buffers are periodically saved to recovery files\nso they can be recovered if the editor crashes.",
393
459
  "type": "boolean",
394
460
  "default": true,
395
461
  "x-section": "Recovery"
396
462
  },
397
- "auto_save_interval_secs": {
398
- "description": "Auto-save interval in seconds for file recovery\nModified buffers are saved to recovery files at this interval.\nDefault: 2 seconds for fast recovery with minimal data loss.\nSet to 0 to disable periodic auto-save (manual recovery only).",
463
+ "auto_recovery_save_interval_secs": {
464
+ "description": "Interval in seconds for auto-recovery-save.\nModified buffers are saved to recovery files at this interval.\nOnly effective when recovery_enabled is true.\nDefault: 2 seconds",
399
465
  "type": "integer",
400
466
  "format": "uint32",
401
467
  "minimum": 0,
@@ -474,6 +540,14 @@
474
540
  "default": 80,
475
541
  "x-section": "Performance"
476
542
  },
543
+ "read_concurrency": {
544
+ "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",
545
+ "type": "integer",
546
+ "format": "uint",
547
+ "minimum": 0,
548
+ "default": 64,
549
+ "x-section": "Performance"
550
+ },
477
551
  "file_tree_poll_interval_ms": {
478
552
  "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)",
479
553
  "type": "integer",
@@ -686,7 +760,8 @@
686
760
  "default",
687
761
  "emacs",
688
762
  "vscode",
689
- "macos"
763
+ "macos",
764
+ "macos-gui"
690
765
  ]
691
766
  },
692
767
  "LanguageConfig": {
@@ -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 (null 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 (all lines share the same namespace/symbol/color/priority)
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;