dsh-ide-git 0.24.0 → 0.27.0
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/README.md +7 -3
- package/README_EN.md +7 -3
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +700 -104
package/src/client.js
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* chrome:
|
|
10
10
|
*
|
|
11
11
|
* columns wide and tall enough : repo/branch bar | tree | graph | changes
|
|
12
|
-
* stack narrow and tall : changes over
|
|
13
|
-
* compact
|
|
12
|
+
* stack narrow and tall : changes over a tabbed history/diff window
|
|
13
|
+
* compact very narrow : changes on top + the same tabbed window
|
|
14
14
|
*
|
|
15
15
|
* Theming: no hard-coded surfaces. Every surface is a --dsw-alias-* token, so
|
|
16
16
|
* transparent themes and background plugins (dsh-any-background) show through,
|
|
@@ -90,6 +90,8 @@ window.__ModuleLoader__.load({
|
|
|
90
90
|
'repo.nested': '子仓库',
|
|
91
91
|
'repo.none': '当前工作区不是 Git 仓库',
|
|
92
92
|
'repo.pick': '选择要查看的仓库',
|
|
93
|
+
'repo.filter': '搜索仓库',
|
|
94
|
+
'picker.empty': '没有匹配项',
|
|
93
95
|
'repo.empty': '这个工作区下没有找到 Git 仓库',
|
|
94
96
|
'repo.switch': '切换仓库',
|
|
95
97
|
'branch.switch': '切换分支',
|
|
@@ -462,6 +464,8 @@ window.__ModuleLoader__.load({
|
|
|
462
464
|
'repo.nested': 'nested',
|
|
463
465
|
'repo.none': 'This workspace is not a Git repository',
|
|
464
466
|
'repo.pick': 'Pick a repository to inspect',
|
|
467
|
+
'repo.filter': 'Search repositories',
|
|
468
|
+
'picker.empty': 'No matches',
|
|
465
469
|
'repo.empty': 'No Git repository found in this workspace',
|
|
466
470
|
'repo.switch': 'Switch repository',
|
|
467
471
|
'branch.switch': 'Switch branch',
|
|
@@ -842,6 +846,8 @@ window.__ModuleLoader__.load({
|
|
|
842
846
|
'repo.nested': '巢狀',
|
|
843
847
|
'repo.none': '呢個工作區唔係 Git 存放庫',
|
|
844
848
|
'repo.pick': '揀一個存放庫嚟睇',
|
|
849
|
+
'repo.filter': '搵存放庫',
|
|
850
|
+
'picker.empty': '冇相符項目',
|
|
845
851
|
'repo.empty': '呢個工作區搵唔到 Git 存放庫',
|
|
846
852
|
'repo.switch': '切換存放庫',
|
|
847
853
|
'branch.switch': '切換分支',
|
|
@@ -1214,6 +1220,8 @@ window.__ModuleLoader__.load({
|
|
|
1214
1220
|
'repo.nested': '巢狀',
|
|
1215
1221
|
'repo.none': '這個工作區不是 Git 版本庫',
|
|
1216
1222
|
'repo.pick': '選擇要檢視的版本庫',
|
|
1223
|
+
'repo.filter': '搜尋版本庫',
|
|
1224
|
+
'picker.empty': '沒有相符項目',
|
|
1217
1225
|
'repo.empty': '這個工作區找不到 Git 版本庫',
|
|
1218
1226
|
'repo.switch': '切換版本庫',
|
|
1219
1227
|
'branch.switch': '切換分支',
|
|
@@ -1586,6 +1594,8 @@ window.__ModuleLoader__.load({
|
|
|
1586
1594
|
'repo.nested': '巢狀',
|
|
1587
1595
|
'repo.none': '呢個工作區唔係 Git 存放庫',
|
|
1588
1596
|
'repo.pick': '揀一個存放庫嚟睇',
|
|
1597
|
+
'repo.filter': '搵存放庫',
|
|
1598
|
+
'picker.empty': '冇相符項目',
|
|
1589
1599
|
'repo.empty': '呢個工作區搵唔到 Git 存放庫',
|
|
1590
1600
|
'repo.switch': '切換存放庫',
|
|
1591
1601
|
'branch.switch': '切換分支',
|
|
@@ -1958,6 +1968,8 @@ window.__ModuleLoader__.load({
|
|
|
1958
1968
|
'repo.nested': 'ネスト',
|
|
1959
1969
|
'repo.none': 'このワークスペースは Git リポジトリではありません',
|
|
1960
1970
|
'repo.pick': '確認するリポジトリを選択',
|
|
1971
|
+
'repo.filter': 'リポジトリを検索',
|
|
1972
|
+
'picker.empty': '一致する項目がありません',
|
|
1961
1973
|
'repo.empty': 'このワークスペースに Git リポジトリが見つかりません',
|
|
1962
1974
|
'repo.switch': 'リポジトリを切り替え',
|
|
1963
1975
|
'branch.switch': 'ブランチを切り替え',
|
|
@@ -2330,6 +2342,8 @@ window.__ModuleLoader__.load({
|
|
|
2330
2342
|
'repo.nested': '중첩',
|
|
2331
2343
|
'repo.none': '이 워크스페이스는 Git 저장소가 아닙니다',
|
|
2332
2344
|
'repo.pick': '확인할 저장소를 선택하세요',
|
|
2345
|
+
'repo.filter': '저장소 검색',
|
|
2346
|
+
'picker.empty': '일치하는 항목이 없습니다',
|
|
2333
2347
|
'repo.empty': '이 워크스페이스에서 Git 저장소를 찾을 수 없습니다',
|
|
2334
2348
|
'repo.switch': '저장소 전환',
|
|
2335
2349
|
'branch.switch': '브랜치 전환',
|
|
@@ -2702,6 +2716,8 @@ window.__ModuleLoader__.load({
|
|
|
2702
2716
|
'repo.nested': 'verschachtelt',
|
|
2703
2717
|
'repo.none': 'Dieser Arbeitsbereich ist kein Git-Repository',
|
|
2704
2718
|
'repo.pick': 'Repository zum Ansehen wählen',
|
|
2719
|
+
'repo.filter': 'Repositories durchsuchen',
|
|
2720
|
+
'picker.empty': 'Keine Treffer',
|
|
2705
2721
|
'repo.empty': 'In diesem Arbeitsbereich wurde kein Git-Repository gefunden',
|
|
2706
2722
|
'repo.switch': 'Repository wechseln',
|
|
2707
2723
|
'branch.switch': 'Branch wechseln',
|
|
@@ -3074,6 +3090,8 @@ window.__ModuleLoader__.load({
|
|
|
3074
3090
|
'repo.nested': 'imbriqué',
|
|
3075
3091
|
'repo.none': 'Cet espace de travail n\'est pas un dépôt Git',
|
|
3076
3092
|
'repo.pick': 'Choisir un dépôt à inspecter',
|
|
3093
|
+
'repo.filter': 'Rechercher des dépôts',
|
|
3094
|
+
'picker.empty': 'Aucun résultat',
|
|
3077
3095
|
'repo.empty': 'Aucun dépôt Git trouvé dans cet espace de travail',
|
|
3078
3096
|
'repo.switch': 'Changer de dépôt',
|
|
3079
3097
|
'branch.switch': 'Changer de branche',
|
|
@@ -3446,6 +3464,8 @@ window.__ModuleLoader__.load({
|
|
|
3446
3464
|
'repo.nested': 'вложенный',
|
|
3447
3465
|
'repo.none': 'Эта рабочая область не является репозиторием Git',
|
|
3448
3466
|
'repo.pick': 'Выберите репозиторий для просмотра',
|
|
3467
|
+
'repo.filter': 'Поиск репозиториев',
|
|
3468
|
+
'picker.empty': 'Нет совпадений',
|
|
3449
3469
|
'repo.empty': 'В этой рабочей области репозиторий Git не найден',
|
|
3450
3470
|
'repo.switch': 'Сменить репозиторий',
|
|
3451
3471
|
'branch.switch': 'Сменить ветку',
|
|
@@ -3818,6 +3838,8 @@ window.__ModuleLoader__.load({
|
|
|
3818
3838
|
'repo.nested': 'aninhado',
|
|
3819
3839
|
'repo.none': 'Esta área de trabalho não é um repositório Git',
|
|
3820
3840
|
'repo.pick': 'Escolha um repositório para inspecionar',
|
|
3841
|
+
'repo.filter': 'Pesquisar repositórios',
|
|
3842
|
+
'picker.empty': 'Nenhum resultado',
|
|
3821
3843
|
'repo.empty': 'Nenhum repositório Git encontrado nesta área de trabalho',
|
|
3822
3844
|
'repo.switch': 'Trocar de repositório',
|
|
3823
3845
|
'branch.switch': 'Trocar de branch',
|
|
@@ -4190,6 +4212,8 @@ window.__ModuleLoader__.load({
|
|
|
4190
4212
|
'repo.nested': 'annidato',
|
|
4191
4213
|
'repo.none': 'Questa area di lavoro non è un repository Git',
|
|
4192
4214
|
'repo.pick': 'Scegli un repository da esaminare',
|
|
4215
|
+
'repo.filter': 'Cerca repository',
|
|
4216
|
+
'picker.empty': 'Nessuna corrispondenza',
|
|
4193
4217
|
'repo.empty': 'Nessun repository Git trovato in questa area di lavoro',
|
|
4194
4218
|
'repo.switch': 'Cambia repository',
|
|
4195
4219
|
'branch.switch': 'Cambia branch',
|
|
@@ -4562,6 +4586,8 @@ window.__ModuleLoader__.load({
|
|
|
4562
4586
|
'repo.nested': 'genest',
|
|
4563
4587
|
'repo.none': 'Deze werkruimte is geen Git-repository',
|
|
4564
4588
|
'repo.pick': 'Kies een repository om te bekijken',
|
|
4589
|
+
'repo.filter': 'Repositories zoeken',
|
|
4590
|
+
'picker.empty': 'Geen resultaten',
|
|
4565
4591
|
'repo.empty': 'Geen Git-repository gevonden in deze werkruimte',
|
|
4566
4592
|
'repo.switch': 'Wissel van repository',
|
|
4567
4593
|
'branch.switch': 'Wissel van branch',
|
|
@@ -4934,6 +4960,8 @@ window.__ModuleLoader__.load({
|
|
|
4934
4960
|
'repo.nested': 'zagnieżdżone',
|
|
4935
4961
|
'repo.none': 'Ten obszar roboczy nie jest repozytorium Git',
|
|
4936
4962
|
'repo.pick': 'Wybierz repozytorium do podglądu',
|
|
4963
|
+
'repo.filter': 'Szukaj repozytoriów',
|
|
4964
|
+
'picker.empty': 'Brak dopasowań',
|
|
4937
4965
|
'repo.empty': 'W tym obszarze roboczym nie znaleziono repozytorium Git',
|
|
4938
4966
|
'repo.switch': 'Zmień repozytorium',
|
|
4939
4967
|
'branch.switch': 'Zmień gałąź',
|
|
@@ -5306,6 +5334,8 @@ window.__ModuleLoader__.load({
|
|
|
5306
5334
|
'repo.nested': 'nästlad',
|
|
5307
5335
|
'repo.none': 'Den här arbetsytan är inte ett Git-arkiv',
|
|
5308
5336
|
'repo.pick': 'Välj ett arkiv att granska',
|
|
5337
|
+
'repo.filter': 'Sök bland arkiv',
|
|
5338
|
+
'picker.empty': 'Inga träffar',
|
|
5309
5339
|
'repo.empty': 'Inget Git-arkiv hittades i den här arbetsytan',
|
|
5310
5340
|
'repo.switch': 'Byt arkiv',
|
|
5311
5341
|
'branch.switch': 'Byt gren',
|
|
@@ -5678,6 +5708,8 @@ window.__ModuleLoader__.load({
|
|
|
5678
5708
|
'repo.nested': 'iç içe',
|
|
5679
5709
|
'repo.none': 'Bu çalışma alanı bir Git deposu değil',
|
|
5680
5710
|
'repo.pick': 'İncelenecek depoyu seçin',
|
|
5711
|
+
'repo.filter': 'Depo ara',
|
|
5712
|
+
'picker.empty': 'Eşleşme yok',
|
|
5681
5713
|
'repo.empty': 'Bu çalışma alanında Git deposu bulunamadı',
|
|
5682
5714
|
'repo.switch': 'Depoyu değiştir',
|
|
5683
5715
|
'branch.switch': 'Dalı değiştir',
|
|
@@ -6050,6 +6082,8 @@ window.__ModuleLoader__.load({
|
|
|
6050
6082
|
'repo.nested': 'bersarang',
|
|
6051
6083
|
'repo.none': 'Ruang kerja ini bukan repositori Git',
|
|
6052
6084
|
'repo.pick': 'Pilih repositori untuk diperiksa',
|
|
6085
|
+
'repo.filter': 'Cari repositori',
|
|
6086
|
+
'picker.empty': 'Tidak ada kecocokan',
|
|
6053
6087
|
'repo.empty': 'Tidak ada repositori Git di ruang kerja ini',
|
|
6054
6088
|
'repo.switch': 'Ganti repositori',
|
|
6055
6089
|
'branch.switch': 'Ganti branch',
|
|
@@ -6422,6 +6456,8 @@ window.__ModuleLoader__.load({
|
|
|
6422
6456
|
'repo.nested': 'lồng nhau',
|
|
6423
6457
|
'repo.none': 'Không gian làm việc này không phải kho Git',
|
|
6424
6458
|
'repo.pick': 'Chọn một kho để xem',
|
|
6459
|
+
'repo.filter': 'Tìm kho',
|
|
6460
|
+
'picker.empty': 'Không có kết quả phù hợp',
|
|
6425
6461
|
'repo.empty': 'Không tìm thấy kho Git trong không gian làm việc này',
|
|
6426
6462
|
'repo.switch': 'Chuyển kho',
|
|
6427
6463
|
'branch.switch': 'Chuyển nhánh',
|
|
@@ -6794,6 +6830,8 @@ window.__ModuleLoader__.load({
|
|
|
6794
6830
|
'repo.nested': 'متشعّب',
|
|
6795
6831
|
'repo.none': 'مساحة العمل هذه ليست مستودع Git',
|
|
6796
6832
|
'repo.pick': 'اختر مستودعًا لعرضه',
|
|
6833
|
+
'repo.filter': 'البحث عن المستودعات',
|
|
6834
|
+
'picker.empty': 'لا توجد نتائج مطابقة',
|
|
6797
6835
|
'repo.empty': 'لم يُعثر على مستودع Git في مساحة العمل هذه',
|
|
6798
6836
|
'repo.switch': 'تبديل المستودع',
|
|
6799
6837
|
'branch.switch': 'تبديل الفرع',
|
|
@@ -7166,6 +7204,8 @@ window.__ModuleLoader__.load({
|
|
|
7166
7204
|
'repo.nested': 'नेस्टेड',
|
|
7167
7205
|
'repo.none': 'यह कार्यक्षेत्र Git रिपॉज़िटरी नहीं है',
|
|
7168
7206
|
'repo.pick': 'देखने के लिए रिपॉज़िटरी चुनें',
|
|
7207
|
+
'repo.filter': 'रिपॉज़िटरी खोजें',
|
|
7208
|
+
'picker.empty': 'कोई मेल नहीं',
|
|
7169
7209
|
'repo.empty': 'इस कार्यक्षेत्र में कोई Git रिपॉज़िटरी नहीं मिली',
|
|
7170
7210
|
'repo.switch': 'रिपॉज़िटरी बदलें',
|
|
7171
7211
|
'branch.switch': 'शाखा बदलें',
|
|
@@ -7538,6 +7578,8 @@ window.__ModuleLoader__.load({
|
|
|
7538
7578
|
'repo.nested': 'ซ้อนกัน',
|
|
7539
7579
|
'repo.none': 'เวิร์กสเปซนี้ไม่ใช่รีโพซิทอรี Git',
|
|
7540
7580
|
'repo.pick': 'เลือกรีโพซิทอรีที่จะดู',
|
|
7581
|
+
'repo.filter': 'ค้นหารีโพซิทอรี',
|
|
7582
|
+
'picker.empty': 'ไม่มีรายการที่ตรงกัน',
|
|
7541
7583
|
'repo.empty': 'ไม่พบรีโพซิทอรี Git ในเวิร์กสเปซนี้',
|
|
7542
7584
|
'repo.switch': 'สลับรีโพซิทอรี',
|
|
7543
7585
|
'branch.switch': 'สลับแบรนช์',
|
|
@@ -8852,8 +8894,12 @@ window.__ModuleLoader__.load({
|
|
|
8852
8894
|
panes measure width (columns). */
|
|
8853
8895
|
const PANE_CHROME_KEYS = {
|
|
8854
8896
|
columns: ['tree', 'changes', 'diff'],
|
|
8855
|
-
|
|
8856
|
-
|
|
8897
|
+
/* v0.26.1: BOTH stacked chromes put the history list and the diff in one
|
|
8898
|
+
TABBED window (history | diff) under the changes list, so the diff is
|
|
8899
|
+
no longer a pane of its own here — it fills the tab body. Only the
|
|
8900
|
+
three-column chrome still splits the diff out of the middle column. */
|
|
8901
|
+
stack: ['tree', 'changes'],
|
|
8902
|
+
compact: ['tree', 'changes'],
|
|
8857
8903
|
}
|
|
8858
8904
|
/* Whether a key measures a height (stacked chromes) or a width (columns).
|
|
8859
8905
|
This is the chrome's DEFAULT for the tree/changes panes; the diff pane
|
|
@@ -8869,9 +8915,8 @@ window.__ModuleLoader__.load({
|
|
|
8869
8915
|
'columns:diff': { min: 120, max: 900 },
|
|
8870
8916
|
'stack:tree': { min: 100, max: 420 },
|
|
8871
8917
|
'stack:changes': { min: 120, max: 460 },
|
|
8872
|
-
'stack:diff': { min: 120, max: 900 },
|
|
8873
8918
|
'compact:tree': { min: 100, max: 420 },
|
|
8874
|
-
'compact:
|
|
8919
|
+
'compact:changes': { min: 120, max: 460 },
|
|
8875
8920
|
}
|
|
8876
8921
|
/* Today's fixed sizes stay the default — and stay in the stylesheet: a pane
|
|
8877
8922
|
the user never dragged keeps exactly the look it had before, while a
|
|
@@ -8884,6 +8929,7 @@ window.__ModuleLoader__.load({
|
|
|
8884
8929
|
'stack:tree': { ratio: 0.36 },
|
|
8885
8930
|
'stack:changes': { ratio: 0.46 },
|
|
8886
8931
|
'compact:tree': { ratio: 0.42 },
|
|
8932
|
+
'compact:changes': { ratio: 0.46 },
|
|
8887
8933
|
}
|
|
8888
8934
|
const PANE_MAIN_MIN_W = 240
|
|
8889
8935
|
const PANE_MAIN_MIN_H = 150
|
|
@@ -8899,7 +8945,7 @@ window.__ModuleLoader__.load({
|
|
|
8899
8945
|
history↔diff separator lives inside the middle pane, so it is not counted
|
|
8900
8946
|
against the body's own budget. */
|
|
8901
8947
|
const PANE_GUTTER_PX = 8
|
|
8902
|
-
const PANE_BODY_GUTTERS = { columns: 2, stack: 2, compact:
|
|
8948
|
+
const PANE_BODY_GUTTERS = { columns: 2, stack: 2, compact: 2 }
|
|
8903
8949
|
const PANE_BUCKETS = Object.keys(PANE_CHROME_KEYS).reduce(
|
|
8904
8950
|
(all, chrome) => all.concat([chrome + ':wide', chrome + ':tall']), [])
|
|
8905
8951
|
|
|
@@ -10469,12 +10515,187 @@ window.__ModuleLoader__.load({
|
|
|
10469
10515
|
return E('div', { className: 'dig-menu-wrap', ref: ref, style: { left: position.left + 'px', top: position.top + 'px' } },
|
|
10470
10516
|
E('div', { className: 'dig-menu', ref: cardRef, 'data-menu-material': 'translucent' },
|
|
10471
10517
|
E('div', { className: 'dig-menu-scroll' },
|
|
10472
|
-
props.
|
|
10518
|
+
props.body === undefined
|
|
10519
|
+
? (Array.isArray(props.items) ? props.items : []).map((item, index) => renderItem(item, index, false))
|
|
10520
|
+
: props.body)),
|
|
10473
10521
|
sub === null ? null : E('div', { className: 'dig-menu dig-menu-sub', 'data-menu-material': 'translucent', style: sub.flip === true ? { right: '100%', top: sub.top + 'px' } : { left: '100%', top: sub.top + 'px' } },
|
|
10474
10522
|
E('div', { className: 'dig-menu-scroll' },
|
|
10475
10523
|
sub.items.map((item, index) => renderItem(item, index, true)))))
|
|
10476
10524
|
}
|
|
10477
10525
|
|
|
10526
|
+
/* Searchable picker body shared by the panel header switchers and the
|
|
10527
|
+
composer pills. It owns no positioning: the panel renders it as a
|
|
10528
|
+
ContextMenu body, the composer inside its own .dig-cs-menu. Repositories
|
|
10529
|
+
are a flat, filtered list; branches keep their slash namespace as a
|
|
10530
|
+
collapsible tree, and a non-empty query opens every folder so a match
|
|
10531
|
+
is never hidden behind a fold. */
|
|
10532
|
+
function SearchPicker(props) {
|
|
10533
|
+
const t = props.t
|
|
10534
|
+
const mode = props.mode === 'branch' ? 'branch' : 'repo'
|
|
10535
|
+
const [needle, setNeedle] = useState('')
|
|
10536
|
+
const [folded, setFolded] = useState({})
|
|
10537
|
+
const [active, setActive] = useState(0)
|
|
10538
|
+
const inputRef = useRef(null)
|
|
10539
|
+
const listRef = useRef(null)
|
|
10540
|
+
useEffect(() => {
|
|
10541
|
+
const input = inputRef.current
|
|
10542
|
+
if (input !== null && typeof input.focus === 'function') input.focus({ preventScroll: true })
|
|
10543
|
+
}, [])
|
|
10544
|
+
const query = needle.trim().toLowerCase()
|
|
10545
|
+
const matches = (value) => query === '' || String(value === undefined || value === null ? '' : value).toLowerCase().indexOf(query) >= 0
|
|
10546
|
+
const rows = []
|
|
10547
|
+
if (mode === 'repo') {
|
|
10548
|
+
const repos = Array.isArray(props.repos) ? props.repos : []
|
|
10549
|
+
for (const repo of repos) {
|
|
10550
|
+
const name = repo.name === undefined || repo.name === null ? baseNameOf(repo.path) : String(repo.name)
|
|
10551
|
+
if (query !== '' && matches(name) === false && matches(repo.path) === false && matches(repo.sub) === false && matches(repo.branch) === false) continue
|
|
10552
|
+
rows.push({ key: 'repo:' + String(repo.path), kind: 'repo', repo: repo, name: name })
|
|
10553
|
+
}
|
|
10554
|
+
} else {
|
|
10555
|
+
const entries = (Array.isArray(props.branches) ? props.branches : []).filter((entry) => matches(entry.name))
|
|
10556
|
+
const root = { folders: new Map(), leaves: [] }
|
|
10557
|
+
for (const entry of entries) {
|
|
10558
|
+
const parts = String(entry.name).split('/')
|
|
10559
|
+
let node = root
|
|
10560
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
10561
|
+
const key = parts[index]
|
|
10562
|
+
if (node.folders.has(key) === false) node.folders.set(key, { folders: new Map(), leaves: [] })
|
|
10563
|
+
node = node.folders.get(key)
|
|
10564
|
+
}
|
|
10565
|
+
node.leaves.push({ entry: entry, label: parts[parts.length - 1] })
|
|
10566
|
+
}
|
|
10567
|
+
const countLeaves = (node) => node.leaves.length + Array.from(node.folders.values()).reduce((sum, child) => sum + countLeaves(child), 0)
|
|
10568
|
+
const walk = (node, prefix, depth) => {
|
|
10569
|
+
for (const name of Array.from(node.folders.keys()).sort()) {
|
|
10570
|
+
const id = prefix === '' ? name : prefix + '/' + name
|
|
10571
|
+
const open = query !== '' || folded[id] !== true
|
|
10572
|
+
rows.push({ key: 'dir:' + id, kind: 'folder', id: id, name: name, depth: depth, count: countLeaves(node.folders.get(name)), open: open })
|
|
10573
|
+
if (open === true) walk(node.folders.get(name), id, depth + 1)
|
|
10574
|
+
}
|
|
10575
|
+
for (const leaf of node.leaves) {
|
|
10576
|
+
const entry = leaf.entry
|
|
10577
|
+
const worktree = entry.worktree !== null && entry.worktree !== undefined
|
|
10578
|
+
const busy = typeof props.busyName === 'string' && props.busyName !== ''
|
|
10579
|
+
rows.push({
|
|
10580
|
+
key: 'branch:' + String(entry.name), kind: 'branch', entry: entry, label: leaf.label, depth: depth,
|
|
10581
|
+
disabled: worktree === true || busy === true, worktree: worktree,
|
|
10582
|
+
})
|
|
10583
|
+
}
|
|
10584
|
+
}
|
|
10585
|
+
walk(root, '', 0)
|
|
10586
|
+
}
|
|
10587
|
+
const selectable = rows.filter((row) => row.kind !== 'folder' && row.disabled !== true)
|
|
10588
|
+
const activeIndex = selectable.length === 0 ? -1 : Math.min(active, selectable.length - 1)
|
|
10589
|
+
const activeRow = activeIndex < 0 ? null : selectable[activeIndex]
|
|
10590
|
+
const activeKey = activeRow === null ? '' : activeRow.key
|
|
10591
|
+
useEffect(() => {
|
|
10592
|
+
const host = listRef.current
|
|
10593
|
+
if (host === null || activeKey === '') return
|
|
10594
|
+
const found = Array.from(host.querySelectorAll('[data-pick-key]')).find((row) => row.getAttribute('data-pick-key') === activeKey)
|
|
10595
|
+
if (found !== undefined && typeof found.scrollIntoView === 'function') found.scrollIntoView({ block: 'nearest' })
|
|
10596
|
+
}, [activeKey])
|
|
10597
|
+
const pickRow = (row) => {
|
|
10598
|
+
if (row === null || row.disabled === true || typeof props.onPick !== 'function') return
|
|
10599
|
+
if (row.kind === 'repo') props.onPick(row.repo.path, row.repo)
|
|
10600
|
+
else if (row.kind === 'branch') props.onPick(row.entry.name, row.entry)
|
|
10601
|
+
}
|
|
10602
|
+
const onKeyDown = (event) => {
|
|
10603
|
+
if (event.key === 'Escape') {
|
|
10604
|
+
event.preventDefault()
|
|
10605
|
+
event.stopPropagation()
|
|
10606
|
+
if (typeof props.onClose === 'function') props.onClose()
|
|
10607
|
+
return
|
|
10608
|
+
}
|
|
10609
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
10610
|
+
if (selectable.length === 0) return
|
|
10611
|
+
event.preventDefault()
|
|
10612
|
+
event.stopPropagation()
|
|
10613
|
+
setActive((value) => {
|
|
10614
|
+
const current = Math.min(value, selectable.length - 1)
|
|
10615
|
+
return event.key === 'ArrowDown' ? (current + 1) % selectable.length : (current - 1 + selectable.length) % selectable.length
|
|
10616
|
+
})
|
|
10617
|
+
return
|
|
10618
|
+
}
|
|
10619
|
+
if (event.key === 'Enter') {
|
|
10620
|
+
event.preventDefault()
|
|
10621
|
+
event.stopPropagation()
|
|
10622
|
+
pickRow(activeRow)
|
|
10623
|
+
}
|
|
10624
|
+
}
|
|
10625
|
+
const currentRepo = mode === 'repo' && props.current !== undefined && props.current !== null ? String(props.current) : ''
|
|
10626
|
+
const currentBranch = mode === 'branch' && props.current !== undefined && props.current !== null ? String(props.current) : ''
|
|
10627
|
+
const rowClass = (row) => 'dig-pick-row'
|
|
10628
|
+
+ (activeKey === row.key ? ' dig-pick-row-active' : '')
|
|
10629
|
+
+ ((currentRepo !== '' && row.kind === 'repo' && String(row.repo.path) === currentRepo) ? ' dig-pick-row-current' : '')
|
|
10630
|
+
+ ((currentBranch !== '' && row.kind === 'branch' && String(row.entry.name) === currentBranch) ? ' dig-pick-row-current' : '')
|
|
10631
|
+
+ (row.disabled === true ? ' dig-pick-row-disabled' : '')
|
|
10632
|
+
const rowsView = rows.map((row) => {
|
|
10633
|
+
if (row.kind === 'folder') {
|
|
10634
|
+
return E('button', {
|
|
10635
|
+
key: row.key, type: 'button',
|
|
10636
|
+
className: 'dig-pick-folder' + (row.open !== true ? ' dig-pick-folder-closed' : ''),
|
|
10637
|
+
style: { paddingLeft: (6 + row.depth * 14) + 'px' },
|
|
10638
|
+
onClick: () => { if (query !== '') return; setFolded((previous) => Object.assign({}, previous, { [row.id]: row.open === true })) },
|
|
10639
|
+
},
|
|
10640
|
+
E('span', { className: 'dig-pick-chevron' + (row.open === true ? ' dig-pick-chevron-open' : '') }, E(Icon, { name: 'chevron', size: 10 })),
|
|
10641
|
+
E(Icon, { name: 'folder', size: 12 }),
|
|
10642
|
+
E('span', { className: 'dig-pick-name' }, row.name),
|
|
10643
|
+
E('span', { className: 'dig-pick-count' }, String(row.count)))
|
|
10644
|
+
}
|
|
10645
|
+
if (row.kind === 'repo') {
|
|
10646
|
+
return E('button', {
|
|
10647
|
+
key: row.key, type: 'button', className: rowClass(row), 'data-pick-key': row.key,
|
|
10648
|
+
title: row.repo.path,
|
|
10649
|
+
onMouseEnter: () => { const index = selectable.indexOf(row); if (index >= 0) setActive(index) },
|
|
10650
|
+
onClick: () => pickRow(row),
|
|
10651
|
+
},
|
|
10652
|
+
E(Icon, { name: 'folder', size: 13 }),
|
|
10653
|
+
E('span', { className: 'dig-pick-main' },
|
|
10654
|
+
E('span', { className: 'dig-pick-name' }, row.name),
|
|
10655
|
+
row.repo.sub !== undefined && row.repo.sub !== null && row.repo.sub !== '' && row.repo.sub !== row.name
|
|
10656
|
+
? E('span', { className: 'dig-pick-sub' }, row.repo.sub)
|
|
10657
|
+
: null),
|
|
10658
|
+
row.repo.branch === undefined || row.repo.branch === null || row.repo.branch === ''
|
|
10659
|
+
? null
|
|
10660
|
+
: E('span', { className: 'dig-pick-badge' }, row.repo.branch))
|
|
10661
|
+
}
|
|
10662
|
+
const entry = row.entry
|
|
10663
|
+
const head = entry.head === true
|
|
10664
|
+
const worktreeNote = row.worktree === true
|
|
10665
|
+
? (props.worktreeNote === undefined ? t('branches.worktree') : props.worktreeNote)
|
|
10666
|
+
: null
|
|
10667
|
+
return E('button', {
|
|
10668
|
+
key: row.key, type: 'button',
|
|
10669
|
+
className: rowClass(row) + (head === true ? ' dig-pick-row-head' : ''),
|
|
10670
|
+
'data-pick-key': row.key,
|
|
10671
|
+
style: { paddingLeft: (6 + row.depth * 14) + 'px' },
|
|
10672
|
+
disabled: row.disabled === true,
|
|
10673
|
+
title: String(entry.name) + (worktreeNote === null ? '' : ' · ' + worktreeNote),
|
|
10674
|
+
onMouseEnter: () => { const index = selectable.indexOf(row); if (index >= 0) setActive(index) },
|
|
10675
|
+
onClick: () => pickRow(row),
|
|
10676
|
+
},
|
|
10677
|
+
E('span', { className: 'dig-pick-icon dig-tone-' + (head === true ? 'warn' : 'accent') }, E(Icon, { name: head === true ? 'star' : 'branch', size: 12 })),
|
|
10678
|
+
E('span', { className: 'dig-pick-name' }, row.label),
|
|
10679
|
+
worktreeNote === null ? null : E('span', { className: 'dig-pick-note' }, worktreeNote),
|
|
10680
|
+
entry.ahead > 0 ? E('span', { className: 'dig-pick-count' }, '↑' + String(entry.ahead)) : null,
|
|
10681
|
+
entry.behind > 0 ? E('span', { className: 'dig-pick-count' }, '↓' + String(entry.behind)) : null)
|
|
10682
|
+
})
|
|
10683
|
+
return E('div', { className: 'dig-pick dig-pick-' + mode },
|
|
10684
|
+
E('div', { className: 'dig-pick-search' },
|
|
10685
|
+
E('input', {
|
|
10686
|
+
ref: inputRef,
|
|
10687
|
+
className: 'dig-input dig-input-compact dig-pick-input',
|
|
10688
|
+
value: needle,
|
|
10689
|
+
spellCheck: false,
|
|
10690
|
+
placeholder: mode === 'repo' ? t('repo.filter') : t('branches.filter'),
|
|
10691
|
+
'aria-label': mode === 'repo' ? t('repo.filter') : t('branches.filter'),
|
|
10692
|
+
onChange: (event) => { setNeedle(event.target.value); setActive(0) },
|
|
10693
|
+
onKeyDown: onKeyDown,
|
|
10694
|
+
})),
|
|
10695
|
+
E('div', { className: 'dig-pick-list', ref: listRef, role: 'listbox' },
|
|
10696
|
+
rowsView.length === 0 ? E('div', { className: 'dig-pick-empty' }, t('picker.empty')) : rowsView))
|
|
10697
|
+
}
|
|
10698
|
+
|
|
10478
10699
|
/* Lets the user choose which actions the rail shows and in which order. The
|
|
10479
10700
|
list is drag-sortable and arrow-sortable (the arrows also work with a
|
|
10480
10701
|
keyboard); visibility is a per-action toggle, not a delete, so a hidden
|
|
@@ -12027,10 +12248,12 @@ window.__ModuleLoader__.load({
|
|
|
12027
12248
|
return
|
|
12028
12249
|
}
|
|
12029
12250
|
anchorRef.current = key
|
|
12030
|
-
/* Plain click
|
|
12031
|
-
the
|
|
12032
|
-
|
|
12251
|
+
/* Plain click SELECTS and shows the diff (v0.25.1, user request): in the
|
|
12252
|
+
narrow right column the list replaces the pane, so a click that only
|
|
12253
|
+
highlighted the row looked like "nothing happened". Modifier clicks
|
|
12254
|
+
above stay pure multi-select; ignored rows have no diff to show. */
|
|
12033
12255
|
setSelection((previous) => previous.size === 1 && previous.has(key) === true ? previous : new Set([key]))
|
|
12256
|
+
if (group !== 'ignored' && props.onDiff !== undefined) props.onDiff(item, group)
|
|
12034
12257
|
}, [setSelection, anchorRef, props.onDiff])
|
|
12035
12258
|
/* Rubber-band selection now comes from the shared useRubberBand hook
|
|
12036
12259
|
(see its comment): the fixed row heights + the prefix-sum offsets
|
|
@@ -13165,11 +13388,37 @@ window.__ModuleLoader__.load({
|
|
|
13165
13388
|
const [selectedPath, setSelectedPath] = useState(null)
|
|
13166
13389
|
const [patch, setPatch] = useState('')
|
|
13167
13390
|
const [patchLoading, setPatchLoading] = useState(false)
|
|
13391
|
+
/* The compact chrome's lower window is tabbed (history | diff) instead of
|
|
13392
|
+
split, so opening a file does not take the history list away: the diff
|
|
13393
|
+
gets its own tab and the user can switch back (v0.26.0). */
|
|
13394
|
+
const [splitTab, setSplitTab] = useState('history')
|
|
13395
|
+
useEffect(() => {
|
|
13396
|
+
if (splitTab === 'diff' && patch === '' && patchLoading === false) setSplitTab('history')
|
|
13397
|
+
}, [splitTab, patch, patchLoading])
|
|
13398
|
+
/* v0.26.3: the row fires onDiff from its single click now, so a double
|
|
13399
|
+
click used to open the same diff three times — and the responses raced
|
|
13400
|
+
(a slow first request could land after the second and paint the wrong
|
|
13401
|
+
file under the new title). `diffSeq` stamps each request and only the
|
|
13402
|
+
newest one is allowed to write; `diffLast` swallows the burst a double
|
|
13403
|
+
click produces. */
|
|
13404
|
+
const diffSeq = useRef(0)
|
|
13405
|
+
const diffLast = useRef({ key: '', at: 0 })
|
|
13168
13406
|
/* What the open diff pane shows (v0.13.5): the pane is shared by the
|
|
13169
13407
|
commit detail (hash + subject) and the working tree (staged or not),
|
|
13170
13408
|
and the selection menu's payload must say which. Cleared everywhere
|
|
13171
13409
|
the patch is cleared. */
|
|
13172
13410
|
const [diffContext, setDiffContext] = useState(null)
|
|
13411
|
+
/* v0.27: closing / switching away must also invalidate the request in
|
|
13412
|
+
flight and reset the double-click guard. Without the stamp bump a
|
|
13413
|
+
late response repainted the closed pane; without the diffLast reset
|
|
13414
|
+
reopening the same file within 400ms was swallowed. */
|
|
13415
|
+
const resetDiff = useCallback(() => {
|
|
13416
|
+
diffSeq.current += 1
|
|
13417
|
+
diffLast.current = { key: '', at: 0 }
|
|
13418
|
+
setPatch('')
|
|
13419
|
+
setPatchLoading(false)
|
|
13420
|
+
setDiffContext(null)
|
|
13421
|
+
}, [])
|
|
13173
13422
|
/* ---- multi-select (issue #13): the change list's selection lives here
|
|
13174
13423
|
so the right-click menu, the keyboard and the selection bar act on one
|
|
13175
13424
|
set; the branch tree and the history keep theirs closer to their rows
|
|
@@ -13203,6 +13452,10 @@ window.__ModuleLoader__.load({
|
|
|
13203
13452
|
const onChangeSelection = useCallback((update) => { setChangeSelection(update); clearPicksExcept('changes') }, [clearPicksExcept])
|
|
13204
13453
|
const onCommitPicks = useCallback((update) => { setPickedCommits(update); clearPicksExcept('history') }, [clearPicksExcept])
|
|
13205
13454
|
const [menu, setMenu] = useState(null)
|
|
13455
|
+
/* The header repo/branch switchers need a search box and a branch tree,
|
|
13456
|
+
which a plain ContextMenu item list cannot express; they share the
|
|
13457
|
+
same anchored surface through its `body` prop. */
|
|
13458
|
+
const [picker, setPicker] = useState(null)
|
|
13206
13459
|
const [dialog, setDialog] = useState(null)
|
|
13207
13460
|
const [tick, setTick] = useState(0)
|
|
13208
13461
|
const [favTick, setFavTick] = useState(0)
|
|
@@ -13818,11 +14071,10 @@ window.__ModuleLoader__.load({
|
|
|
13818
14071
|
headRef.current = null
|
|
13819
14072
|
setDetail(null)
|
|
13820
14073
|
setSelectedHash(null)
|
|
13821
|
-
|
|
13822
|
-
setDiffContext(null)
|
|
14074
|
+
resetDiff()
|
|
13823
14075
|
setView('history')
|
|
13824
14076
|
setPathFilter('')
|
|
13825
|
-
}, [sessionId])
|
|
14077
|
+
}, [sessionId, resetDiff])
|
|
13826
14078
|
|
|
13827
14079
|
/* The composer status pill and this panel are separate trees; when the
|
|
13828
14080
|
pill (or any other seat) switches the repository or the branch, this
|
|
@@ -13849,16 +14101,24 @@ window.__ModuleLoader__.load({
|
|
|
13849
14101
|
}, [sessionId, repoRoot, pickRepo, refresh])
|
|
13850
14102
|
|
|
13851
14103
|
const openDiff = useCallback(async (seed) => {
|
|
14104
|
+
const key = JSON.stringify([base, seed])
|
|
14105
|
+
const now = Date.now()
|
|
14106
|
+
if (key === diffLast.current.key && now - diffLast.current.at < 400) return
|
|
14107
|
+
diffLast.current = { key: key, at: now }
|
|
14108
|
+
const run = diffSeq.current + 1
|
|
14109
|
+
diffSeq.current = run
|
|
13852
14110
|
setPatchLoading(true)
|
|
13853
14111
|
setPatch('')
|
|
13854
14112
|
try {
|
|
13855
14113
|
const data = await request('diff', Object.assign({}, base, seed))
|
|
14114
|
+
if (diffSeq.current !== run) return
|
|
13856
14115
|
setPatch(data.patch)
|
|
13857
14116
|
} catch (caught) {
|
|
14117
|
+
if (diffSeq.current !== run) return
|
|
13858
14118
|
setErrorCode(null)
|
|
13859
14119
|
setError(caught instanceof Error ? caught.message : String(caught))
|
|
13860
14120
|
} finally {
|
|
13861
|
-
setPatchLoading(false)
|
|
14121
|
+
if (diffSeq.current === run) setPatchLoading(false)
|
|
13862
14122
|
}
|
|
13863
14123
|
}, [base])
|
|
13864
14124
|
|
|
@@ -13939,6 +14199,11 @@ window.__ModuleLoader__.load({
|
|
|
13939
14199
|
file menu, working-diff shortcut): external first, inline fallback. */
|
|
13940
14200
|
const showDiff = useCallback((seed) => {
|
|
13941
14201
|
if (previewDiff(seed) === true) return
|
|
14202
|
+
/* The pane header names the file; its close button clears the selection
|
|
14203
|
+
again, so the inline path owns selectedPath here too. */
|
|
14204
|
+
setSelectedPath(seed.path === undefined ? null : seed.path)
|
|
14205
|
+
/* Compact chrome: the diff lives in a tab, so opening a file shows it. */
|
|
14206
|
+
setSplitTab('diff')
|
|
13942
14207
|
if (seed.hash !== undefined && seed.hash !== null) {
|
|
13943
14208
|
setDiffContext({ kind: 'commit', hash: seed.hash, subject: seed.subject || '' })
|
|
13944
14209
|
void openDiff({ hash: seed.hash, path: seed.path })
|
|
@@ -13951,8 +14216,7 @@ window.__ModuleLoader__.load({
|
|
|
13951
14216
|
const selectCommit = useCallback(async (commit) => {
|
|
13952
14217
|
setSelectedHash(commit.hash)
|
|
13953
14218
|
setSelectedPath(null)
|
|
13954
|
-
|
|
13955
|
-
setDiffContext(null)
|
|
14219
|
+
resetDiff()
|
|
13956
14220
|
setView('detail')
|
|
13957
14221
|
setDetail(null)
|
|
13958
14222
|
try {
|
|
@@ -13961,7 +14225,7 @@ window.__ModuleLoader__.load({
|
|
|
13961
14225
|
setErrorCode(null)
|
|
13962
14226
|
setError(caught instanceof Error ? caught.message : String(caught))
|
|
13963
14227
|
}
|
|
13964
|
-
}, [base])
|
|
14228
|
+
}, [base, resetDiff])
|
|
13965
14229
|
|
|
13966
14230
|
const checkout = useCallback(async (entry) => {
|
|
13967
14231
|
if (entry.tag === true) { void run('checkout', { branch: entry.name }); return }
|
|
@@ -13988,6 +14252,7 @@ window.__ModuleLoader__.load({
|
|
|
13988
14252
|
const openMenuAt = useCallback((event, items) => {
|
|
13989
14253
|
const element = hostRef.current
|
|
13990
14254
|
const rect = element === null ? null : element.getBoundingClientRect()
|
|
14255
|
+
setPicker(null)
|
|
13991
14256
|
setMenu({
|
|
13992
14257
|
x: event.clientX - (rect === null ? 0 : rect.left),
|
|
13993
14258
|
y: event.clientY - (rect === null ? 0 : rect.top),
|
|
@@ -13997,6 +14262,22 @@ window.__ModuleLoader__.load({
|
|
|
13997
14262
|
})
|
|
13998
14263
|
}, [])
|
|
13999
14264
|
|
|
14265
|
+
/* Same anchor math as openMenuAt, but the body is a SearchPicker instead
|
|
14266
|
+
of a fixed item list. Keeping it separate means the generic context
|
|
14267
|
+
menus do not grow search state they never use. */
|
|
14268
|
+
const openPickerAt = useCallback((event, kind) => {
|
|
14269
|
+
const element = hostRef.current
|
|
14270
|
+
const rect = element === null ? null : element.getBoundingClientRect()
|
|
14271
|
+
setMenu(null)
|
|
14272
|
+
setPicker({
|
|
14273
|
+
kind: kind,
|
|
14274
|
+
x: event.clientX - (rect === null ? 0 : rect.left),
|
|
14275
|
+
y: event.clientY - (rect === null ? 0 : rect.top),
|
|
14276
|
+
boundsWidth: rect === null ? 0 : Math.round(rect.width),
|
|
14277
|
+
boundsHeight: rect === null ? 0 : Math.round(rect.height),
|
|
14278
|
+
})
|
|
14279
|
+
}, [])
|
|
14280
|
+
|
|
14000
14281
|
/* ---------- toast plumbing ---------- */
|
|
14001
14282
|
/* Declared BEFORE the context menus on purpose: the send-to-chat bindings
|
|
14002
14283
|
and every menu builder close over pushToast, and a hooks dependency
|
|
@@ -14712,7 +14993,13 @@ window.__ModuleLoader__.load({
|
|
|
14712
14993
|
|
|
14713
14994
|
/* ---------- shared pieces ---------- */
|
|
14714
14995
|
|
|
14715
|
-
|
|
14996
|
+
/* The scanned row keeps the branch it had when the workspace was
|
|
14997
|
+
listed; after a checkout the branch pill and the tree move on, so the
|
|
14998
|
+
repo label must follow the live HEAD instead of naming the old
|
|
14999
|
+
branch. */
|
|
15000
|
+
const repoOptions = repoState === null ? [] : repoState.repos.map((repo) => (
|
|
15001
|
+
repo.path === repoRoot && branches !== null ? Object.assign({}, repo, { branch: branches.branch }) : repo
|
|
15002
|
+
))
|
|
14716
15003
|
/* A linked worktree opened from the worktree view is a repository root
|
|
14717
15004
|
the workspace scan never listed. Without a header entry the top bar
|
|
14718
15005
|
kept naming the primary checkout while every branch and change below
|
|
@@ -14734,27 +15021,12 @@ window.__ModuleLoader__.load({
|
|
|
14734
15021
|
popovers reuse openMenuAt, so they clamp to the panel box and share
|
|
14735
15022
|
the opaque, frosted menu surface instead of the OS select popup. */
|
|
14736
15023
|
const openRepoMenu = useCallback((event) => {
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
icon: 'folder',
|
|
14740
|
-
tone: 'accent',
|
|
14741
|
-
label: repo.name + (repo.branch === null || repo.branch === undefined ? '' : ' · ' + repo.branch),
|
|
14742
|
-
active: repo.path === repoRoot,
|
|
14743
|
-
run: () => { if (repo.path !== repoRoot) pickRepo(repo.path) },
|
|
14744
|
-
})))
|
|
14745
|
-
}, [openMenuAt, headerRepos, repoRoot, pickRepo])
|
|
15024
|
+
openPickerAt(event, 'repo')
|
|
15025
|
+
}, [openPickerAt])
|
|
14746
15026
|
|
|
14747
15027
|
const openBranchSwitchMenu = useCallback((event) => {
|
|
14748
|
-
|
|
14749
|
-
|
|
14750
|
-
id: 'switch:' + entry.name,
|
|
14751
|
-
icon: entry.head === true ? 'star' : 'branch',
|
|
14752
|
-
tone: entry.head === true ? 'warn' : 'accent',
|
|
14753
|
-
label: entry.name,
|
|
14754
|
-
active: entry.head === true,
|
|
14755
|
-
run: () => { if (entry.head !== true) void checkout(entry) },
|
|
14756
|
-
})))
|
|
14757
|
-
}, [openMenuAt, branches, checkout])
|
|
15028
|
+
openPickerAt(event, 'branch')
|
|
15029
|
+
}, [openPickerAt])
|
|
14758
15030
|
|
|
14759
15031
|
const topBar = E('div', { className: 'dig-topbar' },
|
|
14760
15032
|
E(RepoSelect, {
|
|
@@ -14815,7 +15087,7 @@ window.__ModuleLoader__.load({
|
|
|
14815
15087
|
const historyPane = view === 'detail'
|
|
14816
15088
|
? E(CommitDetail, {
|
|
14817
15089
|
t: t, detail: detail, selectedPath: selectedPath,
|
|
14818
|
-
onBack: () => { setView('history'); setSelectedPath(null);
|
|
15090
|
+
onBack: () => { setView('history'); setSelectedPath(null); resetDiff() },
|
|
14819
15091
|
/* Plain click only highlights the file; the DOUBLE-click opens its
|
|
14820
15092
|
diff (v0.18.3, one click model across every list). */
|
|
14821
15093
|
onSelectFile: (file) => { setSelectedPath(file.path) },
|
|
@@ -14846,11 +15118,20 @@ window.__ModuleLoader__.load({
|
|
|
14846
15118
|
request: request,
|
|
14847
15119
|
})
|
|
14848
15120
|
|
|
14849
|
-
|
|
15121
|
+
/* The compact chrome renders the diff INSTEAD of the changes list (the
|
|
15122
|
+
narrow right column has no neighbour to show it beside), so it needs a
|
|
15123
|
+
filling variant; every other chrome keeps the 55%-tall split pane. */
|
|
15124
|
+
const diffPaneOf = (fill) => patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane' + (fill === true ? ' dig-diff-pane-fill' : ''), 'data-pane': 'diff', style: fill === true ? null : paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
14850
15125
|
E('div', { className: 'dig-diff-head' },
|
|
14851
15126
|
E('span', { className: 'dig-mono dig-diff-path' }, selectedPath === null ? '' : selectedPath),
|
|
14852
|
-
E('button', {
|
|
15127
|
+
E('button', {
|
|
15128
|
+
type: 'button',
|
|
15129
|
+
className: 'dig-icon-btn',
|
|
15130
|
+
title: t('toast.close'),
|
|
15131
|
+
onClick: () => { resetDiff(); setSelectedPath(null); setSplitTab('history') },
|
|
15132
|
+
}, E(Icon, { name: 'close', size: 12 }))),
|
|
14853
15133
|
E(DiffBody, { patch: patch, loading: patchLoading, t: t }))
|
|
15134
|
+
const diffPane = diffPaneOf(false)
|
|
14854
15135
|
|
|
14855
15136
|
/* ---------- rail (IDE-style left action strip) ---------- */
|
|
14856
15137
|
|
|
@@ -15226,7 +15507,7 @@ window.__ModuleLoader__.load({
|
|
|
15226
15507
|
else if (spec.id === 'tree') { entry.active = treeOpen; entry.run = () => foldTree(!treeOpen) }
|
|
15227
15508
|
else if (spec.id === 'worktrees') {
|
|
15228
15509
|
entry.active = view === 'worktrees'
|
|
15229
|
-
entry.run = () => {
|
|
15510
|
+
entry.run = () => { resetDiff(); setView(view === 'worktrees' ? 'history' : 'worktrees') }
|
|
15230
15511
|
}
|
|
15231
15512
|
else if (spec.id === 'float') {
|
|
15232
15513
|
// Label AND icon follow where the panel actually is: floating panels get
|
|
@@ -15372,9 +15653,56 @@ window.__ModuleLoader__.load({
|
|
|
15372
15653
|
const worktrees = summary !== null && Array.isArray(summary.worktrees) ? summary.worktrees : []
|
|
15373
15654
|
|
|
15374
15655
|
/* The history↔diff separator only exists while a diff is on screen, and it
|
|
15375
|
-
lives inside the middle pane — the diff is the pane below it.
|
|
15656
|
+
lives inside the middle pane — the diff is the pane below it. Only the
|
|
15657
|
+
three-column chrome keeps that split (v0.26.1). */
|
|
15376
15658
|
const diffGutter = diffPane === null ? null : gutter('diff', 'next')
|
|
15377
15659
|
|
|
15660
|
+
/* v0.26.1 — the stacked chromes (stack and compact) show the history list
|
|
15661
|
+
and the diff in ONE tabbed window below the changes list: clicking a
|
|
15662
|
+
file flips that window to the diff tab instead of splitting the diff out
|
|
15663
|
+
and squeezing the history. `withCount` only matters on compact, whose
|
|
15664
|
+
changes head (and with it the "changes {n}" badge) is hidden; `withTree`
|
|
15665
|
+
adds the tree toggle that chrome has no other way to reach. */
|
|
15666
|
+
const noDiff = patch === '' && patchLoading === false
|
|
15667
|
+
const fillDiff = diffPaneOf(true)
|
|
15668
|
+
const splitTabs = (withCount, withTree) => E('div', { className: 'dig-split-tabs' },
|
|
15669
|
+
withCount === true ? E('span', { className: 'dig-compact-count' }, fill(t('seg.changes'), { n: dirty })) : null,
|
|
15670
|
+
E('button', {
|
|
15671
|
+
type: 'button',
|
|
15672
|
+
className: 'dig-tab',
|
|
15673
|
+
'data-active': splitTab === 'history' ? 'true' : 'false',
|
|
15674
|
+
'data-action': 'history-tab',
|
|
15675
|
+
onClick: () => setSplitTab('history'),
|
|
15676
|
+
}, t('seg.history')),
|
|
15677
|
+
E('button', {
|
|
15678
|
+
type: 'button',
|
|
15679
|
+
className: 'dig-tab',
|
|
15680
|
+
'data-active': splitTab === 'diff' ? 'true' : 'false',
|
|
15681
|
+
'data-action': 'diff-tab',
|
|
15682
|
+
disabled: noDiff,
|
|
15683
|
+
title: noDiff ? t('diff.empty') : t('diff.codeLabel'),
|
|
15684
|
+
onClick: () => setSplitTab('diff'),
|
|
15685
|
+
}, t('diff.codeLabel')),
|
|
15686
|
+
E('span', { className: 'dig-topbar-spacer' }),
|
|
15687
|
+
withTree === true ? E('button', {
|
|
15688
|
+
type: 'button',
|
|
15689
|
+
className: 'dig-icon-btn' + (treeOpen ? ' dig-icon-btn-active' : ''),
|
|
15690
|
+
'data-action': 'tree',
|
|
15691
|
+
'aria-pressed': treeOpen ? 'true' : 'false',
|
|
15692
|
+
title: t('toolbar.tree'),
|
|
15693
|
+
onClick: () => foldTree(!treeOpen),
|
|
15694
|
+
}, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' })) : null)
|
|
15695
|
+
|
|
15696
|
+
/* v0.26.3: a tabbed window keeps BOTH panes mounted and only hides the
|
|
15697
|
+
inactive one. Swapping the elements out unmounted the history list (and
|
|
15698
|
+
the commit detail) on every diff switch, which threw away its scroll
|
|
15699
|
+
position and the windowed list's state. `display:none` keeps the
|
|
15700
|
+
scrollTop (verified in Chromium). The hidden diff measures 0 tall and
|
|
15701
|
+
is dropped by measurePanes' `live > 0` filter, so pane geometry stays
|
|
15702
|
+
honest. */
|
|
15703
|
+
const diffActive = splitTab === 'diff' && fillDiff !== null
|
|
15704
|
+
const tabPane = (active, node) => node === null ? null : E('div', { className: 'dig-tab-pane' + (active === true ? '' : ' dig-tab-pane-hidden') }, node)
|
|
15705
|
+
|
|
15378
15706
|
let body
|
|
15379
15707
|
if (repoState === null) {
|
|
15380
15708
|
/* The scan is still running: a neutral loading state, never the
|
|
@@ -15398,31 +15726,19 @@ window.__ModuleLoader__.load({
|
|
|
15398
15726
|
onRemove: (tree) => setDialog({ kind: 'worktreeRemove', tree: tree, force: false }),
|
|
15399
15727
|
}))
|
|
15400
15728
|
} else if (compact) {
|
|
15729
|
+
/* v0.26.0: the changes list sits on TOP and the lower window is a tab
|
|
15730
|
+
pair (history | diff) instead of a split — opening a file no longer
|
|
15731
|
+
takes the history list away, and the changes list never disappears.
|
|
15732
|
+
The diff tab only exists while there is a patch to show. */
|
|
15401
15733
|
body = E('div', { className: 'dig-body dig-body-compact', ref: bodyRef },
|
|
15402
|
-
E('div', { className: 'dig-compact-bar' },
|
|
15403
|
-
E(Segmented, {
|
|
15404
|
-
value: view === 'detail' ? 'history' : view,
|
|
15405
|
-
onChange: (next) => { setView(next); setPatch(''); setDiffContext(null) },
|
|
15406
|
-
items: [
|
|
15407
|
-
{ id: 'changes', label: fill(t('seg.changes'), { n: dirty }) },
|
|
15408
|
-
{ id: 'history', label: t('seg.history') },
|
|
15409
|
-
],
|
|
15410
|
-
}),
|
|
15411
|
-
E('span', { className: 'dig-topbar-spacer' }),
|
|
15412
|
-
// Two-way on purpose: treeOpen is persisted, so a header button that
|
|
15413
|
-
// only rendered while the tree was OPEN left a folded tree with no way
|
|
15414
|
-
// back on this chrome (the rail now carries one too, for the others).
|
|
15415
|
-
E('button', {
|
|
15416
|
-
type: 'button',
|
|
15417
|
-
className: 'dig-icon-btn' + (treeOpen ? ' dig-icon-btn-active' : ''),
|
|
15418
|
-
'data-action': 'tree',
|
|
15419
|
-
'aria-pressed': treeOpen ? 'true' : 'false',
|
|
15420
|
-
title: t('toolbar.tree'),
|
|
15421
|
-
onClick: () => foldTree(!treeOpen),
|
|
15422
|
-
}, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
|
|
15423
15734
|
treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
|
|
15424
15735
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
15425
|
-
|
|
15736
|
+
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
15737
|
+
gutter('changes', 'prev'),
|
|
15738
|
+
E('div', { className: 'dig-pane dig-pane-main' },
|
|
15739
|
+
splitTabs(true, true),
|
|
15740
|
+
tabPane(diffActive === false, historyPane),
|
|
15741
|
+
fillDiff === null ? null : tabPane(diffActive, fillDiff)))
|
|
15426
15742
|
} else if (columns) {
|
|
15427
15743
|
body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
|
|
15428
15744
|
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBrowse: browseBranch, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues, onPicksChange: onBranchPicks, clearSignal: pickEpochs.tree })) : null,
|
|
@@ -15436,7 +15752,13 @@ window.__ModuleLoader__.load({
|
|
|
15436
15752
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
15437
15753
|
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
15438
15754
|
gutter('changes', 'prev'),
|
|
15439
|
-
|
|
15755
|
+
/* v0.26.1: below the commit box the history list and the diff share
|
|
15756
|
+
one tabbed window — a click on a changed file flips to the diff tab
|
|
15757
|
+
and leaves the list (and its scroll position) alone. */
|
|
15758
|
+
E('div', { className: 'dig-pane dig-pane-main' },
|
|
15759
|
+
splitTabs(false, false),
|
|
15760
|
+
tabPane(diffActive === false, historyPane),
|
|
15761
|
+
fillDiff === null ? null : tabPane(diffActive, fillDiff)))
|
|
15440
15762
|
}
|
|
15441
15763
|
|
|
15442
15764
|
/* ---------- overlays ---------- */
|
|
@@ -15452,6 +15774,37 @@ window.__ModuleLoader__.load({
|
|
|
15452
15774
|
onClose: () => setMenu(null),
|
|
15453
15775
|
}))
|
|
15454
15776
|
}
|
|
15777
|
+
if (picker !== null) {
|
|
15778
|
+
/* The panel header's repo/branch switchers share the ContextMenu
|
|
15779
|
+
surface (same clamping and outside-click), but their body is the
|
|
15780
|
+
searchable picker instead of an item list. */
|
|
15781
|
+
const branchList = branches === null ? [] : branches.local
|
|
15782
|
+
overlays.push(E(ContextMenu, {
|
|
15783
|
+
key: 'picker',
|
|
15784
|
+
anchor: { x: picker.x, y: picker.y },
|
|
15785
|
+
boundsWidth: picker.boundsWidth,
|
|
15786
|
+
boundsHeight: picker.boundsHeight,
|
|
15787
|
+
items: [],
|
|
15788
|
+
body: E(SearchPicker, {
|
|
15789
|
+
key: picker.kind,
|
|
15790
|
+
mode: picker.kind,
|
|
15791
|
+
t: t,
|
|
15792
|
+
repos: picker.kind === 'repo' ? headerRepos : [],
|
|
15793
|
+
branches: picker.kind === 'branch' ? branchList : [],
|
|
15794
|
+
current: picker.kind === 'repo' ? repoRoot : (branches === null ? '' : branches.branch),
|
|
15795
|
+
onPick: (value, row) => {
|
|
15796
|
+
setPicker(null)
|
|
15797
|
+
if (picker.kind === 'repo') {
|
|
15798
|
+
if (value !== repoRoot) pickRepo(value)
|
|
15799
|
+
return
|
|
15800
|
+
}
|
|
15801
|
+
if (row.head !== true) void checkout(row)
|
|
15802
|
+
},
|
|
15803
|
+
onClose: () => setPicker(null),
|
|
15804
|
+
}),
|
|
15805
|
+
onClose: () => setPicker(null),
|
|
15806
|
+
}))
|
|
15807
|
+
}
|
|
15455
15808
|
if (railSettings === true) {
|
|
15456
15809
|
overlays.push(E(RailSettings, {
|
|
15457
15810
|
key: 'rail-settings',
|
|
@@ -15952,7 +16305,21 @@ window.__ModuleLoader__.load({
|
|
|
15952
16305
|
'.dig-pane-main{flex:1;min-width:0}',
|
|
15953
16306
|
'.dig-pane-changes{width:290px;flex:none;border-left:1px solid var(--dsw-alias-border-l2)}',
|
|
15954
16307
|
'.dig-pane-changes-stack{max-height:46%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
15955
|
-
|
|
16308
|
+
/* v0.26.1: both stacked chromes show history and diff in one tabbed
|
|
16309
|
+
window; the strip is the window's tab bar (the compact chrome also
|
|
16310
|
+
hosts the changes count and the tree toggle in it). */
|
|
16311
|
+
'.dig-split-tabs{display:flex;align-items:center;gap:4px;padding:4px 6px;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
16312
|
+
/* v0.26.3: the tabbed window's bodies stay mounted; the inactive one is
|
|
16313
|
+
hidden so the history list keeps its scroll position and the detail
|
|
16314
|
+
keeps its expanded state. */
|
|
16315
|
+
'.dig-tab-pane{display:flex;flex-direction:column;flex:1;min-height:0;min-width:0}',
|
|
16316
|
+
'.dig-tab-pane-hidden{display:none}',
|
|
16317
|
+
'.dig-compact-count{color:var(--dsw-alias-label-secondary);font-weight:500;font-size:11px;padding:0 2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:45%}',
|
|
16318
|
+
'.dig-tab{border:none;background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:11px;font-weight:500;padding:2px 10px;border-radius:var(--dsw-radius-sm,8px);cursor:pointer;white-space:nowrap;flex:none}',
|
|
16319
|
+
'.dig-tab:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
16320
|
+
'.dig-tab[data-active="true"]{background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1));color:var(--dsw-alias-label-primary)}',
|
|
16321
|
+
'.dig-tab:disabled{opacity:.45;cursor:default}',
|
|
16322
|
+
'.dig-tab:disabled:hover{background:transparent;color:var(--dsw-alias-label-secondary)}',
|
|
15956
16323
|
'.dig-compact-tree{max-height:42%;flex:none;border-bottom:1px solid var(--dsw-alias-border-l2)}',
|
|
15957
16324
|
// A divider: an 8px hit area (a fingertip needs it) painted as a 1px hairline
|
|
15958
16325
|
// that lights up on hover, focus or while dragging. touch-action:none is what
|
|
@@ -15986,6 +16353,29 @@ window.__ModuleLoader__.load({
|
|
|
15986
16353
|
'.dig-picker-name{flex:none;font-weight:600}',
|
|
15987
16354
|
'.dig-picker-kind{color:var(--dsw-alias-label-tertiary);flex:none}',
|
|
15988
16355
|
'.dig-picker-path{color:var(--dsw-alias-label-tertiary);margin-left:auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:50%}',
|
|
16356
|
+
/* v0.27 searchable repo / branch picker (panel header + composer pill).
|
|
16357
|
+
The list scrolls, the search field stays pinned on top. */
|
|
16358
|
+
'.dig-pick{display:flex;flex-direction:column;gap:2px;width:300px;max-width:100%;min-height:0;color:var(--dsw-alias-label-primary)}',
|
|
16359
|
+
'.dig-pick-search{position:sticky;top:0;z-index:1;padding:2px 2px 6px;background:var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1))}',
|
|
16360
|
+
'.dig-pick-list{display:flex;flex-direction:column;gap:1px;overflow:auto;min-height:0;max-height:300px;overscroll-behavior:contain}',
|
|
16361
|
+
'.dig-pick-row{display:flex;align-items:center;gap:6px;width:100%;padding:4px 8px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:inherit;font:inherit;font-size:13px;text-align:left;cursor:pointer;min-width:0}',
|
|
16362
|
+
'.dig-pick-row:hover:not(:disabled),.dig-pick-folder:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
16363
|
+
'.dig-pick-row-active{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
16364
|
+
'.dig-pick-row-current{color:var(--dsw-alias-brand-primary)}',
|
|
16365
|
+
'.dig-pick-row-disabled{opacity:.45;cursor:default}',
|
|
16366
|
+
'.dig-pick-row-head{font-weight:600}',
|
|
16367
|
+
'.dig-pick-folder{display:flex;align-items:center;gap:5px;width:100%;padding:3px 8px;border:none;border-radius:var(--dsw-radius-sm,8px);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;font-size:12px;text-align:left;cursor:pointer;min-width:0}',
|
|
16368
|
+
'.dig-pick-chevron{display:inline-flex;flex:none;transition:transform .12s ease;opacity:.6}',
|
|
16369
|
+
'.dig-pick-chevron-open{transform:rotate(90deg)}',
|
|
16370
|
+
'.dig-pick-main{display:flex;flex-direction:column;min-width:0;flex:1}',
|
|
16371
|
+
'.dig-pick-name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
16372
|
+
'.dig-pick-sub{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--dsw-alias-label-tertiary);font-size:10px;line-height:12px}',
|
|
16373
|
+
'.dig-pick-icon{display:inline-flex;flex:none}',
|
|
16374
|
+
'.dig-pick-badge{margin-left:auto;flex:none;max-width:90px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 6px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary);font-size:10px;line-height:16px}',
|
|
16375
|
+
'.dig-pick-note{flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px}',
|
|
16376
|
+
'.dig-pick-count{margin-left:auto;flex:none;color:var(--dsw-alias-label-tertiary);font-size:11px;font-variant-numeric:tabular-nums}',
|
|
16377
|
+
'.dig-pick-empty{padding:10px 8px;color:var(--dsw-alias-label-tertiary);font-size:12px}',
|
|
16378
|
+
'.dig-cs-menu.dig-cs-picker{width:300px;min-width:260px;max-width:340px;max-height:360px;padding:4px;overflow:hidden}',
|
|
15989
16379
|
/* position:relative: the marquee box paints against this wrapper (the
|
|
15990
16380
|
scroll container is offset by the search field — same rule as
|
|
15991
16381
|
.dig-history above). */
|
|
@@ -16008,6 +16398,11 @@ window.__ModuleLoader__.load({
|
|
|
16008
16398
|
'.dig-folder-head:hover{color:var(--dsw-alias-label-primary)}',
|
|
16009
16399
|
'.dig-folder-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}',
|
|
16010
16400
|
'.dig-count{margin-left:auto;opacity:.6;font-weight:400}',
|
|
16401
|
+
/* v0.26.2: the changes head is the one count that belongs to the WORD, not
|
|
16402
|
+
to the right edge — .dig-count pushes itself to the far right (fine for the
|
|
16403
|
+
branch groups), so the changes header opts back in to sitting right after
|
|
16404
|
+
its title (user report: the number floated in the middle of the row). */
|
|
16405
|
+
'.dig-changes-head .dig-count{margin-left:0}',
|
|
16011
16406
|
'.dig-chevron{display:inline-flex;transform:rotate(-90deg);transition:transform .12s ease}',
|
|
16012
16407
|
'.dig-chevron-open{transform:rotate(0deg)}',
|
|
16013
16408
|
'.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
|
|
@@ -16060,7 +16455,10 @@ window.__ModuleLoader__.load({
|
|
|
16060
16455
|
filter bar, and painting from the wrong rect was the v0.19.2 drift. */
|
|
16061
16456
|
'.dig-history{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
16062
16457
|
'.dig-history-scroll{flex:1;overflow:auto}',
|
|
16063
|
-
'
|
|
16458
|
+
// The row's left edge follows the bars above it (filter row / compact
|
|
16459
|
+
// switch: 4px 6px padding), so the time column and the graph start on the
|
|
16460
|
+
// same inset as the input box instead of flush against the pane edge.
|
|
16461
|
+
'.dig-commit{display:flex;align-items:center;gap:6px;padding:1px 8px 1px 6px;cursor:pointer;height:26px;overflow:hidden}',
|
|
16064
16462
|
'.dig-commit:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
16065
16463
|
/* Same theme-aware gray mix as .dig-row-selected (see it). */
|
|
16066
16464
|
'.dig-commit-selected{background:color-mix(in srgb,var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover)) 58%,var(--dsw-static-neutral-bluish-75,var(--dsw-alias-bg-layer-3)) 42%);color:var(--dsw-alias-label-primary);box-shadow:inset 2px 0 0 var(--dsw-alias-brand-primary)}',
|
|
@@ -16171,6 +16569,7 @@ window.__ModuleLoader__.load({
|
|
|
16171
16569
|
'.dig-detail-files{display:flex;flex-direction:column}',
|
|
16172
16570
|
'.dig-mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}',
|
|
16173
16571
|
'.dig-diff-pane{flex:none;max-height:55%;display:flex;flex-direction:column;min-height:0;border-top:1px solid var(--dsw-alias-border-l2)}',
|
|
16572
|
+
'.dig-diff-pane-fill{flex:1;max-height:none;border-top:none}',
|
|
16174
16573
|
'.dig-diff-head{display:flex;align-items:center;gap:8px;padding:3px 8px;color:var(--dsw-alias-label-tertiary);flex:none;min-width:0}',
|
|
16175
16574
|
'.dig-diff-path{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}',
|
|
16176
16575
|
'.dig-diff{flex:1;overflow:auto;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:calc(12px + var(--dsh-code-font-delta, 0px));line-height:1.5;font-weight:500;min-height:0}',
|
|
@@ -16422,6 +16821,13 @@ window.__ModuleLoader__.load({
|
|
|
16422
16821
|
'.dig-wt-label{color:var(--dsw-alias-label-secondary);font-size:12px;font-weight:500}',
|
|
16423
16822
|
'.dig-wt-error{padding:6px 8px;margin-bottom:8px;border-radius:var(--dsw-radius-sm,8px);background:rgba(220,38,38,.12);color:var(--dsw-alias-state-error-primary);font-size:11px;line-height:16px;word-break:break-word}',
|
|
16424
16823
|
'body[data-ds-dark-theme] .dig-wt-error{background:rgba(248,113,113,.16)}',
|
|
16824
|
+
/* ---- icon tooltip ---- */
|
|
16825
|
+
/* A body child, not a panel child: the bottom workbench declares
|
|
16826
|
+
`contain: layout style`, so a fixed element inside it would take the
|
|
16827
|
+
panel box as its containing block and land off-screen (invariant 13).
|
|
16828
|
+
Styled from the same menu tokens so it reads as part of the product. */
|
|
16829
|
+
'.dig-tip{position:fixed;z-index:9999;display:none;box-sizing:border-box;max-width:280px;padding:5px 8px;border-radius:var(--dsw-radius-sm,8px);background:var(--dsw-specific-menu,var(--dsw-alias-bg-layer-2,var(--dsw-alias-bg-layer-1)));color:var(--dsw-alias-label-primary);box-shadow:var(--dsw-elevation-prominent,0 8px 22px rgba(0,0,0,.32));font-family:var(--dsw-font-family,system-ui,sans-serif);font-size:calc(11px + var(--dsh-content-font-delta-secondary,0px));font-weight:500;line-height:1.45;text-align:left;white-space:normal;overflow-wrap:anywhere;pointer-events:none}',
|
|
16830
|
+
'.dig-tip[data-show="true"]{display:block}',
|
|
16425
16831
|
].concat(LANE_TRACE_RULES).join('\n')
|
|
16426
16832
|
|
|
16427
16833
|
/* ============================== plugin ============================== */
|
|
@@ -17168,50 +17574,48 @@ window.__ModuleLoader__.load({
|
|
|
17168
17574
|
behind > 0 ? E('span', { className: 'dig-cs-badge dig-cs-behind', title: t('composer.behindHint') }, '↓' + String(behind)) : null,
|
|
17169
17575
|
ahead > 0 ? E('span', { className: 'dig-cs-badge dig-cs-ahead', title: t('composer.aheadHint') }, '↑' + String(ahead)) : null,
|
|
17170
17576
|
error !== '' ? E('span', { className: 'dig-cs-error', title: error }, '×') : null,
|
|
17171
|
-
menu === 'repo' ? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
|
|
17577
|
+
menu === 'repo' ? E('div', { className: 'dig-cs-menu dig-cs-picker', 'data-cordis-panel': '' },
|
|
17172
17578
|
/* v0.19.2: one row per checkout — the repository's own name first
|
|
17173
17579
|
(origin URL basename, falling back to the directory name), and the
|
|
17174
17580
|
single-level directory name as a small sub line only when the two
|
|
17175
17581
|
differ (two clones of one repository stay tellable apart). The
|
|
17176
|
-
full path lives in the tooltip, not in the row.
|
|
17177
|
-
|
|
17178
|
-
|
|
17179
|
-
|
|
17180
|
-
|
|
17181
|
-
|
|
17182
|
-
|
|
17183
|
-
|
|
17184
|
-
|
|
17185
|
-
|
|
17186
|
-
|
|
17187
|
-
|
|
17188
|
-
|
|
17189
|
-
|
|
17190
|
-
|
|
17191
|
-
|
|
17582
|
+
full path lives in the tooltip, not in the row. v0.27 adds the
|
|
17583
|
+
search field so seven clones need no scrolling to find one. */
|
|
17584
|
+
E(SearchPicker, {
|
|
17585
|
+
key: 'composer-repo',
|
|
17586
|
+
mode: 'repo',
|
|
17587
|
+
t: t,
|
|
17588
|
+
repos: data.repos.map((row) => {
|
|
17589
|
+
const dirName = baseNameOf(row.path)
|
|
17590
|
+
const displayName = row.origin !== undefined && row.origin !== '' ? row.origin : dirName
|
|
17591
|
+
return {
|
|
17592
|
+
path: row.path,
|
|
17593
|
+
name: displayName,
|
|
17594
|
+
sub: dirName !== displayName ? dirName : undefined,
|
|
17595
|
+
}
|
|
17596
|
+
}),
|
|
17597
|
+
current: repoRoot,
|
|
17598
|
+
onPick: (path) => pickRepoRoot(path),
|
|
17599
|
+
onClose: () => setMenu(null),
|
|
17192
17600
|
}))
|
|
17193
17601
|
: null,
|
|
17194
17602
|
menu === 'branch' ? (branchLoading === true || branchList === null
|
|
17195
17603
|
? E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' }, E('div', { className: 'dig-cs-item dig-cs-static' }, '…'))
|
|
17196
|
-
: E('div', { className: 'dig-cs-menu', 'data-cordis-panel': '' },
|
|
17197
|
-
|
|
17198
|
-
key:
|
|
17199
|
-
|
|
17200
|
-
|
|
17201
|
-
|
|
17202
|
-
|
|
17203
|
-
|
|
17204
|
-
|
|
17205
|
-
|
|
17206
|
-
|
|
17207
|
-
|
|
17208
|
-
|
|
17209
|
-
|
|
17210
|
-
|
|
17211
|
-
? E('span', { className: 'dig-cs-item-counts' },
|
|
17212
|
-
entry.ahead > 0 ? E('span', { className: 'dig-cs-ahead' }, '↑' + String(entry.ahead)) : null,
|
|
17213
|
-
entry.behind > 0 ? E('span', { className: 'dig-cs-behind' }, '↓' + String(entry.behind)) : null)
|
|
17214
|
-
: null))))
|
|
17604
|
+
: E('div', { className: 'dig-cs-menu dig-cs-picker', 'data-cordis-panel': '' },
|
|
17605
|
+
E(SearchPicker, {
|
|
17606
|
+
key: 'composer-branch',
|
|
17607
|
+
mode: 'branch',
|
|
17608
|
+
t: t,
|
|
17609
|
+
branches: branchList.local,
|
|
17610
|
+
current: branchName === null ? '' : branchName,
|
|
17611
|
+
busyName: checkingOut,
|
|
17612
|
+
worktreeNote: t('composer.inWorktree'),
|
|
17613
|
+
onPick: (name, entry) => {
|
|
17614
|
+
if (entry.head === true) { setMenu(null); return }
|
|
17615
|
+
void checkoutBranch(name)
|
|
17616
|
+
},
|
|
17617
|
+
onClose: () => setMenu(null),
|
|
17618
|
+
})))
|
|
17215
17619
|
: null,
|
|
17216
17620
|
)
|
|
17217
17621
|
}
|
|
@@ -17518,6 +17922,193 @@ window.__ModuleLoader__.load({
|
|
|
17518
17922
|
}))))
|
|
17519
17923
|
}
|
|
17520
17924
|
|
|
17925
|
+
/* ---- icon tooltips ----
|
|
17926
|
+
Every icon action already carries its name in `title` (the rail adds the
|
|
17927
|
+
disabled reason; the toolbar / mini / pill buttons carry the action
|
|
17928
|
+
verb). Native title bubbles are the wrong surface for this product: the
|
|
17929
|
+
bottom workbench is a dense horizontal strip, the bubble needs a full
|
|
17930
|
+
second of rest, it is drawn in the browser's own styling, and over a
|
|
17931
|
+
DISABLED rail button it never appears at all. Draw one themed card
|
|
17932
|
+
instead, shared by every seat (panel, composer pill, diff preview) and
|
|
17933
|
+
hung off <body> — see the `.dig-tip` note in CSS for why it can't live
|
|
17934
|
+
inside the panel. The card mirrors the anchor's own `title`, which is
|
|
17935
|
+
removed while the card is up so the native bubble never doubles it. */
|
|
17936
|
+
const TIP_ID = 'dsh-ide-git-tip'
|
|
17937
|
+
const TIP_DELAY_MS = 120
|
|
17938
|
+
const TIP_GAP = 6
|
|
17939
|
+
/* Only this plugin's own surfaces explain themselves. `.dig-root` is the
|
|
17940
|
+
panel (native and better-sidebar seats alike), `.dig-cs-root` the
|
|
17941
|
+
composer status pill, `.dig-preview-root` the external diff seat. */
|
|
17942
|
+
const TIP_ROOTS = '.dig-root, .dig-cs-root, .dig-preview-root'
|
|
17943
|
+
|
|
17944
|
+
function installHoverTips() {
|
|
17945
|
+
/* The SSR self-check drives apply() with a minimal document stub: it has
|
|
17946
|
+
no body, no id lookup and no event targets. Those environments get the
|
|
17947
|
+
no-op installer rather than a throw during plugin activation. */
|
|
17948
|
+
if (typeof document === 'undefined' || document.body == null
|
|
17949
|
+
|| typeof document.createElement !== 'function'
|
|
17950
|
+
|| typeof document.getElementById !== 'function'
|
|
17951
|
+
|| typeof document.addEventListener !== 'function'
|
|
17952
|
+
|| typeof window === 'undefined'
|
|
17953
|
+
|| typeof window.addEventListener !== 'function') return () => {}
|
|
17954
|
+
/* HMR / re-apply: never stack two cards (or two listener sets on one). */
|
|
17955
|
+
const stale = document.getElementById(TIP_ID)
|
|
17956
|
+
if (stale !== null) stale.remove()
|
|
17957
|
+
const card = document.createElement('div')
|
|
17958
|
+
card.id = TIP_ID
|
|
17959
|
+
card.className = 'dig-tip'
|
|
17960
|
+
card.setAttribute('data-dsh-ide-git', '')
|
|
17961
|
+
card.setAttribute('role', 'tooltip')
|
|
17962
|
+
card.setAttribute('aria-hidden', 'true')
|
|
17963
|
+
document.body.appendChild(card)
|
|
17964
|
+
|
|
17965
|
+
/* `anchor` is the button the pointer is on (or about to rest on, while
|
|
17966
|
+
`timer` runs); `shown` is the one whose title the card has taken. */
|
|
17967
|
+
let anchor = null
|
|
17968
|
+
let shown = null
|
|
17969
|
+
let takenTitle = null
|
|
17970
|
+
let timer = null
|
|
17971
|
+
|
|
17972
|
+
const restore = () => {
|
|
17973
|
+
if (shown === null) return
|
|
17974
|
+
/* React may have rewritten the attribute while the card was up (toggle
|
|
17975
|
+
buttons swap their copy): only put the taken text back when nothing
|
|
17976
|
+
else has claimed the slot, so a fresh label is never clobbered. */
|
|
17977
|
+
try {
|
|
17978
|
+
if (shown.getAttribute('title') === null && takenTitle !== null) shown.setAttribute('title', takenTitle)
|
|
17979
|
+
} catch (error) { void error }
|
|
17980
|
+
shown = null
|
|
17981
|
+
takenTitle = null
|
|
17982
|
+
}
|
|
17983
|
+
|
|
17984
|
+
const hide = () => {
|
|
17985
|
+
if (timer !== null) { clearTimeout(timer); timer = null }
|
|
17986
|
+
anchor = null
|
|
17987
|
+
restore()
|
|
17988
|
+
card.removeAttribute('data-show')
|
|
17989
|
+
card.setAttribute('aria-hidden', 'true')
|
|
17990
|
+
card.textContent = ''
|
|
17991
|
+
}
|
|
17992
|
+
|
|
17993
|
+
/* Viewport coordinates on a body child: no panel containment to fight. */
|
|
17994
|
+
const place = () => {
|
|
17995
|
+
const rect = anchor.getBoundingClientRect()
|
|
17996
|
+
const width = card.offsetWidth
|
|
17997
|
+
const height = card.offsetHeight
|
|
17998
|
+
const viewportW = window.innerWidth || document.documentElement.clientWidth || 0
|
|
17999
|
+
const viewportH = window.innerHeight || document.documentElement.clientHeight || 0
|
|
18000
|
+
/* The vertical action rail is a 24px column on the panel's left edge:
|
|
18001
|
+
a card under an icon there would cover the icons below it, so this one
|
|
18002
|
+
sits to its right and follows the row. (The horizontal rail uses
|
|
18003
|
+
`.dig-rail-row`, so the `closest` query only matches the vertical
|
|
18004
|
+
seat — top rows keep the below-the-button placement.) */
|
|
18005
|
+
if (anchor.closest('.dig-rail') !== null) {
|
|
18006
|
+
let left = rect.left + rect.width + TIP_GAP
|
|
18007
|
+
if (left + width > viewportW - 4) left = rect.left - TIP_GAP - width
|
|
18008
|
+
if (left < 4) left = 4
|
|
18009
|
+
let top = rect.top + (rect.height - height) / 2
|
|
18010
|
+
if (top + height > viewportH - 4) top = viewportH - height - 4
|
|
18011
|
+
if (top < 4) top = 4
|
|
18012
|
+
card.style.left = Math.round(left) + 'px'
|
|
18013
|
+
card.style.top = Math.round(top) + 'px'
|
|
18014
|
+
return
|
|
18015
|
+
}
|
|
18016
|
+
/* Below the anchor; flip above when the bottom edge would clip. */
|
|
18017
|
+
let top = rect.bottom + TIP_GAP
|
|
18018
|
+
if (top + height > viewportH - 4) top = rect.top - height - TIP_GAP
|
|
18019
|
+
if (top < 4) top = 4
|
|
18020
|
+
let left = rect.left + (rect.width - width) / 2
|
|
18021
|
+
if (left + width > viewportW - 4) left = viewportW - width - 4
|
|
18022
|
+
if (left < 4) left = 4
|
|
18023
|
+
card.style.left = Math.round(left) + 'px'
|
|
18024
|
+
card.style.top = Math.round(top) + 'px'
|
|
18025
|
+
}
|
|
18026
|
+
|
|
18027
|
+
const reveal = () => {
|
|
18028
|
+
timer = null
|
|
18029
|
+
if (anchor === null) return
|
|
18030
|
+
const label = anchor.getAttribute('title')
|
|
18031
|
+
if (label === null || label === '') { hide(); return }
|
|
18032
|
+
takenTitle = label
|
|
18033
|
+
try { anchor.removeAttribute('title') } catch (error) { void error }
|
|
18034
|
+
shown = anchor
|
|
18035
|
+
card.textContent = label
|
|
18036
|
+
card.setAttribute('data-show', 'true')
|
|
18037
|
+
card.removeAttribute('aria-hidden')
|
|
18038
|
+
place()
|
|
18039
|
+
}
|
|
18040
|
+
|
|
18041
|
+
/* Whatever sits under the pointer, not whatever received the event: a
|
|
18042
|
+
disabled button still hit-tests (rail icons), and the SVG glyph inside
|
|
18043
|
+
the button is usually the element under the cursor. `closest` walks up
|
|
18044
|
+
to the button. The label is read here, but the anchor's own copy is
|
|
18045
|
+
already taken while its card is up — so the shown button answers from
|
|
18046
|
+
the saved text, otherwise it would stop counting as a candidate the
|
|
18047
|
+
moment the card opened. */
|
|
18048
|
+
const buttonAt = (x, y) => {
|
|
18049
|
+
if (typeof document.elementFromPoint !== 'function') return null
|
|
18050
|
+
let hit = null
|
|
18051
|
+
try { hit = document.elementFromPoint(x, y) } catch (error) { void error; return null }
|
|
18052
|
+
if (hit === null || hit === undefined || typeof hit.closest !== 'function') return null
|
|
18053
|
+
if (hit.closest(TIP_ROOTS) === null) return null
|
|
18054
|
+
return hit.closest('button, [role="button"]')
|
|
18055
|
+
}
|
|
18056
|
+
|
|
18057
|
+
const labelOf = (button) => {
|
|
18058
|
+
if (button === shown) return takenTitle
|
|
18059
|
+
const label = button.getAttribute('title')
|
|
18060
|
+
return label === null || label === '' ? null : label
|
|
18061
|
+
}
|
|
18062
|
+
|
|
18063
|
+
const onPointerMove = (event) => {
|
|
18064
|
+
if (event !== null && event !== undefined && event.pointerType === 'touch') return
|
|
18065
|
+
/* A drag is not a hover: reordering rail actions, dragging a divider or
|
|
18066
|
+
selecting rows all sweep across buttons with a pressed pointer, and
|
|
18067
|
+
no card should pop up under the drag. */
|
|
18068
|
+
if (event !== null && event !== undefined && typeof event.buttons === 'number' && event.buttons !== 0) {
|
|
18069
|
+
if (anchor !== null || timer !== null || shown !== null) hide()
|
|
18070
|
+
return
|
|
18071
|
+
}
|
|
18072
|
+
const button = buttonAt(event === null || event === undefined ? 0 : event.clientX, event === null || event === undefined ? 0 : event.clientY)
|
|
18073
|
+
if (button === null || labelOf(button) === null) {
|
|
18074
|
+
if (anchor !== null || timer !== null || shown !== null) hide()
|
|
18075
|
+
return
|
|
18076
|
+
}
|
|
18077
|
+
if (button === anchor) return
|
|
18078
|
+
/* Another button: the old card (and the title it took) goes now, the
|
|
18079
|
+
new one waits out the grace period — sweeping the rail must not
|
|
18080
|
+
flash a card per icon. */
|
|
18081
|
+
if (timer !== null) { clearTimeout(timer); timer = null }
|
|
18082
|
+
restore()
|
|
18083
|
+
card.removeAttribute('data-show')
|
|
18084
|
+
anchor = button
|
|
18085
|
+
timer = setTimeout(reveal, TIP_DELAY_MS)
|
|
18086
|
+
}
|
|
18087
|
+
|
|
18088
|
+
document.addEventListener('pointermove', onPointerMove, { passive: true })
|
|
18089
|
+
document.addEventListener('pointerdown', hide, true)
|
|
18090
|
+
document.addEventListener('pointerleave', hide)
|
|
18091
|
+
document.addEventListener('wheel', hide, { passive: true, capture: true })
|
|
18092
|
+
/* Scroll and resize move the anchor under a card that cannot follow it:
|
|
18093
|
+
the card would lie about where the button is. Drop it and let the next
|
|
18094
|
+
pointer move re-open it. */
|
|
18095
|
+
window.addEventListener('scroll', hide, true)
|
|
18096
|
+
window.addEventListener('resize', hide)
|
|
18097
|
+
window.addEventListener('blur', hide)
|
|
18098
|
+
|
|
18099
|
+
return () => {
|
|
18100
|
+
hide()
|
|
18101
|
+
document.removeEventListener('pointermove', onPointerMove)
|
|
18102
|
+
document.removeEventListener('pointerdown', hide, true)
|
|
18103
|
+
document.removeEventListener('pointerleave', hide)
|
|
18104
|
+
document.removeEventListener('wheel', hide, { capture: true })
|
|
18105
|
+
window.removeEventListener('scroll', hide, true)
|
|
18106
|
+
window.removeEventListener('resize', hide)
|
|
18107
|
+
window.removeEventListener('blur', hide)
|
|
18108
|
+
card.remove()
|
|
18109
|
+
}
|
|
18110
|
+
}
|
|
18111
|
+
|
|
17521
18112
|
function apply(ctx) {
|
|
17522
18113
|
const style = document.createElement('style')
|
|
17523
18114
|
style.setAttribute('data-dsh-ide-git', '')
|
|
@@ -17525,6 +18116,11 @@ window.__ModuleLoader__.load({
|
|
|
17525
18116
|
document.head.appendChild(style)
|
|
17526
18117
|
ctx.effect(() => () => { style.remove() }, 'dsh-ide-git: panel styles')
|
|
17527
18118
|
|
|
18119
|
+
/* Icon affordances explain themselves on hover wherever this plugin
|
|
18120
|
+
draws one — the panel in any seat, the composer pill, the diff
|
|
18121
|
+
preview. One document-level card covers them all. */
|
|
18122
|
+
ctx.effect(() => installHoverTips(), 'dsh-ide-git: icon tooltips')
|
|
18123
|
+
|
|
17528
18124
|
const t = translatorOf(ctx)
|
|
17529
18125
|
|
|
17530
18126
|
/* DSH's own locale registry: bind this plugin's namespace so host-side
|