Pull editable logic into separate service

This commit is contained in:
Daniel Imms
2025-09-28 22:20:10 +09:00
parent 66322d0f87
commit 4a8432f24f
6 changed files with 113 additions and 50 deletions
@@ -21,6 +21,7 @@ import { WorkbenchPhase, registerWorkbenchContribution2 } from '../../../common/
import { EditorExtensions, IEditorFactoryRegistry } from '../../../common/editor.js';
import { IViewContainersRegistry, IViewsRegistry, Extensions as ViewContainerExtensions, ViewContainerLocation } from '../../../common/views.js';
import { ITerminalProfileService, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js';
import { TerminalEditingService } from './terminalEditingService.js';
import { registerColors } from '../common/terminalColorRegistry.js';
import { registerTerminalConfiguration } from '../common/terminalConfiguration.js';
import { terminalStrings } from '../common/terminalStrings.js';
@@ -29,7 +30,7 @@ import './media/terminalVoice.css';
import './media/widgets.css';
import './media/xterm.css';
import { RemoteTerminalBackendContribution } from './remoteTerminalBackend.js';
import { ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from './terminal.js';
import { ITerminalConfigurationService, ITerminalEditingService, ITerminalEditorService, ITerminalGroupService, ITerminalInstanceService, ITerminalService, TerminalDataTransfers, terminalEditorId } from './terminal.js';
import { registerTerminalActions } from './terminalActions.js';
import { setupTerminalCommands } from './terminalCommands.js';
import { TerminalConfigurationService } from './terminalConfigurationService.js';
@@ -52,6 +53,7 @@ registerSingleton(ITerminalLogService, TerminalLogService, InstantiationType.Del
registerSingleton(ITerminalConfigurationService, TerminalConfigurationService, InstantiationType.Delayed);
registerSingleton(ITerminalService, TerminalService, InstantiationType.Delayed);
registerSingleton(ITerminalEditorService, TerminalEditorService, InstantiationType.Delayed);
registerSingleton(ITerminalEditingService, TerminalEditingService, InstantiationType.Delayed);
registerSingleton(ITerminalGroupService, TerminalGroupService, InstantiationType.Delayed);
registerSingleton(ITerminalInstanceService, TerminalInstanceService, InstantiationType.Delayed);
registerSingleton(ITerminalProfileService, TerminalProfileService, InstantiationType.Delayed);
@@ -38,6 +38,7 @@ import type { TerminalEditorInput } from './terminalEditorInput.js';
export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const ITerminalConfigurationService = createDecorator<ITerminalConfigurationService>('terminalConfigurationService');
export const ITerminalEditorService = createDecorator<ITerminalEditorService>('terminalEditorService');
export const ITerminalEditingService = createDecorator<ITerminalEditingService>('terminalEditingService');
export const ITerminalGroupService = createDecorator<ITerminalGroupService>('terminalGroupService');
export const ITerminalInstanceService = createDecorator<ITerminalInstanceService>('terminalInstanceService');
@@ -99,6 +100,48 @@ export interface ITerminalInstanceService {
didRegisterBackend(backend: ITerminalBackend): void;
}
/**
* A service responsible for managing terminal editing state and functionality. This includes
* tracking which terminal is currently being edited and managing editable data associated with
* terminal instances.
*/
export interface ITerminalEditingService {
readonly _serviceBrand: undefined;
/**
* Get the editable data for a terminal instance.
* @param instance The terminal instance.
* @returns The editable data if the instance is editable, undefined otherwise.
*/
getEditableData(instance: ITerminalInstance): IEditableData | undefined;
/**
* Set the editable data for a terminal instance.
* @param instance The terminal instance.
* @param data The editable data to set, or null to clear.
*/
setEditable(instance: ITerminalInstance, data: IEditableData | null): void;
/**
* Check if a terminal instance is currently editable.
* @param instance The terminal instance to check.
* @returns True if the instance is editable, false otherwise.
*/
isEditable(instance: ITerminalInstance | undefined): boolean;
/**
* Get the terminal instance that is currently being edited.
* @returns The terminal instance being edited, or undefined if none.
*/
getEditingTerminal(): ITerminalInstance | undefined;
/**
* Set the terminal instance that is currently being edited.
* @param instance The terminal instance to set as editing, or undefined to clear.
*/
setEditingTerminal(instance: ITerminalInstance | undefined): void;
}
export const enum Direction {
Left = 0,
Right = 1,
@@ -337,9 +380,6 @@ export interface ITerminalService extends ITerminalInstanceHost {
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined>;
isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean;
getEditableData(instance: ITerminalInstance): IEditableData | undefined;
setEditable(instance: ITerminalInstance, data: IEditableData | null): void;
isEditable(instance: ITerminalInstance | undefined): boolean;
safeDisposeTerminal(instance: ITerminalInstance): Promise<void>;
getDefaultInstanceHost(): ITerminalInstanceHost;
@@ -348,9 +388,6 @@ export interface ITerminalService extends ITerminalInstanceHost {
resolveLocation(location?: ITerminalLocationOptions): Promise<TerminalLocation | undefined>;
setNativeDelegate(nativeCalls: ITerminalServiceNativeDelegate): void;
getEditingTerminal(): ITerminalInstance | undefined;
setEditingTerminal(instance: ITerminalInstance | undefined): void;
/**
* Creates an instance event listener that listens to all instances, dynamically adding new
* instances and removing old instances as needed.
@@ -55,7 +55,7 @@ import { accessibleViewCurrentProviderId, accessibleViewIsShown, accessibleViewO
import { IRemoteTerminalAttachTarget, ITerminalProfileResolverService, ITerminalProfileService, TERMINAL_VIEW_ID, TerminalCommandId } from '../common/terminal.js';
import { TerminalContextKeys } from '../common/terminalContextKey.js';
import { terminalStrings } from '../common/terminalStrings.js';
import { Direction, ICreateTerminalOptions, IDetachedTerminalInstance, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService, IXtermTerminal } from './terminal.js';
import { Direction, ICreateTerminalOptions, IDetachedTerminalInstance, ITerminalConfigurationService, ITerminalEditorService, ITerminalEditingService, ITerminalGroupService, ITerminalInstance, ITerminalInstanceService, ITerminalService, IXtermTerminal } from './terminal.js';
import { isAuxiliaryWindow } from '../../../../base/browser/window.js';
import { InstanceContext } from './terminalContextMenu.js';
import { getColorClass, getIconId, getUriClasses } from './terminalIcon.js';
@@ -275,6 +275,7 @@ export interface ITerminalServicesCollection {
groupService: ITerminalGroupService;
instanceService: ITerminalInstanceService;
editorService: ITerminalEditorService;
editingService: ITerminalEditingService;
profileService: ITerminalProfileService;
profileResolverService: ITerminalProfileResolverService;
}
@@ -286,6 +287,7 @@ function getTerminalServices(accessor: ServicesAccessor): ITerminalServicesColle
groupService: accessor.get(ITerminalGroupService),
instanceService: accessor.get(ITerminalInstanceService),
editorService: accessor.get(ITerminalEditorService),
editingService: accessor.get(ITerminalEditingService),
profileService: accessor.get(ITerminalProfileService),
profileResolverService: accessor.get(ITerminalProfileResolverService)
};
@@ -787,13 +789,13 @@ export function registerTerminalActions() {
return renameWithQuickPick(c, accessor, firstInstance);
}
c.service.setEditingTerminal(firstInstance);
c.service.setEditable(firstInstance, {
c.editingService.setEditingTerminal(firstInstance);
c.editingService.setEditable(firstInstance, {
validationMessage: value => validateTerminalName(value),
onFinish: async (value, success) => {
// Cancel editing first as instance.rename will trigger a rerender automatically
c.service.setEditable(firstInstance, null);
c.service.setEditingTerminal(undefined);
c.editingService.setEditable(firstInstance, null);
c.editingService.setEditingTerminal(undefined);
if (success) {
const promises: Promise<void>[] = [];
for (const instance of instances) {
@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IEditableData } from '../../../common/views.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { ITerminalEditingService, ITerminalInstance } from './terminal.js';
import { TERMINAL_VIEW_ID } from '../common/terminal.js';
import { TerminalViewPane } from './terminalView.js';
export class TerminalEditingService implements ITerminalEditingService {
readonly _serviceBrand: undefined;
private _editable: { instance: ITerminalInstance; data: IEditableData } | undefined;
private _editingTerminal: ITerminalInstance | undefined;
constructor(
@IViewsService private readonly _viewsService: IViewsService
) {
}
getEditableData(instance: ITerminalInstance): IEditableData | undefined {
return this._editable && this._editable.instance === instance ? this._editable.data : undefined;
}
setEditable(instance: ITerminalInstance, data: IEditableData | null): void {
if (!data) {
this._editable = undefined;
} else {
this._editable = { instance: instance, data };
}
const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID);
const isEditing = this.isEditable(instance);
pane?.terminalTabbedView?.setEditable(isEditing);
}
isEditable(instance: ITerminalInstance | undefined): boolean {
return !!this._editable && (this._editable.instance === instance || !instance);
}
getEditingTerminal(): ITerminalInstance | undefined {
return this._editingTerminal;
}
setEditingTerminal(instance: ITerminalInstance | undefined): void {
this._editingTerminal = instance;
}
}
@@ -29,16 +29,14 @@ import { IThemeService, Themable } from '../../../../platform/theme/common/theme
import { ThemeIcon } from '../../../../base/common/themables.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { VirtualWorkspaceContext } from '../../../common/contextkeys.js';
import { IEditableData } from '../../../common/views.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { ICreateTerminalOptions, IDetachedTerminalInstance, IDetachedXTermOptions, IRequestAddInstanceToGroupEvent, ITerminalConfigurationService, ITerminalEditorService, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalLocationOptions, ITerminalService, ITerminalServiceNativeDelegate, TerminalConnectionState, TerminalEditorLocation } from './terminal.js';
import { getCwdForSplit } from './terminalActions.js';
import { TerminalEditorInput } from './terminalEditorInput.js';
import { getColorStyleContent, getUriClasses } from './terminalIcon.js';
import { TerminalProfileQuickpick } from './terminalProfileQuickpick.js';
import { getInstanceFromResource, getTerminalUri, parseTerminalUri } from './terminalUri.js';
import { TerminalViewPane } from './terminalView.js';
import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileService, TERMINAL_VIEW_ID } from '../common/terminal.js';
import { IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalProcessExtHostProxy, ITerminalProfileService } from '../common/terminal.js';
import { TerminalContextKeys } from '../common/terminalContextKey.js';
import { columnToEditorGroup } from '../../../services/editor/common/editorGroupColumn.js';
import { IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
@@ -80,8 +78,6 @@ export class TerminalService extends Disposable implements ITerminalService {
private _nativeDelegate?: ITerminalServiceNativeDelegate;
private _shutdownWindowCount?: number;
private _editable: { instance: ITerminalInstance; data: IEditableData } | undefined;
get isProcessSupportRegistered(): boolean { return !!this._processSupportContextKey.get(); }
private _connectionState: TerminalConnectionState = TerminalConnectionState.Connecting;
@@ -127,8 +123,6 @@ export class TerminalService extends Disposable implements ITerminalService {
return this._activeInstance;
}
private _editingTerminal: ITerminalInstance | undefined;
private readonly _onDidCreateInstance = this._register(new Emitter<ITerminalInstance>());
get onDidCreateInstance(): Event<ITerminalInstance> { return this._onDidCreateInstance.event; }
private readonly _onDidChangeInstanceDimensions = this._register(new Emitter<ITerminalInstance>());
@@ -175,7 +169,6 @@ export class TerminalService extends Disposable implements ITerminalService {
@IDialogService private _dialogService: IDialogService,
@IInstantiationService private _instantiationService: IInstantiationService,
@IRemoteAgentService private _remoteAgentService: IRemoteAgentService,
@IViewsService private _viewsService: IViewsService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalConfigurationService private readonly _terminalConfigService: ITerminalConfigurationService,
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
@@ -588,24 +581,7 @@ export class TerminalService extends Disposable implements ITerminalService {
await this.revealTerminal(instance, preserveFocus);
}
setEditable(instance: ITerminalInstance, data?: IEditableData | null): void {
if (!data) {
this._editable = undefined;
} else {
this._editable = { instance: instance, data };
}
const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID);
const isEditing = this.isEditable(instance);
pane?.terminalTabbedView?.setEditable(isEditing);
}
isEditable(instance: ITerminalInstance | undefined): boolean {
return !!this._editable && (this._editable.instance === instance || !instance);
}
getEditableData(instance: ITerminalInstance): IEditableData | undefined {
return this._editable && this._editable.instance === instance ? this._editable.data : undefined;
}
requestStartExtensionTerminal(proxy: ITerminalProcessExtHostProxy, cols: number, rows: number): Promise<ITerminalLaunchError | undefined> {
// The initial request came from the extension host, no need to wait for it
@@ -1229,13 +1205,7 @@ export class TerminalService extends Disposable implements ITerminalService {
this._terminalGroupService.setContainer(terminalContainer);
}
getEditingTerminal(): ITerminalInstance | undefined {
return this._editingTerminal;
}
setEditingTerminal(instance: ITerminalInstance | undefined) {
this._editingTerminal = instance;
}
createOnInstanceEvent<T>(getEvent: (instance: ITerminalInstance) => Event<T>): DynamicListEventMultiplexer<ITerminalInstance, T> {
return new DynamicListEventMultiplexer(this.instances, this.onDidCreateInstance, this.onDidDisposeInstance, getEvent);
@@ -10,7 +10,7 @@ import { IContextKey, IContextKeyService } from '../../../../platform/contextkey
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { ITerminalConfigurationService, ITerminalGroupService, ITerminalInstance, ITerminalService, TerminalDataTransfers } from './terminal.js';
import { ITerminalConfigurationService, ITerminalGroupService, ITerminalInstance, ITerminalService, ITerminalEditingService, TerminalDataTransfers } from './terminal.js';
import { localize } from '../../../../nls.js';
import * as DOM from '../../../../base/browser/dom.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
@@ -81,6 +81,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
@ITerminalEditingService private readonly _terminalEditingService: ITerminalEditingService,
@IInstantiationService instantiationService: IInstantiationService,
@IDecorationsService decorationsService: IDecorationsService,
@IThemeService private readonly _themeService: IThemeService,
@@ -154,7 +155,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
await instance.focusWhenReady();
}
if (this._terminalService.getEditingTerminal()?.instanceId === e.element?.instanceId) {
if (this._terminalEditingService.getEditingTerminal()?.instanceId === e.element?.instanceId) {
return;
}
@@ -166,7 +167,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
// on left click, if focus mode = single click, focus the element
// unless multi-selection is in progress
this.disposables.add(this.onMouseClick(async e => {
if (this._terminalService.getEditingTerminal()?.instanceId === e.element?.instanceId) {
if (this._terminalEditingService.getEditingTerminal()?.instanceId === e.element?.instanceId) {
return;
}
@@ -220,7 +221,7 @@ export class TerminalTabList extends WorkbenchList<ITerminalInstance> {
}
refresh(cancelEditing: boolean = true): void {
if (cancelEditing && this._terminalService.isEditable(undefined)) {
if (cancelEditing && this._terminalEditingService.isEditable(undefined)) {
this.domFocus();
}
@@ -258,6 +259,7 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer<ITerminal
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
@ITerminalEditingService private readonly _terminalEditingService: ITerminalEditingService,
@IHoverService private readonly _hoverService: IHoverService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IListService private readonly _listService: IListService,
@@ -405,7 +407,7 @@ class TerminalTabsRenderer extends Disposable implements IListRenderer<ITerminal
},
extraClasses
});
const editableData = this._terminalService.getEditableData(instance);
const editableData = this._terminalEditingService.getEditableData(instance);
template.label.element.classList.toggle('editable-tab', !!editableData);
if (editableData) {
template.elementDisposables.add(this._renderInputBox(template.label.element.querySelector('.monaco-icon-label-container')!, instance, editableData));
@@ -597,6 +599,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop<ITe
constructor(
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalGroupService private readonly _terminalGroupService: ITerminalGroupService,
@ITerminalEditingService private readonly _terminalEditingService: ITerminalEditingService,
@IListService private readonly _listService: IListService,
) {
super();
@@ -604,7 +607,7 @@ class TerminalTabsDragAndDrop extends Disposable implements IListDragAndDrop<ITe
}
getDragURI(instance: ITerminalInstance): string | null {
if (this._terminalService.getEditingTerminal()?.instanceId === instance.instanceId) {
if (this._terminalEditingService.getEditingTerminal()?.instanceId === instance.instanceId) {
return null;
}