@fresh-editor/fresh-editor 0.2.18 → 0.2.20

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.
@@ -0,0 +1,196 @@
1
+ /// <reference path="./lib/fresh.d.ts" />
2
+ const editor = getEditor();
3
+
4
+ /**
5
+ * Diff Navigation Plugin
6
+ *
7
+ * Provides unified next/previous change commands that merge changes from all
8
+ * available diff sources: git diff AND piece-tree saved-diff. This means a
9
+ * single keybinding pair navigates both committed and unsaved changes.
10
+ *
11
+ * When only one source is available (e.g. file not tracked by git), it still
12
+ * works using that source alone.
13
+ */
14
+
15
+ // =============================================================================
16
+ // Types
17
+ // =============================================================================
18
+
19
+ interface DiffHunk {
20
+ type: "added" | "modified" | "deleted";
21
+ startLine: number; // 1-indexed
22
+ lineCount: number;
23
+ }
24
+
25
+ /** A jump target with a byte position for sorting/deduplication */
26
+ interface JumpTarget {
27
+ bytePos: number;
28
+ line: number; // 0-indexed, for scrollToLineCenter
29
+ }
30
+
31
+ // =============================================================================
32
+ // Collecting jump targets from all sources
33
+ // =============================================================================
34
+
35
+ async function collectTargets(bid: number): Promise<JumpTarget[]> {
36
+ const targets: JumpTarget[] = [];
37
+
38
+ // Source 1: git gutter hunks
39
+ const hunks = editor.getViewState(bid, "git_gutter_hunks") as DiffHunk[] | null;
40
+ if (hunks && hunks.length > 0) {
41
+ for (const hunk of hunks) {
42
+ const line = Math.max(0, hunk.startLine - 1); // 0-indexed
43
+ const pos = await editor.getLineStartPosition(line);
44
+ if (pos !== null) {
45
+ targets.push({ bytePos: pos, line });
46
+ }
47
+ }
48
+ }
49
+
50
+ // Source 2: saved-diff (unsaved changes)
51
+ const diff = editor.getBufferSavedDiff(bid);
52
+ if (diff && !diff.equal) {
53
+ for (const [start, _end] of diff.byte_ranges) {
54
+ // We don't know the line yet; resolve it lazily after dedup
55
+ targets.push({ bytePos: start, line: -1 });
56
+ }
57
+ }
58
+
59
+ if (targets.length === 0) return targets;
60
+
61
+ // Sort by byte position
62
+ targets.sort((a, b) => a.bytePos - b.bytePos);
63
+
64
+ // Deduplicate: if two targets are on the same line, keep the first.
65
+ // Resolve line numbers for saved-diff targets that still have line = -1.
66
+ const deduped: JumpTarget[] = [];
67
+ const seenLines = new Set<number>();
68
+
69
+ for (const t of targets) {
70
+ // Resolve line if unknown
71
+ if (t.line === -1) {
72
+ // Jump cursor temporarily to find the line, then restore.
73
+ // Instead, use a simpler heuristic: find the line by checking
74
+ // existing targets or using getLineStartPosition in reverse.
75
+ // Actually, we can set cursor, read line, but that's side-effectful.
76
+ // Simpler: just check if any existing target has a bytePos close enough.
77
+ // For dedup, we check if any already-added target has same bytePos.
78
+ let isDup = false;
79
+ for (const existing of deduped) {
80
+ if (Math.abs(existing.bytePos - t.bytePos) < 2) {
81
+ isDup = true;
82
+ break;
83
+ }
84
+ }
85
+ if (isDup) continue;
86
+ deduped.push(t);
87
+ } else {
88
+ if (seenLines.has(t.line)) continue;
89
+ seenLines.add(t.line);
90
+ // Also check if a saved-diff target at similar byte pos was already added
91
+ let isDup = false;
92
+ for (const existing of deduped) {
93
+ if (existing.line === -1 && Math.abs(existing.bytePos - t.bytePos) < 2) {
94
+ // Replace the unresolved one with this one (which has a known line)
95
+ existing.line = t.line;
96
+ isDup = true;
97
+ break;
98
+ }
99
+ }
100
+ if (isDup) continue;
101
+ deduped.push(t);
102
+ }
103
+ }
104
+
105
+ return deduped;
106
+ }
107
+
108
+ // =============================================================================
109
+ // Navigation
110
+ // =============================================================================
111
+
112
+ function goToTarget(bid: number, target: JumpTarget): void {
113
+ if (target.line >= 0) {
114
+ const splitId = editor.getActiveSplitId();
115
+ editor.scrollToLineCenter(splitId, bid, target.line);
116
+ }
117
+ editor.setBufferCursor(bid, target.bytePos);
118
+ }
119
+
120
+ async function diff_nav_next(): Promise<void> {
121
+ const bid = editor.getActiveBufferId();
122
+ const targets = await collectTargets(bid);
123
+
124
+ if (targets.length === 0) {
125
+ editor.setStatus(editor.t("status.no_changes"));
126
+ return;
127
+ }
128
+
129
+ const cursor = editor.getCursorPosition();
130
+ let idx = targets.findIndex((t) => t.bytePos > cursor);
131
+ let wrapped = false;
132
+ if (idx === -1) {
133
+ idx = 0;
134
+ wrapped = true;
135
+ }
136
+
137
+ goToTarget(bid, targets[idx]);
138
+
139
+ const msg = wrapped
140
+ ? editor.t("status.change_wrapped", { n: String(idx + 1), total: String(targets.length) })
141
+ : editor.t("status.change", { n: String(idx + 1), total: String(targets.length) });
142
+ editor.setStatus(msg);
143
+ }
144
+ registerHandler("diff_nav_next", diff_nav_next);
145
+
146
+ async function diff_nav_prev(): Promise<void> {
147
+ const bid = editor.getActiveBufferId();
148
+ const targets = await collectTargets(bid);
149
+
150
+ if (targets.length === 0) {
151
+ editor.setStatus(editor.t("status.no_changes"));
152
+ return;
153
+ }
154
+
155
+ const cursor = editor.getCursorPosition();
156
+ let idx = -1;
157
+ for (let i = targets.length - 1; i >= 0; i--) {
158
+ if (targets[i].bytePos < cursor) {
159
+ idx = i;
160
+ break;
161
+ }
162
+ }
163
+ let wrapped = false;
164
+ if (idx === -1) {
165
+ idx = targets.length - 1;
166
+ wrapped = true;
167
+ }
168
+
169
+ goToTarget(bid, targets[idx]);
170
+
171
+ const msg = wrapped
172
+ ? editor.t("status.change_wrapped", { n: String(idx + 1), total: String(targets.length) })
173
+ : editor.t("status.change", { n: String(idx + 1), total: String(targets.length) });
174
+ editor.setStatus(msg);
175
+ }
176
+ registerHandler("diff_nav_prev", diff_nav_prev);
177
+
178
+ // =============================================================================
179
+ // Registration
180
+ // =============================================================================
181
+
182
+ editor.registerCommand(
183
+ "%cmd.next_change",
184
+ "%cmd.next_change_desc",
185
+ "diff_nav_next",
186
+ null
187
+ );
188
+
189
+ editor.registerCommand(
190
+ "%cmd.prev_change",
191
+ "%cmd.prev_change_desc",
192
+ "diff_nav_prev",
193
+ null
194
+ );
195
+
196
+ editor.debug("Diff Nav plugin loaded");
@@ -248,6 +248,8 @@ async function updateGitGutter(bufferId: number): Promise<void> {
248
248
  editor.debug("Git Gutter: file not tracked by git");
249
249
  editor.clearLineIndicators(bufferId, NAMESPACE);
250
250
  state.hunks = [];
251
+ // Signal to other plugins that git is not available for this buffer
252
+ editor.setViewState(bufferId, "git_gutter_hunks", null);
251
253
  return;
252
254
  }
253
255
 
@@ -304,6 +306,9 @@ async function updateGitGutter(bufferId: number): Promise<void> {
304
306
  }
305
307
 
306
308
  state.hunks = hunks;
309
+
310
+ // Export hunks for other plugins (e.g. diff_nav) via shared view state
311
+ editor.setViewState(bufferId, "git_gutter_hunks", hunks);
307
312
  } finally {
308
313
  state.updating = false;
309
314
  }
@@ -115,6 +115,9 @@ export interface FinderConfig<T> {
115
115
 
116
116
  /** Panel-specific: navigate source split when cursor moves (preview without focus change) */
117
117
  navigateOnCursorMove?: boolean;
118
+
119
+ /** Called when the panel or prompt is closed (e.g. via Escape) */
120
+ onClose?: () => void;
118
121
  }
119
122
 
120
123
  /**
@@ -1254,18 +1257,17 @@ export class Finder<T> {
1254
1257
  if (this.config.onSelect) {
1255
1258
  this.config.onSelect(item, entry);
1256
1259
  } else if (entry.location) {
1257
- // Default: open file at location
1258
- if (this.panelState.sourceSplitId !== null) {
1259
- this.editor.focusSplit(this.panelState.sourceSplitId);
1260
- }
1261
- this.editor.openFile(
1262
- entry.location.file,
1263
- entry.location.line,
1264
- entry.location.column
1265
- );
1266
- this.editor.setStatus(
1267
- `Jumped to ${entry.location.file}:${entry.location.line}`
1268
- );
1260
+ const loc = entry.location;
1261
+
1262
+ // Close the panel first. This is necessary because
1263
+ // navigateOnCursorMove's focusSplit(panelSplitId) can interfere with
1264
+ // the jump — it queues a FocusSplit that runs after OpenFileInSplit
1265
+ // and restores the panel as the active split.
1266
+ this.closePanel();
1267
+
1268
+ // Now navigate with the panel gone — only one split remains
1269
+ this.editor.openFile(loc.file, loc.line, loc.column);
1270
+ this.editor.setStatus(`Jumped to ${loc.file}:${loc.line}`);
1269
1271
  }
1270
1272
  }
1271
1273
 
@@ -1306,6 +1308,11 @@ export class Finder<T> {
1306
1308
  }
1307
1309
 
1308
1310
  this.editor.setStatus("Closed");
1311
+
1312
+ // Notify the caller that the panel was closed
1313
+ if (this.config.onClose) {
1314
+ this.config.onClose();
1315
+ }
1309
1316
  }
1310
1317
 
1311
1318
  private revealItem(index: number): void {
@@ -539,7 +539,6 @@ type BackgroundProcessResult = {
539
539
  type BufferSavedDiff = {
540
540
  equal: boolean;
541
541
  byte_ranges: Array<[number, number]>;
542
- line_ranges: Array<[number, number]> | null;
543
542
  };
544
543
  type CreateVirtualBufferInExistingSplitOptions = {
545
544
  /**
@@ -1096,6 +1095,11 @@ interface EditorAPI {
1096
1095
  */
1097
1096
  reloadGrammars(): Promise<void>;
1098
1097
  /**
1098
+ * Get the directory where this plugin's files are stored.
1099
+ * For package plugins this is `<plugins_dir>/packages/<plugin_name>/`.
1100
+ */
1101
+ getPluginDir(): string;
1102
+ /**
1099
1103
  * Get config directory path
1100
1104
  */
1101
1105
  getConfigDir(): string;
package/plugins/pkg.ts CHANGED
@@ -3035,34 +3035,9 @@ editor.registerCommand("%cmd.install_url", "%cmd.install_url_desc", "pkg_install
3035
3035
  // Note: Other commands (install_plugin, install_theme, update, remove, sync, etc.)
3036
3036
  // are available via the package manager UI and don't need global command palette entries.
3037
3037
 
3038
- // =============================================================================
3039
- // Startup: Load installed language packs and bundles
3040
- // =============================================================================
3041
-
3042
- (async function loadInstalledPackages() {
3043
- // Load language packs
3044
- const languages = getInstalledPackages("language");
3045
- for (const pkg of languages) {
3046
- if (pkg.manifest) {
3047
- editor.debug(`[pkg] Loading language pack: ${pkg.name}`);
3048
- await loadLanguagePack(pkg.path, pkg.manifest);
3049
- }
3050
- }
3051
- if (languages.length > 0) {
3052
- editor.debug(`[pkg] Loaded ${languages.length} language pack(s)`);
3053
- }
3054
-
3055
- // Load bundles
3056
- const bundles = getInstalledPackages("bundle");
3057
- for (const pkg of bundles) {
3058
- if (pkg.manifest) {
3059
- editor.debug(`[pkg] Loading bundle: ${pkg.name}`);
3060
- await loadBundle(pkg.path, pkg.manifest);
3061
- }
3062
- }
3063
- if (bundles.length > 0) {
3064
- editor.debug(`[pkg] Loaded ${bundles.length} bundle(s)`);
3065
- }
3066
- })();
3038
+ // Note: Startup loading of installed language packs and bundles is now handled
3039
+ // by Rust (services::packages::scan_installed_packages) during editor init.
3040
+ // The loadLanguagePack() and loadBundle() functions above are still used for
3041
+ // runtime installs via the package manager UI.
3067
3042
 
3068
3043
  editor.debug("Package Manager plugin loaded");