Skip to content

Commit 698d0e8

Browse files
committed
implement ExternalTerminalService
1 parent 7d8ac2f commit 698d0e8

File tree

6 files changed

+271
-5
lines changed

6 files changed

+271
-5
lines changed
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { inject, injectable } from 'inversify';
5+
import { CancellationToken, Disposable, Event, EventEmitter, Terminal, TerminalShellExecution, window } from 'vscode';
6+
import '../../common/extensions';
7+
import { IInterpreterService } from '../../interpreter/contracts';
8+
import { IServiceContainer } from '../../ioc/types';
9+
import { captureTelemetry } from '../../telemetry';
10+
import { EventName } from '../../telemetry/constants';
11+
import { ITerminalAutoActivation } from '../../terminals/types';
12+
import { ITerminalManager } from '../application/types';
13+
import { _SCRIPTS_DIR } from '../process/internal/scripts/constants';
14+
import { IConfigurationService, IDisposableRegistry } from '../types';
15+
import {
16+
ITerminalActivator,
17+
ITerminalHelper,
18+
ITerminalService,
19+
TerminalCreationOptions,
20+
TerminalShellType,
21+
} from './types';
22+
import { traceVerbose } from '../../logging';
23+
import { getConfiguration } from '../vscodeApis/workspaceApis';
24+
import { useEnvExtension } from '../../envExt/api.internal';
25+
import { ensureTerminalLegacy } from '../../envExt/api.legacy';
26+
import { sleep } from '../utils/async';
27+
import { isWindows } from '../utils/platform';
28+
import { getPythonMinorVersion } from '../../repl/replUtils';
29+
import { PythonEnvironment } from '../../pythonEnvironments/info';
30+
31+
@injectable()
32+
export class ExternalTerminalService implements ITerminalService, Disposable {
33+
private terminal?: Terminal;
34+
private ownsTerminal: boolean = false;
35+
private terminalShellType!: TerminalShellType;
36+
private terminalClosed = new EventEmitter<void>();
37+
private terminalManager: ITerminalManager;
38+
private terminalHelper: ITerminalHelper;
39+
private terminalActivator: ITerminalActivator;
40+
private terminalAutoActivator: ITerminalAutoActivation;
41+
private readonly executeCommandListeners: Set<Disposable> = new Set();
42+
private _terminalFirstLaunched: boolean = true;
43+
public get onDidCloseTerminal(): Event<void> {
44+
return this.terminalClosed.event.bind(this.terminalClosed);
45+
}
46+
47+
constructor(
48+
@inject(IServiceContainer) private serviceContainer: IServiceContainer,
49+
private readonly options?: TerminalCreationOptions,
50+
) {
51+
const disposableRegistry = this.serviceContainer.get<Disposable[]>(IDisposableRegistry);
52+
disposableRegistry.push(this);
53+
this.terminalHelper = this.serviceContainer.get<ITerminalHelper>(ITerminalHelper);
54+
this.terminalManager = this.serviceContainer.get<ITerminalManager>(ITerminalManager);
55+
this.terminalAutoActivator = this.serviceContainer.get<ITerminalAutoActivation>(ITerminalAutoActivation);
56+
this.terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry);
57+
this.terminalActivator = this.serviceContainer.get<ITerminalActivator>(ITerminalActivator);
58+
}
59+
public dispose() {
60+
if (this.ownsTerminal) {
61+
this.terminal?.dispose();
62+
}
63+
64+
if (this.executeCommandListeners && this.executeCommandListeners.size > 0) {
65+
this.executeCommandListeners.forEach((d) => {
66+
d?.dispose();
67+
});
68+
}
69+
}
70+
public async sendCommand(command: string, args: string[], _?: CancellationToken): Promise<void> {
71+
await this.ensureTerminal();
72+
const text = this.terminalHelper.buildCommandForTerminal(this.terminalShellType, command, args);
73+
if (!this.options?.hideFromUser) {
74+
this.terminal!.show(true);
75+
}
76+
77+
await this.executeCommand(text, false);
78+
}
79+
80+
/** @deprecated */
81+
public async sendText(text: string): Promise<void> {
82+
await this.ensureTerminal();
83+
if (!this.options?.hideFromUser) {
84+
this.terminal!.show(true);
85+
}
86+
this.terminal!.sendText(text);
87+
this.terminal = undefined;
88+
}
89+
90+
public async executeCommand(
91+
commandLine: string,
92+
isPythonShell: boolean,
93+
): Promise<TerminalShellExecution | undefined> {
94+
const terminal = window.activeTerminal!;
95+
if (!this.options?.hideFromUser) {
96+
terminal.show(true);
97+
}
98+
99+
// If terminal was just launched, wait some time for shell integration to onDidChangeShellIntegration.
100+
if (!terminal.shellIntegration && this._terminalFirstLaunched) {
101+
this._terminalFirstLaunched = false;
102+
const promise = new Promise<boolean>((resolve) => {
103+
const disposable = this.terminalManager.onDidChangeTerminalShellIntegration(() => {
104+
// eslint-disable-next-line @typescript-eslint/no-use-before-define
105+
clearTimeout(timer);
106+
disposable.dispose();
107+
resolve(true);
108+
});
109+
const TIMEOUT_DURATION = 500;
110+
const timer = setTimeout(() => {
111+
disposable.dispose();
112+
resolve(true);
113+
}, TIMEOUT_DURATION);
114+
});
115+
await promise;
116+
}
117+
118+
const config = getConfiguration('python');
119+
const pythonrcSetting = config.get<boolean>('terminal.shellIntegration.enabled');
120+
121+
const minorVersion = this.options?.resource
122+
? await getPythonMinorVersion(
123+
this.options.resource,
124+
this.serviceContainer.get<IInterpreterService>(IInterpreterService),
125+
)
126+
: undefined;
127+
128+
if ((isPythonShell && !pythonrcSetting) || (isPythonShell && isWindows()) || (minorVersion ?? 0) >= 13) {
129+
// If user has explicitly disabled SI for Python, use sendText for inside Terminal REPL.
130+
terminal.sendText(commandLine);
131+
return undefined;
132+
} else if (terminal.shellIntegration) {
133+
const execution = terminal.shellIntegration.executeCommand(commandLine);
134+
traceVerbose(`Shell Integration is enabled, executeCommand: ${commandLine}`);
135+
return execution;
136+
} else {
137+
terminal.sendText(commandLine);
138+
traceVerbose(`Shell Integration is disabled, sendText: ${commandLine}`);
139+
}
140+
141+
this.terminal = undefined;
142+
return undefined;
143+
}
144+
145+
public async show(preserveFocus: boolean = true): Promise<void> {
146+
await this.ensureTerminal(preserveFocus);
147+
if (!this.options?.hideFromUser) {
148+
this.terminal!.show(preserveFocus);
149+
}
150+
this.terminal = undefined;
151+
}
152+
153+
private resolveInterpreterPath(
154+
interpreter: PythonEnvironment | undefined,
155+
settingsPythonPath: string | undefined,
156+
): string {
157+
if (interpreter) {
158+
if ('path' in interpreter && interpreter.path) {
159+
return interpreter.path;
160+
}
161+
const uriFsPath = (interpreter as any).uri?.fsPath as string | undefined;
162+
if (uriFsPath) {
163+
return uriFsPath;
164+
}
165+
}
166+
return settingsPythonPath ?? 'python';
167+
}
168+
169+
private runPythonReplInActiveTerminal() {
170+
const settings = this.serviceContainer
171+
.get<IConfigurationService>(IConfigurationService)
172+
.getSettings(this.options?.resource);
173+
const interpreterPath = this.resolveInterpreterPath(this.options?.interpreter, settings.pythonPath);
174+
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
175+
const launchCmd = this.terminalHelper.buildCommandForTerminal(this.terminalShellType, interpreterPath, []);
176+
this.terminal!.sendText(launchCmd);
177+
}
178+
179+
// TODO: Debt switch to Promise<Terminal> ---> breaks 20 tests
180+
public async ensureTerminal(preserveFocus: boolean = true): Promise<void> {
181+
this.terminal = window.activeTerminal;
182+
if (this.terminal) {
183+
this.ownsTerminal = false;
184+
if (this.terminal.state.shell !== 'python') {
185+
this.runPythonReplInActiveTerminal();
186+
}
187+
return;
188+
}
189+
190+
if (useEnvExtension()) {
191+
this.terminal = await ensureTerminalLegacy(this.options?.resource, {
192+
name: this.options?.title || 'Python',
193+
hideFromUser: this.options?.hideFromUser,
194+
});
195+
this.ownsTerminal = true;
196+
} else {
197+
this.terminalShellType = this.terminalHelper.identifyTerminalShell(this.terminal);
198+
this.terminal = this.terminalManager.createTerminal({
199+
name: this.options?.title || 'Python',
200+
hideFromUser: this.options?.hideFromUser,
201+
});
202+
this.ownsTerminal = true;
203+
this.terminalAutoActivator.disableAutoActivation(this.terminal);
204+
205+
await sleep(100);
206+
207+
await this.terminalActivator.activateEnvironmentInTerminal(this.terminal, {
208+
resource: this.options?.resource,
209+
preserveFocus,
210+
interpreter: this.options?.interpreter,
211+
hideFromUser: this.options?.hideFromUser,
212+
});
213+
}
214+
215+
if (!this.options?.hideFromUser) {
216+
this.terminal.show(preserveFocus);
217+
}
218+
219+
this.sendTelemetry().ignoreErrors();
220+
return;
221+
}
222+
223+
private terminalCloseHandler(terminal: Terminal) {
224+
if (terminal === this.terminal) {
225+
this.terminalClosed.fire();
226+
this.terminal = undefined;
227+
this.ownsTerminal = false;
228+
}
229+
}
230+
231+
private async sendTelemetry() {
232+
const pythonPath = this.serviceContainer
233+
.get<IConfigurationService>(IConfigurationService)
234+
.getSettings(this.options?.resource).pythonPath;
235+
const interpreterInfo =
236+
this.options?.interpreter ||
237+
(await this.serviceContainer
238+
.get<IInterpreterService>(IInterpreterService)
239+
.getInterpreterDetails(pythonPath));
240+
const pythonVersion = interpreterInfo && interpreterInfo.version ? interpreterInfo.version.raw : undefined;
241+
const interpreterType = interpreterInfo ? interpreterInfo.envType : undefined;
242+
captureTelemetry(EventName.TERMINAL_CREATE, {
243+
terminal: this.terminalShellType,
244+
pythonVersion,
245+
interpreterType,
246+
});
247+
}
248+
249+
public hasActiveTerminal(): boolean {
250+
return !!window.activeTerminal;
251+
}
252+
}

src/client/common/terminal/factory.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import { IFileSystem } from '../platform/types';
1212
import { TerminalService } from './service';
1313
import { SynchronousTerminalService } from './syncTerminalService';
1414
import { ITerminalService, ITerminalServiceFactory, TerminalCreationOptions } from './types';
15+
import { ExternalTerminalService } from './externalTerminalService';
1516

1617
@injectable()
1718
export class TerminalServiceFactory implements ITerminalServiceFactory {
18-
private terminalServices: Map<string, TerminalService>;
19+
private terminalServices: Map<string, TerminalService | ExternalTerminalService>;
1920

2021
constructor(
2122
@inject(IServiceContainer) private serviceContainer: IServiceContainer,
@@ -35,7 +36,8 @@ export class TerminalServiceFactory implements ITerminalServiceFactory {
3536
terminalTitle = `${terminalTitle}: ${path.basename(resource.fsPath).replace('.py', '')}`;
3637
}
3738
options.title = terminalTitle;
38-
const terminalService = new TerminalService(this.serviceContainer, options);
39+
const terminalService = new ExternalTerminalService(this.serviceContainer, options);
40+
// const terminalService = new TerminalService(this.serviceContainer, options);
3941
this.terminalServices.set(id, terminalService);
4042
}
4143

@@ -49,7 +51,8 @@ export class TerminalServiceFactory implements ITerminalServiceFactory {
4951
}
5052
public createTerminalService(resource?: Uri, title?: string): ITerminalService {
5153
title = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python';
52-
return new TerminalService(this.serviceContainer, { resource, title });
54+
return new ExternalTerminalService(this.serviceContainer, { resource, title });
55+
// return new TerminalService(this.serviceContainer, { resource, title });
5356
}
5457
private getTerminalId(
5558
title: string,

src/client/common/terminal/service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,4 +200,8 @@ export class TerminalService implements ITerminalService, Disposable {
200200
interpreterType,
201201
});
202202
}
203+
204+
public hasActiveTerminal(): boolean {
205+
return !!this.terminal;
206+
}
203207
}

src/client/common/terminal/syncTerminalService.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { createDeferred, Deferred } from '../utils/async';
1515
import { noop } from '../utils/misc';
1616
import { TerminalService } from './service';
1717
import { ITerminalService } from './types';
18+
import { ExternalTerminalService } from './externalTerminalService';
1819

1920
enum State {
2021
notStarted = 0,
@@ -101,7 +102,7 @@ export class SynchronousTerminalService implements ITerminalService, Disposable
101102
constructor(
102103
@inject(IFileSystem) private readonly fs: IFileSystem,
103104
@inject(IInterpreterService) private readonly interpreter: IInterpreterService,
104-
public readonly terminalService: TerminalService,
105+
public readonly terminalService: TerminalService | ExternalTerminalService,
105106
private readonly pythonInterpreter?: PythonEnvironment,
106107
) {}
107108
public dispose() {
@@ -158,4 +159,8 @@ export class SynchronousTerminalService implements ITerminalService, Disposable
158159
return l;
159160
});
160161
}
162+
163+
public hasActiveTerminal(): boolean {
164+
return this.terminalService.hasActiveTerminal();
165+
}
161166
}

src/client/common/terminal/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ export interface ITerminalService extends IDisposable {
5656
sendText(text: string): Promise<void>;
5757
executeCommand(commandLine: string, isPythonShell: boolean): Promise<TerminalShellExecution | undefined>;
5858
show(preserveFocus?: boolean): Promise<void>;
59+
hasActiveTerminal(): boolean;
5960
}
6061

6162
export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory');

src/client/terminals/codeExecution/terminalCodeExecution.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,8 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
6565

6666
public async initializeRepl(resource: Resource) {
6767
const terminalService = this.getTerminalService(resource);
68-
if (this.replActive && (await this.replActive)) {
68+
if (terminalService.hasActiveTerminal()) {
69+
// if (this.replActive && (await this.replActive)) {
6970
await terminalService.show();
7071
return;
7172
}

0 commit comments

Comments
 (0)