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
1 change: 1 addition & 0 deletions news/1 Enhancements/1387.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Auto activate Python Environment in terminals.
3 changes: 3 additions & 0 deletions src/client/common/application/terminalManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export class TerminalManager implements ITerminalManager {
public get onDidCloseTerminal(): Event<Terminal> {
return window.onDidCloseTerminal;
}
public get onDidOpenTerminal(): Event<Terminal> {
return window.onDidOpenTerminal;
}
public createTerminal(options: TerminalOptions): Terminal {
return window.createTerminal(options);
}
Expand Down
5 changes: 5 additions & 0 deletions src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,11 @@ export interface ITerminalManager {
* An [event](#Event) which fires when a terminal is disposed.
*/
readonly onDidCloseTerminal: Event<Terminal>;
/**
* An [event](#Event) which fires when a terminal has been created, either through the
* [createTerminal](#window.createTerminal) API or commands.
*/
readonly onDidOpenTerminal: Event<Terminal>;
/**
* Creates a [Terminal](#Terminal). The cwd of the terminal will be the workspace directory
* if it exists, regardless of whether an explicit customStartPath setting exists.
Expand Down
2 changes: 1 addition & 1 deletion src/client/common/terminal/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { ITerminalService, ITerminalServiceFactory } from './types';
export class TerminalServiceFactory implements ITerminalServiceFactory {
private terminalServices: Map<string, ITerminalService>;

constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) {
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {

this.terminalServices = new Map<string, ITerminalService>();
}
Expand Down
25 changes: 25 additions & 0 deletions src/client/common/terminal/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import { inject, injectable } from 'inversify';
import { Terminal, Uri } from 'vscode';
import { sleep } from '../../../utils/async';
import { ICondaService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { ITerminalManager, IWorkspaceService } from '../application/types';
Expand All @@ -29,9 +30,11 @@ const IS_TCSHELL = /(tcsh$)/i;
@injectable()
export class TerminalHelper implements ITerminalHelper {
private readonly detectableShells: Map<TerminalShellType, RegExp>;
private readonly activatedTerminals: Set<Terminal>;
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) {

this.detectableShells = new Map<TerminalShellType, RegExp>();
this.activatedTerminals = new Set<Terminal>();
this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL);
this.detectableShells.set(TerminalShellType.gitbash, IS_GITBASH);
this.detectableShells.set(TerminalShellType.bash, IS_BASH);
Expand Down Expand Up @@ -108,4 +111,26 @@ export class TerminalHelper implements ITerminalHelper {
}
}
}

public async activateEnvironmentInTerminal(terminal: Terminal, preserveFocus: boolean = true, resource?: Uri) {
if (this.activatedTerminals.has(terminal)) {
return;
}
this.activatedTerminals.add(terminal);
const shellPath = this.getTerminalShellPath();
const terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.identifyTerminalShell(shellPath);

const activationCommamnds = await this.getEnvironmentActivationCommands(terminalShellType, resource);
if (activationCommamnds) {
for (const command of activationCommamnds!) {
terminal.show(preserveFocus);
terminal.sendText(command);

// Give the command some time to complete.
// Its been observed that sending commands too early will strip some text off in VS Terminal.
const delay = (terminalShellType === TerminalShellType.powershell || TerminalShellType.powershellCore) ? 1000 : 500;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would really love to see if we can get an event from the shell when it is 'ready' instead of this sleep stuff. Is there any issue related to this that VSCode knows about/can do anything about?

Do we know why this is the case at all?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we can get an event from the shell when it is 'ready' instead o

Not available.

Do we know why this is the case at all?

Powershell is slow. VS Code doesn't get any feedback when process in terminal has completed. Basically the stream is ready to write anytime.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And we just use the VS Code api (which in turn doesn't provide any feedback on when stuff is ready or not, explained above).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, is there anything we can do, any event available we can hook into, to get rid of that sleep?

This has been an ongoing issue in VSC. Unfortunately nothing we can do at this stage.

await sleep(delay);
}
}
}
}
16 changes: 2 additions & 14 deletions src/client/common/terminal/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

import { inject, injectable } from 'inversify';
import { Disposable, Event, EventEmitter, Terminal, Uri } from 'vscode';
import { sleep } from '../../../utils/async';
import '../../common/extensions';
import { IInterpreterService } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
Expand All @@ -21,7 +20,7 @@ export class TerminalService implements ITerminalService, Disposable {
private terminalManager: ITerminalManager;
private terminalHelper: ITerminalHelper;
public get onDidCloseTerminal(): Event<void> {
return this.terminalClosed.event;
return this.terminalClosed.event.bind(this.terminalClosed);
}
constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer,
private resource?: Uri,
Expand Down Expand Up @@ -64,18 +63,7 @@ export class TerminalService implements ITerminalService, Disposable {
// Sometimes the terminal takes some time to start up before it can start accepting input.
await new Promise(resolve => setTimeout(resolve, 100));

const activationCommamnds = await this.terminalHelper.getEnvironmentActivationCommands(this.terminalShellType, this.resource);
if (activationCommamnds) {
for (const command of activationCommamnds!) {
this.terminal!.show(preserveFocus);
this.terminal!.sendText(command);

// Give the command some time to complete.
// Its been observed that sending commands too early will strip some text off.
const delay = (this.terminalShellType === TerminalShellType.powershell || TerminalShellType.powershellCore) ? 1000 : 500;
await sleep(delay);
}
}
await this.terminalHelper.activateEnvironmentInTerminal(this.terminal!, preserveFocus, this.resource);

this.terminal!.show(preserveFocus);

Expand Down
1 change: 1 addition & 0 deletions src/client/common/terminal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export interface ITerminalHelper {
getTerminalShellPath(): string;
buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string;
getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise<string[] | undefined>;
activateEnvironmentInTerminal(terminal: Terminal, preserveFocus?: boolean, resource?: Uri): Promise<void>;
}

export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCommandProvider');
Expand Down
3 changes: 2 additions & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibra
import { sendTelemetryEvent } from './telemetry';
import { EDITOR_LOAD } from './telemetry/constants';
import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry';
import { ICodeExecutionManager } from './terminals/types';
import { ICodeExecutionManager, ITerminalAutoActivation } from './terminals/types';
import { BlockFormatProviders } from './typeFormatters/blockFormatProvider';
import { OnEnterFormatter } from './typeFormatters/onEnterFormatter';
import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants';
Expand All @@ -83,6 +83,7 @@ export async function activate(context: ExtensionContext) {
interpreterManager.initialize();
await interpreterManager.autoSetInterpreter();

serviceManager.get<ITerminalAutoActivation>(ITerminalAutoActivation).register();
const configuration = serviceManager.get<IConfigurationService>(IConfigurationService);
const pythonSettings = configuration.getSettings();

Expand Down
29 changes: 29 additions & 0 deletions src/client/terminals/activation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';

import { inject, injectable } from 'inversify';
import { Disposable, Terminal } from 'vscode';
import { ITerminalManager } from '../common/application/types';
import { ITerminalHelper } from '../common/terminal/types';
import { IDisposableRegistry } from '../common/types';
import { IServiceContainer } from '../ioc/types';
import { ITerminalAutoActivation } from './types';

@injectable()
export class TerminalAutoActivation implements ITerminalAutoActivation {
private readonly helper: ITerminalHelper;
constructor(@inject(IServiceContainer) private container: IServiceContainer) {
this.helper = container.get<ITerminalHelper>(ITerminalHelper);
}
public register() {
const manager = this.container.get<ITerminalManager>(ITerminalManager);
const disposables = this.container.get<Disposable[]>(IDisposableRegistry);
const disposable = manager.onDidOpenTerminal(this.activateTerminal, this);
disposables.push(disposable);
}
private activateTerminal(terminal: Terminal): Promise<void> {
return this.helper.activateEnvironmentInTerminal(terminal);
}
}
4 changes: 3 additions & 1 deletion src/client/terminals/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@
// Licensed under the MIT License.

import { IServiceManager } from '../ioc/types';
import { TerminalAutoActivation } from './activation';
import { CodeExecutionManager } from './codeExecution/codeExecutionManager';
import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution';
import { CodeExecutionHelper } from './codeExecution/helper';
import { ReplProvider } from './codeExecution/repl';
import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution';
import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from './types';
import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService, ITerminalAutoActivation } from './types';

export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<ICodeExecutionHelper>(ICodeExecutionHelper, CodeExecutionHelper);
serviceManager.addSingleton<ICodeExecutionManager>(ICodeExecutionManager, CodeExecutionManager);
serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell');
serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, TerminalCodeExecutionProvider, 'standard');
serviceManager.addSingleton<ICodeExecutionService>(ICodeExecutionService, ReplProvider, 'repl');
serviceManager.addSingleton<ITerminalAutoActivation>(ITerminalAutoActivation, TerminalAutoActivation);
}
5 changes: 5 additions & 0 deletions src/client/terminals/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ export const ICodeExecutionManager = Symbol('ICodeExecutionManager');
export interface ICodeExecutionManager {
registerCommands(): void;
}

export const ITerminalAutoActivation = Symbol('ITerminalAutoActivation');
export interface ITerminalAutoActivation {
register(): void;
}
61 changes: 61 additions & 0 deletions src/test/common/terminals/activation.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

'use strict';
import { expect } from 'chai';
import * as TypeMoq from 'typemoq';
import { Terminal } from 'vscode';
import { ITerminalManager } from '../../../client/common/application/types';
import { ITerminalHelper } from '../../../client/common/terminal/types';
import { IDisposableRegistry } from '../../../client/common/types';
import { IServiceContainer } from '../../../client/ioc/types';
import { TerminalAutoActivation } from '../../../client/terminals/activation';
import { ITerminalAutoActivation } from '../../../client/terminals/types';
import { noop } from '../../../utils/misc';

suite('Terminal Auto Activation', () => {
let helper: TypeMoq.IMock<ITerminalHelper>;
let terminalManager: TypeMoq.IMock<ITerminalManager>;
let terminalAutoActivation: ITerminalAutoActivation;

setup(() => {
terminalManager = TypeMoq.Mock.ofType<ITerminalManager>();
helper = TypeMoq.Mock.ofType<ITerminalHelper>();
const disposables = [];

const serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
serviceContainer
.setup(c => c.get(TypeMoq.It.isValue(ITerminalManager), TypeMoq.It.isAny()))
.returns(() => terminalManager.object);
serviceContainer
.setup(c => c.get(TypeMoq.It.isValue(ITerminalHelper), TypeMoq.It.isAny()))
.returns(() => helper.object);
serviceContainer
.setup(c => c.get(TypeMoq.It.isValue(IDisposableRegistry), TypeMoq.It.isAny()))
.returns(() => disposables);

terminalAutoActivation = new TerminalAutoActivation(serviceContainer.object);
});

test('New Terminals should be activated', async () => {
let eventHandler: undefined | ((e: Terminal) => void);
const terminal = TypeMoq.Mock.ofType<Terminal>();
terminalManager
.setup(m => m.onDidOpenTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.returns(handler => {
eventHandler = handler;
return { dispose: noop };
});
helper
.setup(h => h.activateEnvironmentInTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()))
.verifiable(TypeMoq.Times.once());

terminalAutoActivation.register();

expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized');

eventHandler!.bind(terminalAutoActivation)(terminal.object);

helper.verifyAll();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import { expect } from 'chai';
import * as TypeMoq from 'typemoq';
import { Disposable } from 'vscode';
import { Disposable, Terminal } from 'vscode';
import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types';
import { IPlatformService } from '../../../client/common/platform/types';
import { Bash } from '../../../client/common/terminal/environmentActivationProviders/bash';
Expand Down Expand Up @@ -74,6 +74,61 @@ suite('Terminal Service helpers', () => {

expect(commands).to.equal(undefined, 'Activation command should be undefined if terminal type cannot be determined');
});
[
{ commandCount: 1, preserveFocus: false },
{ commandCount: 2, preserveFocus: false },
{ commandCount: 1, preserveFocus: true },
{ commandCount: 1, preserveFocus: true }
].forEach(item => {
const titleSuffix = `(${item.commandCount} activation command, and preserve focus in terminal is ${item.preserveFocus})`;
const activationCommands = item.commandCount === 1 ? ['CMD1'] : ['CMD1', 'CMD2'];
test(`Terminal is activated ${titleSuffix}`, async function () {
// tslint:disable-next-line:no-invalid-this
this.timeout(10000); // We have delays in place to account for issues with VSC Terminal.
helper.getEnvironmentActivationCommands = (_shellType, _resource) => Promise.resolve(activationCommands);
helper.getTerminalShellPath = () => '';
const terminal = TypeMoq.Mock.ofType<Terminal>();

terminal
.setup(t => t.show(TypeMoq.It.isValue(item.preserveFocus)))
.returns(() => undefined)
.verifiable(TypeMoq.Times.exactly(activationCommands.length));
activationCommands.forEach(cmd => {
terminal
.setup(t => t.sendText(TypeMoq.It.isValue(cmd)))
.returns(() => undefined)
.verifiable(TypeMoq.Times.exactly(1));
});

await helper.activateEnvironmentInTerminal(terminal.object, item.preserveFocus);

terminal.verifyAll();
});
test(`Terminal is activated only once ${titleSuffix}`, async function () {
// tslint:disable-next-line:no-invalid-this
this.timeout(10000); // We have delays in place to account for issues with VSC Terminal.
helper.getEnvironmentActivationCommands = (_shellType, _resource) => Promise.resolve(activationCommands);
helper.getTerminalShellPath = () => '';
const terminal = TypeMoq.Mock.ofType<Terminal>();

terminal
.setup(t => t.show(TypeMoq.It.isValue(item.preserveFocus)))
.returns(() => undefined)
.verifiable(TypeMoq.Times.exactly(activationCommands.length));
activationCommands.forEach(cmd => {
terminal
.setup(t => t.sendText(TypeMoq.It.isValue(cmd)))
.returns(() => undefined)
.verifiable(TypeMoq.Times.exactly(1));
});

await helper.activateEnvironmentInTerminal(terminal.object, item.preserveFocus);
await helper.activateEnvironmentInTerminal(terminal.object, item.preserveFocus);
await helper.activateEnvironmentInTerminal(terminal.object, item.preserveFocus);

terminal.verifyAll();
});
});
});

getNamesAndValues<TerminalShellType>(TerminalShellType).forEach(terminalShell => {
Expand Down
Loading