@fresh-editor/fresh-editor 0.1.76 → 0.1.83

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,62 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.1.83
4
+
5
+ ### Breaking Changes
6
+
7
+ * **QuickJS Plugin Runtime**: Replaced Deno with QuickJS for the plugin system. Each plugin now runs in its own isolated context.
8
+
9
+ ### Features
10
+
11
+ * **Cargo Workspace Architecture**: Refactored into modular crates (fresh-core, fresh-editor, fresh-languages, fresh-parser-js, fresh-plugin-runtime, fresh-plugin-api-macros).
12
+
13
+ ### Bug Fixes
14
+
15
+ * **Toggle Comment YAML**: Fixed toggle comment not working for YAML files by falling back to config-based language detection (#774).
16
+ * **Undo History Panic**: Fixed panic when undoing past a save point and making new edits caused out-of-bounds slice access (#776).
17
+ * **Sudo Save Prompt**: Fixed permission denied crash when saving files owned by another user; now shows sudo prompt correctly (#775).
18
+ * **Musl Plugin Support**: Plugins now work on musl target builds (x86_64/aarch64-unknown-linux-musl).
19
+ * **LSP Server Requests**: Fixed LSP server-to-client request handling not being dispatched to plugins.
20
+ * **Git Find File Selection**: Fixed race condition causing wrong file selection when pressing Enter quickly.
21
+ * **Plugin Cache**: Embedded plugins now cached in XDG cache dir instead of leaking temp directories.
22
+
23
+ ### Internal
24
+
25
+ * Improved compile times via LLVM optimization flag.
26
+ * Cross-platform path handling fixes for Windows.
27
+ * Test reliability improvements.
28
+
29
+ ---
30
+
31
+ ## 0.1.77
32
+
33
+ ### Documentation
34
+
35
+ * **macOS Terminal Tips**: Added keyboard enhancement flags configuration guide.
36
+
37
+ ### Features
38
+
39
+ * **LSP Semantic Highlighting** (@Asuka-Minato).
40
+ * **macOS Keybinding Display**: Native symbols (⌃, ⌥, ⇧) instead of Ctrl+/Alt+/Shift+.
41
+ * **Odin Language Support**: Syntax highlighting (sublime-syntax from @Tetralux) and OLS LSP configuration (@xoxorwr).
42
+ * **File Explorer Git Indicators**: Shows modified/added status for files and folders via new plugin (#526) (@Asuka-Minato).
43
+ * **Keyboard Enhancement Flags Config**: New config options for more granular control over kitty protocol usage (`keyboard_disambiguate_escape_codes`, `keyboard_report_event_types`, `keyboard_report_alternate_keys`, `keyboard_report_all_keys_as_escape_codes`).
44
+
45
+ ### Bug Fixes
46
+
47
+ * **Menu Keybinding Display**: Consistent keybinding symbols in menus on macOS (#703).
48
+ * **Git Find File Popup**: Smart path truncation preserving filename (#707).
49
+ * **File Owner Preservation**: Preserve owner when saving files with group write privileges (#743).
50
+
51
+ ### Internal
52
+
53
+ * Telemetry and update checks now debounce to once per day.
54
+ * Terminal mode handling refactored into dedicated module.
55
+ * Resolved ~300+ clippy warnings.
56
+ * Bumped url (2.5.8), libc (0.2.180) (@dependabot).
57
+
58
+ ---
59
+
3
60
  ## 0.1.76
4
61
 
5
62
  ### Features
package/README.md CHANGED
@@ -71,6 +71,8 @@ Or, pick your preferred method:
71
71
 
72
72
  On macOS and some linux distros (Bazzite/Bluefin/Aurora):
73
73
 
74
+ > **Note:** On macOS, see [macOS Terminal Tips](docs/USER_GUIDE.md#macos-terminal-tips) for recommended terminal configuration.
75
+
74
76
  ```bash
75
77
  brew tap sinelaw/fresh
76
78
  brew install fresh-editor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.1.76",
3
+ "version": "0.1.83",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1103,21 +1103,21 @@ globalThis.review_drill_down = async () => {
1103
1103
  const oldBufferId = await editor.createVirtualBuffer({
1104
1104
  name: `*OLD:${h.file}*`,
1105
1105
  mode: "normal",
1106
- read_only: true,
1106
+ readOnly: true,
1107
1107
  entries: oldEntries,
1108
- show_line_numbers: true,
1109
- editing_disabled: true,
1110
- hidden_from_tabs: true
1108
+ showLineNumbers: true,
1109
+ editingDisabled: true,
1110
+ hiddenFromTabs: true
1111
1111
  });
1112
1112
 
1113
1113
  const newBufferId = await editor.createVirtualBuffer({
1114
1114
  name: `*NEW:${h.file}*`,
1115
1115
  mode: "normal",
1116
- read_only: true,
1116
+ readOnly: true,
1117
1117
  entries: newEntries,
1118
- show_line_numbers: true,
1119
- editing_disabled: true,
1120
- hidden_from_tabs: true
1118
+ showLineNumbers: true,
1119
+ editingDisabled: true,
1120
+ hiddenFromTabs: true
1121
1121
  });
1122
1122
 
1123
1123
  // Convert hunks to composite buffer format (parse counts from git diff)
@@ -1493,7 +1493,7 @@ globalThis.start_review_diff = async () => {
1493
1493
  state.comments = []; // Reset comments for new session
1494
1494
 
1495
1495
  const bufferId = await VirtualBufferFactory.create({
1496
- name: "*Review Diff*", mode: "review-mode", read_only: true,
1496
+ name: "*Review Diff*", mode: "review-mode", readOnly: true,
1497
1497
  entries: (await renderReviewStream()).entries, showLineNumbers: false
1498
1498
  });
1499
1499
  state.reviewBufferId = bufferId;
@@ -1666,21 +1666,21 @@ globalThis.side_by_side_diff_current_file = async () => {
1666
1666
  const oldBufferId = await editor.createVirtualBuffer({
1667
1667
  name: `*OLD:${filePath}*`,
1668
1668
  mode: "normal",
1669
- read_only: true,
1669
+ readOnly: true,
1670
1670
  entries: oldEntries,
1671
- show_line_numbers: true,
1672
- editing_disabled: true,
1673
- hidden_from_tabs: true
1671
+ showLineNumbers: true,
1672
+ editingDisabled: true,
1673
+ hiddenFromTabs: true
1674
1674
  });
1675
1675
 
1676
1676
  const newBufferId = await editor.createVirtualBuffer({
1677
1677
  name: `*NEW:${filePath}*`,
1678
1678
  mode: "normal",
1679
- read_only: true,
1679
+ readOnly: true,
1680
1680
  entries: newEntries,
1681
- show_line_numbers: true,
1682
- editing_disabled: true,
1683
- hidden_from_tabs: true
1681
+ showLineNumbers: true,
1682
+ editingDisabled: true,
1683
+ hiddenFromTabs: true
1684
1684
  });
1685
1685
 
1686
1686
  // Convert hunks to composite buffer format
@@ -765,6 +765,6 @@ editor.on("mouse_click", "onCalculatorMouseClick");
765
765
  editor.on("mouse_move", "onCalculatorMouseMove");
766
766
 
767
767
  // Register main command
768
- editor.registerCommand("%cmd.calculator", "%cmd.calculator_desc", "calculator_open", "normal");
768
+ editor.registerCommand("%cmd.calculator", "%cmd.calculator_desc", "calculator_open", null);
769
769
 
770
770
  editor.setStatus(editor.t("status.loaded"));
@@ -42,6 +42,7 @@
42
42
  "large_file_threshold_bytes": 1048576,
43
43
  "estimated_line_length": 80,
44
44
  "enable_inlay_hints": true,
45
+ "enable_semantic_tokens_full": false,
45
46
  "recovery_enabled": true,
46
47
  "auto_save_interval_secs": 2,
47
48
  "highlight_context_bytes": 10000,
@@ -52,6 +53,10 @@
52
53
  "file_tree_poll_interval_ms": 3000,
53
54
  "default_line_ending": "lf",
54
55
  "cursor_style": "default",
56
+ "keyboard_disambiguate_escape_codes": true,
57
+ "keyboard_report_event_types": false,
58
+ "keyboard_report_alternate_keys": true,
59
+ "keyboard_report_all_keys_as_escape_codes": false,
55
60
  "quick_suggestions": true,
56
61
  "show_menu_bar": true,
57
62
  "show_tab_bar": true
@@ -225,7 +230,7 @@
225
230
  "default": 100
226
231
  },
227
232
  "large_file_threshold_bytes": {
228
- "description": "File size threshold in bytes for \"large file\" behavior\nFiles larger than this will:\n- Skip LSP features\n- Use constant-size scrollbar thumb (1 char)\nFiles smaller will count actual lines for accurate scrollbar rendering",
233
+ "description": "File size threshold in bytes for \"large file\" behavior\nFiles larger than this will:\n- Skip LSP features\n- Use constant-size scrollbar thumb (1 char)\n\nFiles smaller will count actual lines for accurate scrollbar rendering",
229
234
  "type": "integer",
230
235
  "format": "uint64",
231
236
  "minimum": 0,
@@ -243,6 +248,11 @@
243
248
  "type": "boolean",
244
249
  "default": true
245
250
  },
251
+ "enable_semantic_tokens_full": {
252
+ "description": "Whether to request full-document LSP semantic tokens.\nRange requests are still used when supported.\nDefault: false (range-only to avoid heavy full refreshes).",
253
+ "type": "boolean",
254
+ "default": false
255
+ },
246
256
  "recovery_enabled": {
247
257
  "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.",
248
258
  "type": "boolean",
@@ -305,6 +315,26 @@
305
315
  "$ref": "#/$defs/CursorStyle",
306
316
  "default": "default"
307
317
  },
318
+ "keyboard_disambiguate_escape_codes": {
319
+ "description": "Enable keyboard enhancement: disambiguate escape codes using CSI-u sequences.\nThis allows unambiguous reading of Escape and modified keys.\nRequires terminal support (kitty keyboard protocol).\nDefault: true",
320
+ "type": "boolean",
321
+ "default": true
322
+ },
323
+ "keyboard_report_event_types": {
324
+ "description": "Enable keyboard enhancement: report key event types (repeat/release).\nAdds extra events when keys are autorepeated or released.\nRequires terminal support (kitty keyboard protocol).\nDefault: false",
325
+ "type": "boolean",
326
+ "default": false
327
+ },
328
+ "keyboard_report_alternate_keys": {
329
+ "description": "Enable keyboard enhancement: report alternate keycodes.\nSends alternate keycodes in addition to the base keycode.\nRequires terminal support (kitty keyboard protocol).\nDefault: true",
330
+ "type": "boolean",
331
+ "default": true
332
+ },
333
+ "keyboard_report_all_keys_as_escape_codes": {
334
+ "description": "Enable keyboard enhancement: report all keys as escape codes.\nRepresents all keyboard events as CSI-u sequences.\nRequired for repeat/release events on plain-text keys.\nRequires terminal support (kitty keyboard protocol).\nDefault: false",
335
+ "type": "boolean",
336
+ "default": false
337
+ },
308
338
  "quick_suggestions": {
309
339
  "description": "Enable quick suggestions (VS Code-like behavior).\nWhen enabled, completion suggestions appear automatically while typing,\nnot just on trigger characters (like `.` or `::`).\nDefault: true",
310
340
  "type": "boolean",
@@ -787,7 +817,7 @@
787
817
  }
788
818
  },
789
819
  "PluginConfig": {
790
- "description": "Configuration for an individual plugin",
820
+ "description": "Configuration for a single plugin",
791
821
  "type": "object",
792
822
  "properties": {
793
823
  "enabled": {
@@ -481,12 +481,12 @@ globalThis.show_git_blame = async function(): Promise<void> {
481
481
  const bufferId = await editor.createVirtualBufferInExistingSplit({
482
482
  name: bufferName,
483
483
  mode: "git-blame",
484
- read_only: true,
484
+ readOnly: true,
485
485
  entries: entries,
486
- split_id: blameState.splitId!,
487
- show_line_numbers: true, // We DO want line numbers (headers won't have them due to source_offset: null)
488
- show_cursors: true,
489
- editing_disabled: true,
486
+ splitId: blameState.splitId!,
487
+ showLineNumbers: true, // We DO want line numbers (headers won't have them due to source_offset: null)
488
+ showCursors: true,
489
+ editingDisabled: true,
490
490
  });
491
491
 
492
492
  if (bufferId !== null) {
@@ -0,0 +1,159 @@
1
+ /// <reference path="../types/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * Git Explorer Decorations
6
+ *
7
+ * Adds VS Code-style status badges (M/A/U/D/...) to the file explorer.
8
+ */
9
+
10
+ const NAMESPACE = "git-explorer";
11
+
12
+ const COLORS = {
13
+ added: [80, 250, 123] as [number, number, number],
14
+ modified: [255, 184, 108] as [number, number, number],
15
+ deleted: [255, 85, 85] as [number, number, number],
16
+ renamed: [139, 233, 253] as [number, number, number],
17
+ untracked: [241, 250, 140] as [number, number, number],
18
+ conflicted: [255, 121, 198] as [number, number, number],
19
+ };
20
+
21
+ const PRIORITY = {
22
+ conflicted: 90,
23
+ deleted: 80,
24
+ added: 60,
25
+ modified: 50,
26
+ renamed: 40,
27
+ untracked: 30,
28
+ };
29
+
30
+ let refreshInFlight = false;
31
+
32
+ function statusToDecoration(status: string, staged: boolean) {
33
+ switch (status) {
34
+ case "A":
35
+ return { symbol: "A", color: COLORS.added, priority: PRIORITY.added };
36
+ case "M":
37
+ return {
38
+ symbol: "M",
39
+ color: staged ? COLORS.added : COLORS.modified,
40
+ priority: PRIORITY.modified + (staged ? 2 : 0),
41
+ };
42
+ case "D":
43
+ return { symbol: "D", color: COLORS.deleted, priority: PRIORITY.deleted };
44
+ case "R":
45
+ return { symbol: "R", color: COLORS.renamed, priority: PRIORITY.renamed };
46
+ case "C":
47
+ return { symbol: "C", color: COLORS.renamed, priority: PRIORITY.renamed };
48
+ case "U":
49
+ return { symbol: "!", color: COLORS.conflicted, priority: PRIORITY.conflicted };
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function parseStatusOutput(output: string, repoRoot: string) {
56
+ const separator = output.includes("\0") ? "\0" : "\n";
57
+ const entries = output
58
+ .split(separator)
59
+ .map((entry) => entry.replace(/\r$/, ""))
60
+ .filter((entry) => entry.length > 0);
61
+ const byPath = new Map<string, { path: string; symbol: string; color: [number, number, number]; priority: number }>();
62
+
63
+ for (let i = 0; i < entries.length; i++) {
64
+ const entry = entries[i];
65
+ if (entry.length < 3) {
66
+ continue;
67
+ }
68
+ const x = entry[0];
69
+ const y = entry[1];
70
+ let path = entry.slice(3);
71
+
72
+ if ((x === "R" || x === "C") && separator === "\0" && i + 1 < entries.length) {
73
+ i += 1;
74
+ path = entries[i];
75
+ } else if (entry.includes(" -> ") && (x === "R" || x === "C" || y === "R" || y === "C")) {
76
+ path = entry.split(" -> ").pop() ?? path;
77
+ }
78
+
79
+ let decoration = null;
80
+ if (x === "?" && y === "?") {
81
+ decoration = { symbol: "U", color: COLORS.untracked, priority: PRIORITY.untracked };
82
+ } else if (x !== " " && x !== "?") {
83
+ decoration = statusToDecoration(x, true);
84
+ } else if (y !== " ") {
85
+ decoration = statusToDecoration(y, false);
86
+ }
87
+
88
+ if (!decoration) {
89
+ continue;
90
+ }
91
+
92
+ const absolutePath = editor.pathJoin(repoRoot, path);
93
+ const existing = byPath.get(absolutePath);
94
+ if (!existing || decoration.priority >= existing.priority) {
95
+ byPath.set(absolutePath, { path: absolutePath, ...decoration });
96
+ }
97
+ }
98
+
99
+ return Array.from(byPath.values());
100
+ }
101
+
102
+ async function refreshGitExplorerDecorations() {
103
+ if (refreshInFlight) {
104
+ return;
105
+ }
106
+ refreshInFlight = true;
107
+ try {
108
+ const cwd = editor.getCwd();
109
+ const rootResult = await editor.spawnProcess("git", ["rev-parse", "--show-toplevel"], cwd);
110
+ if (rootResult.exit_code !== 0) {
111
+ editor.clearFileExplorerDecorations(NAMESPACE);
112
+ return;
113
+ }
114
+ const repoRoot = rootResult.stdout.trim();
115
+ if (!repoRoot) {
116
+ editor.clearFileExplorerDecorations(NAMESPACE);
117
+ return;
118
+ }
119
+
120
+ const statusResult = await editor.spawnProcess(
121
+ "git",
122
+ ["status", "--porcelain"],
123
+ repoRoot
124
+ );
125
+ if (statusResult.exit_code !== 0) {
126
+ editor.clearFileExplorerDecorations(NAMESPACE);
127
+ return;
128
+ }
129
+
130
+ const decorations = parseStatusOutput(statusResult.stdout, repoRoot);
131
+ if (decorations.length === 0) {
132
+ editor.clearFileExplorerDecorations(NAMESPACE);
133
+ } else {
134
+ editor.setFileExplorerDecorations(NAMESPACE, decorations);
135
+ }
136
+ } catch (_err) {
137
+ editor.clearFileExplorerDecorations(NAMESPACE);
138
+ } finally {
139
+ refreshInFlight = false;
140
+ }
141
+ }
142
+
143
+ globalThis.onGitExplorerAfterFileOpen = () => {
144
+ refreshGitExplorerDecorations();
145
+ };
146
+
147
+ globalThis.onGitExplorerAfterFileSave = () => {
148
+ refreshGitExplorerDecorations();
149
+ };
150
+
151
+ globalThis.onGitExplorerEditorInitialized = () => {
152
+ refreshGitExplorerDecorations();
153
+ };
154
+
155
+ editor.on("after_file_open", "onGitExplorerAfterFileOpen");
156
+ editor.on("after_file_save", "onGitExplorerAfterFileSave");
157
+ editor.on("editor_initialized", "onGitExplorerEditorInitialized");
158
+
159
+ refreshGitExplorerDecorations();
@@ -27,7 +27,11 @@ async function loadGitFiles(): Promise<string[]> {
27
27
  const result = await editor.spawnProcess("git", ["ls-files"]);
28
28
 
29
29
  if (result.exit_code === 0) {
30
- return result.stdout.split("\n").filter((line) => line.trim() !== "");
30
+ // Split by newline and trim each line to handle \r\n on Windows
31
+ return result.stdout
32
+ .split("\n")
33
+ .map((line) => line.trim())
34
+ .filter((line) => line !== "");
31
35
  }
32
36
 
33
37
  editor.debug(`Failed to load git files: ${result.stderr}`);
@@ -58,14 +62,14 @@ editor.registerCommand(
58
62
  "%cmd.find",
59
63
  "%cmd.find_desc",
60
64
  "start_git_find_file",
61
- "normal"
65
+ null
62
66
  );
63
67
 
64
68
  editor.registerCommand(
65
69
  "%cmd.reload",
66
70
  "%cmd.reload_desc",
67
71
  "git_reload_files",
68
- "normal"
72
+ null
69
73
  );
70
74
 
71
75
  editor.debug("Git Find File plugin loaded (using Finder abstraction)");
@@ -67,7 +67,7 @@ globalThis.start_git_grep = function (): void {
67
67
  };
68
68
 
69
69
  // Register command
70
- editor.registerCommand("%cmd.grep", "%cmd.grep_desc", "start_git_grep", "normal");
70
+ editor.registerCommand("%cmd.grep", "%cmd.grep_desc", "start_git_grep", null);
71
71
 
72
72
  // Log that plugin loaded successfully
73
73
  editor.debug("Git Grep plugin loaded (using Finder abstraction)");
@@ -766,12 +766,12 @@ globalThis.show_git_log = async function(): Promise<void> {
766
766
  const bufferId = await editor.createVirtualBufferInExistingSplit({
767
767
  name: "*Git Log*",
768
768
  mode: "git-log",
769
- read_only: true,
769
+ readOnly: true,
770
770
  entries: entries,
771
- split_id: gitLogState.splitId!,
772
- show_line_numbers: false,
773
- show_cursors: true,
774
- editing_disabled: true,
771
+ splitId: gitLogState.splitId!,
772
+ showLineNumbers: false,
773
+ showCursors: true,
774
+ editingDisabled: true,
775
775
  });
776
776
 
777
777
  if (bufferId !== null) {
@@ -900,12 +900,12 @@ globalThis.git_log_show_commit = async function(): Promise<void> {
900
900
  const bufferId = await editor.createVirtualBufferInExistingSplit({
901
901
  name: `*Commit: ${commit.shortHash}*`,
902
902
  mode: "git-commit-detail",
903
- read_only: true,
903
+ readOnly: true,
904
904
  entries: entries,
905
- split_id: gitLogState.splitId!,
906
- show_line_numbers: false, // Disable line numbers for cleaner diff view
907
- show_cursors: true,
908
- editing_disabled: true,
905
+ splitId: gitLogState.splitId!,
906
+ showLineNumbers: false, // Disable line numbers for cleaner diff view
907
+ showCursors: true,
908
+ editingDisabled: true,
909
909
  });
910
910
 
911
911
  if (bufferId !== null) {
@@ -1213,12 +1213,12 @@ globalThis.git_commit_detail_open_file = async function(): Promise<void> {
1213
1213
  const bufferId = await editor.createVirtualBufferInExistingSplit({
1214
1214
  name: `${file} @ ${commit.shortHash}`,
1215
1215
  mode: "git-file-view",
1216
- read_only: true,
1216
+ readOnly: true,
1217
1217
  entries: entries,
1218
- split_id: commitDetailState.splitId!,
1219
- show_line_numbers: true,
1220
- show_cursors: true,
1221
- editing_disabled: true,
1218
+ splitId: commitDetailState.splitId!,
1219
+ showLineNumbers: true,
1220
+ showCursors: true,
1221
+ editingDisabled: true,
1222
1222
  });
1223
1223
 
1224
1224
  if (bufferId !== null) {
@@ -465,7 +465,9 @@ export class Finder<T> {
465
465
  options.initialQuery
466
466
  );
467
467
  } else {
468
- this.editor.startPrompt(options.title, this.config.id);
468
+ this.editor.debug(`[Finder] calling startPrompt with title="${options.title}", id="${this.config.id}"`);
469
+ const result = this.editor.startPrompt(options.title, this.config.id);
470
+ this.editor.debug(`[Finder] startPrompt returned: ${result}`);
469
471
  }
470
472
  this.editor.setStatus("Type to search...");
471
473
  }
@@ -951,11 +953,11 @@ export class Finder<T> {
951
953
  }
952
954
 
953
955
  private closePreview(): void {
954
- if (this.previewState.bufferId !== null) {
956
+ if (this.previewState.bufferId || this.previewState.bufferId == 0) {
955
957
  this.editor.closeBuffer(this.previewState.bufferId);
956
958
  this.previewState.bufferId = null;
957
959
  }
958
- if (this.previewState.splitId !== null) {
960
+ if (this.previewState.splitId || this.previewState.splitId == 0) {
959
961
  this.editor.closeSplit(this.previewState.splitId);
960
962
  this.previewState.splitId = null;
961
963
  }