@fresh-editor/fresh-editor 0.3.6 → 0.3.8
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 +139 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +112 -0
- package/plugins/audit_mode.ts +173 -6
- package/plugins/config-schema.json +34 -3
- package/plugins/dashboard.ts +18 -18
- package/plugins/env-manager.ts +168 -0
- package/plugins/flash.ts +22 -4
- package/plugins/git_blame.ts +10 -6
- package/plugins/git_log.ts +589 -196
- package/plugins/git_statusbar.i18n.json +72 -0
- package/plugins/git_statusbar.ts +133 -0
- package/plugins/lib/fresh.d.ts +412 -6
- package/plugins/lib/widgets.ts +111 -4
- package/plugins/live_diff.ts +168 -58
- package/plugins/merge_conflict.ts +89 -64
- package/plugins/orchestrator.ts +2174 -296
- package/plugins/pkg.ts +169 -4
- package/plugins/schemas/theme.schema.json +53 -0
- package/plugins/search_replace.i18n.json +140 -28
- package/plugins/search_replace.ts +674 -117
- package/plugins/tab_actions.i18n.json +212 -0
- package/plugins/tab_actions.ts +76 -0
- package/plugins/theme_editor.i18n.json +112 -84
- package/plugins/tsconfig.json +2 -0
- package/plugins/vi_mode.ts +11 -0
- package/themes/dark.json +1 -0
- package/themes/dracula.json +1 -0
- package/themes/high-contrast.json +1 -0
- package/themes/light.json +2 -1
- package/themes/nord.json +1 -0
- package/themes/nostalgia.json +1 -0
- package/themes/solarized-dark.json +1 -0
- package/themes/terminal.json +4 -3
package/plugins/audit_mode.ts
CHANGED
|
@@ -1743,7 +1743,30 @@ function applyCursorLineOverlay(panel: 'diff'): void {
|
|
|
1743
1743
|
});
|
|
1744
1744
|
}
|
|
1745
1745
|
|
|
1746
|
-
function review_refresh() {
|
|
1746
|
+
function review_refresh() {
|
|
1747
|
+
// Synchronously acknowledge the keypress before kicking off the
|
|
1748
|
+
// async `git status` + `git diff`. Those calls take long enough on
|
|
1749
|
+
// a non-trivial repo that, without this immediate status update,
|
|
1750
|
+
// the sticky-header totals visibly lag the new content: users
|
|
1751
|
+
// press `r`, see the old `+N / -M`, conclude the keystroke was
|
|
1752
|
+
// dropped, and press `r` again — which then "appears" to work
|
|
1753
|
+
// because the first refresh has by then landed. See #2036.
|
|
1754
|
+
//
|
|
1755
|
+
// In range mode the refresh is intentionally a no-op for
|
|
1756
|
+
// working-tree edits (the diff is always between two refs); the
|
|
1757
|
+
// range-specific message explains that up front so the user
|
|
1758
|
+
// doesn't think `r` is broken when their unstaged changes don't
|
|
1759
|
+
// show up.
|
|
1760
|
+
if (state.mode === 'range' && state.range) {
|
|
1761
|
+
editor.setStatus(
|
|
1762
|
+
editor.t("status.refreshing_range", { range: state.range.label }) ||
|
|
1763
|
+
`Refreshing ${state.range.label}... (working tree not included)`
|
|
1764
|
+
);
|
|
1765
|
+
} else {
|
|
1766
|
+
editor.setStatus(editor.t("status.refreshing") || "Refreshing review diff...");
|
|
1767
|
+
}
|
|
1768
|
+
void refreshMagitData();
|
|
1769
|
+
}
|
|
1747
1770
|
registerHandler("review_refresh", review_refresh);
|
|
1748
1771
|
|
|
1749
1772
|
// --- Cursor-driven navigation ---
|
|
@@ -3163,13 +3186,21 @@ function updateReviewStatus(): void {
|
|
|
3163
3186
|
if (state.groupId === null) return;
|
|
3164
3187
|
const total = state.hunkHeaderRows.length;
|
|
3165
3188
|
const current = currentGlobalHunkIndex();
|
|
3189
|
+
// Range reviews fundamentally don't include working-tree edits; the
|
|
3190
|
+
// suffix makes that visible from the status bar at all times rather
|
|
3191
|
+
// than only flashing past during a refresh. Without it users hit `r`,
|
|
3192
|
+
// see their unsaved changes don't appear, and conclude the refresh is
|
|
3193
|
+
// broken (#2036).
|
|
3194
|
+
const rangeNote = state.mode === 'range' && state.range
|
|
3195
|
+
? ` · ${editor.t("status.working_tree_not_included") || "working tree not included"}`
|
|
3196
|
+
: '';
|
|
3166
3197
|
if (current !== null) {
|
|
3167
3198
|
editor.setStatus(editor.t("status.review_summary_indexed", {
|
|
3168
3199
|
current: String(current),
|
|
3169
3200
|
count: String(total),
|
|
3170
|
-
}));
|
|
3201
|
+
}) + rangeNote);
|
|
3171
3202
|
} else {
|
|
3172
|
-
editor.setStatus(editor.t("status.review_summary", { count: String(total) }));
|
|
3203
|
+
editor.setStatus(editor.t("status.review_summary", { count: String(total) }) + rangeNote);
|
|
3173
3204
|
}
|
|
3174
3205
|
}
|
|
3175
3206
|
|
|
@@ -4640,13 +4671,121 @@ async function review_branch_refresh(): Promise<void> {
|
|
|
4640
4671
|
}
|
|
4641
4672
|
registerHandler("review_branch_refresh", review_branch_refresh);
|
|
4642
4673
|
|
|
4643
|
-
/**
|
|
4674
|
+
/** Is the detail panel the currently-focused buffer? */
|
|
4675
|
+
function isReviewBranchDetailFocused(): boolean {
|
|
4676
|
+
return (
|
|
4677
|
+
branchState.detailBufferId !== null &&
|
|
4678
|
+
editor.getActiveBufferId() === branchState.detailBufferId
|
|
4679
|
+
);
|
|
4680
|
+
}
|
|
4681
|
+
|
|
4682
|
+
/** The currently-selected commit in the log panel, or null. */
|
|
4683
|
+
function selectedReviewBranchCommit(): GitCommit | null {
|
|
4684
|
+
if (branchState.commits.length === 0) return null;
|
|
4685
|
+
const i = Math.max(
|
|
4686
|
+
0,
|
|
4687
|
+
Math.min(branchState.selectedIndex, branchState.commits.length - 1),
|
|
4688
|
+
);
|
|
4689
|
+
return branchState.commits[i] ?? null;
|
|
4690
|
+
}
|
|
4691
|
+
|
|
4692
|
+
/**
|
|
4693
|
+
* Enter: on the log panel jumps focus into the detail panel; on the detail
|
|
4694
|
+
* panel opens the file at the cursor position at the selected commit (if any).
|
|
4695
|
+
*/
|
|
4644
4696
|
function review_branch_enter(): void {
|
|
4645
4697
|
if (branchState.groupId === null) return;
|
|
4698
|
+
if (isReviewBranchDetailFocused()) {
|
|
4699
|
+
void review_branch_detail_open_file();
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4646
4702
|
editor.focusBufferGroupPanel(branchState.groupId, "detail");
|
|
4647
4703
|
}
|
|
4648
4704
|
registerHandler("review_branch_enter", review_branch_enter);
|
|
4649
4705
|
|
|
4706
|
+
/**
|
|
4707
|
+
* Open the file at the cursor's `(file, line)` text-properties at the
|
|
4708
|
+
* currently-selected commit, in a read-only virtual buffer. Mirrors the
|
|
4709
|
+
* git-log plugin's `git_log_detail_open_file` so users get the same
|
|
4710
|
+
* drill-down from the review-branch detail panel.
|
|
4711
|
+
*/
|
|
4712
|
+
async function review_branch_detail_open_file(): Promise<void> {
|
|
4713
|
+
if (branchState.detailBufferId === null) return;
|
|
4714
|
+
const commit = selectedReviewBranchCommit();
|
|
4715
|
+
if (!commit) return;
|
|
4716
|
+
|
|
4717
|
+
const props = editor.getTextPropertiesAtCursor(branchState.detailBufferId);
|
|
4718
|
+
if (props.length === 0) {
|
|
4719
|
+
editor.setStatus(editor.t("status.move_to_diff"));
|
|
4720
|
+
return;
|
|
4721
|
+
}
|
|
4722
|
+
const file = props[0].file as string | undefined;
|
|
4723
|
+
const line = (props[0].line as number | undefined) ?? 1;
|
|
4724
|
+
if (!file) {
|
|
4725
|
+
editor.setStatus(editor.t("status.move_to_diff_with_context"));
|
|
4726
|
+
return;
|
|
4727
|
+
}
|
|
4728
|
+
|
|
4729
|
+
editor.setStatus(
|
|
4730
|
+
editor.t("status.file_loading", { file, hash: commit.shortHash }),
|
|
4731
|
+
);
|
|
4732
|
+
const result = await editor.spawnProcess("git", [
|
|
4733
|
+
"show",
|
|
4734
|
+
`${commit.hash}:${file}`,
|
|
4735
|
+
]);
|
|
4736
|
+
if (result.exit_code !== 0) {
|
|
4737
|
+
editor.setStatus(
|
|
4738
|
+
editor.t("status.file_not_found", { file, hash: commit.shortHash }),
|
|
4739
|
+
);
|
|
4740
|
+
return;
|
|
4741
|
+
}
|
|
4742
|
+
|
|
4743
|
+
const lines = result.stdout.split("\n");
|
|
4744
|
+
const entries: TextPropertyEntry[] = lines.map((l, i) => ({
|
|
4745
|
+
text: l + (i < lines.length - 1 ? "\n" : ""),
|
|
4746
|
+
properties: { type: "content", line: i + 1 },
|
|
4747
|
+
}));
|
|
4748
|
+
|
|
4749
|
+
// `*<hash>:<path>*` matches the virtual-name convention the host uses
|
|
4750
|
+
// to detect syntax from the trailing filename's extension.
|
|
4751
|
+
const name = `*${commit.shortHash}:${file}*`;
|
|
4752
|
+
const view = await editor.createVirtualBuffer({
|
|
4753
|
+
name,
|
|
4754
|
+
mode: "review-branch-file-view",
|
|
4755
|
+
readOnly: true,
|
|
4756
|
+
editingDisabled: true,
|
|
4757
|
+
showLineNumbers: true,
|
|
4758
|
+
entries,
|
|
4759
|
+
});
|
|
4760
|
+
if (view) {
|
|
4761
|
+
const byte = await editor.getLineStartPosition(Math.max(0, line - 1));
|
|
4762
|
+
if (byte !== null) editor.setBufferCursor(view.bufferId, byte);
|
|
4763
|
+
editor.setStatus(
|
|
4764
|
+
editor.t("status.file_view_ready", {
|
|
4765
|
+
file,
|
|
4766
|
+
hash: commit.shortHash,
|
|
4767
|
+
line: String(line),
|
|
4768
|
+
}),
|
|
4769
|
+
);
|
|
4770
|
+
} else {
|
|
4771
|
+
editor.setStatus(editor.t("status.failed_open_file", { file }));
|
|
4772
|
+
}
|
|
4773
|
+
}
|
|
4774
|
+
registerHandler(
|
|
4775
|
+
"review_branch_detail_open_file",
|
|
4776
|
+
review_branch_detail_open_file,
|
|
4777
|
+
);
|
|
4778
|
+
|
|
4779
|
+
/** Tab: toggle focus between the log and detail panels. */
|
|
4780
|
+
function review_branch_tab(): void {
|
|
4781
|
+
if (branchState.groupId === null) return;
|
|
4782
|
+
editor.focusBufferGroupPanel(
|
|
4783
|
+
branchState.groupId,
|
|
4784
|
+
isReviewBranchDetailFocused() ? "log" : "detail",
|
|
4785
|
+
);
|
|
4786
|
+
}
|
|
4787
|
+
registerHandler("review_branch_tab", review_branch_tab);
|
|
4788
|
+
|
|
4650
4789
|
/** q/Escape: focus-back from detail, or close when already on log. */
|
|
4651
4790
|
function review_branch_close_or_back(): void {
|
|
4652
4791
|
if (branchState.groupId === null) return;
|
|
@@ -4683,9 +4822,11 @@ editor.defineMode(
|
|
|
4683
4822
|
// from the Normal keymap via `inheritNormalBindings: true`.
|
|
4684
4823
|
["k", "move_up"],
|
|
4685
4824
|
["j", "move_down"],
|
|
4686
|
-
// Enter: focus the
|
|
4825
|
+
// Enter: from the log, focus the detail panel; from the detail
|
|
4826
|
+
// panel, open the file at the cursor at the selected commit.
|
|
4687
4827
|
["Return", "review_branch_enter"],
|
|
4688
|
-
|
|
4828
|
+
// Tab: toggle focus between the log and detail panels.
|
|
4829
|
+
["Tab", "review_branch_tab"],
|
|
4689
4830
|
["r", "review_branch_refresh"],
|
|
4690
4831
|
["q", "review_branch_close_or_back"],
|
|
4691
4832
|
["Escape", "review_branch_close_or_back"],
|
|
@@ -4695,6 +4836,32 @@ editor.defineMode(
|
|
|
4695
4836
|
true, // inheritNormalBindings — PageUp/PageDown/arrows/Home/End come from Normal
|
|
4696
4837
|
);
|
|
4697
4838
|
|
|
4839
|
+
/** Close the file-view virtual buffer opened from the review-branch detail panel. */
|
|
4840
|
+
function review_branch_file_view_close(): void {
|
|
4841
|
+
const id = editor.getActiveBufferId();
|
|
4842
|
+
if (id) editor.closeBuffer(id);
|
|
4843
|
+
}
|
|
4844
|
+
registerHandler("review_branch_file_view_close", review_branch_file_view_close);
|
|
4845
|
+
|
|
4846
|
+
// Mode for the read-only "git show <hash>:<file>" buffer opened from the
|
|
4847
|
+
// review-branch detail panel. Mirrors git-log's `git-log-file-view`:
|
|
4848
|
+
// q/Escape close the view, j/k alias Up/Down, and all other Normal
|
|
4849
|
+
// bindings (arrows, PageUp/Down, Home/End, Ctrl+C copy) are inherited so
|
|
4850
|
+
// unbound keys don't fall through to edit actions and trip the
|
|
4851
|
+
// `editing_disabled` status message (see #566).
|
|
4852
|
+
editor.defineMode(
|
|
4853
|
+
"review-branch-file-view",
|
|
4854
|
+
[
|
|
4855
|
+
["k", "move_up"],
|
|
4856
|
+
["j", "move_down"],
|
|
4857
|
+
["q", "review_branch_file_view_close"],
|
|
4858
|
+
["Escape", "review_branch_file_view_close"],
|
|
4859
|
+
],
|
|
4860
|
+
true, // read-only
|
|
4861
|
+
false, // allow_text_input
|
|
4862
|
+
true, // inherit Normal-context bindings for unbound keys
|
|
4863
|
+
);
|
|
4864
|
+
|
|
4698
4865
|
// Register Modes and Commands
|
|
4699
4866
|
editor.registerCommand("%cmd.review_diff", "%cmd.review_diff_desc", "start_review_diff", null);
|
|
4700
4867
|
editor.registerCommand("%cmd.review_branch", "%cmd.review_branch_desc", "start_review_branch", null);
|
|
@@ -103,6 +103,7 @@
|
|
|
103
103
|
"auto_save_enabled": false,
|
|
104
104
|
"auto_save_interval_secs": 30,
|
|
105
105
|
"hot_exit": true,
|
|
106
|
+
"confirm_quit": false,
|
|
106
107
|
"restore_previous_session": true,
|
|
107
108
|
"skip_session_restore_when_files_passed": true,
|
|
108
109
|
"auto_create_empty_buffer_on_last_buffer_close": true,
|
|
@@ -134,7 +135,10 @@
|
|
|
134
135
|
"preview_tabs": true,
|
|
135
136
|
"side": "left",
|
|
136
137
|
"auto_open_on_last_buffer_close": true,
|
|
137
|
-
"follow_active_buffer": false
|
|
138
|
+
"follow_active_buffer": false,
|
|
139
|
+
"compact_directories": true,
|
|
140
|
+
"tree_indicator_collapsed": ">",
|
|
141
|
+
"tree_indicator_expanded": "▼"
|
|
138
142
|
}
|
|
139
143
|
},
|
|
140
144
|
"file_browser": {
|
|
@@ -652,6 +656,12 @@
|
|
|
652
656
|
"default": true,
|
|
653
657
|
"x-section": "Recovery"
|
|
654
658
|
},
|
|
659
|
+
"confirm_quit": {
|
|
660
|
+
"description": "Whether to confirm before quitting Fresh. When enabled, pressing\nthe quit shortcut surfaces a confirmation prompt even when no\nbuffers are modified. Useful for users who frequently hit\n`Ctrl+Q` by mistake. Independent of the unsaved-changes prompt,\nwhich always fires regardless of this setting.\nDefault: false",
|
|
661
|
+
"type": "boolean",
|
|
662
|
+
"default": false,
|
|
663
|
+
"x-section": "Startup"
|
|
664
|
+
},
|
|
655
665
|
"restore_previous_session": {
|
|
656
666
|
"description": "Whether to auto-open previously opened files (session restore) when\nstarting Fresh in a directory. When enabled (the default), tabs,\nsplits, cursor positions and the file explorer state are restored\nfrom the last clean exit in the same working directory. When\ndisabled, Fresh starts with a clean workspace. The workspace file\non disk is still written on exit, so re-enabling this setting picks\nup whatever state was saved at the most recent clean exit. The\n`--no-restore` CLI flag is a stronger override: it skips both\nrestoring and saving the workspace.\nDefault: true",
|
|
657
667
|
"type": "boolean",
|
|
@@ -793,7 +803,8 @@
|
|
|
793
803
|
"{messages}"
|
|
794
804
|
],
|
|
795
805
|
"x-section": "Status Bar",
|
|
796
|
-
"x-dual-list-sibling": "/editor/status_bar/right"
|
|
806
|
+
"x-dual-list-sibling": "/editor/status_bar/right",
|
|
807
|
+
"x-dynamically-extendable-status-bar-elements": true
|
|
797
808
|
},
|
|
798
809
|
"right": {
|
|
799
810
|
"description": "Elements shown on the right side of the status bar.\nDefault: [\"{line_ending}\", \"{encoding}\", \"{language}\", \"{lsp}\", \"{warnings}\", \"{update}\", \"{palette}\"]",
|
|
@@ -811,7 +822,8 @@
|
|
|
811
822
|
"{palette}"
|
|
812
823
|
],
|
|
813
824
|
"x-section": "Status Bar",
|
|
814
|
-
"x-dual-list-sibling": "/editor/status_bar/left"
|
|
825
|
+
"x-dual-list-sibling": "/editor/status_bar/left",
|
|
826
|
+
"x-dynamically-extendable-status-bar-elements": true
|
|
815
827
|
}
|
|
816
828
|
}
|
|
817
829
|
},
|
|
@@ -958,6 +970,21 @@
|
|
|
958
970
|
"description": "When the file explorer sidebar is open, automatically expand the\ntree and highlight the file that corresponds to the active buffer\nwhenever you switch tabs. Set to `true` to keep the explorer\nselection in sync with the active tab.\nDefault: false",
|
|
959
971
|
"type": "boolean",
|
|
960
972
|
"default": false
|
|
973
|
+
},
|
|
974
|
+
"compact_directories": {
|
|
975
|
+
"description": "Render single-child directory chains on a single line, e.g.\n`src/main/java/com/example`. Only applies when each intermediate\ndirectory in the chain is expanded and has exactly one visible\nchild that is itself a directory. Mirrors VSCode's\n`explorer.compactFolders`.\nDefault: true",
|
|
976
|
+
"type": "boolean",
|
|
977
|
+
"default": true
|
|
978
|
+
},
|
|
979
|
+
"tree_indicator_collapsed": {
|
|
980
|
+
"description": "Symbol shown next to a collapsed (closed) directory in the file\nexplorer tree. A short string (single character recommended).\nA trailing space is added automatically during rendering; the\nrenderer pads narrower indicators so collapsed/expanded rows align.\nDefault: \">\"",
|
|
981
|
+
"type": "string",
|
|
982
|
+
"default": ">"
|
|
983
|
+
},
|
|
984
|
+
"tree_indicator_expanded": {
|
|
985
|
+
"description": "Symbol shown next to an expanded (open) directory in the file\nexplorer tree. A short string (single character recommended).\nA trailing space is added automatically during rendering; the\nrenderer pads narrower indicators so collapsed/expanded rows align.\nDefault: \"▼\"",
|
|
986
|
+
"type": "string",
|
|
987
|
+
"default": "▼"
|
|
961
988
|
}
|
|
962
989
|
}
|
|
963
990
|
},
|
|
@@ -1643,6 +1670,10 @@
|
|
|
1643
1670
|
"null"
|
|
1644
1671
|
],
|
|
1645
1672
|
"readOnly": true
|
|
1673
|
+
},
|
|
1674
|
+
"settings": {
|
|
1675
|
+
"description": "Plugin-specific settings. The shape is defined by each plugin's\n`<plugin_name>.schema.json` sidecar file; the host stores the value as\nuntyped JSON so a malformed plugin schema can't poison the rest of the\nconfig. Plugins read this via `editor.getPluginConfig()` and the\nSettings UI renders it as a sub-category under \"Plugin Settings\".",
|
|
1676
|
+
"readOnly": true
|
|
1646
1677
|
}
|
|
1647
1678
|
},
|
|
1648
1679
|
"x-display-field": "/enabled"
|
package/plugins/dashboard.ts
CHANGED
|
@@ -1586,18 +1586,20 @@ async function dashboardShowOrFocus() {
|
|
|
1586
1586
|
registerHandler("dashboardShowOrFocus", dashboardShowOrFocus);
|
|
1587
1587
|
|
|
1588
1588
|
// Auto-open resolution: the session override (set via the exported
|
|
1589
|
-
// plugin API from init.ts) wins over the user
|
|
1590
|
-
//
|
|
1591
|
-
//
|
|
1592
|
-
|
|
1589
|
+
// plugin API from init.ts) wins over the user-configured value, which
|
|
1590
|
+
// comes from the typed plugin-config field declared below. The field
|
|
1591
|
+
// shows up in the Settings UI under "Plugin Settings → dashboard".
|
|
1592
|
+
editor.defineConfigBoolean("autoOpen", {
|
|
1593
|
+
default: true,
|
|
1594
|
+
description: "Show the dashboard automatically when Fresh starts with no real files open.",
|
|
1595
|
+
});
|
|
1596
|
+
|
|
1593
1597
|
let autoOpenOverride: boolean | null = null;
|
|
1594
1598
|
|
|
1595
1599
|
function autoOpenEnabled(): boolean {
|
|
1596
1600
|
if (autoOpenOverride !== null) return autoOpenOverride;
|
|
1597
|
-
const cfg = editor.
|
|
1598
|
-
|
|
1599
|
-
const dashboard = plugins?.dashboard as Record<string, unknown> | undefined;
|
|
1600
|
-
return dashboard?.["auto-open"] !== false;
|
|
1601
|
+
const cfg = (editor.getPluginConfig() ?? {}) as { autoOpen?: boolean };
|
|
1602
|
+
return cfg.autoOpen !== false;
|
|
1601
1603
|
}
|
|
1602
1604
|
|
|
1603
1605
|
function shouldShowDashboard(): boolean {
|
|
@@ -1801,12 +1803,14 @@ editor.exportPluginApi("dashboard", {
|
|
|
1801
1803
|
// `plugins.dashboard.enabled` is true in the resolved config — so the
|
|
1802
1804
|
// standard settings UI is the single enable/disable surface.
|
|
1803
1805
|
//
|
|
1804
|
-
//
|
|
1805
|
-
//
|
|
1806
|
-
//
|
|
1807
|
-
//
|
|
1808
|
-
//
|
|
1809
|
-
//
|
|
1806
|
+
// Auto-open is driven exclusively by the `ready` hook (and the
|
|
1807
|
+
// `buffer_closed` handler for the last-tab-closed case). We
|
|
1808
|
+
// deliberately do NOT auto-open at module load: dashboard.ts loads
|
|
1809
|
+
// during the startup plugin batch, *before* the user's init.ts has
|
|
1810
|
+
// been evaluated, so an immediate auto-open would race
|
|
1811
|
+
// `setAutoOpen(false)` and dismiss the user's preference. Users who
|
|
1812
|
+
// hot-load the plugin mid-session (toggle on in Settings) get the
|
|
1813
|
+
// dashboard via the "Show Dashboard" command in the palette.
|
|
1810
1814
|
editor.on("ready", "dashboardOnReady");
|
|
1811
1815
|
editor.on("buffer_closed", "dashboardOnBufferClosed");
|
|
1812
1816
|
editor.on("viewport_changed", "dashboardOnViewportChanged");
|
|
@@ -1820,7 +1824,3 @@ editor.registerCommand(
|
|
|
1820
1824
|
"Open the dashboard, or bring it to the front if it's already open",
|
|
1821
1825
|
"dashboardShowOrFocus",
|
|
1822
1826
|
);
|
|
1823
|
-
|
|
1824
|
-
if (editor.listBuffers().length > 0 && shouldShowDashboard()) {
|
|
1825
|
-
openDashboard();
|
|
1826
|
-
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/// <reference path="./lib/fresh.d.ts" />
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Environment Manager
|
|
5
|
+
*
|
|
6
|
+
* Detects a project's environment manager (Python venv, direnv, mise) and
|
|
7
|
+
* activates it by handing core an activation **snippet** via `editor.setEnv`.
|
|
8
|
+
* Core captures the resulting environment on the active backend (local / SSH)
|
|
9
|
+
* and applies it to every editor-spawned process — language servers,
|
|
10
|
+
* formatters, `spawnProcess`.
|
|
11
|
+
*
|
|
12
|
+
* Detection is passive (reads files only). Activation runs repo-controlled
|
|
13
|
+
* code, so it is gated on Workspace Trust: the plugin only calls `setEnv` when
|
|
14
|
+
* `editor.workspaceTrustLevel() === "trusted"` (and core enforces the same).
|
|
15
|
+
*
|
|
16
|
+
* Freshness: one-shot spawns re-capture automatically when the env inputs
|
|
17
|
+
* change (core's cache is keyed on them). A long-running language server has
|
|
18
|
+
* its env fixed at spawn, so to pick up a changed `.envrc`/`mise.toml` the
|
|
19
|
+
* user runs **Env: Reload**, which re-captures and restarts servers. (Auto
|
|
20
|
+
* file-watching is intentionally not wired yet.)
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const editor = getEditor();
|
|
24
|
+
|
|
25
|
+
const STATUS_TOKEN = "env";
|
|
26
|
+
|
|
27
|
+
interface Detected {
|
|
28
|
+
/** Short label for the status pill, e.g. ".venv" / "direnv" / "mise". */
|
|
29
|
+
name: string;
|
|
30
|
+
/** The activation snippet handed to `editor.setEnv`. */
|
|
31
|
+
snippet: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function fileExists(p: string): boolean {
|
|
35
|
+
try {
|
|
36
|
+
return editor.fileExists(p);
|
|
37
|
+
} catch (_e) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Detect the environment in the current workspace and return its activation
|
|
44
|
+
* snippet, or null if none. These are auto-detected default snippets; direnv
|
|
45
|
+
* and mise need their exporters (they're prompt-hook driven), venv sources its
|
|
46
|
+
* activate script, and anything else is a pure login shell / user snippet.
|
|
47
|
+
*/
|
|
48
|
+
function detect(): Detected | null {
|
|
49
|
+
const cwd = editor.getCwd();
|
|
50
|
+
if (!cwd) return null;
|
|
51
|
+
|
|
52
|
+
for (const name of [".venv", "venv"]) {
|
|
53
|
+
const dir = editor.pathJoin(cwd, name);
|
|
54
|
+
if (
|
|
55
|
+
fileExists(editor.pathJoin(dir, "bin", "python")) ||
|
|
56
|
+
fileExists(editor.pathJoin(dir, "bin", "python3")) ||
|
|
57
|
+
fileExists(editor.pathJoin(dir, "Scripts", "python.exe"))
|
|
58
|
+
) {
|
|
59
|
+
return { name, snippet: `source ${editor.pathJoin(dir, "bin", "activate")}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (fileExists(editor.pathJoin(cwd, ".envrc"))) {
|
|
64
|
+
return { name: "direnv", snippet: `eval "$(direnv export bash)"` };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const name of ["mise.toml", ".mise.toml", ".tool-versions"]) {
|
|
68
|
+
if (fileExists(editor.pathJoin(cwd, name))) {
|
|
69
|
+
return { name: "mise", snippet: `eval "$(mise env -s bash)"` };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isTrusted(): boolean {
|
|
77
|
+
return editor.workspaceTrustLevel() === "trusted";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// === Commands ===
|
|
81
|
+
|
|
82
|
+
/** Activate (or, when already active, reload) the detected environment. */
|
|
83
|
+
function activate(): void {
|
|
84
|
+
if (!isTrusted()) {
|
|
85
|
+
editor.setStatus(
|
|
86
|
+
"Workspace not trusted — run “Workspace Trust: Trust This Folder” to activate the environment",
|
|
87
|
+
);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const det = detect();
|
|
91
|
+
if (!det) {
|
|
92
|
+
editor.setStatus("No environment manager detected in this project");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Core captures `snippet` on the active backend and applies it to every
|
|
96
|
+
// spawn; it restarts so language servers re-spawn under the fresh env.
|
|
97
|
+
editor.setEnv(det.snippet, editor.getCwd());
|
|
98
|
+
editor.setStatus(
|
|
99
|
+
`${editor.envActive() ? "Reloading" : "Activating"} ${det.name} environment…`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
registerHandler("env_activate_handler", activate);
|
|
103
|
+
|
|
104
|
+
function useSystem(): void {
|
|
105
|
+
editor.clearEnv();
|
|
106
|
+
editor.setStatus("Environment deactivated — using the system environment");
|
|
107
|
+
}
|
|
108
|
+
registerHandler("env_use_system_handler", useSystem);
|
|
109
|
+
|
|
110
|
+
function showStatus(): void {
|
|
111
|
+
const det = detect();
|
|
112
|
+
const trust = editor.workspaceTrustLevel() || "unavailable";
|
|
113
|
+
if (editor.envActive()) {
|
|
114
|
+
editor.setStatus(`Environment active${det ? ` (${det.name})` : ""}`);
|
|
115
|
+
} else if (det) {
|
|
116
|
+
editor.setStatus(
|
|
117
|
+
`Detected ${det.name} (trust: ${trust}). Run “Env: Activate” to use it.`,
|
|
118
|
+
);
|
|
119
|
+
} else {
|
|
120
|
+
editor.setStatus(`No environment detected (trust: ${trust})`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
registerHandler("env_status_handler", showStatus);
|
|
124
|
+
|
|
125
|
+
editor.registerCommand(
|
|
126
|
+
"env_activate",
|
|
127
|
+
"Env: Activate Detected Environment (venv / direnv / mise)",
|
|
128
|
+
"env_activate_handler",
|
|
129
|
+
);
|
|
130
|
+
editor.registerCommand(
|
|
131
|
+
"env_reload",
|
|
132
|
+
"Env: Reload Environment (re-capture after .envrc/mise.toml change)",
|
|
133
|
+
"env_activate_handler",
|
|
134
|
+
);
|
|
135
|
+
editor.registerCommand(
|
|
136
|
+
"env_use_system",
|
|
137
|
+
"Env: Use System (Deactivate Environment)",
|
|
138
|
+
"env_use_system_handler",
|
|
139
|
+
);
|
|
140
|
+
editor.registerCommand(
|
|
141
|
+
"env_status",
|
|
142
|
+
"Env: Show Environment Status",
|
|
143
|
+
"env_status_handler",
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
// === Status pill (opt-in to a user's status-bar layout) ===
|
|
147
|
+
|
|
148
|
+
function refreshStatus(): void {
|
|
149
|
+
const bufferId = editor.getActiveBufferId();
|
|
150
|
+
if (bufferId === 0) return;
|
|
151
|
+
const det = detect();
|
|
152
|
+
let value: string;
|
|
153
|
+
if (editor.envActive()) {
|
|
154
|
+
value = det ? `${det.name} ✓` : "active";
|
|
155
|
+
} else {
|
|
156
|
+
value = det ? `${det.name}${isTrusted() ? "" : " (locked)"}` : "system";
|
|
157
|
+
}
|
|
158
|
+
editor.setStatusBarValue(bufferId, STATUS_TOKEN, value);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
editor.registerStatusBarElement(STATUS_TOKEN, "Environment");
|
|
162
|
+
|
|
163
|
+
registerHandler("env_refresh_status", refreshStatus);
|
|
164
|
+
for (const event of ["buffer_activated", "after_file_open", "focus_gained"]) {
|
|
165
|
+
editor.on(event, "env_refresh_status");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
refreshStatus();
|
package/plugins/flash.ts
CHANGED
|
@@ -33,7 +33,24 @@ const VTEXT_PREFIX = "flash-";
|
|
|
33
33
|
// the closest jump targets get the most comfortable keys. All
|
|
34
34
|
// lowercase: case-sensitive matching keeps the label letter from also
|
|
35
35
|
// being a valid pattern continuation, which matters for the skip rule.
|
|
36
|
-
|
|
36
|
+
editor.defineConfigString("labelPool", {
|
|
37
|
+
default: "asdfghjklqwertyuiopzxcvbnm",
|
|
38
|
+
description: "Characters used as jump labels, in comfort order. Labels are assigned to matches by distance from the cursor, so leftmost characters here land on the nearest matches.",
|
|
39
|
+
});
|
|
40
|
+
editor.defineConfigBoolean("skipRule", {
|
|
41
|
+
default: true,
|
|
42
|
+
description: "Skip a label character if it is also the next character after a match — prevents ambiguity between extending the search pattern and jumping.",
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
function flashSettings(): { labelPool: string; skipRule: boolean } {
|
|
46
|
+
const cfg = (editor.getPluginConfig() ?? {}) as { labelPool?: string; skipRule?: boolean };
|
|
47
|
+
return {
|
|
48
|
+
labelPool: cfg.labelPool && cfg.labelPool.length > 0
|
|
49
|
+
? cfg.labelPool
|
|
50
|
+
: "asdfghjklqwertyuiopzxcvbnm",
|
|
51
|
+
skipRule: cfg.skipRule ?? true,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
37
54
|
|
|
38
55
|
interface Match {
|
|
39
56
|
/** Byte offset where the match starts in its buffer. */
|
|
@@ -334,9 +351,10 @@ function assignLabels(
|
|
|
334
351
|
prevLabelByKey: Map<string, string>,
|
|
335
352
|
): Match[] {
|
|
336
353
|
if (matches.length === 0) return matches;
|
|
337
|
-
const
|
|
354
|
+
const { labelPool, skipRule } = flashSettings();
|
|
355
|
+
const skip = skipRule ? buildSkipSet(matches, views, emptyPattern) : new Set<string>();
|
|
338
356
|
const remaining = new Set<string>();
|
|
339
|
-
for (const c of
|
|
357
|
+
for (const c of labelPool) if (!skip.has(c)) remaining.add(c);
|
|
340
358
|
|
|
341
359
|
const sorted = sortMatches(matches, startSplitId, startCursor);
|
|
342
360
|
|
|
@@ -353,7 +371,7 @@ function assignLabels(
|
|
|
353
371
|
// matches in distance order. Iterate the pool in its native
|
|
354
372
|
// (comfort-ranked) order so home-row letters go to nearest matches.
|
|
355
373
|
const orderedRemaining: string[] = [];
|
|
356
|
-
for (const c of
|
|
374
|
+
for (const c of labelPool) if (remaining.has(c)) orderedRemaining.push(c);
|
|
357
375
|
let next = 0;
|
|
358
376
|
for (const m of sorted) {
|
|
359
377
|
if (m.label) continue;
|
package/plugins/git_blame.ts
CHANGED
|
@@ -87,10 +87,13 @@ const blameState: BlameState = {
|
|
|
87
87
|
// Color Definitions for Header Styling
|
|
88
88
|
// =============================================================================
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
// Blame headers are rendered via `addVirtualLine`, which accepts theme
|
|
91
|
+
// keys directly — so we don't expose colors as plugin settings. Themes
|
|
92
|
+
// drive the look; if a theme lacks specific blame keys, these fall
|
|
93
|
+
// through to the editor's status-bar palette which is what every theme
|
|
94
|
+
// defines.
|
|
95
|
+
const HEADER_FG_KEY = "ui.status_bar_fg";
|
|
96
|
+
const HEADER_BG_KEY = "ui.status_bar_bg";
|
|
94
97
|
|
|
95
98
|
// =============================================================================
|
|
96
99
|
// Mode Definition
|
|
@@ -373,7 +376,8 @@ function addBlameHeaders(): void {
|
|
|
373
376
|
// Clear existing headers first
|
|
374
377
|
editor.clearVirtualTextNamespace(blameState.bufferId, BLAME_NAMESPACE);
|
|
375
378
|
|
|
376
|
-
// Add a virtual line above each block
|
|
379
|
+
// Add a virtual line above each block. Pass theme keys so the headers
|
|
380
|
+
// restyle automatically when the user switches themes.
|
|
377
381
|
for (const block of blameState.blocks) {
|
|
378
382
|
const headerText = formatBlockHeader(block);
|
|
379
383
|
|
|
@@ -381,7 +385,7 @@ function addBlameHeaders(): void {
|
|
|
381
385
|
blameState.bufferId,
|
|
382
386
|
block.startByte, // anchor position
|
|
383
387
|
headerText, // text content
|
|
384
|
-
{ fg:
|
|
388
|
+
{ fg: HEADER_FG_KEY, bg: HEADER_BG_KEY },
|
|
385
389
|
true, // above (LineAbove)
|
|
386
390
|
BLAME_NAMESPACE, // namespace for bulk removal
|
|
387
391
|
0 // priority
|