Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions examples/sample1/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,9 +1000,9 @@ export interface PythonProjectModifyApi {
*/
export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {}

export interface PythonTerminalOptions extends TerminalOptions {
export interface PythonTerminalCreateOptions extends TerminalOptions {
/**
* Whether to show the terminal.
* Whether to disable activation on create.
*/
disableActivation?: boolean;
}
Expand All @@ -1016,7 +1016,7 @@ export interface PythonTerminalCreateApi {
*
* Note: Non-activatable environments have no effect on the terminal.
*/
createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1000,9 +1000,9 @@ export interface PythonProjectModifyApi {
*/
export interface PythonProjectApi extends PythonProjectCreationApi, PythonProjectGetterApi, PythonProjectModifyApi {}

export interface PythonTerminalOptions extends TerminalOptions {
export interface PythonTerminalCreateOptions extends TerminalOptions {
/**
* Whether to show the terminal.
* Whether to disable activation on create.
*/
disableActivation?: boolean;
}
Expand All @@ -1016,7 +1016,7 @@ export interface PythonTerminalCreateApi {
*
* Note: Non-activatable environments have no effect on the terminal.
*/
createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
}

/**
Expand Down
7 changes: 4 additions & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,6 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
const statusBar = new PythonStatusBarImpl();
context.subscriptions.push(statusBar);

const terminalManager: TerminalManager = new TerminalManagerImpl();
context.subscriptions.push(terminalManager);

const projectManager: PythonProjectManager = new PythonProjectManagerImpl();
context.subscriptions.push(projectManager);

Expand All @@ -82,6 +79,9 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
const envManagers: EnvironmentManagers = new PythonEnvironmentManagers(projectManager);
context.subscriptions.push(envManagers);

const terminalManager: TerminalManager = new TerminalManagerImpl(projectManager, envManagers);
context.subscriptions.push(terminalManager);

const projectCreators: ProjectCreators = new ProjectCreatorsImpl();
context.subscriptions.push(
projectCreators,
Expand Down Expand Up @@ -229,6 +229,7 @@ export async function activate(context: ExtensionContext): Promise<PythonEnviron
registerCondaFeatures(nativeFinder, context.subscriptions, outputChannel),
]);
sendTelemetryEvent(EventNames.EXTENSION_MANAGER_REGISTRATION_DURATION, start.elapsedTime);
await terminalManager.initialize();
});

sendTelemetryEvent(EventNames.EXTENSION_ACTIVATION_DURATION, start.elapsedTime);
Expand Down
4 changes: 2 additions & 2 deletions src/features/pythonApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import {
PythonTaskExecutionOptions,
PythonTerminalExecutionOptions,
PythonBackgroundRunOptions,
PythonTerminalOptions,
PythonTerminalCreateOptions,
DidChangeEnvironmentVariablesEventArgs,
} from '../api';
import {
Expand Down Expand Up @@ -275,7 +275,7 @@ class PythonEnvironmentApiImpl implements PythonEnvironmentApi {
registerPythonProjectCreator(creator: PythonProjectCreator): Disposable {
return this.projectCreators.registerPythonProjectCreator(creator);
}
async createTerminal(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal> {
async createTerminal(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal> {
return this.terminalManager.create(environment, options);
}
async runInTerminal(environment: PythonEnvironment, options: PythonTerminalExecutionOptions): Promise<Terminal> {
Expand Down
170 changes: 57 additions & 113 deletions src/features/terminal/terminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,25 @@ import {
Terminal,
TerminalShellExecutionEndEvent,
TerminalShellExecutionStartEvent,
TerminalShellIntegrationChangeEvent,
Uri,
} from 'vscode';
import {
createTerminal,
onDidChangeTerminalShellIntegration,
onDidCloseTerminal,
onDidEndTerminalShellExecution,
onDidOpenTerminal,
onDidStartTerminalShellExecution,
terminals,
withProgress,
} from '../../common/window.apis';
import { PythonEnvironment, PythonProject, PythonTerminalOptions } from '../../api';
import { PythonEnvironment, PythonProject, PythonTerminalCreateOptions } from '../../api';
import { getActivationCommand, getDeactivationCommand, isActivatableEnvironment } from '../common/activation';
import { showErrorMessage } from '../../common/errors/utils';
import { quoteArgs } from '../execution/execUtils';
import { createDeferred } from '../../common/utils/deferred';
import { traceError, traceVerbose } from '../../common/logging';
import { getConfiguration } from '../../common/workspace.apis';
import { EnvironmentManagers } from '../../internal.api';

const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds
const SHELL_INTEGRATION_POLL_INTERVAL = 100; // 0.1 seconds
import { EnvironmentManagers, PythonProjectManager } from '../../internal.api';
import { waitForShellIntegration } from './utils';

export interface TerminalActivation {
isActivated(terminal: Terminal, environment?: PythonEnvironment): boolean;
Expand All @@ -40,7 +35,7 @@ export interface TerminalActivation {
}

export interface TerminalCreation {
create(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal>;
create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal>;
}

export interface TerminalGetters {
Expand All @@ -62,7 +57,7 @@ export interface TerminalEnvironment {
}

export interface TerminalInit {
initialize(projects: PythonProject[], em: EnvironmentManagers): Promise<void>;
initialize(): Promise<void>;
}

export interface TerminalManager
Expand All @@ -78,33 +73,28 @@ export class TerminalManagerImpl implements TerminalManager {
private activatedTerminals = new Map<Terminal, PythonEnvironment>();
private activatingTerminals = new Map<Terminal, Promise<void>>();
private deactivatingTerminals = new Map<Terminal, Promise<void>>();
private skipActivationOnOpen = new Set<Terminal>();

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

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

private onTerminalShellIntegrationChangedEmitter = new EventEmitter<TerminalShellIntegrationChangeEvent>();
private onTerminalShellIntegrationChanged = this.onTerminalShellIntegrationChangedEmitter.event;

private onTerminalShellExecutionStartEmitter = new EventEmitter<TerminalShellExecutionStartEvent>();
private onTerminalShellExecutionStart = this.onTerminalShellExecutionStartEmitter.event;

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

constructor() {
constructor(private readonly projectManager: PythonProjectManager, private readonly em: EnvironmentManagers) {
this.disposables.push(
onDidOpenTerminal((t: Terminal) => {
this.onTerminalOpenedEmitter.fire(t);
}),
onDidCloseTerminal((t: Terminal) => {
this.onTerminalClosedEmitter.fire(t);
}),
onDidChangeTerminalShellIntegration((e: TerminalShellIntegrationChangeEvent) => {
this.onTerminalShellIntegrationChangedEmitter.fire(e);
}),
onDidStartTerminalShellExecution((e: TerminalShellExecutionStartEvent) => {
this.onTerminalShellExecutionStartEmitter.fire(e);
}),
Expand All @@ -113,9 +103,20 @@ export class TerminalManagerImpl implements TerminalManager {
}),
this.onTerminalOpenedEmitter,
this.onTerminalClosedEmitter,
this.onTerminalShellIntegrationChangedEmitter,
this.onTerminalShellExecutionStartEmitter,
this.onTerminalShellExecutionEndEmitter,
this.onTerminalOpened(async (t) => {
if (this.skipActivationOnOpen.has(t)) {
return;
}
await this.autoActivateOnTerminalOpen(t);
}),
this.onTerminalClosed((t) => {
this.activatedTerminals.delete(t);
this.activatingTerminals.delete(t);
this.deactivatingTerminals.delete(t);
this.skipActivationOnOpen.delete(t);
}),
);
}

Expand Down Expand Up @@ -212,75 +213,36 @@ export class TerminalManagerImpl implements TerminalManager {
}
}

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

try {
disposables.push(
new Disposable(() => {
this.activatingTerminals.delete(terminal);
}),
this.onTerminalOpened(async (t: Terminal) => {
if (t === terminal) {
if (terminal.shellIntegration) {
// Shell integration is available when the terminal is opened.
activated = true;
await this.activateUsingShellIntegration(terminal.shellIntegration, terminal, environment);
deferred.resolve();
} else {
let seconds = 0;
const timer = setInterval(() => {
seconds += SHELL_INTEGRATION_POLL_INTERVAL;
if (terminal.shellIntegration || activated) {
disposeTimer?.dispose();
return;
}

if (seconds >= SHELL_INTEGRATION_TIMEOUT) {
disposeTimer?.dispose();
activated = true;
this.activateLegacy(terminal, environment);
deferred.resolve();
}
}, 100);

disposeTimer = new Disposable(() => {
clearInterval(timer);
disposeTimer = undefined;
});
}
}
}),
this.onTerminalShellIntegrationChanged(async (e: TerminalShellIntegrationChangeEvent) => {
if (terminal === e.terminal && !activated) {
disposeTimer?.dispose();
activated = true;
await this.activateUsingShellIntegration(e.shellIntegration, terminal, environment);
deferred.resolve();
}
}),
this.onTerminalClosed((t) => {
if (terminal === t && !deferred.completed) {
deferred.reject(new Error('Terminal closed before activation'));
}
}),
new Disposable(() => {
disposeTimer?.dispose();
}),
private async autoActivateOnTerminalOpen(terminal: Terminal, environment?: PythonEnvironment): Promise<void> {
const config = getConfiguration('python');
if (!config.get<boolean>('terminal.activateEnvironment', false)) {
return;
}

const env = environment ?? (await this.getActivationEnvironment());
if (env && isActivatableEnvironment(env)) {
await withProgress(
{
location: ProgressLocation.Window,
title: `Activating environment: ${env.displayName}`,
},
async () => {
await waitForShellIntegration(terminal);
await this.activate(terminal, env);
},
);
await deferred.promise;
} catch (ex) {
traceError('Failed to activate environment:\r\n', ex);
} finally {
disposables.forEach((d) => d.dispose());
}
}

public async create(environment: PythonEnvironment, options: PythonTerminalOptions): Promise<Terminal> {
public async create(environment: PythonEnvironment, options: PythonTerminalCreateOptions): Promise<Terminal> {
// const name = options.name ?? `Python: ${environment.displayName}`;
const newTerminal = createTerminal({
name: options.name,
Expand All @@ -296,25 +258,17 @@ export class TerminalManagerImpl implements TerminalManager {
location: options.location,
isTransient: options.isTransient,
});
const activatable = !options.disableActivation && isActivatableEnvironment(environment);

if (activatable) {
try {
await withProgress(
{
location: ProgressLocation.Window,
title: `Activating ${environment.displayName}`,
},
async () => {
await this.activateEnvironmentOnCreation(newTerminal, environment);
},
);
} catch (e) {
traceError('Failed to activate environment:\r\n', e);
showErrorMessage(`Failed to activate ${environment.displayName}`);
}
if (options.disableActivation) {
this.skipActivationOnOpen.add(newTerminal);
return newTerminal;
}

// We add it to skip activation on open to prevent double activation.
// We can activate it ourselves since we are creating it.
this.skipActivationOnOpen.add(newTerminal);
await this.autoActivateOnTerminalOpen(newTerminal, environment);

return newTerminal;
}

Expand Down Expand Up @@ -462,25 +416,15 @@ export class TerminalManagerImpl implements TerminalManager {
}
}

public async initialize(projects: PythonProject[], em: EnvironmentManagers): Promise<void> {
public async initialize(): Promise<void> {
const config = getConfiguration('python');
if (config.get<boolean>('terminal.activateEnvInCurrentTerminal', false)) {
await Promise.all(
terminals().map(async (t) => {
if (projects.length === 0) {
const manager = em.getEnvironmentManager(undefined);
const env = await manager?.get(undefined);
if (env) {
return this.activate(t, env);
}
} else if (projects.length === 1) {
const manager = em.getEnvironmentManager(projects[0].uri);
const env = await manager?.get(projects[0].uri);
if (env) {
return this.activate(t, env);
}
} else {
// TODO: handle multi project case
this.skipActivationOnOpen.add(t);
const env = await this.getActivationEnvironment();
if (env && isActivatableEnvironment(env)) {
await this.activate(t, env);
}
}),
);
Expand Down
14 changes: 14 additions & 0 deletions src/features/terminal/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Terminal } from 'vscode';
import { sleep } from '../../common/utils/asyncUtils';

const SHELL_INTEGRATION_TIMEOUT = 500; // 0.5 seconds
const SHELL_INTEGRATION_POLL_INTERVAL = 20; // 0.02 seconds

export async function waitForShellIntegration(terminal: Terminal): Promise<boolean> {
let timeout = 0;
while (!terminal.shellIntegration && timeout < SHELL_INTEGRATION_TIMEOUT) {
await sleep(SHELL_INTEGRATION_POLL_INTERVAL);
timeout += SHELL_INTEGRATION_POLL_INTERVAL;
}
return terminal.shellIntegration !== undefined;
}
Loading