feat(desktop): url focus and mru tab shortcuts (#5683)
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
parent
745fc9d1f6
commit
824dce79d0
14 changed files with 508 additions and 6 deletions
|
|
@ -1412,7 +1412,8 @@
|
|||
"send_request": "Send Request",
|
||||
"share_request": "Share Request",
|
||||
"show_code": "Generate code snippet",
|
||||
"title": "Request"
|
||||
"title": "Request",
|
||||
"focus_url": "Focus URL bar"
|
||||
},
|
||||
"response": {
|
||||
"copy": "Copy response to clipboard",
|
||||
|
|
@ -1427,7 +1428,9 @@
|
|||
"next_tab": "Next Tab",
|
||||
"previous_tab": "Previous Tab",
|
||||
"first_tab": "Switch to First Tab",
|
||||
"last_tab": "Switch to Last Tab"
|
||||
"last_tab": "Switch to Last Tab",
|
||||
"mru_switch": "Switch to recent tab (MRU)",
|
||||
"mru_switch_reverse": "Switch to previous recent tab (MRU)"
|
||||
},
|
||||
"theme": {
|
||||
"black": "Switch theme to Black Mode",
|
||||
|
|
@ -1524,6 +1527,8 @@
|
|||
"previous": "Switch to previous tab",
|
||||
"switch_to_first": "Switch to first tab",
|
||||
"switch_to_last": "Switch to last tab",
|
||||
"mru_switch": "Switch to recent tab",
|
||||
"mru_switch_reverse": "Switch to previous recent tab",
|
||||
"title": "Tabs"
|
||||
},
|
||||
"workspace": {
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@
|
|||
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||
>
|
||||
<SmartEnvInput
|
||||
ref="urlInput"
|
||||
v-model="tab.document.request.endpoint"
|
||||
:placeholder="`${t('request.url_placeholder')}`"
|
||||
:auto-complete-source="userHistories"
|
||||
|
|
@ -322,6 +323,7 @@ const show = ref<any | null>(null)
|
|||
const clearAll = ref<any | null>(null)
|
||||
const copyRequestAction = ref<any | null>(null)
|
||||
const saveRequestAction = ref<any | null>(null)
|
||||
const urlInput = ref<{ focus: () => void } | null>(null)
|
||||
|
||||
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
|
||||
|
||||
|
|
@ -667,6 +669,10 @@ defineActionHandler("request.show-code", () => {
|
|||
showCodegenModal.value = true
|
||||
})
|
||||
|
||||
defineActionHandler("request.focus-url", () => {
|
||||
urlInput.value?.focus()
|
||||
})
|
||||
|
||||
const isCustomMethod = computed(() => {
|
||||
return (
|
||||
tab.value.document.request.method === "CUSTOM" ||
|
||||
|
|
|
|||
|
|
@ -658,6 +658,18 @@ const triggerTextSelection = () => {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Focuses the input editor
|
||||
*/
|
||||
const focusInput = () => {
|
||||
view.value?.focus()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
focus: focusInput,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (editor.value) {
|
||||
if (!view.value) initView(editor.value)
|
||||
|
|
|
|||
|
|
@ -70,6 +70,9 @@ export type HoppAction =
|
|||
| "tab.switch-to-first" // Switch to first tab
|
||||
| "tab.switch-to-last" // Switch to last tab
|
||||
| "tab.reopen-closed" // Reopen recently closed tab
|
||||
| "tab.mru-switch" // Switch to MRU tab (Ctrl/Cmd+Alt+])
|
||||
| "tab.mru-switch-reverse" // Switch to previous MRU tab (Ctrl/Cmd+Alt+[)
|
||||
| "request.focus-url" // Focus the URL bar
|
||||
| "collection.new" // Create root collection
|
||||
| "flyouts.chat.open" // Shows the keybinds flyout
|
||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ type Key =
|
|||
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
||||
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
||||
| "right" | "/" | "?" | "." | "enter" | "tab" | "delete" | "backspace"
|
||||
| "[" | "]"
|
||||
/* eslint-enable */
|
||||
|
||||
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
||||
|
|
@ -108,6 +109,9 @@ const desktopBindings: {
|
|||
"ctrl-alt-0": "tab.switch-to-last",
|
||||
"ctrl-alt-9": "tab.switch-to-first",
|
||||
"ctrl-q": "app.quit",
|
||||
"ctrl-alt-u": "request.focus-url",
|
||||
"ctrl-alt-]": "tab.mru-switch",
|
||||
"ctrl-alt-[": "tab.mru-switch-reverse",
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -314,6 +318,8 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
|||
// Check if slash, period or enter
|
||||
if (key === "/" || key === "." || key === "enter") return key
|
||||
|
||||
if (key === "[" || key === "]") return key
|
||||
|
||||
// If no other cases match, this is not a valid key
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,6 +174,11 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
|||
|
||||
// Desktop-only shortcuts
|
||||
const desktopShortcuts: ShortcutDef[] = [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "U"],
|
||||
label: t("shortcut.request.focus_url"),
|
||||
section: t("shortcut.request.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "T"],
|
||||
label: t("shortcut.tabs.new_tab"),
|
||||
|
|
@ -204,6 +209,16 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
|||
label: t("shortcut.tabs.last_tab"),
|
||||
section: t("shortcut.tabs.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "]"],
|
||||
label: t("shortcut.tabs.mru_switch"),
|
||||
section: t("shortcut.tabs.title"),
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "["],
|
||||
label: t("shortcut.tabs.mru_switch_reverse"),
|
||||
section: t("shortcut.tabs.title"),
|
||||
},
|
||||
]
|
||||
|
||||
// Return base shortcuts + platform-specific shortcuts
|
||||
|
|
|
|||
|
|
@ -270,4 +270,12 @@ defineActionHandler("tab.switch-to-last", () => {
|
|||
defineActionHandler("tab.reopen-closed", () => {
|
||||
tabs.reopenClosedTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.mru-switch", () => {
|
||||
tabs.goToMRUTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.mru-switch-reverse", () => {
|
||||
tabs.goToPreviousMRUTab()
|
||||
})
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -448,6 +448,14 @@ defineActionHandler("tab.reopen-closed", () => {
|
|||
tabs.reopenClosedTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.mru-switch", () => {
|
||||
tabs.goToMRUTab()
|
||||
})
|
||||
|
||||
defineActionHandler("tab.mru-switch-reverse", () => {
|
||||
tabs.goToPreviousMRUTab()
|
||||
})
|
||||
|
||||
useService(RequestInspectorService)
|
||||
useService(EnvironmentInspectorService)
|
||||
useService(ResponseInspectorService)
|
||||
|
|
|
|||
|
|
@ -146,6 +146,31 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||
this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
tab_mru_switch: {
|
||||
text: [this.t("spotlight.tab.title"), this.t("spotlight.tab.mru_switch")],
|
||||
alternates: ["tab", "recent", "mru", "switch"],
|
||||
icon: markRaw(IconArrowRight),
|
||||
excludeFromSearch: computed(
|
||||
() =>
|
||||
!this.showAction.value ||
|
||||
!this.isDesktopMode.value ||
|
||||
this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
tab_mru_switch_reverse: {
|
||||
text: [
|
||||
this.t("spotlight.tab.title"),
|
||||
this.t("spotlight.tab.mru_switch_reverse"),
|
||||
],
|
||||
alternates: ["tab", "recent", "mru", "previous", "switch"],
|
||||
icon: markRaw(IconArrowLeft),
|
||||
excludeFromSearch: computed(
|
||||
() =>
|
||||
!this.showAction.value ||
|
||||
!this.isDesktopMode.value ||
|
||||
this.isOnlyTab.value
|
||||
),
|
||||
},
|
||||
})
|
||||
|
||||
// TODO: Constructors are no longer recommended as of dioc > 3, use onServiceInit instead
|
||||
|
|
@ -184,5 +209,7 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
|||
if (id === "tab_next") invokeAction("tab.next")
|
||||
if (id === "tab_switch_to_first") invokeAction("tab.switch-to-first")
|
||||
if (id === "tab_switch_to_last") invokeAction("tab.switch-to-last")
|
||||
if (id === "tab_mru_switch") invokeAction("tab.mru-switch")
|
||||
if (id === "tab_mru_switch_reverse") invokeAction("tab.mru-switch-reverse")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,14 @@ class MockTabService extends TabService<{ request: string }> {
|
|||
|
||||
this.watchCurrentTabID()
|
||||
}
|
||||
|
||||
public getMRUOrder(): string[] {
|
||||
return [...this.mruOrder]
|
||||
}
|
||||
|
||||
public getMRUNavigationIndex(): number {
|
||||
return this.mruNavigationIndex
|
||||
}
|
||||
}
|
||||
|
||||
describe("TabService", () => {
|
||||
|
|
@ -341,4 +349,239 @@ describe("TabService", () => {
|
|||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe("MRU Tab Navigation", () => {
|
||||
it("should track MRU order when switching tabs", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
expect(service.getMRUOrder()[0]).toEqual(tab3.id)
|
||||
|
||||
service.setActiveTab("test")
|
||||
expect(service.getMRUOrder()[0]).toEqual("test")
|
||||
expect(service.getMRUOrder()[1]).toEqual(tab3.id)
|
||||
expect(service.getMRUOrder()[2]).toEqual(tab2.id)
|
||||
|
||||
service.setActiveTab(tab2.id)
|
||||
expect(service.getMRUOrder()[0]).toEqual(tab2.id)
|
||||
expect(service.getMRUOrder()[1]).toEqual("test")
|
||||
expect(service.getMRUOrder()[2]).toEqual(tab3.id)
|
||||
})
|
||||
|
||||
it("should navigate forward through MRU list with goToMRUTab", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
expect(service.getActiveTab()?.id).toEqual(tab3.id)
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(tab2.id)
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual("test")
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(tab3.id)
|
||||
})
|
||||
|
||||
it("should navigate backward through MRU list with goToPreviousMRUTab", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
expect(service.getActiveTab()?.id).toEqual(tab3.id)
|
||||
|
||||
service.goToPreviousMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual("test")
|
||||
|
||||
service.goToPreviousMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(tab2.id)
|
||||
|
||||
service.goToPreviousMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(tab3.id)
|
||||
})
|
||||
|
||||
it("should allow bidirectional MRU navigation", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
service.goToMRUTab() // -> tab2
|
||||
service.goToMRUTab() // -> test
|
||||
expect(service.getActiveTab()?.id).toEqual("test")
|
||||
|
||||
service.goToPreviousMRUTab() // -> tab2
|
||||
expect(service.getActiveTab()?.id).toEqual(tab2.id)
|
||||
|
||||
service.goToMRUTab() // -> test
|
||||
expect(service.getActiveTab()?.id).toEqual("test")
|
||||
})
|
||||
|
||||
it("should handle MRU navigation with single tab", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const originalActiveTab = service.getActiveTab()
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(originalActiveTab?.id)
|
||||
|
||||
service.goToPreviousMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(originalActiveTab?.id)
|
||||
})
|
||||
|
||||
it("should remove closed tabs from MRU order", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
expect(service.getMRUOrder()).toContain(tab2.id)
|
||||
|
||||
service.closeTab(tab2.id)
|
||||
|
||||
expect(service.getMRUOrder()).not.toContain(tab2.id)
|
||||
expect(service.getMRUOrder().length).toEqual(2)
|
||||
})
|
||||
|
||||
it("should reset MRU navigation index when tab is explicitly activated", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getMRUNavigationIndex()).toEqual(1)
|
||||
|
||||
service.setActiveTab("test")
|
||||
|
||||
expect(service.getMRUNavigationIndex()).toEqual(-1)
|
||||
})
|
||||
|
||||
it("should commit MRU navigation and update order", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
const tab3 = service.createNewTab({ request: "third request" })
|
||||
|
||||
service.setActiveTab("test")
|
||||
service.setActiveTab(tab2.id)
|
||||
service.setActiveTab(tab3.id)
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getActiveTab()?.id).toEqual(tab2.id)
|
||||
expect(service.getMRUNavigationIndex()).toEqual(1)
|
||||
|
||||
service.commitMRUNavigation()
|
||||
|
||||
expect(service.getMRUOrder()[0]).toEqual(tab2.id)
|
||||
expect(service.getMRUNavigationIndex()).toEqual(-1)
|
||||
})
|
||||
|
||||
it("should reset MRU navigation without committing", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
service.createNewTab({ request: "second request" })
|
||||
service.createNewTab({ request: "third request" })
|
||||
|
||||
service.goToMRUTab()
|
||||
expect(service.getMRUNavigationIndex()).toBeGreaterThan(-1)
|
||||
|
||||
service.resetMRUNavigation()
|
||||
|
||||
expect(service.getMRUNavigationIndex()).toEqual(-1)
|
||||
})
|
||||
|
||||
it("should not add background tabs to MRU until activated", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const backgroundTab = service.createNewTab(
|
||||
{ request: "background request" },
|
||||
false
|
||||
)
|
||||
|
||||
// (it will be added when first activated)
|
||||
const mruBeforeActivation = service.getMRUOrder()
|
||||
|
||||
expect(mruBeforeActivation).not.toContain(backgroundTab.id)
|
||||
|
||||
expect(mruBeforeActivation[0]).toEqual("test")
|
||||
|
||||
expect(mruBeforeActivation.length).toEqual(1)
|
||||
|
||||
service.setActiveTab(backgroundTab.id)
|
||||
|
||||
expect(service.getMRUOrder()[0]).toEqual(backgroundTab.id)
|
||||
|
||||
expect(service.getMRUOrder().length).toEqual(2)
|
||||
})
|
||||
|
||||
it("should initialize MRU order from persisted state", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const persistedState = {
|
||||
lastActiveTabID: "persisted-tab-2",
|
||||
orderedDocs: [
|
||||
{ tabID: "persisted-tab-1", doc: { request: "request 1" } },
|
||||
{ tabID: "persisted-tab-2", doc: { request: "request 2" } },
|
||||
{ tabID: "persisted-tab-3", doc: { request: "request 3" } },
|
||||
],
|
||||
}
|
||||
|
||||
service.loadTabsFromPersistedState(persistedState)
|
||||
|
||||
expect(service.getMRUOrder().length).toEqual(3)
|
||||
|
||||
expect(service.getMRUOrder()[0]).toEqual("persisted-tab-2")
|
||||
})
|
||||
|
||||
it("should handle closeOtherTabs correctly with MRU", () => {
|
||||
const container = new TestContainer()
|
||||
const service = container.bind(MockTabService)
|
||||
|
||||
const tab2 = service.createNewTab({ request: "second request" })
|
||||
service.createNewTab({ request: "third request" })
|
||||
|
||||
service.closeOtherTabs(tab2.id)
|
||||
|
||||
expect(service.getMRUOrder()).toEqual([tab2.id])
|
||||
expect(service.getMRUNavigationIndex()).toEqual(-1)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -132,6 +132,29 @@ export interface TabService<Doc> {
|
|||
*/
|
||||
reopenClosedTab(): boolean
|
||||
|
||||
/**
|
||||
* Navigates forward through the MRU list (to older tabs).
|
||||
* Each call moves one step forward in the MRU history.
|
||||
*/
|
||||
goToMRUTab(): void
|
||||
|
||||
/**
|
||||
* Navigates backward through the MRU list (to more recent tabs).
|
||||
* Each call moves one step backward in the MRU history.
|
||||
*/
|
||||
goToPreviousMRUTab(): void
|
||||
|
||||
/**
|
||||
* Commits the current MRU navigation selection.
|
||||
* Should be called when the modifier key is released to finalize the tab switch.
|
||||
*/
|
||||
commitMRUNavigation(): void
|
||||
|
||||
/**
|
||||
* Resets MRU navigation state without committing.
|
||||
*/
|
||||
resetMRUNavigation(): void
|
||||
|
||||
/**
|
||||
* Gets a computed reference to a persistable tab state.
|
||||
* @returns A computed reference to a persistable tab state object.
|
||||
|
|
|
|||
|
|
@ -28,6 +28,13 @@ export abstract class TabService<Doc>
|
|||
protected recentlyClosedTabs: Array<{ tab: HoppTab<Doc>; index: number }> = []
|
||||
protected readonly MAX_CLOSED_TABS_HISTORY = 10
|
||||
|
||||
// MRU (Most Recently Used) tracking
|
||||
// mruOrder[0] is the most recently used tab, mruOrder[n-1] is the least recently used
|
||||
protected mruOrder: string[] = ["test"]
|
||||
// Navigation index for cycling through MRU list while modifier key is held
|
||||
// -1 means not currently navigating, 0+ means current position in mruOrder
|
||||
protected mruNavigationIndex: number = -1
|
||||
|
||||
public currentTabID = refWithControl("test", {
|
||||
onBeforeChange: (newTabID) => {
|
||||
if (!newTabID || !this.tabMap.has(newTabID)) {
|
||||
|
|
@ -80,6 +87,8 @@ export abstract class TabService<Doc>
|
|||
if (switchToIt) {
|
||||
this.setActiveTab(id)
|
||||
}
|
||||
// Note: We don't add to mruOrder here if switchToIt is false
|
||||
// The tab will be added to mruOrder when it's first activated via setActiveTab
|
||||
|
||||
return tab
|
||||
}
|
||||
|
|
@ -94,12 +103,27 @@ export abstract class TabService<Doc>
|
|||
|
||||
public setActiveTab(tabID: string): void {
|
||||
this.currentTabID.value = tabID
|
||||
this.updateMRUOrder(tabID)
|
||||
}
|
||||
|
||||
private updateMRUOrder(tabID: string): void {
|
||||
const index = this.mruOrder.indexOf(tabID)
|
||||
if (index > -1) {
|
||||
this.mruOrder.splice(index, 1)
|
||||
}
|
||||
this.mruOrder.unshift(tabID)
|
||||
|
||||
// Reset navigation index when a tab is explicitly activated
|
||||
// This ensures the next MRU navigation starts fresh
|
||||
this.mruNavigationIndex = -1
|
||||
}
|
||||
|
||||
public loadTabsFromPersistedState(data: PersistableTabState<Doc>): void {
|
||||
if (data) {
|
||||
this.tabMap.clear()
|
||||
this.tabOrdering.value = []
|
||||
this.mruOrder = []
|
||||
this.mruNavigationIndex = -1
|
||||
|
||||
for (const doc of data.orderedDocs) {
|
||||
this.tabMap.set(doc.tabID, {
|
||||
|
|
@ -108,6 +132,7 @@ export abstract class TabService<Doc>
|
|||
})
|
||||
|
||||
this.tabOrdering.value.push(doc.tabID)
|
||||
this.mruOrder.push(doc.tabID)
|
||||
}
|
||||
|
||||
this.setActiveTab(data.lastActiveTabID)
|
||||
|
|
@ -175,6 +200,14 @@ export abstract class TabService<Doc>
|
|||
|
||||
this.addToRecentlyClosedTabs(tab, tabIndex)
|
||||
|
||||
const mruIndex = this.mruOrder.indexOf(tabID)
|
||||
if (mruIndex > -1) {
|
||||
this.mruOrder.splice(mruIndex, 1)
|
||||
}
|
||||
|
||||
// Reset navigation index when tabs change
|
||||
this.mruNavigationIndex = -1
|
||||
|
||||
this.tabOrdering.value.splice(tabIndex, 1)
|
||||
|
||||
nextTick(() => {
|
||||
|
|
@ -191,6 +224,8 @@ export abstract class TabService<Doc>
|
|||
}
|
||||
|
||||
this.tabOrdering.value = [tabID]
|
||||
this.mruOrder = [tabID]
|
||||
this.mruNavigationIndex = -1
|
||||
|
||||
this.tabMap.forEach((_, id) => {
|
||||
if (id !== tabID) this.tabMap.delete(id)
|
||||
|
|
@ -249,6 +284,94 @@ export abstract class TabService<Doc>
|
|||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates forward through the MRU list (to older tabs).
|
||||
* Each call moves one step forward in the MRU history.
|
||||
*
|
||||
* Behavior:
|
||||
* - First call: moves from current tab (index 0) to second most recent (index 1)
|
||||
* - Subsequent calls: continue moving forward through history
|
||||
* - Wraps around to the beginning when reaching the end
|
||||
*/
|
||||
public goToMRUTab(): void {
|
||||
// Clean up any stale entries
|
||||
this.mruOrder = this.mruOrder.filter((tabID) => this.tabMap.has(tabID))
|
||||
|
||||
if (this.mruOrder.length <= 1) return
|
||||
|
||||
// If not currently navigating, start from position 0 (current tab)
|
||||
if (this.mruNavigationIndex === -1) {
|
||||
this.mruNavigationIndex = 0
|
||||
}
|
||||
|
||||
// Move forward in MRU list (toward older tabs)
|
||||
this.mruNavigationIndex =
|
||||
(this.mruNavigationIndex + 1) % this.mruOrder.length
|
||||
|
||||
const targetTabID = this.mruOrder[this.mruNavigationIndex]
|
||||
if (targetTabID) {
|
||||
// Use currentTabID directly to avoid resetting navigation state
|
||||
this.currentTabID.value = targetTabID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigates backward through the MRU list (to more recent tabs).
|
||||
* Each call moves one step backward in the MRU history.
|
||||
*
|
||||
* Behavior:
|
||||
* - Moves backward from current position in MRU navigation
|
||||
* - Wraps around to the end when reaching the beginning
|
||||
*/
|
||||
public goToPreviousMRUTab(): void {
|
||||
// Clean up any stale entries
|
||||
this.mruOrder = this.mruOrder.filter((tabID) => this.tabMap.has(tabID))
|
||||
|
||||
if (this.mruOrder.length <= 1) return
|
||||
|
||||
// If not currently navigating, start from position 0 (current tab)
|
||||
if (this.mruNavigationIndex === -1) {
|
||||
this.mruNavigationIndex = 0
|
||||
}
|
||||
|
||||
// Move backward in MRU list (toward more recent tabs)
|
||||
this.mruNavigationIndex =
|
||||
this.mruNavigationIndex === 0
|
||||
? this.mruOrder.length - 1
|
||||
: this.mruNavigationIndex - 1
|
||||
|
||||
const targetTabID = this.mruOrder[this.mruNavigationIndex]
|
||||
if (targetTabID) {
|
||||
// Use currentTabID directly to avoid resetting navigation state
|
||||
this.currentTabID.value = targetTabID
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the current MRU navigation selection.
|
||||
* Should be called when the modifier key is released to finalize the tab switch.
|
||||
* This moves the selected tab to the front of the MRU order.
|
||||
*/
|
||||
public commitMRUNavigation(): void {
|
||||
if (this.mruNavigationIndex > 0) {
|
||||
const selectedTabID = this.mruOrder[this.mruNavigationIndex]
|
||||
if (selectedTabID) {
|
||||
// Move the selected tab to the front of MRU order
|
||||
this.mruOrder.splice(this.mruNavigationIndex, 1)
|
||||
this.mruOrder.unshift(selectedTabID)
|
||||
}
|
||||
}
|
||||
this.mruNavigationIndex = -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Resets MRU navigation state without committing.
|
||||
* Useful if navigation is cancelled.
|
||||
*/
|
||||
public resetMRUNavigation(): void {
|
||||
this.mruNavigationIndex = -1
|
||||
}
|
||||
|
||||
private addToRecentlyClosedTabs(tab: HoppTab<Doc>, index: number): void {
|
||||
this.recentlyClosedTabs.push({ tab, index })
|
||||
|
||||
|
|
|
|||
1
packages/hoppscotch-desktop/.gitignore
vendored
1
packages/hoppscotch-desktop/.gitignore
vendored
|
|
@ -18,6 +18,7 @@ manifest.json
|
|||
src-tauri/hopp_bundle.zip
|
||||
src-tauri/hopp_manifest.json
|
||||
src-tauri/hoppscotch-desktop-data
|
||||
target
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
|
|
|
|||
|
|
@ -368,14 +368,36 @@ async function initApp() {
|
|||
} else if (
|
||||
isCtrlOrCmd &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
e.key.toLowerCase() === "l"
|
||||
e.altKey &&
|
||||
e.code === "KeyU"
|
||||
) {
|
||||
// Ctrl/Cmd + L - Focus Address Bar
|
||||
// Ctrl/Cmd + Alt + U - Focus URL Bar
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
shortcutEvent = "focus-url"
|
||||
shortcutEvent = "ctrl-alt-u"
|
||||
} else if (
|
||||
isCtrlOrCmd &&
|
||||
!e.shiftKey &&
|
||||
e.altKey &&
|
||||
e.code === "BracketRight"
|
||||
) {
|
||||
// Ctrl/Cmd + Alt + ] - MRU Tab Switch
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
shortcutEvent = "ctrl-alt-]"
|
||||
} else if (
|
||||
isCtrlOrCmd &&
|
||||
!e.shiftKey &&
|
||||
e.altKey &&
|
||||
e.code === "BracketLeft"
|
||||
) {
|
||||
// Ctrl/Cmd + Alt + [ - MRU Tab Switch (Reverse)
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
e.stopImmediatePropagation()
|
||||
shortcutEvent = "ctrl-alt-["
|
||||
}
|
||||
|
||||
if (shortcutEvent) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue