@fresh-editor/fresh-editor 0.3.5 → 0.3.7
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 +147 -0
- package/README.md +9 -2
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +84 -0
- package/plugins/audit_mode.ts +139 -3
- package/plugins/config-schema.json +33 -3
- package/plugins/dashboard.ts +34 -111
- package/plugins/flash.ts +22 -4
- package/plugins/git_blame.ts +10 -6
- package/plugins/git_log.ts +705 -323
- package/plugins/git_statusbar.i18n.json +72 -0
- package/plugins/git_statusbar.ts +133 -0
- package/plugins/goto_with_selection.i18n.json +58 -0
- package/plugins/goto_with_selection.ts +17 -0
- package/plugins/lib/fresh.d.ts +911 -15
- package/plugins/lib/index.ts +34 -0
- package/plugins/lib/widgets.ts +903 -0
- package/plugins/live_diff.ts +442 -32
- package/plugins/merge_conflict.ts +89 -64
- package/plugins/orchestrator.ts +3425 -0
- package/plugins/pkg.ts +235 -54
- package/plugins/rust-lsp.ts +58 -40
- package/plugins/schemas/theme.schema.json +18 -0
- package/plugins/search_replace.i18n.json +140 -28
- package/plugins/search_replace.ts +1335 -515
- package/plugins/tab_actions.i18n.json +212 -0
- package/plugins/tab_actions.ts +76 -0
- package/plugins/theme_editor.i18n.json +112 -0
- package/plugins/theme_editor.ts +30 -5
- package/plugins/tsconfig.json +3 -0
- package/plugins/vi_mode.ts +49 -17
- package/themes/dark.json +1 -0
- package/themes/dracula.json +1 -0
- package/themes/high-contrast.json +1 -0
- package/themes/light.json +1 -0
- package/themes/nord.json +1 -0
- package/themes/nostalgia.json +1 -0
- package/themes/solarized-dark.json +1 -0
- package/themes/terminal.json +4 -0
package/plugins/pkg.ts
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
/// <reference path="./lib/fresh.d.ts" />
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
hintBar,
|
|
5
|
+
key,
|
|
6
|
+
list,
|
|
7
|
+
parseHintString,
|
|
8
|
+
WidgetPanel,
|
|
9
|
+
} from "./lib/index.ts";
|
|
10
|
+
|
|
3
11
|
/**
|
|
4
12
|
* Fresh Package Manager Plugin
|
|
5
13
|
*
|
|
@@ -668,7 +676,7 @@ function validatePackage(packageDir: string, packageName: string): ValidationRes
|
|
|
668
676
|
|
|
669
677
|
// For plugins, validate entry file exists
|
|
670
678
|
if (manifest.type === "plugin") {
|
|
671
|
-
const entryFile = manifest.fresh?.entry || `${manifest.name}.ts`;
|
|
679
|
+
const entryFile = manifest.fresh?.entry || manifest.fresh?.main || `${manifest.name}.ts`;
|
|
672
680
|
const entryPath = editor.pathJoin(packageDir, entryFile);
|
|
673
681
|
|
|
674
682
|
if (!editor.fileExists(entryPath)) {
|
|
@@ -1658,6 +1666,16 @@ interface PkgManagerState {
|
|
|
1658
1666
|
// Buffer group fields
|
|
1659
1667
|
groupId: number | null;
|
|
1660
1668
|
panelBuffers: Record<string, number>;
|
|
1669
|
+
/** Widget panels owned by the pkg manager. The list panel hosts a
|
|
1670
|
+
* `List` widget (one row per package) so the host owns selection
|
|
1671
|
+
* highlight, scroll, click-to-select, mouse wheel, and PageUp/Down.
|
|
1672
|
+
* The footer panel hosts a `HintBar` widget (the Navigate / Tab /
|
|
1673
|
+
* Search / Esc cheat-sheet). Header and detail panels still use
|
|
1674
|
+
* `setPanelContent` since their UI shape spans cross-panel focus
|
|
1675
|
+
* (filter / sync / search / action buttons) which is a larger
|
|
1676
|
+
* redesign. */
|
|
1677
|
+
listPanel: WidgetPanel | null;
|
|
1678
|
+
footerPanel: WidgetPanel | null;
|
|
1661
1679
|
}
|
|
1662
1680
|
|
|
1663
1681
|
const pkgState: PkgManagerState = {
|
|
@@ -1674,6 +1692,8 @@ const pkgState: PkgManagerState = {
|
|
|
1674
1692
|
viewportHeight: 24,
|
|
1675
1693
|
groupId: null,
|
|
1676
1694
|
panelBuffers: {},
|
|
1695
|
+
listPanel: null,
|
|
1696
|
+
footerPanel: null,
|
|
1677
1697
|
};
|
|
1678
1698
|
|
|
1679
1699
|
// Theme-aware color configuration
|
|
@@ -2040,53 +2060,163 @@ function buildPkgHeaderEntries(): TextPropertyEntry[] {
|
|
|
2040
2060
|
return entries;
|
|
2041
2061
|
}
|
|
2042
2062
|
|
|
2043
|
-
|
|
2063
|
+
/** Build the per-row item entries for the package List widget.
|
|
2064
|
+
* The list iterates "installed first, then available" to match the
|
|
2065
|
+
* existing buffer-render order, so the row's index in this array
|
|
2066
|
+
* matches `pkgState.selectedIndex`. The leading `▸` arrow that the
|
|
2067
|
+
* old hand-rolled renderer used on the focused row is gone — the
|
|
2068
|
+
* List widget paints the selection highlight via `ui.menu_active_bg`
|
|
2069
|
+
* which carries the same "this row is selected" signal. Section
|
|
2070
|
+
* headers (`INSTALLED (n)` / `AVAILABLE (n)`) are kept as
|
|
2071
|
+
* non-selectable rows; selecting them would be a no-op so the list
|
|
2072
|
+
* navigation skips them implicitly via the index math. */
|
|
2073
|
+
interface PkgListRow {
|
|
2074
|
+
entry: TextPropertyEntry;
|
|
2075
|
+
/** Stable widget-key for the List item. Empty string for non-
|
|
2076
|
+
* selectable header rows (the List still tracks them in
|
|
2077
|
+
* itemKeys but they don't carry useful identity). */
|
|
2078
|
+
key: string;
|
|
2079
|
+
/** Index within `getFilteredItems()` if this row is a real
|
|
2080
|
+
* package, else -1. The widget's `selectedIndex` is reported
|
|
2081
|
+
* back via `widget_event "select"` and we use it to look up
|
|
2082
|
+
* the matching package via this map. */
|
|
2083
|
+
itemIndex: number;
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
function buildPkgListRows(): PkgListRow[] {
|
|
2044
2087
|
const items = getFilteredItems();
|
|
2045
|
-
const installedItems = items.filter(i => i.installed);
|
|
2046
|
-
const availableItems = items.filter(i => !i.installed);
|
|
2047
|
-
const
|
|
2088
|
+
const installedItems = items.filter((i) => i.installed);
|
|
2089
|
+
const availableItems = items.filter((i) => !i.installed);
|
|
2090
|
+
const rows: PkgListRow[] = [];
|
|
2048
2091
|
|
|
2049
2092
|
if (installedItems.length > 0) {
|
|
2050
|
-
|
|
2093
|
+
rows.push({
|
|
2094
|
+
entry: {
|
|
2095
|
+
text: `INSTALLED (${installedItems.length})`,
|
|
2096
|
+
properties: { type: "section-title" },
|
|
2097
|
+
},
|
|
2098
|
+
key: "section.installed",
|
|
2099
|
+
itemIndex: -1,
|
|
2100
|
+
});
|
|
2051
2101
|
let idx = 0;
|
|
2052
2102
|
for (const item of installedItems) {
|
|
2053
|
-
const isSelected = idx === pkgState.selectedIndex;
|
|
2054
|
-
const listFocused = pkgState.focus.type === "list";
|
|
2055
|
-
const prefix = isSelected && listFocused ? "▸" : " ";
|
|
2056
2103
|
const status = item.updateAvailable ? "↑" : "✓";
|
|
2057
|
-
const ver =
|
|
2104
|
+
const ver =
|
|
2105
|
+
item.version.length > 7 ? item.version.slice(0, 6) + "…" : item.version;
|
|
2058
2106
|
const nameW = Math.max(8, LIST_WIDTH - 16);
|
|
2059
|
-
const name =
|
|
2060
|
-
|
|
2107
|
+
const name =
|
|
2108
|
+
item.name.length > nameW ? item.name.slice(0, nameW - 1) + "…" : item.name;
|
|
2109
|
+
rows.push({
|
|
2110
|
+
entry: {
|
|
2111
|
+
text: ` ${name.padEnd(nameW)} ${ver.padEnd(7)} ${status}`,
|
|
2112
|
+
properties: { type: "package-row", installed: true },
|
|
2113
|
+
},
|
|
2114
|
+
key: `pkg.${item.name}`,
|
|
2115
|
+
itemIndex: idx,
|
|
2116
|
+
});
|
|
2061
2117
|
idx++;
|
|
2062
2118
|
}
|
|
2063
2119
|
}
|
|
2064
2120
|
|
|
2065
2121
|
if (availableItems.length > 0) {
|
|
2066
|
-
if (
|
|
2067
|
-
|
|
2122
|
+
if (rows.length > 0) {
|
|
2123
|
+
rows.push({
|
|
2124
|
+
entry: { text: "", properties: { type: "blank" } },
|
|
2125
|
+
key: "blank",
|
|
2126
|
+
itemIndex: -1,
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
rows.push({
|
|
2130
|
+
entry: {
|
|
2131
|
+
text: `AVAILABLE (${availableItems.length})`,
|
|
2132
|
+
properties: { type: "section-title" },
|
|
2133
|
+
},
|
|
2134
|
+
key: "section.available",
|
|
2135
|
+
itemIndex: -1,
|
|
2136
|
+
});
|
|
2068
2137
|
let idx = installedItems.length;
|
|
2069
2138
|
for (const item of availableItems) {
|
|
2070
|
-
const
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2139
|
+
const typeTag =
|
|
2140
|
+
item.packageType === "theme"
|
|
2141
|
+
? "T"
|
|
2142
|
+
: item.packageType === "language"
|
|
2143
|
+
? "L"
|
|
2144
|
+
: item.packageType === "bundle"
|
|
2145
|
+
? "B"
|
|
2146
|
+
: "P";
|
|
2074
2147
|
const availNameW = Math.max(8, LIST_WIDTH - 10);
|
|
2075
|
-
const name =
|
|
2076
|
-
|
|
2148
|
+
const name =
|
|
2149
|
+
item.name.length > availNameW
|
|
2150
|
+
? item.name.slice(0, availNameW - 1) + "…"
|
|
2151
|
+
: item.name;
|
|
2152
|
+
rows.push({
|
|
2153
|
+
entry: {
|
|
2154
|
+
text: ` ${name.padEnd(availNameW)} [${typeTag}]`,
|
|
2155
|
+
properties: { type: "package-row", installed: false },
|
|
2156
|
+
},
|
|
2157
|
+
key: `pkg.${item.name}`,
|
|
2158
|
+
itemIndex: idx,
|
|
2159
|
+
});
|
|
2077
2160
|
idx++;
|
|
2078
2161
|
}
|
|
2079
2162
|
}
|
|
2080
2163
|
|
|
2081
2164
|
if (items.length === 0) {
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2165
|
+
rows.push({
|
|
2166
|
+
entry: {
|
|
2167
|
+
text: pkgState.isLoading ? "Loading..." : "No packages found",
|
|
2168
|
+
properties: { type: "empty-state" },
|
|
2169
|
+
},
|
|
2170
|
+
key: "empty",
|
|
2171
|
+
itemIndex: -1,
|
|
2172
|
+
});
|
|
2087
2173
|
}
|
|
2088
2174
|
|
|
2089
|
-
return
|
|
2175
|
+
return rows;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
/** Cache the row→item-index lookup so `widget_event "select"` can
|
|
2179
|
+
* map the host's reported selection index back to a real package
|
|
2180
|
+
* index in O(1) without rebuilding the row array. Refreshed by
|
|
2181
|
+
* `renderPkgList` every time the list is re-emitted. */
|
|
2182
|
+
let pkgListRowCache: PkgListRow[] = [];
|
|
2183
|
+
|
|
2184
|
+
function selectedRowToItemIndex(rowIndex: number): number {
|
|
2185
|
+
const row = pkgListRowCache[rowIndex];
|
|
2186
|
+
return row ? row.itemIndex : -1;
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function itemIndexToRow(itemIndex: number): number {
|
|
2190
|
+
for (let i = 0; i < pkgListRowCache.length; i++) {
|
|
2191
|
+
if (pkgListRowCache[i].itemIndex === itemIndex) return i;
|
|
2192
|
+
}
|
|
2193
|
+
return -1;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function renderPkgList(): void {
|
|
2197
|
+
if (pkgState.listPanel === null) return;
|
|
2198
|
+
const rows = buildPkgListRows();
|
|
2199
|
+
pkgListRowCache = rows;
|
|
2200
|
+
// Map the plugin's item-index selection to the row-index the List
|
|
2201
|
+
// widget understands. -1 falls back to the first selectable row.
|
|
2202
|
+
let rowSel = itemIndexToRow(pkgState.selectedIndex);
|
|
2203
|
+
if (rowSel < 0) {
|
|
2204
|
+
for (let i = 0; i < rows.length; i++) {
|
|
2205
|
+
if (rows[i].itemIndex >= 0) {
|
|
2206
|
+
rowSel = i;
|
|
2207
|
+
break;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
pkgState.listPanel.set(
|
|
2212
|
+
list({
|
|
2213
|
+
items: rows.map((r) => r.entry),
|
|
2214
|
+
itemKeys: rows.map((r) => r.key),
|
|
2215
|
+
selectedIndex: rowSel,
|
|
2216
|
+
visibleRows: Math.max(1, rows.length),
|
|
2217
|
+
key: "pkg-list",
|
|
2218
|
+
}),
|
|
2219
|
+
);
|
|
2090
2220
|
}
|
|
2091
2221
|
|
|
2092
2222
|
function buildPkgDetailEntries(): TextPropertyEntry[] {
|
|
@@ -2133,23 +2263,33 @@ function buildPkgDetailEntries(): TextPropertyEntry[] {
|
|
|
2133
2263
|
return entries;
|
|
2134
2264
|
}
|
|
2135
2265
|
|
|
2136
|
-
function
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
else if (pkgState.focus.type === "
|
|
2141
|
-
else if (pkgState.focus.type === "
|
|
2142
|
-
else
|
|
2143
|
-
|
|
2144
|
-
|
|
2266
|
+
function renderPkgFooter(): void {
|
|
2267
|
+
if (pkgState.footerPanel === null) return;
|
|
2268
|
+
let actionLabel = "Select";
|
|
2269
|
+
if (pkgState.focus.type === "action") actionLabel = "Activate";
|
|
2270
|
+
else if (pkgState.focus.type === "filter") actionLabel = "Filter";
|
|
2271
|
+
else if (pkgState.focus.type === "sync") actionLabel = "Sync";
|
|
2272
|
+
else if (pkgState.focus.type === "search") actionLabel = "Search";
|
|
2273
|
+
// The hint string follows the existing `parseHintString` shape:
|
|
2274
|
+
// tokens separated by two spaces, each token is `<keys>:<label>`
|
|
2275
|
+
// or `<keys> <label>`. The host's HintBar styles the keys portion
|
|
2276
|
+
// with `ui.help_key_fg`.
|
|
2277
|
+
const hintString = `↑↓:Navigate Tab:Next /:Search Enter:${actionLabel} Esc:Close`;
|
|
2278
|
+
pkgState.footerPanel.set(hintBar(parseHintString(hintString)));
|
|
2145
2279
|
}
|
|
2146
2280
|
|
|
2147
2281
|
function updatePkgManagerView(): void {
|
|
2148
2282
|
if (pkgState.groupId === null) return;
|
|
2283
|
+
// Header + detail still flow through the legacy `setPanelContent`
|
|
2284
|
+
// path — their UI shape (filter/sync/search inputs in the header,
|
|
2285
|
+
// action buttons in the detail) spans cross-panel focus, which
|
|
2286
|
+
// is a larger redesign best done together with the §4.1
|
|
2287
|
+
// Compositor work. List and footer migrate independently because
|
|
2288
|
+
// they don't participate in cross-panel focus.
|
|
2149
2289
|
editor.setPanelContent(pkgState.groupId, "header", buildPkgHeaderEntries());
|
|
2150
|
-
|
|
2290
|
+
renderPkgList();
|
|
2151
2291
|
editor.setPanelContent(pkgState.groupId, "detail", buildPkgDetailEntries());
|
|
2152
|
-
|
|
2292
|
+
renderPkgFooter();
|
|
2153
2293
|
}
|
|
2154
2294
|
|
|
2155
2295
|
/**
|
|
@@ -2206,6 +2346,17 @@ async function openPackageManager(): Promise<void> {
|
|
|
2206
2346
|
pkgState.panelBuffers = groupResult.panels;
|
|
2207
2347
|
pkgState.isOpen = true;
|
|
2208
2348
|
|
|
2349
|
+
// Mount widget panels for the list and footer. Header / detail
|
|
2350
|
+
// still use `setPanelContent` (see `updatePkgManagerView`).
|
|
2351
|
+
const listBufferId = pkgState.panelBuffers["list"];
|
|
2352
|
+
if (typeof listBufferId === "number") {
|
|
2353
|
+
pkgState.listPanel = new WidgetPanel(listBufferId);
|
|
2354
|
+
}
|
|
2355
|
+
const footerBufferId = pkgState.panelBuffers["footer"];
|
|
2356
|
+
if (typeof footerBufferId === "number") {
|
|
2357
|
+
pkgState.footerPanel = new WidgetPanel(footerBufferId);
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2209
2360
|
// Set initial content for all panels
|
|
2210
2361
|
updatePkgManagerView();
|
|
2211
2362
|
|
|
@@ -2239,11 +2390,17 @@ function closePackageManager(): void {
|
|
|
2239
2390
|
editor.showBuffer(pkgState.sourceBufferId);
|
|
2240
2391
|
}
|
|
2241
2392
|
|
|
2242
|
-
// Reset state
|
|
2393
|
+
// Reset state. The buffer group's close will tear down the panel
|
|
2394
|
+
// buffers and (implicitly) the widget panels rendering into them;
|
|
2395
|
+
// we just null the handles so any stray render call after close
|
|
2396
|
+
// is a no-op.
|
|
2243
2397
|
pkgState.isOpen = false;
|
|
2244
2398
|
pkgState.bufferId = null;
|
|
2245
2399
|
pkgState.splitId = null;
|
|
2246
2400
|
pkgState.sourceBufferId = null;
|
|
2401
|
+
pkgState.listPanel = null;
|
|
2402
|
+
pkgState.footerPanel = null;
|
|
2403
|
+
pkgListRowCache = [];
|
|
2247
2404
|
}
|
|
2248
2405
|
|
|
2249
2406
|
/**
|
|
@@ -2290,32 +2447,56 @@ function getCurrentFocusIndex(): number {
|
|
|
2290
2447
|
}
|
|
2291
2448
|
|
|
2292
2449
|
// Navigation commands
|
|
2293
|
-
function pkg_nav_up()
|
|
2450
|
+
function pkg_nav_up(): void {
|
|
2294
2451
|
if (!pkgState.isOpen) return;
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
// Always focus list and navigate (auto-focus behavior)
|
|
2300
|
-
pkgState.selectedIndex = Math.max(0, pkgState.selectedIndex - 1);
|
|
2452
|
+
// Always snap focus back to the list and let the host move the
|
|
2453
|
+
// List widget's selection. The resulting `widget_event "select"`
|
|
2454
|
+
// updates `pkgState.selectedIndex` and refreshes the detail
|
|
2455
|
+
// panel — see the `widget_event` listener installed below.
|
|
2301
2456
|
pkgState.focus = { type: "list" };
|
|
2302
|
-
|
|
2457
|
+
pkgState.listPanel?.command(key("Up"));
|
|
2303
2458
|
}
|
|
2304
2459
|
registerHandler("pkg_nav_up", pkg_nav_up);
|
|
2305
2460
|
|
|
2306
|
-
function pkg_nav_down()
|
|
2461
|
+
function pkg_nav_down(): void {
|
|
2307
2462
|
if (!pkgState.isOpen) return;
|
|
2308
|
-
|
|
2309
|
-
const items = getFilteredItems();
|
|
2310
|
-
if (items.length === 0) return;
|
|
2311
|
-
|
|
2312
|
-
// Always focus list and navigate (auto-focus behavior)
|
|
2313
|
-
pkgState.selectedIndex = Math.min(items.length - 1, pkgState.selectedIndex + 1);
|
|
2314
2463
|
pkgState.focus = { type: "list" };
|
|
2315
|
-
|
|
2464
|
+
pkgState.listPanel?.command(key("Down"));
|
|
2316
2465
|
}
|
|
2317
2466
|
registerHandler("pkg_nav_down", pkg_nav_down);
|
|
2318
2467
|
|
|
2468
|
+
editor.on("widget_event", (data) => {
|
|
2469
|
+
if (
|
|
2470
|
+
pkgState.listPanel === null ||
|
|
2471
|
+
data.panel_id !== pkgState.listPanel.id()
|
|
2472
|
+
) {
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
if (data.event_type === "select") {
|
|
2476
|
+
const rowIdx =
|
|
2477
|
+
typeof data.payload?.index === "number" ? data.payload.index : -1;
|
|
2478
|
+
if (rowIdx < 0) return;
|
|
2479
|
+
const itemIdx = selectedRowToItemIndex(rowIdx);
|
|
2480
|
+
if (itemIdx < 0) return; // selection landed on a section header
|
|
2481
|
+
if (itemIdx === pkgState.selectedIndex) return;
|
|
2482
|
+
pkgState.selectedIndex = itemIdx;
|
|
2483
|
+
pkgState.focus = { type: "list" };
|
|
2484
|
+
// Re-render header/detail to reflect the new selection;
|
|
2485
|
+
// the list itself is already updated by the host (we don't
|
|
2486
|
+
// need to call renderPkgList again).
|
|
2487
|
+
if (pkgState.groupId !== null) {
|
|
2488
|
+
editor.setPanelContent(pkgState.groupId, "header", buildPkgHeaderEntries());
|
|
2489
|
+
editor.setPanelContent(pkgState.groupId, "detail", buildPkgDetailEntries());
|
|
2490
|
+
}
|
|
2491
|
+
renderPkgFooter();
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
if (data.event_type === "activate") {
|
|
2495
|
+
void pkg_activate();
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
});
|
|
2499
|
+
|
|
2319
2500
|
function pkg_next_button() : void {
|
|
2320
2501
|
if (!pkgState.isOpen) return;
|
|
2321
2502
|
|
package/plugins/rust-lsp.ts
CHANGED
|
@@ -42,9 +42,38 @@ const INSTALL_COMMANDS = {
|
|
|
42
42
|
brew: "brew install rust-analyzer",
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
//
|
|
45
|
+
// Stable plugin id used as the namespace for our menu contributions
|
|
46
|
+
// and as the prefix on every `action_popup_result.action_id` we
|
|
47
|
+
// receive back from the editor.
|
|
48
|
+
const PLUGIN_ID = "rust-lsp";
|
|
49
|
+
|
|
50
|
+
// Track error state for Rust LSP so the menu contributions can be
|
|
51
|
+
// installed (when there's an error) or cleared (after recovery).
|
|
46
52
|
let rustLspError: { serverCommand: string; message: string } | null = null;
|
|
47
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Install the "fix-it" rows into the LSP-Servers popup for `rust`.
|
|
56
|
+
* Mirrors the previous `showActionPopup` payload: copy-install
|
|
57
|
+
* commands and a disable shortcut. Re-call with an empty array to
|
|
58
|
+
* clear our slice.
|
|
59
|
+
*
|
|
60
|
+
* Implements the merge half of #1941 follow-up "Option B": we no
|
|
61
|
+
* longer push our own separate popup; instead the editor's built-in
|
|
62
|
+
* LSP-Servers popup includes our rows under a "Plugin actions"
|
|
63
|
+
* section.
|
|
64
|
+
*/
|
|
65
|
+
function publishMenuContributions(): void {
|
|
66
|
+
if (rustLspError === null) {
|
|
67
|
+
editor.setLspMenuContributions(PLUGIN_ID, "rust", []);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
editor.setLspMenuContributions(PLUGIN_ID, "rust", [
|
|
71
|
+
{ id: "copy_rustup", label: `Copy: ${INSTALL_COMMANDS.rustup}` },
|
|
72
|
+
{ id: "copy_brew", label: `Copy: ${INSTALL_COMMANDS.brew}` },
|
|
73
|
+
{ id: "disable", label: "Disable Rust LSP" },
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
/**
|
|
49
78
|
* Handle LSP server errors for Rust
|
|
50
79
|
*/
|
|
@@ -59,16 +88,18 @@ editor.on("lsp_server_error", (data) => {
|
|
|
59
88
|
|
|
60
89
|
editor.debug(`rust-lsp: Server error - ${data.error_type}: ${data.message}`);
|
|
61
90
|
|
|
62
|
-
// Store error state for later reference
|
|
91
|
+
// Store error state for later reference, install fix-it rows into
|
|
92
|
+
// the LSP-Servers popup.
|
|
63
93
|
rustLspError = {
|
|
64
94
|
serverCommand: data.server_command,
|
|
65
95
|
message: data.message,
|
|
66
96
|
};
|
|
97
|
+
publishMenuContributions();
|
|
67
98
|
|
|
68
99
|
// Show a status message for immediate feedback
|
|
69
100
|
if (data.error_type === "not_found") {
|
|
70
101
|
editor.setStatus(
|
|
71
|
-
`Rust LSP server '${data.server_command}' not found. Click
|
|
102
|
+
`Rust LSP server '${data.server_command}' not found. Click the LSP indicator for help.`
|
|
72
103
|
);
|
|
73
104
|
} else {
|
|
74
105
|
editor.setStatus(`Rust LSP error: ${data.message}`);
|
|
@@ -76,39 +107,28 @@ editor.on("lsp_server_error", (data) => {
|
|
|
76
107
|
});
|
|
77
108
|
|
|
78
109
|
/**
|
|
79
|
-
*
|
|
110
|
+
* Detect recovery and clear stale fix-it rows
|
|
80
111
|
*/
|
|
81
112
|
|
|
82
113
|
|
|
83
|
-
// Register hook for status bar clicks
|
|
114
|
+
// Register hook for status bar clicks — used here ONLY to detect
|
|
115
|
+
// LSP recovery and clear stale contributions. The actual "fix-it"
|
|
116
|
+
// popup is the editor's built-in LSP-Servers popup with our
|
|
117
|
+
// contributed rows merged in (no more separate popup).
|
|
84
118
|
editor.on("lsp_status_clicked", (data) => {
|
|
85
|
-
|
|
86
|
-
`rust-lsp: lsp_status_clicked hook received - language=${data.language}, has_error=${data.has_error}, rustLspError=${rustLspError ? "SET" : "NULL"}`
|
|
87
|
-
);
|
|
88
|
-
|
|
89
|
-
// Only handle Rust language clicks when there's an error
|
|
90
|
-
if (data.language !== "rust" || !rustLspError) {
|
|
91
|
-
editor.debug(
|
|
92
|
-
`rust-lsp: Skipping - language check=${data.language !== "rust"}, error check=${!rustLspError}`
|
|
93
|
-
);
|
|
119
|
+
if (data.language !== "rust") {
|
|
94
120
|
return;
|
|
95
121
|
}
|
|
96
122
|
|
|
97
|
-
editor
|
|
98
|
-
|
|
99
|
-
//
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
{ id: "copy_brew", label: `Copy: ${INSTALL_COMMANDS.brew}` },
|
|
107
|
-
{ id: "disable", label: "Disable Rust LSP" },
|
|
108
|
-
{ id: "dismiss", label: "Dismiss (ESC)" },
|
|
109
|
-
],
|
|
110
|
-
});
|
|
111
|
-
editor.debug(`rust-lsp: showActionPopup returned ${result}`);
|
|
123
|
+
// Recovery: editor now reports no error for rust → LSP came back
|
|
124
|
+
// up (e.g. successful auto-restart after an external kill). Clear
|
|
125
|
+
// our error state and remove the fix-it rows from the popup so
|
|
126
|
+
// the user just sees the standard server actions. (#1941 issue 3)
|
|
127
|
+
if (!data.has_error && rustLspError !== null) {
|
|
128
|
+
editor.debug("rust-lsp: LSP recovered; clearing rustLspError + menu rows");
|
|
129
|
+
rustLspError = null;
|
|
130
|
+
publishMenuContributions();
|
|
131
|
+
}
|
|
112
132
|
});
|
|
113
133
|
|
|
114
134
|
/**
|
|
@@ -122,15 +142,17 @@ editor.on("action_popup_result", (data) => {
|
|
|
122
142
|
`rust-lsp: action_popup_result received - popup_id=${data.popup_id}, action_id=${data.action_id}`
|
|
123
143
|
);
|
|
124
144
|
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
|
|
145
|
+
// The editor routes contributed-row picks with `popup_id =
|
|
146
|
+
// "lsp_status"` and `action_id = "{plugin_id}|{item_id}"`.
|
|
147
|
+
const prefix = `${PLUGIN_ID}|`;
|
|
148
|
+
if (data.popup_id !== "lsp_status" || !data.action_id.startsWith(prefix)) {
|
|
128
149
|
return;
|
|
129
150
|
}
|
|
151
|
+
const itemId = data.action_id.slice(prefix.length);
|
|
130
152
|
|
|
131
|
-
editor.debug(`rust-lsp: Action selected - ${
|
|
153
|
+
editor.debug(`rust-lsp: Action selected - ${itemId}`);
|
|
132
154
|
|
|
133
|
-
switch (
|
|
155
|
+
switch (itemId) {
|
|
134
156
|
case "copy_rustup":
|
|
135
157
|
editor.setClipboard(INSTALL_COMMANDS.rustup);
|
|
136
158
|
editor.setStatus("Copied: " + INSTALL_COMMANDS.rustup);
|
|
@@ -145,15 +167,11 @@ editor.on("action_popup_result", (data) => {
|
|
|
145
167
|
editor.disableLspForLanguage("rust");
|
|
146
168
|
editor.setStatus("Rust LSP disabled");
|
|
147
169
|
rustLspError = null;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
case "dismiss":
|
|
151
|
-
case "dismissed":
|
|
152
|
-
// Just close the popup without action
|
|
170
|
+
publishMenuContributions();
|
|
153
171
|
break;
|
|
154
172
|
|
|
155
173
|
default:
|
|
156
|
-
editor.debug(`rust-lsp: Unknown action: ${
|
|
174
|
+
editor.debug(`rust-lsp: Unknown action: ${itemId}`);
|
|
157
175
|
}
|
|
158
176
|
});
|
|
159
177
|
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
79,
|
|
41
41
|
120
|
|
42
42
|
],
|
|
43
|
+
"selection_modifier": null,
|
|
43
44
|
"current_line_bg": [
|
|
44
45
|
40,
|
|
45
46
|
40,
|
|
@@ -180,6 +181,8 @@
|
|
|
180
181
|
"status_palette_bg": null,
|
|
181
182
|
"status_lsp_on_fg": null,
|
|
182
183
|
"status_lsp_on_bg": null,
|
|
184
|
+
"status_lsp_actionable_fg": null,
|
|
185
|
+
"status_lsp_actionable_bg": null,
|
|
183
186
|
"prompt_fg": "White",
|
|
184
187
|
"prompt_bg": "Black",
|
|
185
188
|
"prompt_selection_fg": "White",
|
|
@@ -205,6 +208,11 @@
|
|
|
205
208
|
255
|
|
206
209
|
],
|
|
207
210
|
"popup_text_fg": "White",
|
|
211
|
+
"text_input_selection_bg": [
|
|
212
|
+
58,
|
|
213
|
+
79,
|
|
214
|
+
120
|
|
215
|
+
],
|
|
208
216
|
"suggestion_bg": [
|
|
209
217
|
30,
|
|
210
218
|
30,
|
|
@@ -246,6 +254,7 @@
|
|
|
246
254
|
60,
|
|
247
255
|
80
|
|
248
256
|
],
|
|
257
|
+
"semantic_highlight_modifier": null,
|
|
249
258
|
"terminal_bg": "Default",
|
|
250
259
|
"terminal_fg": "Default",
|
|
251
260
|
"status_warning_indicator_bg": [
|
|
@@ -958,6 +967,15 @@
|
|
|
958
967
|
"$ref": "#/$defs/ColorDef",
|
|
959
968
|
"default": "White"
|
|
960
969
|
},
|
|
970
|
+
"text_input_selection_bg": {
|
|
971
|
+
"description": "Selection background inside a widget Text input. Reads against prompt_bg, so it needs higher contrast against that tint than editor.selection_bg.",
|
|
972
|
+
"$ref": "#/$defs/ColorDef",
|
|
973
|
+
"default": [
|
|
974
|
+
58,
|
|
975
|
+
79,
|
|
976
|
+
120
|
|
977
|
+
]
|
|
978
|
+
},
|
|
961
979
|
"suggestion_bg": {
|
|
962
980
|
"description": "Autocomplete suggestion background",
|
|
963
981
|
"$ref": "#/$defs/ColorDef",
|