@fresh-editor/fresh-editor 0.3.4 → 0.3.6

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.
@@ -98,10 +98,20 @@ declare global {
98
98
  }
99
99
  }
100
100
 
101
+ // Cap on the number of matches a single search returns. Higher than
102
+ // the previous 100 to actually fit a typical refactor's worth of
103
+ // hits in one snapshot, but bounded so a runaway query doesn't
104
+ // stream the entire codebase into the overlay.
105
+ const MAX_RESULTS = 1000;
106
+
101
107
  // ── Registry ──────────────────────────────────────────────────────
102
108
 
103
109
  const providers: LiveGrepProvider[] = [];
104
110
  let cachedSelected: LiveGrepProvider | null | undefined = undefined;
111
+ // Set by `search` after each query so the toolbar can show
112
+ // "1000+ matches" when a result set was clipped at MAX_RESULTS.
113
+ // Reset to false on every new query (before the provider call).
114
+ let lastSearchTruncated = false;
105
115
 
106
116
  function sortByPriority(): void {
107
117
  providers.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0));
@@ -145,31 +155,57 @@ function unregisterProvider(name: string): boolean {
145
155
  }
146
156
 
147
157
  function updateOverlayTitle(provider: LiveGrepProvider | null): void {
148
- // Reflect the active provider in the floating overlay's frame
149
- // header so the user always knows which backend is producing
150
- // the results, even after the search-result status overwrites
151
- // any one-shot "switched to" message. Append the actual bound
152
- // shortcuts (whatever the user remapped to) as hints pulled
153
- // from the editor's keybinding registry, not hardcoded, so they
154
- // always match the user's actual config.
155
- const hints: string[] = [];
156
- const cycleKey = editor.getKeybindingLabel(
157
- "cycle_live_grep_provider",
158
- "prompt"
158
+ // The input row's prefix already says "Live grep: …", so the
159
+ // frame title doesn't repeat the feature name it's reserved
160
+ // for the active provider plus shortcut hints. Shortcuts come
161
+ // from the keybinding registry (not hardcoded) so labels match
162
+ // the user's actual binds. Each segment carries its own theme
163
+ // key (`ui.help_key_fg` for keys, `ui.popup_border_fg` for
164
+ // separators) so the renderer doesn't have to parse the title.
165
+ // `resume_live_grep` is intentionally NOT shown here — it only
166
+ // matters once the prompt is closed; it's surfaced in the
167
+ // status bar at that point instead.
168
+ const sepStyle = { fg: "ui.popup_border_fg" };
169
+ const hintStyle = { fg: "ui.help_key_fg" };
170
+ const segments: StyledText[] = [];
171
+ const pushSegment = (parts: StyledText[]) => {
172
+ if (segments.length > 0) {
173
+ segments.push({ text: " · ", style: sepStyle });
174
+ }
175
+ segments.push(...parts);
176
+ };
177
+ if (provider) {
178
+ pushSegment([
179
+ { text: "Provider: " },
180
+ { text: provider.name, style: { bold: true } },
181
+ ]);
182
+ }
183
+ // Match-count indicator goes BEFORE the keybinding hints so a
184
+ // narrow terminal that truncates the toolbar still shows it —
185
+ // the trailing hints are easier to lose than the result-set
186
+ // status.
187
+ if (lastSearchTruncated) {
188
+ pushSegment([{ text: `${MAX_RESULTS}+ matches` }]);
189
+ }
190
+ const pushHint = (key: string | null, label: string) => {
191
+ if (!key) return;
192
+ pushSegment([
193
+ { text: key, style: hintStyle },
194
+ { text: ` ${label}` },
195
+ ]);
196
+ };
197
+ pushHint(
198
+ editor.getKeybindingLabel("cycle_live_grep_provider", "prompt"),
199
+ "switch grep provider"
159
200
  );
160
- if (cycleKey) hints.push(`${cycleKey} cycle`);
161
- const exportKey = editor.getKeybindingLabel(
162
- "live_grep_export_quickfix",
163
- "prompt"
201
+ pushHint(
202
+ editor.getKeybindingLabel("live_grep_export_quickfix", "prompt"),
203
+ "save matches"
164
204
  );
165
- if (exportKey) hints.push(`${exportKey} Quickfix`);
166
- const resumeKey = editor.getKeybindingLabel("resume_live_grep", "normal");
167
- if (resumeKey) hints.push(`${resumeKey} resume`);
168
- const hintSuffix = hints.length > 0 ? ` · ${hints.join(" · ")}` : "";
169
- const label = provider
170
- ? `Live Grep · ${provider.name}${hintSuffix}`
171
- : `Live Grep${hintSuffix}`;
172
- editor.setPromptTitle(label);
205
+ if (segments.length > 0) {
206
+ segments.push({ text: " " });
207
+ }
208
+ editor.setPromptTitle(segments);
173
209
  }
174
210
 
175
211
  async function selectProvider(): Promise<LiveGrepProvider | null> {
@@ -198,7 +234,7 @@ async function selectProvider(): Promise<LiveGrepProvider | null> {
198
234
  // ── Built-in providers ──────────────────────────────────────────
199
235
 
200
236
  registerProvider({
201
- name: "ripgrep",
237
+ name: "rg",
202
238
  priority: -1,
203
239
  isAvailable: async () => {
204
240
  try {
@@ -228,9 +264,9 @@ registerProvider({
228
264
  cwd
229
265
  );
230
266
  if (r.exit_code === 0) {
231
- return parseGrepOutput(r.stdout, maxResults) as GrepMatch[];
267
+ return parseGrepOutput(r.stdout, maxResults, (msg) => editor.debug(msg)) as GrepMatch[];
232
268
  }
233
- return [];
269
+ throw new Error(`rg exited with code ${r.exit_code}: ${r.stderr}`);
234
270
  },
235
271
  });
236
272
 
@@ -264,9 +300,9 @@ registerProvider({
264
300
  cwd
265
301
  );
266
302
  if (r.exit_code === 0 || r.exit_code === 1) {
267
- return parseGrepOutput(r.stdout, maxResults) as GrepMatch[];
303
+ return parseGrepOutput(r.stdout, maxResults, (msg) => editor.debug(msg)) as GrepMatch[];
268
304
  }
269
- return [];
305
+ throw new Error(`ag exited with code ${r.exit_code}: ${r.stderr}`);
270
306
  },
271
307
  });
272
308
 
@@ -301,9 +337,9 @@ registerProvider({
301
337
  );
302
338
  // git grep exits 1 when no matches — treat as empty, not error.
303
339
  if (r.exit_code === 0 || r.exit_code === 1) {
304
- return parseGrepOutput(r.stdout, maxResults) as GrepMatch[];
340
+ return parseGrepOutput(r.stdout, maxResults, (msg) => editor.debug(msg)) as GrepMatch[];
305
341
  }
306
- return [];
342
+ throw new Error(`git grep exited with code ${r.exit_code}: ${r.stderr}`);
307
343
  },
308
344
  });
309
345
 
@@ -334,9 +370,9 @@ registerProvider({
334
370
  cwd
335
371
  );
336
372
  if (r.exit_code === 0 || r.exit_code === 1) {
337
- return parseGrepOutput(r.stdout, maxResults) as GrepMatch[];
373
+ return parseGrepOutput(r.stdout, maxResults, (msg) => editor.debug(msg)) as GrepMatch[];
338
374
  }
339
- return [];
375
+ throw new Error(`ack exited with code ${r.exit_code}: ${r.stderr}`);
340
376
  },
341
377
  });
342
378
 
@@ -366,7 +402,6 @@ registerProvider({
366
402
  "grep",
367
403
  [
368
404
  "-rn",
369
- "--column",
370
405
  "-I",
371
406
  "--exclude-dir=.git",
372
407
  "--exclude-dir=node_modules",
@@ -378,13 +413,11 @@ registerProvider({
378
413
  cwd
379
414
  );
380
415
  if (r.exit_code === 0 || r.exit_code === 1) {
381
- // POSIX grep doesn't emit `path:line:col:content` natively
382
- // even with `--column`; on most BSD/GNU greps the format is
383
- // still `path:line:content`. parseGrepOutput tolerates the
384
- // missing column.
385
- return parseGrepOutput(r.stdout, maxResults) as GrepMatch[];
416
+ // grep emits `path:line:content` (no column). parseGrepOutput's
417
+ // 3-field fallback handles the missing column (defaults to 1).
418
+ return parseGrepOutput(r.stdout, maxResults, (msg) => editor.debug(msg)) as GrepMatch[];
386
419
  }
387
- return [];
420
+ throw new Error(`grep exited with code ${r.exit_code}: ${r.stderr}`);
388
421
  },
389
422
  });
390
423
 
@@ -404,8 +437,26 @@ const finder = new Finder<GrepMatch>(editor, {
404
437
  column: match.column,
405
438
  },
406
439
  }),
440
+ // Override the Finder's default "open file + status: Opened X"
441
+ // so we can surface the resume shortcut here. The shortcut is
442
+ // hidden inside the overlay (it can't apply while the overlay
443
+ // is open), but it's exactly what the user needs to know once
444
+ // they've jumped to a result and want to keep browsing.
445
+ onSelect: (_item, entry) => {
446
+ if (entry.location) {
447
+ editor.openFile(
448
+ entry.location.file,
449
+ entry.location.line,
450
+ entry.location.column
451
+ );
452
+ }
453
+ const resumeKey = editor.getKeybindingLabel("resume_live_grep", "normal");
454
+ if (resumeKey) {
455
+ editor.setStatus(`${resumeKey} to resume search`);
456
+ }
457
+ },
407
458
  preview: false,
408
- maxResults: 100,
459
+ maxResults: MAX_RESULTS,
409
460
  });
410
461
 
411
462
  /**
@@ -483,19 +534,34 @@ editor.registerCommand(
483
534
  async function search(query: string): Promise<GrepMatch[]> {
484
535
  const provider = await selectProvider();
485
536
  if (!provider) {
486
- editor.setStatus(
487
- "Live Grep: no search backend available install ripgrep, or register a provider via init.ts (`editor.getPluginApi(\"live-grep\")?.registerProvider(...)`)."
537
+ // Throw rather than return [] — the Finder catches and shows
538
+ // the message inside the overlay. Returning [] would be
539
+ // indistinguishable from a real "no matches" result.
540
+ throw new Error(
541
+ "no search backend available — install ripgrep, or register a provider via init.ts (`editor.getPluginApi(\"live-grep\")?.registerProvider(...)`)."
488
542
  );
489
- return [];
490
543
  }
544
+ const wasTruncated = lastSearchTruncated;
491
545
  try {
492
- return await provider.search(query, {
546
+ const results = await provider.search(query, {
493
547
  cwd: editor.getCwd(),
494
- maxResults: 100,
548
+ maxResults: MAX_RESULTS,
495
549
  });
550
+ lastSearchTruncated = results.length >= MAX_RESULTS;
551
+ // Refresh the toolbar whenever the truncation indicator
552
+ // changes so it appears (or disappears) alongside the new
553
+ // results in the same render.
554
+ if (lastSearchTruncated !== wasTruncated) {
555
+ updateOverlayTitle(provider);
556
+ }
557
+ return results;
496
558
  } catch (e) {
497
- editor.setStatus(`Live Grep (${provider.name}) failed: ${e}`);
498
- return [];
559
+ // Log to tracing for diagnostics, then re-throw so the Finder
560
+ // surfaces the failure in the overlay itself.
561
+ editor.error(`[live_grep:${provider.name}] ${e}`);
562
+ throw new Error(
563
+ `${provider.name}: ${e instanceof Error ? e.message : String(e)}`
564
+ );
499
565
  }
500
566
  }
501
567
 
@@ -512,7 +578,7 @@ function start_live_grep(): void {
512
578
  });
513
579
  // Pre-populate the overlay's frame title with the cached
514
580
  // provider name (if any) before the user types — avoids the
515
- // brief "Live Grep" → "Live Grep · ripgrep" flash when the
581
+ // brief "Live Grep" → "Live Grep · rg" flash when the
516
582
  // first search resolves selectProvider().
517
583
  if (cachedSelected) {
518
584
  updateOverlayTitle(cachedSelected);