diff --git a/src/client/common/application/documentManager.ts b/src/client/common/application/documentManager.ts index afd4577417eb..e4f1fa3dc225 100644 --- a/src/client/common/application/documentManager.ts +++ b/src/client/common/application/documentManager.ts @@ -1,10 +1,10 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -// tslint:disable:no-any +// tslint:disable:no-any unified-signatures import { injectable } from 'inversify'; -import { Event, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, window, workspace } from 'vscode'; +import { Event, TextDocument, TextDocumentShowOptions, TextEditor, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, window, workspace, WorkspaceEdit } from 'vscode'; import { IDocumentManager } from './types'; @injectable() @@ -18,7 +18,7 @@ export class DocumentManager implements IDocumentManager { public get visibleTextEditors(): TextEditor[] { return window.visibleTextEditors; } - public get onDidChangeActiveTextEditor(): Event { + public get onDidChangeActiveTextEditor(): Event { return window.onDidChangeActiveTextEditor; } public get onDidChangeVisibleTextEditors(): Event { @@ -47,4 +47,14 @@ export class DocumentManager implements IDocumentManager { public showTextDocument(uri: any, options?: any, preserveFocus?: any): Thenable { return window.showTextDocument(uri, options, preserveFocus); } + + public openTextDocument(uri: Uri): Thenable; + public openTextDocument(fileName: string): Thenable; + public openTextDocument(options?: { language?: string; content?: string }): Thenable; + public openTextDocument(arg?: any): Thenable { + return workspace.openTextDocument(arg); + } + public applyEdit(edit: WorkspaceEdit): Thenable { + return workspace.applyEdit(edit); + } } diff --git a/src/client/common/application/types.ts b/src/client/common/application/types.ts index 3943d9cb7504..2ec0f333b3b4 100644 --- a/src/client/common/application/types.ts +++ b/src/client/common/application/types.ts @@ -8,8 +8,8 @@ import { CancellationToken, ConfigurationChangeEvent, DebugConfiguration, DebugSession, Disposable, Event, FileSystemWatcher, GlobPattern, InputBoxOptions, MessageItem, MessageOptions, OpenDialogOptions, QuickPickItem, QuickPickOptions, SaveDialogOptions, StatusBarAlignment, StatusBarItem, Terminal, TerminalOptions, TextDocument, TextDocumentShowOptions, TextEditor, - TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, WorkspaceConfiguration, WorkspaceFolder, - WorkspaceFolderPickOptions, WorkspaceFoldersChangeEvent + TextEditorEdit, TextEditorOptionsChangeEvent, TextEditorSelectionChangeEvent, TextEditorViewColumnChangeEvent, Uri, ViewColumn, WorkspaceConfiguration, WorkspaceEdit, + WorkspaceFolder, WorkspaceFolderPickOptions, WorkspaceFoldersChangeEvent } from 'vscode'; export const IApplicationShell = Symbol('IApplicationShell'); @@ -336,7 +336,7 @@ export interface IDocumentManager { * has changed. *Note* that the event also fires when the active editor changes * to `undefined`. */ - readonly onDidChangeActiveTextEditor: Event; + readonly onDidChangeActiveTextEditor: Event; /** * An [event](#Event) which fires when the array of [visible editors](#window.visibleTextEditors) @@ -405,6 +405,55 @@ export interface IDocumentManager { * @return A promise that resolves to an [editor](#TextEditor). */ showTextDocument(uri: Uri, options?: TextDocumentShowOptions): Thenable; + /** + * Opens a document. Will return early if this document is already open. Otherwise + * the document is loaded and the [didOpen](#workspace.onDidOpenTextDocument)-event fires. + * + * The document is denoted by an [uri](#Uri). Depending on the [scheme](#Uri.scheme) the + * following rules apply: + * * `file`-scheme: Open a file on disk, will be rejected if the file does not exist or cannot be loaded. + * * `untitled`-scheme: A new file that should be saved on disk, e.g. `untitled:c:\frodo\new.js`. The language + * will be derived from the file name. + * * For all other schemes the registered text document content [providers](#TextDocumentContentProvider) are consulted. + * + * *Note* that the lifecycle of the returned document is owned by the editor and not by the extension. That means an + * [`onDidClose`](#workspace.onDidCloseTextDocument)-event can occur at any time after opening it. + * + * @param uri Identifies the resource to open. + * @return A promise that resolves to a [document](#TextDocument). + */ + openTextDocument(uri: Uri): Thenable; + + /** + * A short-hand for `openTextDocument(Uri.file(fileName))`. + * + * @see [openTextDocument](#openTextDocument) + * @param fileName A name of a file on disk. + * @return A promise that resolves to a [document](#TextDocument). + */ + openTextDocument(fileName: string): Thenable; + + /** + * Opens an untitled text document. The editor will prompt the user for a file + * path when the document is to be saved. The `options` parameter allows to + * specify the *language* and/or the *content* of the document. + * + * @param options Options to control how the document will be created. + * @return A promise that resolves to a [document](#TextDocument). + */ + openTextDocument(options?: { language?: string; content?: string }): Thenable; + /** + * Make changes to one or many resources as defined by the given + * [workspace edit](#WorkspaceEdit). + * + * When applying a workspace edit, the editor implements an 'all-or-nothing'-strategy, + * that means failure to load one document or make changes to one document will cause + * the edit to be rejected. + * + * @param edit A workspace edit. + * @return A thenable that resolves when the edit could be applied. + */ + applyEdit(edit: WorkspaceEdit): Thenable; } export const IWorkspaceService = Symbol('IWorkspaceService'); diff --git a/src/client/common/editor.ts b/src/client/common/editor.ts index e839a13e6a8b..8f70a2714f48 100644 --- a/src/client/common/editor.ts +++ b/src/client/common/editor.ts @@ -1,10 +1,11 @@ import * as dmp from 'diff-match-patch'; -import * as fs from 'fs'; +import * as fs from 'fs-extra'; +import { injectable } from 'inversify'; import * as md5 from 'md5'; import { EOL } from 'os'; import * as path from 'path'; -import { Position, Range, TextDocument, TextEdit, WorkspaceEdit } from 'vscode'; -import * as vscode from 'vscode'; +import { Position, Range, TextDocument, TextEdit, Uri, WorkspaceEdit } from 'vscode'; +import { IEditorUtils } from './types'; // Code borrowed from goFormat.ts (Go Extension for VS Code) enum EditAction { @@ -16,17 +17,17 @@ enum EditAction { const NEW_LINE_LENGTH = EOL.length; class Patch { - public diffs: dmp.Diff[]; - public start1: number; - public start2: number; - public length1: number; - public length2: number; + public diffs!: dmp.Diff[]; + public start1!: number; + public start2!: number; + public length1!: number; + public length2!: number; } class Edit { public action: EditAction; public start: Position; - public end: Position; + public end!: Position; public text: string; constructor(action: number, start: Position) { @@ -120,7 +121,7 @@ export function getWorkspaceEditsFromPatch(filePatches: string[], workspaceRoot? } const fileSource = fs.readFileSync(fileName).toString('utf8'); - const fileUri = vscode.Uri.file(fileName); + const fileUri = Uri.file(fileName); // Add line feeds and build the text edits patches.forEach(p => { @@ -322,3 +323,50 @@ function patch_fromText(textline): Patch[] { } return patches; } + +@injectable() +export class EditorUtils implements IEditorUtils { + public getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit { + const workspaceEdit = new WorkspaceEdit(); + if (patch.startsWith('---')) { + // Strip the first two lines + patch = patch.substring(patch.indexOf('@@')); + } + if (patch.length === 0) { + return workspaceEdit; + } + // Remove the text added by unified_diff + // # Work around missing newline (http://bugs.python.org/issue2142). + patch = patch.replace(/\\ No newline at end of file[\r\n]/, ''); + + const d = new dmp.diff_match_patch(); + const patches = patch_fromText.call(d, patch); + if (!Array.isArray(patches) || patches.length === 0) { + throw new Error('Unable to parse Patch string'); + } + + // Add line feeds and build the text edits + patches.forEach(p => { + p.diffs.forEach(diff => { + diff[1] += EOL; + }); + getTextEditsInternal(originalContents, p.diffs, p.start1).forEach(edit => { + switch (edit.action) { + case EditAction.Delete: + workspaceEdit.delete(uri, new Range(edit.start, edit.end)); + break; + case EditAction.Insert: + workspaceEdit.insert(uri, edit.start, edit.text); + break; + case EditAction.Replace: + workspaceEdit.replace(uri, new Range(edit.start, edit.end), edit.text); + break; + default: + break; + } + }); + }); + + return workspaceEdit; + } +} diff --git a/src/client/common/platform/fileSystem.ts b/src/client/common/platform/fileSystem.ts index 80cd7fba227a..00cdb21f0615 100644 --- a/src/client/common/platform/fileSystem.ts +++ b/src/client/common/platform/fileSystem.ts @@ -47,6 +47,10 @@ export class FileSystem implements IFileSystem { return fs.readFile(filePath).then(buffer => buffer.toString()); } + public async writeFile(filePath: string, data: {}): Promise { + await fs.writeFile(filePath, data, { encoding: 'utf8' }); + } + public directoryExists(filePath: string): Promise { return this.objectExists(filePath, (stats) => stats.isDirectory()); } @@ -168,8 +172,4 @@ export class FileSystem implements IFileSystem { }); }); } - - public writeFile(filePath: string, data: {}): Promise { - return fs.writeFile(filePath, data); - } } diff --git a/src/client/common/serviceRegistry.ts b/src/client/common/serviceRegistry.ts index 40f44a0a0bc8..871c58deca41 100644 --- a/src/client/common/serviceRegistry.ts +++ b/src/client/common/serviceRegistry.ts @@ -14,6 +14,7 @@ import { } from './application/types'; import { WorkspaceService } from './application/workspace'; import { ConfigurationService } from './configuration/service'; +import { EditorUtils } from './editor'; import { FeatureDeprecationManager } from './featureDeprecationManager'; import { ProductInstaller } from './installer/productInstaller'; import { Logger } from './logger'; @@ -34,9 +35,9 @@ import { } from './terminal/types'; import { IBrowserService, IConfigurationService, ICurrentProcess, - IFeatureDeprecationManager, IInstaller, ILogger, - IPathUtils, IPersistentStateFactory, - IRandom, Is64Bit, IsWindows + IEditorUtils, IFeatureDeprecationManager, IInstaller, + ILogger, IPathUtils, + IPersistentStateFactory, IRandom, Is64Bit, IsWindows } from './types'; import { Random } from './utils'; @@ -60,6 +61,7 @@ export function registerTypes(serviceManager: IServiceManager) { serviceManager.addSingleton(IDebugService, DebugService); serviceManager.addSingleton(IApplicationEnvironment, ApplicationEnvironment); serviceManager.addSingleton(IBrowserService, BrowserService); + serviceManager.addSingleton(IEditorUtils, EditorUtils); serviceManager.addSingleton(ITerminalHelper, TerminalHelper); serviceManager.addSingleton( diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 6231a2442fb1..9c4e3b9b71f0 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -3,7 +3,7 @@ // Licensed under the MIT License. import { Socket } from 'net'; -import { ConfigurationTarget, DiagnosticSeverity, Disposable, ExtensionContext, OutputChannel, Uri } from 'vscode'; +import { ConfigurationTarget, DiagnosticSeverity, Disposable, ExtensionContext, OutputChannel, Uri, WorkspaceEdit } from 'vscode'; import { EnvironmentVariables } from './variables/types'; export const IOutputChannel = Symbol('IOutputChannel'); @@ -12,7 +12,7 @@ export const IDocumentSymbolProvider = Symbol('IDocumentSymbolProvider'); export const IsWindows = Symbol('IS_WINDOWS'); export const Is64Bit = Symbol('Is64Bit'); export const IDisposableRegistry = Symbol('IDiposableRegistry'); -export type IDisposableRegistry = Disposable[]; +export type IDisposableRegistry = { push(disposable: Disposable): void }; export const IMemento = Symbol('IGlobalMemento'); export const GLOBAL_MEMENTO = Symbol('IGlobalMemento'); export const WORKSPACE_MEMENTO = Symbol('IWorkspaceMemento'); @@ -303,3 +303,9 @@ export interface IFeatureDeprecationManager extends Disposable { initialize(): void; registerDeprecation(deprecatedInfo: DeprecatedFeatureInfo): void; } + +export const IEditorUtils = Symbol('IEditorUtils'); +export interface IEditorUtils { + // getTextEditor(uri: Uri): Promise<{ editor: TextEditor; dispose?(): void }>; + getWorkspaceEditsFromPatch(originalContents: string, patch: string, uri: Uri): WorkspaceEdit; +} diff --git a/src/client/extension.ts b/src/client/extension.ts index a69f95bb889f..85ecd92a9fd7 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -51,10 +51,11 @@ import { PythonFormattingEditProvider } from './providers/formatProvider'; import { LinterProvider } from './providers/linterProvider'; import { PythonRenameProvider } from './providers/renameProvider'; import { ReplProvider } from './providers/replProvider'; +import { registerTypes as providersRegisterTypes } from './providers/serviceRegistry'; import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider'; import { TerminalProvider } from './providers/terminalProvider'; +import { ISortImportsEditingProvider } from './providers/types'; import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider'; -import * as sortImports from './sortImports'; import { sendTelemetryEvent } from './telemetry'; import { EDITOR_LOAD } from './telemetry/constants'; import { registerTypes as commonRegisterTerminalTypes } from './terminals/serviceRegistry'; @@ -92,7 +93,8 @@ export async function activate(context: ExtensionContext) { const activationService = serviceContainer.get(IExtensionActivationService); await activationService.activate(); - sortImports.activate(context, standardOutputChannel, serviceManager); + const sortImports = serviceContainer.get(ISortImportsEditingProvider); + sortImports.registerCommands(); serviceManager.get(ICodeExecutionManager).registerCommands(); sendStartupTelemetry(activated, serviceContainer).ignoreErrors(); @@ -190,6 +192,7 @@ function registerServices(context: ExtensionContext, serviceManager: ServiceMana debugConfigurationRegisterTypes(serviceManager); debuggerRegisterTypes(serviceManager); appRegisterTypes(serviceManager); + providersRegisterTypes(serviceManager); } async function sendStartupTelemetry(activatedPromise: Promise, serviceContainer: IServiceContainer) { diff --git a/src/client/providers/importSortProvider.ts b/src/client/providers/importSortProvider.ts index 90733f15bfe1..24506aadb051 100644 --- a/src/client/providers/importSortProvider.ts +++ b/src/client/providers/importSortProvider.ts @@ -1,54 +1,123 @@ -import * as fs from 'fs-extra'; +import { inject, injectable } from 'inversify'; +import { EOL } from 'os'; import * as path from 'path'; -import { TextDocument, TextEdit } from 'vscode'; -import { PythonSettings } from '../common/configSettings'; -import { getTempFileWithDocumentContents, getTextEditsFromPatch } from '../common/editor'; -import { ExecutionResult, IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { CancellationToken, Uri, WorkspaceEdit } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../common/application/types'; +import { Commands, EXTENSION_ROOT_DIR, PYTHON_LANGUAGE, STANDARD_OUTPUT_CHANNEL } from '../common/constants'; +import { noop } from '../common/core.utils'; +import { IFileSystem } from '../common/platform/types'; +import { IProcessServiceFactory, IPythonExecutionFactory } from '../common/process/types'; +import { IConfigurationService, IDisposableRegistry, IEditorUtils, ILogger, IOutputChannel } from '../common/types'; import { IServiceContainer } from '../ioc/types'; import { captureTelemetry } from '../telemetry'; import { FORMAT_SORT_IMPORTS } from '../telemetry/constants'; +import { ISortImportsEditingProvider } from './types'; -// tslint:disable-next-line:completed-docs -export class PythonImportSortProvider { +@injectable() +export class SortImportsEditingProvider implements ISortImportsEditingProvider { private readonly processServiceFactory: IProcessServiceFactory; private readonly pythonExecutionFactory: IPythonExecutionFactory; - constructor(serviceContainer: IServiceContainer) { + private readonly shell: IApplicationShell; + private readonly documentManager: IDocumentManager; + private readonly configurationService: IConfigurationService; + private readonly editorUtils: IEditorUtils; + public constructor(@inject(IServiceContainer) private serviceContainer: IServiceContainer) { + this.shell = serviceContainer.get(IApplicationShell); + this.documentManager = serviceContainer.get(IDocumentManager); + this.configurationService = serviceContainer.get(IConfigurationService); this.pythonExecutionFactory = serviceContainer.get(IPythonExecutionFactory); this.processServiceFactory = serviceContainer.get(IProcessServiceFactory); + this.editorUtils = serviceContainer.get(IEditorUtils); } @captureTelemetry(FORMAT_SORT_IMPORTS) - public async sortImports(extensionDir: string, document: TextDocument): Promise { - if (document.lineCount === 1) { - return []; + public async provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise { + const document = await this.documentManager.openTextDocument(uri); + if (!document) { + return; + } + if (document.lineCount <= 1) { + return; } // isort does have the ability to read from the process input stream and return the formatted code out of the output stream. // However they don't support returning the diff of the formatted text when reading data from the input stream. // Yes getting text formatted that way avoids having to create a temporary file, however the diffing will have // to be done here in node (extension), i.e. extension cpu, i.e. less responsive solution. - const importScript = path.join(extensionDir, 'pythonFiles', 'sortImports.py'); - const tmpFileCreated = document.isDirty; - const filePath = tmpFileCreated ? await getTempFileWithDocumentContents(document) : document.fileName; - const settings = PythonSettings.getInstance(document.uri); + const importScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'sortImports.py'); + const fsService = this.serviceContainer.get(IFileSystem); + const tmpFile = document.isDirty ? await fsService.createTemporaryFile(path.extname(document.uri.fsPath)) : undefined; + if (tmpFile) { + await fsService.writeFile(tmpFile.filePath, document.getText()); + } + const settings = this.configurationService.getSettings(uri); const isort = settings.sortImports.path; + const filePath = tmpFile ? tmpFile.filePath : document.uri.fsPath; const args = [filePath, '--diff'].concat(settings.sortImports.args); - let promise: Promise>; + let diffPatch: string; - if (typeof isort === 'string' && isort.length > 0) { - // Lets just treat this as a standard tool. - const processService = await this.processServiceFactory.create(document.uri); - promise = processService.exec(isort, args, { throwOnStdErr: true }); - } else { - promise = this.pythonExecutionFactory.create({ resource: document.uri }) - .then(executionService => executionService.exec([importScript].concat(args), { throwOnStdErr: true })); + if (token && token.isCancellationRequested) { + return; } - try { - const result = await promise; - return getTextEditsFromPatch(document.getText(), result.stdout); + if (typeof isort === 'string' && isort.length > 0) { + // Lets just treat this as a standard tool. + const processService = await this.processServiceFactory.create(document.uri); + diffPatch = (await processService.exec(isort, args, { throwOnStdErr: true, token })).stdout; + } else { + const processExeService = await this.pythonExecutionFactory.create({ resource: document.uri }); + diffPatch = (await processExeService.exec([importScript].concat(args), { throwOnStdErr: true, token })).stdout; + } + + return this.editorUtils.getWorkspaceEditsFromPatch(document.getText(), diffPatch, document.uri); } finally { - if (tmpFileCreated) { - fs.unlinkSync(filePath); + if (tmpFile) { + tmpFile.dispose(); + } + } + } + + public registerCommands() { + const cmdManager = this.serviceContainer.get(ICommandManager); + const disposable = cmdManager.registerCommand(Commands.Sort_Imports, this.sortImports, this); + this.serviceContainer.get(IDisposableRegistry).push(disposable); + } + public async sortImports(uri?: Uri): Promise { + if (!uri) { + const activeEditor = this.documentManager.activeTextEditor; + if (!activeEditor || activeEditor.document.languageId !== PYTHON_LANGUAGE) { + this.shell.showErrorMessage('Please open a Python file to sort the imports.').then(noop, noop); + return; + } + uri = activeEditor.document.uri; + } + + const document = await this.documentManager.openTextDocument(uri); + if (document.lineCount <= 1) { + return; + } + + // Hack, if the document doesn't contain an empty line at the end, then add it + // Else the library strips off the last line + const lastLine = document.lineAt(document.lineCount - 1); + if (lastLine.text.trim().length > 0) { + const edit = new WorkspaceEdit(); + edit.insert(uri, lastLine.range.end, EOL); + await this.documentManager.applyEdit(edit); + } + + try { + const changes = await this.provideDocumentSortImportsEdits(uri); + if (!changes || changes.entries().length === 0) { + return; } + await this.documentManager.applyEdit(changes); + } catch (error) { + const message = typeof error === 'string' ? error : (error.message ? error.message : error); + const outputChannel = this.serviceContainer.get(IOutputChannel, STANDARD_OUTPUT_CHANNEL); + outputChannel.appendLine(error); + outputChannel.show(); + const logger = this.serviceContainer.get(ILogger); + logger.logError(`Failed to format imports for '${uri.fsPath}'.`, error); + this.shell.showErrorMessage(message).then(noop, noop); } } } diff --git a/src/client/providers/serviceRegistry.ts b/src/client/providers/serviceRegistry.ts new file mode 100644 index 000000000000..7418e0175e51 --- /dev/null +++ b/src/client/providers/serviceRegistry.ts @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { IServiceManager } from '../ioc/types'; +import { SortImportsEditingProvider } from './importSortProvider'; +import { ISortImportsEditingProvider } from './types'; + +export function registerTypes(serviceManager: IServiceManager) { + serviceManager.addSingleton(ISortImportsEditingProvider, SortImportsEditingProvider); +} diff --git a/src/client/providers/types.ts b/src/client/providers/types.ts new file mode 100644 index 000000000000..f2d1bc6eea3a --- /dev/null +++ b/src/client/providers/types.ts @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +import { CancellationToken, Uri, WorkspaceEdit } from 'vscode'; + +export const ISortImportsEditingProvider = Symbol('ISortImportsEditingProvider'); +export interface ISortImportsEditingProvider { + provideDocumentSortImportsEdits(uri: Uri, token?: CancellationToken): Promise; + sortImports(uri?: Uri): Promise; + registerCommands(): void; +} diff --git a/src/client/sortImports.ts b/src/client/sortImports.ts deleted file mode 100644 index b32488ec1ed3..000000000000 --- a/src/client/sortImports.ts +++ /dev/null @@ -1,48 +0,0 @@ -import * as os from 'os'; -import * as vscode from 'vscode'; -import { IServiceContainer } from './ioc/types'; -import * as sortProvider from './providers/importSortProvider'; - -export function activate(context: vscode.ExtensionContext, outChannel: vscode.OutputChannel, serviceContainer: IServiceContainer) { - const rootDir = context.asAbsolutePath('.'); - const disposable = vscode.commands.registerCommand('python.sortImports', () => { - const activeEditor = vscode.window.activeTextEditor; - if (!activeEditor || activeEditor.document.languageId !== 'python') { - vscode.window.showErrorMessage('Please open a Python source file to sort the imports.'); - return Promise.resolve(); - } - if (activeEditor.document.lineCount <= 1) { - return Promise.resolve(); - } - - // Hack, if the document doesn't contain an empty line at the end, then add it - // Else the library strips off the last line - const lastLine = activeEditor.document.lineAt(activeEditor.document.lineCount - 1); - let emptyLineAdded = Promise.resolve(true); - if (lastLine.text.trim().length > 0) { - // tslint:disable-next-line:no-any - emptyLineAdded = new Promise((resolve, reject) => { - activeEditor.edit(builder => { - builder.insert(lastLine.range.end, os.EOL); - }).then(resolve, reject); - }); - } - return emptyLineAdded.then(() => { - return new sortProvider.PythonImportSortProvider(serviceContainer).sortImports(rootDir, activeEditor.document); - }).then(changes => { - if (!changes || changes!.length === 0) { - return; - } - - // tslint:disable-next-line:no-any - return new Promise((resolve, reject) => activeEditor.edit(builder => changes.forEach(change => builder.replace(change.range, change.newText))).then(resolve, reject)); - }).catch(error => { - const message = typeof error === 'string' ? error : (error.message ? error.message : error); - outChannel.appendLine(error); - outChannel.show(); - vscode.window.showErrorMessage(message); - }); - }); - - context.subscriptions.push(disposable); -} diff --git a/src/test/format/extension.sort.test.ts b/src/test/format/extension.sort.test.ts index 884113bdb77b..9e97364cc197 100644 --- a/src/test/format/extension.sort.test.ts +++ b/src/test/format/extension.sort.test.ts @@ -1,9 +1,12 @@ import * as assert from 'assert'; +import { expect } from 'chai'; import * as fs from 'fs'; import { EOL } from 'os'; import * as path from 'path'; import { commands, ConfigurationTarget, Position, Range, Uri, window, workspace } from 'vscode'; -import { PythonImportSortProvider } from '../../client/providers/importSortProvider'; +import { Commands } from '../../client/common/constants'; +import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; +import { ISortImportsEditingProvider } from '../../client/providers/types'; import { updateSetting } from '../common'; import { closeActiveWindows, initialize, initializeTest, IS_MULTI_ROOT_TEST } from '../initialize'; import { UnitTestIocContainer } from '../unittests/serviceRegistry'; @@ -15,12 +18,11 @@ const fileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'before.py') const originalFileToFormatWithConfig = path.join(sortingPath, 'withconfig', 'original.py'); const fileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'before.1.py'); const originalFileToFormatWithConfig1 = path.join(sortingPath, 'withconfig', 'original.1.py'); -const extensionDir = path.join(__dirname, '..', '..', '..'); // tslint:disable-next-line:max-func-body-length -suite('Sorting', () => { +suite('Sortingx', () => { let ioc: UnitTestIocContainer; - let sorter: PythonImportSortProvider; + let sorter: ISortImportsEditingProvider; const configTarget = IS_MULTI_ROOT_TEST ? ConfigurationTarget.WorkspaceFolder : ConfigurationTarget.Workspace; suiteSetup(initialize); suiteTeardown(async () => { @@ -38,7 +40,7 @@ suite('Sorting', () => { fs.writeFileSync(fileToFormatWithConfig1, fs.readFileSync(originalFileToFormatWithConfig1)); await updateSetting('sortImports.args', [], Uri.file(sortingPath), configTarget); await closeActiveWindows(); - sorter = new PythonImportSortProvider(ioc.serviceContainer); + sorter = new SortImportsEditingProvider(ioc.serviceContainer); }); teardown(async () => { ioc.dispose(); @@ -53,7 +55,9 @@ suite('Sorting', () => { test('Without Config', async () => { const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); await window.showTextDocument(textDocument); - const edits = await sorter.sortImports(extensionDir, textDocument); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; assert.equal(edits.filter(value => value.newText === EOL && value.range.isEqual(new Range(2, 0, 2, 0))).length, 1, 'EOL not found'); assert.equal(edits.filter(value => value.newText === '' && value.range.isEqual(new Range(3, 0, 4, 0))).length, 1, '"" not found'); assert.equal(edits.filter(value => value.newText === `from rope.base import libutils${EOL}from rope.refactor.extract import ExtractMethod, ExtractVariable${EOL}from rope.refactor.rename import Rename${EOL}` && value.range.isEqual(new Range(6, 0, 6, 0))).length, 1, 'Text not found'); @@ -64,14 +68,16 @@ suite('Sorting', () => { const textDocument = await workspace.openTextDocument(fileToFormatWithoutConfig); const originalContent = textDocument.getText(); await window.showTextDocument(textDocument); - await commands.executeCommand('python.sortImports'); + await commands.executeCommand(Commands.Sort_Imports); assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); test('With Config', async () => { const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); await window.showTextDocument(textDocument); - const edits = await sorter.sortImports(extensionDir, textDocument); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; const newValue = `from third_party import lib2${EOL}from third_party import lib3${EOL}from third_party import lib4${EOL}from third_party import lib5${EOL}from third_party import lib6${EOL}from third_party import lib7${EOL}from third_party import lib8${EOL}from third_party import lib9${EOL}`; assert.equal(edits.filter(value => value.newText === newValue && value.range.isEqual(new Range(0, 0, 3, 0))).length, 1, 'New Text not found'); }); @@ -80,7 +86,7 @@ suite('Sorting', () => { const textDocument = await workspace.openTextDocument(fileToFormatWithConfig); const originalContent = textDocument.getText(); await window.showTextDocument(textDocument); - await commands.executeCommand('python.sortImports'); + await commands.executeCommand(Commands.Sort_Imports); assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); @@ -91,7 +97,9 @@ suite('Sorting', () => { await editor.edit(builder => { builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); }); - const edits = await sorter.sortImports(extensionDir, textDocument); + const edit = (await sorter.provideDocumentSortImportsEdits(textDocument.uri))!; + expect(edit.entries()).to.be.lengthOf(1); + const edits = edit.entries()[0][1]; assert.notEqual(edits.length, 0, 'No edits'); }); test('With Changes and Config in Args (via Command)', async () => { @@ -102,7 +110,7 @@ suite('Sorting', () => { builder.insert(new Position(0, 0), `from third_party import lib0${EOL}`); }); const originalContent = textDocument.getText(); - await commands.executeCommand('python.sortImports'); + await commands.executeCommand(Commands.Sort_Imports); assert.notEqual(originalContent, textDocument.getText(), 'Contents have not changed'); }); }); diff --git a/src/test/providers/importSortProvider.unit.test.ts b/src/test/providers/importSortProvider.unit.test.ts new file mode 100644 index 000000000000..961c5b3aafff --- /dev/null +++ b/src/test/providers/importSortProvider.unit.test.ts @@ -0,0 +1,368 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +'use strict'; + +// tslint:disable:no-any max-func-body-length + +import { expect } from 'chai'; +import { EOL } from 'os'; +import * as path from 'path'; +import * as TypeMoq from 'typemoq'; +import { Range, TextDocument, TextEditor, TextLine, Uri, WorkspaceEdit } from 'vscode'; +import { IApplicationShell, ICommandManager, IDocumentManager } from '../../client/common/application/types'; +import { Commands, EXTENSION_ROOT_DIR } from '../../client/common/constants'; +import { noop } from '../../client/common/core.utils'; +import { IFileSystem, TemporaryFile } from '../../client/common/platform/types'; +import { ProcessService } from '../../client/common/process/proc'; +import { IProcessServiceFactory, IPythonExecutionFactory, IPythonExecutionService } from '../../client/common/process/types'; +import { IConfigurationService, IDisposableRegistry, IEditorUtils, IPythonSettings, ISortImportSettings } from '../../client/common/types'; +import { IServiceContainer } from '../../client/ioc/types'; +import { SortImportsEditingProvider } from '../../client/providers/importSortProvider'; +import { ISortImportsEditingProvider } from '../../client/providers/types'; + +suite('Import Sort Provider', () => { + let serviceContainer: TypeMoq.IMock; + let shell: TypeMoq.IMock; + let documentManager: TypeMoq.IMock; + let configurationService: TypeMoq.IMock; + let pythonExecFactory: TypeMoq.IMock; + let processServiceFactory: TypeMoq.IMock; + let editorUtils: TypeMoq.IMock; + let commandManager: TypeMoq.IMock; + let pythonSettings: TypeMoq.IMock; + let sortProvider: ISortImportsEditingProvider; + let fs: TypeMoq.IMock; + setup(() => { + serviceContainer = TypeMoq.Mock.ofType(); + commandManager = TypeMoq.Mock.ofType(); + fs = TypeMoq.Mock.ofType(); + documentManager = TypeMoq.Mock.ofType(); + shell = TypeMoq.Mock.ofType(); + configurationService = TypeMoq.Mock.ofType(); + pythonExecFactory = TypeMoq.Mock.ofType(); + processServiceFactory = TypeMoq.Mock.ofType(); + pythonSettings = TypeMoq.Mock.ofType(); + editorUtils = TypeMoq.Mock.ofType(); + fs = TypeMoq.Mock.ofType(); + serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object); + serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object); + serviceContainer.setup(c => c.get(IApplicationShell)).returns(() => shell.object); + serviceContainer.setup(c => c.get(IConfigurationService)).returns(() => configurationService.object); + serviceContainer.setup(c => c.get(IPythonExecutionFactory)).returns(() => pythonExecFactory.object); + serviceContainer.setup(c => c.get(IProcessServiceFactory)).returns(() => processServiceFactory.object); + serviceContainer.setup(c => c.get(IEditorUtils)).returns(() => editorUtils.object); + serviceContainer.setup(c => c.get(IDisposableRegistry)).returns(() => []); + serviceContainer.setup(c => c.get(IFileSystem)).returns(() => fs.object); + configurationService.setup(c => c.getSettings(TypeMoq.It.isAny())).returns(() => pythonSettings.object); + + sortProvider = new SortImportsEditingProvider(serviceContainer.object); + }); + + test('Ensure command is registered', () => { + commandManager + .setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Sort_Imports), TypeMoq.It.isAny(), TypeMoq.It.isValue(sortProvider))) + .verifiable(TypeMoq.Times.once()); + + sortProvider.registerCommands(); + commandManager.verifyAll(); + }); + test('Ensure message is displayed when no doc is opened and uri isn\'t provided', async () => { + documentManager + .setup(d => d.activeTextEditor).returns(() => undefined) + .verifiable(TypeMoq.Times.once()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isValue('Please open a Python file to sort the imports.'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await sortProvider.sortImports(); + + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure message is displayed when uri isn\'t provided and current doc is non-python', async () => { + const mockEditor = TypeMoq.Mock.ofType(); + const mockDoc = TypeMoq.Mock.ofType(); + mockDoc.setup(d => d.languageId) + .returns(() => 'xyz') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockEditor.setup(d => d.document) + .returns(() => mockDoc.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + + documentManager + .setup(d => d.activeTextEditor) + .returns(() => mockEditor.object) + .verifiable(TypeMoq.Times.once()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isValue('Please open a Python file to sort the imports.'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + await sortProvider.sortImports(); + + mockEditor.verifyAll(); + mockDoc.verifyAll(); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure document is opened', async () => { + const uri = Uri.file('TestDoc'); + + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.activeTextEditor) + .verifiable(TypeMoq.Times.never()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + await sortProvider.sortImports(uri).catch(noop); + + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there is only one line', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType(); + // tslint:disable-next-line:no-any + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 1) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.sortImports(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there are no lines', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType(); + // tslint:disable-next-line:no-any + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 0) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.sortImports(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure empty line is added when line does not end with an empty line', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + + const lastLine = TypeMoq.Mock.ofType(); + let editApplied: WorkspaceEdit | undefined; + lastLine.setup(l => l.text) + .returns(() => '1234') + .verifiable(TypeMoq.Times.atLeastOnce()); + lastLine.setup(l => l.range) + .returns(() => new Range(1, 0, 10, 1)) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.lineAt(TypeMoq.It.isValue(9))) + .returns(() => lastLine.object) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.applyEdit(TypeMoq.It.isAny())) + .callback(e => editApplied = e) + .returns(() => Promise.resolve(true)) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + + sortProvider.provideDocumentSortImportsEdits = () => Promise.resolve(undefined); + await sortProvider.sortImports(uri); + + expect(editApplied).not.to.be.equal(undefined, 'Applied edit is undefined'); + expect(editApplied!.entries()).to.be.lengthOf(1); + expect(editApplied!.entries()[0][1]).to.be.lengthOf(1); + expect(editApplied!.entries()[0][1][0].newText).to.be.equal(EOL); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there is only one line (when using provider method)', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 1) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure no edits are provided when there are no lines (when using provider method)', async () => { + const uri = Uri.file('TestDoc'); + const mockDoc = TypeMoq.Mock.ofType(); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 0) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + shell + .setup(s => s.showErrorMessage(TypeMoq.It.isAny())) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.never()); + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(undefined, 'not undefined'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure temporary file is created for sorting when document is dirty', async () => { + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType(); + let tmpFileDisposed = false; + const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; + const processService = TypeMoq.Mock.ofType(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.getText(TypeMoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.uri) + .returns(() => uri) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup(f => f.createTemporaryFile(TypeMoq.It.isValue('.py'))) + .returns(() => Promise.resolve(tmpFile)) + .verifiable(TypeMoq.Times.once()); + fs.setup(f => f.writeFile(TypeMoq.It.isValue(tmpFile.filePath), TypeMoq.It.isValue('Hello'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonSettings.setup(s => s.sortImports) + .returns(() => { return { path: 'CUSTOM_ISORT', args: ['1', '2'] } as any as ISortImportSettings; }) + .verifiable(TypeMoq.Times.once()); + processServiceFactory.setup(p => p.create(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(processService.object)) + .verifiable(TypeMoq.Times.once()); + + const expectedArgs = [tmpFile.filePath, '--diff', '1', '2']; + processService + .setup(p => p.exec(TypeMoq.It.isValue('CUSTOM_ISORT'), TypeMoq.It.isValue(expectedArgs), TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined }))) + .returns(() => Promise.resolve({ stdout: 'DIFF' })) + .verifiable(TypeMoq.Times.once()); + const expectedEdit = new WorkspaceEdit(); + editorUtils + .setup(e => e.getWorkspaceEditsFromPatch(TypeMoq.It.isValue('Hello'), TypeMoq.It.isValue('DIFF'), TypeMoq.It.isValue(uri))) + .returns(() => expectedEdit) + .verifiable(TypeMoq.Times.once()); + + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(expectedEdit); + expect(tmpFileDisposed).to.be.equal(true, 'Temporary file not disposed'); + shell.verifyAll(); + documentManager.verifyAll(); + }); + test('Ensure temporary file is created for sorting when document is dirty (with custom isort path)', async () => { + const uri = Uri.file('something.py'); + const mockDoc = TypeMoq.Mock.ofType(); + let tmpFileDisposed = false; + const tmpFile: TemporaryFile = { filePath: 'TmpFile', dispose: () => tmpFileDisposed = true }; + const processService = TypeMoq.Mock.ofType(); + processService.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup((d: any) => d.then).returns(() => undefined); + mockDoc.setup(d => d.lineCount) + .returns(() => 10) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.getText(TypeMoq.It.isAny())) + .returns(() => 'Hello') + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.isDirty) + .returns(() => true) + .verifiable(TypeMoq.Times.atLeastOnce()); + mockDoc.setup(d => d.uri) + .returns(() => uri) + .verifiable(TypeMoq.Times.atLeastOnce()); + documentManager + .setup(d => d.openTextDocument(TypeMoq.It.isValue(uri))) + .returns(() => Promise.resolve(mockDoc.object)) + .verifiable(TypeMoq.Times.atLeastOnce()); + fs.setup(f => f.createTemporaryFile(TypeMoq.It.isValue('.py'))) + .returns(() => Promise.resolve(tmpFile)) + .verifiable(TypeMoq.Times.once()); + fs.setup(f => f.writeFile(TypeMoq.It.isValue(tmpFile.filePath), TypeMoq.It.isValue('Hello'))) + .returns(() => Promise.resolve(undefined)) + .verifiable(TypeMoq.Times.once()); + pythonSettings.setup(s => s.sortImports) + .returns(() => { return { args: ['1', '2'] } as any as ISortImportSettings; }) + .verifiable(TypeMoq.Times.once()); + + const processExeService = TypeMoq.Mock.ofType(); + processExeService.setup((p: any) => p.then).returns(() => undefined); + pythonExecFactory.setup(p => p.create(TypeMoq.It.isValue({ resource: uri }))) + .returns(() => Promise.resolve(processExeService.object)) + .verifiable(TypeMoq.Times.once()); + const importScript = path.join(EXTENSION_ROOT_DIR, 'pythonFiles', 'sortImports.py'); + const expectedArgs = [importScript, tmpFile.filePath, '--diff', '1', '2']; + processExeService + .setup(p => p.exec(TypeMoq.It.isValue(expectedArgs), TypeMoq.It.isValue({ throwOnStdErr: true, token: undefined }))) + .returns(() => Promise.resolve({ stdout: 'DIFF' })) + .verifiable(TypeMoq.Times.once()); + const expectedEdit = new WorkspaceEdit(); + editorUtils + .setup(e => e.getWorkspaceEditsFromPatch(TypeMoq.It.isValue('Hello'), TypeMoq.It.isValue('DIFF'), TypeMoq.It.isValue(uri))) + .returns(() => expectedEdit) + .verifiable(TypeMoq.Times.once()); + + const edit = await sortProvider.provideDocumentSortImportsEdits(uri); + + expect(edit).to.be.equal(expectedEdit); + expect(tmpFileDisposed).to.be.equal(true, 'Temporary file not disposed'); + shell.verifyAll(); + documentManager.verifyAll(); + }); +}); diff --git a/src/test/vscode-mock.ts b/src/test/vscode-mock.ts index a1a35ed406c2..a928bcd33a09 100644 --- a/src/test/vscode-mock.ts +++ b/src/test/vscode-mock.ts @@ -65,6 +65,8 @@ mockedVSCode.ConfigurationTarget = vscodeMocks.vscMockExtHostedTypes.Configurati mockedVSCode.StatusBarAlignment = vscodeMocks.vscMockExtHostedTypes.StatusBarAlignment; mockedVSCode.SignatureHelp = vscodeMocks.vscMockExtHostedTypes.SignatureHelp; mockedVSCode.DocumentLink = vscodeMocks.vscMockExtHostedTypes.DocumentLink; +mockedVSCode.TextEdit = vscodeMocks.vscMockExtHostedTypes.TextEdit; +mockedVSCode.WorkspaceEdit = vscodeMocks.vscMockExtHostedTypes.WorkspaceEdit; // This API is used in src/client/telemetry/telemetry.ts const extensions = TypeMoq.Mock.ofType();