@fresh-editor/fresh-editor 0.1.75 → 0.1.77

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 (40) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README.md +8 -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 +75 -3
  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_explorer.ts +159 -0
  17. package/plugins/git_find_file.ts +42 -270
  18. package/plugins/git_grep.ts +50 -167
  19. package/plugins/git_gutter.ts +1 -1
  20. package/plugins/git_log.ts +4 -11
  21. package/plugins/lib/finder.ts +1499 -0
  22. package/plugins/lib/fresh.d.ts +118 -17
  23. package/plugins/lib/index.ts +23 -1
  24. package/plugins/lib/navigation-controller.ts +1 -1
  25. package/plugins/lib/panel-manager.ts +7 -13
  26. package/plugins/lib/results-panel.ts +914 -0
  27. package/plugins/lib/search-utils.ts +343 -0
  28. package/plugins/lib/types.ts +14 -0
  29. package/plugins/lib/virtual-buffer-factory.ts +3 -2
  30. package/plugins/live_grep.ts +56 -379
  31. package/plugins/markdown_compose.ts +1 -17
  32. package/plugins/merge_conflict.ts +16 -14
  33. package/plugins/odin-lsp.ts +135 -0
  34. package/plugins/path_complete.ts +1 -1
  35. package/plugins/search_replace.i18n.json +13 -13
  36. package/plugins/search_replace.ts +11 -9
  37. package/plugins/theme_editor.ts +15 -9
  38. package/plugins/todo_highlighter.ts +1 -0
  39. package/plugins/vi_mode.ts +9 -5
  40. package/plugins/welcome.ts +1 -1
@@ -1,4 +1,4 @@
1
- /// <reference path="../types/fresh.d.ts" />
1
+ /// <reference path="./lib/fresh.d.ts" />
2
2
  const editor = getEditor();
3
3
 
4
4
 
@@ -664,14 +664,9 @@ globalThis.git_blame_copy_hash = function(): void {
664
664
  return;
665
665
  }
666
666
 
667
- // Use spawn to copy to clipboard
668
- editor.spawnProcess("sh", ["-c", `echo -n "${hash}" | xclip -selection clipboard 2>/dev/null || echo -n "${hash}" | pbcopy 2>/dev/null || echo -n "${hash}" | xsel --clipboard 2>/dev/null`])
669
- .then(() => {
670
- editor.setStatus(editor.t("status.hash_copied", { short: hash.slice(0, 7), full: hash }));
671
- })
672
- .catch(() => {
673
- editor.setStatus(editor.t("status.hash_display", { hash }));
674
- });
667
+ // Copy hash to clipboard
668
+ editor.copyToClipboard(hash);
669
+ editor.setStatus(editor.t("status.hash_copied", { short: hash.slice(0, 7), full: hash }));
675
670
  };
676
671
 
677
672
  // =============================================================================
@@ -0,0 +1,159 @@
1
+ /// <reference path="../types/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * Git Explorer Decorations
6
+ *
7
+ * Adds VS Code-style status badges (M/A/U/D/...) to the file explorer.
8
+ */
9
+
10
+ const NAMESPACE = "git-explorer";
11
+
12
+ const COLORS = {
13
+ added: [80, 250, 123] as [number, number, number],
14
+ modified: [255, 184, 108] as [number, number, number],
15
+ deleted: [255, 85, 85] as [number, number, number],
16
+ renamed: [139, 233, 253] as [number, number, number],
17
+ untracked: [241, 250, 140] as [number, number, number],
18
+ conflicted: [255, 121, 198] as [number, number, number],
19
+ };
20
+
21
+ const PRIORITY = {
22
+ conflicted: 90,
23
+ deleted: 80,
24
+ added: 60,
25
+ modified: 50,
26
+ renamed: 40,
27
+ untracked: 30,
28
+ };
29
+
30
+ let refreshInFlight = false;
31
+
32
+ function statusToDecoration(status: string, staged: boolean) {
33
+ switch (status) {
34
+ case "A":
35
+ return { symbol: "A", color: COLORS.added, priority: PRIORITY.added };
36
+ case "M":
37
+ return {
38
+ symbol: "M",
39
+ color: staged ? COLORS.added : COLORS.modified,
40
+ priority: PRIORITY.modified + (staged ? 2 : 0),
41
+ };
42
+ case "D":
43
+ return { symbol: "D", color: COLORS.deleted, priority: PRIORITY.deleted };
44
+ case "R":
45
+ return { symbol: "R", color: COLORS.renamed, priority: PRIORITY.renamed };
46
+ case "C":
47
+ return { symbol: "C", color: COLORS.renamed, priority: PRIORITY.renamed };
48
+ case "U":
49
+ return { symbol: "!", color: COLORS.conflicted, priority: PRIORITY.conflicted };
50
+ default:
51
+ return null;
52
+ }
53
+ }
54
+
55
+ function parseStatusOutput(output: string, repoRoot: string) {
56
+ const separator = output.includes("\0") ? "\0" : "\n";
57
+ const entries = output
58
+ .split(separator)
59
+ .map((entry) => entry.replace(/\r$/, ""))
60
+ .filter((entry) => entry.length > 0);
61
+ const byPath = new Map<string, { path: string; symbol: string; color: [number, number, number]; priority: number }>();
62
+
63
+ for (let i = 0; i < entries.length; i++) {
64
+ const entry = entries[i];
65
+ if (entry.length < 3) {
66
+ continue;
67
+ }
68
+ const x = entry[0];
69
+ const y = entry[1];
70
+ let path = entry.slice(3);
71
+
72
+ if ((x === "R" || x === "C") && separator === "\0" && i + 1 < entries.length) {
73
+ i += 1;
74
+ path = entries[i];
75
+ } else if (entry.includes(" -> ") && (x === "R" || x === "C" || y === "R" || y === "C")) {
76
+ path = entry.split(" -> ").pop() ?? path;
77
+ }
78
+
79
+ let decoration = null;
80
+ if (x === "?" && y === "?") {
81
+ decoration = { symbol: "U", color: COLORS.untracked, priority: PRIORITY.untracked };
82
+ } else if (x !== " " && x !== "?") {
83
+ decoration = statusToDecoration(x, true);
84
+ } else if (y !== " ") {
85
+ decoration = statusToDecoration(y, false);
86
+ }
87
+
88
+ if (!decoration) {
89
+ continue;
90
+ }
91
+
92
+ const absolutePath = editor.pathJoin(repoRoot, path);
93
+ const existing = byPath.get(absolutePath);
94
+ if (!existing || decoration.priority >= existing.priority) {
95
+ byPath.set(absolutePath, { path: absolutePath, ...decoration });
96
+ }
97
+ }
98
+
99
+ return Array.from(byPath.values());
100
+ }
101
+
102
+ async function refreshGitExplorerDecorations() {
103
+ if (refreshInFlight) {
104
+ return;
105
+ }
106
+ refreshInFlight = true;
107
+ try {
108
+ const cwd = editor.getCwd();
109
+ const rootResult = await editor.spawnProcess("git", ["rev-parse", "--show-toplevel"], cwd);
110
+ if (rootResult.exit_code !== 0) {
111
+ editor.clearFileExplorerDecorations(NAMESPACE);
112
+ return;
113
+ }
114
+ const repoRoot = rootResult.stdout.trim();
115
+ if (!repoRoot) {
116
+ editor.clearFileExplorerDecorations(NAMESPACE);
117
+ return;
118
+ }
119
+
120
+ const statusResult = await editor.spawnProcess(
121
+ "git",
122
+ ["status", "--porcelain"],
123
+ repoRoot
124
+ );
125
+ if (statusResult.exit_code !== 0) {
126
+ editor.clearFileExplorerDecorations(NAMESPACE);
127
+ return;
128
+ }
129
+
130
+ const decorations = parseStatusOutput(statusResult.stdout, repoRoot);
131
+ if (decorations.length === 0) {
132
+ editor.clearFileExplorerDecorations(NAMESPACE);
133
+ } else {
134
+ editor.setFileExplorerDecorations(NAMESPACE, decorations);
135
+ }
136
+ } catch (_err) {
137
+ editor.clearFileExplorerDecorations(NAMESPACE);
138
+ } finally {
139
+ refreshInFlight = false;
140
+ }
141
+ }
142
+
143
+ globalThis.onGitExplorerAfterFileOpen = () => {
144
+ refreshGitExplorerDecorations();
145
+ };
146
+
147
+ globalThis.onGitExplorerAfterFileSave = () => {
148
+ refreshGitExplorerDecorations();
149
+ };
150
+
151
+ globalThis.onGitExplorerEditorInitialized = () => {
152
+ refreshGitExplorerDecorations();
153
+ };
154
+
155
+ editor.on("after_file_open", "onGitExplorerAfterFileOpen");
156
+ editor.on("after_file_save", "onGitExplorerAfterFileSave");
157
+ editor.on("editor_initialized", "onGitExplorerEditorInitialized");
158
+
159
+ refreshGitExplorerDecorations();
@@ -1,287 +1,65 @@
1
- /// <reference path="../types/fresh.d.ts" />
2
- const editor = getEditor();
3
-
1
+ /// <reference path="./lib/fresh.d.ts" />
4
2
 
5
3
  /**
6
4
  * Git Find File Plugin
7
5
  *
8
6
  * Provides interactive file finding functionality with fuzzy search
9
- * for git-tracked files. Uses the prompt API for interactive selection.
7
+ * for git-tracked files. Uses the Finder abstraction with filter mode.
10
8
  */
11
9
 
12
- // State management
13
- let allFiles: string[] = [];
14
- let filteredFiles: string[] = [];
15
- let isLoading = false;
16
-
17
- // Simple fuzzy filter function
18
- function fuzzyMatch(str: string, pattern: string): boolean {
19
- if (pattern === "") {
20
- return true;
21
- }
22
-
23
- str = str.toLowerCase();
24
- pattern = pattern.toLowerCase();
25
-
26
- let strIdx = 0;
27
- let patIdx = 0;
28
-
29
- while (strIdx < str.length && patIdx < pattern.length) {
30
- if (str[strIdx] === pattern[patIdx]) {
31
- patIdx++;
32
- }
33
- strIdx++;
34
- }
35
-
36
- return patIdx >= pattern.length;
37
- }
38
-
39
- // Score a fuzzy match (higher is better)
40
- function fuzzyScore(str: string, pattern: string): number {
41
- if (pattern === "") return 0;
42
-
43
- str = str.toLowerCase();
44
- pattern = pattern.toLowerCase();
45
-
46
- let score = 0;
47
- let strIdx = 0;
48
- let patIdx = 0;
49
- let consecutiveMatches = 0;
50
- let lastMatchIdx = -1;
51
-
52
- while (strIdx < str.length && patIdx < pattern.length) {
53
- if (str[strIdx] === pattern[patIdx]) {
54
- // Bonus for consecutive matches
55
- if (lastMatchIdx === strIdx - 1) {
56
- consecutiveMatches++;
57
- score += consecutiveMatches * 10;
58
- } else {
59
- consecutiveMatches = 1;
60
- score += 1;
61
- }
62
-
63
- // Bonus for matching at start of path segments
64
- if (strIdx === 0 || str[strIdx - 1] === "/" || str[strIdx - 1] === "_" || str[strIdx - 1] === "-") {
65
- score += 15;
66
- }
10
+ import { Finder } from "./lib/finder.ts";
67
11
 
68
- // Bonus for matching filename (after last /)
69
- const lastSlash = str.lastIndexOf("/");
70
- if (strIdx > lastSlash) {
71
- score += 5;
72
- }
73
-
74
- lastMatchIdx = strIdx;
75
- patIdx++;
76
- }
77
- strIdx++;
78
- }
79
-
80
- // Penalty for longer paths
81
- score -= str.length * 0.1;
82
-
83
- return patIdx >= pattern.length ? score : -1;
84
- }
85
-
86
- // Filter and sort files by query using fuzzy matching
87
- function filterFiles(files: string[], query: string): string[] {
88
- if (query === "" || query.trim() === "") {
89
- // Return first 100 files for empty query
90
- return files.slice(0, 100);
91
- }
92
-
93
- const scored: Array<{ file: string; score: number }> = [];
94
-
95
- for (const file of files) {
96
- const score = fuzzyScore(file, query);
97
- if (score > 0) {
98
- scored.push({ file, score });
99
- }
100
-
101
- // Stop early if we have enough high-quality matches
102
- if (scored.length >= 500) {
103
- break;
104
- }
105
- }
106
-
107
- // Sort by score descending
108
- scored.sort((a, b) => b.score - a.score);
109
-
110
- // Return top 100 results
111
- return scored.slice(0, 100).map((s) => s.file);
112
- }
113
-
114
- // Load git-tracked files asynchronously
115
- async function loadGitFiles(): Promise<void> {
116
- if (isLoading) {
117
- return;
118
- }
119
-
120
- isLoading = true;
121
- editor.setStatus(editor.t("status.loading"));
122
-
123
- try {
124
- const result = await editor.spawnProcess("git", ["ls-files"]);
125
-
126
- if (result.exit_code === 0) {
127
- allFiles = result.stdout.split("\n").filter((line) => line.trim() !== "");
12
+ const editor = getEditor();
128
13
 
129
- editor.debug(`Loaded ${allFiles.length} git-tracked files`);
130
- editor.setStatus(editor.t("status.indexed", { count: String(allFiles.length) }));
131
- } else {
132
- editor.debug(`Failed to load git files: ${result.stderr}`);
133
- editor.setStatus(editor.t("status.error_loading", { error: result.stderr }));
134
- allFiles = [];
135
- }
136
- } catch (e) {
137
- editor.debug(`Exception loading git files: ${e}`);
138
- editor.setStatus(editor.t("status.failed_load"));
139
- allFiles = [];
140
- } finally {
141
- isLoading = false;
14
+ // Create the finder instance with filter mode
15
+ const finder = new Finder<string>(editor, {
16
+ id: "git-find-file",
17
+ format: (file) => ({
18
+ label: file,
19
+ location: { file, line: 1, column: 1 },
20
+ }),
21
+ preview: false, // No preview for file finder
22
+ maxResults: 100,
23
+ });
24
+
25
+ // Load git-tracked files
26
+ async function loadGitFiles(): Promise<string[]> {
27
+ const result = await editor.spawnProcess("git", ["ls-files"]);
28
+
29
+ if (result.exit_code === 0) {
30
+ return result.stdout.split("\n").filter((line) => line.trim() !== "");
142
31
  }
143
- }
144
32
 
145
- // Convert filtered files to prompt suggestions
146
- function filesToSuggestions(files: string[]): PromptSuggestion[] {
147
- return files.map((file) => {
148
- return {
149
- text: file,
150
- description: undefined,
151
- value: file,
152
- disabled: false,
153
- };
154
- });
33
+ editor.debug(`Failed to load git files: ${result.stderr}`);
34
+ return [];
155
35
  }
156
36
 
157
37
  // Global function to start file finder
158
- globalThis.start_git_find_file = async function (): Promise<void> {
159
- // Load files if not already loaded
160
- if (allFiles.length === 0 && !isLoading) {
161
- await loadGitFiles();
162
- }
163
-
164
- if (allFiles.length === 0) {
165
- editor.setStatus(editor.t("status.no_files"));
166
- return;
167
- }
168
-
169
- // Clear previous results
170
- filteredFiles = [];
171
-
172
- // Start the prompt
173
- editor.startPrompt(editor.t("prompt.find_file"), "git-find-file");
174
-
175
- // Show initial suggestions (first 100 files)
176
- const initial = filterFiles(allFiles, "");
177
- filteredFiles = initial;
178
- editor.setPromptSuggestions(filesToSuggestions(initial));
179
- editor.setStatus(editor.t("status.files_available", { count: String(allFiles.length) }));
180
- };
181
-
182
- // React to prompt input changes
183
- globalThis.onGitFindFilePromptChanged = function (args: { prompt_type: string; input: string }): boolean {
184
- if (args.prompt_type !== "git-find-file") {
185
- return true; // Not our prompt
186
- }
187
-
188
- const query = args.input;
189
-
190
- // Filter files based on query
191
- const matches = filterFiles(allFiles, query);
192
- filteredFiles = matches;
193
-
194
- // Update suggestions
195
- editor.setPromptSuggestions(filesToSuggestions(matches));
196
-
197
- // Update status
198
- if (matches.length > 0) {
199
- if (query.trim() === "") {
200
- editor.setStatus(editor.t("status.showing_first", { shown: String(matches.length), total: String(allFiles.length) }));
201
- } else {
202
- editor.setStatus(editor.t("status.found_matching", { count: String(matches.length), query }));
203
- }
204
- } else {
205
- editor.setStatus(editor.t("status.no_matching", { query }));
206
- }
207
-
208
- return true;
209
- };
210
-
211
- // Handle prompt confirmation (user pressed Enter)
212
- globalThis.onGitFindFilePromptConfirmed = function (args: {
213
- prompt_type: string;
214
- selected_index: number | null;
215
- input: string;
216
- }): boolean {
217
- if (args.prompt_type !== "git-find-file") {
218
- return true; // Not our prompt
219
- }
220
-
221
- editor.debug(`git-find-file confirmed: selected_index=${args.selected_index}, input=${args.input}`);
222
-
223
- // Check if user selected a suggestion
224
- if (args.selected_index !== null && filteredFiles[args.selected_index]) {
225
- const selectedFile = filteredFiles[args.selected_index];
226
-
227
- editor.debug(`Opening file: ${selectedFile}`);
228
-
229
- // Open the file at line 1
230
- editor.openFile(selectedFile, 1, 1);
231
- editor.setStatus(editor.t("status.opened", { file: selectedFile }));
232
- } else if (args.input.trim() !== "") {
233
- // Try to open input directly if it's a valid file path
234
- const inputFile = args.input.trim();
235
-
236
- // Check if the exact input matches any file
237
- if (allFiles.includes(inputFile)) {
238
- editor.openFile(inputFile, 1, 1);
239
- editor.setStatus(editor.t("status.opened", { file: inputFile }));
240
- } else {
241
- editor.setStatus(editor.t("status.file_not_found", { file: inputFile }));
242
- }
243
- } else {
244
- editor.setStatus(editor.t("status.no_selection"));
245
- }
246
-
247
- return true;
248
- };
249
-
250
- // Handle prompt cancellation (user pressed Escape)
251
- globalThis.onGitFindFilePromptCancelled = function (args: { prompt_type: string }): boolean {
252
- if (args.prompt_type !== "git-find-file") {
253
- return true; // Not our prompt
254
- }
255
-
256
- // Clear results
257
- filteredFiles = [];
258
- editor.setStatus(editor.t("status.cancelled"));
259
-
260
- return true;
38
+ globalThis.start_git_find_file = function (): void {
39
+ finder.prompt({
40
+ title: editor.t("prompt.find_file"),
41
+ source: {
42
+ mode: "filter",
43
+ load: loadGitFiles,
44
+ // Uses built-in fuzzy filter by default
45
+ },
46
+ });
261
47
  };
262
48
 
263
- // Register event handlers
264
- editor.on("prompt_changed", "onGitFindFilePromptChanged");
265
- editor.on("prompt_confirmed", "onGitFindFilePromptConfirmed");
266
- editor.on("prompt_cancelled", "onGitFindFilePromptCancelled");
267
-
268
49
  // Reload git files command
269
50
  globalThis.git_reload_files = async function (): Promise<void> {
270
- allFiles = [];
271
- await loadGitFiles();
272
- };
273
-
274
- // Show file count command
275
- globalThis.git_file_count = function (): void {
276
- if (allFiles.length === 0) {
277
- editor.setStatus(editor.t("status.no_files_loaded"));
278
- } else {
279
- editor.setStatus(editor.t("status.indexed", { count: String(allFiles.length) }));
280
- }
51
+ // Just re-trigger the prompt which will reload
52
+ globalThis.start_git_find_file();
53
+ editor.setStatus(editor.t("status.reloading"));
281
54
  };
282
55
 
283
56
  // Register commands
284
- editor.registerCommand("%cmd.find", "%cmd.find_desc", "start_git_find_file", "normal");
57
+ editor.registerCommand(
58
+ "%cmd.find",
59
+ "%cmd.find_desc",
60
+ "start_git_find_file",
61
+ "normal"
62
+ );
285
63
 
286
64
  editor.registerCommand(
287
65
  "%cmd.reload",
@@ -290,11 +68,5 @@ editor.registerCommand(
290
68
  "normal"
291
69
  );
292
70
 
293
- editor.registerCommand("%cmd.count", "%cmd.count_desc", "git_file_count", "normal");
294
-
295
- // Note: We don't load git files on plugin init because spawning processes requires async context
296
- // Files will be loaded lazily on first use
297
-
298
- editor.debug("Git Find File plugin loaded successfully (TypeScript)");
299
- editor.debug("Usage: Call start_git_find_file() or use command palette 'Git Find File'");
71
+ editor.debug("Git Find File plugin loaded (using Finder abstraction)");
300
72
  editor.setStatus(editor.t("status.ready"));