New feature: duplicate tab

This commit is contained in:
aleksey-hoffman committed 2026-08-24 16:33:01 +03:00
1 parent 662a6988b3
commit dc5c8cb32e
27 files changed
+252 -38

No files matched your search

+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "新标签页",
"duplicateTab": "复制标签页",
"closeOtherTabs": "关闭其他标签",
"closeDuplicateTabs": "关闭所有重复标签",
"closeAllTabs": "关闭所有标签",
@@ -556,6 +557,8 @@
"openEntry": "打开地址编辑器",
"copyCurrentDirectoryPathToClipboard": "复制当前目录路径到剪贴板",
"openCopiedPath": "打开复制的路径",
"openNewTab": "打开新标签页",
"duplicateCurrentTab": "复制当前标签页",
"openCurrentDirInNewTab": "在新标签页中打开当前目录",
"openCurrentDirInTerminal": "在终端打开当前目录",
"openCurrentDirInTerminalAsAdmin": "以管理员身份在终端中打开当前目录",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "文件已在网络上共享。当前共享将被停止并替换为新的共享。",
"replaceShareDialogConfirm": "替换"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Neuer Tab",
"duplicateTab": "Tab duplizieren",
"closeOtherTabs": "Andere Tabs schließen",
"closeDuplicateTabs": "Alle Duplikate schließen",
"closeAllTabs": "Alle Tabs schließen",
@@ -556,6 +557,8 @@
"openEntry": "Adresseditor öffnen",
"copyCurrentDirectoryPathToClipboard": "Aktuellen Verzeichnispfad in die Zwischenablage kopieren",
"openCopiedPath": "Kopierpfad öffnen",
"openNewTab": "Neuen Tab öffnen",
"duplicateCurrentTab": "Aktuellen Tab duplizieren",
"openCurrentDirInNewTab": "Aktuelles Verzeichnis in neuem Tab öffnen",
"openCurrentDirInTerminal": "Aktuelles Verzeichnis im Terminal öffnen",
"openCurrentDirInTerminalAsAdmin": "Aktuelles Verzeichnis im Terminal als Administrator öffnen",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Es werden bereits Dateien im Netzwerk freigegeben. Die aktuelle Freigabe wird gestoppt und durch eine neue ersetzt.",
"replaceShareDialogConfirm": "Ersetzen"
}
}
}
+3
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "New tab",
"duplicateTab": "Duplicate tab",
"closeOtherTabs": "Close other tabs",
"closeDuplicateTabs": "Close all duplicates",
"closeAllTabs": "Close all tabs",
@@ -585,6 +586,8 @@
"openEntry": "Open address editor",
"copyCurrentDirectoryPathToClipboard": "Copy current directory path to clipboard",
"openCopiedPath": "Open copied path",
"openNewTab": "Open new tab",
"duplicateCurrentTab": "Duplicate current tab",
"openCurrentDirInNewTab": "Open current dir in new tab",
"openCurrentDirInTerminal": "Open current dir in terminal",
"openCurrentDirInTerminalAsAdmin": "Open current dir in terminal as admin",
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Nueva pestaña",
"duplicateTab": "Duplicar pestaña",
"closeOtherTabs": "Cerrar otras pestañas",
"closeDuplicateTabs": "Cerrar todos los duplicados",
"closeAllTabs": "Cerrar todas las pestañas",
@@ -556,6 +557,8 @@
"openEntry": "Abrir editor de dirección",
"copyCurrentDirectoryPathToClipboard": "Copiar la ruta del directorio actual al portapapeles",
"openCopiedPath": "Abrir ruta copiada",
"openNewTab": "Abrir una nueva pestaña",
"duplicateCurrentTab": "Duplicar la pestaña actual",
"openCurrentDirInNewTab": "Abrir el directorio actual en una nueva pestaña",
"openCurrentDirInTerminal": "Abrir el directorio actual en la terminal",
"openCurrentDirInTerminalAsAdmin": "Abrir el directorio actual en la terminal como administrador",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Ya se están compartiendo archivos en la red. El recurso compartido actual se detendrá y será reemplazado por uno nuevo.",
"replaceShareDialogConfirm": "Reemplazar"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "برگه جدید",
"duplicateTab": "تکرار برگه",
"closeOtherTabs": "بستن سایر برگه ها",
"closeDuplicateTabs": "بستن همه تکراری‌ها",
"closeAllTabs": "بستن همه برگه ها",
@@ -552,6 +553,8 @@
"openEntry": "باز کردن ویرایشگر آدرس",
"copyCurrentDirectoryPathToClipboard": "کپی مسیر دایرکتوری فعلی در کلیپ بورد",
"openCopiedPath": "باز کردن مسیر کپی شده",
"openNewTab": "باز کردن برگه جدید",
"duplicateCurrentTab": "تکرار برگه فعلی",
"openCurrentDirInNewTab": "باز کردن مسیر فعلی در تب جدید",
"openCurrentDirInTerminal": "باز کردن مسیر فعلی در ترمینال",
"openCurrentDirInTerminalAsAdmin": "باز کردن dir فعلی در ترمینال به عنوان مدیر",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "فایل‌ها از قبل از طریق شبکه به اشتراک گذاشته شده‌اند. اشتراک‌گذاری فعلی متوقف و با مورد جدید جایگزین خواهد شد.",
"replaceShareDialogConfirm": "جایگزینی"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Nouvel onglet",
"duplicateTab": "Dupliquer l'onglet",
"closeOtherTabs": "Fermer les autres onglets",
"closeDuplicateTabs": "Fermer tous les doublons",
"closeAllTabs": "Fermer tous les onglets",
@@ -556,6 +557,8 @@
"openEntry": "Ouvrir l'éditeur d'adresse",
"copyCurrentDirectoryPathToClipboard": "Copier le chemin du répertoire actuel dans le presse-papiers",
"openCopiedPath": "Ouvrir le chemin copié",
"openNewTab": "Ouvrir un nouvel onglet",
"duplicateCurrentTab": "Dupliquer l'onglet actuel",
"openCurrentDirInNewTab": "Ouvrir le répertoire actuel dans un nouvel onglet",
"openCurrentDirInTerminal": "Ouvrir le répertoire actuel dans le terminal",
"openCurrentDirInTerminalAsAdmin": "Ouvrir le répertoire actuel dans le terminal en tant qu'administrateur",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Des fichiers sont déjà partagés sur le réseau. Le partage actuel sera arrêté et remplacé par le nouveau.",
"replaceShareDialogConfirm": "Remplacer"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "לשונית חדשה",
"duplicateTab": "שכפל לשונית",
"closeOtherTabs": "סגור לשוניות אחרות",
"closeDuplicateTabs": "סגור את כל הכפילויות",
"closeAllTabs": "סגור את כל הלשוניות",
@@ -585,6 +586,8 @@
"openEntry": "פתח את עורך הכתובת",
"copyCurrentDirectoryPathToClipboard": "העתק את נתיב התיקייה הנוכחית ללוח",
"openCopiedPath": "פתח את הנתיב שהועתק",
"openNewTab": "פתח לשונית חדשה",
"duplicateCurrentTab": "שכפל את הלשונית הנוכחית",
"openCurrentDirInNewTab": "פתח את התיקייה הנוכחית בלשונית חדשה",
"openCurrentDirInTerminal": "פתח את התיקייה הנוכחית במסוף",
"openCurrentDirInTerminalAsAdmin": "פתח את התיקייה הנוכחית במסוף כמנהל",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "קבצים כבר משותפים ברשת. השיתוף הנוכחי ייעצר ויוחלף בחדש.",
"replaceShareDialogConfirm": "החלף"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "नया टैब",
"duplicateTab": "टैब डुप्लिकेट करें",
"closeOtherTabs": "अन्य टैब बंद करें",
"closeDuplicateTabs": "सभी डुप्लिकेट बंद करें",
"closeAllTabs": "सभी टैब को बंद करें",
@@ -585,6 +586,8 @@
"openEntry": "पता संपादक खोलें",
"copyCurrentDirectoryPathToClipboard": "वर्तमान निर्देशिका पथ को क्लिपबोर्ड पर कॉपी करें",
"openCopiedPath": "कॉपी किया गया पथ खोलें",
"openNewTab": "नया टैब खोलें",
"duplicateCurrentTab": "वर्तमान टैब डुप्लिकेट करें",
"openCurrentDirInNewTab": "वर्तमान डीआईआर को नए टैब में खोलें",
"openCurrentDirInTerminal": "टर्मिनल में वर्तमान डीआईआर खोलें",
"openCurrentDirInTerminalAsAdmin": "टर्मिनल में वर्तमान डीआईआर को व्यवस्थापक के रूप में खोलें",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "फ़ाइलें पहले से ही नेटवर्क पर साझा की जा रही हैं. वर्तमान शेयर बंद कर दिया जाएगा और उसके स्थान पर नया शेयर ले लिया जाएगा।",
"replaceShareDialogConfirm": "प्रतिस्थापित करें"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Նոր թաբ",
"duplicateTab": "Կրկնօրինակել թաբը",
"closeOtherTabs": "Փակել այլ թաբերը",
"closeDuplicateTabs": "Փակել կրկնվող թաբերը",
"closeAllTabs": "Փակել բոլոր թաբերը",
@@ -585,6 +586,8 @@
"openEntry": "Փոփոխել հասցեն",
"copyCurrentDirectoryPathToClipboard": "Պատճենել ընտրված պանակի ուղին",
"openCopiedPath": "Բացել պատճենված ուղին",
"openNewTab": "Բացել նոր թաբ",
"duplicateCurrentTab": "Կրկնօրինակել ընթացիկ թաբը",
"openCurrentDirInNewTab": "Բացել ընտրված պանակը նոր թաբում",
"openCurrentDirInTerminal": "Բացել ընտրված պանակը տերմինալում",
"openCurrentDirInTerminalAsAdmin": "Բացել ընտրված պանակը ադմինի անունից՝ տերմինալում",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Ֆայլերն արդեն կիսվում են ցանցում: Ընթացիկ կիսումը կդադարեցվի և կփոխարինվի նորով:",
"replaceShareDialogConfirm": "Փոխարինել"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Nuova scheda",
"duplicateTab": "Duplica scheda",
"closeOtherTabs": "Chiudi altre schede",
"closeDuplicateTabs": "Chiudi tutti i duplicati",
"closeAllTabs": "Chiudi tutte le schede",
@@ -556,6 +557,8 @@
"openEntry": "Apri l'editor dell'indirizzo",
"copyCurrentDirectoryPathToClipboard": "Copia il percorso della directory corrente negli appunti",
"openCopiedPath": "Apri il percorso copiato",
"openNewTab": "Apri una nuova scheda",
"duplicateCurrentTab": "Duplica la scheda attuale",
"openCurrentDirInNewTab": "Apri la cartella attuale in una nuova scheda",
"openCurrentDirInTerminal": "Apri la cartella attuale nel terminale",
"openCurrentDirInTerminalAsAdmin": "Apri la cartella attuale nel terminale come amministratore",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Ci sono già file condivisi in rete. La condivisione attuale verrà interrotta e sostituita con quella nuova.",
"replaceShareDialogConfirm": "Sostituisci"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "新しいタブ",
"duplicateTab": "タブを複製",
"closeOtherTabs": "他のタブを閉じる",
"closeDuplicateTabs": "すべての重複を閉じる",
"closeAllTabs": "すべてのタブを閉じる",
@@ -556,6 +557,8 @@
"openEntry": "アドレスエディターを開く",
"copyCurrentDirectoryPathToClipboard": "現在のディレクトリパスをクリップボードにコピー",
"openCopiedPath": "コピーしたパスを開く",
"openNewTab": "新しいタブを開く",
"duplicateCurrentTab": "現在のタブを複製",
"openCurrentDirInNewTab": "現在のディレクトリを新しいタブで開く",
"openCurrentDirInTerminal": "ターミナルで現在のディレクトリを開く",
"openCurrentDirInTerminalAsAdmin": "ターミナルで現在のディレクトリを管理者として開く",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "すでにネットワーク上でファイルが共有されています。現在の共有を停止し、新しいものに置き換えます。",
"replaceShareDialogConfirm": "置き換え"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Nova guia",
"duplicateTab": "Duplicar guia",
"closeOtherTabs": "Fechar outras guias",
"closeDuplicateTabs": "Fechar todos os duplicados",
"closeAllTabs": "Fechar todas as guias",
@@ -556,6 +557,8 @@
"openEntry": "Abrir editor de endereço",
"copyCurrentDirectoryPathToClipboard": "Copiar caminho do diretório atual para a área de transferência",
"openCopiedPath": "Caminho copiado aberto",
"openNewTab": "Abrir nova aba",
"duplicateCurrentTab": "Duplicar a aba atual",
"openCurrentDirInNewTab": "Abrir o diretório atual em nova aba",
"openCurrentDirInTerminal": "Abra o diretório atual no terminal",
"openCurrentDirInTerminalAsAdmin": "Abra o diretório atual no terminal como administrador",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Já existem arquivos sendo compartilhados na rede. O compartilhamento atual será interrompido e substituído pelo novo.",
"replaceShareDialogConfirm": "Substituir"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Новая вкладка",
"duplicateTab": "Дублировать вкладку",
"closeOtherTabs": "Закрыть другие вкладки",
"closeDuplicateTabs": "Закрыть все дубликаты",
"closeAllTabs": "Закрыть все вкладки",
@@ -585,6 +586,8 @@
"openEntry": "Открыть редактор адреса",
"copyCurrentDirectoryPathToClipboard": "Скопировать текущий путь к папке в буфер обмена",
"openCopiedPath": "Открыть скопированный путь",
"openNewTab": "Открыть новую вкладку",
"duplicateCurrentTab": "Дублировать текущую вкладку",
"openCurrentDirInNewTab": "Открыть текущую папку в новой вкладке",
"openCurrentDirInTerminal": "Открыть текущую папку в терминале",
"openCurrentDirInTerminalAsAdmin": "Открыть текущую папку в терминале от имени администратора",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Файлы уже доступны по сети. Текущий доступ будет остановлен и заменён новым.",
"replaceShareDialogConfirm": "Заменить"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Nov zavihek",
"duplicateTab": "Podvoji zavihek",
"closeOtherTabs": "Zapri druge zavihke",
"closeDuplicateTabs": "Zapri vse podvojene",
"closeAllTabs": "Zapri vse zavihke",
@@ -585,6 +586,8 @@
"openEntry": "Odpri urejevalnik naslova",
"copyCurrentDirectoryPathToClipboard": "Kopiraj pot trenutne mape v odložišče",
"openCopiedPath": "Odpri kopirano pot",
"openNewTab": "Odpri nov zavihek",
"duplicateCurrentTab": "Podvoji trenutni zavihek",
"openCurrentDirInNewTab": "Odpri trenutno mapo v novem zavihku",
"openCurrentDirInTerminal": "Odpri trenutno mapo v terminalu",
"openCurrentDirInTerminalAsAdmin": "Odpri trenutno mapo v terminalu kot admin",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Datoteke se že delijo po omrežju. Trenutno deljenje bo ustavljeno in zamenjano z novim.",
"replaceShareDialogConfirm": "Zamenjaj"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Yeni sekme",
"duplicateTab": "Sekmeyi çoğalt",
"closeOtherTabs": "Diğer sekmeleri kapat",
"closeDuplicateTabs": "Tüm yinelenenleri kapat",
"closeAllTabs": "Tüm sekmeleri kapat",
@@ -556,6 +557,8 @@
"openEntry": "Adres düzenleyiciyi aç",
"copyCurrentDirectoryPathToClipboard": "Geçerli dizin yolunu panoya kopyala",
"openCopiedPath": "Kopyalanan yolu aç",
"openNewTab": "Yeni sekme aç",
"duplicateCurrentTab": "Geçerli sekmeyi çoğalt",
"openCurrentDirInNewTab": "Geçerli dizini yeni sekmede aç",
"openCurrentDirInTerminal": "Geçerli dizini terminalde aç",
"openCurrentDirInTerminalAsAdmin": "Geçerli dizini terminalde yönetici olarak aç",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Ağ üzerinden zaten dosyalar paylaşılıyor. Mevcut paylaşım durdurulup yenisiyle değiştirilecek.",
"replaceShareDialogConfirm": "Değiştir"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "نیا ٹیب",
"duplicateTab": "ٹیب نقل کریں",
"closeOtherTabs": "دوسرے ٹیب بند کریں",
"closeDuplicateTabs": "تمام ڈپلیکیٹ بند کریں",
"closeAllTabs": "تمام ٹیب بند کریں",
@@ -585,6 +586,8 @@
"openEntry": "پتہ ایڈیٹر کھولیں",
"copyCurrentDirectoryPathToClipboard": "موجودہ فولڈر کا راستہ کلپ بورڈ پر کاپی کریں",
"openCopiedPath": "کاپی شدہ راستہ کھولیں",
"openNewTab": "نیا ٹیب کھولیں",
"duplicateCurrentTab": "موجودہ ٹیب نقل کریں",
"openCurrentDirInNewTab": "موجودہ فولڈر نئے ٹیب میں کھولیں",
"openCurrentDirInTerminal": "موجودہ فولڈر ٹرمینل میں کھولیں",
"openCurrentDirInTerminalAsAdmin": "موجودہ فولڈر ایڈمن ٹرمینل میں کھولیں",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "فائلیں پہلے سے نیٹ ورک پر ہیں۔ موجودہ شیئر بند ہو کر نئے سے بدل جائے گا۔",
"replaceShareDialogConfirm": "بدل دیں"
}
}
}
+4 -1
View File
@@ -77,6 +77,7 @@
},
"tabs": {
"newTab": "Thẻ mới",
"duplicateTab": "Nhân bản thẻ",
"closeOtherTabs": "Đóng các thẻ khác",
"closeDuplicateTabs": "Đóng tất cả bản trùng",
"closeAllTabs": "Đóng tất cả thẻ",
@@ -556,6 +557,8 @@
"openEntry": "Mở trình chỉnh sửa địa chỉ",
"copyCurrentDirectoryPathToClipboard": "Sao chép đường dẫn thư mục hiện tại vào clipboard",
"openCopiedPath": "Mở đường dẫn đã sao chép",
"openNewTab": "Mở tab mới",
"duplicateCurrentTab": "Nhân bản tab hiện tại",
"openCurrentDirInNewTab": "Mở thư mục hiện tại trong tab mới",
"openCurrentDirInTerminal": "Mở thư mục hiện tại trong thiết bị đầu cuối",
"openCurrentDirInTerminalAsAdmin": "Mở thư mục hiện tại trong thiết bị đầu cuối với tư cách quản trị viên",
@@ -1257,4 +1260,4 @@
"replaceShareDialogDescription": "Các tập tin đang được chia sẻ qua mạng. Chia sẻ hiện tại sẽ bị dừng và thay thế bằng chia sẻ mới.",
"replaceShareDialogConfirm": "Thay thế"
}
}
}
@@ -391,14 +391,10 @@ function handleCreateLink(linkKind: LinkCreationKind) {
<component
:is="menuItemComponent"
v-if="isActionVisible('open-in-new-tab')"
class="file-browser-actions-menu__item-with-shortcut"
@select="emitAction('open-in-new-tab')"
>
<SquarePlusIcon :size="16" />
<span>{{ t('fileBrowser.actions.openInNewTab') }}</span>
<ContextMenuShortcut v-if="shortcutsStore.getShortcutLabel('openNewTab')">
{{ shortcutsStore.getShortcutLabel('openNewTab') }}
</ContextMenuShortcut>
</component>
<component
:is="menuItemComponent"
+25 -12
View File
@@ -24,7 +24,10 @@ import { useDismissalLayerStore } from '@/stores/runtime/dismissal-layer';
import { useGlobalSearchStore } from '@/stores/runtime/global-search';
import { useShortcutsStore, getSelectedTextForCopy } from '@/stores/runtime/shortcuts';
import { toast, ToastStatic } from '@/components/ui/toaster';
import { getVirtualLocationActionContext } from '@/utils/virtual-location-action-target';
import {
getVirtualLocationActionContext,
resolveDuplicateCurrentTabShortcutTarget,
} from '@/utils/virtual-location-action-target';
import { blurFocusedDirEntry } from '@/modules/navigator/utils/blur-focused-dir-entry';
import { useTerminalsStore } from '@/stores/runtime/terminals';
import { useDirSizesStore } from '@/stores/runtime/dir-sizes';
@@ -950,23 +953,32 @@ async function openTerminalWithOptions(asAdmin: boolean) {
}
async function handleOpenNewTabShortcut() {
const actionContext = getNavigatorActionContext();
if (actionContext.isBrowsingVirtualLocations) {
if (actionContext.actionTargetEntries.length > 0) {
getActivePaneRef()?.performSelectionAction?.('open-in-new-tab');
}
return;
}
await workspacesStore.openNewTabGroup();
}
async function handleDuplicateCurrentTabShortcut() {
const currentPath = getActiveCurrentPath();
const target = resolveDuplicateCurrentTabShortcutTarget(
currentPath,
getNavigatorActionContext(currentPath),
);
if (!currentPath) {
if (!target) {
return;
}
await workspacesStore.openNewTabGroup(currentPath);
switch (target.kind) {
case 'open-selection':
getActivePaneRef()?.performSelectionAction?.('open-in-new-tab');
return;
case 'clone-path':
await workspacesStore.openNewTabGroup(target.path);
return;
default: {
const exhaustiveCheck: never = target;
return exhaustiveCheck;
}
}
}
async function handleCloseCurrentTabShortcut() {
@@ -1119,6 +1131,7 @@ function registerShortcutHandlers() {
shortcutsStore.registerHandler('print', handlePrintShortcut, { checkItemSelected: hasSelectedItems });
shortcutsStore.registerHandler('properties', handlePropertiesShortcut, { checkItemSelected: hasSelectedItems });
shortcutsStore.registerHandler('openNewTab', handleOpenNewTabShortcut);
shortcutsStore.registerHandler('duplicateCurrentTab', handleDuplicateCurrentTabShortcut);
shortcutsStore.registerHandler('closeCurrentTab', handleCloseCurrentTabShortcut);
shortcutsStore.registerHandler('restoreLastClosedTab', handleRestoreLastClosedTabShortcut);
shortcutsStore.registerHandler('switchToNextTab', handleSwitchToNextTabShortcut);
@@ -38,6 +38,7 @@ import {
SearchIcon,
TextSearchIcon,
CopyIcon,
CopyPlusIcon,
FolderInputIcon,
ClipboardPasteIcon,
CheckCheckIcon,
@@ -133,6 +134,7 @@ const shortcutIcons: Record<ShortcutId, Component> = {
print: PrinterIcon,
properties: InfoIcon,
openNewTab: PlusIcon,
duplicateCurrentTab: CopyPlusIcon,
closeCurrentTab: XIcon,
restoreLastClosedTab: RotateCcwIcon,
switchToNextTab: ChevronsRightIcon,
+15 -3
View File
@@ -201,9 +201,15 @@ onBeforeUnmount(() => {
</Button>
</TooltipTrigger>
<TooltipContent>
<div class="tab-bar__tooltip-row">
{{ t('tabs.newTab') }}
<ContextMenuShortcut>{{ shortcutsStore.getShortcutLabel('openNewTab') }}</ContextMenuShortcut>
<div class="tab-bar__tooltip">
<div class="tab-bar__tooltip-row">
{{ t('tabs.newTab') }}
<ContextMenuShortcut>{{ shortcutsStore.getShortcutLabel('openNewTab') }}</ContextMenuShortcut>
</div>
<div class="tab-bar__tooltip-row">
{{ t('shortcuts.duplicateCurrentTab') }}
<ContextMenuShortcut>{{ shortcutsStore.getShortcutLabel('duplicateCurrentTab') }}</ContextMenuShortcut>
</div>
</div>
</TooltipContent>
</Tooltip>
@@ -244,6 +250,12 @@ onBeforeUnmount(() => {
color: hsl(var(--muted-foreground));
}
.tab-bar__tooltip {
display: flex;
flex-direction: column;
gap: 6px;
}
.tab-bar__tooltip-row {
display: flex;
align-items: center;
+32 -2
View File
@@ -10,7 +10,7 @@ import { useTimeoutFn, useEventListener } from '@vueuse/core';
import { useWorkspacesStore } from '@/stores/storage/workspaces';
import { useUserSettingsStore } from '@/stores/storage/user-settings';
import type { Tab } from '@/types/workspaces';
import { Layers, XIcon, XLineTopIcon } from '@lucide/vue';
import { CopyPlusIcon, Layers, XIcon, XLineTopIcon } from '@lucide/vue';
import { getPathDisplayName, getPathDisplayValue } from '@/utils/normalize-path';
import {
Tooltip,
@@ -21,8 +21,10 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { useShortcutsStore } from '@/stores/runtime/shortcuts';
import { useFileBrowserDragSession } from '@/modules/navigator/components/file-browser/composables/use-file-browser-drag-session';
interface Props {
@@ -42,6 +44,7 @@ const emit = defineEmits<Emits>();
const { t } = useI18n();
const workspacesStore = useWorkspacesStore();
const shortcutsStore = useShortcutsStore();
const userSettingsStore = useUserSettingsStore();
const dragSession = useFileBrowserDragSession();
const activeFileBrowserDragState = dragSession.dragState;
@@ -192,6 +195,16 @@ function handleTabMouseUp(event: MouseEvent) {
dragSession.dropOn(dropTargetTab.value.path, event.shiftKey ? 'copy' : undefined);
}
async function duplicateTab() {
const path = props.tabGroup[0]?.path;
if (!path) {
return;
}
await workspacesStore.openNewTabGroup(path);
}
function closeOtherTabs() {
workspacesStore.closeOtherTabGroups(props.tabGroup);
}
@@ -265,6 +278,19 @@ onBeforeUnmount(() => {
align="start"
class="tab__dropdown-menu"
>
<DropdownMenuItem
class="tab__menu-item-with-shortcut"
@select="duplicateTab"
>
<CopyPlusIcon
class="tab__menu-button-icon"
:size="14"
/>
{{ t('tabs.duplicateTab') }}
<DropdownMenuShortcut v-if="shortcutsStore.getShortcutLabel('duplicateCurrentTab')">
{{ shortcutsStore.getShortcutLabel('duplicateCurrentTab') }}
</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem @select="closeOtherTabs">
<XLineTopIcon
class="tab__menu-button-icon"
@@ -422,7 +448,11 @@ onBeforeUnmount(() => {
}
.tab__dropdown-menu {
min-width: 160px;
min-width: 220px;
}
.tab__menu-item-with-shortcut {
gap: 8px;
}
.tab__menu-button {
@@ -578,6 +578,41 @@ describe('shortcuts store', () => {
expect(event.defaultPrevented).toBe(true);
});
it('matches Ctrl+T for opening a new tab and Ctrl+Alt+T for duplicating the current tab', async () => {
const shortcutsStore = useShortcutsStore();
const openNewTabHandler = vi.fn();
const duplicateCurrentTabHandler = vi.fn();
shortcutsStore.registerHandler('openNewTab', openNewTabHandler);
shortcutsStore.registerHandler('duplicateCurrentTab', duplicateCurrentTabHandler);
expect(shortcutsStore.getShortcutLabel('openNewTab')).toBe('Ctrl+T');
expect(shortcutsStore.getShortcutLabel('duplicateCurrentTab')).toBe('Ctrl+Alt+T');
const openNewTabEvent = new KeyboardEvent('keydown', {
key: 't',
code: 'KeyT',
ctrlKey: true,
bubbles: true,
cancelable: true,
});
const duplicateCurrentTabEvent = new KeyboardEvent('keydown', {
key: 't',
code: 'KeyT',
ctrlKey: true,
altKey: true,
bubbles: true,
cancelable: true,
});
await expect(shortcutsStore.handleKeydown(openNewTabEvent)).resolves.toBe(true);
await expect(shortcutsStore.handleKeydown(duplicateCurrentTabEvent)).resolves.toBe(true);
expect(openNewTabHandler).toHaveBeenCalledTimes(1);
expect(duplicateCurrentTabHandler).toHaveBeenCalledTimes(1);
expect(openNewTabEvent.defaultPrevented).toBe(true);
expect(duplicateCurrentTabEvent.defaultPrevented).toBe(true);
});
it('matches Ctrl+Shift+T for restoring the last closed tab', async () => {
const shortcutsStore = useShortcutsStore();
const restoreLastClosedTabHandler = vi.fn();
+16 -1
View File
@@ -452,7 +452,7 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
{
id: 'openNewTab',
labelKey: 'shortcuts.openCurrentDirInNewTab',
labelKey: 'shortcuts.openNewTab',
defaultKeys: {
ctrl: true,
key: 't',
@@ -464,6 +464,21 @@ const DEFAULT_SHORTCUTS: ShortcutDefinition[] = [
},
isReadOnly: false,
},
{
id: 'duplicateCurrentTab',
labelKey: 'shortcuts.duplicateCurrentTab',
defaultKeys: {
ctrl: true,
alt: true,
key: 't',
},
scope: 'navigator',
conditions: {
inputFieldIsActive: false,
dialogIsOpened: false,
},
isReadOnly: false,
},
{
id: 'closeCurrentTab',
labelKey: 'shortcuts.closeCurrentTab',
+1
View File
@@ -121,6 +121,7 @@ export type ShortcutId
| 'print'
| 'properties'
| 'openNewTab'
| 'duplicateCurrentTab'
| 'closeCurrentTab'
| 'restoreLastClosedTab'
| 'switchToNextTab'
@@ -10,6 +10,7 @@ import {
getVirtualLocationActionContext,
isBrowsingVirtualLocations,
resolveActionDirectoryPath,
resolveDuplicateCurrentTabShortcutTarget,
} from '@/utils/virtual-location-action-target';
import { LOCATIONS_VIRTUAL_PATH, WSL_HOST_VIRTUAL_PATH } from '@/utils/virtual-path-constants';
@@ -156,4 +157,43 @@ describe('virtual-location-action-target', () => {
});
});
});
describe('resolveDuplicateCurrentTabShortcutTarget', () => {
it('clones the current path for a normal directory', () => {
expect(resolveDuplicateCurrentTabShortcutTarget(
'C:/Users',
getVirtualLocationActionContext([], 'C:/Users'),
)).toEqual({ kind: 'clone-path', path: 'C:/Users' });
});
it('clones the locations virtual path when nothing is selected', () => {
expect(resolveDuplicateCurrentTabShortcutTarget(
LOCATIONS_VIRTUAL_PATH,
getVirtualLocationActionContext([], LOCATIONS_VIRTUAL_PATH),
)).toEqual({ kind: 'clone-path', path: LOCATIONS_VIRTUAL_PATH });
});
it('clones the wsl host virtual path when nothing is selected', () => {
expect(resolveDuplicateCurrentTabShortcutTarget(
WSL_HOST_VIRTUAL_PATH,
getVirtualLocationActionContext([], WSL_HOST_VIRTUAL_PATH),
)).toEqual({ kind: 'clone-path', path: WSL_HOST_VIRTUAL_PATH });
});
it('opens the selected location when browsing virtual locations', () => {
const selectedEntries = [createEntry({ path: 'D:/' })];
expect(resolveDuplicateCurrentTabShortcutTarget(
LOCATIONS_VIRTUAL_PATH,
getVirtualLocationActionContext(selectedEntries, LOCATIONS_VIRTUAL_PATH),
)).toEqual({ kind: 'open-selection' });
});
it('returns null when there is no current path', () => {
expect(resolveDuplicateCurrentTabShortcutTarget(
undefined,
getVirtualLocationActionContext([], undefined),
)).toBeNull();
});
});
});
@@ -99,3 +99,22 @@ export function getVirtualLocationActionContext(
actionTargetPathsText: actionTargetEntries.map(entry => entry.path).join('\n'),
};
}
export type DuplicateCurrentTabShortcutTarget =
| { kind: 'open-selection' }
| { kind: 'clone-path'; path: string };
export function resolveDuplicateCurrentTabShortcutTarget(
currentDirectoryPath: string | null | undefined,
actionContext: VirtualLocationActionContext,
): DuplicateCurrentTabShortcutTarget | null {
if (actionContext.isBrowsingVirtualLocations && actionContext.actionTargetEntries.length > 0) {
return { kind: 'open-selection' };
}
if (!currentDirectoryPath) {
return null;
}
return { kind: 'clone-path', path: currentDirectoryPath };
}