@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
@@ -1,16 +1,21 @@
1
1
  /// <reference path="./lib/fresh.d.ts" />
2
- const editor = getEditor();
3
-
4
2
 
5
3
  /**
6
4
  * Live Grep Plugin
7
5
  *
8
6
  * Project-wide search with ripgrep and live preview.
7
+ * Uses the Finder abstraction for unified search UX.
8
+ *
9
9
  * - Type to search across all files
10
10
  * - Navigate results with Up/Down to see preview
11
11
  * - Press Enter to open file at location
12
12
  */
13
13
 
14
+ import { Finder, parseGrepOutput } from "./lib/finder.ts";
15
+
16
+ const editor = getEditor();
17
+
18
+ // Result type from ripgrep
14
19
  interface GrepMatch {
15
20
  file: string;
16
21
  line: number;
@@ -18,398 +23,70 @@ interface GrepMatch {
18
23
  content: string;
19
24
  }
20
25
 
21
- // State management
22
- let grepResults: GrepMatch[] = [];
23
- let previewBufferId: number | null = null;
24
- let previewSplitId: number | null = null;
25
- let originalSplitId: number | null = null;
26
- let lastQuery: string = "";
27
- let previewCreated: boolean = false;
28
- let currentSearch: ProcessHandle | null = null;
29
- let pendingKill: Promise<boolean> | null = null; // Track pending kill globally
30
- let searchVersion = 0; // Incremented on each input change for debouncing
31
-
32
- const DEBOUNCE_MS = 150; // Wait 150ms after last keystroke before searching
33
-
34
- // Parse ripgrep output line
35
- // Format: file:line:column:content
36
- function parseRipgrepLine(line: string): GrepMatch | null {
37
- const match = line.match(/^([^:]+):(\d+):(\d+):(.*)$/);
38
- if (match) {
39
- return {
40
- file: match[1],
41
- line: parseInt(match[2], 10),
42
- column: parseInt(match[3], 10),
43
- content: match[4],
44
- };
45
- }
46
- return null;
47
- }
48
-
49
- // Parse ripgrep output into suggestions
50
- function parseRipgrepOutput(stdout: string): {
51
- results: GrepMatch[];
52
- suggestions: PromptSuggestion[];
53
- } {
54
- const results: GrepMatch[] = [];
55
- const suggestions: PromptSuggestion[] = [];
56
-
57
- for (const line of stdout.split("\n")) {
58
- if (!line.trim()) continue;
59
- const match = parseRipgrepLine(line);
60
- if (match) {
61
- results.push(match);
62
-
63
- // Truncate long content for display
64
- const displayContent =
65
- match.content.length > 60
66
- ? match.content.substring(0, 57) + "..."
67
- : match.content;
68
-
69
- suggestions.push({
70
- text: `${match.file}:${match.line}`,
71
- description: displayContent.trim(),
72
- value: `${results.length - 1}`, // Store index as value
73
- disabled: false,
74
- });
75
-
76
- // Limit to 100 results for performance
77
- if (results.length >= 100) {
78
- break;
79
- }
80
- }
81
- }
82
-
83
- return { results, suggestions };
84
- }
85
-
86
- // Create or update preview buffer with file content
87
- async function updatePreview(match: GrepMatch): Promise<void> {
88
- try {
89
- // Read the file content
90
- const content = await editor.readFile(match.file);
91
- const lines = content.split("\n");
92
-
93
- // Calculate context window (5 lines before and after)
94
- const contextBefore = 5;
95
- const contextAfter = 5;
96
- const startLine = Math.max(0, match.line - 1 - contextBefore);
97
- const endLine = Math.min(lines.length, match.line + contextAfter);
98
-
99
- // Build preview entries with highlighting
100
- const entries: TextPropertyEntry[] = [];
101
-
102
- // Header
103
- entries.push({
104
- text: ` ${match.file}:${match.line}:${match.column}\n`,
105
- properties: { type: "header" },
106
- });
107
- entries.push({
108
- text: "─".repeat(60) + "\n",
109
- properties: { type: "separator" },
110
- });
111
-
112
- // Content lines with line numbers
113
- for (let i = startLine; i < endLine; i++) {
114
- const lineNum = i + 1;
115
- const lineContent = lines[i] || "";
116
- const isMatchLine = lineNum === match.line;
117
- const prefix = isMatchLine ? "> " : " ";
118
- const lineNumStr = String(lineNum).padStart(4, " ");
119
-
120
- entries.push({
121
- text: `${prefix}${lineNumStr} │ ${lineContent}\n`,
122
- properties: {
123
- type: isMatchLine ? "match" : "context",
124
- line: lineNum,
125
- },
126
- });
127
- }
128
-
129
- // Create or update the preview buffer
130
- if (previewBufferId === null) {
131
- // Define mode for preview buffer
132
- editor.defineMode("live-grep-preview", "special", [["q", "close_buffer"]], true);
133
-
134
- // Create preview in a split on the right
135
- const result = await editor.createVirtualBufferInSplit({
136
- name: "*Preview*",
137
- mode: "live-grep-preview",
138
- read_only: true,
139
- entries,
140
- ratio: 0.5,
141
- direction: "vertical",
142
- panel_id: "live-grep-preview",
143
- show_line_numbers: false,
144
- editing_disabled: true,
145
- });
146
-
147
- // Extract buffer and split IDs from result
148
- previewBufferId = result.buffer_id;
149
- previewSplitId = result.split_id ?? null;
150
-
151
- // Return focus to original split so prompt stays active
152
- if (originalSplitId !== null) {
153
- editor.focusSplit(originalSplitId);
154
- }
155
- } else {
156
- // Update existing buffer content
157
- editor.setVirtualBufferContent(previewBufferId, entries);
158
- }
159
- } catch (e) {
160
- editor.debug(`Failed to update preview: ${e}`);
161
- }
162
- }
163
-
164
- // Close preview buffer and its split
165
- function closePreview(): void {
166
- // Close the buffer first
167
- if (previewBufferId !== null) {
168
- editor.closeBuffer(previewBufferId);
169
- previewBufferId = null;
170
- }
171
- // Then close the split
172
- if (previewSplitId !== null) {
173
- editor.closeSplit(previewSplitId);
174
- previewSplitId = null;
175
- }
176
- }
177
-
178
- // Run ripgrep search with debouncing
179
- async function runSearch(query: string): Promise<void> {
180
- // Increment version to invalidate any pending debounced search
181
- const thisVersion = ++searchVersion;
182
- editor.debug(`[live_grep] runSearch called: query="${query}", version=${thisVersion}`);
183
-
184
- // Kill any existing search immediately (don't wait) to stop wasting CPU
185
- // Store the kill promise globally so ALL pending searches wait for it
186
- if (currentSearch) {
187
- editor.debug(`[live_grep] killing existing search immediately`);
188
- pendingKill = currentSearch.kill();
189
- currentSearch = null;
190
- }
191
-
192
- if (!query || query.trim().length < 2) {
193
- // Wait for any pending kill to complete before returning
194
- if (pendingKill) {
195
- await pendingKill;
196
- pendingKill = null;
197
- }
198
- editor.debug(`[live_grep] query too short, clearing`);
199
- editor.setPromptSuggestions([]);
200
- grepResults = [];
201
- return;
202
- }
203
-
204
- // Debounce: wait a bit to see if user is still typing
205
- editor.debug(`[live_grep] debouncing for ${DEBOUNCE_MS}ms...`);
206
- await editor.delay(DEBOUNCE_MS);
207
-
208
- // Always await any pending kill before continuing - ensures old process is dead
209
- if (pendingKill) {
210
- editor.debug(`[live_grep] waiting for previous search to terminate`);
211
- await pendingKill;
212
- pendingKill = null;
213
- editor.debug(`[live_grep] previous search terminated`);
214
- }
215
-
216
- // If version changed during delay, a newer search was triggered - abort this one
217
- if (searchVersion !== thisVersion) {
218
- editor.debug(`[live_grep] version mismatch after debounce (${thisVersion} vs ${searchVersion}), aborting`);
219
- return;
220
- }
221
-
222
- // Avoid duplicate searches
223
- if (query === lastQuery) {
224
- editor.debug(`[live_grep] duplicate query, skipping`);
225
- return;
226
- }
227
- lastQuery = query;
228
-
229
- try {
230
- const cwd = editor.getCwd();
231
- editor.debug(`[live_grep] spawning rg for query="${query}" in cwd="${cwd}"`);
232
- const searchStartTime = Date.now();
233
- const search = editor.spawnProcess("rg", [
26
+ // Create the finder instance
27
+ const finder = new Finder<GrepMatch>(editor, {
28
+ id: "live-grep",
29
+ format: (match) => ({
30
+ label: `${match.file}:${match.line}`,
31
+ description:
32
+ match.content.length > 60
33
+ ? match.content.substring(0, 57).trim() + "..."
34
+ : match.content.trim(),
35
+ location: {
36
+ file: match.file,
37
+ line: match.line,
38
+ column: match.column,
39
+ },
40
+ }),
41
+ preview: true,
42
+ maxResults: 100,
43
+ });
44
+
45
+ // Search function that parses ripgrep output
46
+ async function searchWithRipgrep(query: string): Promise<GrepMatch[]> {
47
+ const cwd = editor.getCwd();
48
+ const result = await editor.spawnProcess(
49
+ "rg",
50
+ [
234
51
  "--line-number",
235
52
  "--column",
236
53
  "--no-heading",
237
54
  "--color=never",
238
55
  "--smart-case",
239
56
  "--max-count=100",
240
- "-g", "!.git",
241
- "-g", "!node_modules",
242
- "-g", "!target",
243
- "-g", "!*.lock",
57
+ "-g",
58
+ "!.git",
59
+ "-g",
60
+ "!node_modules",
61
+ "-g",
62
+ "!target",
63
+ "-g",
64
+ "!*.lock",
244
65
  "--",
245
66
  query,
246
- ], cwd);
67
+ ],
68
+ cwd
69
+ );
247
70
 
248
- currentSearch = search;
249
- editor.debug(`[live_grep] awaiting search result...`);
250
- const result = await search;
251
- const searchDuration = Date.now() - searchStartTime;
252
- editor.debug(`[live_grep] rg completed in ${searchDuration}ms, exit_code=${result.exit_code}, stdout_len=${result.stdout.length}`);
253
-
254
- // Check if this search was cancelled (a new search started)
255
- if (currentSearch !== search) {
256
- editor.debug(`[live_grep] search was superseded, discarding results`);
257
- return; // Discard stale results
258
- }
259
- currentSearch = null;
260
-
261
- if (result.exit_code === 0) {
262
- const parseStart = Date.now();
263
- const { results, suggestions } = parseRipgrepOutput(result.stdout);
264
- editor.debug(`[live_grep] parsed ${results.length} results in ${Date.now() - parseStart}ms`);
265
- grepResults = results;
266
- editor.setPromptSuggestions(suggestions);
267
-
268
- if (results.length > 0) {
269
- editor.setStatus(editor.t("status.found_matches", { count: String(results.length) }));
270
- // Show preview of first result
271
- await updatePreview(results[0]);
272
- } else {
273
- editor.setStatus(editor.t("status.no_matches"));
274
- }
275
- } else if (result.exit_code === 1) {
276
- // No matches
277
- editor.debug(`[live_grep] no matches (exit_code=1)`);
278
- grepResults = [];
279
- editor.setPromptSuggestions([]);
280
- editor.setStatus(editor.t("status.no_matches"));
281
- } else if (result.exit_code === -1) {
282
- // Process was killed, ignore
283
- editor.debug(`[live_grep] process was killed`);
284
- } else {
285
- editor.debug(`[live_grep] search error: ${result.stderr}`);
286
- editor.setStatus(editor.t("status.search_error", { error: result.stderr }));
287
- }
288
- } catch (e) {
289
- // Ignore errors from killed processes
290
- const errorMsg = String(e);
291
- editor.debug(`[live_grep] caught error: ${errorMsg}`);
292
- if (!errorMsg.includes("killed") && !errorMsg.includes("not found")) {
293
- editor.setStatus(editor.t("status.search_error", { error: String(e) }));
294
- }
71
+ if (result.exit_code === 0) {
72
+ return parseGrepOutput(result.stdout, 100) as GrepMatch[];
295
73
  }
74
+ return [];
296
75
  }
297
76
 
298
77
  // Start live grep
299
78
  globalThis.start_live_grep = function (): void {
300
- // Clear previous state
301
- grepResults = [];
302
- lastQuery = "";
303
- previewBufferId = null;
304
-
305
- // Remember original split to keep focus
306
- originalSplitId = editor.getActiveSplitId();
307
-
308
- // Start the prompt
309
- editor.startPrompt(editor.t("prompt.live_grep"), "live-grep");
310
- editor.setStatus(editor.t("status.type_to_search"));
79
+ finder.prompt({
80
+ title: editor.t("prompt.live_grep"),
81
+ source: {
82
+ mode: "search",
83
+ search: searchWithRipgrep,
84
+ debounceMs: 150,
85
+ minQueryLength: 2,
86
+ },
87
+ });
311
88
  };
312
89
 
313
- // Handle prompt input changes
314
- globalThis.onLiveGrepPromptChanged = function (args: {
315
- prompt_type: string;
316
- input: string;
317
- }): boolean {
318
- if (args.prompt_type !== "live-grep") {
319
- return true;
320
- }
321
-
322
- editor.debug(`[live_grep] onPromptChanged: input="${args.input}"`);
323
-
324
- // runSearch handles debouncing internally
325
- runSearch(args.input);
326
-
327
- return true;
328
- };
329
-
330
- // Handle selection changes - update preview
331
- globalThis.onLiveGrepSelectionChanged = function (args: {
332
- prompt_type: string;
333
- selected_index: number;
334
- }): boolean {
335
- if (args.prompt_type !== "live-grep") {
336
- return true;
337
- }
338
-
339
- const match = grepResults[args.selected_index];
340
- if (match) {
341
- updatePreview(match);
342
- }
343
-
344
- return true;
345
- };
346
-
347
- // Handle prompt confirmation - open file
348
- globalThis.onLiveGrepPromptConfirmed = function (args: {
349
- prompt_type: string;
350
- selected_index: number | null;
351
- input: string;
352
- }): boolean {
353
- if (args.prompt_type !== "live-grep") {
354
- return true;
355
- }
356
-
357
- // Kill any running search
358
- if (currentSearch) {
359
- currentSearch.kill();
360
- currentSearch = null;
361
- }
362
-
363
- // Close preview first
364
- closePreview();
365
-
366
- // Open selected file
367
- if (args.selected_index !== null && grepResults[args.selected_index]) {
368
- const selected = grepResults[args.selected_index];
369
- editor.openFile(selected.file, selected.line, selected.column);
370
- editor.setStatus(editor.t("status.opened_file", { file: selected.file, line: String(selected.line) }));
371
- } else {
372
- editor.setStatus(editor.t("status.no_file_selected"));
373
- }
374
-
375
- // Clear state
376
- grepResults = [];
377
- originalSplitId = null;
378
- previewSplitId = null;
379
-
380
- return true;
381
- };
382
-
383
- // Handle prompt cancellation
384
- globalThis.onLiveGrepPromptCancelled = function (args: {
385
- prompt_type: string;
386
- }): boolean {
387
- if (args.prompt_type !== "live-grep") {
388
- return true;
389
- }
390
-
391
- // Kill any running search
392
- if (currentSearch) {
393
- currentSearch.kill();
394
- currentSearch = null;
395
- }
396
-
397
- // Close preview and cleanup
398
- closePreview();
399
- grepResults = [];
400
- originalSplitId = null;
401
- previewSplitId = null;
402
- editor.setStatus(editor.t("status.cancelled"));
403
-
404
- return true;
405
- };
406
-
407
- // Register event handlers
408
- editor.on("prompt_changed", "onLiveGrepPromptChanged");
409
- editor.on("prompt_selection_changed", "onLiveGrepSelectionChanged");
410
- editor.on("prompt_confirmed", "onLiveGrepPromptConfirmed");
411
- editor.on("prompt_cancelled", "onLiveGrepPromptCancelled");
412
-
413
90
  // Register command
414
91
  editor.registerCommand(
415
92
  "%cmd.live_grep",
@@ -418,5 +95,5 @@ editor.registerCommand(
418
95
  "normal"
419
96
  );
420
97
 
421
- editor.debug("Live Grep plugin loaded");
98
+ editor.debug("Live Grep plugin loaded (using Finder abstraction)");
422
99
  editor.setStatus(editor.t("status.ready"));
@@ -1,3 +1,4 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
1
2
  // Markdown Compose Mode Plugin
2
3
  // Provides compose mode for Markdown documents with:
3
4
  // - Soft wrapping at a configurable width
@@ -24,23 +25,6 @@ const config: MarkdownConfig = {
24
25
  // Track buffers in compose mode (explicit toggle)
25
26
  const composeBuffers = new Set<number>();
26
27
 
27
- // Types match the Rust ViewTokenWire structure
28
- interface ViewTokenWire {
29
- source_offset: number | null;
30
- kind: ViewTokenWireKind;
31
- }
32
-
33
- type ViewTokenWireKind =
34
- | { Text: string }
35
- | "Newline"
36
- | "Space"
37
- | "Break";
38
-
39
- interface LayoutHints {
40
- compose_width?: number | null;
41
- column_guides?: number[] | null;
42
- }
43
-
44
28
  // =============================================================================
45
29
  // Block-based parser for hanging indent support
46
30
  // =============================================================================
@@ -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
 
@@ -14,6 +14,9 @@ const editor = getEditor();
14
14
  * - Visual highlighting with intra-line diffing
15
15
  *
16
16
  * Architecture: Plugin-based implementation following the spec in docs/MERGE.md
17
+ *
18
+ * TODO: Consider rewriting to use the built-in side-by-side diff in the core
19
+ * editor, or use composite buffers for a simpler implementation.
17
20
  */
18
21
 
19
22
  // =============================================================================
@@ -915,19 +918,19 @@ function buildResultEntries(): TextPropertyEntry[] {
915
918
  function applyHighlighting(): void {
916
919
  // Highlight OURS panel
917
920
  if (mergeState.oursPanelId !== null) {
918
- editor.removeOverlaysByPrefix(mergeState.oursPanelId, "merge-");
921
+ editor.clearAllOverlays(mergeState.oursPanelId);
919
922
  highlightPanel(mergeState.oursPanelId, "ours");
920
923
  }
921
924
 
922
925
  // Highlight THEIRS panel
923
926
  if (mergeState.theirsPanelId !== null) {
924
- editor.removeOverlaysByPrefix(mergeState.theirsPanelId, "merge-");
927
+ editor.clearAllOverlays(mergeState.theirsPanelId);
925
928
  highlightPanel(mergeState.theirsPanelId, "theirs");
926
929
  }
927
930
 
928
931
  // Highlight RESULT panel
929
932
  if (mergeState.resultPanelId !== null) {
930
- editor.removeOverlaysByPrefix(mergeState.resultPanelId, "merge-");
933
+ editor.clearAllOverlays(mergeState.resultPanelId);
931
934
  highlightResultPanel(mergeState.resultPanelId);
932
935
  }
933
936
  }
@@ -1337,7 +1340,6 @@ async function createMergePanels(): Promise<void> {
1337
1340
  mode: "merge-conflict",
1338
1341
  read_only: true,
1339
1342
  entries: buildFullFileEntries("ours"),
1340
- panel_id: "merge-ours",
1341
1343
  show_line_numbers: true,
1342
1344
  show_cursors: true,
1343
1345
  editing_disabled: true,
@@ -1349,7 +1351,7 @@ async function createMergePanels(): Promise<void> {
1349
1351
  }
1350
1352
 
1351
1353
  // Create THEIRS panel to the right (vertical split)
1352
- const theirsId = await editor.createVirtualBufferInSplit({
1354
+ const theirsResult = await editor.createVirtualBufferInSplit({
1353
1355
  name: `*THEIRS*${sourceExt}`,
1354
1356
  mode: "merge-conflict",
1355
1357
  read_only: true,
@@ -1362,9 +1364,9 @@ async function createMergePanels(): Promise<void> {
1362
1364
  editing_disabled: true,
1363
1365
  });
1364
1366
 
1365
- if (theirsId !== null) {
1366
- mergeState.theirsPanelId = theirsId;
1367
- mergeState.theirsSplitId = editor.getActiveSplitId();
1367
+ if (theirsResult !== null) {
1368
+ mergeState.theirsPanelId = theirsResult.buffer_id;
1369
+ mergeState.theirsSplitId = theirsResult.split_id ?? editor.getActiveSplitId();
1368
1370
  }
1369
1371
 
1370
1372
  // Focus back on OURS and create RESULT in the middle
@@ -1372,7 +1374,7 @@ async function createMergePanels(): Promise<void> {
1372
1374
  editor.focusSplit(mergeState.oursSplitId);
1373
1375
  }
1374
1376
 
1375
- const resultId = await editor.createVirtualBufferInSplit({
1377
+ const resultResult = await editor.createVirtualBufferInSplit({
1376
1378
  name: `*RESULT*${sourceExt}`,
1377
1379
  mode: "merge-result",
1378
1380
  read_only: false,
@@ -1385,9 +1387,9 @@ async function createMergePanels(): Promise<void> {
1385
1387
  editing_disabled: false,
1386
1388
  });
1387
1389
 
1388
- if (resultId !== null) {
1389
- mergeState.resultPanelId = resultId;
1390
- mergeState.resultSplitId = editor.getActiveSplitId();
1390
+ if (resultResult !== null) {
1391
+ mergeState.resultPanelId = resultResult.buffer_id;
1392
+ mergeState.resultSplitId = resultResult.split_id ?? editor.getActiveSplitId();
1391
1393
  }
1392
1394
 
1393
1395
  // Distribute splits evenly so all three panels get equal width
@@ -1606,7 +1608,7 @@ globalThis.merge_save_and_exit = async function(): Promise<void> {
1606
1608
 
1607
1609
  // Delete all content
1608
1610
  if (bufferLength > 0) {
1609
- editor.deleteRange(mergeState.sourceBufferId, { start: 0, end: bufferLength });
1611
+ editor.deleteRange(mergeState.sourceBufferId, 0, bufferLength);
1610
1612
  }
1611
1613
 
1612
1614
  // Insert resolved content
@@ -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