From db88b27da5a5bab8af7095aecca94006e93744c1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 11:47:32 +0000 Subject: [PATCH 1/4] Initial plan From 1761a82ff74dd92742649e1ad864baa2bd2ae701 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 11:55:46 +0000 Subject: [PATCH 2/4] fix: add non-conflicting recent-tab keyboard shortcut for yasgui tabs Agent-Logs-Url: https://github.com/Matdata-eu/Yasgui/sessions/126e2073-a921-4c2a-bf72-f750a377b737 --- docs/user-guide.md | 7 +++++ packages/yasgui/src/Tab.ts | 7 +++++ packages/yasgui/src/TabSettingsModal.ts | 13 ++++++++ packages/yasgui/src/index.ts | 25 ++++++++++++++++ packages/yasgui/src/tabNavigationHistory.ts | 18 +++++++++++ .../yasgui-tab-navigation-history-test.ts | 30 +++++++++++++++++++ 6 files changed, 100 insertions(+) create mode 100644 packages/yasgui/src/tabNavigationHistory.ts create mode 100644 test/unit/yasgui-tab-navigation-history-test.ts diff --git a/docs/user-guide.md b/docs/user-guide.md index 93c91ae5..8befbbab 100644 --- a/docs/user-guide.md +++ b/docs/user-guide.md @@ -1646,6 +1646,13 @@ Master YASGUI with these keyboard shortcuts for faster querying. | `F9` | Switch between YASQE and YASR fullscreen | | `Esc` | Exit fullscreen mode | +### YASGUI Tabs + +| Shortcut | Action | +| ---------------------------------------- | --------------------------------------------- | +| `Ctrl+Alt+Tab` / `Cmd+Alt+Tab` | Switch to previously used YASGUI tab | +| `Ctrl+Alt+Shift+Tab` / `Cmd+Alt+Shift+Tab` | Switch to next tab in recently used order | + ### General Editor | Shortcut | Action | diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index 53f210a7..e42898fa 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -436,6 +436,13 @@ export class Tab extends EventEmitter { private handleKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; + const isTabSwitchShortcut = (event.ctrlKey || event.metaKey) && event.altKey && event.key === "Tab"; + if (isTabSwitchShortcut) { + event.preventDefault(); + this.yasgui.selectRecentlyUsedTab(event.shiftKey ? "forward" : "backward"); + return; + } + const saveModalOpen = !!document.querySelector(".saveManagedQueryModalOverlay.open"); if (!saveModalOpen) { const isSaveShortcut = diff --git a/packages/yasgui/src/TabSettingsModal.ts b/packages/yasgui/src/TabSettingsModal.ts index 030ce7ee..8ccbb0c6 100644 --- a/packages/yasgui/src/TabSettingsModal.ts +++ b/packages/yasgui/src/TabSettingsModal.ts @@ -2058,6 +2058,19 @@ export default class TabSettingsModal { { keys: ["Esc"], description: "Exit fullscreen mode" }, ], }, + { + category: "YASGUI Tabs", + shortcuts: [ + { + keys: ["Ctrl+Alt+Tab", "Cmd+Alt+Tab"], + description: "Switch to previously used YASGUI tab", + }, + { + keys: ["Ctrl+Alt+Shift+Tab", "Cmd+Alt+Shift+Tab"], + description: "Switch to next YASGUI tab in recently used order", + }, + ], + }, ]; shortcutsData.forEach((section) => { diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index 8d756918..a1f69cd8 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -13,6 +13,7 @@ import { addClass, removeClass } from "@matdata/yasgui-utils"; import GeoPlugin from "yasgui-geo-tg"; import GraphPlugin from "@matdata/yasgui-graph-plugin"; import TablePlugin from "@matdata/yasgui-table-plugin"; +import { getRecentlyUsedTabId, moveTabIdToFront, removeTabId } from "./tabNavigationHistory"; import "@matdata/yasgui-graph-plugin/dist/yasgui-graph-plugin.min.css"; import "@matdata/yasgui-table-plugin/dist/yasgui-table-plugin.min.css"; import { ThemeManager, Theme } from "./ThemeManager"; @@ -155,6 +156,7 @@ export class Yasgui extends EventEmitter { public persistentConfig: PersistentConfig; public themeManager: ThemeManager; public queryBrowser: QueryBrowser; + private recentTabIds: string[] = []; public static Tab = Tab; constructor(parent: HTMLElement, config: PartialConfig) { super(); @@ -185,6 +187,9 @@ export class Yasgui extends EventEmitter { this.tabPanelsEl = document.createElement("div"); this.queryBrowser = new QueryBrowser(this); + this.on("tabClose", (_yasgui, tab) => { + this.removeTabFromRecentHistory(tab.getId()); + }); this.rootEl.appendChild(this.tabElements.drawTabsList()); this.rootEl.appendChild(this.tabPanelsEl); @@ -227,6 +232,7 @@ export class Yasgui extends EventEmitter { const newTab = this.addTab(true); this.persistentConfig.setActive(newTab.getId()); this.emit("tabChange", this, newTab); + this.recordTabInRecentHistory(newTab.getId()); } else { for (const tabId of tabs) { this._tabs[tabId] = new Tab(this, this.persistentConfig.getTab(tabId)); @@ -237,6 +243,7 @@ export class Yasgui extends EventEmitter { const activeTabId = this.persistentConfig.getActiveId(); if (activeTabId) { this.markTabSelected(activeTabId); + this.recordTabInRecentHistory(activeTabId); if (executeIdAfterInit && executeIdAfterInit === activeTabId) { (this.getTab(activeTabId) as Tab).query().catch(() => {}); } @@ -300,6 +307,7 @@ export class Yasgui extends EventEmitter { const tab = this.getTab(); if (tab && tab.getId() !== tabId) { if (this.markTabSelected(tabId)) { + this.recordTabInRecentHistory(tabId); //emit this.emit("tabSelect", this, tabId); this.persistentConfig.setActive(tabId); @@ -307,6 +315,22 @@ export class Yasgui extends EventEmitter { } return tab; } + private recordTabInRecentHistory(tabId: string) { + this.recentTabIds = moveTabIdToFront(this.recentTabIds, tabId); + } + private removeTabFromRecentHistory(tabId: string) { + this.recentTabIds = removeTabId(this.recentTabIds, tabId); + } + public selectRecentlyUsedTab(direction: "backward" | "forward" = "backward") { + const activeTab = this.getTab(); + if (!activeTab) return; + const activeTabId = activeTab.getId(); + this.recordTabInRecentHistory(activeTabId); + const existingRecentTabIds = this.recentTabIds.filter((id) => !!this._tabs[id]); + const nextTabId = getRecentlyUsedTabId(existingRecentTabIds, activeTabId, direction); + if (!nextTabId) return activeTab; + return this.selectTabId(nextTabId); + } /** * Checks if two persistent tab configuration are the same based. * It isnt a strict equality, as falsy values (e.g. a header that isnt set in one tabjson) isnt taken into consideration @@ -417,6 +441,7 @@ export class Yasgui extends EventEmitter { if (setActive) { this.persistentConfig.setActive(tabId); this._tabs[tabId].show(); + this.recordTabInRecentHistory(tabId); } return this._tabs[tabId]; } diff --git a/packages/yasgui/src/tabNavigationHistory.ts b/packages/yasgui/src/tabNavigationHistory.ts new file mode 100644 index 00000000..8873ce5c --- /dev/null +++ b/packages/yasgui/src/tabNavigationHistory.ts @@ -0,0 +1,18 @@ +export const moveTabIdToFront = (tabIds: string[], tabId: string): string[] => { + const idsWithoutTab = tabIds.filter((id) => id !== tabId); + return [tabId, ...idsWithoutTab]; +}; + +export const removeTabId = (tabIds: string[], tabId: string): string[] => tabIds.filter((id) => id !== tabId); + +export const getRecentlyUsedTabId = ( + tabIds: string[], + activeTabId: string, + direction: "backward" | "forward" = "backward", +): string | undefined => { + const uniqueTabIds = tabIds.filter((id, index) => tabIds.indexOf(id) === index); + const recentlyUsedIds = moveTabIdToFront(uniqueTabIds, activeTabId); + const candidateTabIds = recentlyUsedIds.filter((id) => id !== activeTabId); + if (!candidateTabIds.length) return undefined; + return direction === "backward" ? candidateTabIds[0] : candidateTabIds[candidateTabIds.length - 1]; +}; diff --git a/test/unit/yasgui-tab-navigation-history-test.ts b/test/unit/yasgui-tab-navigation-history-test.ts new file mode 100644 index 00000000..ffb88c67 --- /dev/null +++ b/test/unit/yasgui-tab-navigation-history-test.ts @@ -0,0 +1,30 @@ +import * as chai from "chai"; +import { describe, it } from "mocha"; + +import { getRecentlyUsedTabId, moveTabIdToFront, removeTabId } from "../../packages/yasgui/src/tabNavigationHistory.js"; + +const expect = chai.expect; + +describe("Yasgui tab navigation history", () => { + it("moves selected tab to the front of recent history", () => { + expect(moveTabIdToFront(["tab-1", "tab-2", "tab-3"], "tab-2")).to.deep.equal(["tab-2", "tab-1", "tab-3"]); + }); + + it("returns previously used tab when navigating backward", () => { + const recentTabIds = ["tab-1", "tab-2", "tab-3"]; + expect(getRecentlyUsedTabId(recentTabIds, "tab-1", "backward")).to.equal("tab-2"); + }); + + it("returns oldest tab when navigating forward through recent history", () => { + const recentTabIds = ["tab-1", "tab-2", "tab-3"]; + expect(getRecentlyUsedTabId(recentTabIds, "tab-1", "forward")).to.equal("tab-3"); + }); + + it("returns undefined when there is no other tab in history", () => { + expect(getRecentlyUsedTabId(["tab-1"], "tab-1", "backward")).to.equal(undefined); + }); + + it("removes closed tabs from history", () => { + expect(removeTabId(["tab-1", "tab-2", "tab-3"], "tab-2")).to.deep.equal(["tab-1", "tab-3"]); + }); +}); From beeccd4ce0ec7d3eb231a794a4e41dbcdfa667b4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 11 May 2026 12:05:19 +0000 Subject: [PATCH 3/4] refactor: finalize tab history helper behavior Agent-Logs-Url: https://github.com/Matdata-eu/Yasgui/sessions/126e2073-a921-4c2a-bf72-f750a377b737 --- packages/yasgui/src/index.ts | 10 ++++------ packages/yasgui/src/tabNavigationHistory.ts | 3 +-- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index a1f69cd8..4ad5a986 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -321,15 +321,13 @@ export class Yasgui extends EventEmitter { private removeTabFromRecentHistory(tabId: string) { this.recentTabIds = removeTabId(this.recentTabIds, tabId); } - public selectRecentlyUsedTab(direction: "backward" | "forward" = "backward") { + public selectRecentlyUsedTab(direction: "backward" | "forward" = "backward"): void { const activeTab = this.getTab(); if (!activeTab) return; const activeTabId = activeTab.getId(); - this.recordTabInRecentHistory(activeTabId); - const existingRecentTabIds = this.recentTabIds.filter((id) => !!this._tabs[id]); - const nextTabId = getRecentlyUsedTabId(existingRecentTabIds, activeTabId, direction); - if (!nextTabId) return activeTab; - return this.selectTabId(nextTabId); + const nextTabId = getRecentlyUsedTabId(this.recentTabIds, activeTabId, direction); + if (!nextTabId) return; + this.selectTabId(nextTabId); } /** * Checks if two persistent tab configuration are the same based. diff --git a/packages/yasgui/src/tabNavigationHistory.ts b/packages/yasgui/src/tabNavigationHistory.ts index 8873ce5c..038b22ea 100644 --- a/packages/yasgui/src/tabNavigationHistory.ts +++ b/packages/yasgui/src/tabNavigationHistory.ts @@ -10,8 +10,7 @@ export const getRecentlyUsedTabId = ( activeTabId: string, direction: "backward" | "forward" = "backward", ): string | undefined => { - const uniqueTabIds = tabIds.filter((id, index) => tabIds.indexOf(id) === index); - const recentlyUsedIds = moveTabIdToFront(uniqueTabIds, activeTabId); + const recentlyUsedIds = moveTabIdToFront(tabIds, activeTabId); const candidateTabIds = recentlyUsedIds.filter((id) => id !== activeTabId); if (!candidateTabIds.length) return undefined; return direction === "backward" ? candidateTabIds[0] : candidateTabIds[candidateTabIds.length - 1]; From 4d18a96653aea92072e0b019c07083c4f40c8dc2 Mon Sep 17 00:00:00 2001 From: Mathias Vanden Auweele Date: Mon, 11 May 2026 15:46:00 +0200 Subject: [PATCH 4/4] fix --- packages/yasgui/src/Tab.ts | 6 ++++-- packages/yasgui/src/index.ts | 35 +++++++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/yasgui/src/Tab.ts b/packages/yasgui/src/Tab.ts index e42898fa..b174d13d 100644 --- a/packages/yasgui/src/Tab.ts +++ b/packages/yasgui/src/Tab.ts @@ -436,10 +436,12 @@ export class Tab extends EventEmitter { private handleKeyDown = (event: KeyboardEvent) => { if (event.defaultPrevented) return; - const isTabSwitchShortcut = (event.ctrlKey || event.metaKey) && event.altKey && event.key === "Tab"; + // Ctrl+, → backward (less recently used), Ctrl+Alt+, → forward (more recently used) + // Ctrl+Tab / Ctrl+Shift+Tab cannot be used: the browser intercepts them before keydown fires + const isTabSwitchShortcut = (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key === ","; if (isTabSwitchShortcut) { event.preventDefault(); - this.yasgui.selectRecentlyUsedTab(event.shiftKey ? "forward" : "backward"); + this.yasgui.selectRecentlyUsedTab(event.altKey ? "forward" : "backward"); return; } diff --git a/packages/yasgui/src/index.ts b/packages/yasgui/src/index.ts index 4ad5a986..8238960e 100644 --- a/packages/yasgui/src/index.ts +++ b/packages/yasgui/src/index.ts @@ -13,7 +13,7 @@ import { addClass, removeClass } from "@matdata/yasgui-utils"; import GeoPlugin from "yasgui-geo-tg"; import GraphPlugin from "@matdata/yasgui-graph-plugin"; import TablePlugin from "@matdata/yasgui-table-plugin"; -import { getRecentlyUsedTabId, moveTabIdToFront, removeTabId } from "./tabNavigationHistory"; +import { moveTabIdToFront, removeTabId } from "./tabNavigationHistory"; import "@matdata/yasgui-graph-plugin/dist/yasgui-graph-plugin.min.css"; import "@matdata/yasgui-table-plugin/dist/yasgui-table-plugin.min.css"; import { ThemeManager, Theme } from "./ThemeManager"; @@ -157,6 +157,8 @@ export class Yasgui extends EventEmitter { public themeManager: ThemeManager; public queryBrowser: QueryBrowser; private recentTabIds: string[] = []; + private navigationSnapshot: string[] | null = null; + private navigationCursor = 0; public static Tab = Tab; constructor(parent: HTMLElement, config: PartialConfig) { super(); @@ -316,6 +318,8 @@ export class Yasgui extends EventEmitter { return tab; } private recordTabInRecentHistory(tabId: string) { + // Any non-navigation selection commits the navigation and ends navigation mode. + this.navigationSnapshot = null; this.recentTabIds = moveTabIdToFront(this.recentTabIds, tabId); } private removeTabFromRecentHistory(tabId: string) { @@ -325,9 +329,32 @@ export class Yasgui extends EventEmitter { const activeTab = this.getTab(); if (!activeTab) return; const activeTabId = activeTab.getId(); - const nextTabId = getRecentlyUsedTabId(this.recentTabIds, activeTabId, direction); - if (!nextTabId) return; - this.selectTabId(nextTabId); + + // Initialize (or re-initialize) the navigation snapshot when starting a new navigation + // sequence or when the active tab no longer matches the cursor position. + // Snapshotting the history order lets repeated presses cycle through all tabs + // instead of ping-ponging between the two most-recently-used ones. + if (this.navigationSnapshot === null || this.navigationSnapshot[this.navigationCursor] !== activeTabId) { + this.navigationSnapshot = [...this.recentTabIds]; + const activeIdx = this.navigationSnapshot.indexOf(activeTabId); + this.navigationCursor = activeIdx >= 0 ? activeIdx : 0; + } + + if (direction === "backward") { + this.navigationCursor = Math.min(this.navigationCursor + 1, this.navigationSnapshot.length - 1); + } else { + this.navigationCursor = Math.max(this.navigationCursor - 1, 0); + } + + const nextTabId = this.navigationSnapshot[this.navigationCursor]; + if (!nextTabId || nextTabId === activeTabId) return; + + // Select the tab without updating recentTabIds. The history is committed when the + // user performs any non-navigation action that calls recordTabInRecentHistory. + if (this.markTabSelected(nextTabId)) { + this.emit("tabSelect", this, nextTabId); + this.persistentConfig.setActive(nextTabId); + } } /** * Checks if two persistent tab configuration are the same based.