@fresh-editor/fresh-editor 0.1.75 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +6 -0
  3. package/package.json +1 -1
  4. package/plugins/audit_mode.ts +9 -4
  5. package/plugins/buffer_modified.ts +1 -1
  6. package/plugins/calculator.ts +1 -1
  7. package/plugins/check-types.sh +41 -0
  8. package/plugins/clangd_support.ts +1 -1
  9. package/plugins/color_highlighter.ts +4 -1
  10. package/plugins/config-schema.json +44 -2
  11. package/plugins/diagnostics_panel.i18n.json +52 -52
  12. package/plugins/diagnostics_panel.ts +168 -540
  13. package/plugins/find_references.ts +82 -324
  14. package/plugins/git_blame.i18n.json +260 -247
  15. package/plugins/git_blame.ts +4 -9
  16. package/plugins/git_find_file.ts +42 -270
  17. package/plugins/git_grep.ts +50 -167
  18. package/plugins/git_gutter.ts +1 -1
  19. package/plugins/git_log.ts +4 -11
  20. package/plugins/lib/finder.ts +1499 -0
  21. package/plugins/lib/fresh.d.ts +93 -17
  22. package/plugins/lib/index.ts +14 -0
  23. package/plugins/lib/navigation-controller.ts +1 -1
  24. package/plugins/lib/panel-manager.ts +7 -13
  25. package/plugins/lib/results-panel.ts +914 -0
  26. package/plugins/lib/search-utils.ts +343 -0
  27. package/plugins/lib/virtual-buffer-factory.ts +3 -2
  28. package/plugins/live_grep.ts +56 -379
  29. package/plugins/markdown_compose.ts +1 -17
  30. package/plugins/merge_conflict.ts +16 -14
  31. package/plugins/path_complete.ts +1 -1
  32. package/plugins/search_replace.i18n.json +13 -13
  33. package/plugins/search_replace.ts +11 -9
  34. package/plugins/theme_editor.ts +15 -9
  35. package/plugins/todo_highlighter.ts +1 -0
  36. package/plugins/vi_mode.ts +9 -5
  37. package/plugins/welcome.ts +1 -1
@@ -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="../../types/fresh.d.ts" />
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
- return await editor.createVirtualBufferInSplit({
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
  /**