dnd terminals bw windows in panel and fix moving from editor to panel (#128875)

This commit is contained in:
Megan Rogge 2021-07-20 17:16:41 -05:00 committed by GitHub
parent cfc48e81aa
commit a7c466d649
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 300 additions and 135 deletions

View file

@ -401,6 +401,11 @@ export interface ICreateTerminalOptions {
* Where to create the terminal, when not specified the default target will be used.
*/
target?: TerminalLocation;
/**
* The terminal's resource, passed when the terminal has moved windows.
*/
resource?: URI;
}
export interface ICreateContributedTerminalProfileOptions {

View file

@ -113,8 +113,20 @@ export function extractEditorsDropData(e: DragEvent, externalOnly?: boolean): Ar
// Invalid transfer
}
}
}
// Check for terminals transfer
const terminals = e.dataTransfer.getData(DataTransfers.TERMINALS);
if (terminals) {
try {
const terminalEditors: string[] = JSON.parse(terminals);
for (const terminalEditor of terminalEditors) {
editors.push({ resource: URI.parse(terminalEditor), isExternal: true });
}
} catch (error) {
// Invalid transfer
}
}
}
return editors;
}

View file

@ -19,7 +19,7 @@ import { ICompleteTerminalConfiguration } from 'vs/workbench/contrib/terminal/co
import { Orientation } from 'vs/base/browser/ui/splitview/splitview';
import { IEditableData } from 'vs/workbench/common/views';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { SerializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
export const ITerminalService = createDecorator<ITerminalService>('terminalService');
export const ITerminalEditorService = createDecorator<ITerminalEditorService>('terminalEditorService');
@ -54,7 +54,7 @@ export interface ITerminalInstanceService {
*/
preparePathForTerminalAsync(path: string, executable: string | undefined, title: string, shellType: TerminalShellType, isRemote: boolean): Promise<string>;
createInstance(launchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance;
createInstance(launchConfig: IShellLaunchConfig, target?: TerminalLocation, resource?: URI): ITerminalInstance;
}
export interface IBrowserTerminalConfigHelper extends ITerminalConfigHelper {
@ -140,7 +140,8 @@ export interface ITerminalService extends ITerminalInstanceHost {
*/
getInstanceFromId(terminalId: number): ITerminalInstance | undefined;
getInstanceFromIndex(terminalIndex: number): ITerminalInstance;
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined;
getActiveOrCreateInstance(): ITerminalInstance;
splitInstance(instance: ITerminalInstance, shell?: IShellLaunchConfig, cwd?: string | URI): ITerminalInstance | null;
splitInstance(instance: ITerminalInstance, profile: ITerminalProfile): ITerminalInstance | null;
@ -198,7 +199,7 @@ export interface ITerminalEditorService extends ITerminalInstanceHost, ITerminal
readonly instances: readonly ITerminalInstance[];
openEditor(instance: ITerminalInstance): Promise<void>;
getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput): TerminalEditorInput;
getOrCreateEditorInput(instance: ITerminalInstance | DeserializedTerminalEditorInput | URI): TerminalEditorInput;
detachActiveEditorInstance(): ITerminalInstance;
detachInstance(instance: ITerminalInstance): void;
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig?: IShellLaunchConfig): ITerminalInstance;
@ -271,6 +272,11 @@ export interface ITerminalInstanceHost {
readonly onDidChangeInstances: Event<void>;
setActiveInstance(instance: ITerminalInstance): void;
/**
* Gets an instance from a resource if it exists. This MUST be used instead of getInstanceFromId
* when you only know about a terminal's URI. (a URI's instance ID may not be this window's instance ID)
*/
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined;
}
export interface ITerminalFindHost {
@ -341,6 +347,8 @@ export interface ITerminalInstance {
* A unique URI for this terminal instance with the following encoding:
* path: /<workspace ID>/<instance ID>
* fragment: Title
* Note that when dragging terminals across windows, this will retain the original workspace ID /instance ID
* from the other window.
*/
readonly resource: URI;

View file

@ -19,9 +19,13 @@ import { ConfirmOnKill } from 'vs/workbench/contrib/terminal/common/terminal';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { Emitter } from 'vs/base/common/event';
export class TerminalEditorInput extends EditorInput {
protected readonly _onDidRequestAttach = this._register(new Emitter<ITerminalInstance>());
readonly onDidRequestAttach = this._onDidRequestAttach.event;
static readonly ID = 'workbench.editors.terminal';
private _isDetached = false;
@ -47,11 +51,26 @@ export class TerminalEditorInput extends EditorInput {
return TerminalEditor.ID;
}
setTerminalInstance(instance: ITerminalInstance): void {
if (this._terminalInstance) {
throw new Error('cannot set instance that has already been set');
}
this._terminalInstance = instance;
this._setupInstanceListeners();
// Refresh dirty state when the confirm on kill setting is changed
this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(TerminalSettingId.ConfirmOnKill)) {
this._onDidChangeDirty.fire();
}
});
}
override copy(): IEditorInput {
const instance = this._copyInstance || this._terminalInstanceService.createInstance({}, TerminalLocation.Editor);
instance.focusWhenReady();
this._copyInstance = undefined;
return this._instantiationService.createInstance(TerminalEditorInput, instance);
return this._instantiationService.createInstance(TerminalEditorInput, instance.resource, instance);
}
/**
@ -69,47 +88,27 @@ export class TerminalEditorInput extends EditorInput {
return this._isDetached ? undefined : this._terminalInstance;
}
get resource(): URI {
return this._terminalInstance.resource;
}
override isDirty(): boolean {
const confirmOnKill = this._configurationService.getValue<ConfirmOnKill>(TerminalSettingId.ConfirmOnKill);
if (confirmOnKill === 'editor' || confirmOnKill === 'always') {
return this._terminalInstance.hasChildProcesses;
return this._terminalInstance?.hasChildProcesses || false;
}
return false;
}
constructor(
private readonly _terminalInstance: ITerminalInstance,
public readonly resource: URI,
private _terminalInstance: ITerminalInstance | undefined,
@IThemeService private readonly _themeService: IThemeService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@ILifecycleService lifecycleService: ILifecycleService,
@IContextKeyService contextKeyService: IContextKeyService
@ILifecycleService private readonly _lifecycleService: ILifecycleService,
@IContextKeyService _contextKeyService: IContextKeyService
) {
super();
this._terminalEditorFocusContextKey = TerminalContextKeys.editorFocus.bindTo(contextKeyService);
this._register(toDisposable(() => {
if (!this._isDetached && !this._isShuttingDown) {
this._terminalInstance.dispose();
}
}));
const disposeListeners = [
this._terminalInstance.onExit(() => this.dispose()),
this._terminalInstance.onDisposed(() => this.dispose()),
this._terminalInstance.onTitleChanged(() => this._onDidChangeLabel.fire()),
this._terminalInstance.onIconChanged(() => this._onDidChangeLabel.fire()),
this._terminalInstance.onDidFocus(() => this._terminalEditorFocusContextKey.set(true)),
this._terminalInstance.onDidBlur(() => this._terminalEditorFocusContextKey.reset()),
this._terminalInstance.onDidChangeHasChildProcesses(() => this._onDidChangeDirty.fire()),
this._terminalInstance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeLabel.fire())
];
this._terminalEditorFocusContextKey = TerminalContextKeys.editorFocus.bindTo(_contextKeyService);
// Refresh dirty state when the confirm on kill setting is changed
this._configurationService.onDidChangeConfiguration(e => {
@ -117,20 +116,50 @@ export class TerminalEditorInput extends EditorInput {
this._onDidChangeDirty.fire();
}
});
if (_terminalInstance) {
this._setupInstanceListeners();
}
}
private _setupInstanceListeners(): void {
const instance = this._terminalInstance;
if (!instance) {
return;
}
this._register(toDisposable(() => {
if (!this._isDetached && !this._isShuttingDown) {
instance.dispose();
}
}));
const disposeListeners = [
instance.onExit(() => this.dispose()),
instance.onDisposed(() => this.dispose()),
instance.onTitleChanged(() => this._onDidChangeLabel.fire()),
instance.onIconChanged(() => this._onDidChangeLabel.fire()),
instance.onDidFocus(() => this._terminalEditorFocusContextKey.set(true)),
instance.onDidBlur(() => this._terminalEditorFocusContextKey.reset()),
instance.onDidChangeHasChildProcesses(() => this._onDidChangeDirty.fire()),
instance.statusList.onDidChangePrimaryStatus(() => this._onDidChangeLabel.fire())
];
// Don't dispose editor when instance is torn down on shutdown to avoid extra work and so
// the editor/tabs don't disappear
lifecycleService.onWillShutdown(() => {
this._lifecycleService.onWillShutdown(() => {
this._isShuttingDown = true;
dispose(disposeListeners);
});
}
override getName() {
return this._terminalInstance.title;
return this._terminalInstance?.title || this.resource.fragment;
}
override getLabelExtraClasses(): string[] {
if (!this._terminalInstance) {
return [];
}
const extraClasses: string[] = ['terminal-tab'];
const colorClass = getColorClass(this._terminalInstance);
if (colorClass) {
@ -152,7 +181,7 @@ export class TerminalEditorInput extends EditorInput {
*/
detachInstance() {
if (!this._isShuttingDown) {
this._terminalInstance.detachFromElement();
this._terminalInstance?.detachFromElement();
this._isDetached = true;
}
}

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { URI } from 'vs/base/common/uri';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { TerminalIcon, TitleEventSource } from 'vs/platform/terminal/common/terminal';
import { IEditorSerializer } from 'vs/workbench/common/editor';
@ -29,6 +30,7 @@ export class TerminalInputSerializer implements IEditorSerializer {
public deserialize(instantiationService: IInstantiationService, serializedEditorInput: string): EditorInput | undefined {
const terminalInstance = JSON.parse(serializedEditorInput);
terminalInstance.resource = URI.parse(terminalInstance.resource);
const editor = this._terminalEditorService.getOrCreateEditorInput(terminalInstance);
return editor;
}
@ -41,12 +43,13 @@ export class TerminalInputSerializer implements IEditorSerializer {
titleSource: instance.titleSource,
cwd: '',
icon: instance.icon,
color: instance.color
color: instance.color,
resource: instance.resource.toString()
};
}
}
export interface SerializedTerminalEditorInput {
interface TerminalEditorInputObject {
readonly id: number;
readonly pid: number;
readonly title: string;
@ -55,3 +58,11 @@ export interface SerializedTerminalEditorInput {
readonly icon: TerminalIcon | undefined;
readonly color: string | undefined;
}
export interface SerializedTerminalEditorInput extends TerminalEditorInputObject {
readonly resource: string
}
export interface DeserializedTerminalEditorInput extends TerminalEditorInputObject {
readonly resource: URI
}

View file

@ -5,17 +5,21 @@
import { Emitter } from 'vs/base/common/event';
import { Disposable, dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { EditorActivation } from 'vs/platform/editor/common/editor';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, optional } from 'vs/platform/instantiation/common/instantiation';
import { IShellLaunchConfig, TerminalLocation } from 'vs/platform/terminal/common/terminal';
import { IEditorInput, IEditorPane } from 'vs/workbench/common/editor';
import { ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteTerminalService, ITerminalEditorService, ITerminalInstance, ITerminalInstanceService } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor';
import { TerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorInput';
import { SerializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { DeserializedTerminalEditorInput } from 'vs/workbench/contrib/terminal/browser/terminalEditorSerializer';
import { parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
import { ILocalTerminalService, IOffProcessTerminalService } from 'vs/workbench/contrib/terminal/common/terminal';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle';
export class TerminalEditorService extends Disposable implements ITerminalEditorService {
@ -25,8 +29,10 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
private _activeInstanceIndex: number = -1;
private _isShuttingDown = false;
private _editorInputs: Map</*instanceId*/number, TerminalEditorInput> = new Map();
private _instanceDisposables: Map</*instanceId*/number, IDisposable[]> = new Map();
private _editorInputs: Map</*resource*/string, TerminalEditorInput> = new Map();
private _instanceDisposables: Map</*resource*/string, IDisposable[]> = new Map();
private readonly _primaryOffProcessTerminalService: IOffProcessTerminalService;
private readonly _onDidDisposeInstance = new Emitter<ITerminalInstance>();
readonly onDidDisposeInstance = this._onDidDisposeInstance.event;
@ -42,9 +48,13 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
@IEditorService private readonly _editorService: IEditorService,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ILifecycleService lifecycleService: ILifecycleService
@IRemoteTerminalService private readonly _remoteTerminalService: IRemoteTerminalService,
@ILifecycleService lifecycleService: ILifecycleService,
@IWorkbenchEnvironmentService environmentService: IWorkbenchEnvironmentService,
@optional(ILocalTerminalService) private readonly _localTerminalService: ILocalTerminalService
) {
super();
this._primaryOffProcessTerminalService = !!environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService);
this._register(toDisposable(() => {
for (const d of this._instanceDisposables.values()) {
dispose(d);
@ -65,7 +75,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
const terminalEditors = this._getActiveTerminalEditors();
const unknownEditor = terminalEditors.find(input => !knownIds.includes((input as any).terminalInstance.instanceId));
if (unknownEditor instanceof TerminalEditorInput && unknownEditor.terminalInstance) {
this._editorInputs.set(unknownEditor.terminalInstance.instanceId, unknownEditor);
this._editorInputs.set(unknownEditor.terminalInstance.resource.path, unknownEditor);
this.instances.push(unknownEditor.terminalInstance);
}
}));
@ -153,31 +163,72 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
input.setGroup(editorPane?.group);
}
getOrCreateEditorInput(instance: ITerminalInstance | SerializedTerminalEditorInput, isFutureSplit: boolean = false): TerminalEditorInput {
let cachedEditor;
if ('id' in instance) {
cachedEditor = this._editorInputs.get(instance.id);
} else if ('instanceId' in instance) {
cachedEditor = this._editorInputs.get(instance.instanceId);
}
getOrCreateEditorInput(instanceOrUri: ITerminalInstance | DeserializedTerminalEditorInput | URI, isFutureSplit: boolean = false): TerminalEditorInput {
const resource: URI = URI.isUri(instanceOrUri) ? instanceOrUri : instanceOrUri.resource;
const inputKey = resource.path;
const cachedEditor = this._editorInputs.get(inputKey);
if (cachedEditor) {
return cachedEditor;
}
if ('pid' in instance) {
instance = this._terminalInstanceService.createInstance({ attachPersistentProcess: instance }, TerminalLocation.Editor);
if ('pid' in instanceOrUri) {
instanceOrUri = this._terminalInstanceService.createInstance({ attachPersistentProcess: instanceOrUri }, TerminalLocation.Editor);
}
const input = this._instantiationService.createInstance(TerminalEditorInput, instance);
instance.target = TerminalLocation.Editor;
this._editorInputs.set(instance.instanceId, input);
this._instanceDisposables.set(instance.instanceId, [
instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance),
instance.onDidFocus(this._onDidFocusInstance.fire, this._onDidFocusInstance)
// Terminal from a different window
if (URI.isUri(instanceOrUri)) {
const terminalIdentifier = parseTerminalUri(instanceOrUri);
if (terminalIdentifier.instanceId) {
this._primaryOffProcessTerminalService.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId).then(attachPersistentProcess => {
const instance = this._terminalInstanceService.createInstance({ attachPersistentProcess }, TerminalLocation.Editor, resource);
input.setTerminalInstance(instance);
// trigger setInput on TerminalEditor setInput
// which attaches to the element and updates the input
this._editorService.openEditor(input, {
pinned: true,
forceReload: true
},
input.group
);
this._registerInstance(inputKey, input, instance);
});
}
}
let input: TerminalEditorInput;
if ('instanceId' in instanceOrUri) {
instanceOrUri.target = TerminalLocation.Editor;
input = this._instantiationService.createInstance(TerminalEditorInput, resource, instanceOrUri);
this._registerInstance(inputKey, input, instanceOrUri);
} else {
input = this._instantiationService.createInstance(TerminalEditorInput, instanceOrUri, undefined);
this._editorInputs.set(inputKey, input);
}
return input;
}
private _registerInstance(inputKey: string, input: TerminalEditorInput, instance: ITerminalInstance): void {
this._editorInputs.set(inputKey, input);
this._instanceDisposables.set(inputKey, [
instance.onDidFocus(this._onDidFocusInstance.fire, this._onDidFocusInstance),
toDisposable(() => this._editorInputs.delete(inputKey)),
instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)
]);
this.instances.push(instance);
this._onDidChangeInstances.fire();
return input;
}
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined {
if (URI.isUri(resource)) {
// note that the uri and and instance id might
// not match this window
for (const instance of this.instances) {
if (instance.resource.path === resource.path) {
return instance;
}
}
}
return undefined;
}
splitInstance(instanceToSplit: ITerminalInstance, shellLaunchConfig: IShellLaunchConfig = {}): ITerminalInstance {
@ -202,9 +253,10 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
}
detachInstance(instance: ITerminalInstance) {
const editorInput = this._editorInputs.get(instance.instanceId);
const inputKey = instance.resource.path;
const editorInput = this._editorInputs.get(inputKey);
editorInput?.detachInstance();
this._editorInputs.delete(instance.instanceId);
this._editorInputs.delete(inputKey);
const instanceIndex = this.instances.findIndex(e => e === instance);
if (instanceIndex !== -1) {
this.instances.splice(instanceIndex, 1);
@ -213,8 +265,8 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
if (!this._isShuttingDown) {
editorInput?.dispose();
}
const disposables = this._instanceDisposables.get(instance.instanceId);
this._instanceDisposables.delete(instance.instanceId);
const disposables = this._instanceDisposables.get(inputKey);
this._instanceDisposables.delete(inputKey);
if (disposables) {
dispose(disposables);
}
@ -227,7 +279,7 @@ export class TerminalEditorService extends Disposable implements ITerminalEditor
return;
}
const editorInput = this._editorInputs.get(instance.instanceId)!;
const editorInput = this._editorInputs.get(instance.resource.path)!;
this._editorService.openEditor(
editorInput,
{

View file

@ -7,6 +7,7 @@ import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { timeout } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
@ -175,6 +176,19 @@ export class TerminalGroupService extends Disposable implements ITerminalGroupSe
}
}
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined {
if (URI.isUri(resource)) {
// note that the uri and and instance id might
// not match this window
for (const instance of this.instances) {
if (instance.resource.path === resource.path) {
return instance;
}
}
}
return undefined;
}
findNext(): void {
const pane = this._viewsService.getActiveViewWithId<TerminalViewPane>(TERMINAL_VIEW_ID);
if (pane?.terminalTabbedView) {

View file

@ -66,6 +66,7 @@ import { Color } from 'vs/base/common/color';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { getTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
// How long in milliseconds should an average frame take to render for a notification to appear
// which suggests the fallback DOM-based renderer
@ -152,6 +153,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
private _navigationModeAddon: INavigationMode & ITerminalAddon | undefined;
private _dndObserver: IDisposable | undefined;
private readonly _resource: URI;
private _lastLayoutDimensions: dom.Dimension | undefined;
private _hasHadInput: boolean;
@ -160,13 +163,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
disableLayout: boolean = false;
target?: TerminalLocation;
get instanceId(): number { return this._instanceId; }
get resource(): URI {
return URI.from({
scheme: Schemas.vscodeTerminal,
path: `/${this._workspaceContextService.getWorkspace().id}/${this.instanceId}`,
fragment: this.title,
});
}
get resource(): URI { return this._resource; }
get cols(): number {
if (this._dimensionsOverride && this._dimensionsOverride.cols) {
if (this._dimensionsOverride.forceExactSize) {
@ -253,6 +250,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
private readonly _terminalAltBufferActiveContextKey: IContextKey<boolean>,
private readonly _configHelper: TerminalConfigHelper,
private _shellLaunchConfig: IShellLaunchConfig,
resource: URI | undefined,
@ITerminalInstanceService private readonly _terminalInstanceService: ITerminalInstanceService,
@ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService,
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@ -287,6 +285,9 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
this._titleReadyComplete = c;
});
// the resource is already set when it's been moved from another window
this._resource = resource || getTerminalUri(this._workspaceContextService.getWorkspace().id, this.instanceId, this.title);
this._terminalHasTextContextKey = TerminalContextKeys.textSelected.bindTo(this._contextKeyService);
this._terminalA11yTreeFocusContextKey = TerminalContextKeys.a11yTreeFocus.bindTo(this._contextKeyService);
this._terminalAltBufferActiveContextKey = TerminalContextKeys.altBufferActive.bindTo(this._contextKeyService);

View file

@ -53,16 +53,17 @@ export class TerminalInstanceService extends Disposable implements ITerminalInst
this._configHelper = _instantiationService.createInstance(TerminalConfigHelper);
}
createInstance(profile: ITerminalProfile, target?: TerminalLocation): ITerminalInstance;
createInstance(shellLaunchConfig: IShellLaunchConfig, target?: TerminalLocation): ITerminalInstance;
createInstance(config: IShellLaunchConfig | ITerminalProfile, target?: TerminalLocation): ITerminalInstance {
createInstance(profile: ITerminalProfile, target?: TerminalLocation, resource?: URI): ITerminalInstance;
createInstance(shellLaunchConfig: IShellLaunchConfig, target?: TerminalLocation, resource?: URI): ITerminalInstance;
createInstance(config: IShellLaunchConfig | ITerminalProfile, target?: TerminalLocation, resource?: URI): ITerminalInstance {
const shellLaunchConfig = this._convertProfileToShellLaunchConfig(config);
const instance = this._instantiationService.createInstance(TerminalInstance,
this._terminalFocusContextKey,
this._terminalShellTypeContextKey,
this._terminalAltBufferActiveContextKey,
this._configHelper,
shellLaunchConfig
shellLaunchConfig,
resource
);
instance.target = target;
this._onDidCreateInstance.fire(instance);

View file

@ -11,7 +11,6 @@ import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { equals } from 'vs/base/common/objects';
import { basename } from 'vs/base/common/path';
import { isMacintosh, isWeb, isWindows, OperatingSystem, OS } from 'vs/base/common/platform';
import { URI } from 'vs/base/common/uri';
import { FindReplaceState } from 'vs/editor/contrib/find/findState';
@ -30,14 +29,14 @@ import { iconForeground } from 'vs/platform/theme/common/colorRegistry';
import { IconDefinition } from 'vs/platform/theme/common/iconRegistry';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { IThemeService, Themable, ThemeIcon } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { VirtualWorkspaceContext } from 'vs/workbench/browser/contextkeys';
import { IEditableData, IViewsService } from 'vs/workbench/common/views';
import { IRemoteTerminalService, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal';
import { IRemoteTerminalService, IRequestAddInstanceToGroupEvent, ITerminalEditorService, ITerminalExternalLinkProvider, ITerminalFindHost, ITerminalGroup, ITerminalGroupService, ITerminalInstance, ITerminalInstanceHost, ITerminalInstanceService, ITerminalProfileProvider, ITerminalService, TerminalConnectionState } from 'vs/workbench/contrib/terminal/browser/terminal';
import { TerminalConfigHelper } from 'vs/workbench/contrib/terminal/browser/terminalConfigHelper';
import { TerminalEditor } from 'vs/workbench/contrib/terminal/browser/terminalEditor';
import { getColorClass, getUriClasses } from 'vs/workbench/contrib/terminal/browser/terminalIcon';
import { configureTerminalProfileIcon } from 'vs/workbench/contrib/terminal/browser/terminalIcons';
import { getTerminalUri, parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
import { TerminalViewPane } from 'vs/workbench/contrib/terminal/browser/terminalView';
import { ILocalTerminalService, IOffProcessTerminalService, IRemoteTerminalAttachTarget, IStartExtensionTerminalRequest, ITerminalConfigHelper, ITerminalProcessExtHostProxy, ITerminalProfileContribution, TERMINAL_VIEW_ID } from 'vs/workbench/contrib/terminal/common/terminal';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
@ -160,7 +159,6 @@ export class TerminalService implements ITerminalService {
@IEditorResolverService editorResolverService: IEditorResolverService,
@IExtensionService private readonly _extensionService: IExtensionService,
@INotificationService private readonly _notificationService: INotificationService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@optional(ILocalTerminalService) localTerminalService: ILocalTerminalService
) {
this._localTerminalService = localTerminalService;
@ -187,11 +185,9 @@ export class TerminalService implements ITerminalService {
if (sourceGroup) {
sourceGroup.removeInstance(instance);
}
} else {
instance = _terminalInstanceService.createInstance({});
}
return {
editor: this._terminalEditorService.getOrCreateEditorInput(instance),
editor: this._terminalEditorService.getOrCreateEditorInput(instance || resource),
options: {
...options,
pinned: true,
@ -262,10 +258,11 @@ export class TerminalService implements ITerminalService {
: Promise.resolve();
this._primaryOffProcessTerminalService = !!this._environmentService.remoteAuthority ? this._remoteTerminalService : (this._localTerminalService || this._remoteTerminalService);
this._primaryOffProcessTerminalService.onDidRequestDetach(async (e) => {
if (e.workspaceId && this._workspaceContextService.getWorkspace().id === e.workspaceId) {
const instanceToDetach = this.getInstanceFromId(e.instanceId);
const instanceToDetach = this.getInstanceFromResource(getTerminalUri(e.workspaceId, e.instanceId));
if (instanceToDetach) {
const persistentProcessId = instanceToDetach?.persistentProcessId;
if (persistentProcessId && !instanceToDetach.shellLaunchConfig.isFeatureTerminal && !instanceToDetach.shellLaunchConfig.customPtyImplementation) {
this._terminalEditorService.detachInstance(instanceToDetach);
await instanceToDetach.detachFromProcess();
await this._primaryOffProcessTerminalService?.acceptDetachInstanceReply(e.requestId, persistentProcessId);
} else {
@ -274,6 +271,7 @@ export class TerminalService implements ITerminalService {
}
}
});
initPromise.then(() => this._setConnected());
// Wait up to 5 seconds for profiles to be ready so it's assured that we know the actual
@ -609,25 +607,17 @@ export class TerminalService implements ITerminalService {
getInstanceFromResource(resource: URI | undefined): ITerminalInstance | undefined {
if (URI.isUri(resource)) {
const instanceId = this._getInstanceIdFromUri(resource);
if (instanceId) {
return this.getInstanceFromId(instanceId);
// note that the uri and and instance id might
// not match this window
for (const instance of this.instances) {
if (instance.resource.path === resource.path) {
return instance;
}
}
}
return undefined;
}
private _getInstanceIdFromUri(resource: URI): number | undefined {
if (resource.scheme !== Schemas.vscodeTerminal) {
return undefined;
}
const base = basename(resource.path);
if (base === '') {
return undefined;
}
return parseInt(base);
}
isAttachedToTerminal(remoteTerm: IRemoteTerminalAttachTarget): boolean {
return this.instances.some(term => term.processId === remoteTerm.pid);
}
@ -673,7 +663,6 @@ export class TerminalService implements ITerminalService {
break;
}
this._initInstanceListeners(instance);
if (instanceToSplit.target !== TerminalLocation.Editor) {
this._terminalGroupService.groups.forEach((g, i) => g.setVisible(i === this._terminalGroupService.activeGroupIndex));
@ -753,25 +742,41 @@ export class TerminalService implements ITerminalService {
}));
instance.addDisposable(instance.onMaximumDimensionsChanged(() => this._onDidMaxiumumDimensionsChange.fire(instance)));
instance.addDisposable(instance.onDidFocus(this._onDidChangeActiveInstance.fire, this._onDidChangeActiveInstance));
instance.addDisposable(instance.onRequestAddInstanceToGroup(e => {
const instanceId = this._getInstanceIdFromUri(e.uri);
if (instanceId === undefined) {
return;
}
instance.addDisposable(instance.onRequestAddInstanceToGroup(async e => await this._addInstanceToGroup(instance, e)));
}
// View terminals
let sourceInstance = this._terminalGroupService.instances.find(e => e.instanceId === instanceId);
if (sourceInstance) {
private async _addInstanceToGroup(instance: ITerminalInstance, e: IRequestAddInstanceToGroupEvent): Promise<void> {
const terminalIdentifier = parseTerminalUri(e.uri);
if (terminalIdentifier.instanceId === undefined) {
return;
}
let sourceInstance: ITerminalInstance | undefined = this.getInstanceFromResource(e.uri);
// Terminal from a different window
if (!sourceInstance) {
const attachPersistentProcess = await this._primaryOffProcessTerminalService?.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId);
if (attachPersistentProcess) {
sourceInstance = this.createTerminal({ config: { attachPersistentProcess }, resource: e.uri });
this._terminalGroupService.moveInstance(sourceInstance, instance, e.side);
return;
}
}
// Terminal editors
sourceInstance = this._terminalEditorService.instances.find(e => e.instanceId === instanceId);
if (sourceInstance) {
this.moveToTerminalView(sourceInstance, instance, e.side);
}
}));
// View terminals
sourceInstance = this._terminalGroupService.getInstanceFromResource(e.uri);
if (sourceInstance) {
this._terminalGroupService.moveInstance(sourceInstance, instance, e.side);
return;
}
// Terminal editors
sourceInstance = this._terminalEditorService.getInstanceFromResource(e.uri);
if (sourceInstance) {
this.moveToTerminalView(sourceInstance, instance, e.side);
return;
}
return;
}
registerProcessSupport(isSupported: boolean): void {
@ -1073,12 +1078,11 @@ export class TerminalService implements ITerminalService {
throw new Error('Could not create terminal when process support is not registered');
}
if (shellLaunchConfig.hideFromUser) {
const instance = this._terminalInstanceService.createInstance(shellLaunchConfig);
const instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource);
this._backgroundedTerminalInstances.push(instance);
this._backgroundedTerminalDisposables.set(instance.instanceId, [
instance.onDisposed(this._onDidDisposeInstance.fire, this._onDidDisposeInstance)
]);
this._initInstanceListeners(instance);
return instance;
}
@ -1087,14 +1091,14 @@ export class TerminalService implements ITerminalService {
let instance: ITerminalInstance;
const target = options?.target || this.configHelper.config.defaultLocation;
if (target === TerminalLocation.Editor) {
instance = this._terminalInstanceService.createInstance(shellLaunchConfig);
instance = this._terminalInstanceService.createInstance(shellLaunchConfig, undefined, options?.resource);
instance.target = TerminalLocation.Editor;
this._terminalEditorService.openEditor(instance);
} else {
// TODO: pass resource?
const group = this._terminalGroupService.createGroup(shellLaunchConfig);
instance = group.terminalInstances[0];
}
this._initInstanceListeners(instance);
return instance;
}

View file

@ -47,6 +47,7 @@ import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecy
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IProcessDetails } from 'vs/platform/terminal/common/terminalProcess';
import { TerminalContextKeys } from 'vs/workbench/contrib/terminal/common/terminalContextKey';
import { parseTerminalUri } from 'vs/workbench/contrib/terminal/browser/terminalUri';
const $ = DOM.$;
@ -577,8 +578,8 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> {
// Attach terminals type to event
const terminals: ITerminalInstance[] = dndData.filter(e => 'instanceId' in (e as any));
if (terminals.length > 0) {
originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.resource.toString())));
originalEvent.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify(terminals.map(e => e.resource.toString())));
originalEvent.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify(terminals.map(e => e.instanceId)));
}
}
@ -618,21 +619,22 @@ class TerminalTabsDragAndDrop implements IListDragAndDrop<ITerminalInstance> {
this._autoFocusInstance = undefined;
let sourceInstances: ITerminalInstance[] | undefined;
const terminalResources = originalEvent.dataTransfer?.getData(DataTransfers.RESOURCES);
const terminals = originalEvent.dataTransfer?.getData(DataTransfers.TERMINALS);
let promises: Promise<IProcessDetails | undefined>[] = [];
if (terminals && terminalResources) {
const json = JSON.parse(terminalResources);
const resources = originalEvent.dataTransfer?.getData(DataTransfers.TERMINALS);
if (resources) {
const json = JSON.parse(resources);
for (const entry of json) {
const uri = URI.parse(entry);
const [, workspaceId, instanceId] = uri.path.split('/');
if (workspaceId && instanceId) {
const instance = this._terminalService.instances.find(e => e.resource.path === instanceId);
if (instance) {
sourceInstances = [instance];
this._terminalService.moveToTerminalView(instance);
} else if (this._offProcessTerminalService && workspaceId !== this._workspaceContextService.getWorkspace().id) {
promises.push(this._offProcessTerminalService.requestDetachInstance(workspaceId, Number.parseInt(instanceId)));
const instance = this._terminalService.getInstanceFromResource(uri);
if (instance) {
sourceInstances = [instance];
this._terminalService.moveToTerminalView(instance);
} else if (this._offProcessTerminalService) {
// why is this undefined
const terminalIdentifier = parseTerminalUri(uri);
if (terminalIdentifier.instanceId) {
promises.push(this._offProcessTerminalService.requestDetachInstance(terminalIdentifier.workspaceId, terminalIdentifier.instanceId));
}
}
}

View file

@ -0,0 +1,28 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Schemas } from 'vs/base/common/network';
import { URI } from 'vs/base/common/uri';
export function parseTerminalUri(resource: URI): ITerminalIdentifier {
const [, workspaceId, instanceId] = resource.path.split('/');
if (!workspaceId || !Number.parseInt(instanceId)) {
throw new Error(`Could not parse terminal uri for resource ${resource}`);
}
return { workspaceId, instanceId: Number.parseInt(instanceId) };
}
export function getTerminalUri(workspaceId: string, instanceId: number, title?: string): URI {
return URI.from({
scheme: Schemas.vscodeTerminal,
path: `/${workspaceId}/${instanceId}`,
fragment: title || undefined,
});
}
export interface ITerminalIdentifier {
workspaceId: string;
instanceId: number | undefined;
}

View file

@ -481,7 +481,7 @@ class SingleTerminalTabActionViewItem extends MenuEntryActionViewItem {
const instance = this._terminalGroupService.activeInstance;
if (e.dataTransfer && instance) {
e.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([instance.resource.toString()]));
e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.instanceId]));
e.dataTransfer.setData(DataTransfers.TERMINALS, JSON.stringify([instance.resource.toString()]));
}
}));
}

View file

@ -1667,9 +1667,7 @@ export class TestLocalTerminalService implements ILocalTerminalService {
onDidMoveWindowInstance = Event.None;
onDidRequestDetach = Event.None;
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess> {
return new TestTerminalChildProcess(shouldPersist);
}
async createProcess(shellLaunchConfig: IShellLaunchConfig, cwd: string, cols: number, rows: number, env: IProcessEnvironment, windowsEnableConpty: boolean, shouldPersist: boolean): Promise<ITerminalChildProcess> { return new TestTerminalChildProcess(shouldPersist); }
async attachToProcess(id: number): Promise<ITerminalChildProcess | undefined> { throw new Error('Method not implemented.'); }
async listProcesses(): Promise<IProcessDetails[]> { throw new Error('Method not implemented.'); }
getDefaultSystemShell(osOverride?: OperatingSystem): Promise<string> { throw new Error('Method not implemented.'); }