@fresh-editor/fresh-editor 0.3.6 → 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 CHANGED
@@ -1,5 +1,96 @@
1
1
  # Release Notes
2
2
 
3
+ ## 0.3.7
4
+
5
+ Introduces an experimental **multi-window orchestrator** command (`Alt+Q`) - a UI that manages multiple Fresh windows side by side in a single Fresh process.
6
+
7
+ ### Features / Improvements
8
+
9
+ #### Orchestrator (early experiment)
10
+
11
+ The new orchestrator command (`Alt+Q`) lets one Fresh process juggle several independent windows — switch between sessions, open new ones from a project path / branch, or visit one in a separate Fresh process. This is an early experiment; expect rough edges. Please share your ideas and bug reports!
12
+
13
+ #### Search and Replace
14
+
15
+ Made an effort to improve the search & replace command, tackling several issues:
16
+
17
+ * **All Files** scope toggle + dedicated current-file command.
18
+ * Editor shortcuts (`Ctrl+P` etc.) pass through the panel.
19
+ * Proper empty / searching / no-results states.
20
+ * Large result sets stay responsive — incremental tree, throttled streaming, capped per-iteration work.
21
+
22
+ #### Git Log / Review Diff
23
+
24
+ * **Performance**: Per-commit diffs **stream into file-backed buffers** with full syntax highlighting, per-line add/delete backgrounds, working scrollbar, and `Fold by File` / `Fold by Hunk` commands. Now opens the famous "rewrite bun in rust" commit instantly!
25
+ * **Open file at commit** (#1962): jump from a diff line in the review-branch detail panel to the file at that commit (read-only, `q` to go back).
26
+
27
+ #### File Explorer
28
+
29
+ * **Compact directories** (#1406, thanks @Nandaleio!): a directory containing a single child directory renders inline as `com.example.name`, VSCode/IntelliJ-style. On by default; toggle with `file_explorer.compact_directories`.
30
+ * **Configurable tree indicators** (#1940, reported by @MicTheSquid): expand/collapse glyphs themable via *File Explorer → Tree Indicator Expanded / Collapsed* in the Settings UI.
31
+
32
+ #### Plugins & API
33
+
34
+ * **`git_statusbar` plugin + status-bar token registration API** (thanks @PavelLoparev!): show the current git branch in the status bar; plugins register their own tokens via `editor.registerStatusBarElement`. Add via *Settings → Editor → Status Bar → Left / Right*.
35
+ * **`tab_actions` plugin** (#1844, thanks @PavelLoparev!): close all / to-the-left / to-the-right and move tabs within the current split.
36
+ * **Plugins can register their own config items**: a new typed registration API lets plugins declare settings that appear in the Settings UI alongside built-in ones (sorted at the bottom of the category list), so users can toggle plugin behaviour from the same place as everything else. The bundled `vi_mode` plugin uses it to expose an **autoStart** option.
37
+
38
+ #### Editing & Widgets
39
+
40
+ * **Add Cursors to Line Ends** (#1870, reported by @aquasync): places a cursor at the end of every line in the selection.
41
+ * Widget text inputs gain real selection rendering, `Shift+arrow` / `Ctrl+Shift+arrow` extension, and `Ctrl+C/V/X/A` — used by Search & Replace and plugin dialogs.
42
+ * Floating completion popup on widget `Text` inputs.
43
+
44
+ #### Live Diff
45
+
46
+ * Word-level underline highlights on deletion lines; similarity threshold and size caps are runtime-tunable, so large diffs render as modified lines rather than paired add/remove blocks.
47
+
48
+ ### Bug Fixes
49
+
50
+ * **`init.ts` `setAutoOpen(false)` ignored**: dashboard auto-open is now driven by the `ready` hook so user `init.ts` runs first.
51
+
52
+ #### Git Log / Review Diff
53
+
54
+ * **Detail panel `PageUp` / `PageDown`** (action name was wrong); scrollbar now functional on the streamed buffer.
55
+
56
+ #### File Explorer
57
+
58
+ * **UI / navigation shortcuts** (#1903): `Ctrl+P` and friends are no longer swallowed when the file explorer has focus.
59
+ * **Switch Project double-click** (#1931, reported by @SolarLune): double-clicking a directory navigates into it instead of dismissing the dialog.
60
+
61
+ #### Plugins
62
+
63
+ * **Plugin init scaffold** (#1986, @PavelLoparev): new plugins now generate `fresh.entry` (the current entry-point name) instead of stale `fresh.main`.
64
+ * **Idle CPU from git plugins** (#2009, #2012): `git_statusbar` and `merge_conflict` no longer spawn idle subprocesses; `git_statusbar` is driven off `watchPath(.git/HEAD)`.
65
+
66
+ #### Editing
67
+
68
+ * **Smart Home with multiple cursors**: now runs for every cursor, not only the primary.
69
+ * **Soft wrap at trailing space** (#1363, reported by @dragonfyre13): when a trailing space would overflow the wrap column, wrap backs up one word instead of breaking mid-token.
70
+ * **Horizontal scroll-to-view on long lines** (#1873): the viewport now follows the cursor on long lines.
71
+
72
+ #### Live Diff
73
+
74
+ * **Wrapped overlay backgrounds** now extend across every visual row of a wrapped line.
75
+
76
+ #### Rendering
77
+
78
+ * **Highlighter cache preserved across bulk edits** (#1958): large pastes and replace-all no longer trigger a whole-file reparse.
79
+ * **Phantom cursor offscreen** (#1965): a buffer cursor scrolled past the viewport no longer leaves a stray block at the screen edge.
80
+ * **Split separator background** (#1963): separator cells now paint with `editor_bg` so split borders blend with the panes.
81
+ * **Next / Prev Split un-maximizes** (#1961): navigating to another split un-maximizes first.
82
+ * **Stable cursor-position indicator** (#1967): `Ln L, Col C` no longer jitters as the column count grows.
83
+ * **PTY terminal background** now fills the render rect with the editor bg.
84
+ * **Nostalgia theme** (#1890, reported by @NGRIT41): terminal-pane blue now covers full pane.
85
+ * **Column rulers** suppressed on virtual buffers (dashboard, PTY panes, etc.).
86
+
87
+ #### Misc
88
+
89
+ * **No more stray `.fresh/` directory in your working dir** (#1991): orchestrator/cross-restart state now lives under `data_dir` instead of being dropped next to your project files.
90
+ * **Buffer "library path" detection** (#1970, reported by @FF-AntiK): only `.cargo/registry` and `.cargo/git` are treated as Cargo library paths — local `.cargo/config.toml` etc. are no longer flagged.
91
+ * **Julia grammar** (#1852, reported by @goszlanyi): the adjoint operator (`'`) no longer flips the rest of the line into string mode.
92
+ * **`move_word_end` / `select_word_end` labels** (#1878, reported by @sour-dani): translated in all locales.
93
+
3
94
  ## 0.3.6
4
95
 
5
96
  This version includes a major internal refactoring to support multiple windows in a single Fresh process. The work will be used to add a multi-window orchestrator in a future version.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fresh-editor/fresh-editor",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "A modern terminal-based text editor with plugin support",
5
5
  "repository": {
6
6
  "type": "git",
@@ -21,6 +21,12 @@
21
21
  "status.review_branch_empty": "No commits in %{base}..HEAD — nothing to review.",
22
22
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
23
23
  "panel.review_branch_footer": "j/k: navigate | Enter: focus detail | r: refresh | q: close",
24
+ "status.move_to_diff": "Move cursor to a diff line",
25
+ "status.move_to_diff_with_context": "Move cursor to a diff line with file context",
26
+ "status.file_loading": "Loading %{file} at %{hash}...",
27
+ "status.file_not_found": "File %{file} not found at commit %{hash}",
28
+ "status.file_view_ready": "%{file} @ %{hash} (read-only) | Target: line %{line} | q: back",
29
+ "status.failed_open_file": "Failed to open %{file}",
24
30
  "cmd.side_by_side_diff": "Side-by-Side Diff",
25
31
  "cmd.side_by_side_diff_desc": "Show side-by-side diff for current file",
26
32
  "cmd.add_comment": "Review: Add Comment",
@@ -97,6 +103,12 @@
97
103
  "status.review_branch_empty": "Žádné commity v %{base}..HEAD — není co revidovat.",
98
104
  "panel.review_branch_header": "Commity (%{base}..HEAD)",
99
105
  "panel.review_branch_footer": "j/k: navigace | Enter: detail | r: obnovit | q: zavřít",
106
+ "status.move_to_diff": "Presunte kurzor na radek diffu",
107
+ "status.move_to_diff_with_context": "Presunte kurzor na radek diffu s kontextem souboru",
108
+ "status.file_loading": "Nacitam %{file} v %{hash}...",
109
+ "status.file_not_found": "Soubor %{file} nenalezen v commitu %{hash}",
110
+ "status.file_view_ready": "%{file} @ %{hash} (pouze pro cteni) | Cil: radek %{line} | q: zpet",
111
+ "status.failed_open_file": "Nepodarilo se otevrit %{file}",
100
112
  "cmd.side_by_side_diff": "Rozdily vedle sebe",
101
113
  "cmd.side_by_side_diff_desc": "Zobrazit rozdily aktualniho souboru vedle sebe",
102
114
  "cmd.add_comment": "Revize: Pridat komentar",
@@ -173,6 +185,12 @@
173
185
  "status.review_branch_empty": "Keine Commits in %{base}..HEAD — nichts zu überprüfen.",
174
186
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
175
187
  "panel.review_branch_footer": "j/k: Navigation | Enter: Detail | r: Aktualisieren | q: Schließen",
188
+ "status.move_to_diff": "Cursor auf eine Diff-Zeile bewegen",
189
+ "status.move_to_diff_with_context": "Cursor auf eine Diff-Zeile mit Dateikontext bewegen",
190
+ "status.file_loading": "Lade %{file} bei %{hash}...",
191
+ "status.file_not_found": "Datei %{file} nicht gefunden bei Commit %{hash}",
192
+ "status.file_view_ready": "%{file} @ %{hash} (schreibgeschuetzt) | Ziel: Zeile %{line} | q: zurueck",
193
+ "status.failed_open_file": "%{file} konnte nicht geoeffnet werden",
176
194
  "cmd.side_by_side_diff": "Nebeneinander-Ansicht",
177
195
  "cmd.side_by_side_diff_desc": "Unterschiede der aktuellen Datei nebeneinander anzeigen",
178
196
  "cmd.add_comment": "Review: Kommentar hinzufugen",
@@ -249,6 +267,12 @@
249
267
  "status.review_branch_empty": "No hay commits en %{base}..HEAD — nada que revisar.",
250
268
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
251
269
  "panel.review_branch_footer": "j/k: navegar | Enter: detalle | r: actualizar | q: cerrar",
270
+ "status.move_to_diff": "Mueve el cursor a una linea de diff",
271
+ "status.move_to_diff_with_context": "Mueve el cursor a una linea de diff con contexto de archivo",
272
+ "status.file_loading": "Cargando %{file} en %{hash}...",
273
+ "status.file_not_found": "Archivo %{file} no encontrado en commit %{hash}",
274
+ "status.file_view_ready": "%{file} @ %{hash} (solo lectura) | Objetivo: linea %{line} | q: volver",
275
+ "status.failed_open_file": "Error al abrir %{file}",
252
276
  "cmd.side_by_side_diff": "Diferencias Lado a Lado",
253
277
  "cmd.side_by_side_diff_desc": "Mostrar diferencias lado a lado del archivo actual",
254
278
  "cmd.add_comment": "Revision: Agregar Comentario",
@@ -325,6 +349,12 @@
325
349
  "status.review_branch_empty": "Aucun commit dans %{base}..HEAD — rien à revoir.",
326
350
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
327
351
  "panel.review_branch_footer": "j/k : naviguer | Entrée : détail | r : actualiser | q : fermer",
352
+ "status.move_to_diff": "Deplacez le curseur sur une ligne de diff",
353
+ "status.move_to_diff_with_context": "Deplacez le curseur sur une ligne de diff avec contexte de fichier",
354
+ "status.file_loading": "Chargement de %{file} a %{hash}...",
355
+ "status.file_not_found": "Fichier %{file} non trouve au commit %{hash}",
356
+ "status.file_view_ready": "%{file} @ %{hash} (lecture seule) | Cible: ligne %{line} | q: retour",
357
+ "status.failed_open_file": "Echec de l'ouverture de %{file}",
328
358
  "cmd.side_by_side_diff": "Differences Cote a Cote",
329
359
  "cmd.side_by_side_diff_desc": "Afficher les differences du fichier actuel cote a cote",
330
360
  "cmd.add_comment": "Revue: Ajouter un Commentaire",
@@ -401,6 +431,12 @@
401
431
  "status.review_branch_empty": "Nessun commit in %{base}..HEAD — niente da revisionare.",
402
432
  "panel.review_branch_header": "Commit (%{base}..HEAD)",
403
433
  "panel.review_branch_footer": "j/k: naviga | Invio: dettaglio | r: aggiorna | q: chiudi",
434
+ "status.move_to_diff": "Sposta il cursore su una riga di diff",
435
+ "status.move_to_diff_with_context": "Sposta il cursore su una riga di diff con contesto file",
436
+ "status.file_loading": "Caricamento %{file} a %{hash}...",
437
+ "status.file_not_found": "File %{file} non trovato al commit %{hash}",
438
+ "status.file_view_ready": "%{file} @ %{hash} (sola lettura) | Destinazione: riga %{line} | q: indietro",
439
+ "status.failed_open_file": "Apertura di %{file} fallita",
404
440
  "cmd.side_by_side_diff": "Diff affiancato",
405
441
  "cmd.side_by_side_diff_desc": "Mostra diff affiancato per il file corrente",
406
442
  "cmd.add_comment": "Revisione: Aggiungi commento",
@@ -477,6 +513,12 @@
477
513
  "status.review_branch_empty": "%{base}..HEAD にコミットはありません — レビューする対象がありません。",
478
514
  "panel.review_branch_header": "コミット (%{base}..HEAD)",
479
515
  "panel.review_branch_footer": "j/k: 移動 | Enter: 詳細 | r: 更新 | q: 閉じる",
516
+ "status.move_to_diff": "カーソルを差分行に移動してください",
517
+ "status.move_to_diff_with_context": "ファイルコンテキストのある差分行にカーソルを移動してください",
518
+ "status.file_loading": "%{file} を %{hash} で読み込み中...",
519
+ "status.file_not_found": "コミット %{hash} でファイル %{file} が見つかりません",
520
+ "status.file_view_ready": "%{file} @ %{hash} (読み取り専用) | 対象: %{line}行目 | q: 戻る",
521
+ "status.failed_open_file": "%{file} を開けませんでした",
480
522
  "cmd.side_by_side_diff": "サイドバイサイド差分",
481
523
  "cmd.side_by_side_diff_desc": "現在のファイルの差分を横並びで表示",
482
524
  "cmd.add_comment": "レビュー: コメント追加",
@@ -553,6 +595,12 @@
553
595
  "status.review_branch_empty": "%{base}..HEAD에 커밋이 없습니다 — 리뷰할 내용이 없습니다.",
554
596
  "panel.review_branch_header": "커밋 (%{base}..HEAD)",
555
597
  "panel.review_branch_footer": "j/k: 이동 | Enter: 상세 | r: 새로 고침 | q: 닫기",
598
+ "status.move_to_diff": "커서를 diff 줄로 이동하세요",
599
+ "status.move_to_diff_with_context": "파일 컨텍스트가 있는 diff 줄로 커서를 이동하세요",
600
+ "status.file_loading": "%{hash}에서 %{file} 로딩 중...",
601
+ "status.file_not_found": "커밋 %{hash}에서 파일 %{file}을 찾을 수 없습니다",
602
+ "status.file_view_ready": "%{file} @ %{hash} (읽기 전용) | 대상: %{line}행 | q: 뒤로",
603
+ "status.failed_open_file": "%{file}을 열지 못했습니다",
556
604
  "cmd.side_by_side_diff": "나란히 비교",
557
605
  "cmd.side_by_side_diff_desc": "현재 파일의 차이점을 나란히 표시",
558
606
  "cmd.add_comment": "검토: 코멘트 추가",
@@ -629,6 +677,12 @@
629
677
  "status.review_branch_empty": "Sem commits em %{base}..HEAD — nada para revisar.",
630
678
  "panel.review_branch_header": "Commits (%{base}..HEAD)",
631
679
  "panel.review_branch_footer": "j/k: navegar | Enter: detalhe | r: atualizar | q: fechar",
680
+ "status.move_to_diff": "Mova o cursor para uma linha de diff",
681
+ "status.move_to_diff_with_context": "Mova o cursor para uma linha de diff com contexto de arquivo",
682
+ "status.file_loading": "Carregando %{file} em %{hash}...",
683
+ "status.file_not_found": "Arquivo %{file} nao encontrado no commit %{hash}",
684
+ "status.file_view_ready": "%{file} @ %{hash} (somente leitura) | Destino: linha %{line} | q: voltar",
685
+ "status.failed_open_file": "Falha ao abrir %{file}",
632
686
  "cmd.side_by_side_diff": "Diferencas Lado a Lado",
633
687
  "cmd.side_by_side_diff_desc": "Mostrar diferencas do arquivo atual lado a lado",
634
688
  "cmd.add_comment": "Revisao: Adicionar Comentario",
@@ -705,6 +759,12 @@
705
759
  "status.review_branch_empty": "Нет коммитов в %{base}..HEAD — нечего просматривать.",
706
760
  "panel.review_branch_header": "Коммиты (%{base}..HEAD)",
707
761
  "panel.review_branch_footer": "j/k: навигация | Enter: детали | r: обновить | q: закрыть",
762
+ "status.move_to_diff": "Peremesstite kursor na stroku diff",
763
+ "status.move_to_diff_with_context": "Peremesstite kursor na stroku diff s kontekstom fayla",
764
+ "status.file_loading": "Zagruzka %{file} v %{hash}...",
765
+ "status.file_not_found": "Fayl %{file} ne nayden v kommite %{hash}",
766
+ "status.file_view_ready": "%{file} @ %{hash} (tol'ko dlya chteniya) | Tsel': stroka %{line} | q: nazad",
767
+ "status.failed_open_file": "Ne udalos' otkryt' %{file}",
708
768
  "cmd.side_by_side_diff": "Сравнение бок о бок",
709
769
  "cmd.side_by_side_diff_desc": "Показать изменения текущего файла бок о бок",
710
770
  "cmd.add_comment": "Ревью: Добавить комментарий",
@@ -781,6 +841,12 @@
781
841
  "status.review_branch_empty": "ไม่มีคอมมิตใน %{base}..HEAD — ไม่มีอะไรต้องตรวจสอบ",
782
842
  "panel.review_branch_header": "คอมมิต (%{base}..HEAD)",
783
843
  "panel.review_branch_footer": "j/k: นำทาง | Enter: รายละเอียด | r: รีเฟรช | q: ปิด",
844
+ "status.move_to_diff": "เลื่อนเคอร์เซอร์ไปที่บรรทัด diff",
845
+ "status.move_to_diff_with_context": "เลื่อนเคอร์เซอร์ไปที่บรรทัด diff ที่มีบริบทไฟล์",
846
+ "status.file_loading": "กำลังโหลด %{file} ที่ %{hash}...",
847
+ "status.file_not_found": "ไม่พบไฟล์ %{file} ในคอมมิต %{hash}",
848
+ "status.file_view_ready": "%{file} @ %{hash} (อ่านอย่างเดียว) | เป้าหมาย: บรรทัด %{line} | q: กลับ",
849
+ "status.failed_open_file": "ไม่สามารถเปิด %{file} ได้",
784
850
  "cmd.side_by_side_diff": "เปรียบเทียบแบบเคียงข้าง",
785
851
  "cmd.side_by_side_diff_desc": "แสดงความแตกต่างของไฟล์ปัจจุบันแบบเคียงข้าง",
786
852
  "cmd.add_comment": "ตรวจสอบ: เพิ่มความคิดเห็น",
@@ -857,6 +923,12 @@
857
923
  "status.review_branch_empty": "Немає комітів у %{base}..HEAD — нічого переглядати.",
858
924
  "panel.review_branch_header": "Коміти (%{base}..HEAD)",
859
925
  "panel.review_branch_footer": "j/k: навігація | Enter: деталі | r: оновити | q: закрити",
926
+ "status.move_to_diff": "Peremistit' kursor na ryadok diff",
927
+ "status.move_to_diff_with_context": "Peremistit' kursor na ryadok diff z kontekstom faylu",
928
+ "status.file_loading": "Zavantazhennya %{file} v %{hash}...",
929
+ "status.file_not_found": "Fayl %{file} ne znaydeno v komiti %{hash}",
930
+ "status.file_view_ready": "%{file} @ %{hash} (til'ky dlya chytannya) | Tsil': ryadok %{line} | q: nazad",
931
+ "status.failed_open_file": "Ne vdalosya vidkryty %{file}",
860
932
  "cmd.side_by_side_diff": "Порівняння поруч",
861
933
  "cmd.side_by_side_diff_desc": "Показати зміни поточного файлу поруч",
862
934
  "cmd.add_comment": "Рев'ю: Додати коментар",
@@ -933,6 +1005,12 @@
933
1005
  "status.review_branch_empty": "Không có commit nào trong %{base}..HEAD — không có gì để xem xét.",
934
1006
  "panel.review_branch_header": "Commit (%{base}..HEAD)",
935
1007
  "panel.review_branch_footer": "j/k: điều hướng | Enter: chi tiết | r: làm mới | q: đóng",
1008
+ "status.move_to_diff": "Di chuyển con trỏ đến dòng diff",
1009
+ "status.move_to_diff_with_context": "Di chuyển con trỏ đến dòng diff có ngữ cảnh tệp",
1010
+ "status.file_loading": "Đang tải %{file} tại %{hash}...",
1011
+ "status.file_not_found": "Không tìm thấy tệp %{file} tại commit %{hash}",
1012
+ "status.file_view_ready": "%{file} @ %{hash} (chỉ đọc) | Đích: dòng %{line} | q: quay lại",
1013
+ "status.failed_open_file": "Không thể mở %{file}",
936
1014
  "cmd.side_by_side_diff": "So sánh song song",
937
1015
  "cmd.side_by_side_diff_desc": "Hiển thị khác biệt song song cho tệp hiện tại",
938
1016
  "cmd.add_comment": "Xem xét: Thêm nhận xét",
@@ -1009,6 +1087,12 @@
1009
1087
  "status.review_branch_empty": "%{base}..HEAD 中没有提交 — 无需审查。",
1010
1088
  "panel.review_branch_header": "提交 (%{base}..HEAD)",
1011
1089
  "panel.review_branch_footer": "j/k:导航 | Enter:详情 | r:刷新 | q:关闭",
1090
+ "status.move_to_diff": "请将光标移动到差异行",
1091
+ "status.move_to_diff_with_context": "请将光标移动到带有文件上下文的差异行",
1092
+ "status.file_loading": "正在加载 %{file} 于 %{hash}...",
1093
+ "status.file_not_found": "在提交 %{hash} 中未找到文件 %{file}",
1094
+ "status.file_view_ready": "%{file} @ %{hash} (只读) | 目标: 第%{line}行 | q: 返回",
1095
+ "status.failed_open_file": "无法打开 %{file}",
1012
1096
  "cmd.side_by_side_diff": "并排差异",
1013
1097
  "cmd.side_by_side_diff_desc": "并排显示当前文件的差异",
1014
1098
  "cmd.add_comment": "审查: 添加评论",
@@ -4640,13 +4640,121 @@ async function review_branch_refresh(): Promise<void> {
4640
4640
  }
4641
4641
  registerHandler("review_branch_refresh", review_branch_refresh);
4642
4642
 
4643
- /** Enter: focus the detail panel (so the user can scroll/click within it). */
4643
+ /** Is the detail panel the currently-focused buffer? */
4644
+ function isReviewBranchDetailFocused(): boolean {
4645
+ return (
4646
+ branchState.detailBufferId !== null &&
4647
+ editor.getActiveBufferId() === branchState.detailBufferId
4648
+ );
4649
+ }
4650
+
4651
+ /** The currently-selected commit in the log panel, or null. */
4652
+ function selectedReviewBranchCommit(): GitCommit | null {
4653
+ if (branchState.commits.length === 0) return null;
4654
+ const i = Math.max(
4655
+ 0,
4656
+ Math.min(branchState.selectedIndex, branchState.commits.length - 1),
4657
+ );
4658
+ return branchState.commits[i] ?? null;
4659
+ }
4660
+
4661
+ /**
4662
+ * Enter: on the log panel jumps focus into the detail panel; on the detail
4663
+ * panel opens the file at the cursor position at the selected commit (if any).
4664
+ */
4644
4665
  function review_branch_enter(): void {
4645
4666
  if (branchState.groupId === null) return;
4667
+ if (isReviewBranchDetailFocused()) {
4668
+ void review_branch_detail_open_file();
4669
+ return;
4670
+ }
4646
4671
  editor.focusBufferGroupPanel(branchState.groupId, "detail");
4647
4672
  }
4648
4673
  registerHandler("review_branch_enter", review_branch_enter);
4649
4674
 
4675
+ /**
4676
+ * Open the file at the cursor's `(file, line)` text-properties at the
4677
+ * currently-selected commit, in a read-only virtual buffer. Mirrors the
4678
+ * git-log plugin's `git_log_detail_open_file` so users get the same
4679
+ * drill-down from the review-branch detail panel.
4680
+ */
4681
+ async function review_branch_detail_open_file(): Promise<void> {
4682
+ if (branchState.detailBufferId === null) return;
4683
+ const commit = selectedReviewBranchCommit();
4684
+ if (!commit) return;
4685
+
4686
+ const props = editor.getTextPropertiesAtCursor(branchState.detailBufferId);
4687
+ if (props.length === 0) {
4688
+ editor.setStatus(editor.t("status.move_to_diff"));
4689
+ return;
4690
+ }
4691
+ const file = props[0].file as string | undefined;
4692
+ const line = (props[0].line as number | undefined) ?? 1;
4693
+ if (!file) {
4694
+ editor.setStatus(editor.t("status.move_to_diff_with_context"));
4695
+ return;
4696
+ }
4697
+
4698
+ editor.setStatus(
4699
+ editor.t("status.file_loading", { file, hash: commit.shortHash }),
4700
+ );
4701
+ const result = await editor.spawnProcess("git", [
4702
+ "show",
4703
+ `${commit.hash}:${file}`,
4704
+ ]);
4705
+ if (result.exit_code !== 0) {
4706
+ editor.setStatus(
4707
+ editor.t("status.file_not_found", { file, hash: commit.shortHash }),
4708
+ );
4709
+ return;
4710
+ }
4711
+
4712
+ const lines = result.stdout.split("\n");
4713
+ const entries: TextPropertyEntry[] = lines.map((l, i) => ({
4714
+ text: l + (i < lines.length - 1 ? "\n" : ""),
4715
+ properties: { type: "content", line: i + 1 },
4716
+ }));
4717
+
4718
+ // `*<hash>:<path>*` matches the virtual-name convention the host uses
4719
+ // to detect syntax from the trailing filename's extension.
4720
+ const name = `*${commit.shortHash}:${file}*`;
4721
+ const view = await editor.createVirtualBuffer({
4722
+ name,
4723
+ mode: "review-branch-file-view",
4724
+ readOnly: true,
4725
+ editingDisabled: true,
4726
+ showLineNumbers: true,
4727
+ entries,
4728
+ });
4729
+ if (view) {
4730
+ const byte = await editor.getLineStartPosition(Math.max(0, line - 1));
4731
+ if (byte !== null) editor.setBufferCursor(view.bufferId, byte);
4732
+ editor.setStatus(
4733
+ editor.t("status.file_view_ready", {
4734
+ file,
4735
+ hash: commit.shortHash,
4736
+ line: String(line),
4737
+ }),
4738
+ );
4739
+ } else {
4740
+ editor.setStatus(editor.t("status.failed_open_file", { file }));
4741
+ }
4742
+ }
4743
+ registerHandler(
4744
+ "review_branch_detail_open_file",
4745
+ review_branch_detail_open_file,
4746
+ );
4747
+
4748
+ /** Tab: toggle focus between the log and detail panels. */
4749
+ function review_branch_tab(): void {
4750
+ if (branchState.groupId === null) return;
4751
+ editor.focusBufferGroupPanel(
4752
+ branchState.groupId,
4753
+ isReviewBranchDetailFocused() ? "log" : "detail",
4754
+ );
4755
+ }
4756
+ registerHandler("review_branch_tab", review_branch_tab);
4757
+
4650
4758
  /** q/Escape: focus-back from detail, or close when already on log. */
4651
4759
  function review_branch_close_or_back(): void {
4652
4760
  if (branchState.groupId === null) return;
@@ -4683,9 +4791,11 @@ editor.defineMode(
4683
4791
  // from the Normal keymap via `inheritNormalBindings: true`.
4684
4792
  ["k", "move_up"],
4685
4793
  ["j", "move_down"],
4686
- // Enter: focus the right-hand detail panel.
4794
+ // Enter: from the log, focus the detail panel; from the detail
4795
+ // panel, open the file at the cursor at the selected commit.
4687
4796
  ["Return", "review_branch_enter"],
4688
- ["Tab", "review_branch_enter"],
4797
+ // Tab: toggle focus between the log and detail panels.
4798
+ ["Tab", "review_branch_tab"],
4689
4799
  ["r", "review_branch_refresh"],
4690
4800
  ["q", "review_branch_close_or_back"],
4691
4801
  ["Escape", "review_branch_close_or_back"],
@@ -4695,6 +4805,32 @@ editor.defineMode(
4695
4805
  true, // inheritNormalBindings — PageUp/PageDown/arrows/Home/End come from Normal
4696
4806
  );
4697
4807
 
4808
+ /** Close the file-view virtual buffer opened from the review-branch detail panel. */
4809
+ function review_branch_file_view_close(): void {
4810
+ const id = editor.getActiveBufferId();
4811
+ if (id) editor.closeBuffer(id);
4812
+ }
4813
+ registerHandler("review_branch_file_view_close", review_branch_file_view_close);
4814
+
4815
+ // Mode for the read-only "git show <hash>:<file>" buffer opened from the
4816
+ // review-branch detail panel. Mirrors git-log's `git-log-file-view`:
4817
+ // q/Escape close the view, j/k alias Up/Down, and all other Normal
4818
+ // bindings (arrows, PageUp/Down, Home/End, Ctrl+C copy) are inherited so
4819
+ // unbound keys don't fall through to edit actions and trip the
4820
+ // `editing_disabled` status message (see #566).
4821
+ editor.defineMode(
4822
+ "review-branch-file-view",
4823
+ [
4824
+ ["k", "move_up"],
4825
+ ["j", "move_down"],
4826
+ ["q", "review_branch_file_view_close"],
4827
+ ["Escape", "review_branch_file_view_close"],
4828
+ ],
4829
+ true, // read-only
4830
+ false, // allow_text_input
4831
+ true, // inherit Normal-context bindings for unbound keys
4832
+ );
4833
+
4698
4834
  // Register Modes and Commands
4699
4835
  editor.registerCommand("%cmd.review_diff", "%cmd.review_diff_desc", "start_review_diff", null);
4700
4836
  editor.registerCommand("%cmd.review_branch", "%cmd.review_branch_desc", "start_review_branch", null);
@@ -134,7 +134,10 @@
134
134
  "preview_tabs": true,
135
135
  "side": "left",
136
136
  "auto_open_on_last_buffer_close": true,
137
- "follow_active_buffer": false
137
+ "follow_active_buffer": false,
138
+ "compact_directories": true,
139
+ "tree_indicator_collapsed": ">",
140
+ "tree_indicator_expanded": "▼"
138
141
  }
139
142
  },
140
143
  "file_browser": {
@@ -793,7 +796,8 @@
793
796
  "{messages}"
794
797
  ],
795
798
  "x-section": "Status Bar",
796
- "x-dual-list-sibling": "/editor/status_bar/right"
799
+ "x-dual-list-sibling": "/editor/status_bar/right",
800
+ "x-dynamically-extendable-status-bar-elements": true
797
801
  },
798
802
  "right": {
799
803
  "description": "Elements shown on the right side of the status bar.\nDefault: [\"{line_ending}\", \"{encoding}\", \"{language}\", \"{lsp}\", \"{warnings}\", \"{update}\", \"{palette}\"]",
@@ -811,7 +815,8 @@
811
815
  "{palette}"
812
816
  ],
813
817
  "x-section": "Status Bar",
814
- "x-dual-list-sibling": "/editor/status_bar/left"
818
+ "x-dual-list-sibling": "/editor/status_bar/left",
819
+ "x-dynamically-extendable-status-bar-elements": true
815
820
  }
816
821
  }
817
822
  },
@@ -958,6 +963,21 @@
958
963
  "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
964
  "type": "boolean",
960
965
  "default": false
966
+ },
967
+ "compact_directories": {
968
+ "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",
969
+ "type": "boolean",
970
+ "default": true
971
+ },
972
+ "tree_indicator_collapsed": {
973
+ "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: \">\"",
974
+ "type": "string",
975
+ "default": ">"
976
+ },
977
+ "tree_indicator_expanded": {
978
+ "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: \"▼\"",
979
+ "type": "string",
980
+ "default": "▼"
961
981
  }
962
982
  }
963
983
  },
@@ -1643,6 +1663,10 @@
1643
1663
  "null"
1644
1664
  ],
1645
1665
  "readOnly": true
1666
+ },
1667
+ "settings": {
1668
+ "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\".",
1669
+ "readOnly": true
1646
1670
  }
1647
1671
  },
1648
1672
  "x-display-field": "/enabled"
@@ -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 config. We read from
1590
- // getUserConfig (raw file) rather than getConfig because unknown
1591
- // fields are dropped when the Config struct reserializes. Default
1592
- // is true.
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.getUserConfig() as Record<string, unknown> | null;
1598
- const plugins = cfg?.plugins as Record<string, unknown> | undefined;
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
- // If the plugin loads mid-session (user toggles it on in Settings),
1805
- // the `ready` hook has already fired, so we also run an immediate
1806
- // check. At startup the `listBuffers().length > 0` guard keeps us
1807
- // dormant until the workspace has actually restored: plugins load
1808
- // before restore, and opening a buffer here would race with the
1809
- // restore and leave a stray Dashboard tab even when real files exist.
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
- }
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
- const LABEL_POOL = "asdfghjklqwertyuiopzxcvbnm";
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 skip = buildSkipSet(matches, views, emptyPattern);
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 LABEL_POOL) if (!skip.has(c)) remaining.add(c);
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 LABEL_POOL) if (remaining.has(c)) orderedRemaining.push(c);
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;