@fresh-editor/fresh-editor 0.1.74 → 0.1.76
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 +54 -0
- package/README.md +6 -0
- package/package.json +1 -1
- package/plugins/audit_mode.ts +9 -4
- package/plugins/buffer_modified.ts +1 -1
- package/plugins/calculator.ts +1 -1
- package/plugins/check-types.sh +41 -0
- package/plugins/clangd_support.ts +1 -1
- package/plugins/color_highlighter.ts +4 -1
- package/plugins/config-schema.json +44 -2
- package/plugins/diagnostics_panel.i18n.json +52 -52
- package/plugins/diagnostics_panel.ts +168 -540
- package/plugins/find_references.ts +82 -324
- package/plugins/git_blame.i18n.json +260 -247
- package/plugins/git_blame.ts +4 -9
- package/plugins/git_find_file.ts +42 -270
- package/plugins/git_grep.ts +50 -167
- package/plugins/git_gutter.ts +1 -1
- package/plugins/git_log.ts +4 -11
- package/plugins/lib/finder.ts +1499 -0
- package/plugins/lib/fresh.d.ts +104 -19
- package/plugins/lib/index.ts +14 -0
- package/plugins/lib/navigation-controller.ts +1 -1
- package/plugins/lib/panel-manager.ts +7 -13
- package/plugins/lib/results-panel.ts +914 -0
- package/plugins/lib/search-utils.ts +343 -0
- package/plugins/lib/virtual-buffer-factory.ts +3 -2
- package/plugins/live_grep.ts +56 -379
- package/plugins/markdown_compose.ts +1 -17
- package/plugins/merge_conflict.ts +16 -14
- package/plugins/path_complete.ts +1 -1
- package/plugins/search_replace.i18n.json +13 -13
- package/plugins/search_replace.ts +11 -9
- package/plugins/theme_editor.ts +57 -30
- package/plugins/todo_highlighter.ts +1 -0
- package/plugins/vi_mode.ts +9 -5
- package/plugins/welcome.ts +1 -1
- package/themes/dark.json +102 -0
- package/themes/high-contrast.json +102 -0
- package/themes/light.json +102 -0
- package/themes/nostalgia.json +102 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/// <reference path="./fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Shared utilities for search plugins (Live Grep, Git Grep, etc.)
|
|
5
|
+
*
|
|
6
|
+
* Provides:
|
|
7
|
+
* - Debounced search execution
|
|
8
|
+
* - Preview panel management
|
|
9
|
+
* - Common search result types
|
|
10
|
+
*
|
|
11
|
+
* NOTE: These utilities receive the editor instance as a parameter
|
|
12
|
+
* to avoid calling getEditor() at module scope (which causes errors).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
// ============================================================================
|
|
16
|
+
// Types
|
|
17
|
+
// ============================================================================
|
|
18
|
+
|
|
19
|
+
export interface SearchMatch {
|
|
20
|
+
file: string;
|
|
21
|
+
line: number;
|
|
22
|
+
column: number;
|
|
23
|
+
content: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface PreviewState {
|
|
27
|
+
bufferId: number | null;
|
|
28
|
+
splitId: number | null;
|
|
29
|
+
originalSplitId: number | null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface DebouncedSearchOptions {
|
|
33
|
+
debounceMs?: number;
|
|
34
|
+
minQueryLength?: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Editor interface (subset of what we need)
|
|
38
|
+
interface EditorApi {
|
|
39
|
+
readFile(path: string): Promise<string>;
|
|
40
|
+
defineMode(name: string, parent: string, bindings: [string, string][], readOnly: boolean): void;
|
|
41
|
+
createVirtualBufferInSplit(options: {
|
|
42
|
+
name: string;
|
|
43
|
+
mode: string;
|
|
44
|
+
read_only: boolean;
|
|
45
|
+
entries: TextPropertyEntry[];
|
|
46
|
+
ratio: number;
|
|
47
|
+
direction: string;
|
|
48
|
+
panel_id: string;
|
|
49
|
+
show_line_numbers: boolean;
|
|
50
|
+
editing_disabled: boolean;
|
|
51
|
+
}): Promise<{ buffer_id: number; split_id?: number }>;
|
|
52
|
+
setVirtualBufferContent(bufferId: number, entries: TextPropertyEntry[]): void;
|
|
53
|
+
closeBuffer(bufferId: number): void;
|
|
54
|
+
closeSplit(splitId: number): void;
|
|
55
|
+
focusSplit(splitId: number): void;
|
|
56
|
+
delay(ms: number): Promise<void>;
|
|
57
|
+
debug(msg: string): void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ============================================================================
|
|
61
|
+
// Preview Panel
|
|
62
|
+
// ============================================================================
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Creates and manages a preview panel for search results.
|
|
66
|
+
* Shows file content with context around the match.
|
|
67
|
+
*/
|
|
68
|
+
export class SearchPreview {
|
|
69
|
+
private bufferId: number | null = null;
|
|
70
|
+
private splitId: number | null = null;
|
|
71
|
+
private originalSplitId: number | null = null;
|
|
72
|
+
private panelId: string;
|
|
73
|
+
private modeName: string;
|
|
74
|
+
private editor: EditorApi;
|
|
75
|
+
|
|
76
|
+
constructor(editor: EditorApi, panelId: string) {
|
|
77
|
+
this.editor = editor;
|
|
78
|
+
this.panelId = panelId;
|
|
79
|
+
this.modeName = `${panelId}-preview`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Remember the original split before creating preview
|
|
84
|
+
*/
|
|
85
|
+
setOriginalSplit(splitId: number): void {
|
|
86
|
+
this.originalSplitId = splitId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Update the preview to show a match with surrounding context
|
|
91
|
+
*/
|
|
92
|
+
async update(match: SearchMatch): Promise<void> {
|
|
93
|
+
try {
|
|
94
|
+
const content = await this.editor.readFile(match.file);
|
|
95
|
+
const lines = content.split("\n");
|
|
96
|
+
|
|
97
|
+
// Calculate context window (5 lines before and after)
|
|
98
|
+
const contextBefore = 5;
|
|
99
|
+
const contextAfter = 5;
|
|
100
|
+
const startLine = Math.max(0, match.line - 1 - contextBefore);
|
|
101
|
+
const endLine = Math.min(lines.length, match.line + contextAfter);
|
|
102
|
+
|
|
103
|
+
const entries: TextPropertyEntry[] = [];
|
|
104
|
+
|
|
105
|
+
// Header
|
|
106
|
+
entries.push({
|
|
107
|
+
text: ` ${match.file}:${match.line}:${match.column}\n`,
|
|
108
|
+
properties: { type: "header" },
|
|
109
|
+
});
|
|
110
|
+
entries.push({
|
|
111
|
+
text: "─".repeat(60) + "\n",
|
|
112
|
+
properties: { type: "separator" },
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Content lines with line numbers
|
|
116
|
+
for (let i = startLine; i < endLine; i++) {
|
|
117
|
+
const lineNum = i + 1;
|
|
118
|
+
const lineContent = lines[i] || "";
|
|
119
|
+
const isMatchLine = lineNum === match.line;
|
|
120
|
+
const prefix = isMatchLine ? "> " : " ";
|
|
121
|
+
const lineNumStr = String(lineNum).padStart(4, " ");
|
|
122
|
+
|
|
123
|
+
entries.push({
|
|
124
|
+
text: `${prefix}${lineNumStr} │ ${lineContent}\n`,
|
|
125
|
+
properties: {
|
|
126
|
+
type: isMatchLine ? "match" : "context",
|
|
127
|
+
line: lineNum,
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (this.bufferId === null) {
|
|
133
|
+
// Create preview mode if not exists
|
|
134
|
+
this.editor.defineMode(this.modeName, "special", [["q", "close_buffer"]], true);
|
|
135
|
+
|
|
136
|
+
// Create preview in a split on the right
|
|
137
|
+
const result = await this.editor.createVirtualBufferInSplit({
|
|
138
|
+
name: "*Preview*",
|
|
139
|
+
mode: this.modeName,
|
|
140
|
+
read_only: true,
|
|
141
|
+
entries,
|
|
142
|
+
ratio: 0.5,
|
|
143
|
+
direction: "vertical",
|
|
144
|
+
panel_id: this.panelId,
|
|
145
|
+
show_line_numbers: false,
|
|
146
|
+
editing_disabled: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
this.bufferId = result.buffer_id;
|
|
150
|
+
this.splitId = result.split_id ?? null;
|
|
151
|
+
|
|
152
|
+
// Return focus to original split so prompt stays active
|
|
153
|
+
if (this.originalSplitId !== null) {
|
|
154
|
+
this.editor.focusSplit(this.originalSplitId);
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
// Update existing buffer content
|
|
158
|
+
this.editor.setVirtualBufferContent(this.bufferId, entries);
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
this.editor.debug(`[SearchPreview] Failed to update: ${e}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Close the preview panel and clean up
|
|
167
|
+
*/
|
|
168
|
+
close(): void {
|
|
169
|
+
if (this.bufferId !== null) {
|
|
170
|
+
this.editor.closeBuffer(this.bufferId);
|
|
171
|
+
this.bufferId = null;
|
|
172
|
+
}
|
|
173
|
+
if (this.splitId !== null) {
|
|
174
|
+
this.editor.closeSplit(this.splitId);
|
|
175
|
+
this.splitId = null;
|
|
176
|
+
}
|
|
177
|
+
this.originalSplitId = null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Check if preview is currently open
|
|
182
|
+
*/
|
|
183
|
+
isOpen(): boolean {
|
|
184
|
+
return this.bufferId !== null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Debounced Search
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Creates a debounced search executor that:
|
|
194
|
+
* - Waits for user to stop typing before searching
|
|
195
|
+
* - Cancels previous searches when new input arrives
|
|
196
|
+
* - Tracks search version to discard stale results
|
|
197
|
+
*/
|
|
198
|
+
export class DebouncedSearch {
|
|
199
|
+
private currentSearch: ProcessHandle | null = null;
|
|
200
|
+
private pendingKill: Promise<boolean> | null = null;
|
|
201
|
+
private searchVersion = 0;
|
|
202
|
+
private lastQuery = "";
|
|
203
|
+
private debounceMs: number;
|
|
204
|
+
private minQueryLength: number;
|
|
205
|
+
private editor: EditorApi;
|
|
206
|
+
|
|
207
|
+
constructor(editor: EditorApi, options: DebouncedSearchOptions = {}) {
|
|
208
|
+
this.editor = editor;
|
|
209
|
+
this.debounceMs = options.debounceMs ?? 150;
|
|
210
|
+
this.minQueryLength = options.minQueryLength ?? 2;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Execute a search with debouncing.
|
|
215
|
+
* Returns results via callback to allow async processing.
|
|
216
|
+
*/
|
|
217
|
+
async search(
|
|
218
|
+
query: string,
|
|
219
|
+
executor: () => ProcessHandle,
|
|
220
|
+
onResults: (result: SpawnResult) => void
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
const thisVersion = ++this.searchVersion;
|
|
223
|
+
|
|
224
|
+
// Kill any existing search immediately
|
|
225
|
+
if (this.currentSearch) {
|
|
226
|
+
this.pendingKill = this.currentSearch.kill();
|
|
227
|
+
this.currentSearch = null;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check minimum query length
|
|
231
|
+
if (!query || query.trim().length < this.minQueryLength) {
|
|
232
|
+
if (this.pendingKill) {
|
|
233
|
+
await this.pendingKill;
|
|
234
|
+
this.pendingKill = null;
|
|
235
|
+
}
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Debounce
|
|
240
|
+
await this.editor.delay(this.debounceMs);
|
|
241
|
+
|
|
242
|
+
// Wait for pending kill
|
|
243
|
+
if (this.pendingKill) {
|
|
244
|
+
await this.pendingKill;
|
|
245
|
+
this.pendingKill = null;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check if superseded
|
|
249
|
+
if (this.searchVersion !== thisVersion) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Skip duplicate queries
|
|
254
|
+
if (query === this.lastQuery) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
this.lastQuery = query;
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
this.currentSearch = executor();
|
|
261
|
+
const result = await this.currentSearch;
|
|
262
|
+
|
|
263
|
+
// Check if this search was cancelled
|
|
264
|
+
if (this.searchVersion !== thisVersion) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
this.currentSearch = null;
|
|
269
|
+
onResults(result);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
const errorMsg = String(e);
|
|
272
|
+
if (!errorMsg.includes("killed") && !errorMsg.includes("not found")) {
|
|
273
|
+
this.editor.debug(`[DebouncedSearch] Error: ${e}`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Cancel any running search
|
|
280
|
+
*/
|
|
281
|
+
cancel(): void {
|
|
282
|
+
if (this.currentSearch) {
|
|
283
|
+
this.currentSearch.kill();
|
|
284
|
+
this.currentSearch = null;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Reset state for new search session
|
|
290
|
+
*/
|
|
291
|
+
reset(): void {
|
|
292
|
+
this.cancel();
|
|
293
|
+
this.lastQuery = "";
|
|
294
|
+
this.searchVersion = 0;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ============================================================================
|
|
299
|
+
// Utility Functions
|
|
300
|
+
// ============================================================================
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse a grep-style output line (file:line:column:content)
|
|
304
|
+
*/
|
|
305
|
+
export function parseGrepLine(line: string): SearchMatch | null {
|
|
306
|
+
const match = line.match(/^([^:]+):(\d+):(\d+):(.*)$/);
|
|
307
|
+
if (match) {
|
|
308
|
+
return {
|
|
309
|
+
file: match[1],
|
|
310
|
+
line: parseInt(match[2], 10),
|
|
311
|
+
column: parseInt(match[3], 10),
|
|
312
|
+
content: match[4],
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Convert search matches to prompt suggestions
|
|
320
|
+
*/
|
|
321
|
+
export function matchesToSuggestions(
|
|
322
|
+
matches: SearchMatch[],
|
|
323
|
+
maxResults: number = 100
|
|
324
|
+
): PromptSuggestion[] {
|
|
325
|
+
const suggestions: PromptSuggestion[] = [];
|
|
326
|
+
|
|
327
|
+
for (let i = 0; i < Math.min(matches.length, maxResults); i++) {
|
|
328
|
+
const match = matches[i];
|
|
329
|
+
const displayContent =
|
|
330
|
+
match.content.length > 60
|
|
331
|
+
? match.content.substring(0, 57) + "..."
|
|
332
|
+
: match.content;
|
|
333
|
+
|
|
334
|
+
suggestions.push({
|
|
335
|
+
text: `${match.file}:${match.line}`,
|
|
336
|
+
description: displayContent.trim(),
|
|
337
|
+
value: `${i}`,
|
|
338
|
+
disabled: false,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return suggestions;
|
|
343
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/// <reference path="
|
|
1
|
+
/// <reference path="./fresh.d.ts" />
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Options for creating a virtual buffer
|
|
@@ -108,7 +108,7 @@ export function createVirtualBufferFactory(editor: EditorAPI) {
|
|
|
108
108
|
readOnly = true,
|
|
109
109
|
} = options;
|
|
110
110
|
|
|
111
|
-
|
|
111
|
+
const result = await editor.createVirtualBufferInSplit({
|
|
112
112
|
name,
|
|
113
113
|
mode,
|
|
114
114
|
read_only: readOnly,
|
|
@@ -118,6 +118,7 @@ export function createVirtualBufferFactory(editor: EditorAPI) {
|
|
|
118
118
|
show_line_numbers: showLineNumbers,
|
|
119
119
|
editing_disabled: editingDisabled,
|
|
120
120
|
});
|
|
121
|
+
return result.buffer_id;
|
|
121
122
|
},
|
|
122
123
|
|
|
123
124
|
/**
|