@fresh-editor/fresh-editor 0.3.4 → 0.3.5
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 +16 -0
- package/package.json +1 -1
- package/plugins/git_grep.ts +3 -1
- package/plugins/lib/finder.ts +27 -6
- package/plugins/lib/fresh.d.ts +14 -5
- package/plugins/live_grep.ts +114 -48
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## 0.3.5
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
* **Live Grep overlay polish**: Surrounding editor is now visibly dimmed; shortcut hints and the active provider moved into a toolbar row with plainer labels; match cap raised from 100 to 1000 (with a `1000+ matches` indicator); provider errors render as a disabled result entry instead of a silent "0 matches"; `ripgrep` provider renamed to `rg`.
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* **Live Grep on Windows returned no results**: `git grep` outputs `\r\n`; splitting on `\n` left a trailing `\r` that broke the result regex. Split on `\r?\n` instead.
|
|
12
|
+
|
|
13
|
+
* **Plain `grep` provider was broken**: `--column` is a ripgrep-only flag and was being passed unconditionally. Removed.
|
|
14
|
+
|
|
15
|
+
### Under the Hood
|
|
16
|
+
|
|
17
|
+
* **Plugin API: `setPromptTitle` takes styled segments** (`{ text, style? }[]`) instead of a single string, so plugins control hint colouring directly instead of the renderer guessing structure from punctuation.
|
|
18
|
+
|
|
3
19
|
## 0.3.4
|
|
4
20
|
|
|
5
21
|
### Features
|
package/package.json
CHANGED
package/plugins/git_grep.ts
CHANGED
|
@@ -48,8 +48,10 @@ async function searchWithGitGrep(query: string): Promise<GrepMatch[]> {
|
|
|
48
48
|
);
|
|
49
49
|
|
|
50
50
|
if (result.exit_code === 0) {
|
|
51
|
-
return parseGrepOutput(result.stdout, 100) as GrepMatch[];
|
|
51
|
+
return parseGrepOutput(result.stdout, 100, (msg) => editor.debug(msg)) as GrepMatch[];
|
|
52
52
|
}
|
|
53
|
+
editor.error(`[git_grep] process exited with code ${result.exit_code}: ${result.stderr}`);
|
|
54
|
+
editor.setStatus(`git grep failed (exit ${result.exit_code})`);
|
|
53
55
|
return [];
|
|
54
56
|
}
|
|
55
57
|
|
package/plugins/lib/finder.ts
CHANGED
|
@@ -346,7 +346,8 @@ export function parseGrepLine(line: string): {
|
|
|
346
346
|
*/
|
|
347
347
|
export function parseGrepOutput(
|
|
348
348
|
stdout: string,
|
|
349
|
-
maxResults: number = 100
|
|
349
|
+
maxResults: number = 100,
|
|
350
|
+
debug?: (msg: string) => void
|
|
350
351
|
): Array<{ file: string; line: number; column: number; content: string }> {
|
|
351
352
|
const results: Array<{
|
|
352
353
|
file: string;
|
|
@@ -355,14 +356,16 @@ export function parseGrepOutput(
|
|
|
355
356
|
content: string;
|
|
356
357
|
}> = [];
|
|
357
358
|
|
|
358
|
-
for (const line of stdout.split(
|
|
359
|
-
if (!line
|
|
359
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
360
|
+
if (!line) continue;
|
|
360
361
|
const match = parseGrepLine(line);
|
|
361
362
|
if (match) {
|
|
362
363
|
results.push(match);
|
|
363
364
|
if (results.length >= maxResults) {
|
|
364
365
|
break;
|
|
365
366
|
}
|
|
367
|
+
} else if (debug) {
|
|
368
|
+
debug(`[parseGrepOutput] failed to parse line: ${line}`);
|
|
366
369
|
}
|
|
367
370
|
}
|
|
368
371
|
|
|
@@ -784,7 +787,8 @@ export class Finder<T> {
|
|
|
784
787
|
// Parse as grep output by default
|
|
785
788
|
const parsed = parseGrepOutput(
|
|
786
789
|
result.stdout,
|
|
787
|
-
this.config.maxResults
|
|
790
|
+
this.config.maxResults,
|
|
791
|
+
(msg) => this.editor.debug(msg)
|
|
788
792
|
) as unknown as T[];
|
|
789
793
|
this.updatePromptResults(parsed);
|
|
790
794
|
|
|
@@ -827,9 +831,26 @@ export class Finder<T> {
|
|
|
827
831
|
}
|
|
828
832
|
} catch (e) {
|
|
829
833
|
const errorMsg = String(e);
|
|
830
|
-
|
|
831
|
-
|
|
834
|
+
// "killed" / "not found" come from the cancellation path (a
|
|
835
|
+
// newer search aborted this one). Suppress those entirely —
|
|
836
|
+
// the user didn't ask for them.
|
|
837
|
+
if (errorMsg.includes("killed") || errorMsg.includes("not found")) {
|
|
838
|
+
return;
|
|
832
839
|
}
|
|
840
|
+
// Render the error inside the overlay's result list itself.
|
|
841
|
+
// The status bar is shared and clobbered by other code paths,
|
|
842
|
+
// so it's not a reliable place to surface a feature-scoped
|
|
843
|
+
// error — the overlay is where the user is looking.
|
|
844
|
+
this.promptState.results = [];
|
|
845
|
+
this.promptState.entries = [];
|
|
846
|
+
const display = errorMsg.replace(/^Error:\s*/, "");
|
|
847
|
+
this.editor.setPromptSuggestions([
|
|
848
|
+
{
|
|
849
|
+
text: `⚠ ${display}`,
|
|
850
|
+
value: "",
|
|
851
|
+
disabled: true,
|
|
852
|
+
},
|
|
853
|
+
]);
|
|
833
854
|
}
|
|
834
855
|
}
|
|
835
856
|
|
package/plugins/lib/fresh.d.ts
CHANGED
|
@@ -989,6 +989,10 @@ type SpawnResult = {
|
|
|
989
989
|
*/
|
|
990
990
|
exit_code: number;
|
|
991
991
|
};
|
|
992
|
+
type StyledText = {
|
|
993
|
+
text: string;
|
|
994
|
+
style?: Partial<OverlayOptions>;
|
|
995
|
+
};
|
|
992
996
|
type TextPropertiesAtCursor = Array<Record<string, unknown>>;
|
|
993
997
|
type TsHighlightSpan = {
|
|
994
998
|
start: number;
|
|
@@ -1735,11 +1739,16 @@ interface EditorAPI {
|
|
|
1735
1739
|
setPromptInputSync(sync: boolean): boolean;
|
|
1736
1740
|
/**
|
|
1737
1741
|
* Set the title shown in the floating-overlay prompt's frame
|
|
1738
|
-
* header (issue #1796)
|
|
1739
|
-
*
|
|
1740
|
-
*
|
|
1741
|
-
|
|
1742
|
-
|
|
1742
|
+
* header (issue #1796) as styled segments. Each segment
|
|
1743
|
+
* carries optional `Partial<OverlayOptions>`, the same
|
|
1744
|
+
* styling primitive used by virtual text — plugins mark
|
|
1745
|
+
* keybinding hints with `{ fg: "ui.help_key_fg" }`,
|
|
1746
|
+
* separators with `{ fg: "ui.popup_border_fg" }`, etc. Pass
|
|
1747
|
+
* an empty array to clear the title and fall back to the
|
|
1748
|
+
* prompt-type default. Has no visible effect on non-overlay
|
|
1749
|
+
* prompts.
|
|
1750
|
+
*/
|
|
1751
|
+
setPromptTitle(title: StyledText[]): boolean;
|
|
1743
1752
|
/**
|
|
1744
1753
|
* Define a buffer mode (takes bindings as array of [key, command] pairs)
|
|
1745
1754
|
*/
|
package/plugins/live_grep.ts
CHANGED
|
@@ -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
|
-
//
|
|
149
|
-
//
|
|
150
|
-
// the
|
|
151
|
-
//
|
|
152
|
-
//
|
|
153
|
-
//
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
161
|
-
|
|
162
|
-
"
|
|
163
|
-
"prompt"
|
|
201
|
+
pushHint(
|
|
202
|
+
editor.getKeybindingLabel("live_grep_export_quickfix", "prompt"),
|
|
203
|
+
"save matches"
|
|
164
204
|
);
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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: "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
382
|
-
//
|
|
383
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
487
|
-
|
|
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
|
-
|
|
546
|
+
const results = await provider.search(query, {
|
|
493
547
|
cwd: editor.getCwd(),
|
|
494
|
-
maxResults:
|
|
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
|
-
|
|
498
|
-
|
|
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 ·
|
|
581
|
+
// brief "Live Grep" → "Live Grep · rg" flash when the
|
|
516
582
|
// first search resolves selectProvider().
|
|
517
583
|
if (cachedSelected) {
|
|
518
584
|
updateOverlayTitle(cachedSelected);
|