@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.
Files changed (38) hide show
  1. package/.gitignore +2 -0
  2. package/LICENSE +117 -0
  3. package/README.md +54 -0
  4. package/binary-install.js +212 -0
  5. package/binary.js +126 -0
  6. package/install.js +4 -0
  7. package/npm-shrinkwrap.json +900 -0
  8. package/package.json +100 -0
  9. package/plugins/README.md +121 -0
  10. package/plugins/clangd_support.md +20 -0
  11. package/plugins/clangd_support.ts +323 -0
  12. package/plugins/color_highlighter.ts +302 -0
  13. package/plugins/diagnostics_panel.ts +308 -0
  14. package/plugins/examples/README.md +245 -0
  15. package/plugins/examples/async_demo.ts +165 -0
  16. package/plugins/examples/bookmarks.ts +329 -0
  17. package/plugins/examples/buffer_query_demo.ts +110 -0
  18. package/plugins/examples/git_grep.ts +262 -0
  19. package/plugins/examples/hello_world.ts +93 -0
  20. package/plugins/examples/virtual_buffer_demo.ts +116 -0
  21. package/plugins/find_references.ts +357 -0
  22. package/plugins/git_find_file.ts +298 -0
  23. package/plugins/git_grep.ts +188 -0
  24. package/plugins/git_log.ts +1283 -0
  25. package/plugins/lib/fresh.d.ts +849 -0
  26. package/plugins/lib/index.ts +24 -0
  27. package/plugins/lib/navigation-controller.ts +214 -0
  28. package/plugins/lib/panel-manager.ts +218 -0
  29. package/plugins/lib/types.ts +72 -0
  30. package/plugins/lib/virtual-buffer-factory.ts +158 -0
  31. package/plugins/manual_help.ts +243 -0
  32. package/plugins/markdown_compose.ts +1207 -0
  33. package/plugins/merge_conflict.ts +1811 -0
  34. package/plugins/path_complete.ts +163 -0
  35. package/plugins/search_replace.ts +481 -0
  36. package/plugins/todo_highlighter.ts +204 -0
  37. package/plugins/welcome.ts +74 -0
  38. 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");