feat(editor): Chrome-like grouped tabs#315366
Open
mostafa wants to merge 27 commits intomicrosoft:mainfrom
Open
feat(editor): Chrome-like grouped tabs#315366mostafa wants to merge 27 commits intomicrosoft:mainfrom
mostafa wants to merge 27 commits intomicrosoft:mainfrom
Conversation
Introduce Chrome-style tab grouping at the model layer: - Define IEditorTabGroup interface (id, name, color, collapsed, startIndex, count) - Extend EditorGroupModel with tabGroups field and CRUD operations (create, add, remove, dissolve, collapse, expand, rename, recolor) - Add GroupModelChangeKind.TAB_GROUP_* event variants - Extend IReadonlyEditorGroupModel with tabGroups accessor - Add IEditorPartTabGroupsConfiguration for settings - Implement index maintenance in splice() for insert/remove - Serialize/deserialize tab groups in ISerializedEditorGroupModel Tab groups enforce contiguity and only apply to non-sticky tabs, mirroring Chrome's behavior where pinned tabs cannot be grouped. Refs: microsoft#100335
Add the visual layer for tab groups in the multi-tab editor control: - Create tabgroups.css with badge styles (8 color variants), colored bottom borders for grouped tabs, and collapse visibility - Render group badge on the first tab of each group (click to collapse/expand, drop to add editors to the group) - Apply in-tab-group and tab-group-collapsed CSS classes per tab - Register TAB_GROUP_HEADER_BACKGROUND/FOREGROUND theme tokens - Badge approach preserves DOM index alignment (no extra children in tabsContainer) so getTabAtIndex() remains correct Refs: microsoft#100335
Wire up the interaction and configuration layers: - Register 7 commands: addToNewTabGroup, removeFromTabGroup, dissolveTabGroup, collapseTabGroup, expandTabGroup, renameTabGroup, recolorTabGroup - Add tab group entries to EditorTitleContext menu (group '4_tabgroup') with sticky-tab precondition guards - Add tabGroups default to DEFAULT_EDITOR_PART_OPTIONS with verifier - Register workbench.editor.tabGroups.enabled and workbench.editor.tabGroups.collapseOnSwitch settings Refs: microsoft#100335
Refactor from inline badge (inside first tab) to a standalone .tab-group-header element inserted directly into the tabs container before each group's first tab. This matches Chrome's grouped-tabs UX where the group chip is a visually separate element in the tab bar. Key changes: - getTabAtIndex, doWithTab now skip non-.tab children when resolving model indices to DOM elements - handleOpenedEditors/handleClosedEditors count only .tab children - computeDropTarget skips headers when finding sibling tabs - Wrap-layout iteration skips headers - CSS rewritten for standalone header styling with color variants - FilteredEditorGroupModel implements tabGroups/getTabGroupForEditor - Tab group mutation methods exposed as optional on the readonly interface
Two bugs fixed: 1. Collapse/expand: FilteredEditorGroupModel (used by multi-row tab controls) did not expose collapseTabGroup/expandTabGroup/addToTabGroup, so optional-chaining calls resolved to undefined. Added concrete delegating methods. 2. Rename/recolor not updating UI: MultiEditorTabsControl had no listener for TAB_GROUP_* model change events, so visual updates only happened on the next unrelated redraw. Added onDidModelChange subscription that calls redrawTabGroups() for all tab-group event kinds.
Right-clicking the standalone tab group header now shows a dedicated context menu with: Expand/Collapse, Rename, Change Color, and Ungroup. Actions operate directly on the target group by ID rather than relying on the active editor's group membership, so they work correctly even when the group is collapsed. Also exposed renameTabGroup, recolorTabGroup, and dissolveTabGroup as optional methods on IReadonlyEditorGroupModel and delegated them through FilteredEditorGroupModel.
Tab group headers are now draggable. Dropping a group header onto another group header moves the entire group (all its tabs) to that position. Dropping onto the empty area of the tab bar moves the group to the end. Implementation: - Added moveTabGroup(groupId, toIndex) to EditorGroupModel that bulk-relocates editors and recalculates all group startIndex values - Made .tab-group-header elements draggable with dragstart/dragend - dragover/drop on group headers detect group-reorder vs editor-add - Container-level drop handler supports moving group to end - Model change listener now calls full redraw() (not just redrawTabGroups) so tab labels update after editor reordering
moveEditor() was directly splicing the editors array without updating tab group startIndex/count, causing dropped tabs to appear outside their target group until a manual redraw. Add adjustTabGroupsForMove() to maintain group boundaries on every move, fire TAB_GROUP_CHANGED so the UI redraws immediately, and call redrawTabGroups() at the end of the control's moveEditor to prevent stale group styling. Also adds includeInTabGroup() for position-preserving membership, a "Move to Tab Group..." context menu command, removes the collapsed arrow icon, and replaces unsafe `as any` casts with a typed helper.
The previous moveTabGroup used fragile relative index arithmetic that miscalculated other groups' startIndex after a splice, corrupting tab order. Replace with a snapshot-based approach: record each group's first editor before mutation, splice the array, then rebuild all startIndex values via indexOf. Also fix the "move to end" target index to account for sticky tabs, and allow dropping a group header onto any tab position (not just other headers) for more flexible reordering.
When dropping a tab at the start or end of a tab group, the adjustTabGroupsForInsert logic would push the group rather than expand it. Add a post-move boundary check in onDrop that calls includeInTabGroup when the editor lands adjacent to a group.
- Context menu "Remove from Tab Group" now works on the right-clicked tab (using resolveCommandsContext) and supports multi-selection. - "Move to Tab Group..." also uses resolved context for right-click. - Dragging a tab out of a group no longer re-absorbs it at the boundary; the post-move absorption only triggers when the drop target was originally within a group.
The "Remove from Tab Group" context menu action now removes all selected editors in addition to the right-clicked one.
Previously, removing an editor from the middle of a group just decremented count, causing the last editor to silently fall out of the group. Now the removed editor is spliced to the end of the group range before shrinking, maintaining contiguity for all remaining members.
New tab groups now get a random color from the preset palette instead of always defaulting to blue. The "Change Color" picker in both the context menu and command palette now offers a "Custom (#hex)..." option that accepts arbitrary #hex or rgb() values. Custom colors are applied via inline styles on the header and the tab border CSS variable.
Use color-mix(in srgb) for the background opacity instead of appending hex alpha digits, which only worked for 6-digit hex and broke for rgb() or short hex values.
Custom colors now mix 30% white into the text color via color-mix(), ensuring readability on dark backgrounds. The header uses display: inline-block with text-overflow: ellipsis so long group names are properly truncated at 150px instead of overflowing.
Closes all editors belonging to the group after dissolving it. Complements the existing "Ungroup Tabs" which only removes the grouping without closing editors.
When a tab group is collapsed, the currently active editor's tab remains visible until the user clicks another tab outside the group. This matches Chrome's behavior where the focused tab stays accessible even after collapsing its group.
- Fix createTabGroup index re-derivation bug: after sorting editor indices, the re-derivation loop now correctly uses the sorted editor-index pairs instead of the original unsorted editors array. - Remove redundant TAB_GROUP_CHANGED event from moveEditor to avoid double-redraw (the control's moveEditor already calls redrawTabGroups). - Remove unused TAB_GROUP_HEADER_BACKGROUND/FOREGROUND theme tokens that were registered but never referenced. - Remove unused collapseOnSwitch setting that was declared but never implemented.
Author
|
@microsoft-github-policy-service agree company="Tiger Data" |
Contributor
There was a problem hiding this comment.
Pull request overview
This PR adds Chrome-style tab grouping to the VS Code workbench editor tab strip by extending the editor group model with persisted tab-group metadata, rendering group headers in the multi-editor tabs UI, and wiring up commands/menus and configuration.
Changes:
- Extend
EditorGroupModelwith tab-group data structures, mutation APIs, change events, and (de)serialization. - Render tab-group headers and per-tab group styling in
MultiEditorTabsControl, including drag-and-drop interactions and header context menus. - Register new tab-group commands and Editor Title context menu items, plus a new
workbench.editor.tabGroups.enabledsetting and editor option plumbing.
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 11 comments.
Show a summary per file
| File | Description |
|---|---|
| src/vs/workbench/common/theme.ts | Minor whitespace change near tab-related colors. |
| src/vs/workbench/common/editor/filteredEditorGroupModel.ts | Adds delegation for tab-group APIs on filtered models (sticky/unsticky). |
| src/vs/workbench/common/editor/editorGroupModel.ts | Implements tab-group model, events, and persistence within editor groups. |
| src/vs/workbench/common/editor.ts | Adds new GroupModelChangeKind values and editor options surface for tab groups. |
| src/vs/workbench/browser/workbench.contribution.ts | Registers workbench.editor.tabGroups.enabled user setting. |
| src/vs/workbench/browser/parts/editor/multiEditorTabsControl.ts | Renders group headers, applies group styling, and adds DnD/context menu behavior. |
| src/vs/workbench/browser/parts/editor/media/tabgroups.css | New styling for group headers and collapsed-group tab hiding. |
| src/vs/workbench/browser/parts/editor/editorCommands.ts | Adds command registrations for creating/managing tab groups. |
| src/vs/workbench/browser/parts/editor/editor.ts | Adds default options + validation for the new tabGroups editor part option. |
| src/vs/workbench/browser/parts/editor/editor.contribution.ts | Adds Editor Title context menu items for tab-group operations. |
| build/lib/stylelint/vscode-known-variables.json | Adds --tab-group-color to the known CSS variables list. |
- Fix adjustTabGroupsForInsert boundary: change <= to < so inserting at group end does not auto-expand the group - Re-sort _tabGroups after moveTabGroup rebuilds indices - Remove editors from existing groups in createTabGroup to prevent overlap - Create ITabGroupMutations interface and move mutable methods off IReadonlyEditorGroupModel for proper separation of concerns - Adjust tabGroups/getTabGroupForEditor in StickyEditorGroupModel (returns empty) and UnstickyEditorGroupModel (translates indices by -stickyCount) - Add role=button, tabIndex, aria-expanded, aria-label, and keyboard support (Enter/Space) to group headers - Localize group header tooltip text - Gate redrawTabGroups and tab group model change listener on tabGroups.enabled setting - Add config.workbench.editor.tabGroups.enabled when clause to all tab group context menu items - Cache tab elements array for O(1) index lookup in getTabAtIndex, getTabElementCount, and getLastTabElement - Add comprehensive unit tests for tab group model operations
Author
|
@copilot apply changes based on the comments in this thread |
- Fix potential infinite loop in handleClosedEditors: pop from tabElementsCache when removing tab elements so the count decreases - Clean up stale group headers and tab classes when tabGroups.enabled is toggled off at runtime - Capture group.id in event handlers and re-fetch current group state to avoid stale references from UnstickyEditorGroupModel - Convert relative startIndex to absolute index (add stickyCount) when dropping a group header in the unsticky tab bar - Preserve group membership in adjustTabGroupsForMove when an editor moves within the same group (no shrink+expand) - Enforce adjacency in includeInTabGroup: only allow absorption at startIndex-1 or startIndex+count to prevent non-contiguous groups - Emit EDITOR_MOVE events in removeFromTabGroup when the internal reorder changes tab positions (keeps Open Editors view in sync) - Emit EDITOR_MOVE events in moveTabGroup for each relocated editor so extension host and other listeners update correctly - Remove extra blank line in theme.ts
- getTabElementCount/getLastTabElement: fall back to DOM query when tabElementsCache is empty (feature disabled or pre-first-redraw) - Container/header group drop: only add stickyCount when tabsModel is UnstickyEditorGroupModel to avoid double-counting sticky tabs - includeInTabGroup: add isSticky guard to prevent sticky editors from being absorbed into tab groups - createTabGroup: return undefined instead of throwing when no valid editors provided (consistent with no-op pattern) - Deserialization: sort restored tab groups by startIndex and drop overlapping ranges to enforce invariants - Quick pick label: use template literal to keep icon outside localize call per hygiene rules - PR description: clarify collapse/expand are header-only interactions (6 menu items, not 8)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements Chrome-style tab grouping for VS Code's multi-editor tab bar, as requested in #100335.
Architecture
EditorGroupModelextended withIEditorTabGroup[], managing contiguous editor ranges with proper index maintenance on all mutations (move, insert, remove, close)MultiEditorTabsControlrenders.tab-group-headerelements interleaved with tabs, with index-skipping logic to maintain correct model-to-DOM mappingFilteredEditorGroupModel(Sticky/Unsticky) properly delegates all tab group operations to the underlying modeleditorCommands.ts; 6 have menu contributions in EditorTitleContext (collapse/expand are header-only interactions)Files changed
editorGroupModel.tsfilteredEditorGroupModel.tsmultiEditorTabsControl.tseditorCommands.tseditor.contribution.tseditor.tscommon/editor.tsworkbench.contribution.tsmedia/tabgroups.csscommon/theme.tsvscode-known-variables.json--tab-group-colorCSS variableTest plan
Fixes #100335
Observed behavior and known limitations
Some notes from my testing and development that may help during review:
EditorGroupModelowns its own set of editors. If cross-panel grouping is desired, that would be a separate effort.#hexorrgb()values through the "Change Color" picker. I don't have a strong preference for the specific preset colors I chose. If the team has established color conventions or wants to align with a particular palette, I am open to changing them.Tab groups (all in expanded state)
Context menu on the tab group
Context menu on individual tabs
Active tab in a collapsed tab group stays visible