@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.
- package/CHANGELOG.md +60 -0
- package/package.json +1 -1
- package/plugins/config-schema.json +242 -25
- package/plugins/diagnostics_panel.ts +4 -12
- package/plugins/diff_nav.i18n.json +128 -0
- package/plugins/diff_nav.ts +196 -0
- package/plugins/git_gutter.ts +5 -0
- package/plugins/lib/finder.ts +19 -12
- package/plugins/lib/fresh.d.ts +5 -1
- package/plugins/pkg.ts +4 -29
- package/plugins/schemas/package.schema.json +437 -272
- package/plugins/schemas/theme.schema.json +18 -0
|
@@ -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");
|
package/plugins/git_gutter.ts
CHANGED
|
@@ -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
|
}
|
package/plugins/lib/finder.ts
CHANGED
|
@@ -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
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
this.editor.
|
|
1267
|
-
|
|
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 {
|
package/plugins/lib/fresh.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
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");
|