Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
171 changes: 58 additions & 113 deletions src/features/terminal/terminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,26 @@ import {
Terminal,
TerminalShellExecutionEndEvent,
TerminalShellExecutionStartEvent,
TerminalShellIntegrationChangeEvent,
Uri,
TerminalOptions,
} 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 +36,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 +58,7 @@ export interface TerminalEnvironment {
}

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

export interface TerminalManager
Expand All @@ -78,33 +74,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 +104,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) || (t.creationOptions as TerminalOptions)?.hideFromUser) {
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 +214,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 +259,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 +417,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