Skip to content

Commit 940acb1

Browse files
authored
Activation of terminal using conda activate and fixes to powershell script execution (#665)
fixed conda activation on windows
1 parent 8e0e645 commit 940acb1

28 files changed

+1013
-312
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,12 @@
14871487
"description": "When executing a file in the terminal, whether to use execute in the file's directory, instead of the current open folder.",
14881488
"scope": "resource"
14891489
},
1490+
"python.terminal.activateEnvironment": {
1491+
"type": "boolean",
1492+
"default": true,
1493+
"description": "Activate Python Environment in Terminal created using the Extension.",
1494+
"scope": "resource"
1495+
},
14901496
"python.terminal.launchArgs": {
14911497
"type": "array",
14921498
"default": [],

src/client/common/configSettings.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,11 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
282282
this.terminal = {} as ITerminalSettings;
283283
}
284284
}
285-
// Support for travis
285+
// Support for travis.
286286
this.terminal = this.terminal ? this.terminal : {
287287
executeInFileDir: true,
288-
launchArgs: []
288+
launchArgs: [],
289+
activateEnvironment: true
289290
};
290291

291292
// If workspace config changes, then we could have a cascading effect of on change events.

src/client/common/installer/condaInstaller.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { inject, injectable } from 'inversify';
55
import { Uri } from 'vscode';
66
import { ICondaService, IInterpreterService, InterpreterType } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
8-
import { ExecutionInfo } from '../types';
8+
import { ExecutionInfo, IConfigurationService } from '../types';
99
import { ModuleInstaller } from './moduleInstaller';
1010
import { IModuleInstaller } from './types';
1111

@@ -41,21 +41,21 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
4141
return this.isCurrentEnvironmentACondaEnvironment(resource);
4242
}
4343
protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> {
44-
const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService);
45-
const condaFile = await condaLocator.getCondaFile();
44+
const condaService = this.serviceContainer.get<ICondaService>(ICondaService);
45+
const condaFile = await condaService.getCondaFile();
4646

47-
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
48-
const info = await interpreterService.getActiveInterpreter(resource);
47+
const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath;
48+
const info = await condaService.getCondaEnvironment(pythonPath);
4949
const args = ['install'];
5050

51-
if (info!.envName) {
51+
if (info.name) {
5252
// If we have the name of the conda environment, then use that.
5353
args.push('--name');
54-
args.push(info!.envName!);
55-
} else {
54+
args.push(info.name!);
55+
} else if (info.path) {
5656
// Else provide the full path to the environment path.
5757
args.push('--prefix');
58-
args.push(info!.envPath!);
58+
args.push(info.path);
5959
}
6060
args.push(moduleName);
6161
return {
@@ -64,9 +64,9 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
6464
moduleName: ''
6565
};
6666
}
67-
private isCurrentEnvironmentACondaEnvironment(resource?: Uri) {
68-
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
69-
return interpreterService.getActiveInterpreter(resource)
70-
.then(info => info ? info.type === InterpreterType.Conda : false).catch(() => false);
67+
private async isCurrentEnvironmentACondaEnvironment(resource?: Uri): Promise<boolean> {
68+
const condaService = this.serviceContainer.get<ICondaService>(ICondaService);
69+
const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath;
70+
return condaService.isCondaEnvironment(pythonPath);
7171
}
7272
}

src/client/common/terminal/environmentActivationProviders/baseActivationProvider.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@
33

44
import { injectable } from 'inversify';
55
import * as path from 'path';
6+
import { Uri } from 'vscode';
67
import { PythonInterpreter } from '../../../interpreter/contracts';
78
import { IServiceContainer } from '../../../ioc/types';
89
import { IFileSystem } from '../../platform/types';
10+
import { IConfigurationService } from '../../types';
911
import { TerminalShellType } from '../types';
1012
import { ITerminalActivationCommandProvider } from '../types';
1113

@@ -14,14 +16,15 @@ export abstract class BaseActivationCommandProvider implements ITerminalActivati
1416
constructor(protected readonly serviceContainer: IServiceContainer) { }
1517

1618
public abstract isShellSupported(targetShell: TerminalShellType): boolean;
17-
public abstract getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined>;
19+
public abstract getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined>;
1820

19-
protected async findScriptFile(interpreter: PythonInterpreter, scriptFileNames: string[]): Promise<string | undefined> {
21+
protected async findScriptFile(resource: Uri | undefined, scriptFileNames: string[]): Promise<string | undefined> {
2022
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
23+
const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath;
2124

2225
for (const scriptFileName of scriptFileNames) {
2326
// Generate scripts are found in the same directory as the interpreter.
24-
const scriptFile = path.join(path.dirname(interpreter.path), scriptFileName);
27+
const scriptFile = path.join(path.dirname(pythonPath), scriptFileName);
2528
const found = await fs.fileExistsAsync(scriptFile);
2629
if (found) {
2730
return scriptFile;

src/client/common/terminal/environmentActivationProviders/bash.ts

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5+
import { Uri } from 'vscode';
56
import { PythonInterpreter } from '../../../interpreter/contracts';
67
import { IServiceContainer } from '../../../ioc/types';
78
import '../../extensions';
@@ -18,15 +19,12 @@ export class Bash extends BaseActivationCommandProvider {
1819
targetShell === TerminalShellType.cshell ||
1920
targetShell === TerminalShellType.fish;
2021
}
21-
public async getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined> {
22-
const scriptFile = await this.findScriptFile(interpreter, this.getScriptsInOrderOfPreference(targetShell));
22+
public async getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined> {
23+
const scriptFile = await this.findScriptFile(resource, this.getScriptsInOrderOfPreference(targetShell));
2324
if (!scriptFile) {
2425
return;
2526
}
26-
const envName = interpreter.envName ? interpreter.envName! : '';
27-
// In the case of conda environments, the name of the environment must be provided.
28-
// E.g. `source acrtivate <envname>`.
29-
return [`source ${scriptFile.toCommandArgument()} ${envName}`.trim()];
27+
return [`source ${scriptFile.toCommandArgument()}`];
3028
}
3129

3230
private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] {

src/client/common/terminal/environmentActivationProviders/commandPrompt.ts

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5+
import * as path from 'path';
6+
import { Uri } from 'vscode';
57
import { PythonInterpreter } from '../../../interpreter/contracts';
68
import { IServiceContainer } from '../../../ioc/types';
79
import '../../extensions';
@@ -16,37 +18,46 @@ export class CommandPromptAndPowerShell extends BaseActivationCommandProvider {
1618
}
1719
public isShellSupported(targetShell: TerminalShellType): boolean {
1820
return targetShell === TerminalShellType.commandPrompt ||
19-
targetShell === TerminalShellType.powershell;
21+
targetShell === TerminalShellType.powershell ||
22+
targetShell === TerminalShellType.powershellCore;
2023
}
21-
public async getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined> {
24+
public async getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined> {
2225
// Dependending on the target shell, look for the preferred script file.
23-
const scriptsInOrderOfPreference = targetShell === TerminalShellType.commandPrompt ? ['activate.bat', 'activate.ps1'] : ['activate.ps1', 'activate.bat'];
24-
const scriptFile = await this.findScriptFile(interpreter, scriptsInOrderOfPreference);
26+
const scriptFile = await this.findScriptFile(resource, this.getScriptsInOrderOfPreference(targetShell));
2527
if (!scriptFile) {
2628
return;
2729
}
2830

29-
const envName = interpreter.envName ? interpreter.envName! : '';
30-
3131
if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.bat')) {
32-
return [`${scriptFile.toCommandArgument()} ${envName}`.trim()];
33-
} else if (targetShell === TerminalShellType.powershell && scriptFile.endsWith('activate.ps1')) {
34-
return [`${scriptFile.toCommandArgument()} ${envName}`.trim()];
32+
return [`${scriptFile.toCommandArgument()}`];
33+
} else if ((targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) && scriptFile.endsWith('activate.ps1')) {
34+
return [`& ${scriptFile.toCommandArgument()}`];
3535
} else if (targetShell === TerminalShellType.commandPrompt && scriptFile.endsWith('activate.ps1')) {
36-
return [`powershell ${scriptFile.toCommandArgument()} ${envName}`.trim()];
36+
// lets not try to run the powershell file from command prompt (user may not have powershell)
37+
return [];
3738
} else {
3839
// This means we're in powershell and we have a .bat file.
3940
if (this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows) {
4041
// On windows, the solution is to go into cmd, then run the batch (.bat) file and go back into powershell.
42+
const powershellExe = targetShell === TerminalShellType.powershell ? 'powershell' : 'pwsh';
43+
const activationCmd = `${scriptFile.toCommandArgument()}`;
4144
return [
42-
'cmd',
43-
`${scriptFile.toCommandArgument()} ${envName}`.trim(),
44-
'powershell'
45+
`& cmd /k "${activationCmd} & ${powershellExe}"`
4546
];
4647
} else {
4748
// Powershell on non-windows os, we cannot execute the batch file.
4849
return;
4950
}
5051
}
5152
}
53+
54+
private getScriptsInOrderOfPreference(targetShell: TerminalShellType): string[] {
55+
const batchFiles = ['activate.bat', path.join('Scripts', 'activate.bat'), path.join('scripts', 'activate.bat')];
56+
const powerShellFiles = ['activate.ps1', path.join('Scripts', 'activate.ps1'), path.join('scripts', 'activate.ps1')];
57+
if (targetShell === TerminalShellType.commandPrompt) {
58+
return batchFiles.concat(powerShellFiles);
59+
} else {
60+
return powerShellFiles.concat(batchFiles);
61+
}
62+
}
5263
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { injectable } from 'inversify';
5+
import { Uri } from 'vscode';
6+
import { ICondaService } from '../../../interpreter/contracts';
7+
import { IServiceContainer } from '../../../ioc/types';
8+
import '../../extensions';
9+
import { IPlatformService } from '../../platform/types';
10+
import { IConfigurationService } from '../../types';
11+
import { TerminalShellType } from '../types';
12+
import { ITerminalActivationCommandProvider } from '../types';
13+
14+
@injectable()
15+
export class CondaActivationCommandProvider implements ITerminalActivationCommandProvider {
16+
constructor(private readonly serviceContainer: IServiceContainer) { }
17+
18+
public isShellSupported(_targetShell: TerminalShellType): boolean {
19+
return true;
20+
}
21+
public async getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined> {
22+
const condaService = this.serviceContainer.get<ICondaService>(ICondaService);
23+
const pythonPath = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource).pythonPath;
24+
25+
const envInfo = await condaService.getCondaEnvironment(pythonPath);
26+
if (!envInfo) {
27+
return;
28+
}
29+
30+
const isWindows = this.serviceContainer.get<IPlatformService>(IPlatformService).isWindows;
31+
if (targetShell === TerminalShellType.powershell || targetShell === TerminalShellType.powershellCore) {
32+
// https://github.com/conda/conda/issues/626
33+
return;
34+
} else if (targetShell === TerminalShellType.fish) {
35+
// https://github.com/conda/conda/blob/be8c08c083f4d5e05b06bd2689d2cd0d410c2ffe/shell/etc/fish/conf.d/conda.fish#L18-L28
36+
return [`conda activate ${envInfo.name.toCommandArgument()}`];
37+
} else if (isWindows) {
38+
return [`activate ${envInfo.name.toCommandArgument()}`];
39+
} else {
40+
return [`source activate ${envInfo.name.toCommandArgument()}`];
41+
}
42+
}
43+
}

src/client/common/terminal/helper.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@
33

44
import { inject, injectable } from 'inversify';
55
import { Terminal, Uri } from 'vscode';
6-
import { IInterpreterService } from '../../interpreter/contracts';
6+
import { ICondaService } from '../../interpreter/contracts';
77
import { IServiceContainer } from '../../ioc/types';
88
import { ITerminalManager, IWorkspaceService } from '../application/types';
99
import '../extensions';
1010
import { IPlatformService } from '../platform/types';
11+
import { IConfigurationService } from '../types';
12+
import { CondaActivationCommandProvider } from './environmentActivationProviders/condaActivationProvider';
1113
import { ITerminalActivationCommandProvider, ITerminalHelper, TerminalShellType } from './types';
1214

1315
// Types of shells can be found here:
1416
// 1. https://wiki.ubuntu.com/ChangingShells
1517
const IS_BASH = /(bash.exe$|wsl.exe$|bash$|zsh$|ksh$)/i;
1618
const IS_COMMAND = /cmd.exe$/i;
17-
const IS_POWERSHELL = /(powershell.exe$|pwsh$|powershell$)/i;
19+
const IS_POWERSHELL = /(powershell.exe$|powershell$)/i;
20+
const IS_POWERSHELL_CORE = /(pwsh.exe$|pwsh$)/i;
1821
const IS_FISH = /(fish$)/i;
1922
const IS_CSHELL = /(csh$)/i;
2023

@@ -29,6 +32,7 @@ export class TerminalHelper implements ITerminalHelper {
2932
this.detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND);
3033
this.detectableShells.set(TerminalShellType.fish, IS_FISH);
3134
this.detectableShells.set(TerminalShellType.cshell, IS_CSHELL);
35+
this.detectableShells.set(TerminalShellType.powershellCore, IS_POWERSHELL_CORE);
3236
}
3337
public createTerminal(title?: string): Terminal {
3438
const terminalManager = this.serviceContainer.get<ITerminalManager>(ITerminalManager);
@@ -62,23 +66,33 @@ export class TerminalHelper implements ITerminalHelper {
6266
return shellConfig.get<string>(osSection)!;
6367
}
6468
public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) {
65-
const isPowershell = terminalShellType === TerminalShellType.powershell;
69+
const isPowershell = terminalShellType === TerminalShellType.powershell || terminalShellType === TerminalShellType.powershellCore;
6670
const commandPrefix = isPowershell ? '& ' : '';
6771
return `${commandPrefix}${command.toCommandArgument()} ${args.join(' ')}`.trim();
6872
}
6973
public async getEnvironmentActivationCommands(terminalShellType: TerminalShellType, resource?: Uri): Promise<string[] | undefined> {
70-
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
71-
const interperterInfo = await interpreterService.getActiveInterpreter(resource);
72-
if (!interperterInfo) {
74+
const settings = this.serviceContainer.get<IConfigurationService>(IConfigurationService).getSettings(resource);
75+
const activateEnvironment = settings.terminal.activateEnvironment;
76+
if (!activateEnvironment) {
7377
return;
7478
}
7579

80+
// If we have a conda environment, then use that.
81+
const isCondaEnvironment = await this.serviceContainer.get<ICondaService>(ICondaService).isCondaEnvironment(settings.pythonPath);
82+
if (isCondaEnvironment) {
83+
const condaActivationProvider = new CondaActivationCommandProvider(this.serviceContainer);
84+
const activationCommands = await condaActivationProvider.getActivationCommands(resource, terminalShellType);
85+
if (Array.isArray(activationCommands)) {
86+
return activationCommands;
87+
}
88+
}
89+
7690
// Search from the list of providers.
7791
const providers = this.serviceContainer.getAll<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider);
7892
const supportedProviders = providers.filter(provider => provider.isShellSupported(terminalShellType));
7993

8094
for (const provider of supportedProviders) {
81-
const activationCommands = await provider.getActivationCommands(interperterInfo, terminalShellType);
95+
const activationCommands = await provider.getActivationCommands(resource, terminalShellType);
8296
if (Array.isArray(activationCommands)) {
8397
return activationCommands;
8498
}

src/client/common/terminal/service.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class TerminalService implements ITerminalService, Disposable {
6262
const activationCommamnds = await this.terminalHelper.getEnvironmentActivationCommands(this.terminalShellType, this.resource);
6363
if (activationCommamnds) {
6464
for (const command of activationCommamnds!) {
65+
this.terminal!.show(true);
6566
this.terminal!.sendText(command);
6667

6768
// Give the command some time to complete.

src/client/common/terminal/types.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import { PythonInterpreter } from '../../interpreter/contracts';
77

88
export enum TerminalShellType {
99
powershell = 1,
10-
commandPrompt = 2,
11-
bash = 3,
12-
fish = 4,
13-
cshell = 5,
14-
other = 6
10+
powershellCore = 2,
11+
commandPrompt = 3,
12+
bash = 4,
13+
fish = 5,
14+
cshell = 6,
15+
other = 7
1516
}
1617

1718
export interface ITerminalService {
@@ -50,5 +51,5 @@ export const ITerminalActivationCommandProvider = Symbol('ITerminalActivationCom
5051

5152
export interface ITerminalActivationCommandProvider {
5253
isShellSupported(targetShell: TerminalShellType): boolean;
53-
getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined>;
54+
getActivationCommands(resource: Uri | undefined, targetShell: TerminalShellType): Promise<string[] | undefined>;
5455
}

0 commit comments

Comments
 (0)