@fresh-editor/fresh-editor 0.1.76 → 0.1.77

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,34 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.1.77
4
+
5
+ ### Documentation
6
+
7
+ * **macOS Terminal Tips**: Added keyboard enhancement flags configuration guide.
8
+
9
+ ### Features
10
+
11
+ * **LSP Semantic Highlighting** (@Asuka-Minato).
12
+ * **macOS Keybinding Display**: Native symbols (⌃, ⌥, ⇧) instead of Ctrl+/Alt+/Shift+.
13
+ * **Odin Language Support**: Syntax highlighting (sublime-syntax from @Tetralux) and OLS LSP configuration (@xoxorwr).
14
+ * **File Explorer Git Indicators**: Shows modified/added status for files and folders via new plugin (#526) (@Asuka-Minato).
15
+ * **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`).
16
+
17
+ ### Bug Fixes
18
+
19
+ * **Menu Keybinding Display**: Consistent keybinding symbols in menus on macOS (#703).
20
+ * **Git Find File Popup**: Smart path truncation preserving filename (#707).
21
+ * **File Owner Preservation**: Preserve owner when saving files with group write privileges (#743).
22
+
23
+ ### Internal
24
+
25
+ * Telemetry and update checks now debounce to once per day.
26
+ * Terminal mode handling refactored into dedicated module.
27
+ * Resolved ~300+ clippy warnings.
28
+ * Bumped url (2.5.8), libc (0.2.180) (@dependabot).
29
+
30
+ ---
31
+
3
32
  ## 0.1.76
4
33
 
5
34
  ### 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.77",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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",
@@ -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();
@@ -170,6 +170,18 @@ interface ProcessHandle extends PromiseLike<SpawnResult> {
170
170
  kill(): Promise<boolean>;
171
171
  }
172
172
 
173
+ /** File explorer decoration entry provided by plugins */
174
+ interface FileExplorerDecoration {
175
+ /** Absolute or workspace-relative path to decorate */
176
+ path: string;
177
+ /** Symbol to display (single character recommended) */
178
+ symbol?: string | null;
179
+ /** RGB color for the symbol */
180
+ color?: [u8; 3] | null;
181
+ /** Priority for resolving conflicts (higher wins) */
182
+ priority?: number | null;
183
+ }
184
+
173
185
  /** Result from spawnProcess */
174
186
  interface SpawnResult {
175
187
  /** Complete stdout as string. Newlines preserved; trailing newline included. */
@@ -786,6 +798,19 @@ interface EditorAPI {
786
798
  * @returns true if indicators were cleared
787
799
  */
788
800
  clearLineIndicators(buffer_id: number, namespace: string): boolean;
801
+ /**
802
+ * Set file explorer decorations for a namespace
803
+ * @param namespace - Namespace for grouping (e.g., "git-status")
804
+ * @param decorations - Decoration entries
805
+ * @returns true if decorations were accepted
806
+ */
807
+ setFileExplorerDecorations(namespace: string, decorations: FileExplorerDecoration[]): boolean;
808
+ /**
809
+ * Clear file explorer decorations for a namespace
810
+ * @param namespace - Namespace to clear (e.g., "git-status")
811
+ * @returns true if decorations were cleared
812
+ */
813
+ clearFileExplorerDecorations(namespace: string): boolean;
789
814
  /**
790
815
  * Submit a transformed view stream for a viewport
791
816
  * @param buffer_id - Buffer to apply the transform to
@@ -11,7 +11,15 @@
11
11
  */
12
12
 
13
13
  // Types
14
- export type { RGB, Location, PanelOptions, PanelState, NavigationOptions, HighlightPattern } from "./types.ts";
14
+ export type {
15
+ RGB,
16
+ Location,
17
+ PanelOptions,
18
+ PanelState,
19
+ NavigationOptions,
20
+ HighlightPattern,
21
+ FileExplorerDecoration,
22
+ } from "./types.ts";
15
23
 
16
24
  // Panel Management
17
25
  export { PanelManager } from "./panel-manager.ts";
@@ -11,6 +11,20 @@
11
11
  */
12
12
  export type RGB = [number, number, number];
13
13
 
14
+ /**
15
+ * File explorer decoration metadata provided by plugins
16
+ */
17
+ export interface FileExplorerDecoration {
18
+ /** Absolute or workspace-relative path to decorate */
19
+ path: string;
20
+ /** Symbol to display (single character recommended) */
21
+ symbol?: string;
22
+ /** RGB color for the symbol */
23
+ color?: RGB;
24
+ /** Priority for resolving conflicts (higher wins) */
25
+ priority?: number;
26
+ }
27
+
14
28
  /**
15
29
  * File location with line and column
16
30
  */
@@ -0,0 +1,135 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+
5
+ /**
6
+ * Odin LSP Helper Plugin
7
+ *
8
+ * Provides user-friendly error handling for Odin LSP server issues.
9
+ * When ols (Odin Language Server) fails to start, this plugin shows an actionable
10
+ * popup with installation instructions.
11
+ *
12
+ * Features:
13
+ * - Detects Odin LSP server errors (ols)
14
+ * - Shows popup with build instructions
15
+ * - Allows copying build commands to clipboard
16
+ * - Provides option to disable Odin LSP
17
+ *
18
+ * OLS: https://github.com/DanielGavin/ols
19
+ */
20
+
21
+ interface LspServerErrorData {
22
+ language: string;
23
+ server_command: string;
24
+ error_type: string;
25
+ message: string;
26
+ }
27
+
28
+ interface LspStatusClickedData {
29
+ language: string;
30
+ has_error: boolean;
31
+ }
32
+
33
+ interface ActionPopupResultData {
34
+ popup_id: string;
35
+ action_id: string;
36
+ }
37
+
38
+ // OLS GitHub repository
39
+ const OLS_URL = "https://github.com/DanielGavin/ols";
40
+
41
+ // Track error state for Odin LSP
42
+ let odinLspError: { serverCommand: string; message: string } | null = null;
43
+
44
+ /**
45
+ * Handle LSP server errors for Odin
46
+ */
47
+ globalThis.on_odin_lsp_server_error = function (data: LspServerErrorData): void {
48
+ // Only handle Odin language errors
49
+ if (data.language !== "odin") {
50
+ return;
51
+ }
52
+
53
+ editor.debug(`odin-lsp: Server error - ${data.error_type}: ${data.message}`);
54
+
55
+ // Store error state for later reference
56
+ odinLspError = {
57
+ serverCommand: data.server_command,
58
+ message: data.message,
59
+ };
60
+
61
+ // Show a status message for immediate feedback
62
+ if (data.error_type === "not_found") {
63
+ editor.setStatus(
64
+ `Odin LSP server '${data.server_command}' not found. Click status bar for help.`
65
+ );
66
+ } else {
67
+ editor.setStatus(`Odin LSP error: ${data.message}`);
68
+ }
69
+ };
70
+
71
+ // Register hook for LSP server errors
72
+ editor.on("lsp_server_error", "on_odin_lsp_server_error");
73
+
74
+ /**
75
+ * Handle status bar click when there's an Odin LSP error
76
+ */
77
+ globalThis.on_odin_lsp_status_clicked = function (
78
+ data: LspStatusClickedData
79
+ ): void {
80
+ // Only handle Odin language clicks when there's an error
81
+ if (data.language !== "odin" || !odinLspError) {
82
+ return;
83
+ }
84
+
85
+ editor.debug("odin-lsp: Status clicked, showing help popup");
86
+
87
+ // Show action popup with install options
88
+ editor.showActionPopup({
89
+ id: "odin-lsp-help",
90
+ title: "Odin Language Server Not Found",
91
+ message: `"${odinLspError.serverCommand}" (OLS) provides code completion, diagnostics, and navigation for Odin files.\n\nInstallation: ${OLS_URL}`,
92
+ actions: [
93
+ { id: "disable", label: "Disable Odin LSP" },
94
+ { id: "dismiss", label: "Dismiss (ESC)" },
95
+ ],
96
+ });
97
+ };
98
+
99
+ // Register hook for status bar clicks
100
+ editor.on("lsp_status_clicked", "on_odin_lsp_status_clicked");
101
+
102
+ /**
103
+ * Handle action popup results for Odin LSP help
104
+ */
105
+ globalThis.on_odin_lsp_action_result = function (
106
+ data: ActionPopupResultData
107
+ ): void {
108
+ // Only handle our popup
109
+ if (data.popup_id !== "odin-lsp-help") {
110
+ return;
111
+ }
112
+
113
+ editor.debug(`odin-lsp: Action selected - ${data.action_id}`);
114
+
115
+ switch (data.action_id) {
116
+ case "disable":
117
+ editor.disableLspForLanguage("odin");
118
+ editor.setStatus("Odin LSP disabled");
119
+ odinLspError = null;
120
+ break;
121
+
122
+ case "dismiss":
123
+ case "dismissed":
124
+ // Just close the popup without action
125
+ break;
126
+
127
+ default:
128
+ editor.debug(`odin-lsp: Unknown action: ${data.action_id}`);
129
+ }
130
+ };
131
+
132
+ // Register hook for action popup results
133
+ editor.on("action_popup_result", "on_odin_lsp_action_result");
134
+
135
+ editor.debug("odin-lsp: Plugin loaded");