dsh-ide-git 0.16.4 → 0.18.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/dsh.plugin.json +1 -1
- package/package.json +1 -1
- package/src/client.js +1200 -59
- package/src/index.js +53 -19
package/src/client.js
CHANGED
|
@@ -122,6 +122,25 @@ window.__ModuleLoader__.load({
|
|
|
122
122
|
'changes.commit': '提交',
|
|
123
123
|
'changes.amend': '修补',
|
|
124
124
|
'changes.stageAll': '全部暂存',
|
|
125
|
+
'selection.count': '已选 {n} 项',
|
|
126
|
+
'selection.copied': '已复制 {n} 项',
|
|
127
|
+
'selection.copyPaths': '复制路径',
|
|
128
|
+
'selection.clear': '清除选择',
|
|
129
|
+
'action.copyName': '复制名称',
|
|
130
|
+
'menu.copyNamesN': '复制 {n} 个名称',
|
|
131
|
+
'menu.copyHashesN': '复制 {n} 个哈希',
|
|
132
|
+
'menu.copyPathsN': '复制 {n} 个路径',
|
|
133
|
+
'menu.stageN': '暂存 {n} 项',
|
|
134
|
+
'menu.unstageN': '取消暂存 {n} 项',
|
|
135
|
+
'menu.discardN': '还原 {n} 项…',
|
|
136
|
+
'menu.cherryN': 'Cherry-pick {n} 个提交',
|
|
137
|
+
'menu.deleteLocalN': '删除 {n} 个本地分支…',
|
|
138
|
+
'menu.deleteRemoteN': '删除 {n} 个远程分支…',
|
|
139
|
+
'menu.deleteTagsN': '删除 {n} 个标签…',
|
|
140
|
+
'confirm.deleteRefs': '删除所选引用?本地分支 {b} 个、远程分支 {r} 个、标签 {g} 个。',
|
|
141
|
+
'confirm.discardMany': '还原 {n} 个文件的未提交改动?',
|
|
142
|
+
'confirm.discardManyUntracked': '其中 {m} 个未跟踪文件将被彻底删除。',
|
|
143
|
+
'toast.branchesDeleted': '已删除 {n} 个分支',
|
|
125
144
|
'changes.unstageAll': '全部取消暂存',
|
|
126
145
|
'changes.groupBy': '分组依据',
|
|
127
146
|
'changes.groupFlat': '平铺',
|
|
@@ -279,7 +298,7 @@ window.__ModuleLoader__.load({
|
|
|
279
298
|
'lf.truncated': '只显示前 {count} 行',
|
|
280
299
|
'preview.title': 'Git 预览',
|
|
281
300
|
'preview.hint': '点击文件或提交的「显示差异」,差异会在侧边栏这里打开。',
|
|
282
|
-
'settings.rail.title': '
|
|
301
|
+
'settings.rail.title': '动作条',
|
|
283
302
|
'settings.rail.hint': '勾选要显示在动作条上的动作;顺序固定为默认顺序,取消勾选即隐藏。',
|
|
284
303
|
'settings.rail.unavailable': '当前宿主没有设置服务,请用面板内的「动作条设置」。',
|
|
285
304
|
'settings.commit.title': 'AI 提交信息',
|
|
@@ -411,6 +430,25 @@ window.__ModuleLoader__.load({
|
|
|
411
430
|
'changes.commit': 'Commit',
|
|
412
431
|
'changes.amend': 'Amend',
|
|
413
432
|
'changes.stageAll': 'Stage all',
|
|
433
|
+
'selection.count': '{n} selected',
|
|
434
|
+
'selection.copied': 'Copied {n} item(s)',
|
|
435
|
+
'selection.copyPaths': 'Copy paths',
|
|
436
|
+
'selection.clear': 'Clear selection',
|
|
437
|
+
'action.copyName': 'Copy name',
|
|
438
|
+
'menu.copyNamesN': 'Copy {n} names',
|
|
439
|
+
'menu.copyHashesN': 'Copy {n} hashes',
|
|
440
|
+
'menu.copyPathsN': 'Copy {n} paths',
|
|
441
|
+
'menu.stageN': 'Stage {n} items',
|
|
442
|
+
'menu.unstageN': 'Unstage {n} items',
|
|
443
|
+
'menu.discardN': 'Discard {n} items…',
|
|
444
|
+
'menu.cherryN': 'Cherry-pick {n} commits',
|
|
445
|
+
'menu.deleteLocalN': 'Delete {n} local branches…',
|
|
446
|
+
'menu.deleteRemoteN': 'Delete {n} remote branches…',
|
|
447
|
+
'menu.deleteTagsN': 'Delete {n} tags…',
|
|
448
|
+
'confirm.deleteRefs': 'Delete the selected refs? {b} local, {r} remote, {g} tag(s).',
|
|
449
|
+
'confirm.discardMany': 'Discard uncommitted changes of {n} files?',
|
|
450
|
+
'confirm.discardManyUntracked': '{m} untracked file(s) will be deleted for good.',
|
|
451
|
+
'toast.branchesDeleted': '{n} branch(es) deleted',
|
|
414
452
|
'changes.unstageAll': 'Unstage all',
|
|
415
453
|
'changes.groupBy': 'Group by',
|
|
416
454
|
'changes.groupFlat': 'Flat list',
|
|
@@ -568,7 +606,7 @@ window.__ModuleLoader__.load({
|
|
|
568
606
|
'lf.truncated': 'Showing the first {count} lines',
|
|
569
607
|
'preview.title': 'Git Preview',
|
|
570
608
|
'preview.hint': 'Click “Show diff” on a file or commit — the diff opens here in the sidebar.',
|
|
571
|
-
'settings.rail.title': 'Action rail
|
|
609
|
+
'settings.rail.title': 'Action rail',
|
|
572
610
|
'settings.rail.hint': 'Tick the actions shown on the rail. The order is fixed; unticking one hides it.',
|
|
573
611
|
'settings.rail.unavailable': 'This host has no settings service; use the in-panel “Action rail settings”.',
|
|
574
612
|
'settings.commit.title': 'AI commit message',
|
|
@@ -708,6 +746,25 @@ window.__ModuleLoader__.load({
|
|
|
708
746
|
'changes.commit': '提交',
|
|
709
747
|
'changes.amend': '修訂上次提交',
|
|
710
748
|
'changes.stageAll': '全部暫存',
|
|
749
|
+
'selection.count': '已選 {n} 項',
|
|
750
|
+
'selection.copied': '已複製 {n} 項',
|
|
751
|
+
'selection.copyPaths': '複製路徑',
|
|
752
|
+
'selection.clear': '清除選擇',
|
|
753
|
+
'action.copyName': '複製名稱',
|
|
754
|
+
'menu.copyNamesN': '複製 {n} 個名稱',
|
|
755
|
+
'menu.copyHashesN': '複製 {n} 個雜湊',
|
|
756
|
+
'menu.copyPathsN': '複製 {n} 個路徑',
|
|
757
|
+
'menu.stageN': '暫存 {n} 項',
|
|
758
|
+
'menu.unstageN': '取消暫存 {n} 項',
|
|
759
|
+
'menu.discardN': '還原 {n} 項…',
|
|
760
|
+
'menu.cherryN': 'Cherry-pick {n} 個提交',
|
|
761
|
+
'menu.deleteLocalN': '刪除 {n} 個本地分支…',
|
|
762
|
+
'menu.deleteRemoteN': '刪除 {n} 個遠端分支…',
|
|
763
|
+
'menu.deleteTagsN': '刪除 {n} 個標籤…',
|
|
764
|
+
'confirm.deleteRefs': '刪除所選引用?本地分支 {b} 個、遠端分支 {r} 個、標籤 {g} 個。',
|
|
765
|
+
'confirm.discardMany': '還原 {n} 個檔案的未提交變更?',
|
|
766
|
+
'confirm.discardManyUntracked': '其中 {m} 個未追蹤檔案將被徹底刪除。',
|
|
767
|
+
'toast.branchesDeleted': '已刪除 {n} 個分支',
|
|
711
768
|
'changes.unstageAll': '全部取消暫存',
|
|
712
769
|
'changes.groupBy': '分組方式',
|
|
713
770
|
'changes.groupFlat': '平面清單',
|
|
@@ -865,7 +922,7 @@ window.__ModuleLoader__.load({
|
|
|
865
922
|
'lf.truncated': '只顯示前 {count} 行',
|
|
866
923
|
'preview.title': 'Git 預覽',
|
|
867
924
|
'preview.hint': '喺檔案或提交撳「顯示差異」,差異就會喺側邊欄度開啟。',
|
|
868
|
-
'settings.rail.title': '
|
|
925
|
+
'settings.rail.title': '動作列',
|
|
869
926
|
'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
|
|
870
927
|
'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
|
|
871
928
|
'settings.commit.title': 'AI 提交訊息',
|
|
@@ -997,6 +1054,25 @@ window.__ModuleLoader__.load({
|
|
|
997
1054
|
'changes.commit': '提交',
|
|
998
1055
|
'changes.amend': '修訂上次提交',
|
|
999
1056
|
'changes.stageAll': '全部暫存',
|
|
1057
|
+
'selection.count': '已選 {n} 項',
|
|
1058
|
+
'selection.copied': '已複製 {n} 項',
|
|
1059
|
+
'selection.copyPaths': '複製路徑',
|
|
1060
|
+
'selection.clear': '清除選擇',
|
|
1061
|
+
'action.copyName': '複製名稱',
|
|
1062
|
+
'menu.copyNamesN': '複製 {n} 個名稱',
|
|
1063
|
+
'menu.copyHashesN': '複製 {n} 個雜湊',
|
|
1064
|
+
'menu.copyPathsN': '複製 {n} 個路徑',
|
|
1065
|
+
'menu.stageN': '暫存 {n} 項',
|
|
1066
|
+
'menu.unstageN': '取消暫存 {n} 項',
|
|
1067
|
+
'menu.discardN': '還原 {n} 項…',
|
|
1068
|
+
'menu.cherryN': 'Cherry-pick {n} 個提交',
|
|
1069
|
+
'menu.deleteLocalN': '刪除 {n} 個本地分支…',
|
|
1070
|
+
'menu.deleteRemoteN': '刪除 {n} 個遠端分支…',
|
|
1071
|
+
'menu.deleteTagsN': '刪除 {n} 個標籤…',
|
|
1072
|
+
'confirm.deleteRefs': '刪除所選引用?本地分支 {b} 個、遠端分支 {r} 個、標籤 {g} 個。',
|
|
1073
|
+
'confirm.discardMany': '還原 {n} 個檔案的未提交變更?',
|
|
1074
|
+
'confirm.discardManyUntracked': '其中 {m} 個未追蹤檔案將被徹底刪除。',
|
|
1075
|
+
'toast.branchesDeleted': '已刪除 {n} 個分支',
|
|
1000
1076
|
'changes.unstageAll': '全部取消暫存',
|
|
1001
1077
|
'changes.groupBy': '分組方式',
|
|
1002
1078
|
'changes.groupFlat': '平面清單',
|
|
@@ -1154,7 +1230,7 @@ window.__ModuleLoader__.load({
|
|
|
1154
1230
|
'lf.truncated': '只顯示前 {count} 行',
|
|
1155
1231
|
'preview.title': 'Git 預覽',
|
|
1156
1232
|
'preview.hint': '點選檔案或提交的「顯示差異」,差異就會在側邊欄開啟。',
|
|
1157
|
-
'settings.rail.title': '
|
|
1233
|
+
'settings.rail.title': '動作列',
|
|
1158
1234
|
'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
|
|
1159
1235
|
'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
|
|
1160
1236
|
'settings.commit.title': 'AI 提交訊息',
|
|
@@ -1286,6 +1362,25 @@ window.__ModuleLoader__.load({
|
|
|
1286
1362
|
'changes.commit': '提交',
|
|
1287
1363
|
'changes.amend': '修訂上次提交',
|
|
1288
1364
|
'changes.stageAll': '全部暫存',
|
|
1365
|
+
'selection.count': '已選 {n} 項',
|
|
1366
|
+
'selection.copied': '已複製 {n} 項',
|
|
1367
|
+
'selection.copyPaths': '複製路徑',
|
|
1368
|
+
'selection.clear': '清除選擇',
|
|
1369
|
+
'action.copyName': '複製名稱',
|
|
1370
|
+
'menu.copyNamesN': '複製 {n} 個名稱',
|
|
1371
|
+
'menu.copyHashesN': '複製 {n} 個雜湊',
|
|
1372
|
+
'menu.copyPathsN': '複製 {n} 個路徑',
|
|
1373
|
+
'menu.stageN': '暫存 {n} 項',
|
|
1374
|
+
'menu.unstageN': '取消暫存 {n} 項',
|
|
1375
|
+
'menu.discardN': '還原 {n} 項…',
|
|
1376
|
+
'menu.cherryN': 'Cherry-pick {n} 個提交',
|
|
1377
|
+
'menu.deleteLocalN': '刪除 {n} 個本地分支…',
|
|
1378
|
+
'menu.deleteRemoteN': '刪除 {n} 個遠端分支…',
|
|
1379
|
+
'menu.deleteTagsN': '刪除 {n} 個標籤…',
|
|
1380
|
+
'confirm.deleteRefs': '刪除所選引用?本地分支 {b} 個、遠端分支 {r} 個、標籤 {g} 個。',
|
|
1381
|
+
'confirm.discardMany': '還原 {n} 個檔案的未提交變更?',
|
|
1382
|
+
'confirm.discardManyUntracked': '其中 {m} 個未追蹤檔案將被徹底刪除。',
|
|
1383
|
+
'toast.branchesDeleted': '已刪除 {n} 個分支',
|
|
1289
1384
|
'changes.unstageAll': '全部取消暫存',
|
|
1290
1385
|
'changes.groupBy': '分組方式',
|
|
1291
1386
|
'changes.groupFlat': '平面清單',
|
|
@@ -1443,7 +1538,7 @@ window.__ModuleLoader__.load({
|
|
|
1443
1538
|
'lf.truncated': '只顯示前 {count} 行',
|
|
1444
1539
|
'preview.title': 'Git 預覽',
|
|
1445
1540
|
'preview.hint': '喺檔案或提交撳「顯示差異」,差異就會喺側邊欄度開啟。',
|
|
1446
|
-
'settings.rail.title': '
|
|
1541
|
+
'settings.rail.title': '動作列',
|
|
1447
1542
|
'settings.rail.hint': '勾選要顯示在動作列上的動作;順序固定,取消勾選即隱藏。',
|
|
1448
1543
|
'settings.rail.unavailable': '目前宿主沒有設定服務,請用面板內的「動作條設定」。',
|
|
1449
1544
|
'settings.commit.title': 'AI 提交訊息',
|
|
@@ -1575,6 +1670,25 @@ window.__ModuleLoader__.load({
|
|
|
1575
1670
|
'changes.commit': 'コミット',
|
|
1576
1671
|
'changes.amend': '前回のコミットを修正',
|
|
1577
1672
|
'changes.stageAll': 'すべてステージ',
|
|
1673
|
+
'selection.count': '{n} 件を選択中',
|
|
1674
|
+
'selection.copied': '{n} 件をコピーしました',
|
|
1675
|
+
'selection.copyPaths': 'パスをコピー',
|
|
1676
|
+
'selection.clear': '選択を解除',
|
|
1677
|
+
'action.copyName': '名前をコピー',
|
|
1678
|
+
'menu.copyNamesN': '{n} 件の名前をコピー',
|
|
1679
|
+
'menu.copyHashesN': '{n} 件のハッシュをコピー',
|
|
1680
|
+
'menu.copyPathsN': '{n} 件のパスをコピー',
|
|
1681
|
+
'menu.stageN': '{n} 件をステージ',
|
|
1682
|
+
'menu.unstageN': '{n} 件のステージ解除',
|
|
1683
|
+
'menu.discardN': '{n} 件を破棄…',
|
|
1684
|
+
'menu.cherryN': 'コミット {n} 件をチェリーピック',
|
|
1685
|
+
'menu.deleteLocalN': 'ローカルブランチ {n} 件を削除…',
|
|
1686
|
+
'menu.deleteRemoteN': 'リモートブランチ {n} 件を削除…',
|
|
1687
|
+
'menu.deleteTagsN': 'タグ {n} 件を削除…',
|
|
1688
|
+
'confirm.deleteRefs': '選択した参照を削除しますか?ローカル {b} 件、リモート {r} 件、タグ {g} 件。',
|
|
1689
|
+
'confirm.discardMany': '{n} 件の未コミット変更を破棄しますか?',
|
|
1690
|
+
'confirm.discardManyUntracked': 'うち {m} 件の未追跡ファイルは完全に削除されます。',
|
|
1691
|
+
'toast.branchesDeleted': '{n} 件のブランチを削除しました',
|
|
1578
1692
|
'changes.unstageAll': 'すべてステージ解除',
|
|
1579
1693
|
'changes.groupBy': 'グループ化',
|
|
1580
1694
|
'changes.groupFlat': 'フラット表示',
|
|
@@ -1732,7 +1846,7 @@ window.__ModuleLoader__.load({
|
|
|
1732
1846
|
'lf.truncated': '先頭 {count} 行のみ表示',
|
|
1733
1847
|
'preview.title': 'Git プレビュー',
|
|
1734
1848
|
'preview.hint': 'ファイルやコミットの「差分を表示」をクリックすると、差分がここに開きます。',
|
|
1735
|
-
'settings.rail.title': '
|
|
1849
|
+
'settings.rail.title': 'レール',
|
|
1736
1850
|
'settings.rail.hint': 'レールに表示する操作にチェックを入れてください。順序は固定で、チェックを外すと非表示になります。',
|
|
1737
1851
|
'settings.rail.unavailable': 'このホストには設定サービスがないため、パネル内の「アクションレール設定」を使ってください。',
|
|
1738
1852
|
'settings.commit.title': 'AI コミットメッセージ',
|
|
@@ -1864,6 +1978,25 @@ window.__ModuleLoader__.load({
|
|
|
1864
1978
|
'changes.commit': '커밋',
|
|
1865
1979
|
'changes.amend': '이전 커밋 수정',
|
|
1866
1980
|
'changes.stageAll': '모두 스테이지',
|
|
1981
|
+
'selection.count': '{n}개 선택됨',
|
|
1982
|
+
'selection.copied': '{n}개 복사됨',
|
|
1983
|
+
'selection.copyPaths': '경로 복사',
|
|
1984
|
+
'selection.clear': '선택 해제',
|
|
1985
|
+
'action.copyName': '이름 복사',
|
|
1986
|
+
'menu.copyNamesN': '이름 {n}개 복사',
|
|
1987
|
+
'menu.copyHashesN': '해시 {n}개 복사',
|
|
1988
|
+
'menu.copyPathsN': '경로 {n}개 복사',
|
|
1989
|
+
'menu.stageN': '{n}개 스테이지',
|
|
1990
|
+
'menu.unstageN': '{n}개 스테이지 해제',
|
|
1991
|
+
'menu.discardN': '{n}개 되돌리기…',
|
|
1992
|
+
'menu.cherryN': '커밋 {n}개 체리픽',
|
|
1993
|
+
'menu.deleteLocalN': '로컬 브랜치 {n}개 삭제…',
|
|
1994
|
+
'menu.deleteRemoteN': '원격 브랜치 {n}개 삭제…',
|
|
1995
|
+
'menu.deleteTagsN': '태그 {n}개 삭제…',
|
|
1996
|
+
'confirm.deleteRefs': '선택한 레퍼런스를 삭제할까요? 로컬 {b}개, 원격 {r}개, 태그 {g}개.',
|
|
1997
|
+
'confirm.discardMany': '{n}개 파일의 커밋되지 않은 변경을 되돌릴까요?',
|
|
1998
|
+
'confirm.discardManyUntracked': '이 중 추적되지 않는 {m}개 파일은 완전히 삭제됩니다.',
|
|
1999
|
+
'toast.branchesDeleted': '브랜치 {n}개 삭제됨',
|
|
1867
2000
|
'changes.unstageAll': '모두 스테이지 해제',
|
|
1868
2001
|
'changes.groupBy': '그룹 기준',
|
|
1869
2002
|
'changes.groupFlat': '평면 목록',
|
|
@@ -2021,7 +2154,7 @@ window.__ModuleLoader__.load({
|
|
|
2021
2154
|
'lf.truncated': '처음 {count}줄만 표시',
|
|
2022
2155
|
'preview.title': 'Git 미리보기',
|
|
2023
2156
|
'preview.hint': '파일이나 커밋의 「차이 보기」를 클릭하면 차이가 사이드바 여기에 열립니다.',
|
|
2024
|
-
'settings.rail.title': '
|
|
2157
|
+
'settings.rail.title': '레일',
|
|
2025
2158
|
'settings.rail.hint': '레일에 표시할 동작을 선택하세요. 순서는 고정이며 선택을 해제하면 숨겨집니다.',
|
|
2026
2159
|
'settings.rail.unavailable': '이 호스트에는 설정 서비스가 없으므로 패널의 "액션 레일 설정"을 사용하세요.',
|
|
2027
2160
|
'settings.commit.title': 'AI 커밋 메시지',
|
|
@@ -2153,6 +2286,25 @@ window.__ModuleLoader__.load({
|
|
|
2153
2286
|
'changes.commit': 'Committen',
|
|
2154
2287
|
'changes.amend': 'Letzten Commit ergänzen',
|
|
2155
2288
|
'changes.stageAll': 'Alle vormerken',
|
|
2289
|
+
'selection.count': '{n} ausgewählt',
|
|
2290
|
+
'selection.copied': '{n} Element(e) kopiert',
|
|
2291
|
+
'selection.copyPaths': 'Pfade kopieren',
|
|
2292
|
+
'selection.clear': 'Auswahl aufheben',
|
|
2293
|
+
'action.copyName': 'Name kopieren',
|
|
2294
|
+
'menu.copyNamesN': '{n} Namen kopieren',
|
|
2295
|
+
'menu.copyHashesN': '{n} Hashes kopieren',
|
|
2296
|
+
'menu.copyPathsN': '{n} Pfade kopieren',
|
|
2297
|
+
'menu.stageN': '{n} Elemente vormerken',
|
|
2298
|
+
'menu.unstageN': '{n} Elemente zurücknehmen',
|
|
2299
|
+
'menu.discardN': '{n} Elemente verwerfen…',
|
|
2300
|
+
'menu.cherryN': '{n} Commits cherry-picken',
|
|
2301
|
+
'menu.deleteLocalN': '{n} lokale Branches löschen…',
|
|
2302
|
+
'menu.deleteRemoteN': '{n} Remote-Branches löschen…',
|
|
2303
|
+
'menu.deleteTagsN': '{n} Tags löschen…',
|
|
2304
|
+
'confirm.deleteRefs': 'Ausgewählte Referenzen löschen? {b} lokal, {r} remote, {g} Tag(s).',
|
|
2305
|
+
'confirm.discardMany': 'Uncommittete Änderungen von {n} Dateien verwerfen?',
|
|
2306
|
+
'confirm.discardManyUntracked': '{m} unbeobachtete Datei(en) werden endgültig gelöscht.',
|
|
2307
|
+
'toast.branchesDeleted': '{n} Branch(es) gelöscht',
|
|
2156
2308
|
'changes.unstageAll': 'Alle zurücknehmen',
|
|
2157
2309
|
'changes.groupBy': 'Gruppieren nach',
|
|
2158
2310
|
'changes.groupFlat': 'Flache Liste',
|
|
@@ -2310,7 +2462,7 @@ window.__ModuleLoader__.load({
|
|
|
2310
2462
|
'lf.truncated': 'Nur die ersten {count} Zeilen',
|
|
2311
2463
|
'preview.title': 'Git-Vorschau',
|
|
2312
2464
|
'preview.hint': '„Diff anzeigen“ an einer Datei oder einem Commit öffnet den Diff hier in der Seitenleiste.',
|
|
2313
|
-
'settings.rail.title': '
|
|
2465
|
+
'settings.rail.title': 'Aktionsleiste',
|
|
2314
2466
|
'settings.rail.hint': 'Die auf der Leiste sichtbaren Aktionen ankreuzen. Die Reihenfolge ist fest; abwählen blendet eine aus.',
|
|
2315
2467
|
'settings.rail.unavailable': 'Dieser Host hat keinen Einstellungsdienst; nutze die „Aktionsleisten-Einstellungen“ im Panel.',
|
|
2316
2468
|
'settings.commit.title': 'KI-Commit-Nachricht',
|
|
@@ -2442,6 +2594,25 @@ window.__ModuleLoader__.load({
|
|
|
2442
2594
|
'changes.commit': 'Valider',
|
|
2443
2595
|
'changes.amend': 'Corriger le dernier commit',
|
|
2444
2596
|
'changes.stageAll': 'Tout indexer',
|
|
2597
|
+
'selection.count': '{n} sélectionné(s)',
|
|
2598
|
+
'selection.copied': '{n} élément(s) copié(s)',
|
|
2599
|
+
'selection.copyPaths': 'Copier les chemins',
|
|
2600
|
+
'selection.clear': 'Effacer la sélection',
|
|
2601
|
+
'action.copyName': 'Copier le nom',
|
|
2602
|
+
'menu.copyNamesN': 'Copier {n} noms',
|
|
2603
|
+
'menu.copyHashesN': 'Copier {n} hachages',
|
|
2604
|
+
'menu.copyPathsN': 'Copier {n} chemins',
|
|
2605
|
+
'menu.stageN': 'Indexer {n} éléments',
|
|
2606
|
+
'menu.unstageN': 'Désindexer {n} éléments',
|
|
2607
|
+
'menu.discardN': 'Abandonner {n} éléments…',
|
|
2608
|
+
'menu.cherryN': 'Cherry-pick de {n} commits',
|
|
2609
|
+
'menu.deleteLocalN': 'Supprimer {n} branches locales…',
|
|
2610
|
+
'menu.deleteRemoteN': 'Supprimer {n} branches distantes…',
|
|
2611
|
+
'menu.deleteTagsN': 'Supprimer {n} étiquettes…',
|
|
2612
|
+
'confirm.deleteRefs': 'Supprimer les références sélectionnées ? {b} locale(s), {r} distante(s), {g} étiquette(s).',
|
|
2613
|
+
'confirm.discardMany': 'Abandonner les modifications non commitées de {n} fichiers ?',
|
|
2614
|
+
'confirm.discardManyUntracked': '{m} fichier(s) non suivi(s) seront définitivement supprimés.',
|
|
2615
|
+
'toast.branchesDeleted': '{n} branche(s) supprimée(s)',
|
|
2445
2616
|
'changes.unstageAll': 'Tout désindexer',
|
|
2446
2617
|
'changes.groupBy': 'Grouper par',
|
|
2447
2618
|
'changes.groupFlat': 'Liste plate',
|
|
@@ -2599,7 +2770,7 @@ window.__ModuleLoader__.load({
|
|
|
2599
2770
|
'lf.truncated': 'Affichage des {count} premières lignes',
|
|
2600
2771
|
'preview.title': 'Aperçu Git',
|
|
2601
2772
|
'preview.hint': 'Cliquez sur « Afficher le diff » sur un fichier ou un commit : le diff s’ouvre ici, dans la barre latérale.',
|
|
2602
|
-
'settings.rail.title': '
|
|
2773
|
+
'settings.rail.title': 'Barre d’actions',
|
|
2603
2774
|
'settings.rail.hint': 'Cochez les actions affichées sur la barre. L’ordre est fixe ; décocher masque une action.',
|
|
2604
2775
|
'settings.rail.unavailable': 'Cet hôte n’a pas de service de réglages ; utilisez « Réglages de la barre d’actions » dans le panneau.',
|
|
2605
2776
|
'settings.commit.title': 'Message de commit par IA',
|
|
@@ -2731,6 +2902,25 @@ window.__ModuleLoader__.load({
|
|
|
2731
2902
|
'changes.commit': 'Закоммитить',
|
|
2732
2903
|
'changes.amend': 'Дополнить последний коммит',
|
|
2733
2904
|
'changes.stageAll': 'Добавить всё в индекс',
|
|
2905
|
+
'selection.count': 'Выбрано: {n}',
|
|
2906
|
+
'selection.copied': 'Скопировано: {n}',
|
|
2907
|
+
'selection.copyPaths': 'Копировать пути',
|
|
2908
|
+
'selection.clear': 'Снять выделение',
|
|
2909
|
+
'action.copyName': 'Копировать имя',
|
|
2910
|
+
'menu.copyNamesN': 'Копировать имён: {n}',
|
|
2911
|
+
'menu.copyHashesN': 'Копировать хешей: {n}',
|
|
2912
|
+
'menu.copyPathsN': 'Копировать путей: {n}',
|
|
2913
|
+
'menu.stageN': 'В индекс: {n}',
|
|
2914
|
+
'menu.unstageN': 'Из индекса: {n}',
|
|
2915
|
+
'menu.discardN': 'Отменить изменения: {n}…',
|
|
2916
|
+
'menu.cherryN': 'Cherry-pick {n} коммит(ов)',
|
|
2917
|
+
'menu.deleteLocalN': 'Удалить локальных ветвей: {n}…',
|
|
2918
|
+
'menu.deleteRemoteN': 'Удалить внешних ветвей: {n}…',
|
|
2919
|
+
'menu.deleteTagsN': 'Удалить меток: {n}…',
|
|
2920
|
+
'confirm.deleteRefs': 'Удалить выбранные ссылки? Локальных: {b}, внешних: {r}, меток: {g}.',
|
|
2921
|
+
'confirm.discardMany': 'Отменить незафиксированные изменения в {n} файлах?',
|
|
2922
|
+
'confirm.discardManyUntracked': 'Будет безвозвратно удалено непрослеживаемых файлов: {m}.',
|
|
2923
|
+
'toast.branchesDeleted': 'Ветвей удалено: {n}',
|
|
2734
2924
|
'changes.unstageAll': 'Убрать всё из индекса',
|
|
2735
2925
|
'changes.groupBy': 'Группировать по',
|
|
2736
2926
|
'changes.groupFlat': 'Плоский список',
|
|
@@ -2888,7 +3078,7 @@ window.__ModuleLoader__.load({
|
|
|
2888
3078
|
'lf.truncated': 'Показаны первые {count} строк',
|
|
2889
3079
|
'preview.title': 'Git-предпросмотр',
|
|
2890
3080
|
'preview.hint': 'Нажмите «Показать различия» у файла или коммита — различия откроются здесь, в боковой панели.',
|
|
2891
|
-
'settings.rail.title': '
|
|
3081
|
+
'settings.rail.title': 'Панель действий',
|
|
2892
3082
|
'settings.rail.hint': 'Отметьте действия, показываемые на панели. Порядок фиксирован; снятие флажка скрывает действие.',
|
|
2893
3083
|
'settings.rail.unavailable': 'На этом хосте нет службы настроек; используйте «Настройки панели действий» в панели.',
|
|
2894
3084
|
'settings.commit.title': 'Сообщение коммита от ИИ',
|
|
@@ -3020,6 +3210,25 @@ window.__ModuleLoader__.load({
|
|
|
3020
3210
|
'changes.commit': 'Commitar',
|
|
3021
3211
|
'changes.amend': 'Corrigir o último commit',
|
|
3022
3212
|
'changes.stageAll': 'Preparar tudo',
|
|
3213
|
+
'selection.count': '{n} selecionado(s)',
|
|
3214
|
+
'selection.copied': '{n} item(ns) copiado(s)',
|
|
3215
|
+
'selection.copyPaths': 'Copiar caminhos',
|
|
3216
|
+
'selection.clear': 'Limpar seleção',
|
|
3217
|
+
'action.copyName': 'Copiar nome',
|
|
3218
|
+
'menu.copyNamesN': 'Copiar {n} nomes',
|
|
3219
|
+
'menu.copyHashesN': 'Copiar {n} hashes',
|
|
3220
|
+
'menu.copyPathsN': 'Copiar {n} caminhos',
|
|
3221
|
+
'menu.stageN': 'Preparar {n} itens',
|
|
3222
|
+
'menu.unstageN': 'Despreparar {n} itens',
|
|
3223
|
+
'menu.discardN': 'Descartar {n} itens…',
|
|
3224
|
+
'menu.cherryN': 'Cherry-pick de {n} commits',
|
|
3225
|
+
'menu.deleteLocalN': 'Excluir {n} ramificações locais…',
|
|
3226
|
+
'menu.deleteRemoteN': 'Excluir {n} ramificações remotas…',
|
|
3227
|
+
'menu.deleteTagsN': 'Excluir {n} etiquetas…',
|
|
3228
|
+
'confirm.deleteRefs': 'Excluir as referências selecionadas? {b} locais, {r} remotas, {g} etiqueta(s).',
|
|
3229
|
+
'confirm.discardMany': 'Descartar as alterações não commitadas de {n} arquivos?',
|
|
3230
|
+
'confirm.discardManyUntracked': '{m} arquivo(s) não rastreado(s) serão excluídos em definitivo.',
|
|
3231
|
+
'toast.branchesDeleted': '{n} ramificação(ões) excluída(s)',
|
|
3023
3232
|
'changes.unstageAll': 'Despreparar tudo',
|
|
3024
3233
|
'changes.groupBy': 'Agrupar por',
|
|
3025
3234
|
'changes.groupFlat': 'Lista simples',
|
|
@@ -3177,7 +3386,7 @@ window.__ModuleLoader__.load({
|
|
|
3177
3386
|
'lf.truncated': 'Mostrando as primeiras {count} linhas',
|
|
3178
3387
|
'preview.title': 'Pré-visualização do Git',
|
|
3179
3388
|
'preview.hint': 'Clique em “Mostrar diff” num arquivo ou commit — o diff abre aqui, na barra lateral.',
|
|
3180
|
-
'settings.rail.title': '
|
|
3389
|
+
'settings.rail.title': 'Barra de ações',
|
|
3181
3390
|
'settings.rail.hint': 'Marque as ações exibidas na barra. A ordem é fixa; desmarcar oculta uma ação.',
|
|
3182
3391
|
'settings.rail.unavailable': 'Este host não tem serviço de configurações; use «Configurações da barra de ações» no painel.',
|
|
3183
3392
|
'settings.commit.title': 'Mensagem de commit por IA',
|
|
@@ -3309,6 +3518,25 @@ window.__ModuleLoader__.load({
|
|
|
3309
3518
|
'changes.commit': 'Esegui commit',
|
|
3310
3519
|
'changes.amend': 'Correggi l’ultimo commit',
|
|
3311
3520
|
'changes.stageAll': 'Metti tutto in stage',
|
|
3521
|
+
'selection.count': '{n} selezionato/i',
|
|
3522
|
+
'selection.copied': '{n} elemento/i copiato/i',
|
|
3523
|
+
'selection.copyPaths': 'Copia percorsi',
|
|
3524
|
+
'selection.clear': 'Azzera selezione',
|
|
3525
|
+
'action.copyName': 'Copia nome',
|
|
3526
|
+
'menu.copyNamesN': 'Copia {n} nomi',
|
|
3527
|
+
'menu.copyHashesN': 'Copia {n} hash',
|
|
3528
|
+
'menu.copyPathsN': 'Copia {n} percorsi',
|
|
3529
|
+
'menu.stageN': 'Metti in stage {n} elementi',
|
|
3530
|
+
'menu.unstageN': 'Rimuovi {n} elementi dallo stage',
|
|
3531
|
+
'menu.discardN': 'Scarta {n} elementi…',
|
|
3532
|
+
'menu.cherryN': 'Cherry-pick di {n} commit',
|
|
3533
|
+
'menu.deleteLocalN': 'Elimina {n} branch locali…',
|
|
3534
|
+
'menu.deleteRemoteN': 'Elimina {n} branch remoti…',
|
|
3535
|
+
'menu.deleteTagsN': 'Elimina {n} tag…',
|
|
3536
|
+
'confirm.deleteRefs': 'Eliminare i riferimenti selezionati? {b} locali, {r} remoti, {g} tag.',
|
|
3537
|
+
'confirm.discardMany': 'Scartare le modifiche non commitate di {n} file?',
|
|
3538
|
+
'confirm.discardManyUntracked': '{m} file non tracciati saranno eliminati definitivamente.',
|
|
3539
|
+
'toast.branchesDeleted': '{n} branch eliminati',
|
|
3312
3540
|
'changes.unstageAll': 'Rimuovi tutto dallo stage',
|
|
3313
3541
|
'changes.groupBy': 'Raggruppa per',
|
|
3314
3542
|
'changes.groupFlat': 'Elenco piatto',
|
|
@@ -3466,7 +3694,7 @@ window.__ModuleLoader__.load({
|
|
|
3466
3694
|
'lf.truncated': 'Mostrate le prime {count} righe',
|
|
3467
3695
|
'preview.title': 'Anteprima Git',
|
|
3468
3696
|
'preview.hint': 'Fai clic su «Mostra diff» su un file o un commit: il diff si apre qui, nella barra laterale.',
|
|
3469
|
-
'settings.rail.title': '
|
|
3697
|
+
'settings.rail.title': 'Barra delle azioni',
|
|
3470
3698
|
'settings.rail.hint': 'Seleziona le azioni mostrate sulla barra. L’ordine è fisso; deselezionando se ne nasconde una.',
|
|
3471
3699
|
'settings.rail.unavailable': 'Questo host non ha un servizio di impostazioni; usa «Impostazioni barra azioni» nel pannello.',
|
|
3472
3700
|
'settings.commit.title': 'Messaggio di commit con IA',
|
|
@@ -3598,6 +3826,25 @@ window.__ModuleLoader__.load({
|
|
|
3598
3826
|
'changes.commit': 'Committen',
|
|
3599
3827
|
'changes.amend': 'Laatste commit aanvullen',
|
|
3600
3828
|
'changes.stageAll': 'Alles stagen',
|
|
3829
|
+
'selection.count': '{n} geselecteerd',
|
|
3830
|
+
'selection.copied': '{n} item(s) gekopieerd',
|
|
3831
|
+
'selection.copyPaths': 'Paden kopiëren',
|
|
3832
|
+
'selection.clear': 'Selectie wissen',
|
|
3833
|
+
'action.copyName': 'Naam kopiëren',
|
|
3834
|
+
'menu.copyNamesN': '{n} namen kopiëren',
|
|
3835
|
+
'menu.copyHashesN': '{n} hashes kopiëren',
|
|
3836
|
+
'menu.copyPathsN': '{n} paden kopiëren',
|
|
3837
|
+
'menu.stageN': '{n} items stagen',
|
|
3838
|
+
'menu.unstageN': '{n} items unstagen',
|
|
3839
|
+
'menu.discardN': '{n} items weggooien…',
|
|
3840
|
+
'menu.cherryN': '{n} commits cherry-picken',
|
|
3841
|
+
'menu.deleteLocalN': '{n} lokale branches verwijderen…',
|
|
3842
|
+
'menu.deleteRemoteN': '{n} remote branches verwijderen…',
|
|
3843
|
+
'menu.deleteTagsN': '{n} tags verwijderen…',
|
|
3844
|
+
'confirm.deleteRefs': 'De geselecteerde referenties verwijderen? {b} lokaal, {r} remote, {g} tag(s).',
|
|
3845
|
+
'confirm.discardMany': 'Niet-gecommitte wijzigingen van {n} bestanden weggooien?',
|
|
3846
|
+
'confirm.discardManyUntracked': '{m} niet-gevolgde bestand(en) worden definitief verwijderd.',
|
|
3847
|
+
'toast.branchesDeleted': '{n} branch(es) verwijderd',
|
|
3601
3848
|
'changes.unstageAll': 'Alles unstagen',
|
|
3602
3849
|
'changes.groupBy': 'Groeperen op',
|
|
3603
3850
|
'changes.groupFlat': 'Platte lijst',
|
|
@@ -3755,7 +4002,7 @@ window.__ModuleLoader__.load({
|
|
|
3755
4002
|
'lf.truncated': 'Eerste {count} regels worden weergegeven',
|
|
3756
4003
|
'preview.title': 'Git-voorbeeld',
|
|
3757
4004
|
'preview.hint': 'Klik op „Diff tonen“ bij een bestand of commit — de diff opent hier in de zijbalk.',
|
|
3758
|
-
'settings.rail.title': '
|
|
4005
|
+
'settings.rail.title': 'Actiebalk',
|
|
3759
4006
|
'settings.rail.hint': 'Vink de acties aan die op de balk verschijnen. De volgorde ligt vast; uitvinken verbergt er een.',
|
|
3760
4007
|
'settings.rail.unavailable': 'Deze host heeft geen instellingenservice; gebruik "Actiebalk-instellingen" in het paneel.',
|
|
3761
4008
|
'settings.commit.title': 'AI-commitbericht',
|
|
@@ -3887,6 +4134,25 @@ window.__ModuleLoader__.load({
|
|
|
3887
4134
|
'changes.commit': 'Zatwierdź',
|
|
3888
4135
|
'changes.amend': 'Popraw ostatni commit',
|
|
3889
4136
|
'changes.stageAll': 'Dodaj wszystko do indeksu',
|
|
4137
|
+
'selection.count': 'Wybrano: {n}',
|
|
4138
|
+
'selection.copied': 'Skopiowano: {n}',
|
|
4139
|
+
'selection.copyPaths': 'Kopiuj ścieżki',
|
|
4140
|
+
'selection.clear': 'Wyczyść zaznaczenie',
|
|
4141
|
+
'action.copyName': 'Kopiuj nazwę',
|
|
4142
|
+
'menu.copyNamesN': 'Kopiuj {n} nazw',
|
|
4143
|
+
'menu.copyHashesN': 'Kopiuj {n} skrótów',
|
|
4144
|
+
'menu.copyPathsN': 'Kopiuj {n} ścieżek',
|
|
4145
|
+
'menu.stageN': 'Dodaj {n} do indeksu',
|
|
4146
|
+
'menu.unstageN': 'Usuń {n} z indeksu',
|
|
4147
|
+
'menu.discardN': 'Odrzuć {n}…',
|
|
4148
|
+
'menu.cherryN': 'Cherry-pick {n} commit(ów)',
|
|
4149
|
+
'menu.deleteLocalN': 'Usuń {n} gałęzi lokalnych…',
|
|
4150
|
+
'menu.deleteRemoteN': 'Usuń {n} gałęzi zdalnych…',
|
|
4151
|
+
'menu.deleteTagsN': 'Usuń {n} tagów…',
|
|
4152
|
+
'confirm.deleteRefs': 'Usunąć wybrane referencje? Lokalne: {b}, zdalne: {r}, tagi: {g}.',
|
|
4153
|
+
'confirm.discardMany': 'Odrzucić niezacommitowane zmiany w {n} plikach?',
|
|
4154
|
+
'confirm.discardManyUntracked': '{m} nieśledzonych plików zostanie trwale usuniętych.',
|
|
4155
|
+
'toast.branchesDeleted': 'Usunięto gałęzi: {n}',
|
|
3890
4156
|
'changes.unstageAll': 'Usuń wszystko z indeksu',
|
|
3891
4157
|
'changes.groupBy': 'Grupuj według',
|
|
3892
4158
|
'changes.groupFlat': 'Płaska lista',
|
|
@@ -4044,7 +4310,7 @@ window.__ModuleLoader__.load({
|
|
|
4044
4310
|
'lf.truncated': 'Pokazano pierwsze {count} wierszy',
|
|
4045
4311
|
'preview.title': 'Podgląd Git',
|
|
4046
4312
|
'preview.hint': 'Kliknij „Pokaż różnice” przy pliku lub commicie — różnice otworzą się tutaj, na pasku bocznym.',
|
|
4047
|
-
'settings.rail.title': '
|
|
4313
|
+
'settings.rail.title': 'Pasek akcji',
|
|
4048
4314
|
'settings.rail.hint': 'Zaznacz akcje widoczne na pasku. Kolejność jest stała; odznaczenie ukrywa akcję.',
|
|
4049
4315
|
'settings.rail.unavailable': 'Ten host nie ma usługi ustawień; użyj „Ustawienia paska akcji” w panelu.',
|
|
4050
4316
|
'settings.commit.title': 'Wiadomość commita od AI',
|
|
@@ -4176,6 +4442,25 @@ window.__ModuleLoader__.load({
|
|
|
4176
4442
|
'changes.commit': 'Committa',
|
|
4177
4443
|
'changes.amend': 'Komplettera senaste commit',
|
|
4178
4444
|
'changes.stageAll': 'Klädda alla',
|
|
4445
|
+
'selection.count': '{n} markerade',
|
|
4446
|
+
'selection.copied': 'Kopierade {n} objekt',
|
|
4447
|
+
'selection.copyPaths': 'Kopiera sökvägar',
|
|
4448
|
+
'selection.clear': 'Rensa markering',
|
|
4449
|
+
'action.copyName': 'Kopiera namn',
|
|
4450
|
+
'menu.copyNamesN': 'Kopiera {n} namn',
|
|
4451
|
+
'menu.copyHashesN': 'Kopiera {n} hashar',
|
|
4452
|
+
'menu.copyPathsN': 'Kopiera {n} sökvägar',
|
|
4453
|
+
'menu.stageN': 'Klädda {n} objekt',
|
|
4454
|
+
'menu.unstageN': 'Avklädda {n} objekt',
|
|
4455
|
+
'menu.discardN': 'Förkasta {n} objekt…',
|
|
4456
|
+
'menu.cherryN': 'Cherry-picka {n} commit(s)',
|
|
4457
|
+
'menu.deleteLocalN': 'Ta bort {n} lokala grenar…',
|
|
4458
|
+
'menu.deleteRemoteN': 'Ta bort {n} fjärrgrenar…',
|
|
4459
|
+
'menu.deleteTagsN': 'Ta bort {n} taggar…',
|
|
4460
|
+
'confirm.deleteRefs': 'Ta bort markerade referenser? {b} lokala, {r} fjärr, {g} taggar.',
|
|
4461
|
+
'confirm.discardMany': 'Förkasta icke-committade ändringar i {n} filer?',
|
|
4462
|
+
'confirm.discardManyUntracked': '{m} ospårade filer tas bort permanent.',
|
|
4463
|
+
'toast.branchesDeleted': 'Tog bort {n} gren(ar)',
|
|
4179
4464
|
'changes.unstageAll': 'Avklädda alla',
|
|
4180
4465
|
'changes.groupBy': 'Gruppera efter',
|
|
4181
4466
|
'changes.groupFlat': 'Platt lista',
|
|
@@ -4333,7 +4618,7 @@ window.__ModuleLoader__.load({
|
|
|
4333
4618
|
'lf.truncated': 'Visar de första {count} raderna',
|
|
4334
4619
|
'preview.title': 'Git-förhandsvisning',
|
|
4335
4620
|
'preview.hint': 'Klicka på ”Visa diff” på en fil eller en commit — diffen öppnas här i sidopanelen.',
|
|
4336
|
-
'settings.rail.title': '
|
|
4621
|
+
'settings.rail.title': 'Åtgärdsfältet',
|
|
4337
4622
|
'settings.rail.hint': 'Markera de åtgärder som visas i fältet. Ordningen är fast; avmarkering döljer en.',
|
|
4338
4623
|
'settings.rail.unavailable': 'Denna värd saknar inställningstjänst; använd ”Åtgärdsfältets inställningar” i panelen.',
|
|
4339
4624
|
'settings.commit.title': 'AI-commitmeddelande',
|
|
@@ -4465,6 +4750,25 @@ window.__ModuleLoader__.load({
|
|
|
4465
4750
|
'changes.commit': 'Commit’le',
|
|
4466
4751
|
'changes.amend': 'Son commit’i düzelt',
|
|
4467
4752
|
'changes.stageAll': 'Tümünü hazırla',
|
|
4753
|
+
'selection.count': '{n} seçili',
|
|
4754
|
+
'selection.copied': '{n} öğe kopyalandı',
|
|
4755
|
+
'selection.copyPaths': 'Yolları kopyala',
|
|
4756
|
+
'selection.clear': 'Seçimi temizle',
|
|
4757
|
+
'action.copyName': 'Adı kopyala',
|
|
4758
|
+
'menu.copyNamesN': '{n} adı kopyala',
|
|
4759
|
+
'menu.copyHashesN': '{n} karmayı kopyala',
|
|
4760
|
+
'menu.copyPathsN': '{n} yolu kopyala',
|
|
4761
|
+
'menu.stageN': '{n} öğeyi hazırla',
|
|
4762
|
+
'menu.unstageN': '{n} öğeyi hazırlıktan çıkar',
|
|
4763
|
+
'menu.discardN': '{n} öğeyi at…',
|
|
4764
|
+
'menu.cherryN': '{n} commit seçip al',
|
|
4765
|
+
'menu.deleteLocalN': '{n} yerel dalı sil…',
|
|
4766
|
+
'menu.deleteRemoteN': '{n} uzak dalı sil…',
|
|
4767
|
+
'menu.deleteTagsN': '{n} etiketi sil…',
|
|
4768
|
+
'confirm.deleteRefs': 'Seçili referanslar silinsin mi? {b} yerel, {r} uzak, {g} etiket.',
|
|
4769
|
+
'confirm.discardMany': '{n} dosyanın commitlenmemiş değişiklikleri atılsın mı?',
|
|
4770
|
+
'confirm.discardManyUntracked': '{m} izlenmeyen dosya kalıcı olarak silinecek.',
|
|
4771
|
+
'toast.branchesDeleted': '{n} dal silindi',
|
|
4468
4772
|
'changes.unstageAll': 'Tümünü hazırlıktan çıkar',
|
|
4469
4773
|
'changes.groupBy': 'Gruplama ölçütü',
|
|
4470
4774
|
'changes.groupFlat': 'Düz liste',
|
|
@@ -4622,7 +4926,7 @@ window.__ModuleLoader__.load({
|
|
|
4622
4926
|
'lf.truncated': 'Yalnızca ilk {count} satır gösteriliyor',
|
|
4623
4927
|
'preview.title': 'Git Önizlemesi',
|
|
4624
4928
|
'preview.hint': 'Bir dosya veya commit üzerinde „Farkı göster”e tıklayın — fark burada, kenar çubuğunda açılır.',
|
|
4625
|
-
'settings.rail.title': 'Eylem çubuğu
|
|
4929
|
+
'settings.rail.title': 'Eylem çubuğu',
|
|
4626
4930
|
'settings.rail.hint': 'Çubukta gösterilecek eylemleri işaretleyin. Sıra sabittir; işareti kaldırmak eylemi gizler.',
|
|
4627
4931
|
'settings.rail.unavailable': 'Bu ana bilgisayarda ayar hizmeti yok; paneldeki "Eylem çubuğu ayarları"nı kullanın.',
|
|
4628
4932
|
'settings.commit.title': 'AI commit mesajı',
|
|
@@ -4754,6 +5058,25 @@ window.__ModuleLoader__.load({
|
|
|
4754
5058
|
'changes.commit': 'Commit',
|
|
4755
5059
|
'changes.amend': 'Perbaiki commit terakhir',
|
|
4756
5060
|
'changes.stageAll': 'Stage semua',
|
|
5061
|
+
'selection.count': '{n} dipilih',
|
|
5062
|
+
'selection.copied': '{n} item disalin',
|
|
5063
|
+
'selection.copyPaths': 'Salin jalur',
|
|
5064
|
+
'selection.clear': 'Hapus pilihan',
|
|
5065
|
+
'action.copyName': 'Salin nama',
|
|
5066
|
+
'menu.copyNamesN': 'Salin {n} nama',
|
|
5067
|
+
'menu.copyHashesN': 'Salin {n} hash',
|
|
5068
|
+
'menu.copyPathsN': 'Salin {n} jalur',
|
|
5069
|
+
'menu.stageN': 'Stage {n} item',
|
|
5070
|
+
'menu.unstageN': 'Batalkan stage {n} item',
|
|
5071
|
+
'menu.discardN': 'Buang {n} item…',
|
|
5072
|
+
'menu.cherryN': 'Cherry-pick {n} commit',
|
|
5073
|
+
'menu.deleteLocalN': 'Hapus {n} cabang lokal…',
|
|
5074
|
+
'menu.deleteRemoteN': 'Hapus {n} cabang remote…',
|
|
5075
|
+
'menu.deleteTagsN': 'Hapus {n} tag…',
|
|
5076
|
+
'confirm.deleteRefs': 'Hapus referensi terpilih? {b} lokal, {r} remote, {g} tag.',
|
|
5077
|
+
'confirm.discardMany': 'Buang perubahan yang belum di-commit dari {n} berkas?',
|
|
5078
|
+
'confirm.discardManyUntracked': '{m} berkas yang tidak dilacak akan dihapus permanen.',
|
|
5079
|
+
'toast.branchesDeleted': '{n} cabang dihapus',
|
|
4757
5080
|
'changes.unstageAll': 'Batalkan stage semua',
|
|
4758
5081
|
'changes.groupBy': 'Kelompokkan menurut',
|
|
4759
5082
|
'changes.groupFlat': 'Daftar datar',
|
|
@@ -4911,7 +5234,7 @@ window.__ModuleLoader__.load({
|
|
|
4911
5234
|
'lf.truncated': 'Menampilkan {count} baris pertama',
|
|
4912
5235
|
'preview.title': 'Pratinjau Git',
|
|
4913
5236
|
'preview.hint': 'Klik “Tampilkan diff” pada file atau commit — diff terbuka di sini, di bilah sisi.',
|
|
4914
|
-
'settings.rail.title': '
|
|
5237
|
+
'settings.rail.title': 'Bilah aksi',
|
|
4915
5238
|
'settings.rail.hint': 'Centang aksi yang ditampilkan di bilah. Urutannya tetap; menghapus centang menyembunyikannya.',
|
|
4916
5239
|
'settings.rail.unavailable': 'Host ini tidak punya layanan pengaturan; gunakan "Pengaturan bilah aksi" di panel.',
|
|
4917
5240
|
'settings.commit.title': 'Pesan commit AI',
|
|
@@ -5043,6 +5366,25 @@ window.__ModuleLoader__.load({
|
|
|
5043
5366
|
'changes.commit': 'Commit',
|
|
5044
5367
|
'changes.amend': 'Sửa commit gần nhất',
|
|
5045
5368
|
'changes.stageAll': 'Stage tất cả',
|
|
5369
|
+
'selection.count': 'Đã chọn {n}',
|
|
5370
|
+
'selection.copied': 'Đã sao chép {n} mục',
|
|
5371
|
+
'selection.copyPaths': 'Sao chép đường dẫn',
|
|
5372
|
+
'selection.clear': 'Bỏ chọn',
|
|
5373
|
+
'action.copyName': 'Sao chép tên',
|
|
5374
|
+
'menu.copyNamesN': 'Sao chép {n} tên',
|
|
5375
|
+
'menu.copyHashesN': 'Sao chép {n} hash',
|
|
5376
|
+
'menu.copyPathsN': 'Sao chép {n} đường dẫn',
|
|
5377
|
+
'menu.stageN': 'Stage {n} mục',
|
|
5378
|
+
'menu.unstageN': 'Bỏ stage {n} mục',
|
|
5379
|
+
'menu.discardN': 'Bỏ {n} mục…',
|
|
5380
|
+
'menu.cherryN': 'Cherry-pick {n} commit',
|
|
5381
|
+
'menu.deleteLocalN': 'Xoá {n} nhánh cục bộ…',
|
|
5382
|
+
'menu.deleteRemoteN': 'Xoá {n} nhánh từ xa…',
|
|
5383
|
+
'menu.deleteTagsN': 'Xoá {n} thẻ…',
|
|
5384
|
+
'confirm.deleteRefs': 'Xoá các tham chiếu đã chọn? {b} cục bộ, {r} từ xa, {g} thẻ.',
|
|
5385
|
+
'confirm.discardMany': 'Bỏ thay đổi chưa commit của {n} tệp?',
|
|
5386
|
+
'confirm.discardManyUntracked': '{m} tệp chưa theo dõi sẽ bị xoá vĩnh viễn.',
|
|
5387
|
+
'toast.branchesDeleted': 'Đã xoá {n} nhánh',
|
|
5046
5388
|
'changes.unstageAll': 'Bỏ stage tất cả',
|
|
5047
5389
|
'changes.groupBy': 'Nhóm theo',
|
|
5048
5390
|
'changes.groupFlat': 'Danh sách phẳng',
|
|
@@ -5200,7 +5542,7 @@ window.__ModuleLoader__.load({
|
|
|
5200
5542
|
'lf.truncated': 'Chỉ hiện {count} dòng đầu',
|
|
5201
5543
|
'preview.title': 'Xem trước Git',
|
|
5202
5544
|
'preview.hint': 'Bấm “Xem diff” trên tệp hoặc commit — diff sẽ mở ở đây, trên thanh bên.',
|
|
5203
|
-
'settings.rail.title': '
|
|
5545
|
+
'settings.rail.title': 'Thanh tác vụ',
|
|
5204
5546
|
'settings.rail.hint': 'Chọn các tác vụ hiển thị trên thanh. Thứ tự cố định; bỏ chọn sẽ ẩn tác vụ đó.',
|
|
5205
5547
|
'settings.rail.unavailable': 'Máy chủ này không có dịch vụ cài đặt; hãy dùng “Cài đặt thanh tác vụ” trong bảng điều khiển.',
|
|
5206
5548
|
'settings.commit.title': 'Thông điệp commit do AI viết',
|
|
@@ -5332,6 +5674,25 @@ window.__ModuleLoader__.load({
|
|
|
5332
5674
|
'changes.commit': 'التزام',
|
|
5333
5675
|
'changes.amend': 'تعديل الالتزام الأخير',
|
|
5334
5676
|
'changes.stageAll': 'جهّز الكل',
|
|
5677
|
+
'selection.count': 'تم تحديد {n}',
|
|
5678
|
+
'selection.copied': 'تم نسخ {n} عنصرًا',
|
|
5679
|
+
'selection.copyPaths': 'انسخ المسارات',
|
|
5680
|
+
'selection.clear': 'امسح التحديد',
|
|
5681
|
+
'action.copyName': 'انسخ الاسم',
|
|
5682
|
+
'menu.copyNamesN': 'انسخ {n} اسمًا',
|
|
5683
|
+
'menu.copyHashesN': 'انسخ {n} تجزئة',
|
|
5684
|
+
'menu.copyPathsN': 'انسخ {n} مسارًا',
|
|
5685
|
+
'menu.stageN': 'جهّز {n} عنصرًا',
|
|
5686
|
+
'menu.unstageN': 'ألغِ تجهيز {n} عنصرًا',
|
|
5687
|
+
'menu.discardN': 'تجاهل {n} عنصرًا…',
|
|
5688
|
+
'menu.cherryN': 'Cherry-pick لـ {n} إيداع',
|
|
5689
|
+
'menu.deleteLocalN': 'احذف {n} فرعًا محليًا…',
|
|
5690
|
+
'menu.deleteRemoteN': 'احذف {n} فرعًا بعيدًا…',
|
|
5691
|
+
'menu.deleteTagsN': 'احذف {n} وسمًا…',
|
|
5692
|
+
'confirm.deleteRefs': 'حذف المراجع المحددة؟ {b} محلية و{r} بعيدة و{g} وسوم.',
|
|
5693
|
+
'confirm.discardMany': 'تجاهل التغييرات غير المودعة في {n} ملفًا؟',
|
|
5694
|
+
'confirm.discardManyUntracked': '{m} ملفًا غير متتبَّع سيُحذف نهائيًا.',
|
|
5695
|
+
'toast.branchesDeleted': 'تم حذف {n} فرعًا',
|
|
5335
5696
|
'changes.unstageAll': 'ألغِ تجهيز الكل',
|
|
5336
5697
|
'changes.groupBy': 'التجميع حسب',
|
|
5337
5698
|
'changes.groupFlat': 'قائمة مسطّحة',
|
|
@@ -5489,7 +5850,7 @@ window.__ModuleLoader__.load({
|
|
|
5489
5850
|
'lf.truncated': 'عرض أول {count} سطرًا فقط',
|
|
5490
5851
|
'preview.title': 'معاينة Git',
|
|
5491
5852
|
'preview.hint': 'انقر «عرض الفروق» على ملف أو commit — تُفتح الفروق هنا في الشريط الجانبي.',
|
|
5492
|
-
'settings.rail.title': '
|
|
5853
|
+
'settings.rail.title': 'شريط الإجراءات',
|
|
5493
5854
|
'settings.rail.hint': 'حدّد الإجراءات المعروضة على الشريط. الترتيب ثابت، وإلغاء التحديد يخفي الإجراء.',
|
|
5494
5855
|
'settings.rail.unavailable': 'لا تتوفر خدمة إعدادات على هذا المضيف؛ استخدم «إعدادات شريط الإجراءات» في اللوحة.',
|
|
5495
5856
|
'settings.commit.title': 'رسالة إيداع بالذكاء الاصطناعي',
|
|
@@ -5621,6 +5982,25 @@ window.__ModuleLoader__.load({
|
|
|
5621
5982
|
'changes.commit': 'कमिट करें',
|
|
5622
5983
|
'changes.amend': 'पिछला कमिट सुधारें',
|
|
5623
5984
|
'changes.stageAll': 'सभी स्टेज करें',
|
|
5985
|
+
'selection.count': '{n} चयनित',
|
|
5986
|
+
'selection.copied': '{n} आइटम कॉपी किए',
|
|
5987
|
+
'selection.copyPaths': 'पाथ कॉपी करें',
|
|
5988
|
+
'selection.clear': 'चयन हटाएँ',
|
|
5989
|
+
'action.copyName': 'नाम कॉपी करें',
|
|
5990
|
+
'menu.copyNamesN': '{n} नाम कॉपी करें',
|
|
5991
|
+
'menu.copyHashesN': '{n} हैश कॉपी करें',
|
|
5992
|
+
'menu.copyPathsN': '{n} पाथ कॉपी करें',
|
|
5993
|
+
'menu.stageN': '{n} आइटम स्टेज करें',
|
|
5994
|
+
'menu.unstageN': '{n} आइटम अनस्टेज करें',
|
|
5995
|
+
'menu.discardN': '{n} आइटम छोड़ें…',
|
|
5996
|
+
'menu.cherryN': '{n} कमिट चेरी-पिक करें',
|
|
5997
|
+
'menu.deleteLocalN': '{n} लोकल ब्रांच हटाएँ…',
|
|
5998
|
+
'menu.deleteRemoteN': '{n} रिमोट ब्रांच हटाएँ…',
|
|
5999
|
+
'menu.deleteTagsN': '{n} टैग हटाएँ…',
|
|
6000
|
+
'confirm.deleteRefs': 'चयनित रेफ़रेंस हटाएँ? लोकल {b}, रिमोट {r}, टैग {g}।',
|
|
6001
|
+
'confirm.discardMany': '{n} फ़ाइलों के अनकमिटेड बदलाव छोड़ें?',
|
|
6002
|
+
'confirm.discardManyUntracked': 'इनमें {m} अनट्रैक्ड फ़ाइलें स्थायी रूप से हट जाएँगी।',
|
|
6003
|
+
'toast.branchesDeleted': '{n} ब्रांच हटाई गईं',
|
|
5624
6004
|
'changes.unstageAll': 'सभी अनस्टेज करें',
|
|
5625
6005
|
'changes.groupBy': 'इसके अनुसार समूह',
|
|
5626
6006
|
'changes.groupFlat': 'सपाट सूची',
|
|
@@ -5778,7 +6158,7 @@ window.__ModuleLoader__.load({
|
|
|
5778
6158
|
'lf.truncated': 'केवल पहली {count} पंक्तियाँ दिखाई जा रही हैं',
|
|
5779
6159
|
'preview.title': 'Git पूर्वावलोकन',
|
|
5780
6160
|
'preview.hint': 'फ़ाइल या कमिट पर “अंतर दिखाएँ” पर क्लिक करें — अंतर यहीं साइडबार में खुलेगा।',
|
|
5781
|
-
'settings.rail.title': '
|
|
6161
|
+
'settings.rail.title': 'क्रिया रेल',
|
|
5782
6162
|
'settings.rail.hint': 'रेल पर दिखने वाली क्रियाएँ चुनें। क्रम निश्चित है; चुनाव हटाने पर वह छिप जाती है।',
|
|
5783
6163
|
'settings.rail.unavailable': 'इस होस्ट में सेटिंग सेवा नहीं है; पैनल में «एक्शन रेल सेटिंग» का उपयोग करें।',
|
|
5784
6164
|
'settings.commit.title': 'AI कमिट संदेश',
|
|
@@ -5910,6 +6290,25 @@ window.__ModuleLoader__.load({
|
|
|
5910
6290
|
'changes.commit': 'คอมมิต',
|
|
5911
6291
|
'changes.amend': 'แก้ไขคอมมิตล่าสุด',
|
|
5912
6292
|
'changes.stageAll': 'สเตจทั้งหมด',
|
|
6293
|
+
'selection.count': 'เลือก {n} รายการ',
|
|
6294
|
+
'selection.copied': 'คัดลอก {n} รายการแล้ว',
|
|
6295
|
+
'selection.copyPaths': 'คัดลอกพาธ',
|
|
6296
|
+
'selection.clear': 'ล้างการเลือก',
|
|
6297
|
+
'action.copyName': 'คัดลอกชื่อ',
|
|
6298
|
+
'menu.copyNamesN': 'คัดลอกชื่อ {n} รายการ',
|
|
6299
|
+
'menu.copyHashesN': 'คัดลอกแฮช {n} รายการ',
|
|
6300
|
+
'menu.copyPathsN': 'คัดลอกพาธ {n} รายการ',
|
|
6301
|
+
'menu.stageN': 'สเตจ {n} รายการ',
|
|
6302
|
+
'menu.unstageN': 'ยกเลิกสเตจ {n} รายการ',
|
|
6303
|
+
'menu.discardN': 'ทิ้ง {n} รายการ…',
|
|
6304
|
+
'menu.cherryN': 'Cherry-pick {n} คอมมิต',
|
|
6305
|
+
'menu.deleteLocalN': 'ลบแบรนช์ในเครื่อง {n} รายการ…',
|
|
6306
|
+
'menu.deleteRemoteN': 'ลบแบรนช์รีโมต {n} รายการ…',
|
|
6307
|
+
'menu.deleteTagsN': 'ลบแท็ก {n} รายการ…',
|
|
6308
|
+
'confirm.deleteRefs': 'ลบรีเฟอเรนซ์ที่เลือก? ในเครื่อง {b} รีโมต {r} แท็ก {g}',
|
|
6309
|
+
'confirm.discardMany': 'ทิ้งการเปลี่ยนแปลงที่ยังไม่คอมมิตของ {n} ไฟล์?',
|
|
6310
|
+
'confirm.discardManyUntracked': 'ไฟล์ที่ไม่ได้ติดตาม {m} ไฟล์จะถูกลบถาวร',
|
|
6311
|
+
'toast.branchesDeleted': 'ลบแบรนช์ {n} รายการแล้ว',
|
|
5913
6312
|
'changes.unstageAll': 'ยกเลิกสเตจทั้งหมด',
|
|
5914
6313
|
'changes.groupBy': 'จัดกลุ่มตาม',
|
|
5915
6314
|
'changes.groupFlat': 'รายการแบน',
|
|
@@ -6067,7 +6466,7 @@ window.__ModuleLoader__.load({
|
|
|
6067
6466
|
'lf.truncated': 'แสดงเฉพาะ {count} บรรทัดแรก',
|
|
6068
6467
|
'preview.title': 'ดูตัวอย่าง Git',
|
|
6069
6468
|
'preview.hint': 'คลิก “แสดงความต่าง” ที่ไฟล์หรือคอมมิต — ความต่างจะเปิดที่นี่ในแถบข้าง',
|
|
6070
|
-
'settings.rail.title': '
|
|
6469
|
+
'settings.rail.title': 'แถบการทำงาน',
|
|
6071
6470
|
'settings.rail.hint': 'ทำเครื่องหมายการทำงานที่จะแสดงบนแถบ ลำดับคงที่; ยกเลิกเครื่องหมายเพื่อซ่อน',
|
|
6072
6471
|
'settings.rail.unavailable': 'โฮสต์นี้ไม่มีบริการตั้งค่า ให้ใช้ «ตั้งค่าแถบการทำงาน» ในแผง',
|
|
6073
6472
|
'settings.commit.title': 'ข้อความคอมมิตโดย AI',
|
|
@@ -6226,6 +6625,39 @@ window.__ModuleLoader__.load({
|
|
|
6226
6625
|
})
|
|
6227
6626
|
}
|
|
6228
6627
|
|
|
6628
|
+
/* ---- multi-select shared helpers (selection episode, issue #13) ---- */
|
|
6629
|
+
|
|
6630
|
+
/* Keyboard shortcuts (Ctrl+C / Del / Ctrl+A / Esc) must never hijack the
|
|
6631
|
+
user's own typing or the browser's native copy inside a field. */
|
|
6632
|
+
function isTypingTarget(target) {
|
|
6633
|
+
if (target === null || target === undefined || (target instanceof Element) === false) return false
|
|
6634
|
+
const tag = target.tagName
|
|
6635
|
+
return tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || target.isContentEditable === true
|
|
6636
|
+
}
|
|
6637
|
+
|
|
6638
|
+
/* The modifier that toggles selection: Cmd on macOS, Ctrl everywhere else
|
|
6639
|
+
(VS Code's list convention). Plain Shift is always the range modifier. */
|
|
6640
|
+
function isToggleModifier(event) {
|
|
6641
|
+
return event.metaKey === true || event.ctrlKey === true
|
|
6642
|
+
}
|
|
6643
|
+
|
|
6644
|
+
/* A branch entry's selection key: the kind prefix keeps the (rare) case of
|
|
6645
|
+
a branch and a tag sharing one name from colliding inside one Set. */
|
|
6646
|
+
function branchSelKeyOf(entry) {
|
|
6647
|
+
const kind = entry.tag === true ? 'tag' : entry.remote === true ? 'remote' : 'local'
|
|
6648
|
+
return kind + ':' + entry.name
|
|
6649
|
+
}
|
|
6650
|
+
|
|
6651
|
+
/* Join selected values for the clipboard — one per line, the shape a
|
|
6652
|
+
terminal or an editor expects when pasting a list. */
|
|
6653
|
+
function clipboardLines(values) {
|
|
6654
|
+
return values.join('\n')
|
|
6655
|
+
}
|
|
6656
|
+
|
|
6657
|
+
/* Shared frozen-by-convention empty selection: selection Sets are never
|
|
6658
|
+
mutated in place, so one instance is safe to hand around. */
|
|
6659
|
+
const EMPTY_SELECTION = new Set()
|
|
6660
|
+
|
|
6229
6661
|
/* ============================== api ============================== */
|
|
6230
6662
|
|
|
6231
6663
|
/* ---- remote channel fallback core (pure: tests/smoke.mjs slices this
|
|
@@ -8574,6 +9006,31 @@ window.__ModuleLoader__.load({
|
|
|
8574
9006
|
}, children),
|
|
8575
9007
|
hint === null ? null : E('div', { className: 'dig-settings-subnote' }, hint))
|
|
8576
9008
|
}
|
|
9009
|
+
/* Collapsible sections, HAND-DRAWN. The official disclosure row was ruled
|
|
9010
|
+
out the same way the sibling dsh-gitbash-shell ruled it out (v0.38.0 -
|
|
9011
|
+
0.38.3): the ui-primitives module shape this plugin's restricted require
|
|
9012
|
+
returns does not expose it, so a borrowed component silently degrades to
|
|
9013
|
+
a flat title+stack pile. One button + one conditional draws the whole
|
|
9014
|
+
thing: the header row always renders, the body only while open, and all
|
|
9015
|
+
sections ship collapsed. State lives with the other hooks, above every
|
|
9016
|
+
early return, so a status flip cannot reorder them. */
|
|
9017
|
+
const [folds, setFolds] = useState({})
|
|
9018
|
+
const toggleFold = (key) => {
|
|
9019
|
+
setFolds((previous) => Object.assign({}, previous, { [key]: !(previous[key] === true) }))
|
|
9020
|
+
}
|
|
9021
|
+
const fold = (titleKey, ...body) => {
|
|
9022
|
+
const open = folds[titleKey] === true
|
|
9023
|
+
return E('div', { className: 'dig-fold' },
|
|
9024
|
+
E('button', {
|
|
9025
|
+
type: 'button',
|
|
9026
|
+
className: 'dig-foldHead',
|
|
9027
|
+
'aria-expanded': open ? 'true' : 'false',
|
|
9028
|
+
onClick: () => toggleFold(titleKey),
|
|
9029
|
+
},
|
|
9030
|
+
E('span', { className: 'dig-foldChevron' + (open ? ' dig-foldChevronOpen' : ''), 'aria-hidden': 'true' }, '▸'),
|
|
9031
|
+
E('span', { className: 'dig-foldTitle' }, t(titleKey))),
|
|
9032
|
+
open ? E('div', { className: 'dig-stack' }, body) : null)
|
|
9033
|
+
}
|
|
8577
9034
|
const flush = () => {
|
|
8578
9035
|
pendingFlush.current = null
|
|
8579
9036
|
/* The pending TARGET per field, not "whatever the render-time intent
|
|
@@ -8728,9 +9185,14 @@ window.__ModuleLoader__.load({
|
|
|
8728
9185
|
and a sentence, not controls that silently do nothing. */
|
|
8729
9186
|
const readOnly = snapshot.writable !== true
|
|
8730
9187
|
return wrapCard(E('div', { className: 'dig-settings dig-card-body', 'data-settings-status': status, 'data-settings-readonly': readOnly === true ? 'true' : 'false' },
|
|
8731
|
-
E('div', { className: 'dig-settings-hint' }, t('settings.rail.hint')),
|
|
8732
9188
|
E('div', { className: readOnly === true ? 'dig-settings-note' : 'dig-settings-note dig-settings-note-hidden', 'data-settings-readonly-note': '' }, t('settings.rail.readonly')),
|
|
8733
9189
|
note === '' ? null : E('div', { className: 'dig-settings-error', 'data-settings-error': '' }, note),
|
|
9190
|
+
/* Three collapsible sections, one per functional domain — the header
|
|
9191
|
+
rows are what a collapsed card shows. The readonly/error notes stay
|
|
9192
|
+
OUTSIDE them: they are status, not settings, and must stay visible.
|
|
9193
|
+
Section 1 carries its own hint (it describes exactly this list). */
|
|
9194
|
+
fold('settings.rail.title',
|
|
9195
|
+
E('div', { className: 'dig-settings-hint' }, t('settings.rail.hint')),
|
|
8734
9196
|
E('div', { className: 'dig-settings-list' }, RAIL_SPECS.map((spec) => {
|
|
8735
9197
|
const inputId = 'dig-rail-' + spec.id
|
|
8736
9198
|
const visible = shown(spec)
|
|
@@ -8778,11 +9240,11 @@ window.__ModuleLoader__.load({
|
|
|
8778
9240
|
}),
|
|
8779
9241
|
E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: spec.icon, size: 13 })),
|
|
8780
9242
|
E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t(spec.key)))
|
|
8781
|
-
})),
|
|
8782
|
-
/*
|
|
8783
|
-
the rail rows keep. The conversation tab registers/unregisters
|
|
8784
|
-
(see door 3 in apply), so the flip is visible without a reload. */
|
|
8785
|
-
|
|
9243
|
+
}))),
|
|
9244
|
+
/* Section 2 — placement (issue #8): one switch, the same optimistic-write
|
|
9245
|
+
contract the rail rows keep. The conversation tab registers/unregisters
|
|
9246
|
+
live (see door 3 in apply), so the flip is visible without a reload. */
|
|
9247
|
+
fold('settings.placement.title',
|
|
8786
9248
|
(() => {
|
|
8787
9249
|
const field = 'conversationTab'
|
|
8788
9250
|
const inputId = 'dig-placement-' + field
|
|
@@ -8865,8 +9327,10 @@ window.__ModuleLoader__.load({
|
|
|
8865
9327
|
E('span', { className: 'dig-settings-glyph' }, E(Icon, { name: 'commit', size: 13 })),
|
|
8866
9328
|
E('span', { id: inputId + '-label', className: 'dig-settings-label', onClick: toggle }, t('settings.placement.immersive'))),
|
|
8867
9329
|
E('div', { className: 'dig-settings-subnote' }, t('settings.placement.immersiveHint')))
|
|
8868
|
-
})(),
|
|
8869
|
-
|
|
9330
|
+
})()),
|
|
9331
|
+
/* Section 3 — the AI commit message (issue #6): model, reasoning and the
|
|
9332
|
+
prompt template. */
|
|
9333
|
+
fold('settings.commit.title',
|
|
8870
9334
|
(() => {
|
|
8871
9335
|
/* The model row is a picker over the host's provider catalog; while
|
|
8872
9336
|
the catalog has not answered (or cannot), the row stays the plain
|
|
@@ -8912,7 +9376,7 @@ window.__ModuleLoader__.load({
|
|
|
8912
9376
|
}
|
|
8913
9377
|
return textRow('commitReasoning', 'settings.commit.reasoning', 'settings.commit.reasoningHint', 60, false)
|
|
8914
9378
|
})(),
|
|
8915
|
-
textRow('commitPrompt', 'settings.commit.prompt', 'settings.commit.promptHint', 2000, true)))
|
|
9379
|
+
textRow('commitPrompt', 'settings.commit.prompt', 'settings.commit.promptHint', 2000, true))))
|
|
8916
9380
|
}
|
|
8917
9381
|
|
|
8918
9382
|
function RailSettings(props) {
|
|
@@ -9259,14 +9723,16 @@ window.__ModuleLoader__.load({
|
|
|
9259
9723
|
const tone = kind === 'tag' ? 'warn' : kind === 'remote' ? 'violet' : kind === 'head' ? 'success' : 'accent'
|
|
9260
9724
|
const glyph = kind === 'tag' ? 'tag' : kind === 'remote' ? 'fetch' : kind === 'head' ? 'star' : 'branch'
|
|
9261
9725
|
const where = entry.upstream === null || entry.upstream === undefined ? entry.name : entry.name + ' → ' + entry.upstream
|
|
9262
|
-
// A single click only SELECTS
|
|
9263
|
-
//
|
|
9264
|
-
//
|
|
9726
|
+
// A single click only SELECTS (now with Shift/Ctrl+click multi-select —
|
|
9727
|
+
// the modifiers are resolved one level up in BranchTree): checking a
|
|
9728
|
+
// branch out is a real change of working tree, and a stray click in a
|
|
9729
|
+
// long list must not do that (reported on v0.3.7). Double-click checks
|
|
9730
|
+
// out, right-click has the whole menu.
|
|
9265
9731
|
return E('div', {
|
|
9266
|
-
className: 'dig-row dig-row-' + kind + (props.
|
|
9732
|
+
className: 'dig-row dig-row-' + kind + (props.selected === true ? ' dig-row-selected' : ''),
|
|
9267
9733
|
style: { paddingLeft: (BRANCH_INDENT_BASE + (props.depth === undefined ? 0 : props.depth) * BRANCH_INDENT_STEP) + 'px' },
|
|
9268
9734
|
title: where + ' · ' + t('branches.pickHint'),
|
|
9269
|
-
onClick: () => props.onPick(entry),
|
|
9735
|
+
onClick: (event) => props.onPick(event, entry, props.selKey),
|
|
9270
9736
|
onDoubleClick: () => props.onCheckout(entry),
|
|
9271
9737
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, entry) },
|
|
9272
9738
|
},
|
|
@@ -9287,7 +9753,12 @@ window.__ModuleLoader__.load({
|
|
|
9287
9753
|
const t = props.t
|
|
9288
9754
|
const [filter, setFilter] = useState('')
|
|
9289
9755
|
const [collapsed, setCollapsed] = useState(() => treeSectionsOf(readTreeState()))
|
|
9290
|
-
|
|
9756
|
+
/* Multi-select (issue #13): the old single `picked` name becomes a Set of
|
|
9757
|
+
selKeys (`local:name` / `remote:name` / `tag:name` — the prefix keeps a
|
|
9758
|
+
branch and a tag that share a name from colliding). */
|
|
9759
|
+
const [picks, setPicks] = useState(() => new Set())
|
|
9760
|
+
const pickAnchor = useRef(null)
|
|
9761
|
+
const visibleRef = useRef([])
|
|
9291
9762
|
const [foldedFolders, setFoldedFolders] = useState(() => {
|
|
9292
9763
|
const stored = readTreeState()
|
|
9293
9764
|
return stored !== null && typeof stored === 'object' && stored.folders !== null && typeof stored.folders === 'object'
|
|
@@ -9310,6 +9781,21 @@ window.__ModuleLoader__.load({
|
|
|
9310
9781
|
const remotes = (branches === null ? [] : branches.remote).filter((entry) => match(entry.name))
|
|
9311
9782
|
const tags = (branches === null ? [] : branches.tags).filter((entry) => match(entry.name))
|
|
9312
9783
|
const favorites = Array.isArray(props.favorites) ? props.favorites : []
|
|
9784
|
+
/* A refresh that makes a picked ref disappear must not keep it selected. */
|
|
9785
|
+
useEffect(() => {
|
|
9786
|
+
setPicks((previous) => {
|
|
9787
|
+
if (previous.size === 0) return previous
|
|
9788
|
+
const alive = new Set()
|
|
9789
|
+
for (const entry of locals) alive.add(branchSelKeyOf(entry))
|
|
9790
|
+
for (const entry of remotes) alive.add(branchSelKeyOf(entry))
|
|
9791
|
+
for (const entry of tags) alive.add(branchSelKeyOf(entry))
|
|
9792
|
+
const next = new Set()
|
|
9793
|
+
for (const key of previous) {
|
|
9794
|
+
if (alive.has(key) === true) next.add(key)
|
|
9795
|
+
}
|
|
9796
|
+
return next.size === previous.size ? previous : next
|
|
9797
|
+
})
|
|
9798
|
+
}, [branches])
|
|
9313
9799
|
/**
|
|
9314
9800
|
* Branch names nest on '/', the way an IDE shows a branch namespace: clicking a
|
|
9315
9801
|
* row only SELECTS it (checking out rewrites the working tree), so the row
|
|
@@ -9330,11 +9816,48 @@ window.__ModuleLoader__.load({
|
|
|
9330
9816
|
return root
|
|
9331
9817
|
}
|
|
9332
9818
|
const countLeaves = (node) => node.leaves.length + Array.from(node.folders.values()).reduce((sum, child) => sum + countLeaves(child), 0)
|
|
9819
|
+
/* Depth-first flatten in the EXACT order the tree renders them: expanded
|
|
9820
|
+
folders before their leaves. Shift-ranges and Ctrl+A walk this one
|
|
9821
|
+
order; folded folders contribute nothing (their rows are not shown). */
|
|
9822
|
+
const flatLeaves = (node, prefix, depth, out) => {
|
|
9823
|
+
for (const name of Array.from(node.folders.keys()).sort()) {
|
|
9824
|
+
const id = prefix === '' ? name : prefix + '/' + name
|
|
9825
|
+
if (foldedFolders[id] !== true) flatLeaves(node.folders.get(name), id, depth + 1, out)
|
|
9826
|
+
}
|
|
9827
|
+
for (const leaf of node.leaves) out.push({ entry: leaf.entry, label: leaf.label, depth: depth })
|
|
9828
|
+
return out
|
|
9829
|
+
}
|
|
9830
|
+
const handlePick = useCallback((event, entry, selKey) => {
|
|
9831
|
+
const ordered = visibleRef.current.map((leaf) => branchSelKeyOf(leaf.entry))
|
|
9832
|
+
if (event.shiftKey === true && pickAnchor.current !== null) {
|
|
9833
|
+
const from = ordered.indexOf(pickAnchor.current)
|
|
9834
|
+
const to = ordered.indexOf(selKey)
|
|
9835
|
+
if (from >= 0 && to >= 0) {
|
|
9836
|
+
const low = Math.min(from, to)
|
|
9837
|
+
const high = Math.max(from, to)
|
|
9838
|
+
const next = new Set(picks)
|
|
9839
|
+
for (let index = low; index <= high; index += 1) next.add(ordered[index])
|
|
9840
|
+
setPicks(next)
|
|
9841
|
+
return
|
|
9842
|
+
}
|
|
9843
|
+
}
|
|
9844
|
+
if (isToggleModifier(event) === true) {
|
|
9845
|
+
const next = new Set(picks)
|
|
9846
|
+
if (next.has(selKey) === true) next.delete(selKey)
|
|
9847
|
+
else next.add(selKey)
|
|
9848
|
+
pickAnchor.current = selKey
|
|
9849
|
+
setPicks(next)
|
|
9850
|
+
return
|
|
9851
|
+
}
|
|
9852
|
+
pickAnchor.current = selKey
|
|
9853
|
+
setPicks(new Set([selKey]))
|
|
9854
|
+
}, [picks])
|
|
9333
9855
|
const branchRow = (entry, label, depth) => E(BranchRow, {
|
|
9334
|
-
key: 'row:' + entry
|
|
9335
|
-
|
|
9336
|
-
|
|
9337
|
-
|
|
9856
|
+
key: 'row:' + branchSelKeyOf(entry), entry: entry, label: label, depth: depth, t: t,
|
|
9857
|
+
selKey: branchSelKeyOf(entry),
|
|
9858
|
+
selected: picks.has(branchSelKeyOf(entry)) === true,
|
|
9859
|
+
onPick: handlePick,
|
|
9860
|
+
onCheckout: props.onCheckout, onMenu: onRowMenu,
|
|
9338
9861
|
})
|
|
9339
9862
|
const rowsOf = (node, prefix, depth) => {
|
|
9340
9863
|
const out = []
|
|
@@ -9369,7 +9892,71 @@ window.__ModuleLoader__.load({
|
|
|
9369
9892
|
grouped === true
|
|
9370
9893
|
? rowsOf(forestOf(entries.map((entry) => decorate(entry))), '', 0)
|
|
9371
9894
|
: entries.map((entry) => branchRow(decorate(entry), undefined, 0))))
|
|
9372
|
-
|
|
9895
|
+
/* Render-order mirror for Shift-ranges and Ctrl+A, plus the selKey→entry
|
|
9896
|
+
lookup the right-click menu and the Delete key resolve through. */
|
|
9897
|
+
const visibleLeaves = []
|
|
9898
|
+
if (favorites.length > 0) flatLeaves(forestOf(locals.filter((entry) => favorites.indexOf(entry.name) >= 0)), '', 0, visibleLeaves)
|
|
9899
|
+
flatLeaves(forestOf(locals), '', 0, visibleLeaves)
|
|
9900
|
+
for (const entry of remotes) visibleLeaves.push({ entry: entry, label: undefined, depth: 0 })
|
|
9901
|
+
for (const entry of tags) visibleLeaves.push({ entry: entry, label: undefined, depth: 0 })
|
|
9902
|
+
visibleRef.current = visibleLeaves
|
|
9903
|
+
const entryBySelKey = new Map()
|
|
9904
|
+
for (const entry of locals) entryBySelKey.set(branchSelKeyOf(entry), { selKey: branchSelKeyOf(entry), kind: 'local', name: entry.name, entry: entry })
|
|
9905
|
+
for (const entry of remotes) entryBySelKey.set(branchSelKeyOf(entry), { selKey: branchSelKeyOf(entry), kind: 'remote', name: entry.name, entry: entry })
|
|
9906
|
+
for (const entry of tags) entryBySelKey.set(branchSelKeyOf(entry), { selKey: branchSelKeyOf(entry), kind: 'tag', name: entry.name, entry: entry })
|
|
9907
|
+
const pickedEntries = () => {
|
|
9908
|
+
const entries = []
|
|
9909
|
+
for (const key of picks) {
|
|
9910
|
+
const found = entryBySelKey.get(key)
|
|
9911
|
+
if (found !== undefined) entries.push(found)
|
|
9912
|
+
}
|
|
9913
|
+
return entries
|
|
9914
|
+
}
|
|
9915
|
+
const onRowMenu = (event, entry) => {
|
|
9916
|
+
const selKey = branchSelKeyOf(entry)
|
|
9917
|
+
// Right-clicking a row that is part of a multi-select acts on the WHOLE
|
|
9918
|
+
// selection; anything else collapses to the ordinary single-row menu.
|
|
9919
|
+
if (picks.has(selKey) === true && picks.size > 1 && props.onBranchMenu !== undefined) {
|
|
9920
|
+
props.onBranchMenu(event, entry, pickedEntries())
|
|
9921
|
+
return
|
|
9922
|
+
}
|
|
9923
|
+
props.onBranchMenu(event, entry, null)
|
|
9924
|
+
}
|
|
9925
|
+
/* List-scope keyboard: Ctrl/Cmd+C copies the picked names, Delete asks
|
|
9926
|
+
Panel for a grouped delete (head branches are skipped — git refuses
|
|
9927
|
+
them anyway), Ctrl+A selects every visible row, Esc clears. */
|
|
9928
|
+
const onTreeKeyDown = (event) => {
|
|
9929
|
+
if (isTypingTarget(event.target) === true) return
|
|
9930
|
+
const mod = isToggleModifier(event)
|
|
9931
|
+
if (mod === true && (event.key === 'c' || event.key === 'C')) {
|
|
9932
|
+
if (picks.size === 0 || props.onCopyValues === undefined) return
|
|
9933
|
+
event.preventDefault()
|
|
9934
|
+
props.onCopyValues(pickedEntries().map((item) => item.name))
|
|
9935
|
+
return
|
|
9936
|
+
}
|
|
9937
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
9938
|
+
if (picks.size === 0 || props.onDeleteEntries === undefined) return
|
|
9939
|
+
const entries = pickedEntries().filter((item) => item.kind !== 'local' || item.entry.head !== true)
|
|
9940
|
+
if (entries.length === 0) return
|
|
9941
|
+
event.preventDefault()
|
|
9942
|
+
props.onDeleteEntries(entries)
|
|
9943
|
+
return
|
|
9944
|
+
}
|
|
9945
|
+
if (mod === true && (event.key === 'a' || event.key === 'A')) {
|
|
9946
|
+
event.preventDefault()
|
|
9947
|
+
setPicks(new Set(visibleLeaves.map((leaf) => branchSelKeyOf(leaf.entry))))
|
|
9948
|
+
return
|
|
9949
|
+
}
|
|
9950
|
+
if (event.key === 'Escape') setPicks(new Set())
|
|
9951
|
+
}
|
|
9952
|
+
return E('div', {
|
|
9953
|
+
className: 'dig-tree',
|
|
9954
|
+
tabIndex: 0,
|
|
9955
|
+
onKeyDown: onTreeKeyDown,
|
|
9956
|
+
onPointerDownCapture: (event) => {
|
|
9957
|
+
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
9958
|
+
},
|
|
9959
|
+
},
|
|
9373
9960
|
E('div', { className: 'dig-search' },
|
|
9374
9961
|
E('input', {
|
|
9375
9962
|
className: 'dig-input dig-input-compact', value: filter, spellCheck: false,
|
|
@@ -9403,9 +9990,11 @@ window.__ModuleLoader__.load({
|
|
|
9403
9990
|
const readOnly = props.group === 'ignored'
|
|
9404
9991
|
const status = readOnly ? 'I' : (raw === '' ? 'M' : raw.charAt(0))
|
|
9405
9992
|
return E('div', {
|
|
9406
|
-
className: 'dig-row dig-row-file',
|
|
9993
|
+
className: 'dig-row dig-row-file' + (props.selected === true ? ' dig-row-selected' : ''),
|
|
9407
9994
|
title: item.origPath === undefined ? item.path : item.origPath + ' → ' + item.path,
|
|
9408
|
-
|
|
9995
|
+
/* The click is handled ONE level up (ChangesPanel): plain click still
|
|
9996
|
+
opens the diff, Shift/Ctrl+click join or toggle the multi-select. */
|
|
9997
|
+
onClick: (event) => props.onRowClick(event, item, props.group),
|
|
9409
9998
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, item, props.group) },
|
|
9410
9999
|
},
|
|
9411
10000
|
FileTypeGlyph({ path: item.path }),
|
|
@@ -9420,7 +10009,7 @@ window.__ModuleLoader__.load({
|
|
|
9420
10009
|
? E('button', { type: 'button', className: 'dig-mini', title: t('action.unstage'), onClick: (event) => { event.stopPropagation(); props.onUnstage(item) } }, E(Icon, { name: 'minus', size: 12 }))
|
|
9421
10010
|
: E('button', { type: 'button', className: 'dig-mini', title: t('action.stage'), onClick: (event) => { event.stopPropagation(); props.onStage(item) } }, E(Icon, { name: 'plus', size: 12 }))),
|
|
9422
10011
|
readOnly ? null : E('button', { type: 'button', className: 'dig-mini', title: t('action.discard'), onClick: (event) => { event.stopPropagation(); props.onDiscard(item, props.group) } }, E(Icon, { name: 'undo', size: 12 })))
|
|
9423
|
-
}, (prev, next) => prev.item === next.item && prev.group === next.group && prev.t === next.t)
|
|
10012
|
+
}, (prev, next) => prev.item === next.item && prev.group === next.group && prev.t === next.t && prev.selected === next.selected)
|
|
9424
10013
|
|
|
9425
10014
|
function ChangesPanel(props) {
|
|
9426
10015
|
const t = props.t
|
|
@@ -9457,6 +10046,154 @@ window.__ModuleLoader__.load({
|
|
|
9457
10046
|
state directly through props. */
|
|
9458
10047
|
const foldedDirs = props.foldedDirs === undefined ? {} : props.foldedDirs
|
|
9459
10048
|
const setFoldedDirs = props.onFoldedDirs
|
|
10049
|
+
/* ---- multi-select (issue #13): the state lives in Panel so the right-click
|
|
10050
|
+
menu and the keyboard shortcuts can act on it; this panel renders it,
|
|
10051
|
+
feeds it from clicks/drag, and mirrors it onto row classes. ---- */
|
|
10052
|
+
const selection = props.selection === undefined ? EMPTY_SELECTION : props.selection
|
|
10053
|
+
const setSelection = props.onSelectionChange
|
|
10054
|
+
const anchorRef = props.anchorRef
|
|
10055
|
+
const changeItemsRef = useRef(null)
|
|
10056
|
+
const changeOffsetsRef = useRef(null)
|
|
10057
|
+
const [marquee, setMarquee] = useState(null)
|
|
10058
|
+
const marqueeFrame = useRef(0)
|
|
10059
|
+
const marqueeState = useRef(null)
|
|
10060
|
+
const marqueeSwallowClick = useRef(false)
|
|
10061
|
+
/* Rows in their RENDER order (the order windowing flattens them into) —
|
|
10062
|
+
Shift-click spans follow what the eye sees, not the git order. */
|
|
10063
|
+
const orderedRowKeys = () => {
|
|
10064
|
+
const items = changeItemsRef.current
|
|
10065
|
+
const keys = []
|
|
10066
|
+
if (items === null) return keys
|
|
10067
|
+
for (const entry of items) {
|
|
10068
|
+
if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) keys.push(entry.key)
|
|
10069
|
+
}
|
|
10070
|
+
return keys
|
|
10071
|
+
}
|
|
10072
|
+
const handleRowClick = useCallback((event, item, group) => {
|
|
10073
|
+
const key = group + ':' + item.path
|
|
10074
|
+
/* Functional updates ONLY: the memoised rows may hold a stale closure
|
|
10075
|
+
of this handler (their memo compares item/group/t/selected, not the
|
|
10076
|
+
callback — same stale-callback trade-off as the stage buttons), so
|
|
10077
|
+
the selection must never be read from it. */
|
|
10078
|
+
if (event.shiftKey === true && anchorRef !== undefined && anchorRef.current !== null) {
|
|
10079
|
+
const ordered = orderedRowKeys()
|
|
10080
|
+
const from = ordered.indexOf(anchorRef.current)
|
|
10081
|
+
const to = ordered.indexOf(key)
|
|
10082
|
+
if (from >= 0 && to >= 0) {
|
|
10083
|
+
const low = Math.min(from, to)
|
|
10084
|
+
const high = Math.max(from, to)
|
|
10085
|
+
setSelection((previous) => {
|
|
10086
|
+
const next = new Set(previous)
|
|
10087
|
+
for (let index = low; index <= high; index += 1) next.add(ordered[index])
|
|
10088
|
+
return next
|
|
10089
|
+
})
|
|
10090
|
+
return
|
|
10091
|
+
}
|
|
10092
|
+
}
|
|
10093
|
+
if (isToggleModifier(event) === true) {
|
|
10094
|
+
anchorRef.current = key
|
|
10095
|
+
setSelection((previous) => {
|
|
10096
|
+
const next = new Set(previous)
|
|
10097
|
+
if (next.has(key) === true) next.delete(key)
|
|
10098
|
+
else next.add(key)
|
|
10099
|
+
return next
|
|
10100
|
+
})
|
|
10101
|
+
return
|
|
10102
|
+
}
|
|
10103
|
+
anchorRef.current = key
|
|
10104
|
+
setSelection((previous) => previous.size === 0 ? previous : new Set())
|
|
10105
|
+
props.onDiff(item, group)
|
|
10106
|
+
}, [setSelection, anchorRef, props.onDiff])
|
|
10107
|
+
/* ---- rubber-band selection over the windowed list: the fixed row
|
|
10108
|
+
heights + the prefix-sum offsets turn a pixel span back into the
|
|
10109
|
+
row-index span (the same windowStart binary search the window uses),
|
|
10110
|
+
so the drag never needs the DOM rows to exist. The marquee box itself
|
|
10111
|
+
is a viewport-anchored absolute div inside .dig-changes (invariant 13:
|
|
10112
|
+
never position:fixed inside the workbench's contain:layout). ---- */
|
|
10113
|
+
const marqueeKeysAt = (contentTop, contentBottom) => {
|
|
10114
|
+
const items = changeItemsRef.current
|
|
10115
|
+
const offsets = changeOffsetsRef.current
|
|
10116
|
+
if (items === null || offsets === null || items.length === 0) return []
|
|
10117
|
+
const first = Math.max(0, windowStart(offsets, contentTop))
|
|
10118
|
+
const last = Math.min(items.length - 1, windowStart(offsets, contentBottom))
|
|
10119
|
+
const keys = []
|
|
10120
|
+
for (let index = first; index <= last; index += 1) {
|
|
10121
|
+
const entry = items[index]
|
|
10122
|
+
if (entry.kind === 'row' && entry.key.indexOf('ignored:') !== 0) keys.push(entry.key)
|
|
10123
|
+
}
|
|
10124
|
+
return keys
|
|
10125
|
+
}
|
|
10126
|
+
const syncMarquee = () => {
|
|
10127
|
+
const state = marqueeState.current
|
|
10128
|
+
const host = changesScrollRef.current
|
|
10129
|
+
if (state === null || host === null) return
|
|
10130
|
+
const rect = host.getBoundingClientRect()
|
|
10131
|
+
const contentY0 = state.startY - rect.top + state.scrollY0
|
|
10132
|
+
const contentY1 = state.clientY - rect.top + host.scrollTop
|
|
10133
|
+
setSelection(new Set(marqueeKeysAt(Math.min(contentY0, contentY1), Math.max(contentY0, contentY1))))
|
|
10134
|
+
setMarquee({
|
|
10135
|
+
x: Math.min(state.startX, state.clientX) - rect.left,
|
|
10136
|
+
y: Math.min(state.startY, state.clientY) - rect.top,
|
|
10137
|
+
w: Math.abs(state.clientX - state.startX),
|
|
10138
|
+
h: Math.abs(state.clientY - state.startY),
|
|
10139
|
+
})
|
|
10140
|
+
/* Auto-scroll when the pointer rides an edge; keep the loop alive while
|
|
10141
|
+
it does so the selection follows the content streaming past. */
|
|
10142
|
+
const EDGE = 28
|
|
10143
|
+
let speed = 0
|
|
10144
|
+
if (state.clientY - rect.top < EDGE) speed = -Math.ceil((EDGE - (state.clientY - rect.top)) / 3)
|
|
10145
|
+
else if (rect.bottom - state.clientY < EDGE) speed = Math.ceil((EDGE - (rect.bottom - state.clientY)) / 3)
|
|
10146
|
+
if (speed !== 0) {
|
|
10147
|
+
host.scrollTop += speed * 2
|
|
10148
|
+
cancelAnimationFrame(marqueeFrame.current)
|
|
10149
|
+
marqueeFrame.current = requestAnimationFrame(syncMarquee)
|
|
10150
|
+
}
|
|
10151
|
+
}
|
|
10152
|
+
const scheduleMarqueeSync = () => {
|
|
10153
|
+
cancelAnimationFrame(marqueeFrame.current)
|
|
10154
|
+
marqueeFrame.current = requestAnimationFrame(syncMarquee)
|
|
10155
|
+
}
|
|
10156
|
+
const onListPointerDown = (event) => {
|
|
10157
|
+
if (event.button !== 0) return
|
|
10158
|
+
const host = changesScrollRef.current
|
|
10159
|
+
if (host === null) return
|
|
10160
|
+
if (event.target instanceof Element && event.target.closest('button, input, textarea, select, a') !== null) return
|
|
10161
|
+
// Keep the browser from starting a text selection under the drag; the
|
|
10162
|
+
// real clicks still fire (only the selection machinery is suppressed).
|
|
10163
|
+
event.preventDefault()
|
|
10164
|
+
const rect = host.getBoundingClientRect()
|
|
10165
|
+
marqueeState.current = {
|
|
10166
|
+
startX: event.clientX,
|
|
10167
|
+
startY: event.clientY,
|
|
10168
|
+
scrollY0: host.scrollTop,
|
|
10169
|
+
clientX: event.clientX,
|
|
10170
|
+
clientY: event.clientY,
|
|
10171
|
+
active: false,
|
|
10172
|
+
}
|
|
10173
|
+
const onMove = (moveEvent) => {
|
|
10174
|
+
const state = marqueeState.current
|
|
10175
|
+
if (state === null) return
|
|
10176
|
+
state.clientX = moveEvent.clientX
|
|
10177
|
+
state.clientY = moveEvent.clientY
|
|
10178
|
+
if (state.active === false && Math.abs(moveEvent.clientX - state.startX) < 4 && Math.abs(moveEvent.clientY - state.startY) < 4) return
|
|
10179
|
+
state.active = true
|
|
10180
|
+
marqueeSwallowClick.current = true
|
|
10181
|
+
scheduleMarqueeSync()
|
|
10182
|
+
}
|
|
10183
|
+
const onUp = () => {
|
|
10184
|
+
window.removeEventListener('pointermove', onMove)
|
|
10185
|
+
window.removeEventListener('pointerup', onUp)
|
|
10186
|
+
cancelAnimationFrame(marqueeFrame.current)
|
|
10187
|
+
const wasActive = marqueeState.current !== null && marqueeState.current.active === true
|
|
10188
|
+
marqueeState.current = null
|
|
10189
|
+
setMarquee(null)
|
|
10190
|
+
// The drag's pointerup is followed by a synthetic click on whatever
|
|
10191
|
+
// row is under it — swallow exactly that one click.
|
|
10192
|
+
if (wasActive === true) setTimeout(() => { marqueeSwallowClick.current = false }, 0)
|
|
10193
|
+
}
|
|
10194
|
+
window.addEventListener('pointermove', onMove)
|
|
10195
|
+
window.addEventListener('pointerup', onUp)
|
|
10196
|
+
}
|
|
9460
10197
|
const changes = summary === null ? null : summary.changes
|
|
9461
10198
|
const conflicted = changes === null ? [] : changes.conflicted
|
|
9462
10199
|
const staged = changes === null ? [] : changes.staged
|
|
@@ -9505,6 +10242,8 @@ window.__ModuleLoader__.load({
|
|
|
9505
10242
|
}
|
|
9506
10243
|
const entryRow = (key, item) => E(ChangeRow, {
|
|
9507
10244
|
key: key + ':' + item.path, item: item, group: key, t: t,
|
|
10245
|
+
selected: selection.has(key + ':' + item.path) === true,
|
|
10246
|
+
onRowClick: handleRowClick,
|
|
9508
10247
|
onStage: props.onStage, onUnstage: props.onUnstage, onDiscard: props.onDiscard, onDiff: props.onDiff, onMenu: props.onChangeMenu,
|
|
9509
10248
|
})
|
|
9510
10249
|
const folderOf = (item) => {
|
|
@@ -9572,12 +10311,17 @@ window.__ModuleLoader__.load({
|
|
|
9572
10311
|
emitGroup('untracked', fill(t('counts.untracked'), { n: untracked.length }), untracked)
|
|
9573
10312
|
emitGroup('ignored', fill(t('counts.ignored'), { n: ignored.length }), ignored)
|
|
9574
10313
|
return out
|
|
9575
|
-
}, [conflicted, staged, unstaged, untracked, ignored, groupBy, foldedDirs, sortKey, t, props.onUnstageAll, props.onStageAll])
|
|
10314
|
+
}, [conflicted, staged, unstaged, untracked, ignored, groupBy, foldedDirs, sortKey, t, selection, handleRowClick, props.onUnstageAll, props.onStageAll])
|
|
9576
10315
|
const changeOffsets = useMemo(() => {
|
|
9577
10316
|
const sums = [0]
|
|
9578
10317
|
for (const item of changeItems) sums.push(sums[sums.length - 1] + item.h)
|
|
9579
10318
|
return sums
|
|
9580
10319
|
}, [changeItems])
|
|
10320
|
+
/* Render-phase mirrors: the drag-select and Shift-range logic read these
|
|
10321
|
+
through refs, so their callbacks keep stable identities (invariant 12:
|
|
10322
|
+
a dep array is evaluated during render — no forward references). */
|
|
10323
|
+
changeItemsRef.current = changeItems
|
|
10324
|
+
changeOffsetsRef.current = changeOffsets
|
|
9581
10325
|
/* Virtual window, VSCode-style (ListView.getVisibleRange): both ends are
|
|
9582
10326
|
pixel→index conversions over the same prefix-sum array, so no "row
|
|
9583
10327
|
count" ever enters the math (issue #12 — the old winLimit computed a
|
|
@@ -9675,11 +10419,34 @@ window.__ModuleLoader__.load({
|
|
|
9675
10419
|
'data-action': 'more',
|
|
9676
10420
|
onClick: (event) => props.onMoreMenu(event),
|
|
9677
10421
|
}, E(Icon, { name: 'more', size: 18 })))
|
|
10422
|
+
/* The selection action bar: visible only while a multi-select exists, so
|
|
10423
|
+
the mouse-only path (no right-click, no keyboard) keeps full power. */
|
|
10424
|
+
const selectionBar = selection.size === 0 ? null : E('div', { className: 'dig-selection-bar' },
|
|
10425
|
+
E('span', { className: 'dig-selection-count' }, fill(t('selection.count'), { n: selection.size })),
|
|
10426
|
+
props.onBulkStage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkStage() }, t('action.stage')),
|
|
10427
|
+
props.onBulkUnstage === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onBulkUnstage() }, t('action.unstage')),
|
|
10428
|
+
props.onBulkDiscard === undefined ? null : E('button', { type: 'button', className: 'dig-link dig-link-danger', onClick: () => props.onBulkDiscard() }, t('action.discard')),
|
|
10429
|
+
props.onCopySelection === undefined ? null : E('button', { type: 'button', className: 'dig-link', onClick: () => props.onCopySelection() }, t('selection.copyPaths')),
|
|
10430
|
+
E('button', { type: 'button', className: 'dig-link', onClick: () => setSelection(new Set()) }, t('selection.clear')))
|
|
9678
10431
|
return E('div', { className: 'dig-changes' },
|
|
9679
10432
|
aiConfirmDialog,
|
|
9680
10433
|
head,
|
|
10434
|
+
selectionBar,
|
|
9681
10435
|
E('div', { className: 'dig-changes-body' },
|
|
9682
|
-
E('div', {
|
|
10436
|
+
E('div', {
|
|
10437
|
+
className: 'dig-changes-list', ref: changesScrollRef,
|
|
10438
|
+
/* Focusable list: the keyboard shortcuts (Ctrl+C / Del / Ctrl+A /
|
|
10439
|
+
Esc) land on whichever list currently holds focus. */
|
|
10440
|
+
tabIndex: 0,
|
|
10441
|
+
onKeyDown: props.onKeyDown,
|
|
10442
|
+
onPointerDownCapture: (event) => {
|
|
10443
|
+
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
10444
|
+
},
|
|
10445
|
+
onPointerDown: onListPointerDown,
|
|
10446
|
+
onClickCapture: (event) => {
|
|
10447
|
+
if (marqueeSwallowClick.current === true) { event.preventDefault(); event.stopPropagation() }
|
|
10448
|
+
},
|
|
10449
|
+
},
|
|
9683
10450
|
collapsed ? null : [
|
|
9684
10451
|
total === 0 && ignored.length === 0 ? E('div', { className: 'dig-empty', key: 'empty' }, t('changes.empty')) : null,
|
|
9685
10452
|
/* Windowed slice: spacers above/below stand in for the unmounted
|
|
@@ -9688,7 +10455,11 @@ window.__ModuleLoader__.load({
|
|
|
9688
10455
|
visibleItems.map((item) => E(Fragment, { key: item.key }, item.node)),
|
|
9689
10456
|
winLast < changeItems.length ? E('div', { key: 'pad-bottom', style: { height: String(changeOffsets[changeItems.length] - changeOffsets[winLast]) + 'px' } }) : null,
|
|
9690
10457
|
]),
|
|
9691
|
-
composer)
|
|
10458
|
+
composer),
|
|
10459
|
+
marquee === null ? null : E('div', {
|
|
10460
|
+
className: 'dig-marquee',
|
|
10461
|
+
style: { left: marquee.x + 'px', top: marquee.y + 'px', width: marquee.w + 'px', height: marquee.h + 'px' },
|
|
10462
|
+
}))
|
|
9692
10463
|
}
|
|
9693
10464
|
|
|
9694
10465
|
/* ============================== history ============================== */
|
|
@@ -9812,7 +10583,9 @@ window.__ModuleLoader__.load({
|
|
|
9812
10583
|
graph stroke whose data-lane differs — zero React re-renders. */
|
|
9813
10584
|
'data-lane': String(props.row.lane),
|
|
9814
10585
|
className: 'dig-commit' + (props.selected === true ? ' dig-commit-selected' : ''),
|
|
9815
|
-
|
|
10586
|
+
/* Modifier handling lives in HistoryList (it owns the rendered row
|
|
10587
|
+
order the Shift-range walks); a plain click still opens the detail. */
|
|
10588
|
+
onClick: (event) => props.onRowClick(event, commit),
|
|
9816
10589
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, commit) },
|
|
9817
10590
|
},
|
|
9818
10591
|
E('span', { className: 'dig-commit-date', title: shortDate(commit.date) }, relativeTime(commit.date)),
|
|
@@ -9861,6 +10634,7 @@ window.__ModuleLoader__.load({
|
|
|
9861
10634
|
const commits = props.commits
|
|
9862
10635
|
const historyScrollRef = useRef(null)
|
|
9863
10636
|
const viewport = useViewportBox(historyScrollRef)
|
|
10637
|
+
const pickAnchor = useRef(null)
|
|
9864
10638
|
const [text, setText] = useState('')
|
|
9865
10639
|
const [author, setAuthor] = useState('')
|
|
9866
10640
|
const [refName, setRefName] = useState('')
|
|
@@ -9915,6 +10689,59 @@ window.__ModuleLoader__.load({
|
|
|
9915
10689
|
}, [commits, text, author, refName, since, order])
|
|
9916
10690
|
|
|
9917
10691
|
const rows = useMemo(() => buildRows(filtered), [filtered])
|
|
10692
|
+
/* Multi-select gestures (issue #13): the rendered row order is what the
|
|
10693
|
+
Shift-range walks, and the range is computed over the FILTERED rows —
|
|
10694
|
+
the same list the eye sees. Plain click keeps opening the detail and
|
|
10695
|
+
collapses the multi-select, exactly like the other two lists. The
|
|
10696
|
+
stale-callback risk is nil: rows change identity with the filters, and
|
|
10697
|
+
every row re-renders whenever its commit reference changes. */
|
|
10698
|
+
const handleRowClick = (event, commit) => {
|
|
10699
|
+
const pickedHashes = props.pickedHashes
|
|
10700
|
+
const setPicks = props.onPicksChange
|
|
10701
|
+
if (setPicks === undefined) { props.onSelect(commit); return }
|
|
10702
|
+
const ordered = rows.map((row) => row.commit.hash)
|
|
10703
|
+
const index = ordered.indexOf(commit.hash)
|
|
10704
|
+
/* Functional updates only — memoised CommitRows may hold a stale
|
|
10705
|
+
closure of this handler (their memo compares commit/row/selected),
|
|
10706
|
+
so the picked set is never read from it. */
|
|
10707
|
+
if (event.shiftKey === true && pickAnchor.current !== null) {
|
|
10708
|
+
const from = ordered.indexOf(pickAnchor.current)
|
|
10709
|
+
if (from >= 0 && index >= 0) {
|
|
10710
|
+
const low = Math.min(from, index)
|
|
10711
|
+
const high = Math.max(from, index)
|
|
10712
|
+
setPicks((previous) => {
|
|
10713
|
+
const next = new Set(previous)
|
|
10714
|
+
for (let at = low; at <= high; at += 1) next.add(ordered[at])
|
|
10715
|
+
return next
|
|
10716
|
+
})
|
|
10717
|
+
return
|
|
10718
|
+
}
|
|
10719
|
+
}
|
|
10720
|
+
if (isToggleModifier(event) === true) {
|
|
10721
|
+
pickAnchor.current = commit.hash
|
|
10722
|
+
setPicks((previous) => {
|
|
10723
|
+
const next = new Set(previous)
|
|
10724
|
+
if (next.has(commit.hash) === true) next.delete(commit.hash)
|
|
10725
|
+
else next.add(commit.hash)
|
|
10726
|
+
return next
|
|
10727
|
+
})
|
|
10728
|
+
return
|
|
10729
|
+
}
|
|
10730
|
+
pickAnchor.current = commit.hash
|
|
10731
|
+
setPicks((previous) => previous.size === 0 ? previous : new Set())
|
|
10732
|
+
props.onSelect(commit)
|
|
10733
|
+
}
|
|
10734
|
+
/* Ctrl+A selects the whole filtered list; everything else (Ctrl+C, Esc)
|
|
10735
|
+
belongs to the Panel-level handler that owns the picked set. */
|
|
10736
|
+
const onListKeyDown = (event) => {
|
|
10737
|
+
if (isTypingTarget(event.target) === true) return
|
|
10738
|
+
if (isToggleModifier(event) === true && (event.key === 'a' || event.key === 'A')) {
|
|
10739
|
+
event.preventDefault()
|
|
10740
|
+
if (props.onSelectAll !== undefined) props.onSelectAll(rows.map((row) => row.commit.hash))
|
|
10741
|
+
return
|
|
10742
|
+
}
|
|
10743
|
+
if (props.onKeyDown !== undefined) props.onKeyDown(event)
|
|
10744
|
+
}
|
|
9918
10745
|
const dirtyFilter = text !== '' || author !== '' || refName !== '' || since !== '' || (props.pathFilter !== undefined && props.pathFilter !== '')
|
|
9919
10746
|
const applyPath = (value) => { if (props.onPathFilter !== undefined) props.onPathFilter(value) }
|
|
9920
10747
|
const clearAll = () => {
|
|
@@ -9994,6 +10821,11 @@ window.__ModuleLoader__.load({
|
|
|
9994
10821
|
E('div', {
|
|
9995
10822
|
className: 'dig-history-scroll',
|
|
9996
10823
|
ref: historyScrollRef,
|
|
10824
|
+
tabIndex: 0,
|
|
10825
|
+
onKeyDown: onListKeyDown,
|
|
10826
|
+
onPointerDownCapture: (event) => {
|
|
10827
|
+
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
10828
|
+
},
|
|
9997
10829
|
/* Lane hover highlight (GitLens-style trace): copy the hovered row's
|
|
9998
10830
|
lane into data-hover-lane; generated CSS dims every stroke whose
|
|
9999
10831
|
data-lane differs. Direct DOM dataset writes on purpose — the
|
|
@@ -10013,8 +10845,9 @@ window.__ModuleLoader__.load({
|
|
|
10013
10845
|
visibleRows.map((row) => E(CommitRow, {
|
|
10014
10846
|
key: row.commit.hash,
|
|
10015
10847
|
commit: row.commit, row: row, t: t,
|
|
10016
|
-
selected: props.selectedHash === row.commit.hash
|
|
10017
|
-
|
|
10848
|
+
selected: props.selectedHash === row.commit.hash
|
|
10849
|
+
|| (props.pickedHashes !== undefined && props.pickedHashes.has(row.commit.hash) === true),
|
|
10850
|
+
onRowClick: handleRowClick, onMenu: props.onMenu, openMenuAt: props.openMenuAt,
|
|
10018
10851
|
})),
|
|
10019
10852
|
winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
|
|
10020
10853
|
props.hasMore === true
|
|
@@ -10097,6 +10930,13 @@ window.__ModuleLoader__.load({
|
|
|
10097
10930
|
and the selection menu's payload must say which. Cleared everywhere
|
|
10098
10931
|
the patch is cleared. */
|
|
10099
10932
|
const [diffContext, setDiffContext] = useState(null)
|
|
10933
|
+
/* ---- multi-select (issue #13): the change list's selection lives here
|
|
10934
|
+
so the right-click menu, the keyboard and the selection bar act on one
|
|
10935
|
+
set; the branch tree and the history keep theirs closer to their rows
|
|
10936
|
+
(BranchTree-internal / pickedCommits below). ---- */
|
|
10937
|
+
const [changeSelection, setChangeSelection] = useState(() => new Set())
|
|
10938
|
+
const changeAnchor = useRef(null)
|
|
10939
|
+
const [pickedCommits, setPickedCommits] = useState(() => new Set())
|
|
10100
10940
|
const [menu, setMenu] = useState(null)
|
|
10101
10941
|
const [dialog, setDialog] = useState(null)
|
|
10102
10942
|
const [tick, setTick] = useState(0)
|
|
@@ -10844,30 +11684,230 @@ window.__ModuleLoader__.load({
|
|
|
10844
11684
|
sendToComposer({ text: text, fallback: text })
|
|
10845
11685
|
}, [t, sendToComposer])
|
|
10846
11686
|
|
|
10847
|
-
|
|
11687
|
+
/* ---------- multi-select actions (issue #13) ---------- */
|
|
11688
|
+
|
|
11689
|
+
/* Copy with feedback: plain copyText stays silent, which reads as "did
|
|
11690
|
+
nothing" the moment the paste target is in another app. */
|
|
11691
|
+
const copyValues = useCallback((values) => {
|
|
11692
|
+
if (values === null || values === undefined || values.length === 0) return
|
|
11693
|
+
copyText(clipboardLines(values))
|
|
11694
|
+
pushToast({ text: fill(t('selection.copied'), { n: values.length }), icon: 'tag', tone: 'ok', ttl: 2500 })
|
|
11695
|
+
}, [pushToast, t])
|
|
11696
|
+
|
|
11697
|
+
/* Resolve the change selection against the CURRENT summary: paths move
|
|
11698
|
+
between groups on every stage/unstage, so the key ('group:path') is
|
|
11699
|
+
the only stable handle and the entry lookup must be fresh. */
|
|
11700
|
+
const changeSelectionGroups = () => {
|
|
11701
|
+
const changes = summary === null ? null : summary.changes
|
|
11702
|
+
const groups = { staged: [], other: [], tracked: [], untracked: [] }
|
|
11703
|
+
if (changes === null) return groups
|
|
11704
|
+
const byKey = new Map()
|
|
11705
|
+
for (const entry of changes.staged) byKey.set('staged:' + entry.path, entry)
|
|
11706
|
+
for (const entry of changes.unstaged) byKey.set('unstaged:' + entry.path, entry)
|
|
11707
|
+
for (const entry of changes.untracked) byKey.set('untracked:' + entry.path, entry)
|
|
11708
|
+
for (const entry of changes.conflicted) byKey.set('conflicted:' + entry.path, entry)
|
|
11709
|
+
for (const key of changeSelection) {
|
|
11710
|
+
const entry = byKey.get(key)
|
|
11711
|
+
if (entry === undefined) continue
|
|
11712
|
+
if (key.indexOf('staged:') === 0) groups.staged.push(entry.path)
|
|
11713
|
+
else groups.other.push(entry.path)
|
|
11714
|
+
// Conflicted rides the tracked lane, like the single-file discard does.
|
|
11715
|
+
if (key.indexOf('untracked:') === 0) groups.untracked.push(entry.path)
|
|
11716
|
+
else groups.tracked.push(entry.path)
|
|
11717
|
+
}
|
|
11718
|
+
return groups
|
|
11719
|
+
}
|
|
11720
|
+
const bulkStageChanges = () => {
|
|
11721
|
+
const groups = changeSelectionGroups()
|
|
11722
|
+
if (groups.other.length === 0) return
|
|
11723
|
+
void run('stage', { paths: groups.other })
|
|
11724
|
+
}
|
|
11725
|
+
const bulkUnstageChanges = () => {
|
|
11726
|
+
const groups = changeSelectionGroups()
|
|
11727
|
+
if (groups.staged.length === 0) return
|
|
11728
|
+
void run('unstage', { paths: groups.staged })
|
|
11729
|
+
}
|
|
11730
|
+
const openDiscardManyDialog = () => {
|
|
11731
|
+
const groups = changeSelectionGroups()
|
|
11732
|
+
if (groups.tracked.length === 0 && groups.untracked.length === 0) return
|
|
11733
|
+
setDialog({ kind: 'discardMany', tracked: groups.tracked, untracked: groups.untracked })
|
|
11734
|
+
}
|
|
11735
|
+
const copyChangeSelection = () => {
|
|
11736
|
+
const groups = changeSelectionGroups()
|
|
11737
|
+
copyValues(groups.staged.concat(groups.other))
|
|
11738
|
+
}
|
|
11739
|
+
/* A refresh that resolves a selected key away (file staged, discarded,
|
|
11740
|
+
renamed) must not keep it selected. */
|
|
11741
|
+
useEffect(() => {
|
|
11742
|
+
setChangeSelection((previous) => {
|
|
11743
|
+
if (previous.size === 0) return previous
|
|
11744
|
+
const changes = summary === null ? null : summary.changes
|
|
11745
|
+
if (changes === null) return previous
|
|
11746
|
+
const alive = new Set()
|
|
11747
|
+
for (const entry of changes.conflicted) alive.add('conflicted:' + entry.path)
|
|
11748
|
+
for (const entry of changes.staged) alive.add('staged:' + entry.path)
|
|
11749
|
+
for (const entry of changes.unstaged) alive.add('unstaged:' + entry.path)
|
|
11750
|
+
for (const entry of changes.untracked) alive.add('untracked:' + entry.path)
|
|
11751
|
+
const next = new Set()
|
|
11752
|
+
for (const key of previous) {
|
|
11753
|
+
if (alive.has(key) === true) next.add(key)
|
|
11754
|
+
}
|
|
11755
|
+
return next.size === previous.size ? previous : next
|
|
11756
|
+
})
|
|
11757
|
+
}, [summary])
|
|
11758
|
+
/* Changes-list keyboard scope: Ctrl/Cmd+C copies paths, Delete opens the
|
|
11759
|
+
grouped discard confirm, Ctrl+A selects everything but ignored, Esc
|
|
11760
|
+
clears. Typing targets keep their native behaviour. */
|
|
11761
|
+
const onChangesKeyDown = (event) => {
|
|
11762
|
+
if (isTypingTarget(event.target) === true) return
|
|
11763
|
+
const mod = isToggleModifier(event)
|
|
11764
|
+
if (mod === true && (event.key === 'c' || event.key === 'C')) {
|
|
11765
|
+
const groups = changeSelectionGroups()
|
|
11766
|
+
const paths = groups.staged.concat(groups.other)
|
|
11767
|
+
if (paths.length === 0) return
|
|
11768
|
+
event.preventDefault()
|
|
11769
|
+
copyValues(paths)
|
|
11770
|
+
return
|
|
11771
|
+
}
|
|
11772
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
11773
|
+
if (changeSelection.size === 0) return
|
|
11774
|
+
event.preventDefault()
|
|
11775
|
+
openDiscardManyDialog()
|
|
11776
|
+
return
|
|
11777
|
+
}
|
|
11778
|
+
if (mod === true && (event.key === 'a' || event.key === 'A')) {
|
|
11779
|
+
const changes = summary === null ? null : summary.changes
|
|
11780
|
+
if (changes === null) return
|
|
11781
|
+
event.preventDefault()
|
|
11782
|
+
const next = new Set()
|
|
11783
|
+
for (const entry of changes.conflicted) next.add('conflicted:' + entry.path)
|
|
11784
|
+
for (const entry of changes.staged) next.add('staged:' + entry.path)
|
|
11785
|
+
for (const entry of changes.unstaged) next.add('unstaged:' + entry.path)
|
|
11786
|
+
for (const entry of changes.untracked) next.add('untracked:' + entry.path)
|
|
11787
|
+
setChangeSelection(next)
|
|
11788
|
+
return
|
|
11789
|
+
}
|
|
11790
|
+
if (event.key === 'Escape') setChangeSelection(new Set())
|
|
11791
|
+
}
|
|
11792
|
+
/* History keyboard scope: Ctrl/Cmd+C copies full hashes, Esc clears.
|
|
11793
|
+
Delete has no commit analogue — deliberately unmapped. */
|
|
11794
|
+
const onHistoryKeyDown = (event) => {
|
|
11795
|
+
if (isTypingTarget(event.target) === true) return
|
|
11796
|
+
if (isToggleModifier(event) === true && (event.key === 'c' || event.key === 'C')) {
|
|
11797
|
+
if (pickedCommits.size === 0) return
|
|
11798
|
+
event.preventDefault()
|
|
11799
|
+
copyValues(Array.from(pickedCommits))
|
|
11800
|
+
return
|
|
11801
|
+
}
|
|
11802
|
+
if (event.key === 'Escape') setPickedCommits(new Set())
|
|
11803
|
+
}
|
|
11804
|
+
/* Branch-tree Delete: group the picked refs by kind and hand them to ONE
|
|
11805
|
+
confirm dialog (head branches never get here — BranchTree filters). */
|
|
11806
|
+
const deleteBranchEntries = (entries) => {
|
|
11807
|
+
const locals = []
|
|
11808
|
+
const remotes = []
|
|
11809
|
+
const tags = []
|
|
11810
|
+
for (const item of entries) {
|
|
11811
|
+
if (item.kind === 'local') locals.push(item.name)
|
|
11812
|
+
else if (item.kind === 'tag') tags.push(item.name)
|
|
11813
|
+
else {
|
|
11814
|
+
const slash = item.name.indexOf('/')
|
|
11815
|
+
remotes.push({
|
|
11816
|
+
remote: slash >= 0 ? item.name.slice(0, slash) : item.name,
|
|
11817
|
+
branch: slash >= 0 ? item.name.slice(slash + 1) : item.name,
|
|
11818
|
+
})
|
|
11819
|
+
}
|
|
11820
|
+
}
|
|
11821
|
+
if (locals.length === 0 && remotes.length === 0 && tags.length === 0) return
|
|
11822
|
+
setDialog({ kind: 'deleteRefs', locals: locals, remotes: remotes, tags: tags })
|
|
11823
|
+
}
|
|
11824
|
+
/* Batch local delete with the same undo toast the single delete has.
|
|
11825
|
+
Plain function on purpose (invariant 12): a useCallback here would
|
|
11826
|
+
evaluate `undoAction` in its dep array — declared further below. */
|
|
11827
|
+
const deleteBranches = async (names) => {
|
|
11828
|
+
const data = await run('branchDelete', { names: names, confirm: true })
|
|
11829
|
+
if (data === undefined) return
|
|
11830
|
+
if (data.undo === undefined) return
|
|
11831
|
+
pushToast({
|
|
11832
|
+
text: fill(t('toast.branchesDeleted'), { n: names.length }),
|
|
11833
|
+
icon: 'trash',
|
|
11834
|
+
tone: 'warn',
|
|
11835
|
+
undoId: data.undo.id,
|
|
11836
|
+
actionLabel: t('toast.undo'),
|
|
11837
|
+
onAction: () => { void undoAction(data.undo.id) },
|
|
11838
|
+
})
|
|
11839
|
+
}
|
|
11840
|
+
|
|
11841
|
+
const branchMenu = useCallback((event, entry, picked) => {
|
|
10848
11842
|
const current = branches === null ? '' : branches.branch
|
|
10849
11843
|
const isLocal = entry.remote !== true && entry.tag !== true
|
|
10850
11844
|
const reason = t('action.unavailable')
|
|
11845
|
+
/* Multi-select menu: the verbs that only make sense for one ref
|
|
11846
|
+
(checkout, merge, rebase, rename…) are dropped, not greyed — the
|
|
11847
|
+
selection itself already says what the user is going for. */
|
|
11848
|
+
if (picked !== null && picked !== undefined && picked.length > 1) {
|
|
11849
|
+
const locals = picked.filter((item) => item.kind === 'local' && item.entry.head !== true)
|
|
11850
|
+
const remotes = picked.filter((item) => item.kind === 'remote')
|
|
11851
|
+
const tags = picked.filter((item) => item.kind === 'tag')
|
|
11852
|
+
openMenuAt(event, [
|
|
11853
|
+
{ id: 'copyNames', icon: 'tag', tone: 'secondary', label: fill(t('menu.copyNamesN'), { n: picked.length }), run: () => copyValues(picked.map((item) => item.name)) },
|
|
11854
|
+
null,
|
|
11855
|
+
locals.length > 0 ? { id: 'deleteLocal', icon: 'trash', tone: 'danger', danger: true, label: fill(t('menu.deleteLocalN'), { n: locals.length }), run: () => setDialog({ kind: 'deleteRefs', locals: locals.map((item) => item.name), remotes: [], tags: [] }) } : null,
|
|
11856
|
+
remotes.length > 0 ? { id: 'deleteRemote', icon: 'trash', tone: 'danger', danger: true, label: fill(t('menu.deleteRemoteN'), { n: remotes.length }), run: () => setDialog({ kind: 'deleteRefs', locals: [], remotes: remotes.map((item) => { const slash = item.name.indexOf('/'); return { remote: slash >= 0 ? item.name.slice(0, slash) : item.name, branch: slash >= 0 ? item.name.slice(slash + 1) : item.name } }), tags: [] }) } : null,
|
|
11857
|
+
tags.length > 0 ? { id: 'deleteTags', icon: 'trash', tone: 'danger', danger: true, label: fill(t('menu.deleteTagsN'), { n: tags.length }), run: () => setDialog({ kind: 'deleteRefs', locals: [], remotes: [], tags: tags.map((item) => item.name) }) } : null,
|
|
11858
|
+
].filter((item) => item !== null))
|
|
11859
|
+
return
|
|
11860
|
+
}
|
|
10851
11861
|
const items = [
|
|
10852
11862
|
{ id: 'checkout', icon: 'checkout', tone: 'accent', label: t('action.checkout'), disabled: entry.head === true || entry.tag === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void checkout(entry) } },
|
|
10853
11863
|
isLocal ? { id: 'rebase', icon: 'compare', tone: 'violet', label: t('action.rebaseCurrentOnto'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('rebase', { onto: entry.name }) } } : null,
|
|
10854
11864
|
isLocal ? { id: 'merge', icon: 'compare', tone: 'accent', label: t('action.mergeIntoCurrent'), disabled: entry.head === true || blocked, reason: blocked ? t('operation.hint') : reason, run: () => { void run('merge', { branch: entry.name }) } } : null,
|
|
10855
11865
|
{ id: 'compare', icon: 'filter', tone: 'violet', label: t('action.compare'), disabled: entry.head === true || current === '', reason: reason, run: () => { void compareWith(current, entry.name) } },
|
|
10856
11866
|
{ id: 'send', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendGitSummaryToChat('send.branchText', { name: entry.name, repo: repoLabel }) },
|
|
11867
|
+
{ id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyName'), run: () => copyValues([entry.name]) },
|
|
10857
11868
|
null,
|
|
10858
11869
|
{ id: 'favorite', icon: 'star', tone: 'warn', label: t('action.favorite'), active: favorites.indexOf(entry.name) >= 0, run: () => toggleFavoriteBranch(entry.name) },
|
|
10859
11870
|
{ id: 'newBranch', icon: 'plus', tone: 'success', label: t('action.newBranchFrom'), run: () => setDialog({ kind: 'newBranch', from: entry.name }) },
|
|
10860
11871
|
isLocal ? { id: 'rename', icon: 'file', tone: 'primary', label: t('action.rename'), run: () => setDialog({ kind: 'renameBranch', from: entry.name }) } : null,
|
|
10861
11872
|
isLocal ? { id: 'delete', icon: 'trash', tone: 'danger', label: t('action.delete'), danger: true, disabled: entry.head === true, reason: reason, run: () => setDialog({ kind: 'deleteBranch', name: entry.name }) } : null,
|
|
11873
|
+
entry.tag === true
|
|
11874
|
+
? { id: 'delete', icon: 'trash', tone: 'danger', label: t('action.delete'), danger: true, run: () => setDialog({ kind: 'deleteRefs', locals: [], remotes: [], tags: [entry.name] }) }
|
|
11875
|
+
: (isLocal ? null : {
|
|
11876
|
+
id: 'delete', icon: 'trash', tone: 'danger', label: t('action.delete'), danger: true,
|
|
11877
|
+
run: () => {
|
|
11878
|
+
const slash = entry.name.indexOf('/')
|
|
11879
|
+
setDialog({
|
|
11880
|
+
kind: 'deleteRefs', locals: [], tags: [],
|
|
11881
|
+
remotes: [{ remote: slash >= 0 ? entry.name.slice(0, slash) : entry.name, branch: entry.name.slice(slash + 1) }],
|
|
11882
|
+
})
|
|
11883
|
+
},
|
|
11884
|
+
}),
|
|
10862
11885
|
null,
|
|
10863
11886
|
{ id: 'update', icon: 'fetch', tone: 'cyan', label: t('action.update'), run: () => { void run('fetch', { prune: true }) } },
|
|
10864
11887
|
{ id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) },
|
|
10865
11888
|
].filter((item) => item !== null)
|
|
10866
11889
|
openMenuAt(event, items)
|
|
10867
|
-
}, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendGitSummaryToChat, repoLabel])
|
|
11890
|
+
}, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendGitSummaryToChat, repoLabel, copyValues])
|
|
10868
11891
|
|
|
10869
11892
|
const commitMenu = useCallback((event, commit) => {
|
|
10870
11893
|
const shortHash = commit.hash.slice(0, 7)
|
|
11894
|
+
/* Multi-select: copy every picked hash and cherry-pick them as ONE
|
|
11895
|
+
sequence, oldest first (the graph lists newest first, so the walk
|
|
11896
|
+
order reverses). Verbs that only make sense against one commit
|
|
11897
|
+
(checkout, branch, tag, reset, revert) drop out entirely. */
|
|
11898
|
+
if (pickedCommits.has(commit.hash) === true && pickedCommits.size > 1) {
|
|
11899
|
+
const ordered = []
|
|
11900
|
+
for (const entry of commits) {
|
|
11901
|
+
if (pickedCommits.has(entry.hash) === true) ordered.push(entry.hash)
|
|
11902
|
+
}
|
|
11903
|
+
const oldestFirst = ordered.slice().reverse()
|
|
11904
|
+
openMenuAt(event, [
|
|
11905
|
+
{ id: 'copyHashes', icon: 'tag', tone: 'secondary', label: fill(t('menu.copyHashesN'), { n: pickedCommits.size }), run: () => copyValues(Array.from(pickedCommits)) },
|
|
11906
|
+
null,
|
|
11907
|
+
{ id: 'cherry', icon: 'commit', tone: 'accent', label: fill(t('menu.cherryN'), { n: pickedCommits.size }), disabled: blocked, reason: t('operation.hint'), run: () => { void run('cherryPick', { hashes: oldestFirst }) } },
|
|
11908
|
+
])
|
|
11909
|
+
return
|
|
11910
|
+
}
|
|
10871
11911
|
const items = [
|
|
10872
11912
|
{ id: 'details', icon: 'file', tone: 'primary', label: t('action.details'), run: () => { void selectCommit(commit) } },
|
|
10873
11913
|
{ id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyHash'), run: () => { copyText(commit.hash) } },
|
|
@@ -10890,9 +11930,23 @@ window.__ModuleLoader__.load({
|
|
|
10890
11930
|
{ id: 'resetHard', icon: 'trash', tone: 'danger', label: t('action.resetHard'), danger: true, disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'reset', hash: commit.hash, mode: 'hard' }) },
|
|
10891
11931
|
]
|
|
10892
11932
|
openMenuAt(event, items)
|
|
10893
|
-
}, [run, selectCommit, t, openMenuAt, blocked, sendToComposer, sendGitSummaryToChat, repoLabel])
|
|
11933
|
+
}, [run, selectCommit, t, openMenuAt, blocked, sendToComposer, sendGitSummaryToChat, repoLabel, pickedCommits, commits, copyValues])
|
|
10894
11934
|
|
|
10895
11935
|
const changeMenu = useCallback((event, item, group) => {
|
|
11936
|
+
/* Multi-select: right-clicking a row that belongs to the selection
|
|
11937
|
+
acts on the WHOLE selection (stage N / unstage N / discard N). */
|
|
11938
|
+
if (changeSelection.has(group + ':' + item.path) === true && changeSelection.size > 1) {
|
|
11939
|
+
const groups = changeSelectionGroups()
|
|
11940
|
+
const allPaths = groups.staged.concat(groups.other)
|
|
11941
|
+
openMenuAt(event, [
|
|
11942
|
+
groups.other.length > 0 ? { id: 'stage', icon: 'plus', tone: 'success', label: fill(t('menu.stageN'), { n: groups.other.length }), run: () => { void run('stage', { paths: groups.other }) } } : null,
|
|
11943
|
+
groups.staged.length > 0 ? { id: 'unstage', icon: 'minus', tone: 'warn', label: fill(t('menu.unstageN'), { n: groups.staged.length }), run: () => { void run('unstage', { paths: groups.staged }) } } : null,
|
|
11944
|
+
{ id: 'discard', icon: 'undo', tone: 'danger', danger: true, label: fill(t('menu.discardN'), { n: groups.tracked.length + groups.untracked.length }), run: () => openDiscardManyDialog() },
|
|
11945
|
+
null,
|
|
11946
|
+
{ id: 'copy', icon: 'tag', tone: 'secondary', label: fill(t('menu.copyPathsN'), { n: allPaths.length }), run: () => copyValues(allPaths) },
|
|
11947
|
+
].filter((entry) => entry !== null))
|
|
11948
|
+
return
|
|
11949
|
+
}
|
|
10896
11950
|
const items = [
|
|
10897
11951
|
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) } },
|
|
10898
11952
|
{ id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(item.path) },
|
|
@@ -10905,7 +11959,7 @@ window.__ModuleLoader__.load({
|
|
|
10905
11959
|
{ id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(item.path) } },
|
|
10906
11960
|
]
|
|
10907
11961
|
openMenuAt(event, items)
|
|
10908
|
-
}, [showDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer])
|
|
11962
|
+
}, [showDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer, changeSelection, changeSelectionGroups, openDiscardManyDialog, copyValues])
|
|
10909
11963
|
|
|
10910
11964
|
/* The commit-detail file list shares the working-tree row menu, minus the
|
|
10911
11965
|
staging verbs: a committed file's diff replays against its commit. */
|
|
@@ -11193,6 +12247,11 @@ window.__ModuleLoader__.load({
|
|
|
11193
12247
|
onSelect: (commit) => { void selectCommit(commit) },
|
|
11194
12248
|
onMenu: commitMenu,
|
|
11195
12249
|
onLoadMore: () => { void guard(() => loadCommits(commits.length)) },
|
|
12250
|
+
/* ---- multi-select (issue #13) ---- */
|
|
12251
|
+
pickedHashes: pickedCommits,
|
|
12252
|
+
onPicksChange: setPickedCommits,
|
|
12253
|
+
onSelectAll: (hashes) => setPickedCommits(new Set(hashes)),
|
|
12254
|
+
onKeyDown: onHistoryKeyDown,
|
|
11196
12255
|
})
|
|
11197
12256
|
|
|
11198
12257
|
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
@@ -11457,6 +12516,15 @@ window.__ModuleLoader__.load({
|
|
|
11457
12516
|
onDiscard: (item, group) => setDialog({ kind: 'discard', item: item, group: group }),
|
|
11458
12517
|
onDiff: (item, group) => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) },
|
|
11459
12518
|
onChangeMenu: changeMenu,
|
|
12519
|
+
/* ---- multi-select (issue #13) ---- */
|
|
12520
|
+
selection: changeSelection,
|
|
12521
|
+
onSelectionChange: setChangeSelection,
|
|
12522
|
+
anchorRef: changeAnchor,
|
|
12523
|
+
onBulkStage: bulkStageChanges,
|
|
12524
|
+
onBulkUnstage: bulkUnstageChanges,
|
|
12525
|
+
onBulkDiscard: openDiscardManyDialog,
|
|
12526
|
+
onCopySelection: copyChangeSelection,
|
|
12527
|
+
onKeyDown: onChangesKeyDown,
|
|
11460
12528
|
})
|
|
11461
12529
|
|
|
11462
12530
|
|
|
@@ -11659,19 +12727,19 @@ window.__ModuleLoader__.load({
|
|
|
11659
12727
|
title: t('toolbar.tree'),
|
|
11660
12728
|
onClick: () => foldTree(!treeOpen),
|
|
11661
12729
|
}, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
|
|
11662
|
-
treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu })) : null,
|
|
12730
|
+
treeOpen ? E('div', { className: 'dig-compact-tree', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues })) : null,
|
|
11663
12731
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11664
12732
|
view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
|
|
11665
12733
|
} else if (columns) {
|
|
11666
12734
|
body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
|
|
11667
|
-
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, onBranchMenu: branchMenu })) : null,
|
|
12735
|
+
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, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues })) : null,
|
|
11668
12736
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11669
12737
|
E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane),
|
|
11670
12738
|
gutter('changes', 'next'),
|
|
11671
12739
|
E('div', { className: 'dig-pane dig-pane-changes', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane))
|
|
11672
12740
|
} else {
|
|
11673
12741
|
body = E('div', { className: 'dig-body dig-body-stack', ref: bodyRef },
|
|
11674
|
-
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu })) : null,
|
|
12742
|
+
treeOpen ? E('div', { className: 'dig-pane dig-pane-tree-stack', 'data-pane': 'tree', style: paneStyle('tree') }, E(BranchTree, { t: t, branches: branches, favorites: favorites, onCheckout: checkout, onBranchMenu: branchMenu, onDeleteEntries: deleteBranchEntries, onCopyValues: copyValues })) : null,
|
|
11675
12743
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11676
12744
|
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
11677
12745
|
gutter('changes', 'prev'),
|
|
@@ -11851,6 +12919,60 @@ window.__ModuleLoader__.load({
|
|
|
11851
12919
|
onConfirm: () => { const ref = dialog.ref; setDialog(null); void dropStash(ref) },
|
|
11852
12920
|
}))
|
|
11853
12921
|
}
|
|
12922
|
+
/* Multi-select delete (issue #13): local branches / remote branches / tags
|
|
12923
|
+
in ONE confirm — each rides its existing destructive host method, and a
|
|
12924
|
+
protected local branch keeps the type-the-name gate. */
|
|
12925
|
+
if (dialog !== null && dialog.kind === 'deleteRefs') {
|
|
12926
|
+
const guardedName = dialog.locals.find((name) => isProtectedBranch(name) === true)
|
|
12927
|
+
const listed = dialog.locals
|
|
12928
|
+
.concat(dialog.tags)
|
|
12929
|
+
.concat(dialog.remotes.map((item) => item.remote + '/' + item.branch))
|
|
12930
|
+
const listText = listed.length <= 8 ? listed.join('、') : listed.slice(0, 8).join('、') + ' …'
|
|
12931
|
+
overlays.push(E(ConfirmDialog, {
|
|
12932
|
+
key: 'deleteRefs',
|
|
12933
|
+
t: t,
|
|
12934
|
+
title: t('confirm.title'),
|
|
12935
|
+
text: fill(t('confirm.deleteRefs'), { b: dialog.locals.length, r: dialog.remotes.length, g: dialog.tags.length })
|
|
12936
|
+
+ (listText === '' ? '' : ' ' + listText),
|
|
12937
|
+
requireText: guardedName === undefined ? undefined : guardedName,
|
|
12938
|
+
okLabel: t('confirm.ok'), cancelLabel: t('confirm.cancel'),
|
|
12939
|
+
onCancel: () => setDialog(null),
|
|
12940
|
+
onConfirm: () => {
|
|
12941
|
+
const locals = dialog.locals
|
|
12942
|
+
const tags = dialog.tags
|
|
12943
|
+
const remotes = dialog.remotes
|
|
12944
|
+
setDialog(null)
|
|
12945
|
+
if (locals.length > 0) void deleteBranches(locals)
|
|
12946
|
+
if (tags.length > 0) void run('tagDelete', { names: tags })
|
|
12947
|
+
const byRemote = new Map()
|
|
12948
|
+
for (const item of remotes) {
|
|
12949
|
+
if (byRemote.has(item.remote) === false) byRemote.set(item.remote, [])
|
|
12950
|
+
byRemote.get(item.remote).push(item.branch)
|
|
12951
|
+
}
|
|
12952
|
+
for (const pair of byRemote) void run('branchDeleteRemote', { remote: pair[0], branches: pair[1], confirm: true })
|
|
12953
|
+
},
|
|
12954
|
+
}))
|
|
12955
|
+
}
|
|
12956
|
+
/* Multi-select discard: tracked paths go through `checkout --`, untracked
|
|
12957
|
+
through `clean -fd` — the same two-lane split as Discard All. */
|
|
12958
|
+
if (dialog !== null && dialog.kind === 'discardMany') {
|
|
12959
|
+
overlays.push(E(ConfirmDialog, {
|
|
12960
|
+
key: 'discardMany',
|
|
12961
|
+
t: t,
|
|
12962
|
+
title: t('confirm.title'),
|
|
12963
|
+
text: fill(t('confirm.discardMany'), { n: dialog.tracked.length + dialog.untracked.length })
|
|
12964
|
+
+ (dialog.untracked.length > 0 ? ' ' + fill(t('confirm.discardManyUntracked'), { m: dialog.untracked.length }) : ''),
|
|
12965
|
+
okLabel: t('confirm.ok'), cancelLabel: t('confirm.cancel'),
|
|
12966
|
+
onCancel: () => setDialog(null),
|
|
12967
|
+
onConfirm: () => {
|
|
12968
|
+
const tracked = dialog.tracked
|
|
12969
|
+
const untracked = dialog.untracked
|
|
12970
|
+
setDialog(null)
|
|
12971
|
+
if (tracked.length > 0) void run('discard', { paths: tracked, confirm: true })
|
|
12972
|
+
if (untracked.length > 0) void run('discard', { paths: untracked, untracked: true, confirm: true })
|
|
12973
|
+
},
|
|
12974
|
+
}))
|
|
12975
|
+
}
|
|
11854
12976
|
if (dialog !== null && dialog.kind === 'discard') {
|
|
11855
12977
|
overlays.push(E(ConfirmDialog, {
|
|
11856
12978
|
key: 'discard',
|
|
@@ -12052,6 +13174,14 @@ window.__ModuleLoader__.load({
|
|
|
12052
13174
|
'.dig-settings-glyph{display:inline-flex;flex:none;color:var(--dsw-alias-label-secondary)}',
|
|
12053
13175
|
'.dig-settings-label{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500}',
|
|
12054
13176
|
'.dig-root-narrow .dig-settings-label{white-space:normal}',
|
|
13177
|
+
/* Collapsible setting sections (hand-drawn, same pattern as dsh-gitbash-shell v0.38.4). */
|
|
13178
|
+
'.dig-fold{border:1px solid var(--dsw-alias-border-l2);border-radius:var(--dsw-radius-sm,8px);padding:4px 12px 12px;background:var(--dsw-alias-bg-layer-1,transparent)}',
|
|
13179
|
+
'.dig-fold .dig-stack{padding-top:10px}',
|
|
13180
|
+
'.dig-foldHead{display:flex;align-items:center;gap:8px;width:100%;appearance:none;background:none;border:0;padding:6px 0;font:inherit;font-size:13px;font-weight:600;letter-spacing:.02em;color:var(--dsw-alias-label-secondary);cursor:pointer;text-align:left}',
|
|
13181
|
+
'.dig-foldHead:hover{color:var(--dsw-alias-label-primary)}',
|
|
13182
|
+
'.dig-foldChevron{flex:none;display:inline-block;color:var(--dsw-alias-label-tertiary);transition:transform .16s}',
|
|
13183
|
+
'.dig-foldChevron.dig-foldChevronOpen{transform:rotate(90deg)}',
|
|
13184
|
+
'.dig-foldTitle{min-width:0;overflow-wrap:anywhere}',
|
|
12055
13185
|
'.dig-root-narrow .dig-menu-item{white-space:normal}',
|
|
12056
13186
|
'.dig-root-narrow .dig-toast-text{white-space:normal}',
|
|
12057
13187
|
'.dig-toasts{position:absolute;right:8px;bottom:8px;display:flex;flex-direction:column;gap:6px;z-index:80;max-width:min(340px,92%)}',
|
|
@@ -12139,7 +13269,7 @@ window.__ModuleLoader__.load({
|
|
|
12139
13269
|
'.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
|
|
12140
13270
|
'.dig-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
12141
13271
|
'.dig-row-head{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
12142
|
-
'.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}',
|
|
13272
|
+
'.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary)}',
|
|
12143
13273
|
'.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
12144
13274
|
'.dig-row-tag .dig-row-label{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:500}',
|
|
12145
13275
|
'.dig-row-head .dig-row-label{font-weight:600}',
|
|
@@ -12177,9 +13307,20 @@ window.__ModuleLoader__.load({
|
|
|
12177
13307
|
'.dig-load-more{margin:6px auto;display:block;padding:3px 12px;border-radius:var(--dsw-radius-sm,8px);border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);font:inherit;cursor:pointer}',
|
|
12178
13308
|
'.dig-load-more:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
12179
13309
|
'.dig-empty{padding:14px;text-align:center;color:var(--dsw-alias-label-tertiary);font-weight:500}',
|
|
12180
|
-
'.dig-changes{display:flex;flex-direction:column;min-height:0;flex:1}',
|
|
13310
|
+
'.dig-changes{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
12181
13311
|
'.dig-changes-body{display:flex;flex-direction:column;min-height:0;flex:1}',
|
|
12182
13312
|
'.dig-changes-list{flex:1;overflow:auto;min-height:0}',
|
|
13313
|
+
/* Multi-select chrome (issue #13). The marquee box is anchored to the
|
|
13314
|
+
VIEWPORT-space .dig-changes (invariant 13: position:fixed would be
|
|
13315
|
+
captured by the workbench's contain:layout and flung off-panel). */
|
|
13316
|
+
'.dig-marquee{position:absolute;border:1px solid var(--dsw-alias-brand-primary);background:color-mix(in srgb,var(--dsw-alias-brand-primary) 14%,transparent);pointer-events:none;z-index:6}',
|
|
13317
|
+
'.dig-selection-bar{display:flex;align-items:center;gap:12px;padding:3px 10px;font-size:11px;background:var(--dsw-alias-interactive-bg-hover);border-bottom:1px solid var(--dsw-alias-border-subtle,transparent)}',
|
|
13318
|
+
'.dig-selection-bar .dig-link{margin-left:0}',
|
|
13319
|
+
'.dig-selection-bar .dig-link-danger{color:var(--dsw-alias-state-error-primary)}',
|
|
13320
|
+
'.dig-selection-count{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
|
|
13321
|
+
/* Focusable lists keep their own selection highlight as the focus
|
|
13322
|
+
indicator, so the container ring is suppressed. */
|
|
13323
|
+
'.dig-changes-list:focus-visible,.dig-tree:focus-visible,.dig-history-scroll:focus-visible{outline:none}',
|
|
12183
13324
|
'.dig-group{display:flex;flex-direction:column}',
|
|
12184
13325
|
'.dig-group-head{display:flex;align-items:center;gap:8px;padding:2px 8px;height:17px;color:var(--dsw-alias-label-secondary);font-size:11px;font-weight:600}',
|
|
12185
13326
|
'.dig-link{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;padding:0;flex:none}',
|