@fresh-editor/fresh-editor 0.2.18 → 0.2.21

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,128 @@
1
+ {
2
+ "en": {
3
+ "cmd.next_change": "Next Change",
4
+ "cmd.next_change_desc": "Jump to the next changed region",
5
+ "cmd.prev_change": "Previous Change",
6
+ "cmd.prev_change_desc": "Jump to the previous changed region",
7
+ "status.no_changes": "No changes",
8
+ "status.change": "Change %{n}/%{total}",
9
+ "status.change_wrapped": "Change %{n}/%{total} [wrapped]"
10
+ },
11
+ "cs": {
12
+ "cmd.next_change": "Další změna",
13
+ "cmd.next_change_desc": "Přejít na další změněnou oblast",
14
+ "cmd.prev_change": "Předchozí změna",
15
+ "cmd.prev_change_desc": "Přejít na předchozí změněnou oblast",
16
+ "status.no_changes": "Žádné změny",
17
+ "status.change": "Změna %{n}/%{total}",
18
+ "status.change_wrapped": "Změna %{n}/%{total} [zabaleno]"
19
+ },
20
+ "de": {
21
+ "cmd.next_change": "Nächste Änderung",
22
+ "cmd.next_change_desc": "Zur nächsten geänderten Region springen",
23
+ "cmd.prev_change": "Vorherige Änderung",
24
+ "cmd.prev_change_desc": "Zur vorherigen geänderten Region springen",
25
+ "status.no_changes": "Keine Änderungen",
26
+ "status.change": "Änderung %{n}/%{total}",
27
+ "status.change_wrapped": "Änderung %{n}/%{total} [umgebrochen]"
28
+ },
29
+ "es": {
30
+ "cmd.next_change": "Siguiente cambio",
31
+ "cmd.next_change_desc": "Saltar a la siguiente región modificada",
32
+ "cmd.prev_change": "Cambio anterior",
33
+ "cmd.prev_change_desc": "Saltar a la región modificada anterior",
34
+ "status.no_changes": "Sin cambios",
35
+ "status.change": "Cambio %{n}/%{total}",
36
+ "status.change_wrapped": "Cambio %{n}/%{total} [envuelto]"
37
+ },
38
+ "fr": {
39
+ "cmd.next_change": "Modification suivante",
40
+ "cmd.next_change_desc": "Aller à la prochaine région modifiée",
41
+ "cmd.prev_change": "Modification précédente",
42
+ "cmd.prev_change_desc": "Aller à la région modifiée précédente",
43
+ "status.no_changes": "Aucune modification",
44
+ "status.change": "Modification %{n}/%{total}",
45
+ "status.change_wrapped": "Modification %{n}/%{total} [bouclé]"
46
+ },
47
+ "it": {
48
+ "cmd.next_change": "Modifica successiva",
49
+ "cmd.next_change_desc": "Vai alla prossima regione modificata",
50
+ "cmd.prev_change": "Modifica precedente",
51
+ "cmd.prev_change_desc": "Vai alla regione modificata precedente",
52
+ "status.no_changes": "Nessuna modifica",
53
+ "status.change": "Modifica %{n}/%{total}",
54
+ "status.change_wrapped": "Modifica %{n}/%{total} [avvolto]"
55
+ },
56
+ "ja": {
57
+ "cmd.next_change": "次の変更",
58
+ "cmd.next_change_desc": "次の変更箇所にジャンプ",
59
+ "cmd.prev_change": "前の変更",
60
+ "cmd.prev_change_desc": "前の変更箇所にジャンプ",
61
+ "status.no_changes": "変更なし",
62
+ "status.change": "変更 %{n}/%{total}",
63
+ "status.change_wrapped": "変更 %{n}/%{total} [折り返し]"
64
+ },
65
+ "ko": {
66
+ "cmd.next_change": "다음 변경",
67
+ "cmd.next_change_desc": "다음 변경된 영역으로 이동",
68
+ "cmd.prev_change": "이전 변경",
69
+ "cmd.prev_change_desc": "이전 변경된 영역으로 이동",
70
+ "status.no_changes": "변경 없음",
71
+ "status.change": "변경 %{n}/%{total}",
72
+ "status.change_wrapped": "변경 %{n}/%{total} [순환]"
73
+ },
74
+ "pt-BR": {
75
+ "cmd.next_change": "Próxima alteração",
76
+ "cmd.next_change_desc": "Ir para a próxima região alterada",
77
+ "cmd.prev_change": "Alteração anterior",
78
+ "cmd.prev_change_desc": "Ir para a região alterada anterior",
79
+ "status.no_changes": "Sem alterações",
80
+ "status.change": "Alteração %{n}/%{total}",
81
+ "status.change_wrapped": "Alteração %{n}/%{total} [retornou]"
82
+ },
83
+ "ru": {
84
+ "cmd.next_change": "Следующее изменение",
85
+ "cmd.next_change_desc": "Перейти к следующей изменённой области",
86
+ "cmd.prev_change": "Предыдущее изменение",
87
+ "cmd.prev_change_desc": "Перейти к предыдущей изменённой области",
88
+ "status.no_changes": "Нет изменений",
89
+ "status.change": "Изменение %{n}/%{total}",
90
+ "status.change_wrapped": "Изменение %{n}/%{total} [цикл]"
91
+ },
92
+ "th": {
93
+ "cmd.next_change": "การเปลี่ยนแปลงถัดไป",
94
+ "cmd.next_change_desc": "ข้ามไปยังพื้นที่ที่เปลี่ยนแปลงถัดไป",
95
+ "cmd.prev_change": "การเปลี่ยนแปลงก่อนหน้า",
96
+ "cmd.prev_change_desc": "ข้ามไปยังพื้นที่ที่เปลี่ยนแปลงก่อนหน้า",
97
+ "status.no_changes": "ไม่มีการเปลี่ยนแปลง",
98
+ "status.change": "การเปลี่ยนแปลง %{n}/%{total}",
99
+ "status.change_wrapped": "การเปลี่ยนแปลง %{n}/%{total} [วนรอบ]"
100
+ },
101
+ "uk": {
102
+ "cmd.next_change": "Наступна зміна",
103
+ "cmd.next_change_desc": "Перейти до наступної зміненої області",
104
+ "cmd.prev_change": "Попередня зміна",
105
+ "cmd.prev_change_desc": "Перейти до попередньої зміненої області",
106
+ "status.no_changes": "Немає змін",
107
+ "status.change": "Зміна %{n}/%{total}",
108
+ "status.change_wrapped": "Зміна %{n}/%{total} [цикл]"
109
+ },
110
+ "vi": {
111
+ "cmd.next_change": "Thay đổi tiếp theo",
112
+ "cmd.next_change_desc": "Nhảy đến vùng thay đổi tiếp theo",
113
+ "cmd.prev_change": "Thay đổi trước đó",
114
+ "cmd.prev_change_desc": "Nhảy đến vùng thay đổi trước đó",
115
+ "status.no_changes": "Không có thay đổi",
116
+ "status.change": "Thay đổi %{n}/%{total}",
117
+ "status.change_wrapped": "Thay đổi %{n}/%{total} [quay vòng]"
118
+ },
119
+ "zh-CN": {
120
+ "cmd.next_change": "下一个更改",
121
+ "cmd.next_change_desc": "跳转到下一个已更改的区域",
122
+ "cmd.prev_change": "上一个更改",
123
+ "cmd.prev_change_desc": "跳转到上一个已更改的区域",
124
+ "status.no_changes": "没有更改",
125
+ "status.change": "更改 %{n}/%{total}",
126
+ "status.change_wrapped": "更改 %{n}/%{total} [已循环]"
127
+ }
128
+ }
@@ -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");
@@ -156,8 +156,14 @@ function onGitExplorerEditorInitialized() {
156
156
  }
157
157
  registerHandler("onGitExplorerEditorInitialized", onGitExplorerEditorInitialized);
158
158
 
159
+ function onGitExplorerFocusGained() {
160
+ refreshGitExplorerDecorations();
161
+ }
162
+ registerHandler("onGitExplorerFocusGained", onGitExplorerFocusGained);
163
+
159
164
  editor.on("after_file_open", "onGitExplorerAfterFileOpen");
160
165
  editor.on("after_file_save", "onGitExplorerAfterFileSave");
161
166
  editor.on("editor_initialized", "onGitExplorerEditorInitialized");
167
+ editor.on("focus_gained", "onGitExplorerFocusGained");
162
168
 
163
169
  refreshGitExplorerDecorations();
@@ -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");