Skip to content

Commit d3f07a9

Browse files
authored
Add support for auto-activation (#119)
1 parent e24a565 commit d3f07a9

File tree

6 files changed

+84
-124
lines changed

6 files changed

+84
-124
lines changed

examples/sample1/src/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,9 +1000,9 @@ export interface PythonProjectModifyApi {
10001000
*/
10011001
export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {}
10021002

1003-
export interface PythonTerminalOptions extends TerminalOptions {
1003+
export interface PythonTerminalCreateOptions extends TerminalOptions {
10041004
/**
1005-
* Whether to show the terminal.
1005+
* Whether to disable activation on create.
10061006
*/
10071007
disableActivation?: boolean;
10081008
}
@@ -1016,7 +1016,7 @@ export interface PythonTerminalCreateApi {
10161016
*
10171017
* Note: Non-activatable environments have no effect on the terminal.
10181018
*/
1019-
createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
1019+
createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
10201020
}
10211021

10221022
/**

src/api.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1000,9 +1000,9 @@ export interface PythonProjectModifyApi {
10001000
*/
10011001
export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {}
10021002

1003-
export interface PythonTerminalOptions extends TerminalOptions {
1003+
export interface PythonTerminalCreateOptions extends TerminalOptions {
10041004
/**
1005-
* Whether to show the terminal.
1005+
* Whether to disable activation on create.
10061006
*/
10071007
disableActivation?: boolean;
10081008
}
@@ -1016,7 +1016,7 @@ export interface PythonTerminalCreateApi {
10161016
*
10171017
* Note: Non-activatable environments have no effect on the terminal.
10181018
*/
1019-
createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
1019+
createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
10201020
}
10211021

10221022
/**

src/extension.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,9 +71,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
7171
const statusBar = new PythonStatusBarImpl();
7272
context.subscriptions.push(statusBar);
7373

74-
const terminalManager: TerminalManager = new TerminalManagerImpl();
75-
context.subscriptions.push(terminalManager);
76-
7774
const projectManager: PythonProjectManager = new PythonProjectManagerImpl();
7875
context.subscriptions.push(projectManager);
7976

@@ -83,6 +80,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
8380
const envManagers: EnvironmentManagers = new PythonEnvironmentManagers(projectManager);
8481
context.subscriptions.push(envManagers);
8582

83+
const terminalManager: TerminalManager = new TerminalManagerImpl(projectManager, envManagers);
84+
context.subscriptions.push(terminalManager);
85+
8686
const projectCreators: ProjectCreators = new ProjectCreatorsImpl();
8787
context.subscriptions.push(
8888
projectCreators,
@@ -233,6 +233,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
233233
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
234234
]);
235235
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
236+
await terminalManager.initialize();
236237
});
237238

238239
sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);

src/features/pythonApi.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
PythonTaskExecutionOptions,
2727
PythonTerminalExecutionOptions,
2828
PythonBackgroundRunOptions,
29-
PythonTerminalOptions,
29+
PythonTerminalCreateOptions,
3030
DidChangeEnvironmentVariablesEventArgs,
3131
} from '../api';
3232
import {
@@ -275,7 +275,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
275275
registerPythonProjectCreator(creator: PythonProjectCreator): Disposable {
276276
return this.projectCreators.registerPythonProjectCreator(creator);
277277
}
278-
async createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal> {
278+
async createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal> {
279279
return this.terminalManager.create(environment, options);
280280
}
281281
async runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise<Terminal> {

src/features/terminal/terminalManager.ts

Lines changed: 58 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -8,30 +8,26 @@ import {
88
Terminal,
99
TerminalShellExecutionEndEvent,
1010
TerminalShellExecutionStartEvent,
11-
TerminalShellIntegrationChangeEvent,
1211
Uri,
12+
TerminalOptions,
1313
} from 'vscode';
1414
import {
1515
createTerminal,
16-
onDidChangeTerminalShellIntegration,
1716
onDidCloseTerminal,
1817
onDidEndTerminalShellExecution,
1918
onDidOpenTerminal,
2019
onDidStartTerminalShellExecution,
2120
terminals,
2221
withProgress,
2322
} from '../../common/window.apis';
24-
import { PythonEnvironment, PythonProject, PythonTerminalOptions } from '../../api';
23+
import { PythonEnvironment, PythonProject, PythonTerminalCreateOptions } from '../../api';
2524
import { getActivationCommand, getDeactivationCommand, isActivatableEnvironment } from '../common/activation';
26-
import { showErrorMessage } from '../../common/errors/utils';
2725
import { quoteArgs } from '../execution/execUtils';
2826
import { createDeferred } from '../../common/utils/deferred';
2927
import { traceError, traceVerbose } from '../../common/logging';
3028
import { getConfiguration } from '../../common/workspace.apis';
31-
import { EnvironmentManagers } from '../../internal.api';
32-
33-
const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds
34-
const SHELL_INTEGRATION_POLL_INTERVAL = 100; // 0.1 seconds
29+
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
30+
import { waitForShellIntegration } from './utils';
3531

3632
export interface TerminalActivation {
3733
isActivated(terminal: Terminal, environment?: PythonEnvironment): boolean;
@@ -40,7 +36,7 @@ export interface TerminalActivation {
4036
}
4137

4238
export interface TerminalCreation {
43-
create(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
39+
create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
4440
}
4541

4642
export interface TerminalGetters {
@@ -62,7 +58,7 @@ export interface TerminalEnvironment {
6258
}
6359

6460
export interface TerminalInit {
65-
initialize(projects: PythonProject[], em: EnvironmentManagers): Promise<void>;
61+
initialize(): Promise<void>;
6662
}
6763

6864
export interface TerminalManager
@@ -78,33 +74,28 @@ export class TerminalManagerImpl implements TerminalManager {
7874
private activatedTerminals = new Map<Terminal, PythonEnvironment>();
7975
private activatingTerminals = new Map<Terminal, Promise<void>>();
8076
private deactivatingTerminals = new Map<Terminal, Promise<void>>();
77+
private skipActivationOnOpen = new Set<Terminal>();
8178

8279
private onTerminalOpenedEmitter = new EventEmitter<Terminal>();
8380
private onTerminalOpened = this.onTerminalOpenedEmitter.event;
8481

8582
private onTerminalClosedEmitter = new EventEmitter<Terminal>();
8683
private onTerminalClosed = this.onTerminalClosedEmitter.event;
8784

88-
private onTerminalShellIntegrationChangedEmitter = new EventEmitter<TerminalShellIntegrationChangeEvent>();
89-
private onTerminalShellIntegrationChanged = this.onTerminalShellIntegrationChangedEmitter.event;
90-
9185
private onTerminalShellExecutionStartEmitter = new EventEmitter<TerminalShellExecutionStartEvent>();
9286
private onTerminalShellExecutionStart = this.onTerminalShellExecutionStartEmitter.event;
9387

9488
private onTerminalShellExecutionEndEmitter = new EventEmitter<TerminalShellExecutionEndEvent>();
9589
private onTerminalShellExecutionEnd = this.onTerminalShellExecutionEndEmitter.event;
9690

97-
constructor() {
91+
constructor(private readonly projectManager: PythonProjectManager, private readonly em: EnvironmentManagers) {
9892
this.disposables.push(
9993
onDidOpenTerminal((t: Terminal) => {
10094
this.onTerminalOpenedEmitter.fire(t);
10195
}),
10296
onDidCloseTerminal((t: Terminal) => {
10397
this.onTerminalClosedEmitter.fire(t);
10498
}),
105-
onDidChangeTerminalShellIntegration((e: TerminalShellIntegrationChangeEvent) => {
106-
this.onTerminalShellIntegrationChangedEmitter.fire(e);
107-
}),
10899
onDidStartTerminalShellExecution((e: TerminalShellExecutionStartEvent) => {
109100
this.onTerminalShellExecutionStartEmitter.fire(e);
110101
}),
@@ -113,9 +104,20 @@ export class TerminalManagerImpl implements TerminalManager {
113104
}),
114105
this.onTerminalOpenedEmitter,
115106
this.onTerminalClosedEmitter,
116-
this.onTerminalShellIntegrationChangedEmitter,
117107
this.onTerminalShellExecutionStartEmitter,
118108
this.onTerminalShellExecutionEndEmitter,
109+
this.onTerminalOpened(async (t) => {
110+
if (this.skipActivationOnOpen.has(t) || (t.creationOptions as TerminalOptions)?.hideFromUser) {
111+
return;
112+
}
113+
await this.autoActivateOnTerminalOpen(t);
114+
}),
115+
this.onTerminalClosed((t) => {
116+
this.activatedTerminals.delete(t);
117+
this.activatingTerminals.delete(t);
118+
this.deactivatingTerminals.delete(t);
119+
this.skipActivationOnOpen.delete(t);
120+
}),
119121
);
120122
}
121123

@@ -212,75 +214,36 @@ export class TerminalManagerImpl implements TerminalManager {
212214
}
213215
}
214216

215-
private async activateEnvironmentOnCreation(terminal: Terminal, environment: PythonEnvironment): Promise<void> {
216-
const deferred = createDeferred<void>();
217-
const disposables: Disposable[] = [];
218-
let disposeTimer: Disposable | undefined;
219-
let activated = false;
220-
this.activatingTerminals.set(terminal, deferred.promise);
217+
private async getActivationEnvironment(): Promise<PythonEnvironment | undefined> {
218+
const projects = this.projectManager.getProjects();
219+
const uri = projects.length === 0 ? undefined : projects[0].uri;
220+
const manager = this.em.getEnvironmentManager(uri);
221+
const env = await manager?.get(uri);
222+
return env;
223+
}
221224

222-
try {
223-
disposables.push(
224-
new Disposable(() => {
225-
this.activatingTerminals.delete(terminal);
226-
}),
227-
this.onTerminalOpened(async (t: Terminal) => {
228-
if (t === terminal) {
229-
if (terminal.shellIntegration) {
230-
// Shell integration is available when the terminal is opened.
231-
activated = true;
232-
await this.activateUsingShellIntegration(terminal.shellIntegration, terminal, environment);
233-
deferred.resolve();
234-
} else {
235-
let seconds = 0;
236-
const timer = setInterval(() => {
237-
seconds += SHELL_INTEGRATION_POLL_INTERVAL;
238-
if (terminal.shellIntegration || activated) {
239-
disposeTimer?.dispose();
240-
return;
241-
}
242-
243-
if (seconds >= SHELL_INTEGRATION_TIMEOUT) {
244-
disposeTimer?.dispose();
245-
activated = true;
246-
this.activateLegacy(terminal, environment);
247-
deferred.resolve();
248-
}
249-
}, 100);
250-
251-
disposeTimer = new Disposable(() => {
252-
clearInterval(timer);
253-
disposeTimer = undefined;
254-
});
255-
}
256-
}
257-
}),
258-
this.onTerminalShellIntegrationChanged(async (e: TerminalShellIntegrationChangeEvent) => {
259-
if (terminal === e.terminal && !activated) {
260-
disposeTimer?.dispose();
261-
activated = true;
262-
await this.activateUsingShellIntegration(e.shellIntegration, terminal, environment);
263-
deferred.resolve();
264-
}
265-
}),
266-
this.onTerminalClosed((t) => {
267-
if (terminal === t && !deferred.completed) {
268-
deferred.reject(new Error('Terminal closed before activation'));
269-
}
270-
}),
271-
new Disposable(() => {
272-
disposeTimer?.dispose();
273-
}),
225+
private async autoActivateOnTerminalOpen(terminal: Terminal, environment?: PythonEnvironment): Promise<void> {
226+
const config = getConfiguration('python');
227+
if (!config.get<boolean>('terminal.activateEnvironment', false)) {
228+
return;
229+
}
230+
231+
const env = environment ?? (await this.getActivationEnvironment());
232+
if (env && isActivatableEnvironment(env)) {
233+
await withProgress(
234+
{
235+
location: ProgressLocation.Window,
236+
title: `Activating environment: ${env.displayName}`,
237+
},
238+
async () => {
239+
await waitForShellIntegration(terminal);
240+
await this.activate(terminal, env);
241+
},
274242
);
275-
await deferred.promise;
276-
} catch (ex) {
277-
traceError('Failed to activate environment:\r\n', ex);
278-
} finally {
279-
disposables.forEach((d) => d.dispose());
280243
}
281244
}
282245

283-
public async create(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal> {
246+
public async create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal> {
284247
// const name = options.name ?? `Python: ${environment.displayName}`;
285248
const newTerminal = createTerminal({
286249
name: options.name,
@@ -296,25 +259,17 @@ export class TerminalManagerImpl implements TerminalManager {
296259
location: options.location,
297260
isTransient: options.isTransient,
298261
});
299-
const activatable = !options.disableActivation && isActivatableEnvironment(environment);
300262

301-
if (activatable) {
302-
try {
303-
await withProgress(
304-
{
305-
location: ProgressLocation.Window,
306-
title: `Activating ${environment.displayName}`,
307-
},
308-
async () => {
309-
await this.activateEnvironmentOnCreation(newTerminal, environment);
310-
},
311-
);
312-
} catch (e) {
313-
traceError('Failed to activate environment:\r\n', e);
314-
showErrorMessage(`Failed to activate ${environment.displayName}`);
315-
}
263+
if (options.disableActivation) {
264+
this.skipActivationOnOpen.add(newTerminal);
265+
return newTerminal;
316266
}
317267

268+
// We add it to skip activation on open to prevent double activation.
269+
// We can activate it ourselves since we are creating it.
270+
this.skipActivationOnOpen.add(newTerminal);
271+
await this.autoActivateOnTerminalOpen(newTerminal, environment);
272+
318273
return newTerminal;
319274
}
320275

@@ -462,25 +417,15 @@ export class TerminalManagerImpl implements TerminalManager {
462417
}
463418
}
464419

465-
public async initialize(projects: PythonProject[], em: EnvironmentManagers): Promise<void> {
420+
public async initialize(): Promise<void> {
466421
const config = getConfiguration('python');
467422
if (config.get<boolean>('terminal.activateEnvInCurrentTerminal', false)) {
468423
await Promise.all(
469424
terminals().map(async (t) => {
470-
if (projects.length === 0) {
471-
const manager = em.getEnvironmentManager(undefined);
472-
const env = await manager?.get(undefined);
473-
if (env) {
474-
return this.activate(t, env);
475-
}
476-
} else if (projects.length === 1) {
477-
const manager = em.getEnvironmentManager(projects[0].uri);
478-
const env = await manager?.get(projects[0].uri);
479-
if (env) {
480-
return this.activate(t, env);
481-
}
482-
} else {
483-
// TODO: handle multi project case
425+
this.skipActivationOnOpen.add(t);
426+
const env = await this.getActivationEnvironment();
427+
if (env && isActivatableEnvironment(env)) {
428+
await this.activate(t, env);
484429
}
485430
}),
486431
);

src/features/terminal/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { Terminal } from 'vscode';
2+
import { sleep } from '../../common/utils/asyncUtils';
3+
4+
const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds
5+
const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds
6+
7+
export async function waitForShellIntegration(terminal: Terminal): Promise<boolean> {
8+
let timeout = 0;
9+
while (!terminal.shellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) {
10+
await sleep(SHELL_INTEGRATION_POLL_INTERVAL);
11+
timeout += SHELL_INTEGRATION_POLL_INTERVAL;
12+
}
13+
return terminal.shellIntegration !== undefined;
14+
}

0 commit comments

Comments
 (0)