Skip to content

Commit

Permalink
WIP - Activate environment in terminal (#614)
Browse files Browse the repository at this point in the history
Fixes #383
  • Loading branch information
DonJayamanne committed Jan 23, 2018
1 parent df2b494 commit 0253995
Show file tree
Hide file tree
Showing 55 changed files with 2,581 additions and 912 deletions.
22 changes: 21 additions & 1 deletion src/client/common/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
// tslint:disable:no-any unified-signatures

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

/**
* An event that is emitted when the [configuration](#WorkspaceConfiguration) changed.
*/
readonly onDidChangeConfiguration: Event<void>;

/**
* Returns the [workspace folder](#WorkspaceFolder) that contains a given uri.
* * returns `undefined` when the given uri doesn't match any workspace folder
Expand Down Expand Up @@ -419,6 +424,21 @@ export interface IWorkspaceService {
* [workspace folders](#workspace.workspaceFolders) are opened.
*/
findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable<Uri[]>;

/**
* Get a workspace configuration object.
*
* When a section-identifier is provided only that part of the configuration
* is returned. Dots in the section-identifier are interpreted as child-access,
* like `{ myExt: { setting: { doIt: true }}}` and `getConfiguration('myExt.setting').get('doIt') === true`.
*
* When a resource is provided, configuration scoped to that resource is returned.
*
* @param section A dot-separated identifier.
* @param resource A resource for which the configuration is asked for
* @return The full configuration or a subset.
*/
getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration;
}

export const ITerminalManager = Symbol('ITerminalManager');
Expand Down
8 changes: 7 additions & 1 deletion src/client/common/application/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
// Licensed under the MIT License.

import { injectable } from 'inversify';
import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceConfiguration, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode';
import { IWorkspaceService } from './types';

@injectable()
export class WorkspaceService implements IWorkspaceService {
public get onDidChangeConfiguration(): Event<void> {
return workspace.onDidChangeConfiguration;
}
public get rootPath(): string | undefined {
return workspace.rootPath;
}
Expand All @@ -16,6 +19,9 @@ export class WorkspaceService implements IWorkspaceService {
public get onDidChangeWorkspaceFolders(): Event<WorkspaceFoldersChangeEvent> {
return workspace.onDidChangeWorkspaceFolders;
}
public getConfiguration(section?: string, resource?: Uri): WorkspaceConfiguration {
return workspace.getConfiguration(section, resource);
}
public getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined {
return workspace.getWorkspaceFolder(uri);
}
Expand Down
17 changes: 17 additions & 0 deletions src/client/common/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ interface String {
* @param {SplitLinesOptions=} splitOptions - Options used for splitting the string.
*/
splitLines(splitOptions?: { trim: boolean, removeEmptyEntries: boolean }): string[];
/**
* Appropriately formats a string so it can be used as an argument for a command in a shell.
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
*/
toCommandArgument(): string;
}

/**
Expand All @@ -32,3 +37,15 @@ String.prototype.splitLines = function (this: string, splitOptions: { trim: bool
}
return lines;
};

/**
* Appropriately formats a string so it can be used as an argument for a command in a shell.
* E.g. if an argument contains a space, then it will be enclosed within double quotes.
* @param {String} value.
*/
String.prototype.toCommandArgument = function (this: string): string {
if (!this) {
return this;
}
return (this.indexOf(' ') > 0 && !this.startsWith('"') && !this.endsWith('"')) ? `"${this}"` : this.toString();
};
54 changes: 11 additions & 43 deletions src/client/common/installer/condaInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
// Licensed under the MIT License.

import { inject, injectable } from 'inversify';
import * as path from 'path';
import { Uri } from 'vscode';
import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts';
import { CONDA_RELATIVE_PY_PATH } from '../../interpreter/locators/services/conda';
import { ICondaService, IInterpreterService, InterpreterType } from '../../interpreter/contracts';
import { IServiceContainer } from '../../ioc/types';
import { PythonSettings } from '../configSettings';
import { IPythonExecutionFactory } from '../process/types';
import { ExecutionInfo } from '../types';
import { arePathsSame } from '../utils';
import { ModuleInstaller } from './moduleInstaller';
import { IModuleInstaller } from './types';

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

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

const info = await this.getCurrentInterpreterInfo(resource);
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
const info = await interpreterService.getActiveInterpreter(resource);
const args = ['install'];

if (info.envName) {
if (info!.envName) {
// If we have the name of the conda environment, then use that.
args.push('--name');
args.push(info.envName!);
args.push(info!.envName!);
} else {
// Else provide the full path to the environment path.
args.push('--prefix');
args.push(info.envPath);
args.push(info!.envPath!);
}
args.push(moduleName);
return {
Expand All @@ -68,37 +64,9 @@ export class CondaInstaller extends ModuleInstaller implements IModuleInstaller
moduleName: ''
};
}
private async getCurrentPythonPath(resource?: Uri): Promise<string> {
const pythonPath = PythonSettings.getInstance(resource).pythonPath;
if (path.basename(pythonPath) === pythonPath) {
const pythonProc = await this.serviceContainer.get<IPythonExecutionFactory>(IPythonExecutionFactory).create(resource);
return pythonProc.getExecutablePath().catch(() => pythonPath);
} else {
return pythonPath;
}
}
private isCurrentEnvironmentACondaEnvironment(resource?: Uri) {
return this.getCurrentInterpreterInfo(resource)
.then(info => info && info.isConda === true).catch(() => false);
}
private async getCurrentInterpreterInfo(resource?: Uri) {
// Use this service, though it returns everything it is cached.
const interpreterLocator = this.serviceContainer.get<IInterpreterLocatorService>(IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE);
const interpretersPromise = interpreterLocator.getInterpreters(resource);
const pythonPathPromise = this.getCurrentPythonPath(resource);
const [interpreters, currentPythonPath] = await Promise.all([interpretersPromise, pythonPathPromise]);

// Check if we have the info about the current python path.
const pathToCompareWith = path.dirname(currentPythonPath);
const info = interpreters.find(item => arePathsSame(path.dirname(item.path), pathToCompareWith));
// tslint:disable-next-line:prefer-array-literal
const pathsToRemove = new Array(CONDA_RELATIVE_PY_PATH.length).fill('..') as string[];
const envPath = path.join(path.dirname(currentPythonPath), ...pathsToRemove);
return {
isConda: info && info!.type === InterpreterType.Conda,
pythonPath: currentPythonPath,
envPath,
envName: info ? info!.envName : undefined
};
const interpreterService = this.serviceContainer.get<IInterpreterService>(IInterpreterService);
return interpreterService.getActiveInterpreter(resource)
.then(info => info ? info.type === InterpreterType.Conda : false).catch(() => false);
}
}
7 changes: 4 additions & 3 deletions src/client/common/installer/installer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ import { PythonSettings } from '../configSettings';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IPlatformService } from '../platform/types';
import { IProcessService, IPythonExecutionFactory } from '../process/types';
import { ITerminalService } from '../terminal/types';
import { IInstaller, ILogger, InstallerResponse, IOutputChannel, IsWindows, ModuleNamePurpose, Product } from '../types';
import { ITerminalServiceFactory } from '../terminal/types';
import { IInstaller, ILogger, InstallerResponse, IOutputChannel, ModuleNamePurpose, Product } from '../types';
import { IModuleInstaller } from './types';

export { Product } from '../types';
Expand Down Expand Up @@ -237,7 +237,8 @@ export class Installer implements IInstaller {
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).');
this.outputChannel.show();
} else {
const terminalService = this.serviceContainer.get<ITerminalService>(ITerminalService);
const terminalServiceFactory = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory);
const terminalService = terminalServiceFactory.getTerminalService();
const logger = this.serviceContainer.get<ILogger>(ILogger);
terminalService.sendCommand(CTagsInsllationScript, [])
.catch(logger.logError.bind(logger, `Failed to install ctags. Script sent '${CTagsInsllationScript}'.`));
Expand Down
4 changes: 2 additions & 2 deletions src/client/common/installer/moduleInstaller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { IServiceContainer } from '../../ioc/types';
import { PythonSettings } from '../configSettings';
import { STANDARD_OUTPUT_CHANNEL } from '../constants';
import { IFileSystem } from '../platform/types';
import { ITerminalService } from '../terminal/types';
import { ITerminalServiceFactory } from '../terminal/types';
import { ExecutionInfo, IOutputChannel } from '../types';

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

if (executionInfo.moduleName) {
const settings = PythonSettings.getInstance(resource);
Expand Down
21 changes: 14 additions & 7 deletions src/client/common/platform/fileSystem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,22 @@
// Licensed under the MIT License.
'use strict';

import * as fs from 'fs';
import * as fse from 'fs-extra';
import * as fs from 'fs-extra';
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { IServiceContainer } from '../../ioc/types';
import { IFileSystem, IPlatformService } from './types';

@injectable()
export class FileSystem implements IFileSystem {
constructor( @inject(IServiceContainer) private platformService: IPlatformService) { }
constructor( @inject(IPlatformService) private platformService: IPlatformService) { }

public get directorySeparatorChar(): string {
return path.sep;
}

public objectExistsAsync(filePath: string, statCheck: (s: fs.Stats) => boolean): Promise<boolean> {
return new Promise<boolean>(resolve => {
fse.stat(filePath, (error, stats) => {
fs.stat(filePath, (error, stats) => {
if (error) {
return resolve(false);
}
Expand All @@ -31,13 +29,22 @@ export class FileSystem implements IFileSystem {
public fileExistsAsync(filePath: string): Promise<boolean> {
return this.objectExistsAsync(filePath, (stats) => stats.isFile());
}
/**
* Reads the contents of the file using utf8 and returns the string contents.
* @param {string} filePath
* @returns {Promise<string>}
* @memberof FileSystem
*/
public readFile(filePath: string): Promise<string> {
return fs.readFile(filePath).then(buffer => buffer.toString());
}

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

public createDirectoryAsync(directoryPath: string): Promise<void> {
return fse.mkdirp(directoryPath);
return fs.mkdirp(directoryPath);
}

public getSubDirectoriesAsync(rootDir: string): Promise<string[]> {
Expand All @@ -46,7 +53,7 @@ export class FileSystem implements IFileSystem {
if (error) {
return resolve([]);
}
const subDirs = [];
const subDirs: string[] = [];
files.forEach(name => {
const fullPath = path.join(rootDir, name);
try {
Expand Down
1 change: 1 addition & 0 deletions src/client/common/platform/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ export interface IFileSystem {
createDirectoryAsync(path: string): Promise<void>;
getSubDirectoriesAsync(rootDir: string): Promise<string[]>;
arePathsSame(path1: string, path2: string): boolean;
readFile(filePath: string): Promise<string>;
}
11 changes: 7 additions & 4 deletions src/client/common/serviceRegistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { PersistentStateFactory } from './persistentState';
import { IS_64_BIT, IS_WINDOWS } from './platform/constants';
import { PathUtils } from './platform/pathUtils';
import { CurrentProcess } from './process/currentProcess';
import { Bash } from './terminal/environmentActivationProviders/bash';
import { CommandPromptAndPowerShell } from './terminal/environmentActivationProviders/commandPrompt';
import { TerminalServiceFactory } from './terminal/factory';
import { TerminalHelper } from './terminal/helper';
import { TerminalService } from './terminal/service';
import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './terminal/types';
import { ITerminalActivationCommandProvider, ITerminalHelper, ITerminalServiceFactory } from './terminal/types';
import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types';

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

serviceManager.addSingleton<IPersistentStateFactory>(IPersistentStateFactory, PersistentStateFactory);
serviceManager.addSingleton<ILogger>(ILogger, Logger);
serviceManager.addSingleton<ITerminalService>(ITerminalService, TerminalService);
serviceManager.addSingleton<ITerminalServiceFactory>(ITerminalServiceFactory, TerminalServiceFactory);
serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
serviceManager.addSingleton<IPathUtils>(IPathUtils, PathUtils);
serviceManager.addSingleton<IApplicationShell>(IApplicationShell, ApplicationShell);
serviceManager.addSingleton<ICurrentProcess>(ICurrentProcess, CurrentProcess);
Expand All @@ -39,4 +38,8 @@ export function registerTypes(serviceManager: IServiceManager) {
serviceManager.addSingleton<IWorkspaceService>(IWorkspaceService, WorkspaceService);
serviceManager.addSingleton<IDocumentManager>(IDocumentManager, DocumentManager);
serviceManager.addSingleton<ITerminalManager>(ITerminalManager, TerminalManager);

serviceManager.addSingleton<ITerminalHelper>(ITerminalHelper, TerminalHelper);
serviceManager.addSingleton<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, Bash, 'bashCShellFish');
serviceManager.addSingleton<ITerminalActivationCommandProvider>(ITerminalActivationCommandProvider, CommandPromptAndPowerShell, 'commandPromptAndPowerShell');
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { injectable } from 'inversify';
import * as path from 'path';
import { PythonInterpreter } from '../../../interpreter/contracts';
import { IServiceContainer } from '../../../ioc/types';
import { IFileSystem } from '../../platform/types';
import { TerminalShellType } from '../types';
import { ITerminalActivationCommandProvider } from '../types';

@injectable()
export abstract class BaseActivationCommandProvider implements ITerminalActivationCommandProvider {
constructor(protected readonly serviceContainer: IServiceContainer) { }

public abstract isShellSupported(targetShell: TerminalShellType): boolean;
public abstract getActivationCommands(interpreter: PythonInterpreter, targetShell: TerminalShellType): Promise<string[] | undefined>;

protected async findScriptFile(interpreter: PythonInterpreter, scriptFileNames: string[]): Promise<string | undefined> {
const fs = this.serviceContainer.get<IFileSystem>(IFileSystem);

for (const scriptFileName of scriptFileNames) {
// Generate scripts are found in the same directory as the interpreter.
const scriptFile = path.join(path.dirname(interpreter.path), scriptFileName);
const found = await fs.fileExistsAsync(scriptFile);
if (found) {
return scriptFile;
}
}
}
}
Loading

0 comments on commit 0253995

Please sign in to comment.