dsh-ide-git 0.17.0 → 0.18.1
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 +1138 -32
- 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': '平铺',
|
|
@@ -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',
|
|
@@ -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': '平面清單',
|
|
@@ -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': '平面清單',
|
|
@@ -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': '平面清單',
|
|
@@ -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': 'フラット表示',
|
|
@@ -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': '평면 목록',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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': 'Плоский список',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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',
|
|
@@ -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': 'قائمة مسطّحة',
|
|
@@ -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': 'सपाट सूची',
|
|
@@ -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': 'รายการแบน',
|
|
@@ -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
|
|
@@ -9291,14 +9723,16 @@ window.__ModuleLoader__.load({
|
|
|
9291
9723
|
const tone = kind === 'tag' ? 'warn' : kind === 'remote' ? 'violet' : kind === 'head' ? 'success' : 'accent'
|
|
9292
9724
|
const glyph = kind === 'tag' ? 'tag' : kind === 'remote' ? 'fetch' : kind === 'head' ? 'star' : 'branch'
|
|
9293
9725
|
const where = entry.upstream === null || entry.upstream === undefined ? entry.name : entry.name + ' → ' + entry.upstream
|
|
9294
|
-
// A single click only SELECTS
|
|
9295
|
-
//
|
|
9296
|
-
//
|
|
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.
|
|
9297
9731
|
return E('div', {
|
|
9298
|
-
className: 'dig-row dig-row-' + kind + (props.
|
|
9732
|
+
className: 'dig-row dig-row-' + kind + (props.selected === true ? ' dig-row-selected' : ''),
|
|
9299
9733
|
style: { paddingLeft: (BRANCH_INDENT_BASE + (props.depth === undefined ? 0 : props.depth) * BRANCH_INDENT_STEP) + 'px' },
|
|
9300
9734
|
title: where + ' · ' + t('branches.pickHint'),
|
|
9301
|
-
onClick: () => props.onPick(entry),
|
|
9735
|
+
onClick: (event) => props.onPick(event, entry, props.selKey),
|
|
9302
9736
|
onDoubleClick: () => props.onCheckout(entry),
|
|
9303
9737
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, entry) },
|
|
9304
9738
|
},
|
|
@@ -9311,15 +9745,20 @@ window.__ModuleLoader__.load({
|
|
|
9311
9745
|
className: 'dig-badge dig-badge-muted dig-badge-worktree',
|
|
9312
9746
|
title: t('branches.worktree') + ': ' + entry.worktree,
|
|
9313
9747
|
}, E(Icon, { name: 'folder', size: 10 })),
|
|
9314
|
-
entry.ahead > 0 ? E('span', { className: 'dig-badge' }, '↑' + entry.ahead) : null,
|
|
9315
|
-
entry.behind > 0 ? E('span', { className: 'dig-badge' }, '↓' + entry.behind) : null)
|
|
9748
|
+
entry.ahead > 0 ? E('span', { className: 'dig-badge dig-badge-ahead' }, '↑' + entry.ahead) : null,
|
|
9749
|
+
entry.behind > 0 ? E('span', { className: 'dig-badge dig-badge-behind' }, '↓' + entry.behind) : null)
|
|
9316
9750
|
}
|
|
9317
9751
|
|
|
9318
9752
|
function BranchTree(props) {
|
|
9319
9753
|
const t = props.t
|
|
9320
9754
|
const [filter, setFilter] = useState('')
|
|
9321
9755
|
const [collapsed, setCollapsed] = useState(() => treeSectionsOf(readTreeState()))
|
|
9322
|
-
|
|
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([])
|
|
9323
9762
|
const [foldedFolders, setFoldedFolders] = useState(() => {
|
|
9324
9763
|
const stored = readTreeState()
|
|
9325
9764
|
return stored !== null && typeof stored === 'object' && stored.folders !== null && typeof stored.folders === 'object'
|
|
@@ -9342,6 +9781,21 @@ window.__ModuleLoader__.load({
|
|
|
9342
9781
|
const remotes = (branches === null ? [] : branches.remote).filter((entry) => match(entry.name))
|
|
9343
9782
|
const tags = (branches === null ? [] : branches.tags).filter((entry) => match(entry.name))
|
|
9344
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])
|
|
9345
9799
|
/**
|
|
9346
9800
|
* Branch names nest on '/', the way an IDE shows a branch namespace: clicking a
|
|
9347
9801
|
* row only SELECTS it (checking out rewrites the working tree), so the row
|
|
@@ -9362,11 +9816,48 @@ window.__ModuleLoader__.load({
|
|
|
9362
9816
|
return root
|
|
9363
9817
|
}
|
|
9364
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])
|
|
9365
9855
|
const branchRow = (entry, label, depth) => E(BranchRow, {
|
|
9366
|
-
key: 'row:' + entry
|
|
9367
|
-
|
|
9368
|
-
|
|
9369
|
-
|
|
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,
|
|
9370
9861
|
})
|
|
9371
9862
|
const rowsOf = (node, prefix, depth) => {
|
|
9372
9863
|
const out = []
|
|
@@ -9401,7 +9892,71 @@ window.__ModuleLoader__.load({
|
|
|
9401
9892
|
grouped === true
|
|
9402
9893
|
? rowsOf(forestOf(entries.map((entry) => decorate(entry))), '', 0)
|
|
9403
9894
|
: entries.map((entry) => branchRow(decorate(entry), undefined, 0))))
|
|
9404
|
-
|
|
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
|
+
},
|
|
9405
9960
|
E('div', { className: 'dig-search' },
|
|
9406
9961
|
E('input', {
|
|
9407
9962
|
className: 'dig-input dig-input-compact', value: filter, spellCheck: false,
|
|
@@ -9435,9 +9990,11 @@ window.__ModuleLoader__.load({
|
|
|
9435
9990
|
const readOnly = props.group === 'ignored'
|
|
9436
9991
|
const status = readOnly ? 'I' : (raw === '' ? 'M' : raw.charAt(0))
|
|
9437
9992
|
return E('div', {
|
|
9438
|
-
className: 'dig-row dig-row-file',
|
|
9993
|
+
className: 'dig-row dig-row-file' + (props.selected === true ? ' dig-row-selected' : ''),
|
|
9439
9994
|
title: item.origPath === undefined ? item.path : item.origPath + ' → ' + item.path,
|
|
9440
|
-
|
|
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),
|
|
9441
9998
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, item, props.group) },
|
|
9442
9999
|
},
|
|
9443
10000
|
FileTypeGlyph({ path: item.path }),
|
|
@@ -9452,7 +10009,7 @@ window.__ModuleLoader__.load({
|
|
|
9452
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 }))
|
|
9453
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 }))),
|
|
9454
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 })))
|
|
9455
|
-
}, (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)
|
|
9456
10013
|
|
|
9457
10014
|
function ChangesPanel(props) {
|
|
9458
10015
|
const t = props.t
|
|
@@ -9489,6 +10046,154 @@ window.__ModuleLoader__.load({
|
|
|
9489
10046
|
state directly through props. */
|
|
9490
10047
|
const foldedDirs = props.foldedDirs === undefined ? {} : props.foldedDirs
|
|
9491
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
|
+
}
|
|
9492
10197
|
const changes = summary === null ? null : summary.changes
|
|
9493
10198
|
const conflicted = changes === null ? [] : changes.conflicted
|
|
9494
10199
|
const staged = changes === null ? [] : changes.staged
|
|
@@ -9537,6 +10242,8 @@ window.__ModuleLoader__.load({
|
|
|
9537
10242
|
}
|
|
9538
10243
|
const entryRow = (key, item) => E(ChangeRow, {
|
|
9539
10244
|
key: key + ':' + item.path, item: item, group: key, t: t,
|
|
10245
|
+
selected: selection.has(key + ':' + item.path) === true,
|
|
10246
|
+
onRowClick: handleRowClick,
|
|
9540
10247
|
onStage: props.onStage, onUnstage: props.onUnstage, onDiscard: props.onDiscard, onDiff: props.onDiff, onMenu: props.onChangeMenu,
|
|
9541
10248
|
})
|
|
9542
10249
|
const folderOf = (item) => {
|
|
@@ -9604,12 +10311,17 @@ window.__ModuleLoader__.load({
|
|
|
9604
10311
|
emitGroup('untracked', fill(t('counts.untracked'), { n: untracked.length }), untracked)
|
|
9605
10312
|
emitGroup('ignored', fill(t('counts.ignored'), { n: ignored.length }), ignored)
|
|
9606
10313
|
return out
|
|
9607
|
-
}, [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])
|
|
9608
10315
|
const changeOffsets = useMemo(() => {
|
|
9609
10316
|
const sums = [0]
|
|
9610
10317
|
for (const item of changeItems) sums.push(sums[sums.length - 1] + item.h)
|
|
9611
10318
|
return sums
|
|
9612
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
|
|
9613
10325
|
/* Virtual window, VSCode-style (ListView.getVisibleRange): both ends are
|
|
9614
10326
|
pixel→index conversions over the same prefix-sum array, so no "row
|
|
9615
10327
|
count" ever enters the math (issue #12 — the old winLimit computed a
|
|
@@ -9707,11 +10419,34 @@ window.__ModuleLoader__.load({
|
|
|
9707
10419
|
'data-action': 'more',
|
|
9708
10420
|
onClick: (event) => props.onMoreMenu(event),
|
|
9709
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')))
|
|
9710
10431
|
return E('div', { className: 'dig-changes' },
|
|
9711
10432
|
aiConfirmDialog,
|
|
9712
10433
|
head,
|
|
10434
|
+
selectionBar,
|
|
9713
10435
|
E('div', { className: 'dig-changes-body' },
|
|
9714
|
-
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
|
+
},
|
|
9715
10450
|
collapsed ? null : [
|
|
9716
10451
|
total === 0 && ignored.length === 0 ? E('div', { className: 'dig-empty', key: 'empty' }, t('changes.empty')) : null,
|
|
9717
10452
|
/* Windowed slice: spacers above/below stand in for the unmounted
|
|
@@ -9720,7 +10455,11 @@ window.__ModuleLoader__.load({
|
|
|
9720
10455
|
visibleItems.map((item) => E(Fragment, { key: item.key }, item.node)),
|
|
9721
10456
|
winLast < changeItems.length ? E('div', { key: 'pad-bottom', style: { height: String(changeOffsets[changeItems.length] - changeOffsets[winLast]) + 'px' } }) : null,
|
|
9722
10457
|
]),
|
|
9723
|
-
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
|
+
}))
|
|
9724
10463
|
}
|
|
9725
10464
|
|
|
9726
10465
|
/* ============================== history ============================== */
|
|
@@ -9844,7 +10583,9 @@ window.__ModuleLoader__.load({
|
|
|
9844
10583
|
graph stroke whose data-lane differs — zero React re-renders. */
|
|
9845
10584
|
'data-lane': String(props.row.lane),
|
|
9846
10585
|
className: 'dig-commit' + (props.selected === true ? ' dig-commit-selected' : ''),
|
|
9847
|
-
|
|
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),
|
|
9848
10589
|
onContextMenu: (event) => { event.preventDefault(); props.onMenu(event, commit) },
|
|
9849
10590
|
},
|
|
9850
10591
|
E('span', { className: 'dig-commit-date', title: shortDate(commit.date) }, relativeTime(commit.date)),
|
|
@@ -9893,6 +10634,7 @@ window.__ModuleLoader__.load({
|
|
|
9893
10634
|
const commits = props.commits
|
|
9894
10635
|
const historyScrollRef = useRef(null)
|
|
9895
10636
|
const viewport = useViewportBox(historyScrollRef)
|
|
10637
|
+
const pickAnchor = useRef(null)
|
|
9896
10638
|
const [text, setText] = useState('')
|
|
9897
10639
|
const [author, setAuthor] = useState('')
|
|
9898
10640
|
const [refName, setRefName] = useState('')
|
|
@@ -9947,6 +10689,59 @@ window.__ModuleLoader__.load({
|
|
|
9947
10689
|
}, [commits, text, author, refName, since, order])
|
|
9948
10690
|
|
|
9949
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
|
+
}
|
|
9950
10745
|
const dirtyFilter = text !== '' || author !== '' || refName !== '' || since !== '' || (props.pathFilter !== undefined && props.pathFilter !== '')
|
|
9951
10746
|
const applyPath = (value) => { if (props.onPathFilter !== undefined) props.onPathFilter(value) }
|
|
9952
10747
|
const clearAll = () => {
|
|
@@ -10026,6 +10821,11 @@ window.__ModuleLoader__.load({
|
|
|
10026
10821
|
E('div', {
|
|
10027
10822
|
className: 'dig-history-scroll',
|
|
10028
10823
|
ref: historyScrollRef,
|
|
10824
|
+
tabIndex: 0,
|
|
10825
|
+
onKeyDown: onListKeyDown,
|
|
10826
|
+
onPointerDownCapture: (event) => {
|
|
10827
|
+
if (isTypingTarget(event.target) === false) event.currentTarget.focus({ preventScroll: true })
|
|
10828
|
+
},
|
|
10029
10829
|
/* Lane hover highlight (GitLens-style trace): copy the hovered row's
|
|
10030
10830
|
lane into data-hover-lane; generated CSS dims every stroke whose
|
|
10031
10831
|
data-lane differs. Direct DOM dataset writes on purpose — the
|
|
@@ -10045,8 +10845,9 @@ window.__ModuleLoader__.load({
|
|
|
10045
10845
|
visibleRows.map((row) => E(CommitRow, {
|
|
10046
10846
|
key: row.commit.hash,
|
|
10047
10847
|
commit: row.commit, row: row, t: t,
|
|
10048
|
-
selected: props.selectedHash === row.commit.hash
|
|
10049
|
-
|
|
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,
|
|
10050
10851
|
})),
|
|
10051
10852
|
winEnd < rows.length ? E('div', { key: 'pad-bottom', style: { height: String((rows.length - winEnd) * HISTORY_ROW_H) + 'px' } }) : null,
|
|
10052
10853
|
props.hasMore === true
|
|
@@ -10129,6 +10930,13 @@ window.__ModuleLoader__.load({
|
|
|
10129
10930
|
and the selection menu's payload must say which. Cleared everywhere
|
|
10130
10931
|
the patch is cleared. */
|
|
10131
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())
|
|
10132
10940
|
const [menu, setMenu] = useState(null)
|
|
10133
10941
|
const [dialog, setDialog] = useState(null)
|
|
10134
10942
|
const [tick, setTick] = useState(0)
|
|
@@ -10876,30 +11684,230 @@ window.__ModuleLoader__.load({
|
|
|
10876
11684
|
sendToComposer({ text: text, fallback: text })
|
|
10877
11685
|
}, [t, sendToComposer])
|
|
10878
11686
|
|
|
10879
|
-
|
|
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) => {
|
|
10880
11842
|
const current = branches === null ? '' : branches.branch
|
|
10881
11843
|
const isLocal = entry.remote !== true && entry.tag !== true
|
|
10882
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
|
+
}
|
|
10883
11861
|
const items = [
|
|
10884
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) } },
|
|
10885
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,
|
|
10886
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,
|
|
10887
11865
|
{ id: 'compare', icon: 'filter', tone: 'violet', label: t('action.compare'), disabled: entry.head === true || current === '', reason: reason, run: () => { void compareWith(current, entry.name) } },
|
|
10888
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]) },
|
|
10889
11868
|
null,
|
|
10890
11869
|
{ id: 'favorite', icon: 'star', tone: 'warn', label: t('action.favorite'), active: favorites.indexOf(entry.name) >= 0, run: () => toggleFavoriteBranch(entry.name) },
|
|
10891
11870
|
{ id: 'newBranch', icon: 'plus', tone: 'success', label: t('action.newBranchFrom'), run: () => setDialog({ kind: 'newBranch', from: entry.name }) },
|
|
10892
11871
|
isLocal ? { id: 'rename', icon: 'file', tone: 'primary', label: t('action.rename'), run: () => setDialog({ kind: 'renameBranch', from: entry.name }) } : null,
|
|
10893
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
|
+
}),
|
|
10894
11885
|
null,
|
|
10895
11886
|
{ id: 'update', icon: 'fetch', tone: 'cyan', label: t('action.update'), run: () => { void run('fetch', { prune: true }) } },
|
|
10896
11887
|
{ id: 'push', icon: 'push', tone: 'success', label: t('action.push'), disabled: blocked, reason: t('operation.hint'), run: () => setDialog({ kind: 'push' }) },
|
|
10897
11888
|
].filter((item) => item !== null)
|
|
10898
11889
|
openMenuAt(event, items)
|
|
10899
|
-
}, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendGitSummaryToChat, repoLabel])
|
|
11890
|
+
}, [branches, checkout, run, t, compareWith, toggleFavoriteBranch, openMenuAt, favorites, blocked, sendGitSummaryToChat, repoLabel, copyValues])
|
|
10900
11891
|
|
|
10901
11892
|
const commitMenu = useCallback((event, commit) => {
|
|
10902
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
|
+
}
|
|
10903
11911
|
const items = [
|
|
10904
11912
|
{ id: 'details', icon: 'file', tone: 'primary', label: t('action.details'), run: () => { void selectCommit(commit) } },
|
|
10905
11913
|
{ id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyHash'), run: () => { copyText(commit.hash) } },
|
|
@@ -10922,9 +11930,23 @@ window.__ModuleLoader__.load({
|
|
|
10922
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' }) },
|
|
10923
11931
|
]
|
|
10924
11932
|
openMenuAt(event, items)
|
|
10925
|
-
}, [run, selectCommit, t, openMenuAt, blocked, sendToComposer, sendGitSummaryToChat, repoLabel])
|
|
11933
|
+
}, [run, selectCommit, t, openMenuAt, blocked, sendToComposer, sendGitSummaryToChat, repoLabel, pickedCommits, commits, copyValues])
|
|
10926
11934
|
|
|
10927
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
|
+
}
|
|
10928
11950
|
const items = [
|
|
10929
11951
|
{ id: 'diff', icon: 'file', tone: 'primary', label: t('action.showDiff'), run: () => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) } },
|
|
10930
11952
|
{ id: 'sendRef', icon: 'send', tone: 'accent', label: t('menu.sendToChat'), run: () => sendFileReferenceToChat(item.path) },
|
|
@@ -10937,7 +11959,7 @@ window.__ModuleLoader__.load({
|
|
|
10937
11959
|
{ id: 'copy', icon: 'tag', tone: 'secondary', label: t('action.copyPath'), run: () => { copyText(item.path) } },
|
|
10938
11960
|
]
|
|
10939
11961
|
openMenuAt(event, items)
|
|
10940
|
-
}, [showDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer])
|
|
11962
|
+
}, [showDiff, run, t, openMenuAt, sendFileReferenceToChat, sendToComposer, changeSelection, changeSelectionGroups, openDiscardManyDialog, copyValues])
|
|
10941
11963
|
|
|
10942
11964
|
/* The commit-detail file list shares the working-tree row menu, minus the
|
|
10943
11965
|
staging verbs: a committed file's diff replays against its commit. */
|
|
@@ -11185,7 +12207,8 @@ window.__ModuleLoader__.load({
|
|
|
11185
12207
|
E('span', { className: 'dig-repo-name' }, branches.branch),
|
|
11186
12208
|
E(Icon, { name: 'chevron', size: 12 }))),
|
|
11187
12209
|
summary === null || summary.upstream === null ? null : E('span', { className: 'dig-track' },
|
|
11188
|
-
|
|
12210
|
+
summary.ahead > 0 ? E('span', { className: 'dig-track-ahead' }, '↑' + summary.ahead) : null,
|
|
12211
|
+
summary.behind > 0 ? E('span', { className: 'dig-track-behind' }, (summary.ahead > 0 ? ' ' : '') + '↓' + summary.behind) : null),
|
|
11189
12212
|
operation === null ? null : E('span', { className: 'dig-opchip', title: t('operation.hint') }, t('operation.' + operation)),
|
|
11190
12213
|
E('button', {
|
|
11191
12214
|
type: 'button', className: 'dig-icon-btn dig-icon-btn-small',
|
|
@@ -11225,6 +12248,11 @@ window.__ModuleLoader__.load({
|
|
|
11225
12248
|
onSelect: (commit) => { void selectCommit(commit) },
|
|
11226
12249
|
onMenu: commitMenu,
|
|
11227
12250
|
onLoadMore: () => { void guard(() => loadCommits(commits.length)) },
|
|
12251
|
+
/* ---- multi-select (issue #13) ---- */
|
|
12252
|
+
pickedHashes: pickedCommits,
|
|
12253
|
+
onPicksChange: setPickedCommits,
|
|
12254
|
+
onSelectAll: (hashes) => setPickedCommits(new Set(hashes)),
|
|
12255
|
+
onKeyDown: onHistoryKeyDown,
|
|
11228
12256
|
})
|
|
11229
12257
|
|
|
11230
12258
|
const diffPane = patch === '' && patchLoading === false ? null : E('div', { className: 'dig-diff-pane', 'data-pane': 'diff', style: paneStyle('diff'), onContextMenu: onDiffPaneContextMenu },
|
|
@@ -11489,6 +12517,15 @@ window.__ModuleLoader__.load({
|
|
|
11489
12517
|
onDiscard: (item, group) => setDialog({ kind: 'discard', item: item, group: group }),
|
|
11490
12518
|
onDiff: (item, group) => { showDiff({ path: item.path, staged: group === 'staged', untracked: group === 'untracked' }) },
|
|
11491
12519
|
onChangeMenu: changeMenu,
|
|
12520
|
+
/* ---- multi-select (issue #13) ---- */
|
|
12521
|
+
selection: changeSelection,
|
|
12522
|
+
onSelectionChange: setChangeSelection,
|
|
12523
|
+
anchorRef: changeAnchor,
|
|
12524
|
+
onBulkStage: bulkStageChanges,
|
|
12525
|
+
onBulkUnstage: bulkUnstageChanges,
|
|
12526
|
+
onBulkDiscard: openDiscardManyDialog,
|
|
12527
|
+
onCopySelection: copyChangeSelection,
|
|
12528
|
+
onKeyDown: onChangesKeyDown,
|
|
11492
12529
|
})
|
|
11493
12530
|
|
|
11494
12531
|
|
|
@@ -11691,19 +12728,19 @@ window.__ModuleLoader__.load({
|
|
|
11691
12728
|
title: t('toolbar.tree'),
|
|
11692
12729
|
onClick: () => foldTree(!treeOpen),
|
|
11693
12730
|
}, E(Icon, { name: treeOpen ? 'eye' : 'eyeOff' }))),
|
|
11694
|
-
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,
|
|
12731
|
+
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,
|
|
11695
12732
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11696
12733
|
view === 'changes' ? changesPane : E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane))
|
|
11697
12734
|
} else if (columns) {
|
|
11698
12735
|
body = E('div', { className: 'dig-body dig-body-columns', ref: bodyRef },
|
|
11699
|
-
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,
|
|
12736
|
+
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,
|
|
11700
12737
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11701
12738
|
E('div', { className: 'dig-pane dig-pane-main' }, historyPane, diffGutter, diffPane),
|
|
11702
12739
|
gutter('changes', 'next'),
|
|
11703
12740
|
E('div', { className: 'dig-pane dig-pane-changes', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane))
|
|
11704
12741
|
} else {
|
|
11705
12742
|
body = E('div', { className: 'dig-body dig-body-stack', ref: bodyRef },
|
|
11706
|
-
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,
|
|
12743
|
+
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,
|
|
11707
12744
|
treeOpen ? gutter('tree', 'prev') : null,
|
|
11708
12745
|
E('div', { className: 'dig-pane dig-pane-changes-stack', 'data-pane': 'changes', style: paneStyle('changes') }, changesPane),
|
|
11709
12746
|
gutter('changes', 'prev'),
|
|
@@ -11883,6 +12920,60 @@ window.__ModuleLoader__.load({
|
|
|
11883
12920
|
onConfirm: () => { const ref = dialog.ref; setDialog(null); void dropStash(ref) },
|
|
11884
12921
|
}))
|
|
11885
12922
|
}
|
|
12923
|
+
/* Multi-select delete (issue #13): local branches / remote branches / tags
|
|
12924
|
+
in ONE confirm — each rides its existing destructive host method, and a
|
|
12925
|
+
protected local branch keeps the type-the-name gate. */
|
|
12926
|
+
if (dialog !== null && dialog.kind === 'deleteRefs') {
|
|
12927
|
+
const guardedName = dialog.locals.find((name) => isProtectedBranch(name) === true)
|
|
12928
|
+
const listed = dialog.locals
|
|
12929
|
+
.concat(dialog.tags)
|
|
12930
|
+
.concat(dialog.remotes.map((item) => item.remote + '/' + item.branch))
|
|
12931
|
+
const listText = listed.length <= 8 ? listed.join('、') : listed.slice(0, 8).join('、') + ' …'
|
|
12932
|
+
overlays.push(E(ConfirmDialog, {
|
|
12933
|
+
key: 'deleteRefs',
|
|
12934
|
+
t: t,
|
|
12935
|
+
title: t('confirm.title'),
|
|
12936
|
+
text: fill(t('confirm.deleteRefs'), { b: dialog.locals.length, r: dialog.remotes.length, g: dialog.tags.length })
|
|
12937
|
+
+ (listText === '' ? '' : ' ' + listText),
|
|
12938
|
+
requireText: guardedName === undefined ? undefined : guardedName,
|
|
12939
|
+
okLabel: t('confirm.ok'), cancelLabel: t('confirm.cancel'),
|
|
12940
|
+
onCancel: () => setDialog(null),
|
|
12941
|
+
onConfirm: () => {
|
|
12942
|
+
const locals = dialog.locals
|
|
12943
|
+
const tags = dialog.tags
|
|
12944
|
+
const remotes = dialog.remotes
|
|
12945
|
+
setDialog(null)
|
|
12946
|
+
if (locals.length > 0) void deleteBranches(locals)
|
|
12947
|
+
if (tags.length > 0) void run('tagDelete', { names: tags })
|
|
12948
|
+
const byRemote = new Map()
|
|
12949
|
+
for (const item of remotes) {
|
|
12950
|
+
if (byRemote.has(item.remote) === false) byRemote.set(item.remote, [])
|
|
12951
|
+
byRemote.get(item.remote).push(item.branch)
|
|
12952
|
+
}
|
|
12953
|
+
for (const pair of byRemote) void run('branchDeleteRemote', { remote: pair[0], branches: pair[1], confirm: true })
|
|
12954
|
+
},
|
|
12955
|
+
}))
|
|
12956
|
+
}
|
|
12957
|
+
/* Multi-select discard: tracked paths go through `checkout --`, untracked
|
|
12958
|
+
through `clean -fd` — the same two-lane split as Discard All. */
|
|
12959
|
+
if (dialog !== null && dialog.kind === 'discardMany') {
|
|
12960
|
+
overlays.push(E(ConfirmDialog, {
|
|
12961
|
+
key: 'discardMany',
|
|
12962
|
+
t: t,
|
|
12963
|
+
title: t('confirm.title'),
|
|
12964
|
+
text: fill(t('confirm.discardMany'), { n: dialog.tracked.length + dialog.untracked.length })
|
|
12965
|
+
+ (dialog.untracked.length > 0 ? ' ' + fill(t('confirm.discardManyUntracked'), { m: dialog.untracked.length }) : ''),
|
|
12966
|
+
okLabel: t('confirm.ok'), cancelLabel: t('confirm.cancel'),
|
|
12967
|
+
onCancel: () => setDialog(null),
|
|
12968
|
+
onConfirm: () => {
|
|
12969
|
+
const tracked = dialog.tracked
|
|
12970
|
+
const untracked = dialog.untracked
|
|
12971
|
+
setDialog(null)
|
|
12972
|
+
if (tracked.length > 0) void run('discard', { paths: tracked, confirm: true })
|
|
12973
|
+
if (untracked.length > 0) void run('discard', { paths: untracked, untracked: true, confirm: true })
|
|
12974
|
+
},
|
|
12975
|
+
}))
|
|
12976
|
+
}
|
|
11886
12977
|
if (dialog !== null && dialog.kind === 'discard') {
|
|
11887
12978
|
overlays.push(E(ConfirmDialog, {
|
|
11888
12979
|
key: 'discard',
|
|
@@ -12179,7 +13270,7 @@ window.__ModuleLoader__.load({
|
|
|
12179
13270
|
'.dig-row{display:flex;align-items:center;gap:5px;padding:2px 8px;cursor:pointer;white-space:nowrap;overflow:hidden}',
|
|
12180
13271
|
'.dig-row:hover{background:var(--dsw-alias-interactive-bg-hover)}',
|
|
12181
13272
|
'.dig-row-head{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary))}',
|
|
12182
|
-
'.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover))}',
|
|
13273
|
+
'.dig-row-selected{background:var(--dsw-alias-interactive-bg-active,var(--dsw-alias-interactive-bg-hover));color:var(--dsw-alias-label-primary)}',
|
|
12183
13274
|
'.dig-row-remote .dig-row-label{color:var(--dsw-alias-label-secondary);font-weight:500}',
|
|
12184
13275
|
'.dig-row-tag .dig-row-label{color:var(--dsw-alias-state-warn-primary,var(--dsw-alias-brand-primary));font-weight:500}',
|
|
12185
13276
|
'.dig-row-head .dig-row-label{font-weight:600}',
|
|
@@ -12188,6 +13279,10 @@ window.__ModuleLoader__.load({
|
|
|
12188
13279
|
'.dig-row-sub{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none;max-width:45%;overflow:hidden;text-overflow:ellipsis}',
|
|
12189
13280
|
'.dig-row-dir{margin-left:auto;opacity:.75}',
|
|
12190
13281
|
'.dig-badge{font-size:10px;padding:0 5px;border-radius:999px;corner-shape:round;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);flex:none}',
|
|
13282
|
+
/* Ahead/behind colours (user request): pending pushes read green, pending
|
|
13283
|
+
pulls read blue — the same semantics in the tree rows and the top bar. */
|
|
13284
|
+
'.dig-badge-ahead,.dig-track-ahead{color:var(--dsw-alias-state-success-primary)}',
|
|
13285
|
+
'.dig-badge-behind,.dig-track-behind{color:var(--dsw-alias-state-business-primary,var(--dsw-alias-brand-primary))}',
|
|
12191
13286
|
'.dig-badge-muted{opacity:.8}',
|
|
12192
13287
|
'.dig-badge-worktree{padding:0 3px;display:inline-flex;align-items:center;height:14px}',
|
|
12193
13288
|
'.dig-history{display:flex;flex-direction:column;min-height:0;flex:1}',
|
|
@@ -12217,9 +13312,20 @@ window.__ModuleLoader__.load({
|
|
|
12217
13312
|
'.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}',
|
|
12218
13313
|
'.dig-load-more:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}',
|
|
12219
13314
|
'.dig-empty{padding:14px;text-align:center;color:var(--dsw-alias-label-tertiary);font-weight:500}',
|
|
12220
|
-
'.dig-changes{display:flex;flex-direction:column;min-height:0;flex:1}',
|
|
13315
|
+
'.dig-changes{display:flex;flex-direction:column;min-height:0;flex:1;position:relative}',
|
|
12221
13316
|
'.dig-changes-body{display:flex;flex-direction:column;min-height:0;flex:1}',
|
|
12222
13317
|
'.dig-changes-list{flex:1;overflow:auto;min-height:0}',
|
|
13318
|
+
/* Multi-select chrome (issue #13). The marquee box is anchored to the
|
|
13319
|
+
VIEWPORT-space .dig-changes (invariant 13: position:fixed would be
|
|
13320
|
+
captured by the workbench's contain:layout and flung off-panel). */
|
|
13321
|
+
'.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}',
|
|
13322
|
+
'.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)}',
|
|
13323
|
+
'.dig-selection-bar .dig-link{margin-left:0}',
|
|
13324
|
+
'.dig-selection-bar .dig-link-danger{color:var(--dsw-alias-state-error-primary)}',
|
|
13325
|
+
'.dig-selection-count{color:var(--dsw-alias-label-secondary);white-space:nowrap}',
|
|
13326
|
+
/* Focusable lists keep their own selection highlight as the focus
|
|
13327
|
+
indicator, so the container ring is suppressed. */
|
|
13328
|
+
'.dig-changes-list:focus-visible,.dig-tree:focus-visible,.dig-history-scroll:focus-visible{outline:none}',
|
|
12223
13329
|
'.dig-group{display:flex;flex-direction:column}',
|
|
12224
13330
|
'.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}',
|
|
12225
13331
|
'.dig-link{margin-left:auto;border:none;background:transparent;color:var(--dsw-alias-brand-primary);font:inherit;cursor:pointer;padding:0;flex:none}',
|