Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/vs/sessions/LAYOUT.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ The Editor and Auxiliary Bar compose one side pane next to the active session.
Editor tabs choose either editor content or a details view while the layout
coordinators preserve one coherent visibility model.

The main Editor supports exactly one editor group. Its shared multiple-group
capability is disabled, which removes editor split/grid commands, keybindings,
menus, and split drop targets; the part also rejects group creation and
multi-group layout requests from open-to-side and programmatic paths. The
independent chat grid remains supported.

The durable state and transition catalog lives in
[SINGLE_PANE_SCENARIOS.md](SINGLE_PANE_SCENARIOS.md). Implementation behavior is
covered by the layout-controller and single-pane strategy tests.
Expand Down
4 changes: 4 additions & 0 deletions src/vs/sessions/SINGLE_PANE_SCENARIOS.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ bar spanning the editor content and a docked detail panel).
single-pane layout described here. Phone viewports always use the classic
layout. When the setting is **OFF**, all Agents windows use the classic layout
and nothing else in this document applies.
- The main Editor supports exactly one editor group. Editor split/grid commands,
keybindings, menus, open-to-side requests, and split drop targets are disabled;
programmatic group creation and multi-group layout requests are rejected. This
restriction does not apply to the separate chat grid.
- Companion specs: [Editor presentation](LAYOUT.md#editor-presentation),
[LAYOUT_CONTROLLER.md](LAYOUT_CONTROLLER.md), and
[contrib/layout/browser/desktopSessionLayoutController.md](contrib/layout/browser/desktopSessionLayoutController.md).
Expand Down
33 changes: 32 additions & 1 deletion src/vs/sessions/browser/parts/editorParts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import './media/editorPart.css';
import { InstantiationType, registerSingleton } from '../../../platform/instantiation/common/extensions.js';
import { IEditorGroupView } from '../../../workbench/browser/parts/editor/editor.js';
import { EditorParts as EditorPartsBase } from '../../../workbench/browser/parts/editor/editorParts.js';
import { IEditorGroupsService } from '../../../workbench/services/editor/common/editorGroupsService.js';
import { GroupIdentifier } from '../../../workbench/common/editor.js';
import { GroupDirection, IEditorGroupsService } from '../../../workbench/services/editor/common/editorGroupsService.js';
import { IAgentWorkbenchLayoutService } from '../workbench.js';
import { MainEditorPart } from './editorPart.js';
import { SinglePaneMainEditorPart } from './singlePaneEditorPart.js';
Expand All @@ -21,6 +23,35 @@ export class EditorParts extends EditorPartsBase {

return editorPart;
}

override moveGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
if (this.involvesSinglePaneMainPart(group, location)) {
return this.resolveGroup(group);
}

return super.moveGroup(group, location, direction);
}

override copyGroup(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier, direction: GroupDirection): IEditorGroupView {
if (this.involvesSinglePaneMainPart(group, location)) {
return this.resolveGroup(group);
}

return super.copyGroup(group, location, direction);
}
Comment thread
sandy081 marked this conversation as resolved.

private involvesSinglePaneMainPart(group: IEditorGroupView | GroupIdentifier, location: IEditorGroupView | GroupIdentifier): boolean {
return this.mainPart instanceof SinglePaneMainEditorPart
&& (this.getPart(group) === this.mainPart || this.getPart(location) === this.mainPart);
}

private resolveGroup(group: IEditorGroupView | GroupIdentifier): IEditorGroupView {
const resolvedGroup = typeof group === 'number' ? this.getGroup(group) : group;
if (!resolvedGroup) {
throw new Error('Invalid editor group provided!');
}
return resolvedGroup;
}
}

registerSingleton(IEditorGroupsService, EditorParts, InstantiationType.Eager);
41 changes: 39 additions & 2 deletions src/vs/sessions/browser/parts/singlePaneEditorPart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
*--------------------------------------------------------------------------------------------*/

import { mainWindow } from '../../../base/browser/window.js';
import { DisposableMap, MutableDisposable } from '../../../base/common/lifecycle.js';
import { DisposableMap, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import { IConfigurationService } from '../../../platform/configuration/common/configuration.js';
import { IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
import { IStorageService } from '../../../platform/storage/common/storage.js';
import { IThemeService } from '../../../platform/theme/common/themeService.js';
import { IEditorGroupViewOptions, IEditorPartCreationOptions, IEditorPartsView } from '../../../workbench/browser/parts/editor/editor.js';
import { IEditorGroupView, IEditorGroupViewOptions, IEditorPartCreationOptions, IEditorPartsView } from '../../../workbench/browser/parts/editor/editor.js';
import { IEditorPartUIState } from '../../../workbench/browser/parts/editor/editorPart.js';
import { EditorGroupView } from '../../../workbench/browser/parts/editor/editorGroupView.js';
import { GroupIdentifier } from '../../../workbench/common/editor.js';
import { EditorGroupLayout, GroupDirection, GroupLayoutArgument, IEditorDropTargetDelegate } from '../../../workbench/services/editor/common/editorGroupsService.js';
import { Parts } from '../../../workbench/services/layout/browser/layoutService.js';
import { IHostService } from '../../../workbench/services/host/browser/host.js';
import { DockedAuxiliaryBarController } from '../dockedAuxiliaryBarController.js';
Expand Down Expand Up @@ -156,6 +159,32 @@ export class SinglePaneMainEditorPart extends MainEditorPart {
return container;
}

override addGroup(location: IEditorGroupView | GroupIdentifier, _direction: GroupDirection, _groupToCopy?: IEditorGroupView): IEditorGroupView {
return this.assertGroupView(location);
}

override applyLayout(layout: EditorGroupLayout): void {
if (countEditorGroups(layout.groups) > 1) {
return;
}
super.applyLayout(layout);
}

override createEditorDropTarget(container: unknown, delegate: IEditorDropTargetDelegate): IDisposable {
return super.createEditorDropTarget(container, { ...delegate, supportsSplitting: false });
}

override async applyState(state: IEditorPartUIState | 'empty', options?: IEditorGroupViewOptions): Promise<void> {
await super.applyState(state, options);
this._ensureSingleEditorGroup();
Comment thread
sandy081 marked this conversation as resolved.
}

private _ensureSingleEditorGroup(): void {
if (this.count > 1) {
this.mergeAllGroups(this.activeGroup);
}
}

/**
* Keeps the docked auxiliary bar aligned after group-local relayouts.
*/
Expand Down Expand Up @@ -188,3 +217,11 @@ export class SinglePaneMainEditorPart extends MainEditorPart {
this._dockedAuxBar?.layout();
}
}

function countEditorGroups(groups: GroupLayoutArgument[]): number {
let count = 0;
for (const group of groups) {
count += group.groups ? countEditorGroups(group.groups) : 1;
}
return count;
}
77 changes: 77 additions & 0 deletions src/vs/sessions/test/browser/workbench.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { DockedAuxiliaryBarController, IDockedAuxiliaryBarHost } from '../../bro
import { ISidePaneToggleEvent, Workbench } from '../../browser/workbench.js';
import { DockedEditorSizeMemento, SinglePaneWorkbench } from '../../browser/singlePaneWorkbench.js';
import { SinglePaneMainEditorPart } from '../../browser/parts/singlePaneEditorPart.js';
import { EditorParts } from '../../browser/parts/editorParts.js';
import { DockedEditorInput } from '../../common/dockedEditorInput.js';
import { EditorInputCapabilities } from '../../../workbench/common/editor.js';
import { GroupDirection, GroupOrientation } from '../../../workbench/services/editor/common/editorGroupsService.js';
import { SESSIONS_LIST_MINIMUM_WIDTH } from '../../browser/parts/sidebarPart.js';
import { Menus } from '../../browser/menus.js';
import { DEFAULT_NOTIFICATION_ROW_HEIGHT, onDidChangeNotificationRowHeight, setNotificationRowHeight } from '../../../workbench/browser/parts/notifications/notificationsViewer.js';
Expand Down Expand Up @@ -1633,6 +1635,81 @@ suite('Sessions - Workbench', () => {
});
});

test('single-pane editor part rejects editor group creation and multi-group layouts', () => {
const group = {};
const addGroup = Reflect.get(SinglePaneMainEditorPart.prototype, 'addGroup') as (location: object, direction: GroupDirection) => object;
const applyLayout = Reflect.get(SinglePaneMainEditorPart.prototype, 'applyLayout') as (layout: { orientation: GroupOrientation; groups: object[] }) => void;

assert.deepStrictEqual({
addGroupResult: addGroup.call({ assertGroupView: () => group }, group, GroupDirection.RIGHT),
multiGroupLayoutRejected: (() => {
applyLayout.call({}, { orientation: GroupOrientation.HORIZONTAL, groups: [{}, {}] });
return true;
})(),
}, {
addGroupResult: group,
multiGroupLayoutRejected: true,
});
});

test('single-pane editor parts reject cross-part group moves and copies', () => {
const mainPart = Object.create(SinglePaneMainEditorPart.prototype) as SinglePaneMainEditorPart;
const auxiliaryPart = {};
const mainGroup = {};
const auxiliaryGroup = {};
const involvesSinglePaneMainPart = Reflect.get(EditorParts.prototype, 'involvesSinglePaneMainPart') as (group: object, location: object) => boolean;
const host = {
mainPart,
getPart: (group: object) => group === mainGroup ? mainPart : auxiliaryPart,
resolveGroup: (group: object) => group,
involvesSinglePaneMainPart,
};
const moveGroup = Reflect.get(EditorParts.prototype, 'moveGroup') as (group: object, location: object, direction: GroupDirection) => object;
const copyGroup = Reflect.get(EditorParts.prototype, 'copyGroup') as (group: object, location: object, direction: GroupDirection) => object;

assert.deepStrictEqual({
moveFromMain: moveGroup.call(host, mainGroup, auxiliaryGroup, GroupDirection.RIGHT),
moveToMain: moveGroup.call(host, auxiliaryGroup, mainGroup, GroupDirection.RIGHT),
copyFromMain: copyGroup.call(host, mainGroup, auxiliaryGroup, GroupDirection.RIGHT),
copyToMain: copyGroup.call(host, auxiliaryGroup, mainGroup, GroupDirection.RIGHT),
}, {
moveFromMain: mainGroup,
moveToMain: auxiliaryGroup,
copyFromMain: mainGroup,
copyToMain: auxiliaryGroup,
});
});

test('single-pane editor retains restored editors when collapsing restored groups', () => {
const firstEditor = { id: 'first' };
const secondEditor = { id: 'second' };
const activeGroup = { editors: [secondEditor], activeEditor: secondEditor };
const sourceGroup = { editors: [firstEditor], activeEditor: firstEditor };
const host = {
count: 2,
activeGroup,
groups: [sourceGroup, activeGroup],
mergeAllGroups(target: typeof activeGroup) {
target.editors.unshift(...sourceGroup.editors);
this.groups = [target];
this.count = 1;
},
};
const ensureSingleEditorGroup = Reflect.get(SinglePaneMainEditorPart.prototype, '_ensureSingleEditorGroup') as () => void;

ensureSingleEditorGroup.call(host);

assert.deepStrictEqual({
groupCount: host.count,
editors: host.activeGroup.editors.map(editor => editor.id),
activeEditor: host.activeGroup.activeEditor.id,
}, {
groupCount: 1,
editors: ['first', 'second'],
activeEditor: 'second',
});
});

test('applies an even split when revealing the docked editor with no captured width even after the initial split', () => {
const host = createHost({ single: true, sessionsWidth: 1000, windowWidth: 1300, hasAppliedInitialEditorSplit: true, dockedWidth: 300, editorWidth: 300, partVisibility: { editor: false, auxiliaryBar: true } });

Expand Down
17 changes: 14 additions & 3 deletions src/vs/workbench/browser/parts/editor/editorDropTarget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ class DropOverlay extends Themable {

constructor(
private readonly groupView: IEditorGroupView,
private readonly supportsSplitting: boolean,
@IThemeService themeService: IThemeService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
Expand Down Expand Up @@ -181,8 +182,8 @@ class DropOverlay extends Themable {
// Position overlay and conditionally enable or disable
// editor group splitting support based on setting and
// keymodifiers used.
let splitOnDragAndDrop = !!this.groupView.groupsView.partOptions.splitOnDragAndDrop;
if (this.isToggleSplitOperation(e)) {
let splitOnDragAndDrop = this.supportsSplitting && !!this.groupView.groupsView.partOptions.splitOnDragAndDrop;
if (this.supportsSplitting && this.isToggleSplitOperation(e)) {
splitOnDragAndDrop = !splitOnDragAndDrop;
}
this.positionOverlay(e.offsetX, e.offsetY, isDraggingGroup, splitOnDragAndDrop);
Expand Down Expand Up @@ -388,6 +389,16 @@ class DropOverlay extends Themable {
const editorControlWidth = this.groupView.element.clientWidth;
const editorControlHeight = this.groupView.element.clientHeight - this.getOverlayOffsetHeight();

if (!enableSplitting) {
this.doPositionOverlay({ top: '0', left: '0', width: '100%', height: '100%' });
this.toggleDropIntoPrompt(true);
const overlay = assertReturnsDefined(this.overlay);
overlay.style.opacity = '1';
setTimeout(() => overlay.classList.add('overlay-move-transition'), 0);
this.currentDropOperation = { splitDirection: undefined };
return;
}

let edgeWidthThresholdFactor: number;
let edgeHeightThresholdFactor: number;
if (enableSplitting) {
Expand Down Expand Up @@ -644,7 +655,7 @@ export class EditorDropTarget extends Themable {
if (!this.overlay) {
const targetGroupView = this.findTargetGroupView(target);
if (targetGroupView) {
this._overlay = this.instantiationService.createInstance(DropOverlay, targetGroupView);
this._overlay = this.instantiationService.createInstance(DropOverlay, targetGroupView, this.delegate.supportsSplitting !== false);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,11 @@ export interface IEditorDropTargetDelegate {
* A helper to figure out if the drop target contains the provided group.
*/
containsGroup?(groupView: IEditorGroup): boolean;

/**
* Whether the drop target supports creating editor groups.
*/
readonly supportsSplitting?: boolean;
}

/**
Expand Down
Loading