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",
|
"send_request": "Send Request",
|
||||||
"share_request": "Share Request",
|
"share_request": "Share Request",
|
||||||
"show_code": "Generate code snippet",
|
"show_code": "Generate code snippet",
|
||||||
"title": "Request"
|
"title": "Request",
|
||||||
|
"focus_url": "Focus URL bar"
|
||||||
},
|
},
|
||||||
"response": {
|
"response": {
|
||||||
"copy": "Copy response to clipboard",
|
"copy": "Copy response to clipboard",
|
||||||
|
|
@ -1427,7 +1428,9 @@
|
||||||
"next_tab": "Next Tab",
|
"next_tab": "Next Tab",
|
||||||
"previous_tab": "Previous Tab",
|
"previous_tab": "Previous Tab",
|
||||||
"first_tab": "Switch to First 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": {
|
"theme": {
|
||||||
"black": "Switch theme to Black Mode",
|
"black": "Switch theme to Black Mode",
|
||||||
|
|
@ -1524,6 +1527,8 @@
|
||||||
"previous": "Switch to previous tab",
|
"previous": "Switch to previous tab",
|
||||||
"switch_to_first": "Switch to first tab",
|
"switch_to_first": "Switch to first tab",
|
||||||
"switch_to_last": "Switch to last tab",
|
"switch_to_last": "Switch to last tab",
|
||||||
|
"mru_switch": "Switch to recent tab",
|
||||||
|
"mru_switch_reverse": "Switch to previous recent tab",
|
||||||
"title": "Tabs"
|
"title": "Tabs"
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
class="flex flex-1 whitespace-nowrap rounded-r border-l border-divider bg-primaryLight transition"
|
||||||
>
|
>
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
|
ref="urlInput"
|
||||||
v-model="tab.document.request.endpoint"
|
v-model="tab.document.request.endpoint"
|
||||||
:placeholder="`${t('request.url_placeholder')}`"
|
:placeholder="`${t('request.url_placeholder')}`"
|
||||||
:auto-complete-source="userHistories"
|
:auto-complete-source="userHistories"
|
||||||
|
|
@ -322,6 +323,7 @@ const show = ref<any | null>(null)
|
||||||
const clearAll = ref<any | null>(null)
|
const clearAll = ref<any | null>(null)
|
||||||
const copyRequestAction = ref<any | null>(null)
|
const copyRequestAction = ref<any | null>(null)
|
||||||
const saveRequestAction = ref<any | null>(null)
|
const saveRequestAction = ref<any | null>(null)
|
||||||
|
const urlInput = ref<{ focus: () => void } | null>(null)
|
||||||
|
|
||||||
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
|
const history = useReadonlyStream<RESTHistoryEntry[]>(restHistory$, [])
|
||||||
|
|
||||||
|
|
@ -667,6 +669,10 @@ defineActionHandler("request.show-code", () => {
|
||||||
showCodegenModal.value = true
|
showCodegenModal.value = true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineActionHandler("request.focus-url", () => {
|
||||||
|
urlInput.value?.focus()
|
||||||
|
})
|
||||||
|
|
||||||
const isCustomMethod = computed(() => {
|
const isCustomMethod = computed(() => {
|
||||||
return (
|
return (
|
||||||
tab.value.document.request.method === "CUSTOM" ||
|
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(() => {
|
onMounted(() => {
|
||||||
if (editor.value) {
|
if (editor.value) {
|
||||||
if (!view.value) initView(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-first" // Switch to first tab
|
||||||
| "tab.switch-to-last" // Switch to last tab
|
| "tab.switch-to-last" // Switch to last tab
|
||||||
| "tab.reopen-closed" // Reopen recently closed 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
|
| "collection.new" // Create root collection
|
||||||
| "flyouts.chat.open" // Shows the keybinds flyout
|
| "flyouts.chat.open" // Shows the keybinds flyout
|
||||||
| "flyouts.keybinds.toggle" // 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"
|
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
||||||
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
||||||
| "right" | "/" | "?" | "." | "enter" | "tab" | "delete" | "backspace"
|
| "right" | "/" | "?" | "." | "enter" | "tab" | "delete" | "backspace"
|
||||||
|
| "[" | "]"
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
||||||
|
|
@ -108,6 +109,9 @@ const desktopBindings: {
|
||||||
"ctrl-alt-0": "tab.switch-to-last",
|
"ctrl-alt-0": "tab.switch-to-last",
|
||||||
"ctrl-alt-9": "tab.switch-to-first",
|
"ctrl-alt-9": "tab.switch-to-first",
|
||||||
"ctrl-q": "app.quit",
|
"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
|
// Check if slash, period or enter
|
||||||
if (key === "/" || key === "." || key === "enter") return key
|
if (key === "/" || key === "." || key === "enter") return key
|
||||||
|
|
||||||
|
if (key === "[" || key === "]") return key
|
||||||
|
|
||||||
// If no other cases match, this is not a valid key
|
// If no other cases match, this is not a valid key
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -174,6 +174,11 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
||||||
|
|
||||||
// Desktop-only shortcuts
|
// Desktop-only shortcuts
|
||||||
const desktopShortcuts: ShortcutDef[] = [
|
const desktopShortcuts: ShortcutDef[] = [
|
||||||
|
{
|
||||||
|
keys: [getPlatformSpecialKey(), getPlatformAlternateKey(), "U"],
|
||||||
|
label: t("shortcut.request.focus_url"),
|
||||||
|
section: t("shortcut.request.title"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
keys: [getPlatformSpecialKey(), "T"],
|
keys: [getPlatformSpecialKey(), "T"],
|
||||||
label: t("shortcut.tabs.new_tab"),
|
label: t("shortcut.tabs.new_tab"),
|
||||||
|
|
@ -204,6 +209,16 @@ export function getShortcuts(t: (x: string) => string): ShortcutDef[] {
|
||||||
label: t("shortcut.tabs.last_tab"),
|
label: t("shortcut.tabs.last_tab"),
|
||||||
section: t("shortcut.tabs.title"),
|
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
|
// Return base shortcuts + platform-specific shortcuts
|
||||||
|
|
|
||||||
|
|
@ -270,4 +270,12 @@ defineActionHandler("tab.switch-to-last", () => {
|
||||||
defineActionHandler("tab.reopen-closed", () => {
|
defineActionHandler("tab.reopen-closed", () => {
|
||||||
tabs.reopenClosedTab()
|
tabs.reopenClosedTab()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineActionHandler("tab.mru-switch", () => {
|
||||||
|
tabs.goToMRUTab()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineActionHandler("tab.mru-switch-reverse", () => {
|
||||||
|
tabs.goToPreviousMRUTab()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -448,6 +448,14 @@ defineActionHandler("tab.reopen-closed", () => {
|
||||||
tabs.reopenClosedTab()
|
tabs.reopenClosedTab()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
defineActionHandler("tab.mru-switch", () => {
|
||||||
|
tabs.goToMRUTab()
|
||||||
|
})
|
||||||
|
|
||||||
|
defineActionHandler("tab.mru-switch-reverse", () => {
|
||||||
|
tabs.goToPreviousMRUTab()
|
||||||
|
})
|
||||||
|
|
||||||
useService(RequestInspectorService)
|
useService(RequestInspectorService)
|
||||||
useService(EnvironmentInspectorService)
|
useService(EnvironmentInspectorService)
|
||||||
useService(ResponseInspectorService)
|
useService(ResponseInspectorService)
|
||||||
|
|
|
||||||
|
|
@ -146,6 +146,31 @@ export class TabSpotlightSearcherService extends StaticSpotlightSearcherService<
|
||||||
this.isOnlyTab.value
|
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
|
// 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_next") invokeAction("tab.next")
|
||||||
if (id === "tab_switch_to_first") invokeAction("tab.switch-to-first")
|
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_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()
|
this.watchCurrentTabID()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getMRUOrder(): string[] {
|
||||||
|
return [...this.mruOrder]
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMRUNavigationIndex(): number {
|
||||||
|
return this.mruNavigationIndex
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("TabService", () => {
|
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
|
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.
|
* Gets a computed reference to a persistable tab state.
|
||||||
* @returns A computed reference to a persistable tab state object.
|
* @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 recentlyClosedTabs: Array<{ tab: HoppTab<Doc>; index: number }> = []
|
||||||
protected readonly MAX_CLOSED_TABS_HISTORY = 10
|
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", {
|
public currentTabID = refWithControl("test", {
|
||||||
onBeforeChange: (newTabID) => {
|
onBeforeChange: (newTabID) => {
|
||||||
if (!newTabID || !this.tabMap.has(newTabID)) {
|
if (!newTabID || !this.tabMap.has(newTabID)) {
|
||||||
|
|
@ -80,6 +87,8 @@ export abstract class TabService<Doc>
|
||||||
if (switchToIt) {
|
if (switchToIt) {
|
||||||
this.setActiveTab(id)
|
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
|
return tab
|
||||||
}
|
}
|
||||||
|
|
@ -94,12 +103,27 @@ export abstract class TabService<Doc>
|
||||||
|
|
||||||
public setActiveTab(tabID: string): void {
|
public setActiveTab(tabID: string): void {
|
||||||
this.currentTabID.value = tabID
|
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 {
|
public loadTabsFromPersistedState(data: PersistableTabState<Doc>): void {
|
||||||
if (data) {
|
if (data) {
|
||||||
this.tabMap.clear()
|
this.tabMap.clear()
|
||||||
this.tabOrdering.value = []
|
this.tabOrdering.value = []
|
||||||
|
this.mruOrder = []
|
||||||
|
this.mruNavigationIndex = -1
|
||||||
|
|
||||||
for (const doc of data.orderedDocs) {
|
for (const doc of data.orderedDocs) {
|
||||||
this.tabMap.set(doc.tabID, {
|
this.tabMap.set(doc.tabID, {
|
||||||
|
|
@ -108,6 +132,7 @@ export abstract class TabService<Doc>
|
||||||
})
|
})
|
||||||
|
|
||||||
this.tabOrdering.value.push(doc.tabID)
|
this.tabOrdering.value.push(doc.tabID)
|
||||||
|
this.mruOrder.push(doc.tabID)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setActiveTab(data.lastActiveTabID)
|
this.setActiveTab(data.lastActiveTabID)
|
||||||
|
|
@ -175,6 +200,14 @@ export abstract class TabService<Doc>
|
||||||
|
|
||||||
this.addToRecentlyClosedTabs(tab, tabIndex)
|
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)
|
this.tabOrdering.value.splice(tabIndex, 1)
|
||||||
|
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
|
@ -191,6 +224,8 @@ export abstract class TabService<Doc>
|
||||||
}
|
}
|
||||||
|
|
||||||
this.tabOrdering.value = [tabID]
|
this.tabOrdering.value = [tabID]
|
||||||
|
this.mruOrder = [tabID]
|
||||||
|
this.mruNavigationIndex = -1
|
||||||
|
|
||||||
this.tabMap.forEach((_, id) => {
|
this.tabMap.forEach((_, id) => {
|
||||||
if (id !== tabID) this.tabMap.delete(id)
|
if (id !== tabID) this.tabMap.delete(id)
|
||||||
|
|
@ -249,6 +284,94 @@ export abstract class TabService<Doc>
|
||||||
return true
|
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 {
|
private addToRecentlyClosedTabs(tab: HoppTab<Doc>, index: number): void {
|
||||||
this.recentlyClosedTabs.push({ tab, index })
|
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_bundle.zip
|
||||||
src-tauri/hopp_manifest.json
|
src-tauri/hopp_manifest.json
|
||||||
src-tauri/hoppscotch-desktop-data
|
src-tauri/hoppscotch-desktop-data
|
||||||
|
target
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|
|
||||||
|
|
@ -368,14 +368,36 @@ async function initApp() {
|
||||||
} else if (
|
} else if (
|
||||||
isCtrlOrCmd &&
|
isCtrlOrCmd &&
|
||||||
!e.shiftKey &&
|
!e.shiftKey &&
|
||||||
!e.altKey &&
|
e.altKey &&
|
||||||
e.key.toLowerCase() === "l"
|
e.code === "KeyU"
|
||||||
) {
|
) {
|
||||||
// Ctrl/Cmd + L - Focus Address Bar
|
// Ctrl/Cmd + Alt + U - Focus URL Bar
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.stopImmediatePropagation()
|
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) {
|
if (shortcutEvent) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue