@fresh-editor/fresh-editor 0.3.7 → 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 +48 -0
- package/package.json +1 -1
- package/plugins/audit_mode.i18n.json +28 -0
- package/plugins/audit_mode.ts +34 -3
- package/plugins/config-schema.json +7 -0
- package/plugins/env-manager.ts +168 -0
- package/plugins/git_log.ts +58 -75
- package/plugins/lib/fresh.d.ts +109 -2
- package/plugins/live_diff.ts +12 -17
- package/plugins/orchestrator.ts +320 -182
- package/plugins/pkg.ts +168 -3
- package/plugins/schemas/theme.schema.json +53 -14
- package/plugins/theme_editor.i18n.json +84 -84
- package/plugins/tsconfig.json +1 -0
- package/themes/light.json +1 -1
- package/themes/terminal.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,53 @@
|
|
|
1
1
|
# Release Notes
|
|
2
2
|
|
|
3
|
+
## 0.3.8
|
|
4
|
+
|
|
5
|
+
### Features / Improvements
|
|
6
|
+
|
|
7
|
+
#### Environment Managers (venv / direnv / mise)
|
|
8
|
+
|
|
9
|
+
New built-in **environment-manager** plugin: it detects a project's environment manager — `.venv`/`venv`, `.envrc` (direnv), or `mise.toml`/`.tool-versions` — and, via **Env: Activate**, injects that environment into every editor-spawned process (LSP, formatters, terminals, `spawnProcess`). **Env: Use System (Deactivate)** restores the system environment, and **Env: Show Status** reports the current state. Activation is on-demand (not automatic) and respects Workspace Trust. Adds an opt-in `env` status-bar element.
|
|
10
|
+
|
|
11
|
+
#### Remote Development
|
|
12
|
+
|
|
13
|
+
* **LSP over SSH** now runs the language server *on the remote host* instead of falling back to a host-local process, so completions and diagnostics reflect the remote toolchain.
|
|
14
|
+
|
|
15
|
+
#### Workspace Trust (groundwork)
|
|
16
|
+
|
|
17
|
+
Foundational **Workspace Trust** support — a per-project trust level (persisted in the project state dir), a *Set Workspace Trust Level* command-palette control, and a `workspaceTrustLevel()` plugin API. The enforcement gate is **off by default this release** (no prompt on open; undecided folders are treated as Trusted) while the trust UX is reworked around sandboxed execution — so there is no behavior change unless you set a trust level yourself.
|
|
18
|
+
|
|
19
|
+
#### Orchestrator
|
|
20
|
+
|
|
21
|
+
* The **Open** dialog is now scoped to the current project by default, with a visible scope toggle to reach other projects, a tabular picker, and `/` to filter.
|
|
22
|
+
* New windows open atomically with their terminal (no `[No Name]` placeholder), and terminal output shows immediately.
|
|
23
|
+
|
|
24
|
+
#### Settings UI
|
|
25
|
+
|
|
26
|
+
A broad pass on the Settings editor: **number fields now accept direct typing** instead of `[-]`/`[+]` spinners; list editing gets inline `[+] Add new` / `[x]` rows, mouse support, and `Del` to remove; **`Ctrl+R` resets a field to its default**; the focused field's description is shown above the dialog buttons; and confirmations are required before deleting or discarding dirty edits.
|
|
27
|
+
|
|
28
|
+
#### Editor
|
|
29
|
+
|
|
30
|
+
* **Next / Previous Window** commands to cycle open windows without the Switch-Project picker (#2031).
|
|
31
|
+
* **Move File Explorer to Other Side** command, persisted to config (#1468, requested by @asukaminato0721).
|
|
32
|
+
* **Templ** (`.templ`) syntax highlighting (#463, requested by @TS22082).
|
|
33
|
+
* New `editor.confirm_quit` setting (default off) to prompt before quitting a clean session.
|
|
34
|
+
|
|
35
|
+
#### Plugins & API
|
|
36
|
+
|
|
37
|
+
* Built-in `editor.httpFetch(url, path)` for downloading files — `curl` is no longer bundled.
|
|
38
|
+
* "Install from URL" accepts direct `file://` URLs.
|
|
39
|
+
|
|
40
|
+
### Bug Fixes
|
|
41
|
+
|
|
42
|
+
* **Quit binding overrides honored** (#2030): binding `Ctrl+Q` to `noop`/`none` now actually disables it in every context.
|
|
43
|
+
* **Terminals**: line-number gutter and current-line highlight no longer leak on terminal exit; scroll-back viewport anchors correctly; `Shift+Tab` is forwarded to the child as `ESC[Z` (#2029).
|
|
44
|
+
* **File explorer keeps keyboard focus** when leaving a live terminal (#2029).
|
|
45
|
+
* **Closed terminals no longer reappear**, and buffer groups stay visible when a split closes (#2027, reported by @SolarLune).
|
|
46
|
+
* **Git Log**: selected commit stays aligned with the cursor; selection follows scrolling and row clicks.
|
|
47
|
+
* **Adjacent tab indicators** no longer double up (#1997, reported by @brunnerh).
|
|
48
|
+
* **Theming / Live Diff**: light-theme dim fixes, markdown popup body text inherits the terminal fg, and diff lines preserve syntax foreground.
|
|
49
|
+
* Tree-sitter `.scm` query files are included in the Nix source filter, fixing a source-build failure (#2055, reported by @melekbadreddine).
|
|
50
|
+
|
|
3
51
|
## 0.3.7
|
|
4
52
|
|
|
5
53
|
Introduces an experimental **multi-window orchestrator** command (`Alt+Q`) - a UI that manages multiple Fresh windows side by side in a single Fresh process.
|
package/package.json
CHANGED
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"cmd.export_json": "Review: Export to JSON",
|
|
39
39
|
"cmd.export_json_desc": "Export review to .review/session.json",
|
|
40
40
|
"status.refreshing": "Refreshing review diff...",
|
|
41
|
+
"status.refreshing_range": "Refreshing %{range}... (working tree not included)",
|
|
42
|
+
"status.working_tree_not_included": "working tree not included",
|
|
41
43
|
"status.updated": "Review diff updated. Found %{count} hunks.",
|
|
42
44
|
"status.loading_diff": "Loading side-by-side diff...",
|
|
43
45
|
"status.not_git_repo": "Not in a git repository",
|
|
@@ -120,6 +122,8 @@
|
|
|
120
122
|
"cmd.export_json": "Revize: Exportovat do JSON",
|
|
121
123
|
"cmd.export_json_desc": "Exportovat revizi do .review/session.json",
|
|
122
124
|
"status.refreshing": "Obnovuji rozdily revize...",
|
|
125
|
+
"status.refreshing_range": "Obnovuji %{range}... (pracovni strom neni zahrnut)",
|
|
126
|
+
"status.working_tree_not_included": "pracovni strom neni zahrnut",
|
|
123
127
|
"status.updated": "Rozdily revize aktualizovany. Nalezeno %{count} bloku.",
|
|
124
128
|
"status.loading_diff": "Nacitam rozdily vedle sebe...",
|
|
125
129
|
"status.not_git_repo": "Neni v git repozitari",
|
|
@@ -202,6 +206,8 @@
|
|
|
202
206
|
"cmd.export_json": "Review: Als JSON exportieren",
|
|
203
207
|
"cmd.export_json_desc": "Review nach .review/session.json exportieren",
|
|
204
208
|
"status.refreshing": "Unterschiede werden aktualisiert...",
|
|
209
|
+
"status.refreshing_range": "Aktualisiere %{range}... (Arbeitsbaum nicht enthalten)",
|
|
210
|
+
"status.working_tree_not_included": "Arbeitsbaum nicht enthalten",
|
|
205
211
|
"status.updated": "Unterschiede aktualisiert. %{count} Blocke gefunden.",
|
|
206
212
|
"status.loading_diff": "Nebeneinander-Ansicht wird geladen...",
|
|
207
213
|
"status.not_git_repo": "Kein Git-Repository",
|
|
@@ -284,6 +290,8 @@
|
|
|
284
290
|
"cmd.export_json": "Revision: Exportar a JSON",
|
|
285
291
|
"cmd.export_json_desc": "Exportar revision a .review/session.json",
|
|
286
292
|
"status.refreshing": "Actualizando diferencias de revision...",
|
|
293
|
+
"status.refreshing_range": "Actualizando %{range}... (arbol de trabajo no incluido)",
|
|
294
|
+
"status.working_tree_not_included": "arbol de trabajo no incluido",
|
|
287
295
|
"status.updated": "Diferencias actualizadas. Se encontraron %{count} bloques.",
|
|
288
296
|
"status.loading_diff": "Cargando diferencias lado a lado...",
|
|
289
297
|
"status.not_git_repo": "No esta en un repositorio git",
|
|
@@ -366,6 +374,8 @@
|
|
|
366
374
|
"cmd.export_json": "Revue: Exporter en JSON",
|
|
367
375
|
"cmd.export_json_desc": "Exporter la revue vers .review/session.json",
|
|
368
376
|
"status.refreshing": "Actualisation des differences de revue...",
|
|
377
|
+
"status.refreshing_range": "Actualisation de %{range}... (arbre de travail non inclus)",
|
|
378
|
+
"status.working_tree_not_included": "arbre de travail non inclus",
|
|
369
379
|
"status.updated": "Differences mises a jour. %{count} blocs trouves.",
|
|
370
380
|
"status.loading_diff": "Chargement des differences cote a cote...",
|
|
371
381
|
"status.not_git_repo": "Pas dans un depot git",
|
|
@@ -448,6 +458,8 @@
|
|
|
448
458
|
"cmd.export_json": "Revisione: Esporta in JSON",
|
|
449
459
|
"cmd.export_json_desc": "Esporta la revisione in .review/session.json",
|
|
450
460
|
"status.refreshing": "Aggiornamento differenze revisione...",
|
|
461
|
+
"status.refreshing_range": "Aggiornamento %{range}... (albero di lavoro non incluso)",
|
|
462
|
+
"status.working_tree_not_included": "albero di lavoro non incluso",
|
|
451
463
|
"status.updated": "Differenze revisione aggiornate. Trovati %{count} blocchi.",
|
|
452
464
|
"status.loading_diff": "Caricamento diff affiancato...",
|
|
453
465
|
"status.not_git_repo": "Non in un repository git",
|
|
@@ -530,6 +542,8 @@
|
|
|
530
542
|
"cmd.export_json": "レビュー: JSONにエクスポート",
|
|
531
543
|
"cmd.export_json_desc": "レビューを.review/session.jsonにエクスポート",
|
|
532
544
|
"status.refreshing": "レビュー差分を更新中...",
|
|
545
|
+
"status.refreshing_range": "%{range}を更新中... (ワーキングツリーは含まれません)",
|
|
546
|
+
"status.working_tree_not_included": "ワーキングツリーは含まれません",
|
|
533
547
|
"status.updated": "レビュー差分を更新しました。%{count}個のハンクが見つかりました。",
|
|
534
548
|
"status.loading_diff": "サイドバイサイド差分を読み込み中...",
|
|
535
549
|
"status.not_git_repo": "gitリポジトリではありません",
|
|
@@ -612,6 +626,8 @@
|
|
|
612
626
|
"cmd.export_json": "검토: JSON으로 내보내기",
|
|
613
627
|
"cmd.export_json_desc": "리뷰를 .review/session.json으로 내보내기",
|
|
614
628
|
"status.refreshing": "리뷰 차이점 새로고침 중...",
|
|
629
|
+
"status.refreshing_range": "%{range} 새로고침 중... (작업 트리 미포함)",
|
|
630
|
+
"status.working_tree_not_included": "작업 트리 미포함",
|
|
615
631
|
"status.updated": "리뷰 차이점이 업데이트되었습니다. %{count}개의 헝크를 찾았습니다.",
|
|
616
632
|
"status.loading_diff": "나란히 비교 로딩 중...",
|
|
617
633
|
"status.not_git_repo": "git 저장소가 아닙니다",
|
|
@@ -694,6 +710,8 @@
|
|
|
694
710
|
"cmd.export_json": "Revisao: Exportar para JSON",
|
|
695
711
|
"cmd.export_json_desc": "Exportar revisao para .review/session.json",
|
|
696
712
|
"status.refreshing": "Atualizando diferencas de revisao...",
|
|
713
|
+
"status.refreshing_range": "Atualizando %{range}... (arvore de trabalho nao incluida)",
|
|
714
|
+
"status.working_tree_not_included": "arvore de trabalho nao incluida",
|
|
697
715
|
"status.updated": "Diferencas de revisao atualizadas. Encontrados %{count} blocos.",
|
|
698
716
|
"status.loading_diff": "Carregando diferencas lado a lado...",
|
|
699
717
|
"status.not_git_repo": "Nao esta em um repositorio git",
|
|
@@ -776,6 +794,8 @@
|
|
|
776
794
|
"cmd.export_json": "Ревью: Экспорт в JSON",
|
|
777
795
|
"cmd.export_json_desc": "Экспортировать ревью в .review/session.json",
|
|
778
796
|
"status.refreshing": "Обновление изменений ревью...",
|
|
797
|
+
"status.refreshing_range": "Обновление %{range}... (рабочее дерево не включено)",
|
|
798
|
+
"status.working_tree_not_included": "рабочее дерево не включено",
|
|
779
799
|
"status.updated": "Изменения ревью обновлены. Найдено %{count} блоков.",
|
|
780
800
|
"status.loading_diff": "Загрузка сравнения бок о бок...",
|
|
781
801
|
"status.not_git_repo": "Не в git-репозитории",
|
|
@@ -858,6 +878,8 @@
|
|
|
858
878
|
"cmd.export_json": "ตรวจสอบ: ส่งออกเป็น JSON",
|
|
859
879
|
"cmd.export_json_desc": "ส่งออกการตรวจสอบไปยัง .review/session.json",
|
|
860
880
|
"status.refreshing": "กำลังรีเฟรชความแตกต่างการตรวจสอบ...",
|
|
881
|
+
"status.refreshing_range": "กำลังรีเฟรช %{range}... (ไม่รวม working tree)",
|
|
882
|
+
"status.working_tree_not_included": "ไม่รวม working tree",
|
|
861
883
|
"status.updated": "อัปเดตความแตกต่างการตรวจสอบแล้ว พบ %{count} บล็อก",
|
|
862
884
|
"status.loading_diff": "กำลังโหลดการเปรียบเทียบแบบเคียงข้าง...",
|
|
863
885
|
"status.not_git_repo": "ไม่อยู่ใน git repository",
|
|
@@ -940,6 +962,8 @@
|
|
|
940
962
|
"cmd.export_json": "Рев'ю: Експорт у JSON",
|
|
941
963
|
"cmd.export_json_desc": "Експортувати рев'ю до .review/session.json",
|
|
942
964
|
"status.refreshing": "Оновлення змін рев'ю...",
|
|
965
|
+
"status.refreshing_range": "Оновлення %{range}... (робоче дерево не включено)",
|
|
966
|
+
"status.working_tree_not_included": "робоче дерево не включено",
|
|
943
967
|
"status.updated": "Зміни рев'ю оновлено. Знайдено %{count} блоків.",
|
|
944
968
|
"status.loading_diff": "Завантаження порівняння поруч...",
|
|
945
969
|
"status.not_git_repo": "Не в git-репозиторії",
|
|
@@ -1022,6 +1046,8 @@
|
|
|
1022
1046
|
"cmd.export_json": "Xem xét: Xuất ra JSON",
|
|
1023
1047
|
"cmd.export_json_desc": "Xuất xem xét ra .review/session.json",
|
|
1024
1048
|
"status.refreshing": "Đang làm mới khác biệt xem xét...",
|
|
1049
|
+
"status.refreshing_range": "Đang làm mới %{range}... (không bao gồm cây làm việc)",
|
|
1050
|
+
"status.working_tree_not_included": "không bao gồm cây làm việc",
|
|
1025
1051
|
"status.updated": "Đã cập nhật khác biệt xem xét. Tìm thấy %{count} khối.",
|
|
1026
1052
|
"status.loading_diff": "Đang tải so sánh song song...",
|
|
1027
1053
|
"status.not_git_repo": "Không trong kho git",
|
|
@@ -1104,6 +1130,8 @@
|
|
|
1104
1130
|
"cmd.export_json": "审查: 导出为JSON",
|
|
1105
1131
|
"cmd.export_json_desc": "将审查导出到.review/session.json",
|
|
1106
1132
|
"status.refreshing": "正在刷新审查差异...",
|
|
1133
|
+
"status.refreshing_range": "正在刷新 %{range}... (不包含工作树)",
|
|
1134
|
+
"status.working_tree_not_included": "不包含工作树",
|
|
1107
1135
|
"status.updated": "审查差异已更新。找到%{count}个代码块。",
|
|
1108
1136
|
"status.loading_diff": "正在加载并排差异...",
|
|
1109
1137
|
"status.not_git_repo": "不在git仓库中",
|
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
|
|
|
@@ -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,
|
|
@@ -655,6 +656,12 @@
|
|
|
655
656
|
"default": true,
|
|
656
657
|
"x-section": "Recovery"
|
|
657
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
|
+
},
|
|
658
665
|
"restore_previous_session": {
|
|
659
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",
|
|
660
667
|
"type": "boolean",
|
|
@@ -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/git_log.ts
CHANGED
|
@@ -5,7 +5,7 @@ import {
|
|
|
5
5
|
buildCommitLogEntries,
|
|
6
6
|
fetchGitLog,
|
|
7
7
|
} from "./lib/git_history.ts";
|
|
8
|
-
import { button, flexSpacer,
|
|
8
|
+
import { button, flexSpacer, list, row, WidgetPanel } from "./lib/index.ts";
|
|
9
9
|
|
|
10
10
|
const editor = getEditor();
|
|
11
11
|
|
|
@@ -105,20 +105,23 @@ const SELECT_DEBOUNCE_MS = 60;
|
|
|
105
105
|
// the log, and opens the file at the cursor when pressed in the detail).
|
|
106
106
|
// =============================================================================
|
|
107
107
|
|
|
108
|
-
// j/k/Up/Down/PageUp/PageDown
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
108
|
+
// The log pane is cursor-driven: j/k/Up/Down/PageUp/PageDown move the
|
|
109
|
+
// pane's real buffer cursor (normal editor movement), which scrolls via
|
|
110
|
+
// the standard `ensure_cursor_visible` wheel — only when the cursor
|
|
111
|
+
// crosses the top/bottom edge. The cursor is the source of truth for
|
|
112
|
+
// which commit is selected; a `cursor_moved` subscription mirrors its
|
|
113
|
+
// line into the List highlight + detail pane. On the detail pane the
|
|
114
|
+
// same keys scroll the diff. Other actions (q/r/y/Tab/Return) are direct
|
|
115
|
+
// bindings — they don't depend on the cursor row.
|
|
113
116
|
editor.defineMode(
|
|
114
117
|
"git-log",
|
|
115
118
|
[
|
|
116
|
-
["k", "
|
|
117
|
-
["j", "
|
|
118
|
-
["Up", "
|
|
119
|
-
["Down", "
|
|
120
|
-
["PageUp", "
|
|
121
|
-
["PageDown", "
|
|
119
|
+
["k", "move_up"],
|
|
120
|
+
["j", "move_down"],
|
|
121
|
+
["Up", "move_up"],
|
|
122
|
+
["Down", "move_down"],
|
|
123
|
+
["PageUp", "move_page_up"],
|
|
124
|
+
["PageDown", "move_page_down"],
|
|
122
125
|
["Return", "git_log_enter"],
|
|
123
126
|
["Tab", "git_log_tab"],
|
|
124
127
|
["q", "git_log_q"],
|
|
@@ -130,52 +133,6 @@ editor.defineMode(
|
|
|
130
133
|
true, // inherit Normal-context bindings for unbound keys
|
|
131
134
|
);
|
|
132
135
|
|
|
133
|
-
function git_log_select_up(): void {
|
|
134
|
-
if (isLogPanelActive()) {
|
|
135
|
-
state.logPanel?.command(key("Up"));
|
|
136
|
-
} else {
|
|
137
|
-
editor.executeAction("move_up");
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
function git_log_select_down(): void {
|
|
141
|
-
if (isLogPanelActive()) {
|
|
142
|
-
state.logPanel?.command(key("Down"));
|
|
143
|
-
} else {
|
|
144
|
-
editor.executeAction("move_down");
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
function git_log_select_page_up(): void {
|
|
148
|
-
if (isLogPanelActive()) {
|
|
149
|
-
state.logPanel?.command(key("PageUp"));
|
|
150
|
-
} else {
|
|
151
|
-
editor.executeAction("move_page_up");
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
function git_log_select_page_down(): void {
|
|
155
|
-
if (isLogPanelActive()) {
|
|
156
|
-
state.logPanel?.command(key("PageDown"));
|
|
157
|
-
} else {
|
|
158
|
-
editor.executeAction("move_page_down");
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/** True iff the log panel is the focused buffer in the group. The
|
|
163
|
-
* group's bindings (j/k/Up/Down/PageUp/PageDown) apply to all panels
|
|
164
|
-
* uniformly; we only want navigation to drive the List widget when
|
|
165
|
-
* the user is *on* the log panel. From the detail panel, the same
|
|
166
|
-
* keys must move the buffer cursor (so users can scroll the diff
|
|
167
|
-
* before pressing Enter on a diff line to open the file view). */
|
|
168
|
-
function isLogPanelActive(): boolean {
|
|
169
|
-
return (
|
|
170
|
-
state.logBufferId !== null &&
|
|
171
|
-
editor.getActiveBufferId() === state.logBufferId
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
registerHandler("git_log_select_up", git_log_select_up);
|
|
175
|
-
registerHandler("git_log_select_down", git_log_select_down);
|
|
176
|
-
registerHandler("git_log_select_page_up", git_log_select_page_up);
|
|
177
|
-
registerHandler("git_log_select_page_down", git_log_select_page_down);
|
|
178
|
-
|
|
179
136
|
// =============================================================================
|
|
180
137
|
// Panel layout
|
|
181
138
|
// =============================================================================
|
|
@@ -268,19 +225,13 @@ editor.on("widget_event", (data) => {
|
|
|
268
225
|
}
|
|
269
226
|
return;
|
|
270
227
|
}
|
|
271
|
-
// Log pane (List of commit rows)
|
|
272
|
-
//
|
|
273
|
-
//
|
|
228
|
+
// Log pane (List of commit rows). Selection is cursor-driven (see the
|
|
229
|
+
// `cursor_moved` handler), so the List's `select` event is ignored —
|
|
230
|
+
// a row click places the buffer cursor, and `cursor_moved` mirrors it
|
|
231
|
+
// into the selection. `activate` (Enter / double-click) still opens.
|
|
274
232
|
if (state.logPanel !== null && data.panel_id === state.logPanel.id()) {
|
|
275
|
-
if (data.event_type === "select") {
|
|
276
|
-
const idx =
|
|
277
|
-
typeof data.payload?.index === "number" ? data.payload.index : -1;
|
|
278
|
-
if (idx >= 0) void on_log_select(idx);
|
|
279
|
-
return;
|
|
280
|
-
}
|
|
281
233
|
if (data.event_type === "activate") {
|
|
282
234
|
void git_log_enter();
|
|
283
|
-
return;
|
|
284
235
|
}
|
|
285
236
|
return;
|
|
286
237
|
}
|
|
@@ -300,6 +251,11 @@ function detailFooter(hash: string): string {
|
|
|
300
251
|
return editor.t("status.commit_ready", { hash });
|
|
301
252
|
}
|
|
302
253
|
|
|
254
|
+
/** Stable widget key for the log List. The host keys selection +
|
|
255
|
+
* scroll instance state off this; the plugin re-pins selection
|
|
256
|
+
* through it after click/keyboard `select` events. */
|
|
257
|
+
const LOG_LIST_KEY = "git-log-list";
|
|
258
|
+
|
|
303
259
|
function renderLog(): void {
|
|
304
260
|
if (state.logPanel === null) return;
|
|
305
261
|
// List takes the per-row entries directly. selectedIndex: -1 on the
|
|
@@ -321,7 +277,7 @@ function renderLog(): void {
|
|
|
321
277
|
// scroll handle viewport. Revisit if commit lists grow into the
|
|
322
278
|
// tens of thousands.
|
|
323
279
|
visibleRows: Math.max(1, state.commits.length),
|
|
324
|
-
key:
|
|
280
|
+
key: LOG_LIST_KEY,
|
|
325
281
|
}),
|
|
326
282
|
);
|
|
327
283
|
}
|
|
@@ -652,13 +608,20 @@ async function show_git_log(): Promise<void> {
|
|
|
652
608
|
|
|
653
609
|
renderToolbar();
|
|
654
610
|
renderLog();
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
611
|
+
// Cursor-driven selection: give the log pane a real, visible cursor and
|
|
612
|
+
// take ownership of it (`setBufferShowCursors` locks it so the widget
|
|
613
|
+
// runtime won't clear it on repaint). The cursor's line is the selected
|
|
614
|
+
// commit; `cursor_moved` mirrors it into the List highlight + detail.
|
|
615
|
+
// Start on HEAD (line 0). Scrolling is the normal cursor-follow wheel.
|
|
616
|
+
if (state.logBufferId !== null) {
|
|
617
|
+
editor.setBufferShowCursors(state.logBufferId, true);
|
|
618
|
+
editor.setBufferCursor(state.logBufferId, 0);
|
|
619
|
+
}
|
|
658
620
|
await refreshDetail();
|
|
659
621
|
|
|
660
622
|
editor.on("resize", on_git_log_resize);
|
|
661
623
|
editor.on("buffer_closed", on_git_log_buffer_closed);
|
|
624
|
+
editor.on("cursor_moved", on_git_log_cursor_moved);
|
|
662
625
|
|
|
663
626
|
editor.setStatus(
|
|
664
627
|
editor.t("status.log_ready", { count: String(state.commits.length) })
|
|
@@ -673,6 +636,7 @@ function git_log_cleanup(): void {
|
|
|
673
636
|
if (!state.isOpen) return;
|
|
674
637
|
editor.off("resize", on_git_log_resize);
|
|
675
638
|
editor.off("buffer_closed", on_git_log_buffer_closed);
|
|
639
|
+
editor.off("cursor_moved", on_git_log_cursor_moved);
|
|
676
640
|
// Kill any still-running `git show` spawns — we no longer care.
|
|
677
641
|
for (const [, handle] of state.inFlightSpawns) {
|
|
678
642
|
handle.kill?.();
|
|
@@ -1114,15 +1078,34 @@ function git_log_file_view_close(): void {
|
|
|
1114
1078
|
registerHandler("git_log_file_view_close", git_log_file_view_close);
|
|
1115
1079
|
|
|
1116
1080
|
// =============================================================================
|
|
1117
|
-
// Selection tracking —
|
|
1118
|
-
//
|
|
1081
|
+
// Selection tracking — the log pane is cursor-driven. The buffer cursor's
|
|
1082
|
+
// line (set by arrow-key movement or a click) is the selected commit; this
|
|
1083
|
+
// `cursor_moved` subscription mirrors it into the List highlight and the
|
|
1084
|
+
// detail pane. Scrolling is handled by the normal cursor-follow wheel, so
|
|
1085
|
+
// the viewport only moves when the cursor crosses the top/bottom edge.
|
|
1119
1086
|
// =============================================================================
|
|
1120
1087
|
|
|
1121
|
-
|
|
1088
|
+
function on_git_log_cursor_moved(data: { buffer_id: number; line: number }): void {
|
|
1089
|
+
if (!state.isOpen || state.logBufferId === null) return;
|
|
1090
|
+
if (data.buffer_id !== state.logBufferId) return;
|
|
1091
|
+
// `cursor_moved.line` is 1-based; commit rows are 0-based (no header),
|
|
1092
|
+
// so the selected commit index is `line - 1`.
|
|
1093
|
+
const idx = data.line - 1;
|
|
1094
|
+
if (idx < 0 || idx >= state.commits.length) return;
|
|
1095
|
+
void selectCommitLine(idx);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
async function selectCommitLine(idx: number): Promise<void> {
|
|
1122
1099
|
if (!state.isOpen) return;
|
|
1123
1100
|
if (idx === state.selectedIndex) return;
|
|
1124
1101
|
state.selectedIndex = idx;
|
|
1125
1102
|
|
|
1103
|
+
// Move the List's highlight bar to the cursor's row. The cursor itself
|
|
1104
|
+
// is the real (plugin-owned) buffer cursor, so it stays exactly where
|
|
1105
|
+
// the user moved or clicked it — this only repaints the row styling,
|
|
1106
|
+
// and the repaint preserves the cursor position.
|
|
1107
|
+
state.logPanel?.setSelectedIndex(LOG_LIST_KEY, idx);
|
|
1108
|
+
|
|
1126
1109
|
const commit = state.commits[state.selectedIndex];
|
|
1127
1110
|
if (commit) {
|
|
1128
1111
|
editor.setStatus(
|