Skip to content

Commit 0253995

Browse files
authored
WIP - Activate environment in terminal (#614)
Fixes #383
1 parent df2b494 commit 0253995

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+2581
-912
lines changed

src/client/common/application/types.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// tslint:disable:no-any unified-signatures
66

77
import * as vscode from 'vscode';
8-
import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions } from 'vscode';
8+
import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions, WorkspaceConfiguration } from 'vscode';
99
import { TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent } from 'vscode';
1010
import { Uri, ViewColumn, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
1111
import { Terminal, TerminalOptions } from 'vscode';
@@ -363,6 +363,11 @@ export interface IWorkspaceService {
363363
*/
364364
readonly onDidChangeWorkspaceFolders: Event<WorkspaceFoldersChangeEvent>;
365365

366+
/**
367+
* An event that is emitted when the [configuration](#WorkspaceConfiguration) changed.
368+
*/
369+
readonly onDidChangeConfiguration: Event<void>;
370+
366371
/**
367372
* Returns the [workspace folder](#WorkspaceFolder) that contains a given uri.
368373
* * returns `undefined` when the given uri doesn't match any workspace folder
@@ -419,6 +424,21 @@ export interface IWorkspaceService {
419424
* [workspace folders](#workspace.workspaceFolders) are opened.
420425
*/
421426
findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable<Uri[]>;
427+
428+
/**
429+
* Get a workspace configuration object.
430+
*
431+
* When a section-identifier is provided only that part of the configuration
432+
* is returned. Dots in the section-identifier are interpreted as child-access,
433+
* like `{ myExt: { setting: { doIt: true }}}` and `getConfiguration('myExt.setting').get('doIt') === true`.
434+
*
435+
* When a resource is provided, configuration scoped to that resource is returned.
436+
*
437+
* @param section A dot-separated identifier.
438+
* @param resource A resource for which the configuration is asked for
439+
* @return The full configuration or a subset.
440+
*/
441+
getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration;
422442
}
423443

424444
export const ITerminalManager = Symbol('ITerminalManager');

src/client/common/application/workspace.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,14 @@
22
// Licensed under the MIT License.
33

44
import { injectable } from 'inversify';
5-
import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
5+
import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
66
import { IWorkspaceService } from './types';
77

88
@injectable()
99
export class WorkspaceService implements IWorkspaceService {
10+
public get onDidChangeConfiguration(): Event<void> {
11+
return workspace.onDidChangeConfiguration;
12+
}
1013
public get rootPath(): string | undefined {
1114
return workspace.rootPath;
1215
}
@@ -16,6 +19,9 @@ export class WorkspaceService implements IWorkspaceService {
1619
public get onDidChangeWorkspaceFolders(): Event<WorkspaceFoldersChangeEvent> {
1720
return workspace.onDidChangeWorkspaceFolders;
1821
}
22+
public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration {
23+
return workspace.getConfiguration(section, resource);
24+
}
1925
public getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined {
2026
return workspace.getWorkspaceFolder(uri);
2127
}

src/client/common/extensions.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,11 @@ interface String {
1515
* @param {SplitLinesOptions=} splitOptions - Options used for splitting the string.
1616
*/
1717
splitLines(splitOptions?: { trim: boolean, removeEmptyEntries: boolean }): string[];
18+
/**
19+
* Appropriately formats a string so it can be used as an argument for a command in a shell.
20+
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
21+
*/
22+
toCommandArgument(): string;
1823
}
1924

2025
/**
@@ -32,3 +37,15 @@ String.prototype.splitLines = function (this: string, splitOptions: { trim: bool
3237
}
3338
return lines;
3439
};
40+
41+
/**
42+
* Appropriately formats a string so it can be used as an argument for a command in a shell.
43+
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
44+
* @param {String} value.
45+
*/
46+
String.prototype.toCommandArgument = function (this: string): string {
47+
if (!this) {
48+
return this;
49+
}
50+
return (this.indexOf(' ') > 0 && !this.startsWith('"') && !this.endsWith('"')) ? `"${this}"` : this.toString();
51+
};

src/client/common/installer/condaInstaller.ts

Lines changed: 11 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,10 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import * as path from 'path';
65
import { Uri } from 'vscode';
7-
import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
8-
import { CONDA_RELATIVE_PY_PATH } from '../../interpreter/locators/services/conda';
6+
import { ICondaService, IInterpreterService, InterpreterType } from '../../interpreter/contracts';
97
import { IServiceContainer } from '../../ioc/types';
10-
import { PythonSettings } from '../configSettings';
11-
import { IPythonExecutionFactory } from '../process/types';
128
import { ExecutionInfo } from '../types';
13-
import { arePathsSame } from '../utils';
149
import { ModuleInstaller } from './moduleInstaller';
1510
import { IModuleInstaller } from './types';
1611

@@ -35,7 +30,7 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
3530
if (typeof this.isCondaAvailable === 'boolean') {
3631
return this.isCondaAvailable!;
3732
}
38-
const condaLocator = this.serviceContainer.get<ICondaLocatorService>(ICondaLocatorService);
33+
const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService);
3934
const available = await condaLocator.isCondaAvailable();
4035

4136
if (!available) {
@@ -46,20 +41,21 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
4641
return this.isCurrentEnvironmentACondaEnvironment(resource);
4742
}
4843
protected async getExecutionInfo(moduleName: string, resource?: Uri): Promise<ExecutionInfo> {
49-
const condaLocator = this.serviceContainer.get<ICondaLocatorService>(ICondaLocatorService);
44+
const condaLocator = this.serviceContainer.get<ICondaService>(ICondaService);
5045
const condaFile = await condaLocator.getCondaFile();
5146

52-
const info = await this.getCurrentInterpreterInfo(resource);
47+
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
48+
const info = await interpreterService.getActiveInterpreter(resource);
5349
const args = ['install'];
5450

55-
if (info.envName) {
51+
if (info!.envName) {
5652
// If we have the name of the conda environment, then use that.
5753
args.push('--name');
58-
args.push(info.envName!);
54+
args.push(info!.envName!);
5955
} else {
6056
// Else provide the full path to the environment path.
6157
args.push('--prefix');
62-
args.push(info.envPath);
58+
args.push(info!.envPath!);
6359
}
6460
args.push(moduleName);
6561
return {
@@ -68,37 +64,9 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
6864
moduleName: ''
6965
};
7066
}
71-
private async getCurrentPythonPath(resource?: Uri): Promise<string> {
72-
const pythonPath = PythonSettings.getInstance(resource).pythonPath;
73-
if (path.basename(pythonPath) === pythonPath) {
74-
const pythonProc = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
75-
return pythonProc.getExecutablePath().catch(() => pythonPath);
76-
} else {
77-
return pythonPath;
78-
}
79-
}
8067
private isCurrentEnvironmentACondaEnvironment(resource?: Uri) {
81-
return this.getCurrentInterpreterInfo(resource)
82-
.then(info => info && info.isConda === true).catch(() => false);
83-
}
84-
private async getCurrentInterpreterInfo(resource?: Uri) {
85-
// Use this service, though it returns everything it is cached.
86-
const interpreterLocator = this.serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
87-
const interpretersPromise = interpreterLocator.getInterpreters(resource);
88-
const pythonPathPromise = this.getCurrentPythonPath(resource);
89-
const [interpreters, currentPythonPath] = await Promise.all([interpretersPromise, pythonPathPromise]);
90-
91-
// Check if we have the info about the current python path.
92-
const pathToCompareWith = path.dirname(currentPythonPath);
93-
const info = interpreters.find(item => arePathsSame(path.dirname(item.path), pathToCompareWith));
94-
// tslint:disable-next-line:prefer-array-literal
95-
const pathsToRemove = new Array(CONDA_RELATIVE_PY_PATH.length).fill('..') as string[];
96-
const envPath = path.join(path.dirname(currentPythonPath), ...pathsToRemove);
97-
return {
98-
isConda: info && info!.type === InterpreterType.Conda,
99-
pythonPath: currentPythonPath,
100-
envPath,
101-
envName: info ? info!.envName : undefined
102-
};
68+
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
69+
return interpreterService.getActiveInterpreter(resource)
70+
.then(info => info ? info.type === InterpreterType.Conda : false).catch(() => false);
10371
}
10472
}

src/client/common/installer/installer.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import { PythonSettings } from '../configSettings';
1111
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
1212
import { IPlatformService } from '../platform/types';
1313
import { IProcessService, IPythonExecutionFactory } from '../process/types';
14-
import { ITerminalService } from '../terminal/types';
15-
import { IInstaller, ILogger, InstallerResponse, IOutputChannel, IsWindows, ModuleNamePurpose, Product } from '../types';
14+
import { ITerminalServiceFactory } from '../terminal/types';
15+
import { IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types';
1616
import { IModuleInstaller } from './types';
1717

1818
export { Product } from '../types';
@@ -237,7 +237,8 @@ export class Installer implements IInstaller {
237237
this.outputChannel.appendLine('Option 3: Extract to any folder and define that path in the python.workspaceSymbols.ctagsPath setting of your user settings file (settings.json).');
238238
this.outputChannel.show();
239239
} else {
240-
const terminalService = this.serviceContainer.get<ITerminalService>(ITerminalService);
240+
const terminalServiceFactory = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory);
241+
const terminalService = terminalServiceFactory.getTerminalService();
241242
const logger = this.serviceContainer.get<ILogger>(ILogger);
242243
terminalService.sendCommand(CTagsInsllationScript, [])
243244
.catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`));

src/client/common/installer/moduleInstaller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,15 @@ import { IServiceContainer } from '../../ioc/types';
1313
import { PythonSettings } from '../configSettings';
1414
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
1515
import { IFileSystem } from '../platform/types';
16-
import { ITerminalService } from '../terminal/types';
16+
import { ITerminalServiceFactory } from '../terminal/types';
1717
import { ExecutionInfo, IOutputChannel } from '../types';
1818

1919
@injectable()
2020
export abstract class ModuleInstaller {
2121
constructor(protected serviceContainer: IServiceContainer) { }
2222
public async installModule(name: string, resource?: vscode.Uri): Promise<void> {
2323
const executionInfo = await this.getExecutionInfo(name, resource);
24-
const terminalService = this.serviceContainer.get<ITerminalService>(ITerminalService);
24+
const terminalService = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory).getTerminalService();
2525

2626
if (executionInfo.moduleName) {
2727
const settings = PythonSettings.getInstance(resource);

src/client/common/platform/fileSystem.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,22 @@
22
// Licensed under the MIT License.
33
'use strict';
44

5-
import * as fs from 'fs';
6-
import * as fse from 'fs-extra';
5+
import * as fs from 'fs-extra';
76
import { inject, injectable } from 'inversify';
87
import * as path from 'path';
9-
import { IServiceContainer } from '../../ioc/types';
108
import { IFileSystem, IPlatformService } from './types';
119

1210
@injectable()
1311
export class FileSystem implements IFileSystem {
14-
constructor( @inject(IServiceContainer) private platformService: IPlatformService) { }
12+
constructor( @inject(IPlatformService) private platformService: IPlatformService) { }
1513

1614
public get directorySeparatorChar(): string {
1715
return path.sep;
1816
}
1917

2018
public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
2119
return new Promise<boolean>(resolve => {
22-
fse.stat(filePath, (error, stats) => {
20+
fs.stat(filePath, (error, stats) => {
2321
if (error) {
2422
return resolve(false);
2523
}
@@ -31,13 +29,22 @@ export class FileSystem implements IFileSystem {
3129
public fileExistsAsync(filePath: string): Promise<boolean> {
3230
return this.objectExistsAsync(filePath, (stats) => stats.isFile());
3331
}
32+
/**
33+
* Reads the contents of the file using utf8 and returns the string contents.
34+
* @param {string} filePath
35+
* @returns {Promise<string>}
36+
* @memberof FileSystem
37+
*/
38+
public readFile(filePath: string): Promise<string> {
39+
return fs.readFile(filePath).then(buffer => buffer.toString());
40+
}
3441

3542
public directoryExistsAsync(filePath: string): Promise<boolean> {
3643
return this.objectExistsAsync(filePath, (stats) => stats.isDirectory());
3744
}
3845

3946
public createDirectoryAsync(directoryPath: string): Promise<void> {
40-
return fse.mkdirp(directoryPath);
47+
return fs.mkdirp(directoryPath);
4148
}
4249

4350
public getSubDirectoriesAsync(rootDir: string): Promise<string[]> {
@@ -46,7 +53,7 @@ export class FileSystem implements IFileSystem {
4653
if (error) {
4754
return resolve([]);
4855
}
49-
const subDirs = [];
56+
const subDirs: string[] = [];
5057
files.forEach(name => {
5158
const fullPath = path.join(rootDir, name);
5259
try {

src/client/common/platform/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,5 @@ export interface IFileSystem {
3636
createDirectoryAsync(path: string): Promise<void>;
3737
getSubDirectoriesAsync(rootDir: string): Promise<string[]>;
3838
arePathsSame(path1: string, path2: string): boolean;
39+
readFile(filePath: string): Promise<string>;
3940
}

src/client/common/serviceRegistry.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import { PersistentStateFactory } from './persistentState';
1515
import { IS_64_BIT, IS_WINDOWS } from './platform/constants';
1616
import { PathUtils } from './platform/pathUtils';
1717
import { CurrentProcess } from './process/currentProcess';
18+
import { Bash } from './terminal/environmentActivationProviders/bash';
19+
import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt';
1820
import { TerminalServiceFactory } from './terminal/factory';
1921
import { TerminalHelper } from './terminal/helper';
20-
import { TerminalService } from './terminal/service';
21-
import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './terminal/types';
22+
import { ITerminalActivationCommandProvider, ITerminalHelper, ITerminalServiceFactory } from './terminal/types';
2223
import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types';
2324

2425
export function registerTypes(serviceManager: IServiceManager) {
@@ -27,9 +28,7 @@ export function registerTypes(serviceManager: IServiceManager) {
2728

2829
serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory);
2930
serviceManager.addSingleton<ILogger>(ILogger, Logger);
30-
serviceManager.addSingleton<ITerminalService>(ITerminalService, TerminalService);
3131
serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory);
32-
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
3332
serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
3433
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
3534
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
@@ -39,4 +38,8 @@ export function registerTypes(serviceManager: IServiceManager) {
3938
serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService);
4039
serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager);
4140
serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager);
41+
42+
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
43+
serviceManager.addSingleton<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, Bash, 'bashCShellFish');
44+
serviceManager.addSingleton<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell');
4245
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { injectable } from 'inversify';
5+
import * as path from 'path';
6+
import { PythonInterpreter } from '../../../interpreter/contracts';
7+
import { IServiceContainer } from '../../../ioc/types';
8+
import { IFileSystem } from '../../platform/types';
9+
import { TerminalShellType } from '../types';
10+
import { ITerminalActivationCommandProvider } from '../types';
11+
12+
@injectable()
13+
export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider {
14+
constructor(protected readonly serviceContainer: IServiceContainer) { }
15+
16+
public abstract isShellSupported(targetShell: TerminalShellType): boolean;
17+
public abstract getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined>;
18+
19+
protected async findScriptFile(interpreter: PythonInterpreter, scriptFileNames: string[]): Promise<string | undefined> {
20+
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);
21+
22+
for (const scriptFileName of scriptFileNames) {
23+
// Generate scripts are found in the same directory as the interpreter.
24+
const scriptFile = path.join(path.dirname(interpreter.path), scriptFileName);
25+
const found = await fs.fileExistsAsync(scriptFile);
26+
if (found) {
27+
return scriptFile;
28+
}
29+
}
30+
}
31+
}

0 commit comments

Comments
 (0)