From ad806fabcc533f8b13f88f0cd4ae41b02d301c77 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Thu, 18 Jan 2018 17:53:13 -0800 Subject: [PATCH] Add delay for exec in terminal (#592) Fixes #174 Fixes #60 Fixes #593 --- gulpfile.js | 6 +- .../common/application/commandManager.ts | 75 +++++ .../common/application/documentManager.ts | 39 +++ .../common/application/terminalManager.ts | 16 ++ src/client/common/application/types.ts | 245 ++++++++++++++++ src/client/common/application/workspace.ts | 31 ++ src/client/common/configSettings.ts | 124 +------- src/client/common/configuration/service.ts | 14 + src/client/common/contextKey.ts | 6 +- .../common/installer/pythonInstallation.ts | 2 +- src/client/common/serviceRegistry.ts | 23 +- src/client/common/terminal/factory.ts | 32 +++ src/client/common/terminal/helper.ts | 59 ++++ src/client/common/terminal/service.ts | 92 +++--- src/client/common/terminal/types.ts | 34 +++ src/client/common/types.ts | 122 +++++++- src/client/extension.ts | 9 +- src/client/formatters/helper.ts | 3 +- src/client/formatters/types.ts | 3 +- src/client/linters/baseLinter.ts | 5 +- src/client/linters/helper.ts | 3 +- src/client/linters/types.ts | 3 +- .../providers/execInTerminalProvider.ts | 267 ------------------ src/client/refactor/proxy.ts | 2 +- src/client/telemetry/index.ts | 8 +- .../codeExecution/codeExecutionManager.ts | 68 +++++ .../terminals/codeExecution/djangoContext.ts | 71 +++++ .../codeExecution/djangoShellCodeExecution.ts | 47 +++ src/client/terminals/codeExecution/helper.ts | 56 ++++ .../codeExecution/terminalCodeExecution.ts | 92 ++++++ src/client/terminals/serviceRegistry.ts | 16 ++ src/client/terminals/types.ts | 25 ++ .../common/managers/baseTestManager.ts | 3 +- src/client/unittests/common/runner.ts | 3 +- src/client/unittests/common/testUtils.ts | 2 +- src/client/unittests/common/types.ts | 2 +- src/client/workspaceSymbols/generator.ts | 3 +- src/test/common/configSettings.test.ts | 3 +- src/test/common/configuration/service.test.ts | 27 ++ src/test/common/installer.test.ts | 3 - src/test/common/moduleInstaller.test.ts | 34 ++- src/test/common/terminals/factory.test.ts | 34 +++ src/test/common/terminals/helper.test.ts | 135 +++++++++ src/test/common/terminals/service.test.ts | 103 +++++++ src/test/debugger/envVars.test.ts | 5 +- src/test/format/format.helper.test.ts | 10 +- src/test/install/pythonInstallation.test.ts | 2 +- src/test/linters/lint.helper.test.ts | 10 +- src/test/mocks/terminalService.ts | 17 -- src/test/serviceRegistry.ts | 4 + .../codeExecutionManager.test.ts | 240 ++++++++++++++++ .../djangoShellCodeExect.test.ts | 158 +++++++++++ .../terminals/codeExecution/helper.test.ts | 97 +++++++ .../codeExecution/terminalCodeExec.test.ts | 243 ++++++++++++++++ src/testMultiRootWkspc/multi.code-workspace | 90 +++--- 55 files changed, 2264 insertions(+), 562 deletions(-) create mode 100644 src/client/common/application/commandManager.ts create mode 100644 src/client/common/application/documentManager.ts create mode 100644 src/client/common/application/terminalManager.ts create mode 100644 src/client/common/application/workspace.ts create mode 100644 src/client/common/configuration/service.ts create mode 100644 src/client/common/terminal/factory.ts create mode 100644 src/client/common/terminal/helper.ts delete mode 100644 src/client/providers/execInTerminalProvider.ts create mode 100644 src/client/terminals/codeExecution/codeExecutionManager.ts create mode 100644 src/client/terminals/codeExecution/djangoContext.ts create mode 100644 src/client/terminals/codeExecution/djangoShellCodeExecution.ts create mode 100644 src/client/terminals/codeExecution/helper.ts create mode 100644 src/client/terminals/codeExecution/terminalCodeExecution.ts create mode 100644 src/client/terminals/serviceRegistry.ts create mode 100644 src/client/terminals/types.ts create mode 100644 src/test/common/configuration/service.test.ts create mode 100644 src/test/common/terminals/factory.test.ts create mode 100644 src/test/common/terminals/helper.test.ts create mode 100644 src/test/common/terminals/service.test.ts delete mode 100644 src/test/mocks/terminalService.ts create mode 100644 src/test/terminals/codeExecution/codeExecutionManager.test.ts create mode 100644 src/test/terminals/codeExecution/djangoShellCodeExect.test.ts create mode 100644 src/test/terminals/codeExecution/helper.test.ts create mode 100644 src/test/terminals/codeExecution/terminalCodeExec.test.ts diff --git a/gulpfile.js b/gulpfile.js index 18f64da6e1ce..8b9cbb125349 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -157,7 +157,7 @@ const hygiene = (options) => { * @param {any[]} failures */ function reportLinterFailures(failures) { - failures + return failures .map(failure => { const name = failure.name || failure.fileName; const position = failure.startPosition; @@ -235,7 +235,7 @@ const hygiene = (options) => { const files = options.mode === 'compile' ? tsProject.src() : getFilesToProcess(options); const dest = options.mode === 'compile' ? './out' : '.'; let result = files - .pipe(filter(f => !f.stat.isDirectory())); + .pipe(filter(f => f && f.stat && !f.stat.isDirectory())); if (!options.skipIndentationCheck) { result = result.pipe(filter(indentationFilter)) @@ -261,7 +261,7 @@ const hygiene = (options) => { .js.pipe(gulp.dest(dest)) .pipe(es.through(null, function () { if (errorCount > 0) { - const errorMessage = `Hygiene failed with ${colors.yellow(errorCount)} errors 👎 . Check 'gulpfile.js'.`; + const errorMessage = `Hygiene failed with errors 👎 . Check 'gulpfile.js'.`; console.error(colors.red(errorMessage)); exitHandler(options); } else { diff --git a/src/client/common/application/commandManager.ts b/src/client/common/application/commandManager.ts new file mode 100644 index 000000000000..af42ffe744b9 --- /dev/null +++ b/src/client/common/application/commandManager.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any + +import { injectable } from 'inversify'; +import { commands, Disposable, TextEditor, TextEditorEdit } from 'vscode'; +import { ICommandManager } from './types'; + +@injectable() +export class CommandManager implements ICommandManager { + + /** + * Registers a command that can be invoked via a keyboard shortcut, + * a menu item, an action, or directly. + * + * Registering a command with an existing command identifier twice + * will cause an error. + * + * @param command A unique identifier for the command. + * @param callback A command handler function. + * @param thisArg The `this` context used when invoking the handler function. + * @return Disposable which unregisters this command on disposal. + */ + public registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable { + return commands.registerCommand(command, callback, thisArg); + } + + /** + * Registers a text editor command that can be invoked via a keyboard shortcut, + * a menu item, an action, or directly. + * + * Text editor commands are different from ordinary [commands](#commands.registerCommand) as + * they only execute when there is an active editor when the command is called. Also, the + * command handler of an editor command has access to the active editor and to an + * [edit](#TextEditorEdit)-builder. + * + * @param command A unique identifier for the command. + * @param callback A command handler function with access to an [editor](#TextEditor) and an [edit](#TextEditorEdit). + * @param thisArg The `this` context used when invoking the handler function. + * @return Disposable which unregisters this command on disposal. + */ + public registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable { + return commands.registerTextEditorCommand(command, callback, thisArg); + } + + /** + * Executes the command denoted by the given command identifier. + * + * * *Note 1:* When executing an editor command not all types are allowed to + * be passed as arguments. Allowed are the primitive types `string`, `boolean`, + * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`Uri`](#Uri) and [`Location`](#Location). + * * *Note 2:* There are no restrictions when executing commands that have been contributed + * by extensions. + * + * @param command Identifier of the command to execute. + * @param rest Parameters passed to the command function. + * @return A thenable that resolves to the returned value of the given command. `undefined` when + * the command handler function doesn't return anything. + */ + public executeCommand(command: string, ...rest: any[]): Thenable { + return commands.executeCommand(command, ...rest); + } + + /** + * Retrieve the list of all available commands. Commands starting an underscore are + * treated as internal commands. + * + * @param filterInternal Set `true` to not see internal commands (starting with an underscore) + * @return Thenable that resolves to a list of command ids. + */ + public getCommands(filterInternal?: boolean): Thenable { + return commands.getCommands(filterInternal); + } +} diff --git a/src/client/common/application/documentManager.ts b/src/client/common/application/documentManager.ts new file mode 100644 index 000000000000..8c3c41df5850 --- /dev/null +++ b/src/client/common/application/documentManager.ts @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-any + +import { injectable } from 'inversify'; +import { Event, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, window } from 'vscode'; +import { IDocumentManager } from './types'; + +@injectable() +export class DocumentManager implements IDocumentManager { + public get activeTextEditor(): TextEditor | undefined { + return window.activeTextEditor; + } + public get visibleTextEditors(): TextEditor[] { + return window.visibleTextEditors; + } + public get onDidChangeActiveTextEditor(): Event { + return window.onDidChangeActiveTextEditor; + } + public get onDidChangeVisibleTextEditors(): Event { + return window.onDidChangeVisibleTextEditors; + } + public get onDidChangeTextEditorSelection(): Event { + return window.onDidChangeTextEditorSelection; + } + public get onDidChangeTextEditorOptions(): Event { + return window.onDidChangeTextEditorOptions; + } + public get onDidChangeTextEditorViewColumn(): Event { + return window.onDidChangeTextEditorViewColumn; + } + public showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; + public showTextDocument(document: TextDocument | Uri, options?: TextDocumentShowOptions): Thenable; + public showTextDocument(uri: any, options?: any, preserveFocus?: any): Thenable { + return window.showTextDocument(uri, options, preserveFocus); + } + +} diff --git a/src/client/common/application/terminalManager.ts b/src/client/common/application/terminalManager.ts new file mode 100644 index 000000000000..87aab8e40a9e --- /dev/null +++ b/src/client/common/application/terminalManager.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { Event, Terminal, TerminalOptions, window } from 'vscode'; +import { ITerminalManager } from './types'; + +@injectable() +export class TerminalManager implements ITerminalManager { + public get onDidCloseTerminal(): Event { + return window.onDidCloseTerminal; + } + public createTerminal(options: TerminalOptions): Terminal { + return window.createTerminal(options); + } +} diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 4977256d0af3..21c1bcbf94f4 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -2,7 +2,13 @@ // Licensed under the MIT License. 'use strict'; +// tslint:disable:no-any unified-signatures + import * as vscode from 'vscode'; +import { CancellationToken, Disposable, Event, FileSystemWatcher, GlobPattern, TextDocument, TextDocumentShowOptions } from 'vscode'; +import { TextEditor, TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent } from 'vscode'; +import { Uri, ViewColumn, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { Terminal, TerminalOptions } from 'vscode'; export const IApplicationShell = Symbol('IApplicationShell'); export interface IApplicationShell { @@ -192,3 +198,242 @@ export interface IApplicationShell { */ openUrl(url: string): void; } + +export const ICommandManager = Symbol('ICommandManager'); + +export interface ICommandManager { + + /** + * Registers a command that can be invoked via a keyboard shortcut, + * a menu item, an action, or directly. + * + * Registering a command with an existing command identifier twice + * will cause an error. + * + * @param command A unique identifier for the command. + * @param callback A command handler function. + * @param thisArg The `this` context used when invoking the handler function. + * @return Disposable which unregisters this command on disposal. + */ + registerCommand(command: string, callback: (...args: any[]) => any, thisArg?: any): Disposable; + + /** + * Registers a text editor command that can be invoked via a keyboard shortcut, + * a menu item, an action, or directly. + * + * Text editor commands are different from ordinary [commands](#commands.registerCommand) as + * they only execute when there is an active editor when the command is called. Also, the + * command handler of an editor command has access to the active editor and to an + * [edit](#TextEditorEdit)-builder. + * + * @param command A unique identifier for the command. + * @param callback A command handler function with access to an [editor](#TextEditor) and an [edit](#TextEditorEdit). + * @param thisArg The `this` context used when invoking the handler function. + * @return Disposable which unregisters this command on disposal. + */ + registerTextEditorCommand(command: string, callback: (textEditor: TextEditor, edit: TextEditorEdit, ...args: any[]) => void, thisArg?: any): Disposable; + + /** + * Executes the command denoted by the given command identifier. + * + * * *Note 1:* When executing an editor command not all types are allowed to + * be passed as arguments. Allowed are the primitive types `string`, `boolean`, + * `number`, `undefined`, and `null`, as well as [`Position`](#Position), [`Range`](#Range), [`Uri`](#Uri) and [`Location`](#Location). + * * *Note 2:* There are no restrictions when executing commands that have been contributed + * by extensions. + * + * @param command Identifier of the command to execute. + * @param rest Parameters passed to the command function. + * @return A thenable that resolves to the returned value of the given command. `undefined` when + * the command handler function doesn't return anything. + */ + executeCommand(command: string, ...rest: any[]): Thenable; + + /** + * Retrieve the list of all available commands. Commands starting an underscore are + * treated as internal commands. + * + * @param filterInternal Set `true` to not see internal commands (starting with an underscore) + * @return Thenable that resolves to a list of command ids. + */ + getCommands(filterInternal?: boolean): Thenable; +} + +export const IDocumentManager = Symbol('IDocumentManager'); + +export interface IDocumentManager { + /** + * The currently active editor or `undefined`. The active editor is the one + * that currently has focus or, when none has focus, the one that has changed + * input most recently. + */ + readonly activeTextEditor: TextEditor | undefined; + + /** + * The currently visible editors or an empty array. + */ + readonly visibleTextEditors: TextEditor[]; + + /** + * An [event](#Event) which fires when the [active editor](#window.activeTextEditor) + * has changed. *Note* that the event also fires when the active editor changes + * to `undefined`. + */ + readonly onDidChangeActiveTextEditor: Event; + + /** + * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) + * has changed. + */ + readonly onDidChangeVisibleTextEditors: Event; + + /** + * An [event](#Event) which fires when the selection in an editor has changed. + */ + readonly onDidChangeTextEditorSelection: Event; + + /** + * An [event](#Event) which fires when the options of an editor have changed. + */ + readonly onDidChangeTextEditorOptions: Event; + + /** + * An [event](#Event) which fires when the view column of an editor has changed. + */ + readonly onDidChangeTextEditorViewColumn: Event; + + /** + * Show the given document in a text editor. A [column](#ViewColumn) can be provided + * to control where the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * + * @param document A text document to be shown. + * @param column A view column in which the [editor](#TextEditor) should be shown. The default is the [one](#ViewColumn.One), other values + * are adjusted to be `Min(column, columnCount + 1)`, the [active](#ViewColumn.Active)-column is + * not adjusted. + * @param preserveFocus When `true` the editor will not take focus. + * @return A promise that resolves to an [editor](#TextEditor). + */ + showTextDocument(document: TextDocument, column?: ViewColumn, preserveFocus?: boolean): Thenable; + + /** + * Show the given document in a text editor. [Options](#TextDocumentShowOptions) can be provided + * to control options of the editor is being shown. Might change the [active editor](#window.activeTextEditor). + * + * @param document A text document to be shown. + * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). + * @return A promise that resolves to an [editor](#TextEditor). + */ + showTextDocument(document: TextDocument, options?: TextDocumentShowOptions): Thenable; + + /** + * A short-hand for `openTextDocument(uri).then(document => showTextDocument(document, options))`. + * + * @see [openTextDocument](#openTextDocument) + * + * @param uri A resource identifier. + * @param options [Editor options](#TextDocumentShowOptions) to configure the behavior of showing the [editor](#TextEditor). + * @return A promise that resolves to an [editor](#TextEditor). + */ + showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; +} + +export const IWorkspaceService = Symbol('IWorkspace'); + +export interface IWorkspaceService { + /** + * ~~The folder that is open in the editor. `undefined` when no folder + * has been opened.~~ + * + * @deprecated Use [`workspaceFolders`](#workspace.workspaceFolders) instead. + * + * @readonly + */ + readonly rootPath: string | undefined; + + /** + * List of workspace folders or `undefined` when no folder is open. + * *Note* that the first entry corresponds to the value of `rootPath`. + * + * @readonly + */ + readonly workspaceFolders: WorkspaceFolder[] | undefined; + + /** + * An event that is emitted when a workspace folder is added or removed. + */ + readonly onDidChangeWorkspaceFolders: Event; + + /** + * Returns the [workspace folder](#WorkspaceFolder) that contains a given uri. + * * returns `undefined` when the given uri doesn't match any workspace folder + * * returns the *input* when the given uri is a workspace folder itself + * + * @param uri An uri. + * @return A workspace folder or `undefined` + */ + getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined; + + /** + * Returns a path that is relative to the workspace folder or folders. + * + * When there are no [workspace folders](#workspace.workspaceFolders) or when the path + * is not contained in them, the input is returned. + * + * @param pathOrUri A path or uri. When a uri is given its [fsPath](#Uri.fsPath) is used. + * @param includeWorkspaceFolder When `true` and when the given path is contained inside a + * workspace folder the name of the workspace is prepended. Defaults to `true` when there are + * multiple workspace folders and `false` otherwise. + * @return A path relative to the root or the input. + */ + asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string; + + /** + * Creates a file system watcher. + * + * A glob pattern that filters the file events on their absolute path must be provided. Optionally, + * flags to ignore certain kinds of events can be provided. To stop listening to events the watcher must be disposed. + * + * *Note* that only files within the current [workspace folders](#workspace.workspaceFolders) can be watched. + * + * @param globPattern A [glob pattern](#GlobPattern) that is applied to the absolute paths of created, changed, + * and deleted files. Use a [relative pattern](#RelativePattern) to limit events to a certain [workspace folder](#WorkspaceFolder). + * @param ignoreCreateEvents Ignore when files have been created. + * @param ignoreChangeEvents Ignore when files have been changed. + * @param ignoreDeleteEvents Ignore when files have been deleted. + * @return A new file system watcher instance. + */ + createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher; + + /** + * Find files across all [workspace folders](#workspace.workspaceFolders) in the workspace. + * + * @sample `findFiles('**∕*.js', '**∕node_modules∕**', 10)` + * @param include A [glob pattern](#GlobPattern) that defines the files to search for. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. Use a [relative pattern](#RelativePattern) + * to restrict the search results to a [workspace folder](#WorkspaceFolder). + * @param exclude A [glob pattern](#GlobPattern) that defines files and folders to exclude. The glob pattern + * will be matched against the file paths of resulting matches relative to their workspace. + * @param maxResults An upper-bound for the result. + * @param token A token that can be used to signal cancellation to the underlying search engine. + * @return A thenable that resolves to an array of resource identifiers. Will return no results if no + * [workspace folders](#workspace.workspaceFolders) are opened. + */ + findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable; +} + +export const ITerminalManager = Symbol('ITerminalManager'); + +export interface ITerminalManager { + /** + * An [event](#Event) which fires when a terminal is disposed. + */ + readonly onDidCloseTerminal: Event; + /** + * 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. + * + * @param options A TerminalOptions object describing the characteristics of the new terminal. + * @return A new Terminal. + */ + createTerminal(options: TerminalOptions): Terminal; +} diff --git a/src/client/common/application/workspace.ts b/src/client/common/application/workspace.ts new file mode 100644 index 000000000000..fca0f32a1420 --- /dev/null +++ b/src/client/common/application/workspace.ts @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { CancellationToken, Event, FileSystemWatcher, GlobPattern, Uri, workspace, WorkspaceFolder, WorkspaceFoldersChangeEvent } from 'vscode'; +import { IWorkspaceService } from './types'; + +@injectable() +export class WorkspaceService implements IWorkspaceService { + public get rootPath(): string | undefined { + return workspace.rootPath; + } + public get workspaceFolders(): WorkspaceFolder[] | undefined { + return workspace.workspaceFolders; + } + public get onDidChangeWorkspaceFolders(): Event { + return workspace.onDidChangeWorkspaceFolders; + } + public getWorkspaceFolder(uri: Uri): WorkspaceFolder | undefined { + return workspace.getWorkspaceFolder(uri); + } + public asRelativePath(pathOrUri: string | Uri, includeWorkspaceFolder?: boolean): string { + return workspace.asRelativePath(pathOrUri, includeWorkspaceFolder); + } + public createFileSystemWatcher(globPattern: GlobPattern, ignoreCreateEvents?: boolean, ignoreChangeEvents?: boolean, ignoreDeleteEvents?: boolean): FileSystemWatcher { + return workspace.createFileSystemWatcher(globPattern, ignoreChangeEvents, ignoreChangeEvents, ignoreDeleteEvents); + } + public findFiles(include: GlobPattern, exclude?: GlobPattern, maxResults?: number, token?: CancellationToken): Thenable { + return workspace.findFiles(include, exclude, maxResults, token); + } +} diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 7a8df51c1912..bc5de07e17f4 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -5,6 +5,16 @@ import { EventEmitter } from 'events'; import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; +import { + IAutoCompeteSettings, + IFormattingSettings, + ILintingSettings, + IPythonSettings, + ISortImportSettings, + ITerminalSettings, + IUnitTestSettings, + IWorkspaceSymbolSettings +} from './types'; import { SystemVariables } from './variables/systemVariables'; // tslint:disable-next-line:no-require-imports no-var-requires @@ -12,120 +22,6 @@ const untildify = require('untildify'); export const IS_WINDOWS = /^win/.test(process.platform); -export interface IPythonSettings { - pythonPath: string; - venvPath: string; - jediPath: string; - devOptions: string[]; - linting: ILintingSettings; - formatting: IFormattingSettings; - unitTest: IUnitTestSettings; - autoComplete: IAutoCompeteSettings; - terminal: ITerminalSettings; - sortImports: ISortImportSettings; - workspaceSymbols: IWorkspaceSymbolSettings; - envFile: string; - disablePromptForFeatures: string[]; - disableInstallationChecks: boolean; - globalModuleInstallation: boolean; -} -export interface ISortImportSettings { - path: string; - args: string[]; -} - -export interface IUnitTestSettings { - promptToConfigure: boolean; - debugPort: number; - debugHost?: string; - nosetestsEnabled: boolean; - nosetestPath: string; - nosetestArgs: string[]; - pyTestEnabled: boolean; - pyTestPath: string; - pyTestArgs: string[]; - unittestEnabled: boolean; - unittestArgs: string[]; - cwd?: string; -} -export interface IPylintCategorySeverity { - convention: vscode.DiagnosticSeverity; - refactor: vscode.DiagnosticSeverity; - warning: vscode.DiagnosticSeverity; - error: vscode.DiagnosticSeverity; - fatal: vscode.DiagnosticSeverity; -} -export interface IPep8CategorySeverity { - W: vscode.DiagnosticSeverity; - E: vscode.DiagnosticSeverity; -} -// tslint:disable-next-line:interface-name -export interface Flake8CategorySeverity { - F: vscode.DiagnosticSeverity; - E: vscode.DiagnosticSeverity; - W: vscode.DiagnosticSeverity; -} -export interface IMypyCategorySeverity { - error: vscode.DiagnosticSeverity; - note: vscode.DiagnosticSeverity; -} -export interface ILintingSettings { - enabled: boolean; - enabledWithoutWorkspace: boolean; - ignorePatterns: string[]; - prospectorEnabled: boolean; - prospectorArgs: string[]; - pylintEnabled: boolean; - pylintArgs: string[]; - pep8Enabled: boolean; - pep8Args: string[]; - pylamaEnabled: boolean; - pylamaArgs: string[]; - flake8Enabled: boolean; - flake8Args: string[]; - pydocstyleEnabled: boolean; - pydocstyleArgs: string[]; - lintOnSave: boolean; - maxNumberOfProblems: number; - pylintCategorySeverity: IPylintCategorySeverity; - pep8CategorySeverity: IPep8CategorySeverity; - flake8CategorySeverity: Flake8CategorySeverity; - mypyCategorySeverity: IMypyCategorySeverity; - prospectorPath: string; - pylintPath: string; - pep8Path: string; - pylamaPath: string; - flake8Path: string; - pydocstylePath: string; - mypyEnabled: boolean; - mypyArgs: string[]; - mypyPath: string; -} -export interface IFormattingSettings { - provider: string; - autopep8Path: string; - autopep8Args: string[]; - yapfPath: string; - yapfArgs: string[]; -} -export interface IAutoCompeteSettings { - addBrackets: boolean; - extraPaths: string[]; - preloadModules: string[]; -} -export interface IWorkspaceSymbolSettings { - enabled: boolean; - tagFilePath: string; - rebuildOnStart: boolean; - rebuildOnFileSave: boolean; - ctagsPath: string; - exclusionPatterns: string[]; -} -export interface ITerminalSettings { - executeInFileDir: boolean; - launchArgs: string[]; -} - export function isTestExecution(): boolean { // tslint:disable-next-line:interface-name no-string-literal return process.env['VSC_PYTHON_CI_TEST'] === '1'; diff --git a/src/client/common/configuration/service.ts b/src/client/common/configuration/service.ts new file mode 100644 index 000000000000..8ee86ce0de6f --- /dev/null +++ b/src/client/common/configuration/service.ts @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import { Uri } from 'vscode'; +import { PythonSettings } from '../configSettings'; +import { IConfigurationService, IPythonSettings } from '../types'; + +@injectable() +export class ConfigurationService implements IConfigurationService { + public getSettings(resource?: Uri): IPythonSettings { + return PythonSettings.getInstance(resource); + } +} diff --git a/src/client/common/contextKey.ts b/src/client/common/contextKey.ts index 87fe57f07d34..51d18b9431f1 100644 --- a/src/client/common/contextKey.ts +++ b/src/client/common/contextKey.ts @@ -1,15 +1,15 @@ -import { commands } from 'vscode'; +import { ICommandManager } from './application/types'; export class ContextKey { private lastValue: boolean; - constructor(private name: string) { } + constructor(private name: string, private commandManager: ICommandManager) { } public async set(value: boolean): Promise { if (this.lastValue === value) { return; } this.lastValue = value; - await commands.executeCommand('setContext', this.name, this.lastValue); + await this.commandManager.executeCommand('setContext', this.name, this.lastValue); } } diff --git a/src/client/common/installer/pythonInstallation.ts b/src/client/common/installer/pythonInstallation.ts index 431895cc87cd..bf3bd1384370 100644 --- a/src/client/common/installer/pythonInstallation.ts +++ b/src/client/common/installer/pythonInstallation.ts @@ -5,8 +5,8 @@ import { IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../interpreter/contracts'; import { IServiceContainer } from '../../ioc/types'; import { IApplicationShell } from '../application/types'; -import { IPythonSettings } from '../configSettings'; import { IPlatformService } from '../platform/types'; +import { IPythonSettings } from '../types'; export class PythonInstaller { private locator: IInterpreterLocatorService; diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 5417d71f7e30..66c2c8b50133 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -3,19 +3,23 @@ import { IServiceManager } from '../ioc/types'; import { ApplicationShell } from './application/applicationShell'; -import { IApplicationShell } from './application/types'; +import { CommandManager } from './application/commandManager'; +import { DocumentManager } from './application/documentManager'; +import { TerminalManager } from './application/terminalManager'; +import { IApplicationShell, ICommandManager, IDocumentManager, ITerminalManager, IWorkspaceService } from './application/types'; +import { WorkspaceService } from './application/workspace'; +import { ConfigurationService } from './configuration/service'; import { Installer } from './installer/installer'; import { Logger } from './logger'; import { PersistentStateFactory } from './persistentState'; import { IS_64_BIT, IS_WINDOWS } from './platform/constants'; -import { FileSystem } from './platform/fileSystem'; import { PathUtils } from './platform/pathUtils'; -import { PlatformService } from './platform/platformService'; -import { IFileSystem, IPlatformService } from './platform/types'; import { CurrentProcess } from './process/currentProcess'; +import { TerminalServiceFactory } from './terminal/factory'; +import { TerminalHelper } from './terminal/helper'; import { TerminalService } from './terminal/service'; -import { ITerminalService } from './terminal/types'; -import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; +import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './terminal/types'; +import { IConfigurationService, ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, Is64Bit, IsWindows } from './types'; export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingletonInstance(IsWindows, IS_WINDOWS); @@ -24,8 +28,15 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IPersistentStateFactory, PersistentStateFactory); serviceManager.addSingleton(ILogger, Logger); serviceManager.addSingleton(ITerminalService, TerminalService); + serviceManager.addSingleton(ITerminalServiceFactory, TerminalServiceFactory); + serviceManager.addSingleton(ITerminalHelper, TerminalHelper); serviceManager.addSingleton(IPathUtils, PathUtils); serviceManager.addSingleton(IApplicationShell, ApplicationShell); serviceManager.addSingleton(ICurrentProcess, CurrentProcess); serviceManager.addSingleton(IInstaller, Installer); + serviceManager.addSingleton(ICommandManager, CommandManager); + serviceManager.addSingleton(IConfigurationService, ConfigurationService); + serviceManager.addSingleton(IWorkspaceService, WorkspaceService); + serviceManager.addSingleton(IDocumentManager, DocumentManager); + serviceManager.addSingleton(ITerminalManager, TerminalManager); } diff --git a/src/client/common/terminal/factory.ts b/src/client/common/terminal/factory.ts new file mode 100644 index 000000000000..5a3f167890ed --- /dev/null +++ b/src/client/common/terminal/factory.ts @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Disposable } from 'vscode'; +import { ITerminalManager } from '../application/types'; +import { IDisposableRegistry } from '../types'; +import { TerminalService } from './service'; +import { ITerminalHelper, ITerminalService, ITerminalServiceFactory } from './types'; + +@injectable() +export class TerminalServiceFactory implements ITerminalServiceFactory { + private terminalServices: Map; + + constructor( @inject(ITerminalService) private defaultTerminalService: ITerminalService, + @inject(IDisposableRegistry) private disposableRegistry: Disposable[], + @inject(ITerminalManager) private terminalManager: ITerminalManager, + @inject(ITerminalHelper) private terminalHelper: ITerminalHelper) { + + this.terminalServices = new Map(); + } + public getTerminalService(title?: string): ITerminalService { + if (typeof title !== 'string' || title.trim().length === 0) { + return this.defaultTerminalService; + } + if (!this.terminalServices.has(title)) { + const terminalService = new TerminalService(this.terminalHelper, this.terminalManager, this.disposableRegistry, title); + this.terminalServices.set(title, terminalService); + } + return this.terminalServices.get(title)!; + } +} diff --git a/src/client/common/terminal/helper.ts b/src/client/common/terminal/helper.ts new file mode 100644 index 000000000000..ad2c84839512 --- /dev/null +++ b/src/client/common/terminal/helper.ts @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { Terminal, workspace } from 'vscode'; +import { ITerminalManager } from '../application/types'; +import { IPlatformService } from '../platform/types'; +import { ITerminalHelper, TerminalShellType } from './types'; + +const IS_BASH = /(bash.exe$|wsl.exe$|bash$|zsh$)/i; +const IS_COMMAND = /cmd.exe$/i; +const IS_POWERSHELL = /(powershell.exe$|pwsh$|powershell$)/i; +const IS_FISH = /(fish$)/i; + +@injectable() +export class TerminalHelper implements ITerminalHelper { + private readonly detectableShells: Map; + constructor( @inject(IPlatformService) private platformService: IPlatformService, + @inject(ITerminalManager) private terminalManager: ITerminalManager) { + this.detectableShells = new Map(); + this.detectableShells.set(TerminalShellType.powershell, IS_POWERSHELL); + this.detectableShells.set(TerminalShellType.bash, IS_BASH); + this.detectableShells.set(TerminalShellType.commandPrompt, IS_COMMAND); + this.detectableShells.set(TerminalShellType.fish, IS_FISH); + } + public createTerminal(title?: string): Terminal { + return this.terminalManager.createTerminal({ name: title }); + } + public identifyTerminalShell(shellPath: string): TerminalShellType { + return Array.from(this.detectableShells.keys()) + .reduce((matchedShell, shellToDetect) => { + if (matchedShell === TerminalShellType.other && this.detectableShells.get(shellToDetect)!.test(shellPath)) { + return shellToDetect; + } + return matchedShell; + }, TerminalShellType.other); + } + public getTerminalShellPath(): string { + const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + let osSection = ''; + if (this.platformService.isWindows) { + osSection = 'windows'; + } else if (this.platformService.isMac) { + osSection = 'osx'; + } else if (this.platformService.isLinux) { + osSection = 'linux'; + } + if (osSection.length === 0) { + return ''; + } + return shellConfig.get(osSection)!; + } + public buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]) { + const executable = command.indexOf(' ') > 0 ? `"${command}"` : command; + const isPowershell = terminalShellType === TerminalShellType.powershell; + const commandPrefix = isPowershell ? '& ' : ''; + return `${commandPrefix}${executable} ${args.join(' ')}`.trim(); + } +} diff --git a/src/client/common/terminal/service.ts b/src/client/common/terminal/service.ts index 322ad47e9152..79b488b540d3 100644 --- a/src/client/common/terminal/service.ts +++ b/src/client/common/terminal/service.ts @@ -2,66 +2,60 @@ // Licensed under the MIT License. import { inject, injectable } from 'inversify'; -import { Disposable, Terminal, Uri, window, workspace } from 'vscode'; -import { IServiceContainer } from '../../ioc/types'; -import { IPlatformService } from '../platform/types'; +import { Disposable, Event, EventEmitter, Terminal } from 'vscode'; +import { ITerminalManager } from '../application/types'; import { IDisposableRegistry } from '../types'; -import { ITerminalService } from './types'; - -const IS_POWERSHELL = /powershell.exe$/i; +import { ITerminalHelper, ITerminalService, TerminalShellType } from './types'; @injectable() -export class TerminalService implements ITerminalService { +export class TerminalService implements ITerminalService, Disposable { private terminal?: Terminal; - private textPreviouslySentToTerminal: boolean = false; - constructor( @inject(IServiceContainer) private serviceContainer: IServiceContainer) { } - public async sendCommand(command: string, args: string[]): Promise { - const text = this.buildTerminalText(command, args); - const term = await this.getTerminal(); - term.show(false); - term.sendText(text, true); - this.textPreviouslySentToTerminal = true; + private terminalShellType: TerminalShellType; + private terminalClosed = new EventEmitter(); + public get onDidCloseTerminal(): Event { + return this.terminalClosed.event; } + constructor( @inject(ITerminalHelper) private terminalHelper: ITerminalHelper, + @inject(ITerminalManager) terminalManager: ITerminalManager, + @inject(IDisposableRegistry) disposableRegistry: Disposable[], + private title: string = 'Python') { - private async getTerminal() { + disposableRegistry.push(this); + terminalManager.onDidCloseTerminal(this.terminalCloseHandler, this, disposableRegistry); + } + public dispose() { if (this.terminal) { - return this.terminal!; - } - this.terminal = window.createTerminal('Python'); - this.terminal.show(false); - - // Sometimes the terminal takes some time to start up before it can start accepting input. - // However if we have already sent text to the terminal, then no need to wait. - if (!this.textPreviouslySentToTerminal) { - await new Promise(resolve => setTimeout(resolve, 1000)); + this.terminal.dispose(); } - - const handler = window.onDidCloseTerminal((term) => { - if (term === this.terminal) { - this.terminal = undefined; - } - }); - - const disposables = this.serviceContainer.get(IDisposableRegistry); - disposables.push(this.terminal); - disposables.push(handler); - - return this.terminal; } - - private buildTerminalText(command: string, args: string[]) { - const executable = command.indexOf(' ') ? `"${command}"` : command; - const commandPrefix = this.terminalIsPowershell() ? '& ' : ''; - return `${commandPrefix}${executable} ${args.join(' ')}`.trim(); + public async sendCommand(command: string, args: string[]): Promise { + await this.ensureTerminal(); + const text = this.terminalHelper.buildCommandForTerminal(this.terminalShellType, command, args); + this.terminal!.show(); + this.terminal!.sendText(text, true); + } + public async sendText(text: string): Promise { + await this.ensureTerminal(); + this.terminal!.show(); + this.terminal!.sendText(text); } + private async ensureTerminal(): Promise { + if (this.terminal) { + return; + } + const shellPath = this.terminalHelper.getTerminalShellPath(); + this.terminalShellType = !shellPath || shellPath.length === 0 ? TerminalShellType.other : this.terminalHelper.identifyTerminalShell(shellPath); + this.terminal = this.terminalHelper.createTerminal(this.title); + this.terminal!.show(); - private terminalIsPowershell(resource?: Uri) { - const platform = this.serviceContainer.get(IPlatformService); - if (!platform.isWindows) { - return false; + // Sometimes the terminal takes some time to start up before it can start accepting input. + // tslint:disable-next-line:no-unnecessary-callback-wrapper + await new Promise(resolve => setTimeout(() => resolve(), 1000)); + } + private terminalCloseHandler(terminal: Terminal) { + if (terminal === this.terminal) { + this.terminalClosed.fire(); + this.terminal = undefined; } - // tslint:disable-next-line:no-backbone-get-set-outside-model - const terminalName = workspace.getConfiguration('terminal.integrated.shell', resource).get('windows'); - return typeof terminalName === 'string' && IS_POWERSHELL.test(terminalName); } } diff --git a/src/client/common/terminal/types.ts b/src/client/common/terminal/types.ts index 0bd7ac3fbc72..d279e22e4a52 100644 --- a/src/client/common/terminal/types.ts +++ b/src/client/common/terminal/types.ts @@ -1,8 +1,42 @@ + // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +import { Event, Terminal } from 'vscode'; export const ITerminalService = Symbol('ITerminalService'); +export enum TerminalShellType { + powershell = 1, + commandPrompt = 2, + bash = 3, + fish = 4, + other = 5 +} + export interface ITerminalService { + readonly onDidCloseTerminal: Event; sendCommand(command: string, args: string[]): Promise; + sendText(text: string): Promise; +} + +export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory'); + +export interface ITerminalServiceFactory { + /** + * Gets a terminal service with a specific title. + * If one exists, its returned else a new one is created. + * @param {string} title + * @returns {ITerminalService} + * @memberof ITerminalServiceFactory + */ + getTerminalService(title?: string): ITerminalService; +} + +export const ITerminalHelper = Symbol('ITerminalHelper'); + +export interface ITerminalHelper { + createTerminal(title?: string): Terminal; + identifyTerminalShell(shellPath: string): TerminalShellType; + getTerminalShellPath(): string; + buildCommandForTerminal(terminalShellType: TerminalShellType, command: string, args: string[]): string; } diff --git a/src/client/common/types.ts b/src/client/common/types.ts index cbac6002fb58..7faa2567acc7 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -2,7 +2,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { Uri } from 'vscode'; +import { DiagnosticSeverity, Uri } from 'vscode'; import { EnvironmentVariables } from './variables/types'; export const IOutputChannel = Symbol('IOutputChannel'); export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); @@ -87,3 +87,123 @@ export const ICurrentProcess = Symbol('ICurrentProcess'); export interface ICurrentProcess { env: EnvironmentVariables; } + +export interface IPythonSettings { + pythonPath: string; + venvPath: string; + jediPath: string; + devOptions: string[]; + linting: ILintingSettings; + formatting: IFormattingSettings; + unitTest: IUnitTestSettings; + autoComplete: IAutoCompeteSettings; + terminal: ITerminalSettings; + sortImports: ISortImportSettings; + workspaceSymbols: IWorkspaceSymbolSettings; + envFile: string; + disablePromptForFeatures: string[]; + disableInstallationChecks: boolean; + globalModuleInstallation: boolean; +} +export interface ISortImportSettings { + path: string; + args: string[]; +} + +export interface IUnitTestSettings { + promptToConfigure: boolean; + debugPort: number; + debugHost?: string; + nosetestsEnabled: boolean; + nosetestPath: string; + nosetestArgs: string[]; + pyTestEnabled: boolean; + pyTestPath: string; + pyTestArgs: string[]; + unittestEnabled: boolean; + unittestArgs: string[]; + cwd?: string; +} +export interface IPylintCategorySeverity { + convention: DiagnosticSeverity; + refactor: DiagnosticSeverity; + warning: DiagnosticSeverity; + error: DiagnosticSeverity; + fatal: DiagnosticSeverity; +} +export interface IPep8CategorySeverity { + W: DiagnosticSeverity; + E: DiagnosticSeverity; +} +// tslint:disable-next-line:interface-name +export interface Flake8CategorySeverity { + F: DiagnosticSeverity; + E: DiagnosticSeverity; + W: DiagnosticSeverity; +} +export interface IMypyCategorySeverity { + error: DiagnosticSeverity; + note: DiagnosticSeverity; +} +export interface ILintingSettings { + enabled: boolean; + enabledWithoutWorkspace: boolean; + ignorePatterns: string[]; + prospectorEnabled: boolean; + prospectorArgs: string[]; + pylintEnabled: boolean; + pylintArgs: string[]; + pep8Enabled: boolean; + pep8Args: string[]; + pylamaEnabled: boolean; + pylamaArgs: string[]; + flake8Enabled: boolean; + flake8Args: string[]; + pydocstyleEnabled: boolean; + pydocstyleArgs: string[]; + lintOnSave: boolean; + maxNumberOfProblems: number; + pylintCategorySeverity: IPylintCategorySeverity; + pep8CategorySeverity: IPep8CategorySeverity; + flake8CategorySeverity: Flake8CategorySeverity; + mypyCategorySeverity: IMypyCategorySeverity; + prospectorPath: string; + pylintPath: string; + pep8Path: string; + pylamaPath: string; + flake8Path: string; + pydocstylePath: string; + mypyEnabled: boolean; + mypyArgs: string[]; + mypyPath: string; +} +export interface IFormattingSettings { + provider: string; + autopep8Path: string; + autopep8Args: string[]; + yapfPath: string; + yapfArgs: string[]; +} +export interface IAutoCompeteSettings { + addBrackets: boolean; + extraPaths: string[]; + preloadModules: string[]; +} +export interface IWorkspaceSymbolSettings { + enabled: boolean; + tagFilePath: string; + rebuildOnStart: boolean; + rebuildOnFileSave: boolean; + ctagsPath: string; + exclusionPatterns: string[]; +} +export interface ITerminalSettings { + executeInFileDir: boolean; + launchArgs: string[]; +} + +export const IConfigurationService = Symbol('IConfigurationService'); + +export interface IConfigurationService { + getSettings(resource?: Uri): IPythonSettings; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index b84ef1cbca0e..caae8b7f5913 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -10,8 +10,8 @@ import * as os from 'os'; import * as vscode from 'vscode'; import { Disposable, Memento, OutputChannel, window } from 'vscode'; import { BannerService } from './banner'; -import * as settings from './common/configSettings'; import { PythonSettings } from './common/configSettings'; +import * as settings from './common/configSettings'; import { STANDARD_OUTPUT_CHANNEL } from './common/constants'; import { FeatureDeprecationManager } from './common/featureDeprecationManager'; import { createDeferred } from './common/helpers'; @@ -38,7 +38,6 @@ import { JediFactory } from './languageServices/jediProxyFactory'; import { registerTypes as lintersRegisterTypes } from './linters/serviceRegistry'; import { PythonCompletionItemProvider } from './providers/completionProvider'; import { PythonDefinitionProvider } from './providers/definitionProvider'; -import { activateExecInTerminalProvider } from './providers/execInTerminalProvider'; import { PythonFormattingEditProvider } from './providers/formatProvider'; import { PythonHoverProvider } from './providers/hoverProvider'; import { LintProvider } from './providers/lintProvider'; @@ -54,6 +53,8 @@ import * as sortImports from './sortImports'; import { sendTelemetryEvent } from './telemetry'; import { EDITOR_LOAD } from './telemetry/constants'; import { StopWatch } from './telemetry/stopWatch'; +import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; +import { ICodeExecutionManager } from './terminals/types'; import { BlockFormatProviders } from './typeFormatters/blockFormatProvider'; import { TEST_OUTPUT_CHANNEL } from './unittests/common/constants'; import * as tests from './unittests/main'; @@ -88,6 +89,9 @@ export async function activate(context: vscode.ExtensionContext) { formattersRegisterTypes(serviceManager); platformRegisterTypes(serviceManager); installerRegisterTypes(serviceManager); + commonRegisterTerminalTypes(serviceManager); + + serviceManager.get(ICodeExecutionManager).registerCommands(); const persistentStateFactory = serviceManager.get(IPersistentStateFactory); const pythonSettings = settings.PythonSettings.getInstance(); @@ -108,7 +112,6 @@ export async function activate(context: vscode.ExtensionContext) { const processService = serviceContainer.get(IProcessService); const interpreterVersionService = serviceContainer.get(IInterpreterVersionService); context.subscriptions.push(new SetInterpreterProvider(interpreterManager, interpreterVersionService, processService)); - context.subscriptions.push(...activateExecInTerminalProvider()); context.subscriptions.push(activateUpdateSparkLibraryProvider()); activateSimplePythonRefactorProvider(context, standardOutputChannel, serviceContainer); const jediFactory = new JediFactory(context.asAbsolutePath('.'), serviceContainer); diff --git a/src/client/formatters/helper.ts b/src/client/formatters/helper.ts index 579fbdc7772e..4eb20bea34a5 100644 --- a/src/client/formatters/helper.ts +++ b/src/client/formatters/helper.ts @@ -4,7 +4,8 @@ import { injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { IFormattingSettings, PythonSettings } from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; +import { IFormattingSettings } from '../common/types'; import { ExecutionInfo, Product } from '../common/types'; import { FormatterId, FormatterSettingsPropertyNames, IFormatterHelper } from './types'; diff --git a/src/client/formatters/types.ts b/src/client/formatters/types.ts index c8a076c10f93..d21ace78bb4f 100644 --- a/src/client/formatters/types.ts +++ b/src/client/formatters/types.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { IFormattingSettings } from '../common/configSettings'; -import { ExecutionInfo, Product } from '../common/types'; +import { ExecutionInfo, IFormattingSettings, Product } from '../common/types'; export const IFormatterHelper = Symbol('IFormatterHelper'); diff --git a/src/client/linters/baseLinter.ts b/src/client/linters/baseLinter.ts index c7e1555ad709..7f491efa8ab8 100644 --- a/src/client/linters/baseLinter.ts +++ b/src/client/linters/baseLinter.ts @@ -1,9 +1,10 @@ import * as path from 'path'; -import * as vscode from 'vscode'; import { CancellationToken, OutputChannel, TextDocument, Uri } from 'vscode'; -import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import * as vscode from 'vscode'; +import { PythonSettings } from '../common/configSettings'; import '../common/extensions'; import { IPythonToolExecutionService } from '../common/process/types'; +import { IPythonSettings } from '../common/types'; import { ExecutionInfo, IInstaller, ILogger, Product } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { ErrorHandler } from './errorHandlers/main'; diff --git a/src/client/linters/helper.ts b/src/client/linters/helper.ts index c5c5b3ce1014..f0692400cdc9 100644 --- a/src/client/linters/helper.ts +++ b/src/client/linters/helper.ts @@ -1,7 +1,8 @@ import { injectable } from 'inversify'; import * as path from 'path'; import { Uri } from 'vscode'; -import { ILintingSettings, PythonSettings } from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; +import { ILintingSettings } from '../common/types'; import { ExecutionInfo, Product } from '../common/types'; import { ILinterHelper, LinterId, LinterSettingsPropertyNames } from './types'; diff --git a/src/client/linters/types.ts b/src/client/linters/types.ts index 2d99fefeb194..e8a1155f3cef 100644 --- a/src/client/linters/types.ts +++ b/src/client/linters/types.ts @@ -2,8 +2,7 @@ // Licensed under the MIT License. import { Uri } from 'vscode'; -import { ILintingSettings } from '../common/configSettings'; -import { ExecutionInfo, Product } from '../common/types'; +import { ExecutionInfo, ILintingSettings, Product } from '../common/types'; export interface IErrorHandler { handleError(error: Error, resource: Uri, execInfo: ExecutionInfo): Promise; diff --git a/src/client/providers/execInTerminalProvider.ts b/src/client/providers/execInTerminalProvider.ts deleted file mode 100644 index bd1c356f1b2e..000000000000 --- a/src/client/providers/execInTerminalProvider.ts +++ /dev/null @@ -1,267 +0,0 @@ -'use strict'; -import * as fs from 'fs-extra'; -import { EOL } from 'os'; -import * as path from 'path'; -import * as vscode from 'vscode'; -import { Disposable, workspace } from 'vscode'; -import * as settings from '../common/configSettings'; -import { Commands, PythonLanguage } from '../common/constants'; -import { ContextKey } from '../common/contextKey'; -import { IS_WINDOWS } from '../common/utils'; -import { sendTelemetryEvent } from '../telemetry'; -import { EXECUTION_CODE, EXECUTION_DJANGO } from '../telemetry/constants'; - -let terminal: vscode.Terminal; -export function activateExecInTerminalProvider(): vscode.Disposable[] { - const disposables: vscode.Disposable[] = []; - disposables.push(vscode.commands.registerCommand(Commands.Exec_In_Terminal, execInTerminal)); - disposables.push(vscode.commands.registerCommand(Commands.Exec_Selection_In_Terminal, execSelectionInTerminal)); - disposables.push(vscode.commands.registerCommand(Commands.Exec_Selection_In_Django_Shell, execSelectionInDjangoShell)); - disposables.push(vscode.window.onDidCloseTerminal((closedTermina: vscode.Terminal) => { - if (terminal === closedTermina) { - terminal = null; - } - })); - disposables.push(new DjangoContextInitializer()); - return disposables; -} - -function removeBlankLines(code: string): string { - const codeLines = code.split(/\r?\n/g); - const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); - const lastLineIsEmpty = codeLines.length > 0 && codeLines[codeLines.length - 1].trim().length === 0; - if (lastLineIsEmpty) { - codeLinesWithoutEmptyLines.unshift(''); - } - return codeLinesWithoutEmptyLines.join(EOL); -} -function execInTerminal(fileUri?: vscode.Uri) { - const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); - // tslint:disable-next-line:no-backbone-get-set-outside-model - const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - - const pythonSettings = settings.PythonSettings.getInstance(fileUri); - let filePath: string; - - let currentPythonPath = pythonSettings.pythonPath; - if (currentPythonPath.indexOf(' ') > 0) { - currentPythonPath = `"${currentPythonPath}"`; - } - - if (fileUri === undefined || fileUri === null || typeof fileUri.fsPath !== 'string') { - const activeEditor = vscode.window.activeTextEditor; - if (activeEditor !== undefined) { - if (!activeEditor.document.isUntitled) { - if (activeEditor.document.languageId === PythonLanguage.language) { - filePath = activeEditor.document.fileName; - } else { - vscode.window.showErrorMessage('The active file is not a Python source file'); - return; - } - } else { - vscode.window.showErrorMessage('The active file needs to be saved before it can be run'); - return; - } - } else { - vscode.window.showErrorMessage('No open file to run in terminal'); - return; - } - } else { - filePath = fileUri.fsPath; - } - - if (filePath.indexOf(' ') > 0) { - filePath = `"${filePath}"`; - } - terminal = terminal ? terminal : vscode.window.createTerminal('Python'); - if (pythonSettings.terminal && pythonSettings.terminal.executeInFileDir) { - const fileDirPath = path.dirname(filePath); - const wkspace = vscode.workspace.getWorkspaceFolder(vscode.Uri.file(filePath)); - if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.substring(1) !== wkspace.uri.fsPath) { - terminal.sendText(`cd "${fileDirPath}"`); - } - } - const launchArgs = settings.PythonSettings.getInstance(fileUri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; - const command = `${currentPythonPath}${launchArgsString} ${filePath}`; - if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, '/'); - if (IS_POWERSHELL) { - terminal.sendText(`& ${commandWin}`); - } else { - terminal.sendText(commandWin); - } - } else { - terminal.sendText(command); - } - terminal.show(); - sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'file' }); -} - -function execSelectionInTerminal() { - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - - const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); - // tslint:disable-next-line:no-backbone-get-set-outside-model - const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - - let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; - if (currentPythonPath.indexOf(' ') > 0) { - currentPythonPath = `"${currentPythonPath}"`; - } - - const selection = vscode.window.activeTextEditor.selection; - let code: string; - if (selection.isEmpty) { - code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } else { - const textRange = new vscode.Range(selection.start, selection.end); - code = vscode.window.activeTextEditor.document.getText(textRange); - } - if (code.length === 0) { - return; - } - code = removeBlankLines(code); - const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; - const command = `${currentPythonPath}${launchArgsString}`; - if (!terminal) { - terminal = vscode.window.createTerminal('Python'); - if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, '/'); - if (IS_POWERSHELL) { - terminal.sendText(`& ${commandWin}`); - } else { - terminal.sendText(commandWin); - } - } else { - terminal.sendText(command); - } - } - // tslint:disable-next-line:variable-name - const unix_code = code.replace(/\r\n/g, '\n'); - if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, '\r\n')); - } else { - terminal.sendText(unix_code); - } - terminal.show(); - sendTelemetryEvent(EXECUTION_CODE, undefined, { scope: 'selection' }); -} - -function execSelectionInDjangoShell() { - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return; - } - - const terminalShellSettings = vscode.workspace.getConfiguration('terminal.integrated.shell'); - // tslint:disable-next-line:no-backbone-get-set-outside-model - const IS_POWERSHELL = /powershell/.test(terminalShellSettings.get('windows')); - - let currentPythonPath = settings.PythonSettings.getInstance(activeEditor.document.uri).pythonPath; - if (currentPythonPath.indexOf(' ') > 0) { - currentPythonPath = `"${currentPythonPath}"`; - } - - const workspaceUri = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); - const defaultWorkspace = Array.isArray(vscode.workspace.workspaceFolders) && vscode.workspace.workspaceFolders.length > 0 ? vscode.workspace.workspaceFolders[0].uri.fsPath : ''; - const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; - const djangoShellCmd = `"${path.join(workspaceRoot, 'manage.py')}" shell`; - const selection = vscode.window.activeTextEditor.selection; - let code: string; - if (selection.isEmpty) { - code = vscode.window.activeTextEditor.document.lineAt(selection.start.line).text; - } else { - const textRange = new vscode.Range(selection.start, selection.end); - code = vscode.window.activeTextEditor.document.getText(textRange); - } - if (code.length === 0) { - return; - } - const launchArgs = settings.PythonSettings.getInstance(activeEditor.document.uri).terminal.launchArgs; - const launchArgsString = launchArgs.length > 0 ? ' '.concat(launchArgs.join(' ')) : ''; - const command = `${currentPythonPath}${launchArgsString} ${djangoShellCmd}`; - if (!terminal) { - terminal = vscode.window.createTerminal('Django Shell'); - if (IS_WINDOWS) { - const commandWin = command.replace(/\\/g, '/'); - if (IS_POWERSHELL) { - terminal.sendText(`& ${commandWin}`); - } else { - terminal.sendText(commandWin); - } - } else { - terminal.sendText(command); - } - } - // tslint:disable-next-line:variable-name - const unix_code = code.replace(/\r\n/g, '\n'); - if (IS_WINDOWS) { - terminal.sendText(unix_code.replace(/\n/g, '\r\n')); - } else { - terminal.sendText(unix_code); - } - terminal.show(); - sendTelemetryEvent(EXECUTION_DJANGO); -} - -class DjangoContextInitializer implements vscode.Disposable { - private isDjangoProject: ContextKey; - private monitoringActiveTextEditor: boolean; - private workspaceContextKeyValues = new Map(); - private lastCheckedWorkspace: string; - private disposables: Disposable[] = []; - constructor() { - this.isDjangoProject = new ContextKey('python.isDjangoProject'); - this.ensureState() - .catch(ex => console.error('Python Extension: ensureState', ex)); - this.disposables.push(vscode.workspace.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); - } - - public dispose() { - this.isDjangoProject = null; - this.disposables.forEach(disposable => disposable.dispose()); - } - private updateContextKeyBasedOnActiveWorkspace() { - if (this.monitoringActiveTextEditor) { - return; - } - this.monitoringActiveTextEditor = true; - this.disposables.push(vscode.window.onDidChangeActiveTextEditor(() => this.ensureState())); - } - private getActiveWorkspace(): string | undefined { - if (!Array.isArray(workspace.workspaceFolders) || workspace.workspaceFolders.length === 0) { - return undefined; - } - if (workspace.workspaceFolders.length === 1) { - return workspace.workspaceFolders[0].uri.fsPath; - } - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor) { - return undefined; - } - const workspaceFolder = vscode.workspace.getWorkspaceFolder(activeEditor.document.uri); - return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; - } - private async ensureState(): Promise { - const activeWorkspace = this.getActiveWorkspace(); - if (!activeWorkspace) { - return await this.isDjangoProject.set(false); - } - if (this.lastCheckedWorkspace === activeWorkspace) { - return; - } - if (this.workspaceContextKeyValues.has(activeWorkspace)) { - await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)); - } else { - const exists = await fs.pathExists(path.join(activeWorkspace, 'manage.py')); - await this.isDjangoProject.set(exists); - this.workspaceContextKeyValues.set(activeWorkspace, exists); - this.lastCheckedWorkspace = activeWorkspace; - } - } -} diff --git a/src/client/refactor/proxy.ts b/src/client/refactor/proxy.ts index 93b94553d8df..523b4e726084 100644 --- a/src/client/refactor/proxy.ts +++ b/src/client/refactor/proxy.ts @@ -4,10 +4,10 @@ import { ChildProcess } from 'child_process'; import * as path from 'path'; import * as vscode from 'vscode'; import { Uri } from 'vscode'; -import { IPythonSettings } from '../common/configSettings'; import '../common/extensions'; import { createDeferred, Deferred } from '../common/helpers'; import { IPythonExecutionFactory } from '../common/process/types'; +import { IPythonSettings } from '../common/types'; import { getWindowsLineEndingCount, IS_WINDOWS } from '../common/utils'; import { IServiceContainer } from '../ioc/types'; diff --git a/src/client/telemetry/index.ts b/src/client/telemetry/index.ts index 9f92173db7dc..559e7b61d99a 100644 --- a/src/client/telemetry/index.ts +++ b/src/client/telemetry/index.ts @@ -26,12 +26,18 @@ export function sendTelemetryEvent(eventName: string, durationMs?: number, prope } // tslint:disable-next-line:no-any function-name -export function captureTelemetry(eventName: string, properties?: TelemetryProperties) { +export function captureTelemetry(eventName: string, properties?: TelemetryProperties, captureDuration: boolean = true) { // tslint:disable-next-line:no-function-expression no-any return function (target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor) { const originalMethod = descriptor.value; // tslint:disable-next-line:no-function-expression no-any descriptor.value = function (...args: any[]) { + if (!captureDuration) { + sendTelemetryEvent(eventName, undefined, properties); + // tslint:disable-next-line:no-invalid-this + return originalMethod.apply(this, args); + } + const stopWatch = new StopWatch(); // tslint:disable-next-line:no-invalid-this no-use-before-declare no-unsafe-any const result = originalMethod.apply(this, args); diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts new file mode 100644 index 000000000000..fd967c56318f --- /dev/null +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import { Disposable, Uri } from 'vscode'; +import { ICommandManager, IDocumentManager } from '../../common/application/types'; +import { Commands } from '../../common/constants'; +import { IDisposableRegistry } from '../../common/types'; +import { IServiceContainer } from '../../ioc/types'; +import { captureTelemetry } from '../../telemetry'; +import { EXECUTION_CODE, EXECUTION_DJANGO } from '../../telemetry/constants'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../terminals/types'; + +@injectable() +export class CodeExecutionManager implements ICodeExecutionManager { + constructor( @inject(ICommandManager) private commandManager: ICommandManager, + @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IDisposableRegistry) private disposableRegistry: Disposable[], + @inject(IServiceContainer) private serviceContainer: IServiceContainer) { + + } + + public registerCommands() { + this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_In_Terminal, this.executeFileInterTerminal.bind(this))); + this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_Selection_In_Terminal, this.executeSelectionInTerminal.bind(this))); + this.disposableRegistry.push(this.commandManager.registerCommand(Commands.Exec_Selection_In_Django_Shell, this.executeSelectionInDjangoShell.bind(this))); + } + @captureTelemetry(EXECUTION_CODE, { scope: 'file' }, false) + private async executeFileInterTerminal(file: Uri) { + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + const fileToExecute = file ? file : await codeExecutionHelper.getFileToExecute(); + if (!fileToExecute) { + return; + } + const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); + await executionService.executeFile(fileToExecute); + } + + @captureTelemetry(EXECUTION_CODE, { scope: 'selection' }, false) + private async executeSelectionInTerminal(): Promise { + const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); + + await this.executeSelection(executionService); + } + + @captureTelemetry(EXECUTION_DJANGO, { scope: 'selection' }, false) + private async executeSelectionInDjangoShell(): Promise { + const executionService = this.serviceContainer.get(ICodeExecutionService, 'djangoShell'); + await this.executeSelection(executionService); + } + + private async executeSelection(executionService: ICodeExecutionService): Promise { + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor) { + return; + } + const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); + const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); + const normalizedCode = codeExecutionHelper.normalizeLines(codeToExecute!); + if (!normalizedCode || normalizedCode.trim().length === 0) { + return; + } + + await executionService.execute(codeToExecute!, activeEditor!.document.uri); + } +} diff --git a/src/client/terminals/codeExecution/djangoContext.ts b/src/client/terminals/codeExecution/djangoContext.ts new file mode 100644 index 000000000000..00a5dd8e437b --- /dev/null +++ b/src/client/terminals/codeExecution/djangoContext.ts @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { ContextKey } from '../../common/contextKey'; +import { IFileSystem } from '../../common/platform/types'; + +@injectable() +export class DjangoContextInitializer implements Disposable { + private readonly isDjangoProject: ContextKey; + private monitoringActiveTextEditor: boolean; + private workspaceContextKeyValues = new Map(); + private lastCheckedWorkspace: string; + private disposables: Disposable[] = []; + + constructor(private documentManager: IDocumentManager, + private workpaceService: IWorkspaceService, + private fileSystem: IFileSystem, + commandManager: ICommandManager) { + + this.isDjangoProject = new ContextKey('python.isDjangoProject', commandManager); + this.ensureContextStateIsSet() + .catch(ex => console.error('Python Extension: ensureState', ex)); + this.disposables.push(this.workpaceService.onDidChangeWorkspaceFolders(() => this.updateContextKeyBasedOnActiveWorkspace())); + } + + public dispose() { + this.disposables.forEach(disposable => disposable.dispose()); + } + private updateContextKeyBasedOnActiveWorkspace() { + if (this.monitoringActiveTextEditor) { + return; + } + this.monitoringActiveTextEditor = true; + this.disposables.push(this.documentManager.onDidChangeActiveTextEditor(() => this.ensureContextStateIsSet())); + } + private getActiveWorkspace(): string | undefined { + if (!Array.isArray(this.workpaceService.workspaceFolders) || this.workpaceService.workspaceFolders.length === 0) { + return; + } + if (this.workpaceService.workspaceFolders.length === 1) { + return this.workpaceService.workspaceFolders[0].uri.fsPath; + } + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor) { + return; + } + const workspaceFolder = this.workpaceService.getWorkspaceFolder(activeEditor.document.uri); + return workspaceFolder ? workspaceFolder.uri.fsPath : undefined; + } + private async ensureContextStateIsSet(): Promise { + const activeWorkspace = this.getActiveWorkspace(); + if (!activeWorkspace) { + return await this.isDjangoProject.set(false); + } + if (this.lastCheckedWorkspace === activeWorkspace) { + return; + } + if (this.workspaceContextKeyValues.has(activeWorkspace)) { + await this.isDjangoProject.set(this.workspaceContextKeyValues.get(activeWorkspace)!); + } else { + const exists = await this.fileSystem.fileExistsAsync(path.join(activeWorkspace, 'manage.py')); + await this.isDjangoProject.set(exists); + this.workspaceContextKeyValues.set(activeWorkspace, exists); + this.lastCheckedWorkspace = activeWorkspace; + } + } +} diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts new file mode 100644 index 000000000000..d2c7f0d72943 --- /dev/null +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../common/application/types'; +import { IFileSystem, IPlatformService } from '../../common/platform/types'; +import { ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService } from '../../common/types'; +import { IDisposableRegistry } from '../../common/types'; +import { DjangoContextInitializer } from './djangoContext'; +import { TerminalCodeExecutionProvider } from './terminalCodeExecution'; + +@injectable() +export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvider { + constructor( @inject(ITerminalServiceFactory) terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) configurationService: IConfigurationService, + @inject(IWorkspaceService) workspace: IWorkspaceService, + @inject(IDocumentManager) documentManager: IDocumentManager, + @inject(IPlatformService) platformService: IPlatformService, + @inject(ICommandManager) commandManager: ICommandManager, + @inject(IFileSystem) fileSystem: IFileSystem, + @inject(IDisposableRegistry) disposableRegistry: Disposable[]) { + + super(terminalServiceFactory, configurationService, workspace, disposableRegistry, platformService); + this.terminalTitle = 'Django Shell'; + disposableRegistry.push(new DjangoContextInitializer(documentManager, workspace, fileSystem, commandManager)); + } + public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { + const pythonSettings = this.configurationService.getSettings(resource); + const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; + const args = pythonSettings.terminal.launchArgs.slice(); + + const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined; + const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : ''; + const workspaceRoot = workspaceUri ? workspaceUri.uri.fsPath : defaultWorkspace; + const managePyPath = workspaceRoot.length === 0 ? 'manage.py' : path.join(workspaceRoot, 'manage.py'); + const escapedManagePyPath = managePyPath.indexOf(' ') > 0 ? `"${managePyPath}"` : managePyPath; + + args.push(escapedManagePyPath); + args.push('shell'); + return { command, args }; + } +} diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts new file mode 100644 index 000000000000..efa468dcbc95 --- /dev/null +++ b/src/client/terminals/codeExecution/helper.ts @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { inject, injectable } from 'inversify'; +import { EOL } from 'os'; +import { Range, TextEditor, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../common/application/types'; +import { PythonLanguage } from '../../common/constants'; +import '../../common/extensions'; +import { ICodeExecutionHelper } from '../types'; + +@injectable() +export class CodeExecutionHelper implements ICodeExecutionHelper { + constructor( @inject(IDocumentManager) private documentManager: IDocumentManager, + @inject(IApplicationShell) private applicationShell: IApplicationShell) { + + } + public normalizeLines(code: string): string { + const codeLines = code.splitLines({ trim: false, removeEmptyEntries: false }); + const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0); + return codeLinesWithoutEmptyLines.join(EOL); + } + + public async getFileToExecute(): Promise { + const activeEditor = this.documentManager.activeTextEditor!; + if (!activeEditor) { + this.applicationShell.showErrorMessage('No open file to run in terminal'); + return; + } + if (activeEditor.document.isUntitled) { + this.applicationShell.showErrorMessage('The active file needs to be saved before it can be run'); + return; + } + if (activeEditor.document.languageId !== PythonLanguage.language) { + this.applicationShell.showErrorMessage('The active file is not a Python source file'); + return; + } + return activeEditor.document.uri; + } + + public async getSelectedTextToExecute(textEditor: TextEditor): Promise { + if (!textEditor) { + return; + } + + const selection = textEditor.selection; + let code: string; + if (selection.isEmpty) { + code = textEditor.document.lineAt(selection.start.line).text; + } else { + const textRange = new Range(selection.start, selection.end); + code = textEditor.document.getText(textRange); + } + return code; + } +} diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts new file mode 100644 index 000000000000..ad7c8ef9aeea --- /dev/null +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { inject, injectable } from 'inversify'; +import * as path from 'path'; +import { Disposable, Uri } from 'vscode'; +import { IWorkspaceService } from '../../common/application/types'; +import { IPlatformService } from '../../common/platform/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; +import { IConfigurationService } from '../../common/types'; +import { IDisposableRegistry } from '../../common/types'; +import { ICodeExecutionService } from '../../terminals/types'; + +@injectable() +export class TerminalCodeExecutionProvider implements ICodeExecutionService { + protected terminalTitle: string; + private _terminalService: ITerminalService; + private replActive?: Promise; + private get terminalService(): ITerminalService { + if (!this._terminalService) { + this._terminalService = this.terminalServiceFactory.getTerminalService(this.terminalTitle); + this.disposables.push(this.terminalService.onDidCloseTerminal(() => { + this.replActive = undefined; + })); + } + return this._terminalService; + } + constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + @inject(IConfigurationService) protected readonly configurationService: IConfigurationService, + @inject(IWorkspaceService) protected readonly workspace: IWorkspaceService, + @inject(IDisposableRegistry) protected readonly disposables: Disposable[], + @inject(IPlatformService) protected readonly platformService: IPlatformService) { + + } + public async executeFile(file: Uri) { + const pythonSettings = this.configurationService.getSettings(file); + + this.setCwdForFileExecution(file); + + const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; + const filePath = file.fsPath.indexOf(' ') > 0 ? `"${file.fsPath}"` : file.fsPath; + const launchArgs = pythonSettings.terminal.launchArgs; + + this.terminalService.sendCommand(command, launchArgs.concat(filePath)); + } + + public async execute(code: string, resource?: Uri): Promise { + if (!code || code.trim().length === 0) { + return; + } + + await this.ensureRepl(); + this.terminalService.sendText(code); + } + + public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { + const pythonSettings = this.configurationService.getSettings(resource); + const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath; + const args = pythonSettings.terminal.launchArgs.slice(); + return { command, args }; + } + + private setCwdForFileExecution(file: Uri) { + const pythonSettings = this.configurationService.getSettings(file); + if (!pythonSettings.terminal.executeInFileDir) { + return; + } + const fileDirPath = path.dirname(file.fsPath); + const wkspace = this.workspace.getWorkspaceFolder(file); + if (wkspace && fileDirPath !== wkspace.uri.fsPath && fileDirPath.length > 0) { + const escapedPath = fileDirPath.indexOf(' ') > 0 ? `"${fileDirPath}"` : fileDirPath; + this.terminalService.sendText(`cd ${escapedPath}`); + } + } + + private async ensureRepl(resource?: Uri) { + if (this.replActive && await this.replActive!) { + return; + } + this.replActive = new Promise(resolve => { + const replCommandArgs = this.getReplCommandArgs(resource); + this.terminalService.sendCommand(replCommandArgs.command, replCommandArgs.args); + + // Give python repl time to start before we start sending text. + setTimeout(() => resolve(true), 1000); + }); + + await this.replActive; + } +} diff --git a/src/client/terminals/serviceRegistry.ts b/src/client/terminals/serviceRegistry.ts new file mode 100644 index 000000000000..ef706bf31e51 --- /dev/null +++ b/src/client/terminals/serviceRegistry.ts @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { IServiceManager } from '../ioc/types'; +import { CodeExecutionManager } from './codeExecution/codeExecutionManager'; +import { DjangoShellCodeExecutionProvider } from './codeExecution/djangoShellCodeExecution'; +import { CodeExecutionHelper } from './codeExecution/helper'; +import { TerminalCodeExecutionProvider } from './codeExecution/terminalCodeExecution'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ICodeExecutionHelper, CodeExecutionHelper); + serviceManager.addSingleton(ICodeExecutionManager, CodeExecutionManager); + serviceManager.addSingleton(ICodeExecutionService, DjangoShellCodeExecutionProvider, 'djangoShell'); + serviceManager.addSingleton(ICodeExecutionService, TerminalCodeExecutionProvider, 'standard'); +} diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts new file mode 100644 index 000000000000..3573a886d4b6 --- /dev/null +++ b/src/client/terminals/types.ts @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { TextEditor, Uri } from 'vscode'; + +export const ICodeExecutionService = Symbol('ICodeExecutionService'); + +export interface ICodeExecutionService { + execute(code: string, resource?: Uri): Promise; + executeFile(file: Uri): Promise; +} + +export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); + +export interface ICodeExecutionHelper { + normalizeLines(code: string): string; + getFileToExecute(): Promise; + getSelectedTextToExecute(textEditor: TextEditor): Promise; +} + +export const ICodeExecutionManager = Symbol('ICodeExecutionManager'); + +export interface ICodeExecutionManager { + registerCommands(): void; +} diff --git a/src/client/unittests/common/managers/baseTestManager.ts b/src/client/unittests/common/managers/baseTestManager.ts index 9e727561f9ac..7ee714e718ad 100644 --- a/src/client/unittests/common/managers/baseTestManager.ts +++ b/src/client/unittests/common/managers/baseTestManager.ts @@ -1,7 +1,8 @@ import * as vscode from 'vscode'; import { Disposable, OutputChannel, Uri, workspace } from 'vscode'; -import { IPythonSettings, PythonSettings } from '../../../common/configSettings'; +import { PythonSettings } from '../../../common/configSettings'; import { isNotInstalledError } from '../../../common/helpers'; +import { IPythonSettings } from '../../../common/types'; import { IDisposableRegistry, IInstaller, IOutputChannel, Product } from '../../../common/types'; import { IServiceContainer } from '../../../ioc/types'; import { UNITTEST_DISCOVER, UNITTEST_RUN } from '../../../telemetry/constants'; diff --git a/src/client/unittests/common/runner.ts b/src/client/unittests/common/runner.ts index 31d08a48f784..56f7fba07eab 100644 --- a/src/client/unittests/common/runner.ts +++ b/src/client/unittests/common/runner.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import { CancellationToken, OutputChannel, Uri } from 'vscode'; -import { IPythonSettings, PythonSettings } from '../../common/configSettings'; +import { PythonSettings } from '../../common/configSettings'; import { ErrorUtils } from '../../common/errors/errorUtils'; import { ModuleNotInstalledError } from '../../common/errors/moduleNotInstalledError'; import { IPythonToolExecutionService } from '../../common/process/types'; @@ -10,6 +10,7 @@ import { ObservableExecutionResult, SpawnOptions } from '../../common/process/types'; +import { IPythonSettings } from '../../common/types'; import { ExecutionInfo } from '../../common/types'; import { IServiceContainer } from '../../ioc/types'; import { NOSETEST_PROVIDER, PYTEST_PROVIDER, UNITTEST_PROVIDER } from './constants'; diff --git a/src/client/unittests/common/testUtils.ts b/src/client/unittests/common/testUtils.ts index 123991147951..385185d5555c 100644 --- a/src/client/unittests/common/testUtils.ts +++ b/src/client/unittests/common/testUtils.ts @@ -3,8 +3,8 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { Uri, workspace } from 'vscode'; import { window } from 'vscode'; -import { IUnitTestSettings } from '../../common/configSettings'; import * as constants from '../../common/constants'; +import { IUnitTestSettings } from '../../common/types'; import { Product } from '../../common/types'; import { CommandSource } from './constants'; import { TestFlatteningVisitor } from './testVisitors/flatteningVisitor'; diff --git a/src/client/unittests/common/types.ts b/src/client/unittests/common/types.ts index e9c27c6343a3..11b41183fa51 100644 --- a/src/client/unittests/common/types.ts +++ b/src/client/unittests/common/types.ts @@ -1,5 +1,5 @@ import { CancellationToken, Disposable, OutputChannel, Uri } from 'vscode'; -import { IUnitTestSettings } from '../../common/configSettings'; +import { IUnitTestSettings } from '../../common/types'; import { Product } from '../../common/types'; import { CommandSource } from './constants'; diff --git a/src/client/workspaceSymbols/generator.ts b/src/client/workspaceSymbols/generator.ts index 42205f911e2c..441605860357 100644 --- a/src/client/workspaceSymbols/generator.ts +++ b/src/client/workspaceSymbols/generator.ts @@ -1,8 +1,9 @@ import * as fs from 'fs'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IPythonSettings, PythonSettings } from '../common/configSettings'; +import { PythonSettings } from '../common/configSettings'; import { IProcessService } from '../common/process/types'; +import { IPythonSettings } from '../common/types'; import { captureTelemetry } from '../telemetry'; import { WORKSPACE_SYMBOLS_BUILD } from '../telemetry/constants'; diff --git a/src/test/common/configSettings.test.ts b/src/test/common/configSettings.test.ts index 1c586a033798..6e0278394ef3 100644 --- a/src/test/common/configSettings.test.ts +++ b/src/test/common/configSettings.test.ts @@ -1,8 +1,9 @@ import * as assert from 'assert'; import * as path from 'path'; import * as vscode from 'vscode'; -import { IWorkspaceSymbolSettings, PythonSettings } from '../../client/common/configSettings'; +import { PythonSettings } from '../../client/common/configSettings'; import { IS_WINDOWS } from '../../client/common/platform/constants'; +import { IWorkspaceSymbolSettings } from '../../client/common/types'; import { SystemVariables } from '../../client/common/variables/systemVariables'; import { initialize } from './../initialize'; diff --git a/src/test/common/configuration/service.test.ts b/src/test/common/configuration/service.test.ts new file mode 100644 index 000000000000..63606cf11b94 --- /dev/null +++ b/src/test/common/configuration/service.test.ts @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { workspace } from 'vscode'; +import { PythonSettings } from '../../../client/common/configSettings'; +import { IConfigurationService } from '../../../client/common/types'; +import { initialize } from '../../initialize'; +import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; + +// tslint:disable-next-line:max-func-body-length +suite('Configuration Service', () => { + let ioc: UnitTestIocContainer; + suiteSetup(initialize); + setup(() => { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + }); + teardown(() => ioc.dispose()); + + test('Ensure same instance of settings return', () => { + const workspaceUri = workspace.workspaceFolders![0].uri; + const settings = ioc.serviceContainer.get(IConfigurationService).getSettings(workspaceUri); + const instanceIsSame = settings === PythonSettings.getInstance(workspaceUri); + expect(instanceIsSame).to.be.equal(true, 'Incorrect settings'); + }); +}); diff --git a/src/test/common/installer.test.ts b/src/test/common/installer.test.ts index f9d02ed42b06..bc5f3933981b 100644 --- a/src/test/common/installer.test.ts +++ b/src/test/common/installer.test.ts @@ -10,13 +10,11 @@ import { PersistentStateFactory } from '../../client/common/persistentState'; import { PathUtils } from '../../client/common/platform/pathUtils'; import { CurrentProcess } from '../../client/common/process/currentProcess'; import { IProcessService } from '../../client/common/process/types'; -import { ITerminalService } from '../../client/common/terminal/types'; import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows, ModuleNamePurpose, Product } from '../../client/common/types'; import { updateSetting } from '../common'; import { rootWorkspaceUri } from '../common'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; -import { MockTerminalService } from '../mocks/terminalService'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initializeTest, IS_MULTI_ROOT_TEST } from './../initialize'; @@ -54,7 +52,6 @@ suite('Installer', () => { ioc.serviceManager.addSingleton(ICurrentProcess, CurrentProcess); ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingleton(ITerminalService, MockTerminalService); ioc.serviceManager.addSingletonInstance(IsWindows, false); } async function resetSettings() { diff --git a/src/test/common/moduleInstaller.test.ts b/src/test/common/moduleInstaller.test.ts index b769dc5dd618..7730e09da58a 100644 --- a/src/test/common/moduleInstaller.test.ts +++ b/src/test/common/moduleInstaller.test.ts @@ -1,5 +1,6 @@ import { expect } from 'chai'; import * as path from 'path'; +import * as TypeMoq from 'typemoq'; import { ConfigurationTarget, Uri } from 'vscode'; import { PythonSettings } from '../../client/common/configSettings'; import { CondaInstaller } from '../../client/common/installer/condaInstaller'; @@ -17,20 +18,18 @@ import { IProcessService, IPythonExecutionFactory } from '../../client/common/pr import { ITerminalService } from '../../client/common/terminal/types'; import { ICurrentProcess, IInstaller, ILogger, IPathUtils, IPersistentStateFactory, IsWindows } from '../../client/common/types'; import { ICondaLocatorService, IInterpreterLocatorService, INTERPRETER_LOCATOR_SERVICE, InterpreterType } from '../../client/interpreter/contracts'; -import { PythonInterpreterLocatorService } from '../../client/interpreter/locators/index'; -import { updateSetting } from '../common'; -import { rootWorkspaceUri } from '../common'; +import { rootWorkspaceUri, updateSetting } from '../common'; import { MockProvider } from '../interpreters/mocks'; import { MockCondaLocator } from '../mocks/condaLocator'; import { MockModuleInstaller } from '../mocks/moduleInstaller'; import { MockProcessService } from '../mocks/proc'; -import { MockTerminalService } from '../mocks/terminalService'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; import { closeActiveWindows, initializeTest } from './../initialize'; // tslint:disable-next-line:max-func-body-length suite('Module Installer', () => { let ioc: UnitTestIocContainer; + let mockTerminalService: TypeMoq.IMock; const workspaceUri = Uri.file(path.join(__dirname, '..', '..', '..', 'src', 'test')); suiteSetup(initializeTest); setup(async () => { @@ -58,6 +57,9 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton(ILogger, Logger); ioc.serviceManager.addSingleton(IInstaller, Installer); + mockTerminalService = TypeMoq.Mock.ofType(); + ioc.serviceManager.addSingletonInstance(ITerminalService, mockTerminalService.object); + ioc.serviceManager.addSingleton(IModuleInstaller, PipInstaller); ioc.serviceManager.addSingleton(IModuleInstaller, CondaInstaller); ioc.serviceManager.addSingleton(ICondaLocatorService, MockCondaLocator); @@ -67,7 +69,6 @@ suite('Module Installer', () => { ioc.serviceManager.addSingleton(IPlatformService, PlatformService); ioc.registerMockProcessTypes(); - ioc.serviceManager.addSingleton(ITerminalService, MockTerminalService); ioc.serviceManager.addSingletonInstance(IsWindows, false); } async function resetSettings() { @@ -145,18 +146,19 @@ suite('Module Installer', () => { ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); const moduleName = 'xyz'; - const terminalService = ioc.serviceContainer.get(ITerminalService); const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + let argsSent: string[] = []; + mockTerminalService + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); await pipInstaller.installModule(moduleName); - const commandSent = await terminalService.commandSent; - const commandParts = commandSent.split(' '); - commandParts.shift(); - expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); + + expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName} --user`, 'Invalid command sent to terminal for installation.'); }); test('Validate Conda install arguments', async () => { @@ -164,17 +166,19 @@ suite('Module Installer', () => { ioc.serviceManager.addSingletonInstance(IInterpreterLocatorService, mockInterpreterLocator, INTERPRETER_LOCATOR_SERVICE); const moduleName = 'xyz'; - const terminalService = ioc.serviceContainer.get(ITerminalService); const moduleInstallers = ioc.serviceContainer.getAll(IModuleInstaller); const pipInstaller = moduleInstallers.find(item => item.displayName === 'Pip')!; expect(pipInstaller).not.to.be.an('undefined', 'Pip installer not found'); + let argsSent: string[] = []; + mockTerminalService + .setup(t => t.sendCommand(TypeMoq.It.isAnyString(), TypeMoq.It.isAny())) + .returns((cmd: string, args: string[]) => { argsSent = args; return Promise.resolve(void 0); }); + await pipInstaller.installModule(moduleName); - const commandSent = await terminalService.commandSent; - const commandParts = commandSent.split(' '); - commandParts.shift(); - expect(commandParts.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); + + expect(argsSent.join(' ')).equal(`-m pip install -U ${moduleName}`, 'Invalid command sent to terminal for installation.'); }); }); diff --git a/src/test/common/terminals/factory.test.ts b/src/test/common/terminals/factory.test.ts new file mode 100644 index 000000000000..e853b03c0a75 --- /dev/null +++ b/src/test/common/terminals/factory.test.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { initialize } from '../../initialize'; +import { UnitTestIocContainer } from '../../unittests/serviceRegistry'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service Factory', () => { + let ioc: UnitTestIocContainer; + suiteSetup(initialize); + setup(() => { + ioc = new UnitTestIocContainer(); + ioc.registerCommonTypes(); + ioc.registerPlatformTypes(); + }); + teardown(() => ioc.dispose()); + + test('Ensure same instance of terminal service is returned', () => { + const defaultInstance = ioc.serviceContainer.get(ITerminalService); + const factory = ioc.serviceContainer.get(ITerminalServiceFactory); + const sameInstance = factory.getTerminalService() === defaultInstance; + expect(sameInstance).to.equal(true, 'Instances are not the same'); + }); + + test('Ensure different instance of terminal service is returned when title is provided', () => { + const defaultInstance = ioc.serviceContainer.get(ITerminalService); + const factory = ioc.serviceContainer.get(ITerminalServiceFactory); + const instance = factory.getTerminalService('New Title'); + const sameInstance = instance === defaultInstance; + expect(sameInstance).to.not.equal(true, 'Instances are the same'); + }); +}); diff --git a/src/test/common/terminals/helper.test.ts b/src/test/common/terminals/helper.test.ts new file mode 100644 index 000000000000..6feaa886af4d --- /dev/null +++ b/src/test/common/terminals/helper.test.ts @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Terminal as VSCodeTerminal, workspace } from 'vscode'; +import { ITerminalManager } from '../../../client/common/application/types'; +import { EnumEx } from '../../../client/common/enumUtils'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { TerminalHelper } from '../../../client/common/terminal/helper'; +import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { initialize, IS_MULTI_ROOT_TEST } from '../../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Helper', () => { + let platformService: TypeMoq.IMock; + let terminalManager: TypeMoq.IMock; + let helper: ITerminalHelper; + suiteSetup(function () { + if (!IS_MULTI_ROOT_TEST) { + // tslint:disable-next-line:no-invalid-this + this.skip(); + return; + } + return initialize(); + }); + setup(() => { + platformService = TypeMoq.Mock.ofType(); + terminalManager = TypeMoq.Mock.ofType(); + helper = new TerminalHelper(platformService.object, terminalManager.object); + }); + + test('Test identification of Terminal Shells', async () => { + const shellPathsAndIdentification = new Map(); + shellPathsAndIdentification.set('c:\\windows\\system32\\cmd.exe', TerminalShellType.commandPrompt); + + shellPathsAndIdentification.set('c:\\windows\\system32\\bash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\windows\\system32\\wsl.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('c:\\windows\\system32\\gitbash.exe', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/bash', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.bash); + shellPathsAndIdentification.set('/usr/bin/zsh', TerminalShellType.bash); + + shellPathsAndIdentification.set('c:\\windows\\system32\\powershell.exe', TerminalShellType.powershell); + shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/powershell', TerminalShellType.powershell); + shellPathsAndIdentification.set('/usr/microsoft/xxx/powershell/pwsh', TerminalShellType.powershell); + + shellPathsAndIdentification.set('/usr/bin/fish', TerminalShellType.fish); + + shellPathsAndIdentification.set('c:\\windows\\system32\\shell.exe', TerminalShellType.other); + shellPathsAndIdentification.set('/usr/bin/shell', TerminalShellType.other); + + shellPathsAndIdentification.forEach((shellType, shellPath) => { + expect(helper.identifyTerminalShell(shellPath)).to.equal(shellType, `Incorrect Shell Type for path '${shellPath}'`); + }); + }); + + test('Ensure path for shell is correctly retrieved from settings (osx)', async () => { + const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + + platformService.setup(p => p.isWindows).returns(() => false); + platformService.setup(p => p.isLinux).returns(() => false); + platformService.setup(p => p.isMac).returns(() => true); + expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('osx'), 'Incorrect path for Osx'); + }); + test('Ensure path for shell is correctly retrieved from settings (linux)', async () => { + const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + + platformService.setup(p => p.isWindows).returns(() => false); + platformService.setup(p => p.isLinux).returns(() => true); + platformService.setup(p => p.isMac).returns(() => false); + expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('linux'), 'Incorrect path for Linux'); + }); + test('Ensure path for shell is correctly retrieved from settings (windows)', async () => { + const shellConfig = workspace.getConfiguration('terminal.integrated.shell'); + + platformService.setup(p => p.isWindows).returns(() => true); + platformService.setup(p => p.isLinux).returns(() => false); + platformService.setup(p => p.isMac).returns(() => false); + expect(helper.getTerminalShellPath()).to.equal(shellConfig.get('windows'), 'Incorrect path for Windows'); + }); + + test('Ensure spaces in command is quoted', async () => { + EnumEx.getNamesAndValues(TerminalShellType).forEach(item => { + const command = 'c:\\python 3.7.exe'; + const args = ['1', '2']; + const commandPrefix = (item.value === TerminalShellType.powershell) ? '& ' : ''; + const expectedTerminalCommand = `${commandPrefix}"${command}" 1 2`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + + test('Ensure empty args are ignored', async () => { + EnumEx.getNamesAndValues(TerminalShellType).forEach(item => { + const command = 'python3.7.exe'; + const args = []; + const commandPrefix = (item.value === TerminalShellType.powershell) ? '& ' : ''; + const expectedTerminalCommand = `${commandPrefix}${command}`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell '${item.name}'`); + }); + }); + + test('Ensure empty args are ignored with s in command', async () => { + EnumEx.getNamesAndValues(TerminalShellType).forEach(item => { + const command = 'c:\\python 3.7.exe'; + const args = []; + const commandPrefix = (item.value === TerminalShellType.powershell) ? '& ' : ''; + const expectedTerminalCommand = `${commandPrefix}"${command}"`; + + const terminalCommand = helper.buildCommandForTerminal(item.value, command, args); + expect(terminalCommand).to.equal(expectedTerminalCommand, `Incorrect command for Shell ${item.name}`); + }); + }); + + test('Ensure a terminal is created (without a title)', () => { + const expectedTerminal = { x: 'Dummy' }; + // tslint:disable-next-line:no-any + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => expectedTerminal as any as VSCodeTerminal); + helper.createTerminal(); + terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: undefined })), TypeMoq.Times.once()); + }); + + test('Ensure a terminal is created with the title provided', () => { + const expectedTerminal = { x: 'Dummy' }; + // tslint:disable-next-line:no-any + terminalManager.setup(t => t.createTerminal(TypeMoq.It.isAny())).returns(() => expectedTerminal as any as VSCodeTerminal); + helper.createTerminal('1234'); + terminalManager.verify(t => t.createTerminal(TypeMoq.It.isValue({ name: '1234' })), TypeMoq.Times.once()); + }); + +}); diff --git a/src/test/common/terminals/service.test.ts b/src/test/common/terminals/service.test.ts new file mode 100644 index 000000000000..9cb91f6e7ca1 --- /dev/null +++ b/src/test/common/terminals/service.test.ts @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Terminal as VSCodeTerminal } from 'vscode'; +import { ITerminalManager } from '../../../client/common/application/types'; +import { TerminalService } from '../../../client/common/terminal/service'; +import { ITerminalHelper, TerminalShellType } from '../../../client/common/terminal/types'; +import { initialize } from '../../initialize'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal Service', () => { + let service: TerminalService; + let helper: TypeMoq.IMock; + let terminal: TypeMoq.IMock; + let terminalManager: TypeMoq.IMock; + let disposables: Disposable[] = []; + suiteSetup(initialize); + setup(() => { + helper = TypeMoq.Mock.ofType(); + terminal = TypeMoq.Mock.ofType(); + terminalManager = TypeMoq.Mock.ofType(); + disposables = []; + helper.setup(h => h.createTerminal()).returns(() => terminal.object); + helper.setup(h => h.createTerminal(TypeMoq.It.isAny())).returns(() => terminal.object); + helper.setup(h => h.getTerminalShellPath()).returns(() => ''); + helper.setup(h => h.identifyTerminalShell(TypeMoq.It.isAnyString())).returns(() => TerminalShellType.other); + }); + teardown(() => { + if (service) { + // tslint:disable-next-line:no-any + service.dispose(); + } + disposables.filter(item => !!item).forEach(item => item.dispose()); + }); + + test('Ensure terminal is disposed', async () => { + service = new TerminalService(helper.object, terminalManager.object, disposables); + await service.sendCommand('', []); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); + service.dispose(); + terminal.verify(t => t.dispose(), TypeMoq.Times.exactly(1)); + }); + + test('Ensure command is sent to terminal and it is shown', async () => { + service = new TerminalService(helper.object, terminalManager.object, disposables); + const commandToSend = 'SomeCommand'; + const args = ['1', '2']; + const commandToExpect = [commandToSend].concat(args).join(' '); + helper.setup(h => h.buildCommandForTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => commandToExpect); + await service.sendCommand(commandToSend, args); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); + terminal.verify(t => t.sendText(TypeMoq.It.isValue(commandToExpect), TypeMoq.It.isValue(true)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure text is sent to terminal and it is shown', async () => { + service = new TerminalService(helper.object, terminalManager.object, disposables); + const textToSend = 'Some Text'; + await service.sendText(textToSend); + + terminal.verify(t => t.show(), TypeMoq.Times.exactly(2)); + terminal.verify(t => t.sendText(TypeMoq.It.isValue(textToSend)), TypeMoq.Times.exactly(1)); + }); + + test('Ensure close event is not fired when another terminal is closed', async () => { + let eventFired = false; + let eventHandler: undefined | (() => void); + terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + eventHandler = handler; + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + }); + service = new TerminalService(helper.object, terminalManager.object, disposables); + service.onDidCloseTerminal(() => eventFired = true); + // This will create the terminal. + await service.sendText('blah'); + + expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + eventHandler!.bind(service)(); + expect(eventFired).to.be.equal(false, 'Event fired'); + }); + + test('Ensure close event is not fired when terminal is closed', async () => { + let eventFired = false; + let eventHandler: undefined | ((t: VSCodeTerminal) => void); + terminalManager.setup(m => m.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(handler => { + eventHandler = handler; + // tslint:disable-next-line:no-empty + return { dispose: () => { } }; + }); + service = new TerminalService(helper.object, terminalManager.object, disposables); + service.onDidCloseTerminal(() => eventFired = true); + // This will create the terminal. + await service.sendText('blah'); + + expect(eventHandler).not.to.be.an('undefined', 'event handler not initialized'); + eventHandler!.bind(service)(terminal.object); + expect(eventFired).to.be.equal(true, 'Event not fired'); + }); +}); diff --git a/src/test/debugger/envVars.test.ts b/src/test/debugger/envVars.test.ts index da5283a6e1af..5f2b8fed0a2e 100644 --- a/src/test/debugger/envVars.test.ts +++ b/src/test/debugger/envVars.test.ts @@ -188,10 +188,13 @@ suite('Resolving Environment Variables when Debugging', () => { test('Confirm paths get appended correctly when using json variables and launched in debug console', async () => { // Add 3 for the 3 new json env variables - let expectedNumberOfVariables = Object.keys(mockProcess.env).length + 4; + let expectedNumberOfVariables = Object.keys(mockProcess.env).length + 3; if (mockProcess.env['PYTHONUNBUFFERED'] === undefined) { expectedNumberOfVariables += 1; } + if (mockProcess.env['PYTHONPATH'] === undefined) { + expectedNumberOfVariables += 1; + } if (mockProcess.env['PYTHONIOENCODING'] === undefined) { expectedNumberOfVariables += 1; } diff --git a/src/test/format/format.helper.test.ts b/src/test/format/format.helper.test.ts index 8c04be616b33..a77c4eb3b9b7 100644 --- a/src/test/format/format.helper.test.ts +++ b/src/test/format/format.helper.test.ts @@ -1,8 +1,7 @@ import * as assert from 'assert'; -import * as path from 'path'; -import { IFormattingSettings, PythonSettings } from '../../client/common/configSettings'; +import { PythonSettings } from '../../client/common/configSettings'; import { EnumEx } from '../../client/common/enumUtils'; -import { Product } from '../../client/common/types'; +import { IFormattingSettings, Product } from '../../client/common/types'; import { FormatterHelper } from '../../client/formatters/helper'; import { FormatterId } from '../../client/formatters/types'; import { initialize } from '../initialize'; @@ -26,10 +25,6 @@ suite('Formatting - Helper', () => { const info = formatHelper.getExecutionInfo(formatter, []); const names = formatHelper.getSettingsPropertyNames(formatter); const execPath = settings.formatting[names.pathName] as string; - let moduleName: string | undefined; - if (path.basename(execPath) === execPath) { - moduleName = execPath; - } assert.equal(info.execPath, execPath, `Incorrect executable paths for product ${formatHelper.translateToId(formatter)}`); }); @@ -40,7 +35,6 @@ suite('Formatting - Helper', () => { const customArgs = ['1', '2', '3']; [Product.autopep8, Product.yapf].forEach(formatter => { - const info = formatHelper.getExecutionInfo(formatter, []); const names = formatHelper.getSettingsPropertyNames(formatter); const args: string[] = Array.isArray(settings.formatting[names.argsName]) ? settings.formatting[names.argsName] as string[] : []; const expectedArgs = args.concat(customArgs).join(','); diff --git a/src/test/install/pythonInstallation.test.ts b/src/test/install/pythonInstallation.test.ts index 5fff901329a8..f3114b961f8c 100644 --- a/src/test/install/pythonInstallation.test.ts +++ b/src/test/install/pythonInstallation.test.ts @@ -6,9 +6,9 @@ import * as assert from 'assert'; import { Container } from 'inversify'; import * as TypeMoq from 'typemoq'; import { IApplicationShell } from '../../client/common/application/types'; -import { IPythonSettings } from '../../client/common/configSettings'; import { PythonInstaller } from '../../client/common/installer/pythonInstallation'; import { IPlatformService } from '../../client/common/platform/types'; +import { IPythonSettings } from '../../client/common/types'; import { IInterpreterLocatorService } from '../../client/interpreter/contracts'; import { InterpreterType, PythonInterpreter } from '../../client/interpreter/contracts'; import { ServiceContainer } from '../../client/ioc/container'; diff --git a/src/test/linters/lint.helper.test.ts b/src/test/linters/lint.helper.test.ts index c9bbb4cc68cf..93e523f46cb6 100644 --- a/src/test/linters/lint.helper.test.ts +++ b/src/test/linters/lint.helper.test.ts @@ -1,8 +1,7 @@ import * as assert from 'assert'; -import * as path from 'path'; -import { ILintingSettings, PythonSettings } from '../../client/common/configSettings'; +import { PythonSettings } from '../../client/common/configSettings'; import { EnumEx } from '../../client/common/enumUtils'; -import { Product } from '../../client/common/types'; +import { ILintingSettings, Product } from '../../client/common/types'; import { LinterHelper } from '../../client/linters/helper'; import { LinterId } from '../../client/linters/types'; import { initialize } from '../initialize'; @@ -28,10 +27,6 @@ suite('Linting - Helper', () => { const info = linterHelper.getExecutionInfo(linter, []); const names = linterHelper.getSettingsPropertyNames(linter); const execPath = settings.linting[names.pathName] as string; - let moduleName: string | undefined; - if (path.basename(execPath) === execPath && linter !== Product.prospector) { - moduleName = execPath; - } assert.equal(info.execPath, execPath, `Incorrect executable paths for product ${linterHelper.translateToId(linter)}`); }); @@ -43,7 +38,6 @@ suite('Linting - Helper', () => { [Product.flake8, Product.mypy, Product.pep8, Product.pydocstyle, Product.pylama, Product.pylint].forEach(linter => { - const info = linterHelper.getExecutionInfo(linter, []); const names = linterHelper.getSettingsPropertyNames(linter); const args: string[] = Array.isArray(settings.linting[names.argsName]) ? settings.linting[names.argsName] as string[] : []; const expectedArgs = args.concat(customArgs).join(','); diff --git a/src/test/mocks/terminalService.ts b/src/test/mocks/terminalService.ts deleted file mode 100644 index 5935c40a6297..000000000000 --- a/src/test/mocks/terminalService.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { injectable } from 'inversify'; -import { createDeferred, Deferred } from '../../client/common/helpers'; -import { ITerminalService } from '../../client/common/terminal/types'; - -@injectable() -export class MockTerminalService implements ITerminalService { - private deferred: Deferred; - constructor() { - this.deferred = createDeferred(this); - } - public get commandSent(): Promise { - return this.deferred.promise; - } - public sendCommand(command: string, args: string[]): Promise { - return this.deferred.resolve(`${command} ${args.join(' ')}`.trim()); - } -} diff --git a/src/test/serviceRegistry.ts b/src/test/serviceRegistry.ts index 3edaf19d4128..11a4c39a622c 100644 --- a/src/test/serviceRegistry.ts +++ b/src/test/serviceRegistry.ts @@ -7,6 +7,7 @@ import { STANDARD_OUTPUT_CHANNEL } from '../client/common/constants'; import { Logger } from '../client/common/logger'; import { IS_64_BIT, IS_WINDOWS } from '../client/common/platform/constants'; import { PathUtils } from '../client/common/platform/pathUtils'; +import { registerTypes as platformRegisterTypes } from '../client/common/platform/serviceRegistry'; import { BufferDecoder } from '../client/common/process/decoder'; import { ProcessService } from '../client/common/process/proc'; import { PythonExecutionFactory } from '../client/common/process/pythonExecutionFactory'; @@ -79,6 +80,9 @@ export class IocContainer { public registerFormatterTypes() { formattersRegisterTypes(this.serviceManager); } + public registerPlatformTypes() { + platformRegisterTypes(this.serviceManager); + } public registerInterpreterTypes() { interpretersRegisterTypes(this.serviceManager); } diff --git a/src/test/terminals/codeExecution/codeExecutionManager.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.test.ts new file mode 100644 index 000000000000..7e8c81e2eafd --- /dev/null +++ b/src/test/terminals/codeExecution/codeExecutionManager.test.ts @@ -0,0 +1,240 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace + +import { expect } from 'chai'; +import * as TypeMoq from 'typemoq'; +import { Disposable, TextDocument, TextEditor, Uri } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { Commands } from '../../../client/common/constants'; +import { IServiceContainer } from '../../../client/ioc/types'; +import { CodeExecutionManager } from '../../../client/terminals/codeExecution/codeExecutionManager'; +import { ICodeExecutionHelper, ICodeExecutionManager, ICodeExecutionService } from '../../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Code Execution Manager', () => { + let executionManager: ICodeExecutionManager; + let workspace: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let disposables: Disposable[] = []; + let serviceContainer: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + setup(() => { + workspace = TypeMoq.Mock.ofType(); + workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + return { + dispose: () => void 0 + }; + }); + documentManager = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + serviceContainer = TypeMoq.Mock.ofType(); + executionManager = new CodeExecutionManager(commandManager.object, documentManager.object, disposables, serviceContainer.object); + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + + test('Ensure commands are registered', async () => { + executionManager.registerCommands(); + commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_In_Terminal), TypeMoq.It.isAny()), TypeMoq.Times.once()); + commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Terminal), TypeMoq.It.isAny()), TypeMoq.Times.once()); + commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Exec_Selection_In_Django_Shell), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + + test('Ensure executeFileInterTerminal will do nothing if no file is avialble', async () => { + let commandHandler: undefined | (() => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + + await commandHandler!(); + helper.verify(h => h.getFileToExecute(), TypeMoq.Times.once()); + }); + + test('Ensure executeFileInterTerminal will use provided file', async () => { + let commandHandler: undefined | ((file: Uri) => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + + const executionService = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + + const fileToExecute = Uri.file('x'); + await commandHandler!(fileToExecute); + helper.verify(h => h.getFileToExecute(), TypeMoq.Times.never()); + executionService.verify(e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + }); + + test('Ensure executeFileInterTerminal will use active file', async () => { + let commandHandler: undefined | ((file: Uri) => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === Commands.Exec_In_Terminal) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const fileToExecute = Uri.file('x'); + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(h => h.getFileToExecute).returns(() => () => Promise.resolve(fileToExecute)); + const executionService = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object); + + await commandHandler!(fileToExecute); + executionService.verify(e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + }); + + async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { + let commandHandler: undefined | (() => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + const executionService = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionSericeId))).returns(() => executionService.object); + documentManager.setup(d => d.activeTextEditor).returns(() => undefined); + + await commandHandler!(); + executionService.verify(e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + + test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => { + await testExecutionOfSelectionWithoutAnyActiveDocument(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will do nothing if theres no active document', async () => { + await testExecutionOfSelectionWithoutAnyActiveDocument(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); + + async function testExecutionOfSlectionWithoutAnythingSelected(commandId: string, executionServiceId: string) { + let commandHandler: undefined | (() => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve('')); + const executionService = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); + // tslint:disable-next-line:no-any + documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; }); + + await commandHandler!(); + executionService.verify(e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + } + + test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => { + await testExecutionOfSlectionWithoutAnythingSelected(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will do nothing if no text is selected', async () => { + await testExecutionOfSlectionWithoutAnythingSelected(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); + + async function testExecutionOfSelectionIsSentToTerminal(commandId: string, executionServiceId: string) { + let commandHandler: undefined | (() => Promise); + // tslint:disable-next-line:no-any + commandManager.setup(c => c.registerCommand).returns(() => { + // tslint:disable-next-line:no-any + return (command: string, callback: (...args: any[]) => any, _thisArg?: any) => { + if (command === commandId) { + commandHandler = callback; + } + return { dispose: () => void 0 }; + }; + }); + executionManager.registerCommands(); + + expect(commandHandler).not.to.be.an('undefined', 'Command handler not initialized'); + + const textSelected = 'abcd'; + const activeDocumentUri = Uri.file('abc'); + const helper = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); + helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected)); + helper.setup(h => h.normalizeLines).returns(() => () => textSelected); + const executionService = TypeMoq.Mock.ofType(); + serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object); + const document = TypeMoq.Mock.ofType(); + document.setup(d => d.uri).returns(() => activeDocumentUri); + const activeEditor = TypeMoq.Mock.ofType(); + activeEditor.setup(e => e.document).returns(() => document.object); + documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object); + + await commandHandler!(); + executionService.verify(e => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + } + test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => { + await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Terminal, 'standard'); + }); + + test('Ensure executeSelectionInDjangoShell will normalize selected text and send it to the terminal', async () => { + await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Django_Shell, 'djangoShell'); + }); +}); diff --git a/src/test/terminals/codeExecution/djangoShellCodeExect.test.ts b/src/test/terminals/codeExecution/djangoShellCodeExect.test.ts new file mode 100644 index 000000000000..4778ffa55512 --- /dev/null +++ b/src/test/terminals/codeExecution/djangoShellCodeExect.test.ts @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../../client/common/application/types'; +import { IFileSystem, IPlatformService } from '../../../client/common/platform/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { DjangoShellCodeExecutionProvider } from '../../../client/terminals/codeExecution/djangoShellCodeExecution'; +import { ICodeExecutionService } from '../../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Django Shell Code Execution', () => { + let executor: ICodeExecutionService; + let terminalSettings: TypeMoq.IMock; + let terminalService: TypeMoq.IMock; + let workspace: TypeMoq.IMock; + let platform: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let disposables: Disposable[] = []; + setup(() => { + const terminalFactory = TypeMoq.Mock.ofType(); + terminalSettings = TypeMoq.Mock.ofType(); + terminalService = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + workspace = TypeMoq.Mock.ofType(); + workspace.setup(c => c.onDidChangeWorkspaceFolders(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => { + return { + dispose: () => void 0 + }; + }); + platform = TypeMoq.Mock.ofType(); + const documentManager = TypeMoq.Mock.ofType(); + const commandManager = TypeMoq.Mock.ofType(); + const fileSystem = TypeMoq.Mock.ofType(); + executor = new DjangoShellCodeExecutionProvider(terminalFactory.object, configService.object, + workspace.object, documentManager.object, platform.object, commandManager.object, fileSystem.object, disposables); + + terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); + + settings = TypeMoq.Mock.ofType(); + settings.setup(s => s.terminal).returns(() => terminalSettings.object); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + + function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, + terminalArgs: string[], expectedTerminalArgs: string[], resource?: Uri) { + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + const replCommandArgs = (executor as DjangoShellCodeExecutionProvider).getReplCommandArgs(resource); + expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); + expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); + expect(replCommandArgs.args).to.be.deep.equal(expectedTerminalArgs, 'Incorrect arguments'); + } + + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { + const pythonPath = 'c:\\program files\\python\\python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs, expectedTerminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { + const pythonPath = 'usr/bin/python'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure python path is returned as is, on non Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + const expectedTerminalArgs = terminalArgs.concat('manage.py', 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs); + }); + + test('Ensure current workspace folder (containing spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat(`"${path.join(workspaceUri.fsPath, 'manage.py')}"`, 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure current workspace folder (without spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder); + const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py'), 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure default workspace folder (containing spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'program files')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); + const expectedTerminalArgs = terminalArgs.concat(`"${path.join(workspaceUri.fsPath, 'manage.py')}"`, 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + + test('Ensure default workspace folder (without spaces) is used to prefix manage.py', async () => { + const pythonPath = 'python1234'; + const terminalArgs = ['-a', 'b', 'c']; + const workspaceUri = Uri.file(path.join('c', 'usr', 'programfiles')); + const workspaceFolder: WorkspaceFolder = { index: 0, name: 'blah', uri: workspaceUri }; + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder]); + const expectedTerminalArgs = terminalArgs.concat(path.join(workspaceUri.fsPath, 'manage.py'), 'shell'); + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs, expectedTerminalArgs, Uri.file('x')); + }); + +}); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts new file mode 100644 index 000000000000..064ca0d55e72 --- /dev/null +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as TypeMoq from 'typemoq'; +import { Range, Selection, TextDocument, TextEditor, TextLine, Uri } from 'vscode'; +import { IApplicationShell, IDocumentManager } from '../../../client/common/application/types'; +import { PythonLanguage } from '../../../client/common/constants'; +import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; +import { ICodeExecutionHelper } from '../../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Code Execution Helper', () => { + let documentManager: TypeMoq.IMock; + let applicationShell: TypeMoq.IMock; + let helper: ICodeExecutionHelper; + let document: TypeMoq.IMock; + let editor: TypeMoq.IMock; + setup(() => { + documentManager = TypeMoq.Mock.ofType(); + applicationShell = TypeMoq.Mock.ofType(); + helper = new CodeExecutionHelper(documentManager.object, applicationShell.object); + + document = TypeMoq.Mock.ofType(); + editor = TypeMoq.Mock.ofType(); + editor.setup(e => e.document).returns(() => document.object); + }); + + test('Ensure blank lines are removed', async () => { + const code = ['import sys', '', '', '', 'print(sys.executable)', '', 'print("1234")', '', '', 'print(1)', 'print(2)']; + const expectedCode = code.filter(line => line.trim().length > 0).join(EOL); + const normalizedZCode = helper.normalizeLines(code.join(EOL)); + expect(normalizedZCode).to.be.equal(expectedCode); + }); + test('Display message if there\s no active file', async () => { + documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is unsaved', async () => { + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + document.setup(doc => doc.isUntitled).returns(() => true); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Display message if active file is non-python', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.languageId).returns(() => 'html'); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.an('undefined'); + applicationShell.verify(a => a.showErrorMessage(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + }); + + test('Returns file uri', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.languageId).returns(() => PythonLanguage.language); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + documentManager.setup(doc => doc.activeTextEditor).returns(() => editor.object); + + const uri = await helper.getFileToExecute(); + expect(uri).to.be.deep.equal(expectedUri); + }); + + test('Returns current line if nothing is selected', async () => { + const lineContents = 'Line Contents'; + editor.setup(e => e.selection).returns(() => new Selection(3, 0, 3, 0)); + const textLine = TypeMoq.Mock.ofType(); + textLine.setup(t => t.text).returns(() => lineContents); + document.setup(d => d.lineAt(TypeMoq.It.isAny())).returns(() => textLine.object); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal(lineContents); + }); + + test('Returns selected text', async () => { + const lineContents = 'Line Contents'; + editor.setup(e => e.selection).returns(() => new Selection(3, 0, 10, 5)); + const textLine = TypeMoq.Mock.ofType(); + textLine.setup(t => t.text).returns(() => lineContents); + document.setup(d => d.getText(TypeMoq.It.isAny())).returns((r: Range) => `${r.start.line}.${r.start.character}.${r.end.line}.${r.end.character}`); + + const content = await helper.getSelectedTextToExecute(editor.object); + expect(content).to.be.equal('3.0.10.5'); + }); +}); diff --git a/src/test/terminals/codeExecution/terminalCodeExec.test.ts b/src/test/terminals/codeExecution/terminalCodeExec.test.ts new file mode 100644 index 000000000000..0cd5756e9d8c --- /dev/null +++ b/src/test/terminals/codeExecution/terminalCodeExec.test.ts @@ -0,0 +1,243 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// tslint:disable:no-multiline-string no-trailing-whitespace + +import { expect } from 'chai'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Disposable, Uri, WorkspaceFolder } from 'vscode'; +import { IWorkspaceService } from '../../../client/common/application/types'; +import { IPlatformService } from '../../../client/common/platform/types'; +import { ITerminalService, ITerminalServiceFactory } from '../../../client/common/terminal/types'; +import { IConfigurationService, IPythonSettings, ITerminalSettings } from '../../../client/common/types'; +import { TerminalCodeExecutionProvider } from '../../../client/terminals/codeExecution/terminalCodeExecution'; +import { ICodeExecutionService } from '../../../client/terminals/types'; + +// tslint:disable-next-line:max-func-body-length +suite('Terminal - Code Execution', () => { + let executor: ICodeExecutionService; + let terminalSettings: TypeMoq.IMock; + let terminalService: TypeMoq.IMock; + let workspace: TypeMoq.IMock; + let platform: TypeMoq.IMock; + let workspaceFolder: TypeMoq.IMock; + let settings: TypeMoq.IMock; + let disposables: Disposable[] = []; + setup(() => { + const terminalFactory = TypeMoq.Mock.ofType(); + terminalSettings = TypeMoq.Mock.ofType(); + terminalService = TypeMoq.Mock.ofType(); + const configService = TypeMoq.Mock.ofType(); + workspace = TypeMoq.Mock.ofType(); + platform = TypeMoq.Mock.ofType(); + executor = new TerminalCodeExecutionProvider(terminalFactory.object, configService.object, workspace.object, disposables, platform.object); + workspaceFolder = TypeMoq.Mock.ofType(); + + terminalFactory.setup(f => f.getTerminalService(TypeMoq.It.isAny())).returns(() => terminalService.object); + + settings = TypeMoq.Mock.ofType(); + settings.setup(s => s.terminal).returns(() => terminalSettings.object); + configService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => settings.object); + }); + teardown(() => { + disposables.forEach(disposable => { + if (disposable) { + disposable.dispose(); + } + }); + + disposables = []; + }); + test('Ensure we set current directory before executing file', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + + executor.executeFile(file); + + terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd ${path.dirname(file.path)}`)), TypeMoq.Times.once()); + }); + + test('Ensure we set current directory (and quote it when containing spaces) before executing file', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to'))); + + executor.executeFile(file); + + terminalService.verify(t => t.sendText(TypeMoq.It.isValue(`cd "${path.dirname(file.path)}"`)), TypeMoq.Times.once()); + }); + + test('Ensure we do not set current directory before executing file if in the same directory', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => workspaceFolder.object); + workspaceFolder.setup(w => w.uri).returns(() => Uri.file(path.join('c', 'path', 'to', 'file with spaces in path'))); + + executor.executeFile(file); + + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Ensure we do not set current directory before executing file if file is not in a workspace', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + terminalSettings.setup(t => t.executeInFileDir).returns(() => true); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + executor.executeFile(file); + + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + async function testFileExecution(isWindows: boolean, pythonPath: string, terminalArgs: string[], file: Uri) { + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + terminalSettings.setup(t => t.executeInFileDir).returns(() => false); + workspace.setup(w => w.getWorkspaceFolder(TypeMoq.It.isAny())).returns(() => undefined); + + await executor.executeFile(file); + const expectedPythonPath = isWindows ? pythonPath.replace(/\\/g, '/') : pythonPath; + const expectedArgs = terminalArgs.concat(file.fsPath.indexOf(' ') > 0 ? `"${file.fsPath}"` : file.fsPath); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(expectedPythonPath), TypeMoq.It.isValue(expectedArgs)), TypeMoq.Times.once()); + } + + test('Ensure python file execution script is sent to terminal on windows', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + testFileExecution(true, 'python', [], file); + }); + + test('Ensure python file execution script is sent to terminal on windows with fully qualified python path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file with spaces in path', 'one.py')); + testFileExecution(true, 'c:\\program files\\python', [], file); + }); + + test('Ensure python file execution script is not quoted when no spaces in file path', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + testFileExecution(true, 'python', [], file); + }); + + test('Ensure python file execution script supports custom python arguments', async () => { + const file = Uri.file(path.join('c', 'path', 'to', 'file', 'one.py')); + testFileExecution(false, 'python', ['-a', '-b', '-c'], file); + }); + + function testReplCommandArguments(isWindows: boolean, pythonPath: string, expectedPythonPath: string, terminalArgs: string[]) { + platform.setup(p => p.isWindows).returns(() => isWindows); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + const replCommandArgs = (executor as TerminalCodeExecutionProvider).getReplCommandArgs(); + expect(replCommandArgs).not.to.be.an('undefined', 'Command args is undefined'); + expect(replCommandArgs.command).to.be.equal(expectedPythonPath, 'Incorrect python path'); + expect(replCommandArgs.args).to.be.deep.equal(terminalArgs, 'Incorrect arguments'); + } + + test('Ensure fully qualified python path is escaped when building repl args on Windows', async () => { + const pythonPath = 'c:\\program files\\python\\python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, 'c:/program files/python/python.exe', terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'c:/program files/python/python.exe'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, when building repl args on Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(true, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure fully qualified python path is returned as is, on non Windows', async () => { + const pythonPath = 'usr/bin/python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure python path is returned as is, on non Windows', async () => { + const pythonPath = 'python'; + const terminalArgs = ['-a', 'b', 'c']; + + testReplCommandArguments(false, pythonPath, pythonPath, terminalArgs); + }); + + test('Ensure nothing happens when blank text is sent to the terminal', async () => { + await executor.execute(''); + await executor.execute(' '); + // tslint:disable-next-line:no-any + await executor.execute(undefined as any as string); + + terminalService.verify(t => t.sendCommand(TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.never()); + terminalService.verify(t => t.sendText(TypeMoq.It.isAny()), TypeMoq.Times.never()); + }); + + test('Ensure repl is initialized once before sending text to the repl', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.once()); + }); + + test('Ensure repl is re-initialized when temrinal is closed', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + let closeTerminalCallback: undefined | (() => void); + terminalService.setup(t => t.onDidCloseTerminal(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((callback => { + closeTerminalCallback = callback; + return { + // tslint:disable-next-line:no-empty + dispose: () => void 0 + }; + })); + + await executor.execute('cmd1'); + await executor.execute('cmd2'); + await executor.execute('cmd3'); + + expect(closeTerminalCallback).not.to.be.an('undefined', 'Callback not initialized'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.once()); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd4'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.exactly(2)); + + closeTerminalCallback!.call(terminalService.object); + await executor.execute('cmd5'); + terminalService.verify(t => t.sendCommand(TypeMoq.It.isValue(pythonPath), TypeMoq.It.isValue(terminalArgs)), TypeMoq.Times.exactly(3)); + }); + + test('Ensure code is sent to terminal', async () => { + const pythonPath = 'usr/bin/python1234'; + const terminalArgs = ['-a', 'b', 'c']; + platform.setup(p => p.isWindows).returns(() => false); + settings.setup(s => s.pythonPath).returns(() => pythonPath); + terminalSettings.setup(t => t.launchArgs).returns(() => terminalArgs); + + await executor.execute('cmd1'); + terminalService.verify(t => t.sendText('cmd1'), TypeMoq.Times.once()); + + await executor.execute('cmd2'); + terminalService.verify(t => t.sendText('cmd2'), TypeMoq.Times.once()); + }); +}); diff --git a/src/testMultiRootWkspc/multi.code-workspace b/src/testMultiRootWkspc/multi.code-workspace index cacdd6c7539f..efee27610862 100644 --- a/src/testMultiRootWkspc/multi.code-workspace +++ b/src/testMultiRootWkspc/multi.code-workspace @@ -1,48 +1,46 @@ { - "folders": [ - { - "path": "workspace1" - }, - { - "path": "workspace2" - }, - { - "path": "workspace3" - }, - { - "path": "workspace4" - }, - { - "path": "workspace5" - }, - { - "path": "parent\\child" - }, - { - "path": "disableLinters" - }, - { - "path": "../test" - } - ], - "settings": { - "python.linting.flake8Enabled": false, - "python.linting.mypyEnabled": false, - "python.linting.pydocstyleEnabled": false, - "python.linting.pylamaEnabled": false, - "python.linting.pylintEnabled": true, - "python.linting.pep8Enabled": false, - "python.linting.prospectorEnabled": false, - "python.workspaceSymbols.enabled": false, - "python.formatting.formatOnSave": false, - "python.formatting.provider": "yapf", - "python.sortImports.args": [ - "-sp", - "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" - ], - "python.linting.lintOnSave": false, - "python.linting.lintOnTextChange": false, - "python.linting.enabled": true, - "python.pythonPath": "python" - } + "folders": [ + { + "path": "workspace1" + }, + { + "path": "workspace2" + }, + { + "path": "workspace3" + }, + { + "path": "workspace4" + }, + { + "path": "workspace5" + }, + { + "path": "parent\\child" + }, + { + "path": "disableLinters" + }, + { + "path": "../test" + } + ], + "settings": { + "python.linting.flake8Enabled": false, + "python.linting.mypyEnabled": false, + "python.linting.pydocstyleEnabled": false, + "python.linting.pylamaEnabled": false, + "python.linting.pylintEnabled": true, + "python.linting.pep8Enabled": false, + "python.linting.prospectorEnabled": false, + "python.workspaceSymbols.enabled": false, + "python.formatting.provider": "yapf", + "python.sortImports.args": [ + "-sp", + "/Users/donjayamanne/.vscode/extensions/pythonVSCode/src/test/pythonFiles/sorting/withconfig" + ], + "python.linting.lintOnSave": false, + "python.linting.enabled": true, + "python.pythonPath": "python" + } }