@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.
- package/CHANGELOG.md +72 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/plugins/config-schema.json +7 -1
- package/plugins/dashboard.ts +16 -93
- package/plugins/git_grep.ts +3 -1
- package/plugins/git_log.ts +196 -224
- package/plugins/goto_with_selection.i18n.json +58 -0
- package/plugins/goto_with_selection.ts +17 -0
- package/plugins/lib/finder.ts +27 -6
- package/plugins/lib/fresh.d.ts +620 -14
- package/plugins/lib/index.ts +34 -0
- package/plugins/lib/widgets.ts +796 -0
- package/plugins/live_diff.ts +324 -29
- package/plugins/live_grep.ts +114 -48
- package/plugins/orchestrator.ts +1685 -0
- package/plugins/pkg.ts +234 -53
- package/plugins/rust-lsp.ts +58 -40
- package/plugins/schemas/theme.schema.json +4 -0
- package/plugins/search_replace.ts +780 -517
- package/plugins/theme_editor.i18n.json +84 -0
- package/plugins/theme_editor.ts +30 -5
- package/plugins/tsconfig.json +2 -0
- package/plugins/vi_mode.ts +38 -17
- package/themes/terminal.json +3 -0
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);
|