diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index 125efd8243e8dd4d7e23d4915c88f2ca3e609e9f..922be1f300d999740e20c4764cb7e5a0109c6dd5 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -7744,7 +7744,7 @@ declare module 'vscode' { * your extension should first check to see if any backups exist for the resource. If there is a backup, your * extension should load the file contents from there instead of from the resource in the workspace. * - * `backup` is triggered approximately one second after the the user stops editing the document. If the user + * `backup` is triggered approximately one second after the user stops editing the document. If the user * rapidly edits the document, `backup` will not be invoked until the editing stops. * * `backup` is not invoked when `auto save` is enabled (since auto save already persists the resource). diff --git a/src/vs/workbench/api/browser/viewsExtensionPoint.ts b/src/vs/workbench/api/browser/viewsExtensionPoint.ts index 6be78f4ec8954ae80d0d73231857ea7dcf22441e..b785735c5a6d3d38f91512fdb0c54c873c8aa2e5 100644 --- a/src/vs/workbench/api/browser/viewsExtensionPoint.ts +++ b/src/vs/workbench/api/browser/viewsExtensionPoint.ts @@ -110,7 +110,7 @@ const viewDescriptor: IJSONSchema = { defaultSnippets: [{ body: { id: '${1:id}', name: '${2:name}' } }], properties: { type: { - markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), + markdownDescription: localize('vscode.extension.contributes.view.type', "Type of the view. This can either be `tree` for a tree view based view or `webview` for a webview based view. The default is `tree`."), type: 'string', enum: [ 'tree', diff --git a/src/vs/workbench/api/common/extHostCommands.ts b/src/vs/workbench/api/common/extHostCommands.ts index 228d60c37fc8c2644db1e7f75e7de0faf628ebc6..9c29d1f66c2dcfbc318c40769a9bd64a7b5e9450 100644 --- a/src/vs/workbench/api/common/extHostCommands.ts +++ b/src/vs/workbench/api/common/extHostCommands.ts @@ -103,7 +103,7 @@ export class ExtHostCommands implements ExtHostCommandsShape { const internalArgs = apiCommand.args.map((arg, i) => { if (!arg.validate(apiArgs[i])) { - throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', receieved: ${apiArgs[i]}`); + throw new Error(`Invalid argument '${arg.name}' when running '${apiCommand.id}', received: ${apiArgs[i]}`); } return arg.convert(apiArgs[i]); }); diff --git a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css index 65e31e2fd9f3372393ec0bd9684d55c305447c34..9ec69667ecd9afdaed83d8c9bf1f950ae98652cb 100644 --- a/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css +++ b/src/vs/workbench/browser/parts/editor/media/tabstitlecontrol.css @@ -38,10 +38,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { display: flex; -} - -.monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom { - position: relative; + position: relative; /* position tabs border bottom or editor actions (when tabs wrap) relative to this container */ } .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container.tabs-border-bottom::after { @@ -77,6 +74,13 @@ overflow: scroll !important; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container { + + /* Enable wrapping via flex layout and dynamic height */ + height: auto; + flex-wrap: wrap; +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container::-webkit-scrollbar { display: none; /* Chrome + Safari: hide scrollbar */ } @@ -93,6 +97,10 @@ padding-left: 10px; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab:last-child { + margin-right: var(--last-tab-margin-right); /* when tabs wrap, we need a margin away from the absolute positioned editor actions */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-right, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.has-icon.tab-actions-off:not(.sticky-compact) { padding-left: 5px; /* reduce padding when we show icons and are in shrinking mode and tab actions is not left (unless sticky-compact) */ @@ -105,6 +113,10 @@ flex-shrink: 0; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab.sizing-fit { + flex-grow: 1; /* grow the tabs to fill each row for a more homogeneous look when tabs wrap */ +} + .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink { min-width: 80px; flex-basis: 0; /* all tabs are even */ @@ -149,9 +161,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-compact, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-fit.sticky-shrink, .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container.disable-sticky-tabs > .tab.sizing-shrink.sticky-shrink { - - /** Disable sticky positions for sticky compact/shrink tabs if the available space is too little */ - position: static; + position: static; /** disable sticky positions for sticky compact/shrink tabs if the available space is too little */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-left .action-label { @@ -163,7 +173,7 @@ content: ''; display: flex; flex: 0; - width: 5px; /* Reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ + width: 5px; /* reserve space to hide tab fade when close button is left or off (fixes https://github.com/microsoft/vscode/issues/45728) */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.sizing-shrink.tab-actions-left { @@ -276,7 +286,7 @@ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.tab-actions-right.sizing-shrink > .tab-actions { flex: 0; - overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room... */ + overflow: hidden; /* let the tab actions be pushed out of view when sizing is set to shrink to make more room */ } .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-container > .tab.dirty.tab-actions-right.sizing-shrink > .tab-actions, @@ -366,6 +376,14 @@ height: 35px; } +.monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .editor-actions { + + /* When tabs are wrapped, position the editor actions at the end of the very last row */ + position: absolute; + bottom: 0; + right: 0; +} + /* Breadcrumbs */ .monaco-workbench .part.editor > .content .editor-group-container > .title .tabs-breadcrumbs .breadcrumbs-control { diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 74056984c5203b7f15e94a27da89ebbc1b4e047a..d7cfb96a4883698943ba4c087d8942595e6a6396 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -91,7 +91,7 @@ export class TabsTitleControl extends TitleControl { private tabActionBars: ActionBar[] = []; private tabDisposables: IDisposable[] = []; - private dimensions: ITitleControlDimensions = { + private dimensions: ITitleControlDimensions & { used?: Dimension } = { container: Dimension.None, available: Dimension.None }; @@ -571,6 +571,7 @@ export class TabsTitleControl extends TitleControl { oldOptions.showIcons !== newOptions.showIcons || oldOptions.hasIcons !== newOptions.hasIcons || oldOptions.highlightModifiedTabs !== newOptions.highlightModifiedTabs || + oldOptions.wrapTabs !== newOptions.wrapTabs || !equals(oldOptions.decorations, newOptions.decorations) ) { this.redraw(); @@ -1274,19 +1275,30 @@ export class TabsTitleControl extends TitleControl { } getDimensions(): IEditorGroupTitleDimensions { - let height = TabsTitleControl.TAB_HEIGHT; + let height: number; + + // Wrap: we need to ask `offsetHeight` to get + // the real height of the title area with wrapping. + if (this.accessor.partOptions.wrapTabs && this.tabsAndActionsContainer?.classList.contains('wrapping')) { + height = this.tabsAndActionsContainer.offsetHeight; + } else { + height = TabsTitleControl.TAB_HEIGHT; + } + + const offset = height; + + // Account for breadcrumbs if visible if (this.breadcrumbsControl && !this.breadcrumbsControl.isHidden()) { height += BreadcrumbsControl.HEIGHT; // Account for breadcrumbs if visible } - return { - height, - offset: TabsTitleControl.TAB_HEIGHT - }; + return { height, offset }; } layout(dimensions: ITitleControlDimensions): Dimension { - this.dimensions = dimensions; + + // Remember dimensions that we get + Object.assign(this.dimensions, dimensions); // The layout of tabs can be an expensive operation because we access DOM properties // that can result in the browser doing a full page layout to validate them. To buffer @@ -1303,17 +1315,33 @@ export class TabsTitleControl extends TitleControl { } private doLayout(dimensions: ITitleControlDimensions): void { + + // Only layout if we have valid tab index and dimensions const activeTabAndIndex = this.group.activeEditor ? this.getTabAndIndex(this.group.activeEditor) : undefined; - if (!activeTabAndIndex || dimensions.container === Dimension.None || dimensions.available === Dimension.None) { - return; // nothing to do if not editor opened or we got no dimensions yet + if (activeTabAndIndex && dimensions.container !== Dimension.None && dimensions.available !== Dimension.None) { + + // Breadcrumbs + this.doLayoutBreadcrumbs(dimensions); + + // Tabs + const [activeTab, activeIndex] = activeTabAndIndex; + this.doLayoutTabs(activeTab, activeIndex, dimensions); } - // Breadcrumbs - this.doLayoutBreadcrumbs(dimensions); + // Compute new dimension of tabs title control and remember it for future usages + const oldDimension = this.dimensions.used; + const newDimension = this.dimensions.used = new Dimension(dimensions.container.width, this.getDimensions().height); - // Tabs - const [activeTab, activeIndex] = activeTabAndIndex; - this.doLayoutTabs(activeTab, activeIndex); + // In case the height of the title control changed from before + // (currently only possible if tabs are set to wrap), we need + // to signal this to the outside via a `relayout` call so that + // e.g. the editor control can be adjusted accordingly. + if ( + this.accessor.partOptions.wrapTabs && + oldDimension && oldDimension.height !== newDimension.height + ) { + this.group.relayout(); + } } private doLayoutBreadcrumbs(dimensions: ITitleControlDimensions): void { @@ -1322,8 +1350,8 @@ export class TabsTitleControl extends TitleControl { } } - private doLayoutTabs(activeTab: HTMLElement, activeIndex: number): void { - const [tabsContainer, tabsScrollbar] = assertAllDefined(this.tabsContainer, this.tabsScrollbar); + private doLayoutTabs(activeTab: HTMLElement, activeIndex: number, dimensions: ITitleControlDimensions): void { + const [tabsAndActionsContainer, tabsContainer, tabsScrollbar, editorToolbarContainer] = assertAllDefined(this.tabsAndActionsContainer, this.tabsContainer, this.tabsScrollbar, this.editorToolbarContainer); // // Synopsis @@ -1381,6 +1409,55 @@ export class TabsTitleControl extends TitleControl { tabsContainer.classList.remove('disable-sticky-tabs'); } + // Handle wrapping tabs according to setting: + // - enabled: only add class if tabs wrap and don't exceed available height + // - disabled: remove class + if (this.accessor.partOptions.wrapTabs) { + let tabsWrapMultiLine = tabsAndActionsContainer.classList.contains('wrapping'); + let updateScrollbar = false; + + // Tabs do not wrap multiline: add wrapping if tabs exceed the tabs container width + // and the height of the tabs container does not exceed the maximum + if (!tabsWrapMultiLine && allTabsWidth > visibleTabsContainerWidth) { + tabsAndActionsContainer.classList.add('wrapping'); + tabsWrapMultiLine = true; + } + + // Tabs wrap multiline: remove wrapping if height exceeds available height + // or the maximum allowed height + if (tabsWrapMultiLine && tabsContainer.offsetHeight > dimensions.available.height) { + tabsAndActionsContainer.classList.remove('wrapping'); + tabsWrapMultiLine = false; + updateScrollbar = true; + } + + // If we do not exceed the tabs container width, we cannot simply remove + // the wrap class because by wrapping tabs, they reduce their size + // and we would otherwise constantly add and remove the class. As such + // we need to check if the height of the tabs container is back to normal + // and then remove the wrap class. + if (tabsWrapMultiLine && allTabsWidth === visibleTabsContainerWidth && tabsContainer.offsetHeight === TabsTitleControl.TAB_HEIGHT) { + tabsAndActionsContainer.classList.remove('wrapping'); + tabsWrapMultiLine = false; + updateScrollbar = true; + } + + // Update `last-tab-margin-right` CSS variable to account for the absolute + // positioned editor actions container when tabs wrap. The margin needs to + // be the width of the editor actions container to avoid screen cheese. + tabsContainer.style.setProperty('--last-tab-margin-right', tabsWrapMultiLine ? `${editorToolbarContainer.offsetWidth}px` : '0'); + + // When tabs change from wrapping back to normal, we need to indicate this + // to the scrollbar so that revealing the active tab functions properly. + if (updateScrollbar) { + tabsScrollbar.setScrollPosition({ + scrollLeft: tabsContainer.scrollLeft + }); + } + } else { + tabsAndActionsContainer.classList.remove('wrapping'); + } + let activeTabPosX: number | undefined; let activeTabWidth: number | undefined; @@ -1567,11 +1644,23 @@ registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) = // Add border between tabs and breadcrumbs in high contrast mode. if (theme.type === ColorScheme.HIGH_CONTRAST) { const borderColor = (theme.getColor(TAB_BORDER) || theme.getColor(contrastBorder)); + if (borderColor) { + collector.addRule(` + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container { + border-bottom: 1px solid ${borderColor}; + } + `); + } + } + + // Add bottom border to tabs when wrapping + const borderColor = theme.getColor(TAB_BORDER); + if (borderColor) { collector.addRule(` - .monaco-workbench .part.editor > .content .editor-group-container > .title.tabs > .tabs-and-actions-container { - border-bottom: 1px solid ${borderColor}; - } - `); + .monaco-workbench .part.editor > .content .editor-group-container > .title > .tabs-and-actions-container.wrapping .tabs-container > .tab { + border-bottom: 1px solid ${borderColor}; + } + `); } // Styling with Outline color (e.g. high contrast theme) diff --git a/src/vs/workbench/browser/workbench.contribution.ts b/src/vs/workbench/browser/workbench.contribution.ts index 270463eed9440e03026510ec9246fc460bed09ff..b47ccfbe06afb3034b7565b75b8b40fb8e5e5f85 100644 --- a/src/vs/workbench/browser/workbench.contribution.ts +++ b/src/vs/workbench/browser/workbench.contribution.ts @@ -33,6 +33,11 @@ import { isStandalone } from 'vs/base/browser/browser'; 'description': nls.localize('showEditorTabs', "Controls whether opened editors should show in tabs or not."), 'default': true }, + 'workbench.editor.wrapTabs': { + 'type': 'boolean', + 'description': nls.localize('wrapTabs', "Controls whether tabs should be wrapped over multiple lines when exceeding available space or wether a scrollbar should appear instead."), + 'default': false + }, 'workbench.editor.scrollToSwitchTabs': { 'type': 'boolean', 'markdownDescription': nls.localize({ comment: ['This is the description for a setting. Values surrounded by single quotes are not to be translated.'], key: 'scrollToSwitchTabs' }, "Controls whether scrolling over tabs will open them or not. By default tabs will only reveal upon scrolling, but not open. You can press and hold the Shift-key while scrolling to change this behaviour for that duration. This value is ignored when `#workbench.editor.showTabs#` is disabled."), diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index db82863b0a679a1d1977dfe0b83a107a74f57997..99bde65fbb8e1105761a9f7ed0e5e6ff857f0e3a 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -1265,6 +1265,7 @@ export interface IWorkbenchEditorConfiguration { interface IEditorPartConfiguration { showTabs?: boolean; + wrapTabs?: boolean; scrollToSwitchTabs?: boolean; highlightModifiedTabs?: boolean; tabCloseButton?: 'left' | 'right' | 'off'; diff --git a/src/vs/workbench/contrib/debug/browser/replViewer.ts b/src/vs/workbench/contrib/debug/browser/replViewer.ts index 5f6dbc2a786baaf8764c2e431d09059bcece8179..32e3dcb054353bc26e6104560ec3c04dce8aedc5 100644 --- a/src/vs/workbench/contrib/debug/browser/replViewer.ts +++ b/src/vs/workbench/contrib/debug/browser/replViewer.ts @@ -389,7 +389,7 @@ export class ReplAccessibilityProvider implements IListAccessibilityProvider 1 ? localize({ key: 'occurred', comment: ['Front will the the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, + return element.value + (element instanceof SimpleReplElement && element.count > 1 ? localize({ key: 'occurred', comment: ['Front will the value of the debug console element. Placeholder will be replaced by a number which represents occurrance count.'] }, ", occured {0} times", element.count) : ''); } if (element instanceof RawObjectReplElement) { diff --git a/src/vs/workbench/contrib/debug/common/replModel.ts b/src/vs/workbench/contrib/debug/common/replModel.ts index f10608a01564d1f8be7134dbefb147b7ed227bef..f3e276042246d0ff6db9436ecee2b16acd6ebd5e 100644 --- a/src/vs/workbench/contrib/debug/common/replModel.ts +++ b/src/vs/workbench/contrib/debug/common/replModel.ts @@ -30,8 +30,12 @@ export class SimpleReplElement implements IReplElement { ) { } toString(): string { + let valueRespectCount = this.value; + if (this._count > 1) { + valueRespectCount = valueRespectCount + ('\n' + this.value).repeat(this._count - 1); + } const sourceStr = this.sourceData ? ` ${this.sourceData.source.name}` : ''; - return this.value + sourceStr; + return valueRespectCount + sourceStr; } getId(): string {