@fresh-editor/fresh-editor 0.1.58 → 0.1.63

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.
@@ -89,7 +89,8 @@ globalThis.onGitGrepPromptChanged = function(args: {
89
89
  }
90
90
 
91
91
  // Spawn git grep asynchronously
92
- editor.spawnProcess("git", ["grep", "-n", "--column", "-I", "--", query])
92
+ const cwd = editor.getCwd();
93
+ editor.spawnProcess("git", ["grep", "-n", "--column", "-I", "--", query], cwd)
93
94
  .then((result) => {
94
95
  if (result.exit_code === 0) {
95
96
  // Parse results and update suggestions
@@ -81,6 +81,16 @@ interface LayoutHints {
81
81
  column_guides?: number[] | null;
82
82
  }
83
83
 
84
+ /** Handle for a cancellable process spawned with spawnProcess */
85
+ interface ProcessHandle extends PromiseLike<SpawnResult> {
86
+ /** Promise that resolves to the process ID */
87
+ readonly processId: Promise<number>;
88
+ /** Promise that resolves to the result when the process completes */
89
+ readonly result: Promise<SpawnResult>;
90
+ /** Kill the process. Returns true if killed, false if already completed */
91
+ kill(): Promise<boolean>;
92
+ }
93
+
84
94
  /** Result from spawnProcess */
85
95
  interface SpawnResult {
86
96
  /** Complete stdout as string. Newlines preserved; trailing newline included. */
@@ -626,15 +636,31 @@ interface EditorAPI {
626
636
  */
627
637
  spawnBackgroundProcess(command: string, args: string[], cwd?: string | null): Promise<BackgroundProcessResult>;
628
638
  /**
629
- * Kill a background process by ID
639
+ * Kill a background or cancellable process by ID
630
640
  *
631
641
  * Sends SIGTERM to gracefully terminate the process.
632
642
  * Returns true if the process was found and killed, false if not found.
633
643
  *
634
- * @param process_id - ID returned from spawnBackgroundProcess
644
+ * @param process_id - ID returned from spawnBackgroundProcess or spawnProcessStart
635
645
  * @returns true if process was killed, false if not found
636
646
  */
637
647
  killProcess(#[bigint] process_id: number): Promise<boolean>;
648
+ /**
649
+ * Wait for a cancellable process to complete and get its result
650
+ *
651
+ * @param process_id - ID returned from spawnProcessStart
652
+ * @returns SpawnResult with stdout, stderr, and exit_code
653
+ */
654
+ spawnProcessWait(#[bigint] process_id: number): Promise<SpawnResult>;
655
+ /**
656
+ * Delay execution for a specified number of milliseconds
657
+ *
658
+ * Useful for debouncing user input or adding delays between operations.
659
+ * @param ms - Number of milliseconds to delay
660
+ * @example
661
+ * await editor.delay(100); // Wait 100ms
662
+ */
663
+ delay(#[bigint] ms: number): Promise<[]>;
638
664
  /**
639
665
  * Start a prompt with pre-filled initial value
640
666
  * @param label - Label to display (e.g., "Git grep: ")
@@ -672,24 +698,24 @@ interface EditorAPI {
672
698
  */
673
699
  setBufferCursor(buffer_id: number, position: number): boolean;
674
700
 
675
- // === Async Operations ===
676
701
  /**
677
- * Run an external command and capture its output
702
+ * Spawn an external process and return a cancellable handle
678
703
  *
679
- * Waits for process to complete before returning. For long-running processes,
680
- * consider if this will block your plugin. Output is captured completely;
681
- * very large outputs may use significant memory.
704
+ * Returns a ProcessHandle that can be awaited for the result or killed early.
705
+ * The handle is also a PromiseLike, so `await spawnProcess(...)` works directly.
682
706
  * @param command - Program name (searched in PATH) or absolute path
683
707
  * @param args - Command arguments (each array element is one argument)
684
708
  * @param cwd - Working directory; null uses editor's cwd
685
709
  * @example
686
- * const result = await editor.spawnProcess("git", ["log", "--oneline", "-5"]);
687
- * if (result.exit_code !== 0) {
688
- * editor.setStatus(`git failed: ${result.stderr}`);
689
- * }
710
+ * // Simple usage (backward compatible)
711
+ * const result = await editor.spawnProcess("git", ["status"]);
712
+ *
713
+ * // Cancellable usage
714
+ * const search = editor.spawnProcess("rg", ["pattern"]);
715
+ * // ... later, if user types new query:
716
+ * search.kill(); // Cancel the search
690
717
  */
691
- spawnProcess(command: string, args: string[], cwd?: string | null): Promise<SpawnResult>;
692
-
718
+ spawnProcess(command: string, args?: string[], cwd?: string | null): ProcessHandle;
693
719
  // === Overlay Operations ===
694
720
  /**
695
721
  * Add a colored highlight overlay to text without modifying content
@@ -22,8 +22,12 @@ let previewBufferId: number | null = null;
22
22
  let previewSplitId: number | null = null;
23
23
  let originalSplitId: number | null = null;
24
24
  let lastQuery: string = "";
25
- let searchDebounceTimer: number | null = null;
26
25
  let previewCreated: boolean = false;
26
+ let currentSearch: ProcessHandle | null = null;
27
+ let pendingKill: Promise<boolean> | null = null; // Track pending kill globally
28
+ let searchVersion = 0; // Incremented on each input change for debouncing
29
+
30
+ const DEBOUNCE_MS = 150; // Wait 150ms after last keystroke before searching
27
31
 
28
32
  // Parse ripgrep output line
29
33
  // Format: file:line:column:content
@@ -169,22 +173,62 @@ function closePreview(): void {
169
173
  }
170
174
  }
171
175
 
172
- // Run ripgrep search
176
+ // Run ripgrep search with debouncing
173
177
  async function runSearch(query: string): Promise<void> {
178
+ // Increment version to invalidate any pending debounced search
179
+ const thisVersion = ++searchVersion;
180
+ editor.debug(`[live_grep] runSearch called: query="${query}", version=${thisVersion}`);
181
+
182
+ // Kill any existing search immediately (don't wait) to stop wasting CPU
183
+ // Store the kill promise globally so ALL pending searches wait for it
184
+ if (currentSearch) {
185
+ editor.debug(`[live_grep] killing existing search immediately`);
186
+ pendingKill = currentSearch.kill();
187
+ currentSearch = null;
188
+ }
189
+
174
190
  if (!query || query.trim().length < 2) {
191
+ // Wait for any pending kill to complete before returning
192
+ if (pendingKill) {
193
+ await pendingKill;
194
+ pendingKill = null;
195
+ }
196
+ editor.debug(`[live_grep] query too short, clearing`);
175
197
  editor.setPromptSuggestions([]);
176
198
  grepResults = [];
177
199
  return;
178
200
  }
179
201
 
202
+ // Debounce: wait a bit to see if user is still typing
203
+ editor.debug(`[live_grep] debouncing for ${DEBOUNCE_MS}ms...`);
204
+ await editor.delay(DEBOUNCE_MS);
205
+
206
+ // Always await any pending kill before continuing - ensures old process is dead
207
+ if (pendingKill) {
208
+ editor.debug(`[live_grep] waiting for previous search to terminate`);
209
+ await pendingKill;
210
+ pendingKill = null;
211
+ editor.debug(`[live_grep] previous search terminated`);
212
+ }
213
+
214
+ // If version changed during delay, a newer search was triggered - abort this one
215
+ if (searchVersion !== thisVersion) {
216
+ editor.debug(`[live_grep] version mismatch after debounce (${thisVersion} vs ${searchVersion}), aborting`);
217
+ return;
218
+ }
219
+
180
220
  // Avoid duplicate searches
181
221
  if (query === lastQuery) {
222
+ editor.debug(`[live_grep] duplicate query, skipping`);
182
223
  return;
183
224
  }
184
225
  lastQuery = query;
185
226
 
186
227
  try {
187
- const result = await editor.spawnProcess("rg", [
228
+ const cwd = editor.getCwd();
229
+ editor.debug(`[live_grep] spawning rg for query="${query}" in cwd="${cwd}"`);
230
+ const searchStartTime = Date.now();
231
+ const search = editor.spawnProcess("rg", [
188
232
  "--line-number",
189
233
  "--column",
190
234
  "--no-heading",
@@ -197,10 +241,25 @@ async function runSearch(query: string): Promise<void> {
197
241
  "-g", "!*.lock",
198
242
  "--",
199
243
  query,
200
- ]);
244
+ ], cwd);
245
+
246
+ currentSearch = search;
247
+ editor.debug(`[live_grep] awaiting search result...`);
248
+ const result = await search;
249
+ const searchDuration = Date.now() - searchStartTime;
250
+ editor.debug(`[live_grep] rg completed in ${searchDuration}ms, exit_code=${result.exit_code}, stdout_len=${result.stdout.length}`);
251
+
252
+ // Check if this search was cancelled (a new search started)
253
+ if (currentSearch !== search) {
254
+ editor.debug(`[live_grep] search was superseded, discarding results`);
255
+ return; // Discard stale results
256
+ }
257
+ currentSearch = null;
201
258
 
202
259
  if (result.exit_code === 0) {
260
+ const parseStart = Date.now();
203
261
  const { results, suggestions } = parseRipgrepOutput(result.stdout);
262
+ editor.debug(`[live_grep] parsed ${results.length} results in ${Date.now() - parseStart}ms`);
204
263
  grepResults = results;
205
264
  editor.setPromptSuggestions(suggestions);
206
265
 
@@ -213,14 +272,24 @@ async function runSearch(query: string): Promise<void> {
213
272
  }
214
273
  } else if (result.exit_code === 1) {
215
274
  // No matches
275
+ editor.debug(`[live_grep] no matches (exit_code=1)`);
216
276
  grepResults = [];
217
277
  editor.setPromptSuggestions([]);
218
278
  editor.setStatus("No matches found");
279
+ } else if (result.exit_code === -1) {
280
+ // Process was killed, ignore
281
+ editor.debug(`[live_grep] process was killed`);
219
282
  } else {
283
+ editor.debug(`[live_grep] search error: ${result.stderr}`);
220
284
  editor.setStatus(`Search error: ${result.stderr}`);
221
285
  }
222
286
  } catch (e) {
223
- editor.setStatus(`Search error: ${e}`);
287
+ // Ignore errors from killed processes
288
+ const errorMsg = String(e);
289
+ editor.debug(`[live_grep] caught error: ${errorMsg}`);
290
+ if (!errorMsg.includes("killed") && !errorMsg.includes("not found")) {
291
+ editor.setStatus(`Search error: ${e}`);
292
+ }
224
293
  }
225
294
  }
226
295
 
@@ -248,12 +317,9 @@ globalThis.onLiveGrepPromptChanged = function (args: {
248
317
  return true;
249
318
  }
250
319
 
251
- // Debounce search to avoid too many requests while typing
252
- if (searchDebounceTimer !== null) {
253
- // Can't actually cancel in this runtime, but we track it
254
- }
320
+ editor.debug(`[live_grep] onPromptChanged: input="${args.input}"`);
255
321
 
256
- // Run search (with small delay effect via async)
322
+ // runSearch handles debouncing internally
257
323
  runSearch(args.input);
258
324
 
259
325
  return true;
@@ -286,6 +352,12 @@ globalThis.onLiveGrepPromptConfirmed = function (args: {
286
352
  return true;
287
353
  }
288
354
 
355
+ // Kill any running search
356
+ if (currentSearch) {
357
+ currentSearch.kill();
358
+ currentSearch = null;
359
+ }
360
+
289
361
  // Close preview first
290
362
  closePreview();
291
363
 
@@ -314,6 +386,12 @@ globalThis.onLiveGrepPromptCancelled = function (args: {
314
386
  return true;
315
387
  }
316
388
 
389
+ // Kill any running search
390
+ if (currentSearch) {
391
+ currentSearch.kill();
392
+ currentSearch = null;
393
+ }
394
+
317
395
  // Close preview and cleanup
318
396
  closePreview();
319
397
  grepResults = [];
@@ -185,7 +185,8 @@ async function performSearch(pattern: string, replace: string, isRegex: boolean)
185
185
  args.push("--", pattern);
186
186
 
187
187
  try {
188
- const result = await editor.spawnProcess("git", args);
188
+ const cwd = editor.getCwd();
189
+ const result = await editor.spawnProcess("git", args, cwd);
189
190
 
190
191
  searchResults = [];
191
192
 
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "dracula",
3
+ "editor": {
4
+ "bg": [40, 42, 54],
5
+ "fg": [248, 248, 242],
6
+ "cursor": [255, 121, 198],
7
+ "selection_bg": [68, 71, 90],
8
+ "current_line_bg": [68, 71, 90],
9
+ "line_number_fg": [98, 114, 164],
10
+ "line_number_bg": [40, 42, 54]
11
+ },
12
+ "ui": {
13
+ "tab_active_fg": [248, 248, 242],
14
+ "tab_active_bg": [189, 147, 249],
15
+ "tab_inactive_fg": [248, 248, 242],
16
+ "tab_inactive_bg": [68, 71, 90],
17
+ "tab_separator_bg": [40, 42, 54],
18
+ "status_bar_fg": [40, 42, 54],
19
+ "status_bar_bg": [189, 147, 249],
20
+ "prompt_fg": [40, 42, 54],
21
+ "prompt_bg": [80, 250, 123],
22
+ "prompt_selection_fg": [248, 248, 242],
23
+ "prompt_selection_bg": [189, 147, 249],
24
+ "popup_border_fg": [98, 114, 164],
25
+ "popup_bg": [68, 71, 90],
26
+ "popup_selection_bg": [189, 147, 249],
27
+ "popup_text_fg": [248, 248, 242],
28
+ "suggestion_bg": [68, 71, 90],
29
+ "suggestion_selected_bg": [189, 147, 249],
30
+ "help_bg": [40, 42, 54],
31
+ "help_fg": [248, 248, 242],
32
+ "help_key_fg": [139, 233, 253],
33
+ "help_separator_fg": [98, 114, 164],
34
+ "help_indicator_fg": [255, 85, 85],
35
+ "help_indicator_bg": [40, 42, 54],
36
+ "split_separator_fg": [98, 114, 164]
37
+ },
38
+ "search": {
39
+ "match_bg": [241, 250, 140],
40
+ "match_fg": [40, 42, 54]
41
+ },
42
+ "diagnostic": {
43
+ "error_fg": [255, 85, 85],
44
+ "error_bg": [64, 42, 54],
45
+ "warning_fg": [241, 250, 140],
46
+ "warning_bg": [64, 60, 42],
47
+ "info_fg": [139, 233, 253],
48
+ "info_bg": [40, 56, 70],
49
+ "hint_fg": [98, 114, 164],
50
+ "hint_bg": [40, 42, 54]
51
+ },
52
+ "syntax": {
53
+ "keyword": [255, 121, 198],
54
+ "string": [241, 250, 140],
55
+ "comment": [98, 114, 164],
56
+ "function": [80, 250, 123],
57
+ "type": [139, 233, 253],
58
+ "variable": [248, 248, 242],
59
+ "constant": [189, 147, 249],
60
+ "operator": [255, 121, 198]
61
+ }
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "nord",
3
+ "editor": {
4
+ "bg": [46, 52, 64],
5
+ "fg": [216, 222, 233],
6
+ "cursor": [136, 192, 208],
7
+ "selection_bg": [67, 76, 94],
8
+ "current_line_bg": [59, 66, 82],
9
+ "line_number_fg": [76, 86, 106],
10
+ "line_number_bg": [46, 52, 64]
11
+ },
12
+ "ui": {
13
+ "tab_active_fg": [236, 239, 244],
14
+ "tab_active_bg": [67, 76, 94],
15
+ "tab_inactive_fg": [216, 222, 233],
16
+ "tab_inactive_bg": [59, 66, 82],
17
+ "tab_separator_bg": [46, 52, 64],
18
+ "status_bar_fg": [46, 52, 64],
19
+ "status_bar_bg": [136, 192, 208],
20
+ "prompt_fg": [46, 52, 64],
21
+ "prompt_bg": [163, 190, 140],
22
+ "prompt_selection_fg": [236, 239, 244],
23
+ "prompt_selection_bg": [94, 129, 172],
24
+ "popup_border_fg": [76, 86, 106],
25
+ "popup_bg": [59, 66, 82],
26
+ "popup_selection_bg": [94, 129, 172],
27
+ "popup_text_fg": [216, 222, 233],
28
+ "suggestion_bg": [59, 66, 82],
29
+ "suggestion_selected_bg": [94, 129, 172],
30
+ "help_bg": [46, 52, 64],
31
+ "help_fg": [216, 222, 233],
32
+ "help_key_fg": [136, 192, 208],
33
+ "help_separator_fg": [76, 86, 106],
34
+ "help_indicator_fg": [191, 97, 106],
35
+ "help_indicator_bg": [46, 52, 64],
36
+ "split_separator_fg": [76, 86, 106]
37
+ },
38
+ "search": {
39
+ "match_bg": [235, 203, 139],
40
+ "match_fg": [46, 52, 64]
41
+ },
42
+ "diagnostic": {
43
+ "error_fg": [191, 97, 106],
44
+ "error_bg": [59, 46, 50],
45
+ "warning_fg": [235, 203, 139],
46
+ "warning_bg": [59, 56, 46],
47
+ "info_fg": [129, 161, 193],
48
+ "info_bg": [46, 52, 64],
49
+ "hint_fg": [76, 86, 106],
50
+ "hint_bg": [46, 52, 64]
51
+ },
52
+ "syntax": {
53
+ "keyword": [129, 161, 193],
54
+ "string": [163, 190, 140],
55
+ "comment": [76, 86, 106],
56
+ "function": [136, 192, 208],
57
+ "type": [143, 188, 187],
58
+ "variable": [216, 222, 233],
59
+ "constant": [180, 142, 173],
60
+ "operator": [129, 161, 193]
61
+ }
62
+ }
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "solarized-dark",
3
+ "editor": {
4
+ "bg": [0, 43, 54],
5
+ "fg": [131, 148, 150],
6
+ "cursor": [38, 139, 210],
7
+ "selection_bg": [7, 54, 66],
8
+ "current_line_bg": [7, 54, 66],
9
+ "line_number_fg": [88, 110, 117],
10
+ "line_number_bg": [0, 43, 54]
11
+ },
12
+ "ui": {
13
+ "tab_active_fg": [253, 246, 227],
14
+ "tab_active_bg": [38, 139, 210],
15
+ "tab_inactive_fg": [131, 148, 150],
16
+ "tab_inactive_bg": [7, 54, 66],
17
+ "tab_separator_bg": [0, 43, 54],
18
+ "status_bar_fg": [0, 43, 54],
19
+ "status_bar_bg": [147, 161, 161],
20
+ "prompt_fg": [0, 43, 54],
21
+ "prompt_bg": [181, 137, 0],
22
+ "prompt_selection_fg": [253, 246, 227],
23
+ "prompt_selection_bg": [38, 139, 210],
24
+ "popup_border_fg": [88, 110, 117],
25
+ "popup_bg": [7, 54, 66],
26
+ "popup_selection_bg": [38, 139, 210],
27
+ "popup_text_fg": [131, 148, 150],
28
+ "suggestion_bg": [7, 54, 66],
29
+ "suggestion_selected_bg": [38, 139, 210],
30
+ "help_bg": [0, 43, 54],
31
+ "help_fg": [131, 148, 150],
32
+ "help_key_fg": [42, 161, 152],
33
+ "help_separator_fg": [88, 110, 117],
34
+ "help_indicator_fg": [220, 50, 47],
35
+ "help_indicator_bg": [0, 43, 54],
36
+ "split_separator_fg": [88, 110, 117]
37
+ },
38
+ "search": {
39
+ "match_bg": [181, 137, 0],
40
+ "match_fg": [253, 246, 227]
41
+ },
42
+ "diagnostic": {
43
+ "error_fg": [220, 50, 47],
44
+ "error_bg": [42, 43, 54],
45
+ "warning_fg": [181, 137, 0],
46
+ "warning_bg": [30, 54, 54],
47
+ "info_fg": [38, 139, 210],
48
+ "info_bg": [0, 50, 66],
49
+ "hint_fg": [88, 110, 117],
50
+ "hint_bg": [0, 43, 54]
51
+ },
52
+ "syntax": {
53
+ "keyword": [133, 153, 0],
54
+ "string": [42, 161, 152],
55
+ "comment": [88, 110, 117],
56
+ "function": [38, 139, 210],
57
+ "type": [181, 137, 0],
58
+ "variable": [131, 148, 150],
59
+ "constant": [203, 75, 22],
60
+ "operator": [131, 148, 150]
61
+ }
62
+ }