@fresh-editor/fresh-editor 0.2.13 → 0.2.16

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,66 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.2.16
4
+
5
+ ### Features
6
+
7
+ * **Project-Wide Search & Replace**: Search and replace across the entire project. Works reliably with unsaved buffers, large files, and up to 10,000 results. Alt+Enter to replace in project.
8
+
9
+ * **Hot Exit**: All buffers — including unnamed scratch buffers — persist across sessions automatically. Configurable via `hot_exit` setting (#1148, #1233).
10
+
11
+ * **Workspace Storage**: Session state always restored on startup, even when opening specific files from CLI. Plugin state also persists across sessions.
12
+
13
+ ### Improvements
14
+
15
+ * **Keybinding Editor**: Collapsible section headers and plugin mode bindings shown as first-class entries.
16
+
17
+ * **Markdown Compose Mode**: Easier to discover via global toggle and searchable command palette entries.
18
+
19
+ * **Tab Naming**: Duplicate tab names are disambiguated with appended numbers.
20
+
21
+ * **View...Keybinding Style: Menu Checkboxes**: Submenu items now show checkbox indicators for toggled settings.
22
+
23
+ ### Bug Fixes
24
+
25
+ * Fixed crash when workspace references deleted files (#1278).
26
+
27
+ * Fixed CapsLock breaking keyboard shortcuts like Ctrl+A, Ctrl+C, etc.
28
+
29
+ * Fixed bracketed paste not working on Windows Terminal.
30
+
31
+ * Fixed clipboard not working in client-server session mode.
32
+
33
+ * Fixed Latin-1 files misdetected as UTF-8 for short files with trailing high bytes.
34
+
35
+ * Fixed line number bugs: Delete key not updating status bar (#1261), relative line numbers miscounting (#1262).
36
+
37
+ * Fixed C# LSP not working due to language ID mismatch.
38
+
39
+ * Fixed remote editing using hardcoded `/tmp` instead of querying the remote system.
40
+
41
+ * Fixed high memory usage on Windows (#1205).
42
+
43
+ * Fixed PageUp/PageDown not working in Theme Editor sidebar.
44
+
45
+ * Fixed unbound keys being swallowed in plugin modes.
46
+
47
+ ### Packaging
48
+
49
+ * **Linux**: Icons and desktop files added to all packaging methods (deb, rpm, Flatpak, AppImage). Fixed Flatpak AppStream metadata for app stores.
50
+
51
+ ---
52
+ ## 0.2.14
53
+
54
+ ### Improvements
55
+
56
+ * **Keybinding Map Checkboxes**: Submenu items in the keybinding map now show checkbox indicators for toggled settings.
57
+
58
+ ### Bug Fixes
59
+
60
+ * **Windows Memory Usage**: Fixed high memory usage on Windows caused by buffered input event draining before render (#1205).
61
+
62
+ ---
63
+
3
64
  ## 0.2.13
4
65
 
5
66
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.2.13",
3
+ "version": "0.2.16",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1195,7 +1195,7 @@ registerHandler("review_drill_down", review_drill_down);
1195
1195
 
1196
1196
  // Define the diff-view mode - inherits from "normal" for all standard navigation/selection/copy
1197
1197
  // Only adds diff-specific keybindings (close, hunk navigation)
1198
- editor.defineMode("diff-view", "normal", [
1198
+ editor.defineMode("diff-view", [
1199
1199
  // Close the diff view
1200
1200
  ["q", "close"],
1201
1201
  // Hunk navigation (diff-specific)
@@ -1812,7 +1812,7 @@ registerHandler("on_buffer_closed", on_buffer_closed);
1812
1812
 
1813
1813
  editor.on("buffer_closed", "on_buffer_closed");
1814
1814
 
1815
- editor.defineMode("review-mode", "normal", [
1815
+ editor.defineMode("review-mode", [
1816
1816
  // Staging actions
1817
1817
  ["s", "review_stage_hunk"], ["d", "review_discard_hunk"],
1818
1818
  // Navigation
@@ -72,6 +72,7 @@
72
72
  "double_click_time_ms": 500,
73
73
  "auto_save_enabled": false,
74
74
  "auto_save_interval_secs": 30,
75
+ "hot_exit": true,
75
76
  "recovery_enabled": true,
76
77
  "auto_recovery_save_interval_secs": 2,
77
78
  "auto_revert_poll_interval_ms": 2000,
@@ -489,6 +490,12 @@
489
490
  "default": 30,
490
491
  "x-section": "Recovery"
491
492
  },
493
+ "hot_exit": {
494
+ "description": "Whether to preserve unsaved changes in all buffers (file-backed and\nunnamed) across editor sessions (VS Code \"hot exit\" behavior).\nWhen enabled, modified buffers are backed up on clean exit and their\nunsaved changes are restored on next startup. Unnamed (scratch)\nbuffers are also persisted (Sublime Text / Notepad++ behavior).\nDefault: true",
495
+ "type": "boolean",
496
+ "default": true,
497
+ "x-section": "Recovery"
498
+ },
492
499
  "recovery_enabled": {
493
500
  "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.",
494
501
  "type": "boolean",
@@ -247,10 +247,11 @@ editor.on("buffer_activated", "on_diagnostics_buffer_activated");
247
247
  // Mode Definition (for custom keybindings beyond Enter/Escape)
248
248
  editor.defineMode(
249
249
  "diagnostics-extra",
250
- "diagnostics-results",
251
250
  [
252
251
  ["a", "diagnostics_toggle_all"],
253
252
  ["r", "diagnostics_refresh"],
253
+ ["Return", `_finder_diagnostics_panel_select`],
254
+ ["Escape", `_finder_diagnostics_panel_close`],
254
255
  ],
255
256
  true
256
257
  );
@@ -2,7 +2,7 @@
2
2
 
3
3
  This directory contains example plugins demonstrating the editor's plugin system. These are educational examples showing specific API features.
4
4
 
5
- For the complete API reference, see **[docs/development/plugin-api.md](../../docs/development/plugin-api.md)**.
5
+ For the complete API reference, see **[docs/plugins/api/index.md](../../../../docs/plugins/api/index.md)**.
6
6
 
7
7
  ## Available Examples
8
8
 
@@ -80,5 +80,5 @@ editor.debug("My custom plugin loaded");
80
80
 
81
81
  ## Further Reading
82
82
 
83
- - **Getting Started:** [docs/development/plugin-development.md](../../docs/development/plugin-development.md)
84
- - **API Reference:** [docs/development/plugin-api.md](../../docs/development/plugin-api.md)
83
+ - **Getting Started:** [docs/development/plugin-development.md](../../../../docs/plugins/development/index.md)
84
+ - **API Reference:** [docs/plugins/api/index.md](../../../../docs/plugins/api/index.md)
@@ -5,9 +5,56 @@
5
5
  *
6
6
  * This is a simple example plugin that demonstrates:
7
7
  * - Querying editor state (buffer info, cursor position)
8
+ * - Obtaining selected text and replacing selection (primary cursor, buffer text)
8
9
  * - Sending commands (status messages, text insertion)
9
10
  * - Using async/await for plugin actions
10
11
  */
12
+ // Global action: Replace selection with quoted selection
13
+ async function quote_selection() : void {
14
+ const bufferId = editor.getActiveBufferId();
15
+ const cursorInfo = editor.getPrimaryCursor();
16
+ if (! cursorInfo.selection) {
17
+ editor.setStatus(`Nothing is highlighted!`);
18
+ }
19
+ const startSelection = cursorInfo.selection.start;
20
+ const endSelection = cursorInfo.selection.end;
21
+ const bufText = await
22
+ editor.getBufferText(bufferId, startSelection, endSelection);
23
+
24
+ // Important! Insertions must be done on byte boundaries. But the text
25
+ // may be UTF-8 character and may be multiple bytes.
26
+ // Assumption: a proper selection will be on UTF-8 character boundaries
27
+ // Therefore:
28
+ // a. instert the end quote first to avoid moving the entire string
29
+ // to the right
30
+ // b. insert the start quote second which can now push the string
31
+ // to the right without any problems.
32
+ let success = editor.insertText(bufferId, endSelection, "'");
33
+ if (!success) {
34
+ editor.setStatus("Failed to insert end quote");
35
+ return;
36
+ }
37
+
38
+ success = editor.insertText(bufferId, startSelection, "'");
39
+ if (!success) {
40
+ editor.setStatus("Failed to insert start quote");
41
+ return
42
+ }
43
+ const statusMessage = `Quoted: ${bufText}`;
44
+ editor.setStatus(statusMessage)
45
+ }
46
+ registerHandler("quote_selection", quote_selection);
47
+
48
+ // Global action: Display primary cursor information
49
+ function show_cursor_info() : void {
50
+ const cursorInfo = editor.getPrimaryCursor();
51
+
52
+ const status = `Cursor at ${cursorInfo.position} Selection start-${cursorInfo.selection.start}, end-${cursorInfo.selection.end}`;
53
+
54
+ editor.setStatus(status);
55
+ editor.debug(`Cursor info: ${status}`);
56
+ }
57
+ registerHandler("show_cursor_info", show_cursor_info);
11
58
 
12
59
  // Global action: Display buffer information
13
60
  function show_buffer_info() : void {
@@ -87,6 +134,18 @@ async function async_demo() : Promise<void> {
87
134
  registerHandler("async_demo", async_demo);
88
135
 
89
136
  // Register commands so they appear in the command palette (Ctrl+P)
137
+ editor.registerCommand(
138
+ "Hello: Quote Selection",
139
+ "Quote Selection",
140
+ "quote_selection"
141
+ );
142
+
143
+ editor.registerCommand(
144
+ "Hello: Show Cursor Info",
145
+ "Display primary cursor information",
146
+ "show_cursor_info"
147
+ );
148
+
90
149
  editor.registerCommand(
91
150
  "Hello: Show Buffer Info",
92
151
  "Display active buffer information",
@@ -11,7 +11,6 @@ editor.registerCommand(
11
11
  // Define a custom mode for the demo buffer
12
12
  editor.defineMode(
13
13
  "demo-list", // mode name
14
- null, // no parent mode
15
14
  [
16
15
  ["Return", "demo_goto_item"],
17
16
  ["n", "demo_next_item"],
@@ -98,7 +98,6 @@ const colors = {
98
98
 
99
99
  editor.defineMode(
100
100
  "git-blame",
101
- "normal", // inherit from normal mode for cursor movement
102
101
  [
103
102
  ["b", "git_blame_go_back"],
104
103
  ["q", "git_blame_close"],
@@ -132,7 +132,6 @@ const colors = {
132
132
  // Navigation uses normal cursor movement (arrows, j/k work naturally via parent mode)
133
133
  editor.defineMode(
134
134
  "git-log",
135
- "normal", // inherit from normal mode for cursor movement
136
135
  [
137
136
  ["Return", "git_log_show_commit"],
138
137
  ["Tab", "git_log_show_commit"],
@@ -148,7 +147,6 @@ editor.defineMode(
148
147
  // Inherits from normal mode for natural cursor movement
149
148
  editor.defineMode(
150
149
  "git-commit-detail",
151
- "normal", // inherit from normal mode for cursor movement
152
150
  [
153
151
  ["Return", "git_commit_detail_open_file"],
154
152
  ["q", "git_commit_detail_close"],
@@ -160,7 +158,6 @@ editor.defineMode(
160
158
  // Define git-file-view mode for viewing files at a specific commit
161
159
  editor.defineMode(
162
160
  "git-file-view",
163
- "normal", // inherit from normal mode for cursor movement
164
161
  [
165
162
  ["q", "git_file_view_close"],
166
163
  ["Escape", "git_file_view_close"],
@@ -923,8 +923,7 @@ export class Finder<T> {
923
923
  // Define preview mode
924
924
  this.editor.defineMode(
925
925
  this.previewModeName,
926
- "special",
927
- [["q", "close_buffer"]],
926
+ [["q", "close_buffer"], ["Escape", "close_buffer"]],
928
927
  true
929
928
  );
930
929
 
@@ -978,7 +977,6 @@ export class Finder<T> {
978
977
  // Define panel mode
979
978
  this.editor.defineMode(
980
979
  this.modeName,
981
- "normal",
982
980
  [
983
981
  ["Return", `${this.handlerPrefix}_panel_select`],
984
982
  ["Escape", `${this.handlerPrefix}_panel_close`],
@@ -663,6 +663,36 @@ type CreateVirtualBufferOptions = {
663
663
  */
664
664
  entries?: Array<TextPropertyEntry>;
665
665
  };
666
+ type GrepMatch = {
667
+ /**
668
+ * Absolute file path
669
+ */
670
+ file: string;
671
+ /**
672
+ * Buffer ID if the file is open (0 if not)
673
+ */
674
+ bufferId: number;
675
+ /**
676
+ * Byte offset of match start in the file/buffer content
677
+ */
678
+ byteOffset: number;
679
+ /**
680
+ * Match length in bytes
681
+ */
682
+ length: number;
683
+ /**
684
+ * 1-indexed line number
685
+ */
686
+ line: number;
687
+ /**
688
+ * 1-indexed column number
689
+ */
690
+ column: number;
691
+ /**
692
+ * The matched line content (for display)
693
+ */
694
+ context: string;
695
+ };
666
696
  type LanguagePackConfig = {
667
697
  /**
668
698
  * Comment prefix for line comments (e.g., "//" or "#")
@@ -720,6 +750,16 @@ type LspServerPackConfig = {
720
750
  */
721
751
  processLimits: ProcessLimitsPackConfig | null;
722
752
  };
753
+ type ReplaceResult = {
754
+ /**
755
+ * Number of replacements made
756
+ */
757
+ replacements: number;
758
+ /**
759
+ * Buffer ID of the edited buffer
760
+ */
761
+ bufferId: number;
762
+ };
723
763
  type SpawnResult = {
724
764
  /**
725
765
  * Complete stdout as string
@@ -781,6 +821,11 @@ interface EditorAPI {
781
821
  copyToClipboard(text: string): void;
782
822
  setClipboard(text: string): void;
783
823
  /**
824
+ * Get the display label for a keybinding by action name and optional mode.
825
+ * Returns null if no binding is found.
826
+ */
827
+ getKeybindingLabel(action: string, mode: string | null): string | null;
828
+ /**
784
829
  * Register a command in the command palette (Ctrl+P).
785
830
  *
786
831
  * Usually you should omit `context` so the command is always visible.
@@ -1234,7 +1279,7 @@ interface EditorAPI {
1234
1279
  /**
1235
1280
  * Define a buffer mode (takes bindings as array of [key, command] pairs)
1236
1281
  */
1237
- defineMode(name: string, parent: string | null, bindingsArr: string[][], readOnly?: boolean): boolean;
1282
+ defineMode(name: string, bindingsArr: string[][], readOnly?: boolean, allowTextInput?: boolean): boolean;
1238
1283
  /**
1239
1284
  * Set the global editor mode
1240
1285
  */
@@ -1316,6 +1361,18 @@ interface EditorAPI {
1316
1361
  */
1317
1362
  getViewState(bufferId: number, key: string): unknown;
1318
1363
  /**
1364
+ * Set plugin-managed global state (write-through to snapshot + command for persistence).
1365
+ * State is automatically isolated per plugin using the plugin's name.
1366
+ * TODO: Need to think about plugin isolation / namespacing strategy for these APIs.
1367
+ */
1368
+ setGlobalState(key: string, value: unknown): boolean;
1369
+ /**
1370
+ * Get plugin-managed global state (reads from snapshot).
1371
+ * State is automatically isolated per plugin using the plugin's name.
1372
+ * TODO: Need to think about plugin isolation / namespacing strategy for these APIs.
1373
+ */
1374
+ getGlobalState(key: string): unknown;
1375
+ /**
1319
1376
  * Create a scroll sync group for anchor-based synchronized scrolling
1320
1377
  */
1321
1378
  createScrollSyncGroup(groupId: number, leftSplit: number, rightSplit: number): boolean;
@@ -1399,6 +1456,31 @@ interface EditorAPI {
1399
1456
  */
1400
1457
  delay(durationMs: number): Promise<void>;
1401
1458
  /**
1459
+ * Project-wide grep search (async)
1460
+ * Searches all files in the project, respecting .gitignore.
1461
+ * Open buffers with dirty edits are searched in-memory.
1462
+ */
1463
+ grepProject(pattern: string, fixedString: boolean | null, caseSensitive: boolean | null, maxResults: number | null, wholeWords: boolean | null): Promise<GrepMatch[]>;
1464
+ /**
1465
+ * Streaming project-wide grep search
1466
+ * Returns a thenable with a searchId property. The progressCallback is called
1467
+ * with batches of matches as they are found.
1468
+ */
1469
+ grepProjectStreaming(pattern: string, opts?: {
1470
+ fixedString?: boolean;
1471
+ caseSensitive?: boolean;
1472
+ maxResults?: number;
1473
+ wholeWords?: boolean;
1474
+ }, progressCallback?: (matches: GrepMatch[], done: boolean) => void): PromiseLike<GrepMatch[]> & {
1475
+ searchId: number;
1476
+ };
1477
+ /**
1478
+ * Replace matches in a file's buffer (async)
1479
+ * Opens the file if not already in a buffer, applies edits via the buffer model,
1480
+ * and saves. All edits are grouped as a single undo action.
1481
+ */
1482
+ replaceInFile(filePath: string, matches: number[][], replacement: string): Promise<ReplaceResult>;
1483
+ /**
1402
1484
  * Send LSP request (async, returns request_id)
1403
1485
  */
1404
1486
  sendLspRequest(language: string, method: string, params: Record<string, unknown> | null): Promise<unknown>;
@@ -37,7 +37,7 @@ export interface DebouncedSearchOptions {
37
37
  // Editor interface (subset of what we need)
38
38
  interface EditorApi {
39
39
  readFile(path: string): Promise<string>;
40
- defineMode(name: string, parent: string, bindings: [string, string][], readOnly: boolean): void;
40
+ defineMode(name: string, bindings: [string, string][], readOnly: boolean): void;
41
41
  createVirtualBufferInSplit(options: {
42
42
  name: string;
43
43
  mode: string;
@@ -131,7 +131,7 @@ export class SearchPreview {
131
131
 
132
132
  if (this.bufferId === null) {
133
133
  // Create preview mode if not exists
134
- this.editor.defineMode(this.modeName, "special", [["q", "close_buffer"]], true);
134
+ this.editor.defineMode(this.modeName, [["q", "close_buffer"]], true);
135
135
 
136
136
  // Create preview in a split on the right
137
137
  const result = await this.editor.createVirtualBufferInSplit({