@fresh-editor/fresh-editor 0.1.4
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/.gitignore +2 -0
- package/LICENSE +117 -0
- package/README.md +54 -0
- package/binary-install.js +212 -0
- package/binary.js +126 -0
- package/install.js +4 -0
- package/npm-shrinkwrap.json +900 -0
- package/package.json +100 -0
- package/plugins/README.md +121 -0
- package/plugins/clangd_support.md +20 -0
- package/plugins/clangd_support.ts +323 -0
- package/plugins/color_highlighter.ts +302 -0
- package/plugins/diagnostics_panel.ts +308 -0
- package/plugins/examples/README.md +245 -0
- package/plugins/examples/async_demo.ts +165 -0
- package/plugins/examples/bookmarks.ts +329 -0
- package/plugins/examples/buffer_query_demo.ts +110 -0
- package/plugins/examples/git_grep.ts +262 -0
- package/plugins/examples/hello_world.ts +93 -0
- package/plugins/examples/virtual_buffer_demo.ts +116 -0
- package/plugins/find_references.ts +357 -0
- package/plugins/git_find_file.ts +298 -0
- package/plugins/git_grep.ts +188 -0
- package/plugins/git_log.ts +1283 -0
- package/plugins/lib/fresh.d.ts +849 -0
- package/plugins/lib/index.ts +24 -0
- package/plugins/lib/navigation-controller.ts +214 -0
- package/plugins/lib/panel-manager.ts +218 -0
- package/plugins/lib/types.ts +72 -0
- package/plugins/lib/virtual-buffer-factory.ts +158 -0
- package/plugins/manual_help.ts +243 -0
- package/plugins/markdown_compose.ts +1207 -0
- package/plugins/merge_conflict.ts +1811 -0
- package/plugins/path_complete.ts +163 -0
- package/plugins/search_replace.ts +481 -0
- package/plugins/todo_highlighter.ts +204 -0
- package/plugins/welcome.ts +74 -0
- package/run-fresh.js +4 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Path Completion Plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides path autocompletion for file prompts (Open File, Save File As).
|
|
7
|
+
* Shows directory contents and filters based on user input.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Parse the input to extract directory path and search pattern
|
|
11
|
+
function parsePath(input: string): { dir: string; pattern: string; isAbsolute: boolean } {
|
|
12
|
+
if (input === "") {
|
|
13
|
+
return { dir: ".", pattern: "", isAbsolute: false };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const isAbsolute = input.startsWith("/");
|
|
17
|
+
|
|
18
|
+
// Find the last path separator
|
|
19
|
+
const lastSlash = input.lastIndexOf("/");
|
|
20
|
+
|
|
21
|
+
if (lastSlash === -1) {
|
|
22
|
+
// No slash, searching in current directory
|
|
23
|
+
return { dir: ".", pattern: input, isAbsolute: false };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (lastSlash === 0) {
|
|
27
|
+
// Root directory
|
|
28
|
+
return { dir: "/", pattern: input.slice(1), isAbsolute: true };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Has directory component
|
|
32
|
+
const dir = input.slice(0, lastSlash);
|
|
33
|
+
const pattern = input.slice(lastSlash + 1);
|
|
34
|
+
|
|
35
|
+
return { dir: dir || "/", pattern, isAbsolute };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Filter and sort entries based on pattern
|
|
39
|
+
function filterEntries(entries: DirEntry[], pattern: string): DirEntry[] {
|
|
40
|
+
const patternLower = pattern.toLowerCase();
|
|
41
|
+
|
|
42
|
+
// Filter entries that match the pattern
|
|
43
|
+
const filtered = entries.filter((entry) => {
|
|
44
|
+
const nameLower = entry.name.toLowerCase();
|
|
45
|
+
// Match if pattern is prefix of name (case-insensitive)
|
|
46
|
+
return nameLower.startsWith(patternLower);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Sort: directories first, then alphabetically
|
|
50
|
+
filtered.sort((a, b) => {
|
|
51
|
+
// Directories come first
|
|
52
|
+
if (a.is_dir && !b.is_dir) return -1;
|
|
53
|
+
if (!a.is_dir && b.is_dir) return 1;
|
|
54
|
+
// Alphabetical within same type
|
|
55
|
+
return a.name.localeCompare(b.name);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
return filtered;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Convert directory entries to suggestions
|
|
62
|
+
function entriesToSuggestions(entries: DirEntry[], basePath: string): PromptSuggestion[] {
|
|
63
|
+
return entries.map((entry) => {
|
|
64
|
+
// Build full path
|
|
65
|
+
let fullPath: string;
|
|
66
|
+
if (basePath === ".") {
|
|
67
|
+
fullPath = entry.name;
|
|
68
|
+
} else if (basePath === "/") {
|
|
69
|
+
fullPath = "/" + entry.name;
|
|
70
|
+
} else {
|
|
71
|
+
fullPath = basePath + "/" + entry.name;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Add trailing slash for directories
|
|
75
|
+
const displayName = entry.is_dir ? entry.name + "/" : entry.name;
|
|
76
|
+
const value = entry.is_dir ? fullPath + "/" : fullPath;
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
text: displayName,
|
|
80
|
+
description: entry.is_dir ? "directory" : undefined,
|
|
81
|
+
value: value,
|
|
82
|
+
disabled: false,
|
|
83
|
+
};
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function missingFileSuggestion(
|
|
88
|
+
input: string,
|
|
89
|
+
pattern: string,
|
|
90
|
+
): PromptSuggestion | null {
|
|
91
|
+
if (pattern === "" || input === "") {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let absolutePath = input;
|
|
96
|
+
if (!editor.pathIsAbsolute(absolutePath)) {
|
|
97
|
+
let cwd: string;
|
|
98
|
+
try {
|
|
99
|
+
cwd = editor.getCwd();
|
|
100
|
+
} catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
absolutePath = editor.pathJoin(cwd, absolutePath);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (editor.fileExists(absolutePath)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
text: `${input} (new file)`,
|
|
112
|
+
description: "File does not exist yet",
|
|
113
|
+
value: input,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Generate path completions for the given input
|
|
118
|
+
function generateCompletions(input: string): PromptSuggestion[] {
|
|
119
|
+
const { dir, pattern } = parsePath(input);
|
|
120
|
+
|
|
121
|
+
// Read the directory
|
|
122
|
+
const entries = editor.readDir(dir);
|
|
123
|
+
const newFileSuggestion = missingFileSuggestion(input, pattern);
|
|
124
|
+
|
|
125
|
+
if (!entries) {
|
|
126
|
+
// Directory doesn't exist or can't be read
|
|
127
|
+
return newFileSuggestion ? [newFileSuggestion] : [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Filter hidden files (starting with .) unless pattern starts with .
|
|
131
|
+
const showHidden = pattern.startsWith(".");
|
|
132
|
+
const visibleEntries = entries.filter((e) => showHidden || !e.name.startsWith("."));
|
|
133
|
+
|
|
134
|
+
// Filter by pattern
|
|
135
|
+
const filtered = filterEntries(visibleEntries, pattern);
|
|
136
|
+
|
|
137
|
+
// Limit results
|
|
138
|
+
const limited = filtered.slice(0, 100);
|
|
139
|
+
|
|
140
|
+
// Convert to suggestions
|
|
141
|
+
const suggestions = entriesToSuggestions(limited, dir);
|
|
142
|
+
if (newFileSuggestion) {
|
|
143
|
+
suggestions.push(newFileSuggestion);
|
|
144
|
+
}
|
|
145
|
+
return suggestions;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Handle prompt changes for file prompts
|
|
149
|
+
globalThis.onPathCompletePromptChanged = function (args: { prompt_type: string; input: string }): boolean {
|
|
150
|
+
if (args.prompt_type !== "open-file" && args.prompt_type !== "save-file-as") {
|
|
151
|
+
return true; // Not our prompt
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const suggestions = generateCompletions(args.input);
|
|
155
|
+
editor.setPromptSuggestions(suggestions);
|
|
156
|
+
|
|
157
|
+
return true;
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Register event handler
|
|
161
|
+
editor.on("prompt_changed", "onPathCompletePromptChanged");
|
|
162
|
+
|
|
163
|
+
editor.debug("Path completion plugin loaded successfully");
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
/// <reference path="../types/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Multi-File Search & Replace Plugin
|
|
5
|
+
*
|
|
6
|
+
* Provides project-wide search and replace functionality using git grep.
|
|
7
|
+
* Shows results in a virtual buffer split with preview and confirmation.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Result item structure
|
|
11
|
+
interface SearchResult {
|
|
12
|
+
file: string;
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
content: string;
|
|
16
|
+
selected: boolean; // Whether this result will be replaced
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Plugin state
|
|
20
|
+
let panelOpen = false;
|
|
21
|
+
let resultsBufferId: number | null = null;
|
|
22
|
+
let sourceSplitId: number | null = null;
|
|
23
|
+
let resultsSplitId: number | null = null;
|
|
24
|
+
let searchResults: SearchResult[] = [];
|
|
25
|
+
let searchPattern: string = "";
|
|
26
|
+
let replaceText: string = "";
|
|
27
|
+
let searchRegex: boolean = false;
|
|
28
|
+
|
|
29
|
+
// Maximum results to display
|
|
30
|
+
const MAX_RESULTS = 200;
|
|
31
|
+
|
|
32
|
+
// Define the search-replace mode with keybindings
|
|
33
|
+
editor.defineMode(
|
|
34
|
+
"search-replace-list",
|
|
35
|
+
null,
|
|
36
|
+
[
|
|
37
|
+
["Return", "search_replace_preview"],
|
|
38
|
+
["space", "search_replace_toggle_item"],
|
|
39
|
+
["a", "search_replace_select_all"],
|
|
40
|
+
["n", "search_replace_select_none"],
|
|
41
|
+
["r", "search_replace_execute"],
|
|
42
|
+
["q", "search_replace_close"],
|
|
43
|
+
["Escape", "search_replace_close"],
|
|
44
|
+
],
|
|
45
|
+
true // read-only
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
// Get relative path for display
|
|
49
|
+
function getRelativePath(filePath: string): string {
|
|
50
|
+
const cwd = editor.getCwd();
|
|
51
|
+
if (filePath.startsWith(cwd)) {
|
|
52
|
+
return filePath.slice(cwd.length + 1);
|
|
53
|
+
}
|
|
54
|
+
return filePath;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Parse git grep output
|
|
58
|
+
function parseGitGrepLine(line: string): SearchResult | null {
|
|
59
|
+
const match = line.match(/^([^:]+):(\d+):(\d+):(.*)$/);
|
|
60
|
+
if (match) {
|
|
61
|
+
return {
|
|
62
|
+
file: match[1],
|
|
63
|
+
line: parseInt(match[2], 10),
|
|
64
|
+
column: parseInt(match[3], 10),
|
|
65
|
+
content: match[4],
|
|
66
|
+
selected: true, // Selected by default
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Format a result for display
|
|
73
|
+
function formatResult(item: SearchResult, index: number): string {
|
|
74
|
+
const checkbox = item.selected ? "[x]" : "[ ]";
|
|
75
|
+
const displayPath = getRelativePath(item.file);
|
|
76
|
+
const location = `${displayPath}:${item.line}`;
|
|
77
|
+
|
|
78
|
+
// Truncate for display
|
|
79
|
+
const maxLocationLen = 40;
|
|
80
|
+
const truncatedLocation = location.length > maxLocationLen
|
|
81
|
+
? "..." + location.slice(-(maxLocationLen - 3))
|
|
82
|
+
: location.padEnd(maxLocationLen);
|
|
83
|
+
|
|
84
|
+
const trimmedContent = item.content.trim();
|
|
85
|
+
const maxContentLen = 50;
|
|
86
|
+
const displayContent = trimmedContent.length > maxContentLen
|
|
87
|
+
? trimmedContent.slice(0, maxContentLen - 3) + "..."
|
|
88
|
+
: trimmedContent;
|
|
89
|
+
|
|
90
|
+
return `${checkbox} ${truncatedLocation} ${displayContent}\n`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Build panel entries
|
|
94
|
+
function buildPanelEntries(): TextPropertyEntry[] {
|
|
95
|
+
const entries: TextPropertyEntry[] = [];
|
|
96
|
+
|
|
97
|
+
// Header
|
|
98
|
+
const selectedCount = searchResults.filter(r => r.selected).length;
|
|
99
|
+
entries.push({
|
|
100
|
+
text: `═══ Search & Replace ═══\n`,
|
|
101
|
+
properties: { type: "header" },
|
|
102
|
+
});
|
|
103
|
+
entries.push({
|
|
104
|
+
text: `Search: "${searchPattern}"${searchRegex ? " (regex)" : ""}\n`,
|
|
105
|
+
properties: { type: "info" },
|
|
106
|
+
});
|
|
107
|
+
entries.push({
|
|
108
|
+
text: `Replace: "${replaceText}"\n`,
|
|
109
|
+
properties: { type: "info" },
|
|
110
|
+
});
|
|
111
|
+
entries.push({
|
|
112
|
+
text: `\n`,
|
|
113
|
+
properties: { type: "spacer" },
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
if (searchResults.length === 0) {
|
|
117
|
+
entries.push({
|
|
118
|
+
text: " No matches found\n",
|
|
119
|
+
properties: { type: "empty" },
|
|
120
|
+
});
|
|
121
|
+
} else {
|
|
122
|
+
// Results header
|
|
123
|
+
const limitNote = searchResults.length >= MAX_RESULTS ? ` (limited to ${MAX_RESULTS})` : "";
|
|
124
|
+
entries.push({
|
|
125
|
+
text: `Results: ${searchResults.length}${limitNote} (${selectedCount} selected)\n`,
|
|
126
|
+
properties: { type: "count" },
|
|
127
|
+
});
|
|
128
|
+
entries.push({
|
|
129
|
+
text: `\n`,
|
|
130
|
+
properties: { type: "spacer" },
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Add each result
|
|
134
|
+
for (let i = 0; i < searchResults.length; i++) {
|
|
135
|
+
const result = searchResults[i];
|
|
136
|
+
entries.push({
|
|
137
|
+
text: formatResult(result, i),
|
|
138
|
+
properties: {
|
|
139
|
+
type: "result",
|
|
140
|
+
index: i,
|
|
141
|
+
location: {
|
|
142
|
+
file: result.file,
|
|
143
|
+
line: result.line,
|
|
144
|
+
column: result.column,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Footer
|
|
152
|
+
entries.push({
|
|
153
|
+
text: `───────────────────────────────────────────────────────────────────────────────\n`,
|
|
154
|
+
properties: { type: "separator" },
|
|
155
|
+
});
|
|
156
|
+
entries.push({
|
|
157
|
+
text: `[SPC] toggle [a] all [n] none [r] REPLACE [RET] preview [q] close\n`,
|
|
158
|
+
properties: { type: "help" },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return entries;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update panel content
|
|
165
|
+
function updatePanelContent(): void {
|
|
166
|
+
if (resultsBufferId !== null) {
|
|
167
|
+
const entries = buildPanelEntries();
|
|
168
|
+
editor.setVirtualBufferContent(resultsBufferId, entries);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Perform the search
|
|
173
|
+
async function performSearch(pattern: string, replace: string, isRegex: boolean): Promise<void> {
|
|
174
|
+
searchPattern = pattern;
|
|
175
|
+
replaceText = replace;
|
|
176
|
+
searchRegex = isRegex;
|
|
177
|
+
|
|
178
|
+
// Build git grep args
|
|
179
|
+
const args = ["grep", "-n", "--column", "-I"];
|
|
180
|
+
if (isRegex) {
|
|
181
|
+
args.push("-E"); // Extended regex
|
|
182
|
+
} else {
|
|
183
|
+
args.push("-F"); // Fixed string
|
|
184
|
+
}
|
|
185
|
+
args.push("--", pattern);
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const result = await editor.spawnProcess("git", args);
|
|
189
|
+
|
|
190
|
+
searchResults = [];
|
|
191
|
+
|
|
192
|
+
if (result.exit_code === 0) {
|
|
193
|
+
for (const line of result.stdout.split("\n")) {
|
|
194
|
+
if (!line.trim()) continue;
|
|
195
|
+
const match = parseGitGrepLine(line);
|
|
196
|
+
if (match) {
|
|
197
|
+
searchResults.push(match);
|
|
198
|
+
if (searchResults.length >= MAX_RESULTS) break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (searchResults.length === 0) {
|
|
204
|
+
editor.setStatus(`No matches found for "${pattern}"`);
|
|
205
|
+
} else {
|
|
206
|
+
editor.setStatus(`Found ${searchResults.length} matches`);
|
|
207
|
+
}
|
|
208
|
+
} catch (e) {
|
|
209
|
+
editor.setStatus(`Search error: ${e}`);
|
|
210
|
+
searchResults = [];
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Show the search results panel
|
|
215
|
+
async function showResultsPanel(): Promise<void> {
|
|
216
|
+
if (panelOpen && resultsBufferId !== null) {
|
|
217
|
+
updatePanelContent();
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
sourceSplitId = editor.getActiveSplitId();
|
|
222
|
+
const entries = buildPanelEntries();
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
resultsBufferId = await editor.createVirtualBufferInSplit({
|
|
226
|
+
name: "*Search/Replace*",
|
|
227
|
+
mode: "search-replace-list",
|
|
228
|
+
read_only: true,
|
|
229
|
+
entries: entries,
|
|
230
|
+
ratio: 0.6, // 60/40 split
|
|
231
|
+
panel_id: "search-replace-panel",
|
|
232
|
+
show_line_numbers: false,
|
|
233
|
+
show_cursors: true,
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
panelOpen = true;
|
|
237
|
+
resultsSplitId = editor.getActiveSplitId();
|
|
238
|
+
editor.debug(`Search/Replace panel opened with buffer ID ${resultsBufferId}`);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
241
|
+
editor.setStatus("Failed to open search/replace panel");
|
|
242
|
+
editor.debug(`ERROR: createVirtualBufferInSplit failed: ${errorMessage}`);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Execute replacements
|
|
247
|
+
async function executeReplacements(): Promise<void> {
|
|
248
|
+
const selectedResults = searchResults.filter(r => r.selected);
|
|
249
|
+
|
|
250
|
+
if (selectedResults.length === 0) {
|
|
251
|
+
editor.setStatus("No items selected for replacement");
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Group by file
|
|
256
|
+
const fileGroups: Map<string, SearchResult[]> = new Map();
|
|
257
|
+
for (const result of selectedResults) {
|
|
258
|
+
if (!fileGroups.has(result.file)) {
|
|
259
|
+
fileGroups.set(result.file, []);
|
|
260
|
+
}
|
|
261
|
+
fileGroups.get(result.file)!.push(result);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let filesModified = 0;
|
|
265
|
+
let replacementsCount = 0;
|
|
266
|
+
const errors: string[] = [];
|
|
267
|
+
|
|
268
|
+
for (const [filePath, results] of fileGroups) {
|
|
269
|
+
try {
|
|
270
|
+
// Read file
|
|
271
|
+
const content = await editor.readFile(filePath);
|
|
272
|
+
const lines = content.split("\n");
|
|
273
|
+
|
|
274
|
+
// Sort results by line (descending) to avoid offset issues
|
|
275
|
+
const sortedResults = [...results].sort((a, b) => {
|
|
276
|
+
if (a.line !== b.line) return b.line - a.line;
|
|
277
|
+
return b.column - a.column;
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Apply replacements
|
|
281
|
+
for (const result of sortedResults) {
|
|
282
|
+
const lineIndex = result.line - 1;
|
|
283
|
+
if (lineIndex >= 0 && lineIndex < lines.length) {
|
|
284
|
+
let line = lines[lineIndex];
|
|
285
|
+
|
|
286
|
+
if (searchRegex) {
|
|
287
|
+
// Regex replacement
|
|
288
|
+
const regex = new RegExp(searchPattern, "g");
|
|
289
|
+
lines[lineIndex] = line.replace(regex, replaceText);
|
|
290
|
+
} else {
|
|
291
|
+
// Simple string replacement (all occurrences in line)
|
|
292
|
+
lines[lineIndex] = line.split(searchPattern).join(replaceText);
|
|
293
|
+
}
|
|
294
|
+
replacementsCount++;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Write back
|
|
299
|
+
const newContent = lines.join("\n");
|
|
300
|
+
await editor.writeFile(filePath, newContent);
|
|
301
|
+
filesModified++;
|
|
302
|
+
|
|
303
|
+
} catch (e) {
|
|
304
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
305
|
+
errors.push(`${filePath}: ${errorMessage}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Report results
|
|
310
|
+
if (errors.length > 0) {
|
|
311
|
+
editor.setStatus(`Replaced in ${filesModified} files (${errors.length} errors)`);
|
|
312
|
+
editor.debug(`Replacement errors: ${errors.join(", ")}`);
|
|
313
|
+
} else {
|
|
314
|
+
editor.setStatus(`Replaced ${replacementsCount} occurrences in ${filesModified} files`);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Close panel after replacement
|
|
318
|
+
globalThis.search_replace_close();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Start search/replace workflow
|
|
322
|
+
globalThis.start_search_replace = function(): void {
|
|
323
|
+
searchResults = [];
|
|
324
|
+
searchPattern = "";
|
|
325
|
+
replaceText = "";
|
|
326
|
+
|
|
327
|
+
editor.startPrompt("Search (in project): ", "search-replace-search");
|
|
328
|
+
editor.setStatus("Enter search pattern...");
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
// Handle search prompt confirmation
|
|
332
|
+
globalThis.onSearchReplaceSearchConfirmed = function(args: {
|
|
333
|
+
prompt_type: string;
|
|
334
|
+
selected_index: number | null;
|
|
335
|
+
input: string;
|
|
336
|
+
}): boolean {
|
|
337
|
+
if (args.prompt_type !== "search-replace-search") {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const pattern = args.input.trim();
|
|
342
|
+
if (!pattern) {
|
|
343
|
+
editor.setStatus("Search cancelled - empty pattern");
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
searchPattern = pattern;
|
|
348
|
+
|
|
349
|
+
// Ask for replacement text
|
|
350
|
+
editor.startPrompt("Replace with: ", "search-replace-replace");
|
|
351
|
+
return true;
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
// Handle replace prompt confirmation
|
|
355
|
+
globalThis.onSearchReplaceReplaceConfirmed = async function(args: {
|
|
356
|
+
prompt_type: string;
|
|
357
|
+
selected_index: number | null;
|
|
358
|
+
input: string;
|
|
359
|
+
}): Promise<boolean> {
|
|
360
|
+
if (args.prompt_type !== "search-replace-replace") {
|
|
361
|
+
return true;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
replaceText = args.input; // Can be empty for deletion
|
|
365
|
+
|
|
366
|
+
// Perform search and show results
|
|
367
|
+
await performSearch(searchPattern, replaceText, false);
|
|
368
|
+
await showResultsPanel();
|
|
369
|
+
|
|
370
|
+
return true;
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
// Handle prompt cancellation
|
|
374
|
+
globalThis.onSearchReplacePromptCancelled = function(args: {
|
|
375
|
+
prompt_type: string;
|
|
376
|
+
}): boolean {
|
|
377
|
+
if (args.prompt_type !== "search-replace-search" &&
|
|
378
|
+
args.prompt_type !== "search-replace-replace") {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
editor.setStatus("Search/Replace cancelled");
|
|
383
|
+
return true;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
// Toggle selection of current item
|
|
387
|
+
globalThis.search_replace_toggle_item = function(): void {
|
|
388
|
+
if (resultsBufferId === null || searchResults.length === 0) return;
|
|
389
|
+
|
|
390
|
+
const props = editor.getTextPropertiesAtCursor(resultsBufferId);
|
|
391
|
+
if (props.length > 0 && typeof props[0].index === "number") {
|
|
392
|
+
const index = props[0].index as number;
|
|
393
|
+
if (index >= 0 && index < searchResults.length) {
|
|
394
|
+
searchResults[index].selected = !searchResults[index].selected;
|
|
395
|
+
updatePanelContent();
|
|
396
|
+
const selected = searchResults.filter(r => r.selected).length;
|
|
397
|
+
editor.setStatus(`${selected}/${searchResults.length} selected`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
// Select all items
|
|
403
|
+
globalThis.search_replace_select_all = function(): void {
|
|
404
|
+
for (const result of searchResults) {
|
|
405
|
+
result.selected = true;
|
|
406
|
+
}
|
|
407
|
+
updatePanelContent();
|
|
408
|
+
editor.setStatus(`${searchResults.length}/${searchResults.length} selected`);
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
// Select no items
|
|
412
|
+
globalThis.search_replace_select_none = function(): void {
|
|
413
|
+
for (const result of searchResults) {
|
|
414
|
+
result.selected = false;
|
|
415
|
+
}
|
|
416
|
+
updatePanelContent();
|
|
417
|
+
editor.setStatus(`0/${searchResults.length} selected`);
|
|
418
|
+
};
|
|
419
|
+
|
|
420
|
+
// Execute replacement
|
|
421
|
+
globalThis.search_replace_execute = function(): void {
|
|
422
|
+
const selected = searchResults.filter(r => r.selected).length;
|
|
423
|
+
if (selected === 0) {
|
|
424
|
+
editor.setStatus("No items selected");
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
editor.setStatus(`Replacing ${selected} occurrences...`);
|
|
429
|
+
executeReplacements();
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
// Preview current item (jump to location)
|
|
433
|
+
globalThis.search_replace_preview = function(): void {
|
|
434
|
+
if (sourceSplitId === null || resultsBufferId === null) return;
|
|
435
|
+
|
|
436
|
+
const props = editor.getTextPropertiesAtCursor(resultsBufferId);
|
|
437
|
+
if (props.length > 0) {
|
|
438
|
+
const location = props[0].location as { file: string; line: number; column: number } | undefined;
|
|
439
|
+
if (location) {
|
|
440
|
+
editor.openFileInSplit(sourceSplitId, location.file, location.line, location.column);
|
|
441
|
+
editor.setStatus(`Preview: ${getRelativePath(location.file)}:${location.line}`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// Close the panel
|
|
447
|
+
globalThis.search_replace_close = function(): void {
|
|
448
|
+
if (!panelOpen) return;
|
|
449
|
+
|
|
450
|
+
if (resultsBufferId !== null) {
|
|
451
|
+
editor.closeBuffer(resultsBufferId);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (resultsSplitId !== null && resultsSplitId !== sourceSplitId) {
|
|
455
|
+
editor.closeSplit(resultsSplitId);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
panelOpen = false;
|
|
459
|
+
resultsBufferId = null;
|
|
460
|
+
sourceSplitId = null;
|
|
461
|
+
resultsSplitId = null;
|
|
462
|
+
searchResults = [];
|
|
463
|
+
editor.setStatus("Search/Replace closed");
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
// Register event handlers
|
|
467
|
+
editor.on("prompt_confirmed", "onSearchReplaceSearchConfirmed");
|
|
468
|
+
editor.on("prompt_confirmed", "onSearchReplaceReplaceConfirmed");
|
|
469
|
+
editor.on("prompt_cancelled", "onSearchReplacePromptCancelled");
|
|
470
|
+
|
|
471
|
+
// Register command
|
|
472
|
+
editor.registerCommand(
|
|
473
|
+
"Search and Replace in Project",
|
|
474
|
+
"Search and replace text across all git-tracked files",
|
|
475
|
+
"start_search_replace",
|
|
476
|
+
"normal"
|
|
477
|
+
);
|
|
478
|
+
|
|
479
|
+
// Plugin initialization
|
|
480
|
+
editor.debug("Search & Replace plugin loaded");
|
|
481
|
+
editor.setStatus("Search & Replace plugin ready");
|