mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 23:35:54 +01:00
modal - tweaks to editor and extensions handling (#295542)
This commit is contained in:
@@ -320,6 +320,41 @@ export interface IEditorOptions {
|
||||
*/
|
||||
alwaysOnTop?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options that only apply when `MODAL_GROUP` is used for opening.
|
||||
*/
|
||||
modal?: IModalEditorPartOptions;
|
||||
}
|
||||
|
||||
export interface IModalEditorPartOptions {
|
||||
|
||||
/**
|
||||
* The navigation context for navigating between items
|
||||
* within this modal editor. Pass `undefined` to clear.
|
||||
*/
|
||||
readonly navigation?: IModalEditorNavigation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for navigating between items within a modal editor.
|
||||
*/
|
||||
export interface IModalEditorNavigation {
|
||||
|
||||
/**
|
||||
* Total number of items in the navigation list.
|
||||
*/
|
||||
readonly total: number;
|
||||
|
||||
/**
|
||||
* Current 0-based index in the navigation list.
|
||||
*/
|
||||
readonly current: number;
|
||||
|
||||
/**
|
||||
* Navigate to the item at the given 0-based index.
|
||||
*/
|
||||
readonly navigate: (index: number) => void;
|
||||
}
|
||||
|
||||
export interface ITextEditorSelection {
|
||||
|
||||
@@ -27,7 +27,7 @@ import { ITelemetryService } from '../../../../platform/telemetry/common/telemet
|
||||
import { ActiveGroupEditorsByMostRecentlyUsedQuickAccess } from './editorQuickAccess.js';
|
||||
import { SideBySideEditor } from './sideBySideEditor.js';
|
||||
import { TextDiffEditor } from './textDiffEditor.js';
|
||||
import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, EditorPartModalContext, EditorPartModalMaximizedContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from '../../../common/contextkeys.js';
|
||||
import { ActiveEditorCanSplitInGroupContext, ActiveEditorGroupEmptyContext, ActiveEditorGroupLockedContext, ActiveEditorStickyContext, EditorPartModalContext, EditorPartModalMaximizedContext, EditorPartModalNavigationContext, MultipleEditorGroupsContext, SideBySideEditorActiveContext, TextCompareEditorActiveContext } from '../../../common/contextkeys.js';
|
||||
import { CloseDirection, EditorInputCapabilities, EditorsOrder, IResourceDiffEditorInput, IUntitledTextResourceEditorInput, isEditorInputWithOptionsAndGroup } from '../../../common/editor.js';
|
||||
import { EditorInput } from '../../../common/editor/editorInput.js';
|
||||
import { SideBySideEditorInput } from '../../../common/editor/sideBySideEditorInput.js';
|
||||
@@ -108,6 +108,8 @@ export const NEW_EMPTY_EDITOR_WINDOW_COMMAND_ID = 'workbench.action.newEmptyEdit
|
||||
export const CLOSE_MODAL_EDITOR_COMMAND_ID = 'workbench.action.closeModalEditor';
|
||||
export const MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID = 'workbench.action.moveModalEditorToMain';
|
||||
export const TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID = 'workbench.action.toggleModalEditorMaximized';
|
||||
export const NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID = 'workbench.action.navigateModalEditorPrevious';
|
||||
export const NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID = 'workbench.action.navigateModalEditorNext';
|
||||
|
||||
export const API_OPEN_EDITOR_COMMAND_ID = '_workbench.open';
|
||||
export const API_OPEN_DIFF_EDITOR_COMMAND_ID = '_workbench.diff';
|
||||
@@ -1500,6 +1502,64 @@ function registerModalEditorCommands(): void {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID,
|
||||
title: localize2('navigateModalEditorPrevious', 'Navigate to Previous Item in Modal Editor'),
|
||||
category: Categories.View,
|
||||
precondition: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyCode.UpArrow,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 10,
|
||||
when: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const editorGroupsService = accessor.get(IEditorGroupsService);
|
||||
|
||||
for (const part of editorGroupsService.parts) {
|
||||
if (isModalEditorPart(part)) {
|
||||
const nav = part.navigation;
|
||||
if (nav && nav.current > 0) {
|
||||
nav.navigate(nav.current - 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID,
|
||||
title: localize2('navigateModalEditorNext', 'Navigate to Next Item in Modal Editor'),
|
||||
category: Categories.View,
|
||||
precondition: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext),
|
||||
keybinding: {
|
||||
primary: KeyMod.Alt | KeyCode.DownArrow,
|
||||
weight: KeybindingWeight.WorkbenchContrib + 10,
|
||||
when: ContextKeyExpr.and(EditorPartModalContext, EditorPartModalNavigationContext)
|
||||
}
|
||||
});
|
||||
}
|
||||
run(accessor: ServicesAccessor): void {
|
||||
const editorGroupsService = accessor.get(IEditorGroupsService);
|
||||
|
||||
for (const part of editorGroupsService.parts) {
|
||||
if (isModalEditorPart(part)) {
|
||||
const nav = part.navigation;
|
||||
if (nav && nav.current < nav.total - 1) {
|
||||
nav.navigate(nav.current + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function isModalEditorPart(obj: unknown): obj is IModalEditorPart {
|
||||
@@ -1510,6 +1570,8 @@ function isModalEditorPart(obj: unknown): obj is IModalEditorPart {
|
||||
&& typeof part.onWillClose === 'function'
|
||||
&& typeof part.toggleMaximized === 'function'
|
||||
&& typeof part.maximized === 'boolean'
|
||||
&& typeof part.updateOptions === 'function'
|
||||
&& !!part.modalElement
|
||||
&& part.windowId === mainWindow.vscodeWindowId;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
|
||||
import { DeepPartial } from '../../../../base/common/types.js';
|
||||
import { IStatusbarService } from '../../../services/statusbar/browser/statusbar.js';
|
||||
import { mainWindow } from '../../../../base/browser/window.js';
|
||||
import { IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js';
|
||||
|
||||
interface IEditorPartsUIState {
|
||||
readonly auxiliary: IAuxiliaryEditorPartState[];
|
||||
@@ -157,14 +158,16 @@ export class EditorParts extends MultiWindowParts<EditorPart, IEditorPartsMement
|
||||
private modalEditorPart: IModalEditorPart | undefined;
|
||||
get activeModalEditorPart(): IModalEditorPart | undefined { return this.modalEditorPart; }
|
||||
|
||||
async createModalEditorPart(): Promise<IModalEditorPart> {
|
||||
async createModalEditorPart(options?: IModalEditorPartOptions): Promise<IModalEditorPart> {
|
||||
|
||||
// Reuse existing modal editor part if it exists
|
||||
if (this.modalEditorPart) {
|
||||
this.modalEditorPart.updateOptions(options);
|
||||
|
||||
return this.modalEditorPart;
|
||||
}
|
||||
|
||||
const { part, instantiationService, disposables } = await this.instantiationService.createInstance(ModalEditorPart, this).create();
|
||||
const { part, instantiationService, disposables } = await this.instantiationService.createInstance(ModalEditorPart, this).create(options);
|
||||
|
||||
// Keep instantiation service and reference to reuse
|
||||
this.modalEditorPart = part;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/** Modal Editor Part: Modal Block */
|
||||
/** Modal Background Block */
|
||||
.monaco-modal-editor-block {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
@@ -17,20 +17,16 @@
|
||||
align-items: center;
|
||||
/* Never allow content to escape above the title bar */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-modal-editor-block.dimmed {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
|
||||
.modal-editor-shadow {
|
||||
box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2));
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/** Modal Editor Part: Shadow Container */
|
||||
.monaco-modal-editor-block .modal-editor-shadow {
|
||||
box-shadow: 0 4px 32px var(--vscode-widget-shadow, rgba(0, 0, 0, 0.2));
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/** Modal Editor Part: Editor Container */
|
||||
/** Modal Editor Container */
|
||||
.monaco-modal-editor-block .modal-editor-part {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -40,13 +36,19 @@
|
||||
border: 1px solid var(--vscode-editorWidget-border, var(--vscode-contrastBorder));
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.monaco-modal-editor-block .modal-editor-part:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/** Modal Editor Part: Header with title and close button */
|
||||
/** Modal Editor Header */
|
||||
.monaco-modal-editor-block .modal-editor-header {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto 1fr;
|
||||
@@ -57,36 +59,75 @@
|
||||
color: var(--vscode-titleBar-activeForeground);
|
||||
background-color: var(--vscode-titleBar-activeBackground);
|
||||
border-bottom: 1px solid var(--vscode-titleBar-border, transparent);
|
||||
}
|
||||
|
||||
.monaco-modal-editor-block .modal-editor-title {
|
||||
grid-column: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-titleBar-activeForeground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Modal Editor Title */
|
||||
.modal-editor-title {
|
||||
grid-column: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--vscode-titleBar-activeForeground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-modal-editor-block .modal-editor-action-container {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
/* Modal Editor Navigation */
|
||||
.modal-editor-navigation {
|
||||
grid-column: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 22px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--vscode-commandCenter-border, transparent);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
|
||||
.monaco-modal-editor-block .modal-editor-action-container .actions-container {
|
||||
gap: 4px;
|
||||
}
|
||||
.modal-editor-nav-button.monaco-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 22px;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: none;
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.monaco-modal-editor-block .modal-editor-action-container .actions-container .codicon {
|
||||
color: inherit;
|
||||
}
|
||||
.modal-editor-nav-button.monaco-button:hover:not(.disabled) {
|
||||
opacity: 1;
|
||||
background-color: var(--vscode-commandCenter-activeBackground);
|
||||
}
|
||||
|
||||
/** Modal Editor Part: Ensure proper sizing */
|
||||
.monaco-modal-editor-block .modal-editor-part .content {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
.modal-editor-nav-button.monaco-button.disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.modal-editor-nav-label {
|
||||
font-size: 11px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.7;
|
||||
white-space: nowrap;
|
||||
padding: 0 6px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal Editor Actions */
|
||||
.modal-editor-action-container {
|
||||
grid-column: 3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
|
||||
.actions-container {
|
||||
gap: 4px;
|
||||
|
||||
.codicon {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import './media/modalEditorPart.css';
|
||||
import { $, addDisposableListener, append, EventHelper, EventType, isHTMLElement } from '../../../../base/browser/dom.js';
|
||||
import { $, addDisposableListener, append, EventHelper, EventType, hide, isHTMLElement, show } from '../../../../base/browser/dom.js';
|
||||
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
|
||||
import { Button } from '../../../../base/browser/ui/button/button.js';
|
||||
import { KeyCode } from '../../../../base/common/keyCodes.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
|
||||
@@ -23,13 +24,15 @@ import { IEditorGroupView, IEditorPartsView } from './editor.js';
|
||||
import { EditorPart } from './editorPart.js';
|
||||
import { GroupDirection, GroupsOrder, IModalEditorPart, GroupActivationReason } from '../../../services/editor/common/editorGroupsService.js';
|
||||
import { IEditorService } from '../../../services/editor/common/editorService.js';
|
||||
import { EditorPartModalContext, EditorPartModalMaximizedContext } from '../../../common/contextkeys.js';
|
||||
import { EditorPartModalContext, EditorPartModalMaximizedContext, EditorPartModalNavigationContext } from '../../../common/contextkeys.js';
|
||||
import { Verbosity } from '../../../common/editor.js';
|
||||
import { IHostService } from '../../../services/host/browser/host.js';
|
||||
import { IWorkbenchLayoutService, Parts } from '../../../services/layout/browser/layoutService.js';
|
||||
import { mainWindow } from '../../../../base/browser/window.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js';
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import { CLOSE_MODAL_EDITOR_COMMAND_ID, MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID, NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID, NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID, TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID } from './editorCommands.js';
|
||||
import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js';
|
||||
|
||||
const defaultModalEditorAllowableCommands = new Set([
|
||||
'workbench.action.quit',
|
||||
@@ -40,7 +43,9 @@ const defaultModalEditorAllowableCommands = new Set([
|
||||
'workbench.action.files.saveAll',
|
||||
CLOSE_MODAL_EDITOR_COMMAND_ID,
|
||||
MOVE_MODAL_EDITOR_TO_MAIN_COMMAND_ID,
|
||||
TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID
|
||||
TOGGLE_MODAL_EDITOR_MAXIMIZED_COMMAND_ID,
|
||||
NAVIGATE_MODAL_EDITOR_PREVIOUS_COMMAND_ID,
|
||||
NAVIGATE_MODAL_EDITOR_NEXT_COMMAND_ID,
|
||||
]);
|
||||
|
||||
export interface ICreateModalEditorPartResult {
|
||||
@@ -61,17 +66,50 @@ export class ModalEditorPart {
|
||||
) {
|
||||
}
|
||||
|
||||
async create(): Promise<ICreateModalEditorPartResult> {
|
||||
async create(options?: IModalEditorPartOptions): Promise<ICreateModalEditorPartResult> {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Create modal container
|
||||
const modalElement = $('.monaco-modal-editor-block.dimmed');
|
||||
// Modal container
|
||||
const modalElement = $('.monaco-modal-editor-block');
|
||||
this.layoutService.mainContainer.appendChild(modalElement);
|
||||
disposables.add(toDisposable(() => modalElement.remove()));
|
||||
|
||||
disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => {
|
||||
if (e.target === modalElement) {
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
// Guide focus back into the modal when clicking outside modal
|
||||
editorPartContainer.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
// Close on Escape
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
EventHelper.stop(event, true);
|
||||
|
||||
editorPart.close();
|
||||
}
|
||||
|
||||
// Prevent unsupported commands
|
||||
else {
|
||||
const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer);
|
||||
if (resolved.kind === ResultKind.KbFound && resolved.commandId) {
|
||||
if (
|
||||
resolved.commandId.startsWith('workbench.') &&
|
||||
!defaultModalEditorAllowableCommands.has(resolved.commandId)
|
||||
) {
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
const shadowElement = modalElement.appendChild($('.modal-editor-shadow'));
|
||||
|
||||
// Create editor part container
|
||||
// Editor part container
|
||||
const titleId = 'modal-editor-title';
|
||||
const editorPartContainer = $('.part.editor.modal-editor-part', {
|
||||
role: 'dialog',
|
||||
@@ -81,7 +119,7 @@ export class ModalEditorPart {
|
||||
});
|
||||
shadowElement.appendChild(editorPartContainer);
|
||||
|
||||
// Create header with title and close button
|
||||
// Header
|
||||
const headerElement = editorPartContainer.appendChild($('.modal-editor-header'));
|
||||
|
||||
// Title element
|
||||
@@ -89,7 +127,35 @@ export class ModalEditorPart {
|
||||
titleElement.id = titleId;
|
||||
titleElement.textContent = '';
|
||||
|
||||
// Action buttons
|
||||
// Navigation widget
|
||||
const navigationContainer = append(headerElement, $('div.modal-editor-navigation'));
|
||||
hide(navigationContainer);
|
||||
disposables.add(addDisposableListener(navigationContainer, EventType.DBLCLICK, e => EventHelper.stop(e, true)));
|
||||
|
||||
const previousButton = disposables.add(new Button(navigationContainer, { title: localize('previousItem', "Previous") }));
|
||||
previousButton.icon = Codicon.chevronLeft;
|
||||
previousButton.element.classList.add('modal-editor-nav-button');
|
||||
disposables.add(previousButton.onDidClick(() => {
|
||||
const navigation = editorPart.navigation;
|
||||
if (navigation && navigation.current > 0) {
|
||||
navigation.navigate(navigation.current - 1);
|
||||
}
|
||||
}));
|
||||
|
||||
const navigationLabel = append(navigationContainer, $('span.modal-editor-nav-label'));
|
||||
navigationLabel.setAttribute('aria-live', 'polite');
|
||||
|
||||
const nextButton = disposables.add(new Button(navigationContainer, { title: localize('nextItem', "Next") }));
|
||||
nextButton.icon = Codicon.chevronRight;
|
||||
nextButton.element.classList.add('modal-editor-nav-button');
|
||||
disposables.add(nextButton.onDidClick(() => {
|
||||
const navigation = editorPart.navigation;
|
||||
if (navigation && navigation.current < navigation.total - 1) {
|
||||
navigation.navigate(navigation.current + 1);
|
||||
}
|
||||
}));
|
||||
|
||||
// Toolbar
|
||||
const actionBarContainer = append(headerElement, $('div.modal-editor-action-container'));
|
||||
|
||||
// Create the editor part
|
||||
@@ -98,10 +164,23 @@ export class ModalEditorPart {
|
||||
mainWindow.vscodeWindowId,
|
||||
this.editorPartsView,
|
||||
modalElement,
|
||||
options,
|
||||
));
|
||||
disposables.add(this.editorPartsView.registerPart(editorPart));
|
||||
editorPart.create(editorPartContainer);
|
||||
|
||||
disposables.add(Event.once(editorPart.onWillClose)(() => disposables.dispose()));
|
||||
disposables.add(Event.runAndSubscribe(editorPart.onDidChangeNavigation, ((navigation: IModalEditorNavigation | undefined) => {
|
||||
if (navigation && navigation.total > 1) {
|
||||
show(navigationContainer);
|
||||
navigationLabel.textContent = localize('navigationCounter', "{0} of {1}", navigation.current + 1, navigation.total);
|
||||
previousButton.enabled = navigation.current > 0;
|
||||
nextButton.enabled = navigation.current < navigation.total - 1;
|
||||
} else {
|
||||
hide(navigationContainer);
|
||||
}
|
||||
}), editorPart.navigation));
|
||||
|
||||
// Create scoped instantiation service
|
||||
const modalEditorService = this.editorService.createScoped(editorPart, disposables);
|
||||
const scopedInstantiationService = disposables.add(editorPart.scopedInstantiationService.createChild(new ServiceCollection(
|
||||
@@ -132,44 +211,6 @@ export class ModalEditorPart {
|
||||
editorPart.toggleMaximized();
|
||||
}));
|
||||
|
||||
// Guide focus back into the modal when clicking outside modal
|
||||
disposables.add(addDisposableListener(modalElement, EventType.MOUSE_DOWN, e => {
|
||||
if (e.target === modalElement) {
|
||||
EventHelper.stop(e, true);
|
||||
|
||||
editorPartContainer.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
// Block certain workbench commands from being dispatched while the modal is open
|
||||
disposables.add(addDisposableListener(modalElement, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
// Close on Escape
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
EventHelper.stop(event, true);
|
||||
|
||||
editorPart.close();
|
||||
}
|
||||
|
||||
// Prevent unsupported commands
|
||||
else {
|
||||
const resolved = this.keybindingService.softDispatch(event, this.layoutService.mainContainer);
|
||||
if (resolved.kind === ResultKind.KbFound && resolved.commandId) {
|
||||
if (
|
||||
resolved.commandId.startsWith('workbench.') &&
|
||||
!defaultModalEditorAllowableCommands.has(resolved.commandId)
|
||||
) {
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Handle close event from editor part
|
||||
disposables.add(Event.once(editorPart.onWillClose)(() => {
|
||||
disposables.dispose();
|
||||
}));
|
||||
|
||||
// Layout the modal editor part
|
||||
const layoutModal = () => {
|
||||
@@ -231,9 +272,15 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
private readonly _onDidChangeMaximized = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeMaximized = this._onDidChangeMaximized.event;
|
||||
|
||||
private readonly _onDidChangeNavigation = this._register(new Emitter<IModalEditorNavigation | undefined>());
|
||||
readonly onDidChangeNavigation = this._onDidChangeNavigation.event;
|
||||
|
||||
private _maximized = false;
|
||||
get maximized(): boolean { return this._maximized; }
|
||||
|
||||
private _navigation: IModalEditorNavigation | undefined;
|
||||
get navigation(): IModalEditorNavigation | undefined { return this._navigation; }
|
||||
|
||||
private readonly optionsDisposable = this._register(new MutableDisposable());
|
||||
|
||||
private previousMainWindowActiveElement: Element | null = null;
|
||||
@@ -241,7 +288,8 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
constructor(
|
||||
windowId: number,
|
||||
editorPartsView: IEditorPartsView,
|
||||
private readonly modalElement: HTMLElement,
|
||||
public readonly modalElement: HTMLElement,
|
||||
options: IModalEditorPartOptions | undefined,
|
||||
@IInstantiationService instantiationService: IInstantiationService,
|
||||
@IThemeService themeService: IThemeService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@@ -253,11 +301,9 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
const id = ModalEditorPartImpl.COUNTER++;
|
||||
super(editorPartsView, `workbench.parts.modalEditor.${id}`, localize('modalEditorPart', "Modal Editor Area"), windowId, instantiationService, themeService, configurationService, storageService, layoutService, hostService, contextKeyService);
|
||||
|
||||
this.enforceModalPartOptions();
|
||||
}
|
||||
this._navigation = options?.navigation;
|
||||
|
||||
getModalElement() {
|
||||
return this.modalElement;
|
||||
this.enforceModalPartOptions();
|
||||
}
|
||||
|
||||
override create(parent: HTMLElement, options?: object): void {
|
||||
@@ -270,6 +316,7 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
const editorCount = this.groups.reduce((count, group) => count + group.count, 0);
|
||||
this.optionsDisposable.value = this.enforcePartOptions({
|
||||
showTabs: editorCount > 1 ? 'multiple' : 'none',
|
||||
enablePreview: true,
|
||||
closeEmptyGroups: true,
|
||||
tabActionCloseVisibility: editorCount > 1,
|
||||
editorActionsLocation: 'default',
|
||||
@@ -283,6 +330,12 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
this.enforceModalPartOptions();
|
||||
}
|
||||
|
||||
updateOptions(options?: IModalEditorPartOptions): void {
|
||||
this._navigation = options?.navigation;
|
||||
|
||||
this._onDidChangeNavigation.fire(options?.navigation);
|
||||
}
|
||||
|
||||
toggleMaximized(): void {
|
||||
this._maximized = !this._maximized;
|
||||
|
||||
@@ -297,6 +350,10 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
isMaximizedContext.set(this._maximized);
|
||||
this._register(this.onDidChangeMaximized(maximized => isMaximizedContext.set(maximized)));
|
||||
|
||||
const hasNavigationContext = EditorPartModalNavigationContext.bindTo(this.scopedContextKeyService);
|
||||
hasNavigationContext.set(!!this._navigation && this._navigation.total > 1);
|
||||
this._register(this.onDidChangeNavigation(navigation => hasNavigationContext.set(!!navigation && navigation.total > 1)));
|
||||
|
||||
super.handleContextKeys();
|
||||
}
|
||||
|
||||
@@ -398,4 +455,10 @@ class ModalEditorPartImpl extends EditorPart implements IModalEditorPart {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._navigation = undefined; // ensure to free the reference to the navigation closure
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ export const EditorPartMaximizedEditorGroupContext = new RawContextKey<boolean>(
|
||||
|
||||
export const EditorPartModalContext = new RawContextKey<boolean>('editorPartModal', false, localize('editorPartModal', "Whether focus is in a modal editor part"));
|
||||
export const EditorPartModalMaximizedContext = new RawContextKey<boolean>('editorPartModalMaximized', false, localize('editorPartModalMaximized', "Whether the modal editor part is maximized"));
|
||||
export const EditorPartModalNavigationContext = new RawContextKey<boolean>('editorPartModalNavigation', false, localize('editorPartModalNavigation', "Whether the modal editor part has navigation context"));
|
||||
|
||||
// Editor Layout Context Keys
|
||||
export const EditorsVisibleContext = new RawContextKey<boolean>('editorIsOpen', false, localize('editorIsOpen', "Whether an editor is open"));
|
||||
|
||||
@@ -94,8 +94,8 @@ import { fromNow } from '../../../../base/common/date.js';
|
||||
|
||||
class NavBar extends Disposable {
|
||||
|
||||
private _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());
|
||||
get onChange(): Event<{ id: string | null; focus: boolean }> { return this._onChange.event; }
|
||||
private readonly _onChange = this._register(new Emitter<{ id: string | null; focus: boolean }>());
|
||||
readonly onChange = this._onChange.event;
|
||||
|
||||
private _currentId: string | null = null;
|
||||
get currentId(): string | null { return this._currentId; }
|
||||
@@ -142,6 +142,11 @@ class NavBar extends Disposable {
|
||||
this._onChange.fire({ id, focus: !!focus });
|
||||
this.actions.forEach(a => a.checked = a.id === id);
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this.clear();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
interface ILayoutParticipant {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { IDisposable, dispose, Disposable, DisposableStore, toDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { Action, ActionRunner, IAction, Separator } from '../../../../base/common/actions.js';
|
||||
import { IExtensionsWorkbenchService, IExtension, IExtensionsViewState } from '../common/extensions.js';
|
||||
import { Event } from '../../../../base/common/event.js';
|
||||
@@ -16,7 +16,8 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte
|
||||
import { registerThemingParticipant, IColorTheme, ICssStyleCollector } from '../../../../platform/theme/common/themeService.js';
|
||||
import { IAsyncDataSource, ITreeNode } from '../../../../base/browser/ui/tree/tree.js';
|
||||
import { IListVirtualDelegate, IListRenderer, IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
|
||||
import { CancellationToken } from '../../../../base/common/cancellation.js';
|
||||
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
|
||||
import { IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js';
|
||||
import { isNonEmptyArray } from '../../../../base/common/arrays.js';
|
||||
import { Delegate, Renderer } from './extensionsList.js';
|
||||
import { listFocusForeground, listFocusBackground, foreground, editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
|
||||
@@ -35,6 +36,8 @@ import { INotificationService } from '../../../../platform/notification/common/n
|
||||
import { getLocationBasedViewColors } from '../../../browser/parts/views/viewPane.js';
|
||||
import { DelayedPagedModel, IPagedModel } from '../../../../base/common/paging.js';
|
||||
import { ExtensionIconWidget } from './extensionsWidgets.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { isCancellationError } from '../../../../base/common/errors.js';
|
||||
|
||||
function getAriaLabelForExtension(extension: IExtension | null): string {
|
||||
if (!extension) {
|
||||
@@ -51,6 +54,8 @@ export class ExtensionsList extends Disposable {
|
||||
readonly list: WorkbenchPagedList<IExtension>;
|
||||
private readonly contextMenuActionRunner = this._register(new ActionRunner());
|
||||
|
||||
private readonly modalNavigationDisposable = this._register(new MutableDisposable());
|
||||
|
||||
constructor(
|
||||
parent: HTMLElement,
|
||||
viewId: string,
|
||||
@@ -63,6 +68,7 @@ export class ExtensionsList extends Disposable {
|
||||
@IContextMenuService private readonly contextMenuService: IContextMenuService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
super();
|
||||
this._register(this.contextMenuActionRunner.onDidRun(({ error }) => error && notificationService.error(error)));
|
||||
@@ -115,7 +121,17 @@ export class ExtensionsList extends Disposable {
|
||||
|
||||
private openExtension(extension: IExtension, options: { sideByside?: boolean; preserveFocus?: boolean; pinned?: boolean }): void {
|
||||
extension = this.extensionsWorkbenchService.local.filter(e => areSameExtensions(e.identifier, extension.identifier))[0] || extension;
|
||||
this.extensionsWorkbenchService.open(extension, options);
|
||||
this.extensionsWorkbenchService.open(extension, {
|
||||
...options,
|
||||
modal: options.sideByside ? undefined : buildModalNavigationForPagedList(
|
||||
extension,
|
||||
() => this.list.model,
|
||||
(extA, extB) => areSameExtensions(extA.identifier, extB.identifier),
|
||||
(ext, modal) => this.extensionsWorkbenchService.open(ext, { pinned: false, modal }),
|
||||
this.modalNavigationDisposable,
|
||||
this.logService
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private async onContextMenu(e: IListContextMenuEvent<IExtension>): Promise<void> {
|
||||
@@ -453,6 +469,80 @@ export async function getExtensions(extensions: string[], extensionsWorkbenchSer
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds modal navigation options for navigating items in a paged list model.
|
||||
*/
|
||||
export function buildModalNavigationForPagedList<T>(
|
||||
openedItem: T,
|
||||
getModel: () => IPagedModel<T> | undefined,
|
||||
isSame: (a: T, b: T) => boolean,
|
||||
openItem: (item: T, modal: IModalEditorPartOptions) => void,
|
||||
cancellationStore: MutableDisposable<IDisposable>,
|
||||
logService: ILogService
|
||||
): IModalEditorPartOptions | undefined {
|
||||
const model = getModel();
|
||||
if (!model) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const total = model.length;
|
||||
if (total <= 1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Find the index of the opened item in the list
|
||||
let current = -1;
|
||||
for (let i = 0; i < total; i++) {
|
||||
if (model.isResolved(i) && isSame(model.get(i), openedItem)) {
|
||||
current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (current === -1) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const openAtIndex = (index: number, item: T) => {
|
||||
const currentTotal = getModel()?.length ?? 0;
|
||||
openItem(item, { navigation: { total: currentTotal, current: index, navigate } });
|
||||
};
|
||||
|
||||
let cts: CancellationTokenSource | undefined;
|
||||
const navigate = (index: number) => {
|
||||
cts?.cancel();
|
||||
cts = cancellationStore.value = new CancellationTokenSource();
|
||||
const token = cts.token;
|
||||
|
||||
const currentModel = getModel();
|
||||
if (!currentModel || index < 0 || index >= currentModel.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fast path: item already resolved
|
||||
if (currentModel.isResolved(index)) {
|
||||
openAtIndex(index, currentModel.get(index));
|
||||
}
|
||||
|
||||
// Slow path: resolve the item first
|
||||
else {
|
||||
currentModel.resolve(index, token).then(item => {
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
openAtIndex(index, item);
|
||||
}, error => {
|
||||
if (!isCancellationError(error)) {
|
||||
logService.error(`Error while resolving item at index ${index} for modal navigation`, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return { navigation: { total, current, navigate } };
|
||||
}
|
||||
|
||||
registerThemingParticipant((theme: IColorTheme, collector: ICssStyleCollector) => {
|
||||
const focusBackground = theme.getColor(listFocusBackground);
|
||||
if (focusBackground) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
|
||||
import { IListContextMenuEvent } from '../../../../base/browser/ui/list/list.js';
|
||||
import { Emitter, Event } from '../../../../base/common/event.js';
|
||||
import { createMarkdownCommandLink, MarkdownString } from '../../../../base/common/htmlContent.js';
|
||||
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { combinedDisposable, Disposable, DisposableStore, dispose, IDisposable, isDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
|
||||
import { DelayedPagedModel, IPagedModel, PagedModel, IterativePagedModel } from '../../../../base/common/paging.js';
|
||||
import { localize, localize2 } from '../../../../nls.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
@@ -52,6 +52,8 @@ import { IMcpGalleryManifestService, McpGalleryManifestStatus } from '../../../.
|
||||
import { ProductQualityContext } from '../../../../platform/contextkey/common/contextkeys.js';
|
||||
import { SeverityIcon } from '../../../../base/browser/ui/severityIcon/severityIcon.js';
|
||||
import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { buildModalNavigationForPagedList } from '../../extensions/browser/extensionsViewer.js';
|
||||
|
||||
export interface McpServerListViewOptions {
|
||||
showWelcome?: boolean;
|
||||
@@ -81,6 +83,7 @@ export class McpServersListView extends AbstractExtensionsListView<IWorkbenchMcp
|
||||
mcpServersList: HTMLElement;
|
||||
} | undefined;
|
||||
private readonly contextMenuActionRunner = this._register(new ActionRunner());
|
||||
private readonly modalNavigationDisposable = this._register(new MutableDisposable());
|
||||
private input: IQueryResult | undefined;
|
||||
|
||||
constructor(
|
||||
@@ -100,6 +103,7 @@ export class McpServersListView extends AbstractExtensionsListView<IWorkbenchMcp
|
||||
@IMcpGalleryManifestService protected readonly mcpGalleryManifestService: IMcpGalleryManifestService,
|
||||
@IWorkbenchLayoutService private readonly layoutService: IWorkbenchLayoutService,
|
||||
@IMarkdownRendererService protected readonly markdownRendererService: IMarkdownRendererService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
) {
|
||||
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
|
||||
}
|
||||
@@ -161,7 +165,17 @@ export class McpServersListView extends AbstractExtensionsListView<IWorkbenchMcp
|
||||
openOnSingleClick: true,
|
||||
}) as WorkbenchPagedList<IWorkbenchMcpServer>);
|
||||
this._register(Event.debounce(Event.filter(this.list.onDidOpen, e => e.element !== null), (_, event) => event, 75, true)(options => {
|
||||
this.mcpWorkbenchService.open(options.element!, options.editorOptions);
|
||||
this.mcpWorkbenchService.open(options.element!, {
|
||||
...options.editorOptions,
|
||||
modal: options.sideBySide ? undefined : buildModalNavigationForPagedList(
|
||||
options.element!,
|
||||
() => this.list?.model,
|
||||
(serverA, serverB) => serverA.id === serverB.id,
|
||||
(server, modal) => this.mcpWorkbenchService.open(server, { pinned: false, modal }),
|
||||
this.modalNavigationDisposable,
|
||||
this.logService
|
||||
),
|
||||
});
|
||||
}));
|
||||
this._register(this.list.onContextMenu(e => this.onContextMenu(e), this));
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ export class OverlayWebview extends Disposable implements IOverlayWebview {
|
||||
|
||||
// Webviews cannot be reparented in the dom as it will destroy their contents.
|
||||
// Mount them to a high level node to avoid this depending on the active container.
|
||||
const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.getModalElement();
|
||||
const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.modalElement;
|
||||
let root: HTMLElement;
|
||||
if (isHTMLElement(modalEditorContainer)) {
|
||||
root = modalEditorContainer;
|
||||
|
||||
@@ -195,7 +195,7 @@ export class WebviewEditor extends EditorPane {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.getModalElement();
|
||||
const modalEditorContainer = this._editorGroupsService.activeModalEditorPart?.modalElement;
|
||||
let clippingContainer: HTMLElement | undefined;
|
||||
if (isHTMLElement(modalEditorContainer)) {
|
||||
clippingContainer = modalEditorContainer;
|
||||
|
||||
@@ -119,16 +119,13 @@ function doFindGroup(input: EditorInputWithOptions | IUntypedEditorInput, prefer
|
||||
|
||||
// Group: Aux Window
|
||||
else if (preferredGroup === AUX_WINDOW_GROUP) {
|
||||
group = editorGroupService.createAuxiliaryEditorPart({
|
||||
bounds: options?.auxiliary?.bounds,
|
||||
compact: options?.auxiliary?.compact,
|
||||
alwaysOnTop: options?.auxiliary?.alwaysOnTop
|
||||
}).then(group => group.activeGroup);
|
||||
group = editorGroupService.createAuxiliaryEditorPart(options?.auxiliary)
|
||||
.then(group => group.activeGroup);
|
||||
}
|
||||
|
||||
// Group: Modal (gated behind a setting)
|
||||
else if (preferredGroup === MODAL_GROUP && configurationService.getValue<boolean>('workbench.editor.allowOpenInModalEditor')) {
|
||||
group = editorGroupService.createModalEditorPart()
|
||||
group = editorGroupService.createModalEditorPart(options?.modal)
|
||||
.then(part => part.activeGroup);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Event } from '../../../../base/common/event.js';
|
||||
import { IInstantiationService, createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IEditorPane, GroupIdentifier, EditorInputWithOptions, CloseDirection, IEditorPartOptions, IEditorPartOptionsChangeEvent, EditorsOrder, IVisibleEditorPane, IEditorCloseEvent, IUntypedEditorInput, isEditorInput, IEditorWillMoveEvent, IMatchEditorOptions, IActiveEditorChangeEvent, IFindEditorOptions, IToolbarActions } from '../../../common/editor.js';
|
||||
import { EditorInput } from '../../../common/editor/editorInput.js';
|
||||
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
|
||||
import { IEditorOptions, IModalEditorNavigation, IModalEditorPartOptions } from '../../../../platform/editor/common/editor.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IDimension } from '../../../../editor/common/core/2d/dimension.js';
|
||||
import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
|
||||
@@ -524,9 +524,9 @@ export interface IAuxiliaryEditorPart extends IEditorPart {
|
||||
export interface IModalEditorPart extends IEditorPart {
|
||||
|
||||
/**
|
||||
* Fired when this modal editor part is about to close.
|
||||
* Modal container of the editor part.
|
||||
*/
|
||||
readonly onWillClose: Event<void>;
|
||||
readonly modalElement: unknown /* HTMLElement */;
|
||||
|
||||
/**
|
||||
* Whether the modal editor part is currently maximized.
|
||||
@@ -544,9 +544,19 @@ export interface IModalEditorPart extends IEditorPart {
|
||||
toggleMaximized(): void;
|
||||
|
||||
/**
|
||||
* Modal container of the editor part.
|
||||
* The current navigation context, if any.
|
||||
*/
|
||||
getModalElement(): unknown /* HTMLElement */;
|
||||
readonly navigation: IModalEditorNavigation | undefined;
|
||||
|
||||
/**
|
||||
* Update options for the modal editor part.
|
||||
*/
|
||||
updateOptions(options?: IModalEditorPartOptions): void;
|
||||
|
||||
/**
|
||||
* Fired when this modal editor part is about to close.
|
||||
*/
|
||||
readonly onWillClose: Event<void>;
|
||||
|
||||
/**
|
||||
* Close this modal editor part after moving all
|
||||
@@ -631,7 +641,7 @@ export interface IEditorGroupsService extends IEditorGroupsContainer {
|
||||
* If a modal part already exists, it will be returned
|
||||
* instead of creating a new one.
|
||||
*/
|
||||
createModalEditorPart(): Promise<IModalEditorPart>;
|
||||
createModalEditorPart(options?: IModalEditorPartOptions): Promise<IModalEditorPart>;
|
||||
|
||||
/**
|
||||
* The currently active modal editor part, if any.
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 { Emitter } from '../../../../../base/common/event.js';
|
||||
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { IModalEditorNavigation, IModalEditorPartOptions } from '../../../../../platform/editor/common/editor.js';
|
||||
|
||||
/**
|
||||
* Simple test harness that mimics the ModalEditorPartImpl navigation behavior
|
||||
* without requiring the full editor part infrastructure.
|
||||
*/
|
||||
class TestModalEditorNavigationHost {
|
||||
|
||||
private readonly _onDidChangeNavigation = new Emitter<IModalEditorNavigation | undefined>();
|
||||
readonly onDidChangeNavigation = this._onDidChangeNavigation.event;
|
||||
|
||||
private _navigation: IModalEditorNavigation | undefined;
|
||||
get navigation(): IModalEditorNavigation | undefined { return this._navigation; }
|
||||
|
||||
updateOptions(options: IModalEditorPartOptions): void {
|
||||
this._navigation = options.navigation;
|
||||
this._onDidChangeNavigation.fire(options.navigation);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this._onDidChangeNavigation.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
suite('Modal Editor Navigation', () => {
|
||||
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
teardown(() => disposables.clear());
|
||||
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
test('updateOptions sets navigation and fires event', () => {
|
||||
const host = new TestModalEditorNavigationHost();
|
||||
disposables.add({ dispose: () => host.dispose() });
|
||||
|
||||
const events: (IModalEditorNavigation | undefined)[] = [];
|
||||
disposables.add(host.onDidChangeNavigation(ctx => events.push(ctx)));
|
||||
|
||||
const nav: IModalEditorNavigation = {
|
||||
total: 10,
|
||||
current: 3,
|
||||
navigate: () => { }
|
||||
};
|
||||
|
||||
host.updateOptions({ navigation: nav });
|
||||
|
||||
assert.strictEqual(host.navigation, nav);
|
||||
assert.deepStrictEqual(events, [nav]);
|
||||
});
|
||||
|
||||
test('updateOptions with undefined navigation clears navigation', () => {
|
||||
const host = new TestModalEditorNavigationHost();
|
||||
disposables.add({ dispose: () => host.dispose() });
|
||||
|
||||
const events: (IModalEditorNavigation | undefined)[] = [];
|
||||
disposables.add(host.onDidChangeNavigation(ctx => events.push(ctx)));
|
||||
|
||||
const nav: IModalEditorNavigation = {
|
||||
total: 5,
|
||||
current: 0,
|
||||
navigate: () => { }
|
||||
};
|
||||
|
||||
host.updateOptions({ navigation: nav });
|
||||
host.updateOptions({ navigation: undefined });
|
||||
|
||||
assert.strictEqual(host.navigation, undefined);
|
||||
assert.deepStrictEqual(events, [nav, undefined]);
|
||||
});
|
||||
|
||||
test('navigate callback updates context', () => {
|
||||
const host = new TestModalEditorNavigationHost();
|
||||
disposables.add({ dispose: () => host.dispose() });
|
||||
|
||||
const navigatedIndices: number[] = [];
|
||||
|
||||
const navigate = (index: number) => {
|
||||
navigatedIndices.push(index);
|
||||
// Simulates what real navigation does: update the context with new index
|
||||
host.updateOptions({ navigation: { total: 10, current: index, navigate } });
|
||||
};
|
||||
|
||||
host.updateOptions({ navigation: { total: 10, current: 0, navigate } });
|
||||
|
||||
// Navigate forward
|
||||
host.navigation!.navigate(1);
|
||||
assert.strictEqual(host.navigation!.current, 1);
|
||||
|
||||
host.navigation!.navigate(5);
|
||||
assert.strictEqual(host.navigation!.current, 5);
|
||||
|
||||
assert.deepStrictEqual(navigatedIndices, [1, 5]);
|
||||
});
|
||||
|
||||
test('navigation boundary conditions', () => {
|
||||
const host = new TestModalEditorNavigationHost();
|
||||
disposables.add({ dispose: () => host.dispose() });
|
||||
|
||||
const navigate = (index: number) => {
|
||||
if (index >= 0 && index < 3) {
|
||||
host.updateOptions({ navigation: { total: 3, current: index, navigate } });
|
||||
}
|
||||
};
|
||||
|
||||
host.updateOptions({ navigation: { total: 3, current: 0, navigate } });
|
||||
|
||||
// At first item
|
||||
assert.strictEqual(host.navigation!.current, 0);
|
||||
assert.ok(host.navigation!.current <= 0); // previous disabled
|
||||
|
||||
// Navigate to last
|
||||
host.navigation!.navigate(2);
|
||||
assert.strictEqual(host.navigation!.current, 2);
|
||||
assert.ok(host.navigation!.current >= host.navigation!.total - 1); // next disabled
|
||||
|
||||
// Navigate back to middle
|
||||
host.navigation!.navigate(1);
|
||||
assert.strictEqual(host.navigation!.current, 1);
|
||||
});
|
||||
|
||||
test('navigation context fires multiple events', () => {
|
||||
const host = new TestModalEditorNavigationHost();
|
||||
disposables.add({ dispose: () => host.dispose() });
|
||||
|
||||
let eventCount = 0;
|
||||
disposables.add(host.onDidChangeNavigation(() => eventCount++));
|
||||
|
||||
const navigate = (index: number) => {
|
||||
host.updateOptions({ navigation: { total: 5, current: index, navigate } });
|
||||
};
|
||||
|
||||
host.updateOptions({ navigation: { total: 5, current: 0, navigate } });
|
||||
host.navigation!.navigate(1);
|
||||
host.navigation!.navigate(2);
|
||||
host.updateOptions({ navigation: undefined });
|
||||
|
||||
assert.strictEqual(eventCount, 4); // initial + 2 navigates + clear
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user