editors - introduce MODAL_GROUP for a modal editor part (#293020)

This commit is contained in:
Benjamin Pasero
2026-02-05 12:33:36 +01:00
committed by GitHub
parent e940fedc0f
commit bf79fb66ac
21 changed files with 1162 additions and 67 deletions

View File

@@ -0,0 +1,213 @@
---
description: Architecture documentation for VS Code modal editor part. Use when working with modal editor functionality in `src/vs/workbench/browser/parts/editor/modalEditorPart.ts`
applyTo: src/vs/workbench/**/modal*.ts
---
# Modal Editor Part Design Document
This document describes the conceptual design of the Modal Editor Part feature in VS Code. Use this as a reference when working with modal editor functionality.
## Overview
The Modal Editor Part is a new editor part concept that displays editors in a modal overlay on top of the workbench. It follows the same architectural pattern as `AUX_WINDOW_GROUP` (auxiliary window editor parts) but renders within the main window as an overlay instead of a separate window.
## Architecture
### Constants and Types
Location: `src/vs/workbench/services/editor/common/editorService.ts`
```typescript
export const MODAL_GROUP = -4;
export type MODAL_GROUP_TYPE = typeof MODAL_GROUP;
```
The `MODAL_GROUP` constant follows the pattern of other special group identifiers:
- `ACTIVE_GROUP = -1`
- `SIDE_GROUP = -2`
- `AUX_WINDOW_GROUP = -3`
- `MODAL_GROUP = -4`
### Interfaces
Location: `src/vs/workbench/services/editor/common/editorGroupsService.ts`
```typescript
export interface IModalEditorPart extends IEditorPart {
readonly onWillClose: Event<void>;
close(): boolean;
}
```
The `IModalEditorPart` interface extends `IEditorPart` and adds:
- `onWillClose`: Event fired before the modal closes
- `close()`: Closes the modal, merging confirming editors back to the main part
### Service Method
The `IEditorGroupsService` interface includes:
```typescript
createModalEditorPart(): Promise<IModalEditorPart>;
```
## Implementation
### ModalEditorPart Class
Location: `src/vs/workbench/browser/parts/editor/modalEditorPart.ts`
The implementation consists of two classes:
1. **`ModalEditorPart`**: Factory class that creates the modal UI
- Creates modal backdrop with dimmed overlay
- Creates shadow container for the modal window
- Handles layout relative to main container dimensions
- Registers escape key and click-outside handlers for closing
2. **`ModalEditorPartImpl`**: The actual editor part extending `EditorPart`
- Enforces `showTabs: 'single'` and `closeEmptyGroups: true`
- Overrides `removeGroup` to close modal when last group is removed
- Does not persist state (modal is transient)
- Merges editors back to main part on close
### Key Behaviors
1. **Single Tab Mode**: Modal enforces `showTabs: 'single'` for a focused experience
2. **Auto-close on Empty**: When all editors are closed, the modal closes automatically
3. **Merge on Close**: Confirming editors (dirty, etc.) are merged back to main part
4. **Escape to Close**: Pressing Escape closes the modal
5. **Click Outside to Close**: Clicking the dimmed backdrop closes the modal
### CSS Styling
Location: `src/vs/workbench/browser/parts/editor/media/modalEditorPart.css`
```css
.monaco-modal-editor-block {
/* Full-screen overlay with flexbox centering */
}
.monaco-modal-editor-block.dimmed {
/* Semi-transparent dark background */
}
.modal-editor-shadow {
/* Shadow and border-radius for the modal window */
}
```
## Integration Points
### EditorParts Service
Location: `src/vs/workbench/browser/parts/editor/editorParts.ts`
The `EditorParts` class implements `createModalEditorPart()`:
```typescript
async createModalEditorPart(): Promise<IModalEditorPart> {
const { part, disposables } = await this.instantiationService
.createInstance(ModalEditorPart, this).create();
this._onDidAddGroup.fire(part.activeGroup);
disposables.add(toDisposable(() => {
this._onDidRemoveGroup.fire(part.activeGroup);
}));
return part;
}
```
### Active Part Detection
Location: `src/vs/workbench/browser/parts/editor/editorParts.ts`
Override of `getPartByDocument` to detect when focus is in a modal:
```typescript
protected override getPartByDocument(document: Document): EditorPart {
if (this._parts.size > 1) {
const activeElement = getActiveElement();
for (const part of this._parts) {
if (part !== this.mainPart && part.element?.ownerDocument === document) {
const container = part.getContainer();
if (container && isAncestor(activeElement, container)) {
return part;
}
}
}
}
return super.getPartByDocument(document);
}
```
This ensures that when focus is in the modal, it is considered the active part for editor opening via quick open, etc.
### Editor Group Finder
Location: `src/vs/workbench/services/editor/common/editorGroupFinder.ts`
The `findGroup` function handles `MODAL_GROUP`:
```typescript
else if (preferredGroup === MODAL_GROUP) {
group = editorGroupService.createModalEditorPart()
.then(part => part.activeGroup);
}
```
## Usage Examples
### Opening an Editor in Modal
```typescript
// Using the editor service
await editorService.openEditor(input, options, MODAL_GROUP);
// Using a flag pattern (e.g., settings)
interface IOpenSettingsOptions {
openInModal?: boolean;
}
// Implementation checks the flag
if (options.openInModal) {
group = await findGroup(accessor, {}, MODAL_GROUP);
}
```
### Current Integrations
1. **Settings Editor**: Opens in modal via `openInModal: true` option
2. **Keyboard Shortcuts Editor**: Opens in modal via `openInModal: true` option
3. **Extensions Editor**: Uses `openInModal: true` in `IExtensionEditorOptions`
4. **Profiles Editor**: Opens directly with `MODAL_GROUP`
## Testing
Location: `src/vs/workbench/services/editor/test/browser/modalEditorGroup.test.ts`
Test categories:
- Constants and types verification
- Creation and initial state
- Editor operations (open, split)
- Closing behavior and events
- Options enforcement
- Integration with EditorParts service
## Design Decisions
1. **Why extend EditorPart?**: Reuses all editor group functionality without duplication
2. **Why single tab mode?**: Modal is for focused, single-editor experiences
3. **Why merge on close?**: Prevents data loss for dirty editors
4. **Why same window?**: Avoids complexity of auxiliary windows while providing overlay UX
5. **Why transient state?**: Modal is meant for temporary focused editing, not persistence
## Future Considerations
- Consider adding animation for open/close transitions
- Consider size/position customization
- Consider multiple modal stacking (though likely not needed)
- Consider keyboard navigation between modal and main editor areas

View File

@@ -439,6 +439,9 @@ class AuxiliaryEditorPartImpl extends EditorPart implements IAuxiliaryEditorPart
// Then merge remaining to main part
result = this.mergeGroupsToMainPart();
if (!result) {
return false; // Do not close when editors could not be merged back
}
}
this._onWillClose.fire();

View File

@@ -5,7 +5,7 @@
import { GroupIdentifier, IWorkbenchEditorConfiguration, IEditorIdentifier, IEditorCloseEvent, IEditorPartOptions, IEditorPartOptionsChangeEvent, SideBySideEditor, EditorCloseContext, IEditorPane, IEditorPartLimitOptions, IEditorPartDecorationOptions, IEditorWillOpenEvent, EditorInputWithOptions } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorGroup, GroupDirection, IMergeGroupOptions, GroupsOrder, GroupsArrangement, IAuxiliaryEditorPart, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { Dimension } from '../../../../base/browser/dom.js';
import { Event } from '../../../../base/common/event.js';
@@ -195,6 +195,7 @@ export interface IEditorPartsView {
readonly count: number;
createAuxiliaryEditorPart(options?: IAuxiliaryWindowOpenOptions): Promise<IAuxiliaryEditorPart>;
createModalEditorPart(): Promise<IModalEditorPart>;
bind<T extends ContextKeyValue>(contextKey: RawContextKey<T>, group: IEditorGroupView): IContextKey<T>;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart } from '../../../services/editor/common/editorGroupsService.js';
import { EditorGroupLayout, GroupDirection, GroupLocation, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, IEditorGroupContextKeyProvider, IEditorDropTargetDelegate, IEditorGroupsService, IEditorSideGroup, IEditorWorkingSet, IFindGroupScope, IMergeGroupOptions, IEditorWorkingSetOptions, IEditorPart, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js';
import { Emitter } from '../../../../base/common/event.js';
import { DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { GroupIdentifier, IEditorPartOptions } from '../../../common/editor.js';
@@ -14,6 +14,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { distinct } from '../../../../base/common/arrays.js';
import { AuxiliaryEditorPart, IAuxiliaryEditorPartOpenOptions } from './auxiliaryEditorPart.js';
import { ModalEditorPart } from './modalEditorPart.js';
import { MultiWindowParts } from '../../part.js';
import { DeferredPromise } from '../../../../base/common/async.js';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
@@ -21,7 +22,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js
import { IAuxiliaryWindowOpenOptions, IAuxiliaryWindowService } from '../../../services/auxiliaryWindow/browser/auxiliaryWindowService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ContextKeyValue, IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
import { isHTMLElement } from '../../../../base/browser/dom.js';
import { getActiveElement, isAncestor, isHTMLElement } from '../../../../base/browser/dom.js';
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { DeepPartial } from '../../../../base/common/types.js';
@@ -94,20 +95,34 @@ export class EditorParts extends MultiWindowParts<EditorPart, IEditorPartsMement
//#region Scoped Instantiation Services
private readonly mapPartToInstantiationService = new Map<number /* window ID */, IInstantiationService>();
private modalPartInstantiationService: IInstantiationService | undefined;
getScopedInstantiationService(part: IEditorPart): IInstantiationService {
// Main Part
if (part === this.mainPart) {
if (!this.mapPartToInstantiationService.has(part.windowId)) {
this.instantiationService.invokeFunction(accessor => {
let mainPartInstantiationService = this.mapPartToInstantiationService.get(part.windowId);
if (!mainPartInstantiationService) {
mainPartInstantiationService = this.instantiationService.invokeFunction(accessor => {
const editorService = accessor.get(IEditorService);
const statusbarService = accessor.get(IStatusbarService);
this.mapPartToInstantiationService.set(part.windowId, this._register(this.mainPart.scopedInstantiationService.createChild(new ServiceCollection(
const mainPartInstantiationService = this._register(this.mainPart.scopedInstantiationService.createChild(new ServiceCollection(
[IEditorService, editorService.createScoped(this.mainPart, this._store)],
[IStatusbarService, statusbarService.createScoped(statusbarService, this._store)]
))));
)));
this.mapPartToInstantiationService.set(part.windowId, mainPartInstantiationService);
return mainPartInstantiationService;
});
}
return mainPartInstantiationService;
}
// Modal Part (if opened)
if (part === this.modalEditorPart && this.modalPartInstantiationService) {
return this.modalPartInstantiationService;
}
return this.mapPartToInstantiationService.get(part.windowId) ?? this.instantiationService;
@@ -137,6 +152,35 @@ export class EditorParts extends MultiWindowParts<EditorPart, IEditorPartsMement
//#endregion
//#region Modal Editor Part
private modalEditorPart: IModalEditorPart | undefined;
async createModalEditorPart(): Promise<IModalEditorPart> {
// Reuse existing modal editor part if it exists
if (this.modalEditorPart) {
return this.modalEditorPart;
}
const { part, instantiationService, disposables } = await this.instantiationService.createInstance(ModalEditorPart, this).create();
// Keep instantiation service and reference to reuse
this.modalEditorPart = part;
this.modalPartInstantiationService = instantiationService;
disposables.add(toDisposable(() => {
this.modalPartInstantiationService = undefined;
this.modalEditorPart = undefined;
}));
// Events
this._onDidAddGroup.fire(part.activeGroup);
return part;
}
//#endregion
//#region Registration
override registerPart(part: EditorPart): IDisposable {
@@ -218,6 +262,27 @@ export class EditorParts extends MultiWindowParts<EditorPart, IEditorPartsMement
//#region Helpers
protected override getPartByDocument(document: Document): EditorPart {
if (this._parts.size > 1) {
const activeElement = getActiveElement();
// Find parts that match the document and check if any
// non-main part contains the active element. This handles
// modal parts that share the same document as the main part.
for (const part of this._parts) {
if (part !== this.mainPart && part.element?.ownerDocument === document) {
const container = part.getContainer();
if (container && isAncestor(activeElement, container)) {
return part;
}
}
}
}
return super.getPartByDocument(document);
}
override getPart(group: IEditorGroupView | GroupIdentifier): EditorPart;
override getPart(element: HTMLElement): EditorPart;
override getPart(groupOrElement: IEditorGroupView | GroupIdentifier | HTMLElement): EditorPart {
@@ -309,14 +374,13 @@ export class EditorParts extends MultiWindowParts<EditorPart, IEditorPartsMement
private createState(): IEditorPartsUIState {
return {
auxiliary: this.parts.filter(part => part !== this.mainPart).map(part => {
const auxiliaryWindow = this.auxiliaryWindowService.getWindow(part.windowId);
return {
auxiliary: this.parts
.map(part => ({ part, auxiliaryWindow: this.auxiliaryWindowService.getWindow(part.windowId) }))
.filter(({ auxiliaryWindow }) => auxiliaryWindow !== undefined)
.map(({ part, auxiliaryWindow }) => ({
state: part.createState(),
...auxiliaryWindow?.createState()
};
}),
...auxiliaryWindow!.createState()
})),
mru: this.mostRecentActiveParts.map(part => this.parts.indexOf(part))
};
}

View File

@@ -0,0 +1,82 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/** Modal Editor Part: Modal Block */
.monaco-modal-editor-block {
position: fixed;
height: 100%;
width: 100%;
left: 0;
top: 0;
/* z-index cannot be above iframes (50) to support showing them */
z-index: 40;
display: flex;
justify-content: center;
align-items: center;
}
.monaco-modal-editor-block.dimmed {
background: rgba(0, 0, 0, 0.3);
}
/** Modal Editor Part: Shadow Container */
.monaco-modal-editor-block .modal-editor-shadow {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
border-radius: 8px;
overflow: hidden;
}
/** Modal Editor Part: Editor Container */
.monaco-modal-editor-block .modal-editor-part {
display: flex;
flex-direction: column;
min-width: 400px;
min-height: 300px;
background-color: var(--vscode-editor-background);
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder));
border-radius: 8px;
overflow: hidden;
}
.monaco-modal-editor-block .modal-editor-part:focus {
outline: none;
}
/** Modal Editor Part: Header with title and close button */
.monaco-modal-editor-block .modal-editor-header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
height: 32px;
min-height: 32px;
padding: 0 8px;
background-color: var(--vscode-editorGroupHeader-tabsBackground);
border-bottom: 1px solid var(--vscode-editorGroupHeader-tabsBorder, transparent);
}
.monaco-modal-editor-block .modal-editor-title {
grid-column: 2;
font-size: 12px;
font-weight: 500;
color: var(--vscode-foreground);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.monaco-modal-editor-block .modal-editor-action-container {
grid-column: 3;
display: flex;
align-items: center;
justify-content: flex-end;
}
/** Modal Editor Part: Ensure proper sizing */
.monaco-modal-editor-block .modal-editor-part .content {
flex: 1;
position: relative;
overflow: hidden;
}

View File

@@ -0,0 +1,297 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import './media/modalEditorPart.css';
import { $, addDisposableListener, append, EventType } from '../../../../base/browser/dom.js';
import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { Action } from '../../../../base/common/actions.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { widgetClose } from '../../../../platform/theme/common/iconRegistry.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 { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { IEditorGroupView, IEditorPartsView } from './editor.js';
import { EditorPart } from './editorPart.js';
import { GroupDirection, GroupsOrder, IModalEditorPart } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { Verbosity } from '../../../common/editor.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { IWorkbenchLayoutService } from '../../../services/layout/browser/layoutService.js';
import { mainWindow } from '../../../../base/browser/window.js';
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { localize } from '../../../../nls.js';
export interface ICreateModalEditorPartResult {
readonly part: ModalEditorPartImpl;
readonly instantiationService: IInstantiationService;
readonly disposables: DisposableStore;
}
export class ModalEditorPart {
constructor(
private readonly editorPartsView: IEditorPartsView,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IEditorService private readonly editorService: IEditorService,
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
) {
}
async create(): Promise<ICreateModalEditorPartResult> {
const disposables = new DisposableStore();
// Create modal container
const modalElement = $('.monaco-modal-editor-block.dimmed');
modalElement.tabIndex = -1;
this.layoutService.mainContainer.appendChild(modalElement);
disposables.add(toDisposable(() => modalElement.remove()));
const shadowElement = modalElement.appendChild($('.modal-editor-shadow'));
// Create editor part container
const titleId = 'modal-editor-title';
const editorPartContainer = $('.part.editor.modal-editor-part', {
role: 'dialog',
'aria-modal': 'true',
'aria-labelledby': titleId
});
shadowElement.appendChild(editorPartContainer);
// Create header with title and close button
const headerElement = editorPartContainer.appendChild($('.modal-editor-header'));
// Title element (centered)
const titleElement = append(headerElement, $('div.modal-editor-title'));
titleElement.id = titleId;
titleElement.textContent = '';
// Action buttons using ActionBar for proper accessibility
const actionBarContainer = append(headerElement, $('div.modal-editor-action-container'));
const actionBar = disposables.add(new ActionBar(actionBarContainer));
// Open as Editor
const openAsEditorAction = disposables.add(new Action(
'modalEditorPart.openAsEditor',
localize('openAsEditor', "Open as Editor"),
ThemeIcon.asClassName(Codicon.openInProduct),
true,
async () => {
const activeEditor = editorPart.activeGroup.activeEditor;
if (activeEditor) {
await this.editorService.openEditor(activeEditor, { pinned: true, preserveFocus: false }, this.editorPartsView.mainPart.activeGroup.id);
editorPart.close();
}
}
));
actionBar.push(openAsEditorAction, { icon: true, label: false });
// Close action
const closeAction = disposables.add(new Action(
'modalEditorPart.close',
localize('close', "Close"),
ThemeIcon.asClassName(widgetClose),
true,
async () => editorPart.close()
));
actionBar.push(closeAction, { icon: true, label: false, keybinding: localize('escape', "Escape") });
// Create the editor part
const editorPart = disposables.add(this.instantiationService.createInstance(
ModalEditorPartImpl,
mainWindow.vscodeWindowId,
this.editorPartsView,
localize('modalEditorPart', "Modal Editor Area")
));
disposables.add(this.editorPartsView.registerPart(editorPart));
editorPart.create(editorPartContainer);
// Create scoped instantiation service
const modalEditorService = this.editorService.createScoped(editorPart, disposables);
const scopedInstantiationService = disposables.add(editorPart.scopedInstantiationService.createChild(new ServiceCollection(
[IEditorService, modalEditorService]
)));
// Update title when active editor changes
disposables.add(Event.runAndSubscribe(modalEditorService.onDidActiveEditorChange, (() => {
const activeEditor = editorPart.activeGroup.activeEditor;
titleElement.textContent = activeEditor?.getTitle(Verbosity.MEDIUM) ?? '';
})));
// Handle close on click outside (on the dimmed background)
disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => {
if (e.target === modalElement) {
editorPart.close();
}
}));
// Handle escape key to close
disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
if (event.keyCode === KeyCode.Escape) {
editorPart.close();
}
}));
// Handle close event from editor part
disposables.add(Event.once(editorPart.onWillClose)(() => {
disposables.dispose();
}));
// Layout the modal editor part
disposables.add(Event.runAndSubscribe(this.layoutService.onDidLayoutMainContainer, () => {
const containerDimension = this.layoutService.mainContainerDimension;
const width = Math.min(containerDimension.width * 0.8, 1200);
const height = Math.min(containerDimension.height * 0.8, 800);
editorPartContainer.style.width = `${width}px`;
editorPartContainer.style.height = `${height}px`;
const borderSize = 2; // Account for 1px border on all sides and modal header height
const headerHeight = 35;
editorPart.layout(width - borderSize, height - borderSize - headerHeight, 0, 0);
}));
// Focus the modal
editorPartContainer.focus();
return {
part: editorPart,
instantiationService: scopedInstantiationService,
disposables
};
}
}
class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
private static COUNTER = 1;
private readonly _onWillClose = this._register(new Emitter<void>());
readonly onWillClose = this._onWillClose.event;
private readonly optionsDisposable = this._register(new MutableDisposable());
constructor(
windowId: number,
editorPartsView: IEditorPartsView,
groupsLabel: string,
@IInstantiationService instantiationService: IInstantiationService,
@IThemeService themeService: IThemeService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService storageService: IStorageService,
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
@IHostService hostService: IHostService,
@IContextKeyService contextKeyService: IContextKeyService
) {
const id = ModalEditorPartImpl.COUNTER++;
super(editorPartsView, `workbench.parts.modalEditor.${id}`, groupsLabel, windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService);
// Enforce some editor part options for modal editors
this.optionsDisposable.value = this.enforcePartOptions({
showTabs: 'none',
closeEmptyGroups: true,
tabActionCloseVisibility: false,
editorActionsLocation: 'default',
tabHeight: 'default',
wrapTabs: false
});
}
override removeGroup(group: number | IEditorGroupView, preserveFocus?: boolean): void {
// Close modal when last group removed
const groupView = this.assertGroupView(group);
if (this.count === 1 && this.activeGroup === groupView) {
this.doRemoveLastGroup(preserveFocus);
}
// Otherwise delegate to parent implementation
else {
super.removeGroup(group, preserveFocus);
}
}
private doRemoveLastGroup(preserveFocus?: boolean): void {
const restoreFocus = !preserveFocus && this.shouldRestoreFocus(this.container);
// Activate next group
const mostRecentlyActiveGroups = this.editorPartsView.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
const nextActiveGroup = mostRecentlyActiveGroups[1]; // [0] will be the current group we are about to dispose
if (nextActiveGroup) {
nextActiveGroup.groupsView.activateGroup(nextActiveGroup);
if (restoreFocus) {
nextActiveGroup.focus();
}
}
this.doClose(false /* do not merge any confirming editors to main part */);
}
protected override saveState(): void {
return; // disabled, modal editor part state is not persisted
}
close(): boolean {
return this.doClose(true /* merge all confirming editors to main part */);
}
private doClose(mergeConfirmingEditorsToMainPart: boolean): boolean {
let result = true;
if (mergeConfirmingEditorsToMainPart) {
// First close all editors that are non-confirming
for (const group of this.groups) {
group.closeAllEditors({ excludeConfirming: true });
}
// Then merge remaining to main part
result = this.mergeGroupsToMainPart();
if (!result) {
return false; // Do not close when editors could not be merged back
}
}
this._onWillClose.fire();
return result;
}
private mergeGroupsToMainPart(): boolean {
if (!this.groups.some(group => group.count > 0)) {
return true; // skip if we have no editors opened
}
// Find the most recent group that is not locked
let targetGroup: IEditorGroupView | undefined = undefined;
for (const group of this.editorPartsView.mainPart.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) {
if (!group.isLocked) {
targetGroup = group;
break;
}
}
if (!targetGroup) {
targetGroup = this.editorPartsView.mainPart.addGroup(this.editorPartsView.mainPart.activeGroup, this.partOptions.openSideBySideDirection === 'right' ? GroupDirection.RIGHT : GroupDirection.DOWN);
}
const result = this.mergeAllGroups(targetGroup, {
// Try to reduce the impact of closing the modal
// as much as possible by not changing existing editors
// in the main window.
preserveExistingIndex: true
});
targetGroup.focus();
return result;
}
}

View File

@@ -349,6 +349,15 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'description': localize('revealIfOpen', "Controls whether an editor is revealed in any of the visible groups if opened. If disabled, an editor will prefer to open in the currently active editor group. If enabled, an already opened editor will be revealed instead of opened again in the currently active editor group. Note that there are some cases where this setting is ignored, such as when forcing an editor to open in a specific group or to the side of the currently active group."),
'default': false
},
'workbench.editor.allowOpenInModalEditor': {
'type': 'boolean',
'description': localize('allowOpenInModalEditor', "Controls whether editors can be opened in a modal overlay. When enabled, certain editors such as Settings and Keyboard Shortcuts may open in a centered modal overlay instead of as a regular editor tab."),
'default': product.quality !== 'stable', // TODO@bpasero figure out the default for stable
tags: ['experimental'],
experiment: {
mode: 'auto'
}
},
'workbench.editor.swipeToNavigate': {
'type': 'boolean',
'description': localize('swipeToNavigate', "Navigate between open files using three-finger swipe horizontally. Note that System Preferences > Trackpad > More Gestures > 'Swipe between pages' must be set to 'Swipe with two or three fingers'."),

View File

@@ -15,8 +15,7 @@ import { Registry } from '../../../../../platform/registry/common/platform.js';
import { IEditorPaneRegistry, EditorPaneDescriptor } from '../../../../browser/editor.js';
import { EditorExtensions, IEditorFactoryRegistry, IEditorSerializer } from '../../../../common/editor.js';
import { EditorInput } from '../../../../common/editor/editorInput.js';
import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';
import { IEditorService } from '../../../../services/editor/common/editorService.js';
import { IEditorService, MODAL_GROUP } from '../../../../services/editor/common/editorService.js';
import { ResourceContextKey } from '../../../../common/contextkeys.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { CONTEXT_MODELS_EDITOR, CONTEXT_MODELS_SEARCH_FOCUS, MANAGE_CHAT_COMMAND_ID } from '../../common/constants.js';
@@ -141,9 +140,9 @@ class ChatManagementActionsContribution extends Disposable implements IWorkbench
});
}
async run(accessor: ServicesAccessor, args: string | IOpenManageCopilotEditorActionOptions) {
const editorGroupsService = accessor.get(IEditorGroupsService);
const editorService = accessor.get(IEditorService);
args = sanitizeOpenManageCopilotEditorArgs(args);
return editorGroupsService.activeGroup.openEditor(new ModelsManagementEditorInput(), { pinned: true });
return editorService.openEditor(new ModelsManagementEditorInput(), { pinned: true }, MODAL_GROUP);
}
}));

View File

@@ -35,7 +35,7 @@ import { IConfigurationService } from '../../../../platform/configuration/common
import { IHostService } from '../../../services/host/browser/host.js';
import { URI } from '../../../../base/common/uri.js';
import { IExtension, ExtensionState, IExtensionsWorkbenchService, AutoUpdateConfigurationKey, AutoCheckUpdatesConfigurationKey, HasOutdatedExtensionsContext, AutoUpdateConfigurationValue, InstallExtensionOptions, ExtensionRuntimeState, ExtensionRuntimeActionType, AutoRestartConfigurationKey, VIEWLET_ID, IExtensionsViewPaneContainer, IExtensionsNotification } from '../common/extensions.js';
import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from '../../../services/editor/common/editorService.js';
import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js';
import { IURLService, IURLHandler, IOpenURLOptions } from '../../../../platform/url/common/url.js';
import { ExtensionsInput, IExtensionEditorOptions } from '../common/extensionsInput.js';
import { ILogService } from '../../../../platform/log/common/log.js';
@@ -1563,7 +1563,7 @@ export class ExtensionsWorkbenchService extends Disposable implements IExtension
if (!extension) {
throw new Error(`Extension not found. ${extension}`);
}
await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, options?.sideByside ? SIDE_GROUP : ACTIVE_GROUP);
await this.editorService.openEditor(this.instantiationService.createInstance(ExtensionsInput, extension), options, MODAL_GROUP);
}
async openSearch(searchValue: string, preserveFoucs?: boolean): Promise<void> {

View File

@@ -248,7 +248,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon
run(accessor: ServicesAccessor, args: string | IOpenSettingsActionOptions) {
// args takes a string for backcompat
const opts = typeof args === 'string' ? { query: args } : sanitizeOpenSettingsArgs(args);
return accessor.get(IPreferencesService).openSettings(opts);
return accessor.get(IPreferencesService).openSettings({ openInModal: true, ...opts });
}
}));
this._register(registerAction2(class extends Action2 {
@@ -846,7 +846,7 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon
run(accessor: ServicesAccessor, ...args: unknown[]) {
const query = typeof args[0] === 'string' ? args[0] : undefined;
const groupId = getEditorGroupFromArguments(accessor, args)?.id;
return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query, groupId });
return accessor.get(IPreferencesService).openGlobalKeybindingSettings(false, { query, groupId, openInModal: true });
}
}));
this._register(MenuRegistry.appendMenuItem(MenuId.MenubarPreferencesMenu, {

View File

@@ -27,6 +27,7 @@ import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor
import { UserDataProfilesEditor, UserDataProfilesEditorInput, UserDataProfilesEditorInputSerializer } from './userDataProfilesEditor.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { IUserDataProfilesEditor } from '../common/userDataProfile.js';
@@ -55,7 +56,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IWorkspaceTagsService private readonly workspaceTagsService: IWorkspaceTagsService,
@IContextKeyService contextKeyService: IContextKeyService,
@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,
@IEditorService private readonly editorService: IEditorService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILifecycleService private readonly lifecycleService: ILifecycleService,
@IURLService private readonly urlService: IURLService,
@@ -107,7 +108,7 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements
}
private async openProfilesEditor(): Promise<IUserDataProfilesEditor | undefined> {
const editor = await this.editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(this.instantiationService));
const editor = await this.editorService.openEditor(new UserDataProfilesEditorInput(this.instantiationService), undefined, MODAL_GROUP);
return editor as IUserDataProfilesEditor;
}
@@ -386,9 +387,9 @@ export class UserDataProfilesWorkbenchContribution extends Disposable implements
});
}
run(accessor: ServicesAccessor) {
const editorGroupsService = accessor.get(IEditorGroupsService);
const editorService = accessor.get(IEditorService);
const instantiationService = accessor.get(IInstantiationService);
return editorGroupsService.activeGroup.openEditor(new UserDataProfilesEditorInput(instantiationService));
return editorService.openEditor(new UserDataProfilesEditorInput(instantiationService), undefined, MODAL_GROUP);
}
}));
disposables.add(MenuRegistry.appendMenuItem(MenuId.CommandPalette, {

View File

@@ -17,7 +17,7 @@ import { IWorkspaceTrustEnablementService, IWorkspaceTrustManagementService, IWo
import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/contributions.js';
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IEditorService, MODAL_GROUP } from '../../../services/editor/common/editorService.js';
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, StatusbarAlignment } from '../../../services/statusbar/browser/statusbar.js';
@@ -747,7 +747,7 @@ registerAction2(class extends Action2 {
const input = instantiationService.createInstance(WorkspaceTrustEditorInput);
editorService.openEditor(input, { pinned: true });
editorService.openEditor(input, { pinned: true }, MODAL_GROUP);
return;
}
});

View File

@@ -6,7 +6,7 @@
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { GroupIdentifier } from '../../../common/editor.js';
import { IEditorGroupsService, GroupsOrder, IEditorGroup, preferredSideBySideGroupDirection } from './editorGroupsService.js';
import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP, SIDE_GROUP, SIDE_GROUP_TYPE } from './editorService.js';
import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP, MODAL_GROUP, SIDE_GROUP, SIDE_GROUP_TYPE } from './editorService.js';
/**
* A way to address editor groups through a column based system
@@ -16,7 +16,7 @@ import { ACTIVE_GROUP, ACTIVE_GROUP_TYPE, AUX_WINDOW_GROUP, SIDE_GROUP, SIDE_GRO
export type EditorGroupColumn = number;
export function columnToEditorGroup(editorGroupService: IEditorGroupsService, configurationService: IConfigurationService, column = ACTIVE_GROUP): GroupIdentifier | ACTIVE_GROUP_TYPE | SIDE_GROUP_TYPE {
if (column === ACTIVE_GROUP || column === SIDE_GROUP || column === AUX_WINDOW_GROUP) {
if (column === ACTIVE_GROUP || column === SIDE_GROUP || column === AUX_WINDOW_GROUP || column === MODAL_GROUP) {
return column; // return early for when column is well known
}

View File

@@ -9,19 +9,19 @@ import { ServicesAccessor } from '../../../../platform/instantiation/common/inst
import { EditorInputWithOptions, isEditorInputWithOptions, IUntypedEditorInput, isEditorInput, EditorInputCapabilities } from '../../../common/editor.js';
import { EditorInput } from '../../../common/editor/editorInput.js';
import { IEditorGroup, GroupsOrder, preferredSideBySideGroupDirection, IEditorGroupsService } from './editorGroupsService.js';
import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, PreferredGroup, SIDE_GROUP } from './editorService.js';
import { AUX_WINDOW_GROUP, AUX_WINDOW_GROUP_TYPE, MODAL_GROUP, MODAL_GROUP_TYPE, PreferredGroup, SIDE_GROUP } from './editorService.js';
/**
* Finds the target `IEditorGroup` given the instructions provided
* that is best for the editor and matches the preferred group if
* possible.
*/
export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: AUX_WINDOW_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: Exclude<PreferredGroup, AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE> | undefined): [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE): Promise<[IEditorGroup, EditorActivation | undefined]>;
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): Promise<[IEditorGroup, EditorActivation | undefined]> | [IEditorGroup, EditorActivation | undefined];
export function findGroup(accessor: ServicesAccessor, editor: EditorInputWithOptions | IUntypedEditorInput, preferredGroup: PreferredGroup | undefined): Promise<[IEditorGroup, EditorActivation | undefined]> | [IEditorGroup, EditorActivation | undefined] {
const editorGroupService = accessor.get(IEditorGroupsService);
@@ -99,6 +99,12 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer
}).then(group => group.activeGroup);
}
// Group: Modal (gated behind a setting)
else if (preferredGroup === MODAL_GROUP && configurationService.getValue<boolean>('workbench.editor.allowOpenInModalEditor')) {
group = editorGroupService.createModalEditorPart()
.then(part => part.activeGroup);
}
// Group: Unspecified without a specific index to open
else if (!options || typeof options.index !== 'number') {
const groupsByLastActive = editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);

View File

@@ -502,6 +502,22 @@ export interface IAuxiliaryEditorPart extends IEditorPart {
close(): boolean;
}
export interface IModalEditorPart extends IEditorPart {
/**
* Fired when this modal editor part is about to close.
*/
readonly onWillClose: Event<void>;
/**
* Close this modal editor part after moving all
* editors of all groups back to the main editor part.
*
* @returns `false` if an editor could not be moved back.
*/
close(): boolean;
}
export interface IEditorWorkingSet {
readonly id: string;
readonly name: string;
@@ -567,6 +583,15 @@ export interface IEditorGroupsService extends IEditorGroupsContainer {
*/
createAuxiliaryEditorPart(options?: { bounds?: Partial<IRectangle>; compact?: boolean; alwaysOnTop?: boolean }): Promise<IAuxiliaryEditorPart>;
/**
* Creates a modal editor part that shows in a modal overlay
* on top of the main workbench window.
*
* If a modal part already exists, it will be returned
* instead of creating a new one.
*/
createModalEditorPart(): Promise<IModalEditorPart>;
/**
* Returns the instantiation service that is scoped to the
* provided editor part. Use this method when building UI

View File

@@ -34,7 +34,13 @@ export type SIDE_GROUP_TYPE = typeof SIDE_GROUP;
export const AUX_WINDOW_GROUP = -3;
export type AUX_WINDOW_GROUP_TYPE = typeof AUX_WINDOW_GROUP;
export type PreferredGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE;
/**
* Open an editor in a modal overlay on top of the workbench.
*/
export const MODAL_GROUP = -4;
export type MODAL_GROUP_TYPE = typeof MODAL_GROUP;
export type PreferredGroup = IEditorGroup | GroupIdentifier | SIDE_GROUP_TYPE | ACTIVE_GROUP_TYPE | AUX_WINDOW_GROUP_TYPE | MODAL_GROUP_TYPE;
export function isPreferredGroup(obj: unknown): obj is PreferredGroup {
const candidate = obj as PreferredGroup | undefined;

View File

@@ -0,0 +1,376 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { workbenchInstantiationService, registerTestEditor, TestFileEditorInput, createEditorParts } from '../../../../test/browser/workbenchTestServices.js';
import { GroupsOrder, IEditorGroupsService } from '../../common/editorGroupsService.js';
import { EditorExtensions, IEditorFactoryRegistry } from '../../../../common/editor.js';
import { URI } from '../../../../../base/common/uri.js';
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
import { MockScopableContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
import { SideBySideEditorInput } from '../../../../common/editor/sideBySideEditorInput.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { Registry } from '../../../../../platform/registry/common/platform.js';
import { MODAL_GROUP, MODAL_GROUP_TYPE } from '../../common/editorService.js';
suite('Modal Editor Group', () => {
const TEST_EDITOR_ID = 'MyFileEditorForModalEditorGroup';
const TEST_EDITOR_INPUT_ID = 'testEditorInputForModalEditorGroup';
const disposables = new DisposableStore();
setup(() => {
disposables.add(registerTestEditor(TEST_EDITOR_ID, [new SyncDescriptor(TestFileEditorInput), new SyncDescriptor(SideBySideEditorInput)], TEST_EDITOR_INPUT_ID));
});
teardown(() => {
disposables.clear();
});
function createTestFileEditorInput(resource: URI, typeId: string): TestFileEditorInput {
return disposables.add(new TestFileEditorInput(resource, typeId));
}
test('MODAL_GROUP constant is defined correctly', () => {
assert.strictEqual(MODAL_GROUP, -4);
assert.strictEqual(typeof MODAL_GROUP, 'number');
});
test('MODAL_GROUP_TYPE type exists', () => {
const modalGroupValue: MODAL_GROUP_TYPE = MODAL_GROUP;
assert.strictEqual(modalGroupValue, -4);
});
test('createModalEditorPart creates a modal editor part', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
assert.ok(modalPart);
assert.ok(modalPart.activeGroup);
assert.strictEqual(typeof modalPart.close, 'function');
modalPart.close();
});
test('modal editor part has correct initial state', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
// Modal part should have exactly one group initially with 0 editors
assert.strictEqual(modalPart.activeGroup.count, 0);
modalPart.close();
});
test('modal editor part can open editors', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
assert.strictEqual(modalPart.activeGroup.count, 1);
assert.strictEqual(modalPart.activeGroup.activeEditor, input);
modalPart.close();
});
test('modal editor part is added to parts list', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const initialGroupCount = parts.groups.length;
const modalPart = await parts.createModalEditorPart();
// Modal part's group should be added to the total groups
assert.strictEqual(parts.groups.length, initialGroupCount + 1);
modalPart.close();
});
test('closing modal part fires onWillClose event', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
// Verify onWillClose is an event that can be listened to
assert.ok(typeof modalPart.onWillClose === 'function');
assert.ok(modalPart.onWillClose !== undefined);
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
// Verify close returns true
const result = modalPart.close();
assert.strictEqual(result, true);
});
test('modal editor part close returns true when no confirming editors', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
const result = modalPart.close();
assert.strictEqual(result, true);
});
test('modal editor part getGroups returns groups in correct order', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
// Modal part group should be in the groups list
const allGroups = parts.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
const modalGroup = modalPart.activeGroup;
assert.ok(allGroups.some(g => g.id === modalGroup.id));
modalPart.close();
});
test('modal editor part is singleton - subsequent calls return same instance', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart1 = await parts.createModalEditorPart();
const modalPart2 = await parts.createModalEditorPart();
// Same instance should be returned
assert.ok(modalPart1);
assert.ok(modalPart2);
assert.strictEqual(modalPart1, modalPart2);
assert.strictEqual(modalPart1.activeGroup.id, modalPart2.activeGroup.id);
modalPart1.close();
});
test('modal editor part singleton is reset after close', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
// Create first modal
const modalPart1 = await parts.createModalEditorPart();
const firstGroupId = modalPart1.activeGroup.id;
// Close it
modalPart1.close();
// Create another modal - should be a new instance
const modalPart2 = await parts.createModalEditorPart();
// Should be a different group
assert.notStrictEqual(modalPart2.activeGroup.id, firstGroupId);
modalPart2.close();
});
test('modal editor part onDidAddGroup fires only once for singleton', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
let addGroupCount = 0;
disposables.add(parts.onDidAddGroup(() => {
addGroupCount++;
}));
// Create modal twice
await parts.createModalEditorPart();
await parts.createModalEditorPart();
// onDidAddGroup should fire only once since it's a singleton
assert.strictEqual(addGroupCount, 1);
(await parts.createModalEditorPart()).close();
});
test('modal editor part enforces no tabs mode', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
// Modal parts should enforce no tabs mode
assert.strictEqual(modalPart.partOptions.showTabs, 'none');
modalPart.close();
});
test('modal editor part enforces closeEmptyGroups', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
// Modal parts should enforce closeEmptyGroups
assert.strictEqual(modalPart.partOptions.closeEmptyGroups, true);
modalPart.close();
});
test('closing all editors in modal removes the modal group', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
const modalGroupId = modalPart.activeGroup.id;
// The modal group should exist in parts
assert.ok(parts.getGroup(modalGroupId));
// Closing the last editor in the last group should close the modal
// which removes the group from parts
await modalPart.activeGroup.closeAllEditors();
// The modal group should no longer exist in parts
assert.strictEqual(parts.getGroup(modalGroupId), undefined);
});
test('modal editor part does not persist state', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
// Modal part should have saveState as a no-op (we can't directly test this,
// but we verify the modal was created successfully which means state handling works)
assert.ok(modalPart.activeGroup);
modalPart.close();
});
test('activePart returns modal when focused', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const input = createTestFileEditorInput(URI.file('foo/bar'), TEST_EDITOR_INPUT_ID);
await modalPart.activeGroup.openEditor(input, { pinned: true });
// Focus the modal group
modalPart.activeGroup.focus();
// The modal group should be included in the groups
const groups = parts.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE);
assert.ok(groups.some(g => g.id === modalPart.activeGroup.id));
modalPart.close();
});
test('modal part group can be found by id', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const modalGroup = modalPart.activeGroup;
const foundGroup = parts.getGroup(modalGroup.id);
assert.ok(foundGroup);
assert.strictEqual(foundGroup!.id, modalGroup.id);
modalPart.close();
});
test('onDidAddGroup fires when modal is created', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
let addedGroupId: number | undefined;
disposables.add(parts.onDidAddGroup(group => {
addedGroupId = group.id;
}));
const modalPart = await parts.createModalEditorPart();
assert.ok(addedGroupId !== undefined);
assert.strictEqual(addedGroupId, modalPart.activeGroup.id);
modalPart.close();
});
test('onDidRemoveGroup fires when modal is closed', async () => {
const instantiationService = workbenchInstantiationService({ contextKeyService: instantiationService => instantiationService.createInstance(MockScopableContextKeyService) }, disposables);
instantiationService.invokeFunction(accessor => Registry.as<IEditorFactoryRegistry>(EditorExtensions.EditorFactory).start(accessor));
const parts = await createEditorParts(instantiationService, disposables);
instantiationService.stub(IEditorGroupsService, parts);
const modalPart = await parts.createModalEditorPart();
const modalGroupId = modalPart.activeGroup.id;
let removedGroupId: number | undefined;
disposables.add(parts.onDidRemoveGroup(group => {
removedGroupId = group.id;
}));
modalPart.close();
assert.ok(removedGroupId !== undefined);
assert.strictEqual(removedGroupId, modalGroupId);
});
ensureNoDisposablesAreLeakedInTestSuite();
});

View File

@@ -29,8 +29,8 @@ import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from '../../../common/editor.
import { EditorInput } from '../../../common/editor/editorInput.js';
import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js';
import { IJSONEditingService } from '../../configuration/common/jsonEditing.js';
import { GroupDirection, IEditorGroup, IEditorGroupsService } from '../../editor/common/editorGroupsService.js';
import { IEditorService, SIDE_GROUP } from '../../editor/common/editorService.js';
import { GroupDirection, IEditorGroupsService } from '../../editor/common/editorGroupsService.js';
import { ACTIVE_GROUP, IEditorService, MODAL_GROUP, PreferredGroup, SIDE_GROUP } from '../../editor/common/editorService.js';
import { KeybindingsEditorInput } from './keybindingsEditorInput.js';
import { DEFAULT_SETTINGS_EDITOR_SETTING, FOLDER_SETTINGS_PATH, IKeybindingsEditorPane, IOpenKeybindingsEditorOptions, IOpenSettingsOptions, IPreferencesEditorModel, IPreferencesService, ISetting, ISettingsEditorOptions, ISettingsGroup, SETTINGS_AUTHORITY, USE_SPLIT_JSON_SETTING, validateSettingsEditorOptions } from '../common/preferences.js';
import { PreferencesEditorInput, SettingsEditor2Input } from '../common/preferencesEditorInput.js';
@@ -48,7 +48,6 @@ import { IURLService } from '../../../../platform/url/common/url.js';
import { compareIgnoreCase } from '../../../../base/common/strings.js';
import { IExtensionService } from '../../extensions/common/extensions.js';
import { IProgressService, ProgressLocation } from '../../../../platform/progress/common/progress.js';
import { findGroup } from '../../editor/common/editorGroupFinder.js';
const emptyEditableSettingsContent = '{\n}';
@@ -215,7 +214,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
async openPreferences(): Promise<void> {
await this.editorGroupService.activeGroup.openEditor(this.instantiationService.createInstance(PreferencesEditorInput));
await this.editorService.openEditor(this.instantiationService.createInstance(PreferencesEditorInput));
}
openSettings(options: IOpenSettingsOptions = {}): Promise<IEditorPane | undefined> {
@@ -274,8 +273,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
...options,
focusSearch: true
};
const group = await this.getEditorGroupFromOptions(options);
return group.openEditor(input, validateSettingsEditorOptions(options));
const group = this.getEditorGroupFromOptions(options);
return this.editorService.openEditor(input, validateSettingsEditorOptions(options), group);
}
openApplicationSettings(options: IOpenSettingsOptions = {}): Promise<IEditorPane | undefined> {
@@ -359,7 +358,8 @@ export class PreferencesService extends Disposable implements IPreferencesServic
}
} else {
const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options }, options.groupId)) as IKeybindingsEditorPane;
const group = this.getEditorGroupFromOptions(options);
const editor = (await this.editorService.openEditor(this.instantiationService.createInstance(KeybindingsEditorInput), { ...options }, group)) as IKeybindingsEditorPane;
if (options.query) {
editor.search(options.query);
}
@@ -371,16 +371,21 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return this.editorService.openEditor({ resource: this.defaultKeybindingsResource, label: nls.localize('defaultKeybindings', "Default Keybindings") });
}
private async getEditorGroupFromOptions(options: IOpenSettingsOptions): Promise<IEditorGroup> {
let group = options?.groupId !== undefined ? this.editorGroupService.getGroup(options.groupId) ?? this.editorGroupService.activeGroup : this.editorGroupService.activeGroup;
private getEditorGroupFromOptions(options: { groupId?: number; openInModal?: boolean; openToSide?: boolean }): PreferredGroup {
if (options.openToSide) {
group = (await this.instantiationService.invokeFunction(findGroup, {}, SIDE_GROUP))[0];
return SIDE_GROUP;
}
return group;
if (options.openInModal) {
return MODAL_GROUP;
}
if (options?.groupId !== undefined) {
return this.editorGroupService.getGroup(options.groupId) ?? this.editorGroupService.activeGroup;
}
return ACTIVE_GROUP;
}
private async openSettingsJson(resource: URI, options: IOpenSettingsOptions): Promise<IEditorPane | undefined> {
const group = await this.getEditorGroupFromOptions(options);
const group = this.getEditorGroupFromOptions(options);
const editor = await this.doOpenSettingsJson(resource, options, group);
if (editor && options?.revealSetting) {
await this.revealSetting(options.revealSetting.key, !!options.revealSetting.edit, editor, resource);
@@ -388,7 +393,7 @@ export class PreferencesService extends Disposable implements IPreferencesServic
return editor;
}
private async doOpenSettingsJson(resource: URI, options: ISettingsEditorOptions, group: IEditorGroup): Promise<IEditorPane | undefined> {
private async doOpenSettingsJson(resource: URI, options: ISettingsEditorOptions, group: PreferredGroup): Promise<IEditorPane | undefined> {
const openSplitJSON = !!this.configurationService.getValue(USE_SPLIT_JSON_SETTING);
const openDefaultSettings = !!this.configurationService.getValue(DEFAULT_SETTINGS_EDITOR_SETTING);
if (openSplitJSON || openDefaultSettings) {
@@ -398,15 +403,15 @@ export class PreferencesService extends Disposable implements IPreferencesServic
const configurationTarget = options?.target ?? ConfigurationTarget.USER;
const editableSettingsEditorInput = await this.getOrCreateEditableSettingsEditorInput(configurationTarget, resource);
options = { ...options, pinned: true };
return await group.openEditor(editableSettingsEditorInput, { ...validateSettingsEditorOptions(options) });
return await this.editorService.openEditor(editableSettingsEditorInput, { ...validateSettingsEditorOptions(options) }, group);
}
private async doOpenSplitJSON(resource: URI, options: ISettingsEditorOptions = {}, group: IEditorGroup,): Promise<IEditorPane | undefined> {
private async doOpenSplitJSON(resource: URI, options: ISettingsEditorOptions = {}, group: PreferredGroup,): Promise<IEditorPane | undefined> {
const configurationTarget = options.target ?? ConfigurationTarget.USER;
await this.createSettingsIfNotExists(configurationTarget, resource);
const preferencesEditorInput = this.createSplitJsonEditorInput(configurationTarget, resource);
options = { ...options, pinned: true };
return group.openEditor(preferencesEditorInput, validateSettingsEditorOptions(options));
return this.editorService.openEditor(preferencesEditorInput, validateSettingsEditorOptions(options), group);
}
public createSplitJsonEditorInput(configurationTarget: ConfigurationTarget, resource: URI): EditorInput {

View File

@@ -222,6 +222,7 @@ export interface ISettingsEditorOptions extends IEditorOptions {
export interface IOpenSettingsOptions extends ISettingsEditorOptions {
jsonEditor?: boolean;
openToSide?: boolean;
openInModal?: boolean;
groupId?: number;
}
@@ -245,6 +246,7 @@ export interface IKeybindingsEditorOptions extends IEditorOptions {
export interface IOpenKeybindingsEditorOptions extends IKeybindingsEditorOptions {
groupId?: number;
openInModal?: boolean;
}
export const IPreferencesService = createDecorator<IPreferencesService>('preferencesService');

View File

@@ -10,16 +10,16 @@ import { ICommandService } from '../../../../../platform/commands/common/command
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
import { IURLService } from '../../../../../platform/url/common/url.js';
import { DEFAULT_EDITOR_ASSOCIATION, IEditorPane } from '../../../../common/editor.js';
import { DEFAULT_EDITOR_ASSOCIATION, isEditorInput, IUntypedEditorInput } from '../../../../common/editor.js';
import { EditorInput } from '../../../../common/editor/editorInput.js';
import { IJSONEditingService } from '../../../configuration/common/jsonEditing.js';
import { TestJSONEditingService } from '../../../configuration/test/common/testServices.js';
import { IEditorService, PreferredGroup } from '../../../editor/common/editorService.js';
import { PreferencesService } from '../../browser/preferencesService.js';
import { IPreferencesService, ISettingsEditorOptions } from '../../common/preferences.js';
import { IRemoteAgentService } from '../../../remote/common/remoteAgentService.js';
import { TestRemoteAgentService, ITestInstantiationService, workbenchInstantiationService, TestEditorGroupView, TestEditorGroupsService } from '../../../../test/browser/workbenchTestServices.js';
import { IEditorGroupsService } from '../../../editor/common/editorGroupsService.js';
import { TestRemoteAgentService, ITestInstantiationService, workbenchInstantiationService, TestEditorService } from '../../../../test/browser/workbenchTestServices.js';
import { IEditorOptions } from '../../../../../platform/editor/common/editor.js';
import { SettingsEditor2Input } from '../../common/preferencesEditorInput.js';
suite('PreferencesService', () => {
let testInstantiationService: ITestInstantiationService;
@@ -30,17 +30,18 @@ suite('PreferencesService', () => {
setup(() => {
testInstantiationService = workbenchInstantiationService({}, disposables);
class TestOpenEditorGroupView extends TestEditorGroupView {
lastOpenEditorOptions: any;
override openEditor(_editor: SettingsEditor2Input, options?: IEditorOptions): Promise<IEditorPane> {
lastOpenEditorOptions = options;
_editor.dispose();
return Promise.resolve(undefined!);
class TestPreferencesEditorService extends TestEditorService {
override async openEditor(editor: EditorInput | IUntypedEditorInput, optionsOrGroup?: IEditorOptions | PreferredGroup, group?: PreferredGroup): Promise<undefined> {
lastOpenEditorOptions = optionsOrGroup as IEditorOptions;
// openEditor takes ownership of the input
if (isEditorInput(editor)) {
editor.dispose();
}
return undefined;
}
}
const testEditorGroupService = new TestEditorGroupsService([new TestOpenEditorGroupView(0)]);
testInstantiationService.stub(IEditorGroupsService, testEditorGroupService);
testInstantiationService.stub(IEditorService, disposables.add(new TestPreferencesEditorService()));
testInstantiationService.stub(IJSONEditingService, TestJSONEditingService);
testInstantiationService.stub(IRemoteAgentService, TestRemoteAgentService);
testInstantiationService.stub(ICommandService, TestCommandService);

View File

@@ -148,7 +148,7 @@ import { CodeEditorService } from '../../services/editor/browser/codeEditorServi
import { EditorPaneService } from '../../services/editor/browser/editorPaneService.js';
import { EditorResolverService } from '../../services/editor/browser/editorResolverService.js';
import { CustomEditorLabelService, ICustomEditorLabelService } from '../../services/editor/common/customEditorLabelService.js';
import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions } from '../../services/editor/common/editorGroupsService.js';
import { EditorGroupLayout, GroupDirection, GroupOrientation, GroupsArrangement, GroupsOrder, IAuxiliaryEditorPart, ICloseAllEditorsOptions, ICloseEditorOptions, ICloseEditorsFilter, IEditorDropTargetDelegate, IEditorGroup, IEditorGroupContextKeyProvider, IEditorGroupsContainer, IEditorGroupsService, IEditorPart, IEditorReplacement, IEditorWorkingSet, IEditorWorkingSetOptions, IFindGroupScope, IMergeGroupOptions, IModalEditorPart } from '../../services/editor/common/editorGroupsService.js';
import { IEditorPaneService } from '../../services/editor/common/editorPaneService.js';
import { IEditorResolverService } from '../../services/editor/common/editorResolverService.js';
import { IEditorsChangeEvent, IEditorService, IRevertAllEditorsOptions, ISaveEditorsOptions, ISaveEditorsResult, PreferredGroup } from '../../services/editor/common/editorService.js';
@@ -923,6 +923,7 @@ export class TestEditorGroupsService implements IEditorGroupsService {
readonly mainPart = this;
registerEditorPart(part: any): IDisposable { return Disposable.None; }
createAuxiliaryEditorPart(): Promise<IAuxiliaryEditorPart> { throw new Error('Method not implemented.'); }
createModalEditorPart(): Promise<IModalEditorPart> { throw new Error('Method not implemented.'); }
}
export class TestEditorGroupView implements IEditorGroupView {
@@ -1662,6 +1663,10 @@ export class TestEditorPart extends MainEditorPart implements IEditorGroupsServi
throw new Error('Method not implemented.');
}
createModalEditorPart(): Promise<IModalEditorPart> {
throw new Error('Method not implemented.');
}
getScopedInstantiationService(part: IEditorPart): IInstantiationService {
throw new Error('Method not implemented.');
}