diff --git a/news/1 Enhancements/1207.md b/news/1 Enhancements/1207.md new file mode 100644 index 000000000000..f668cdf00aae --- /dev/null +++ b/news/1 Enhancements/1207.md @@ -0,0 +1 @@ +Remove empty spaces from the selected text of the active editor when executing in a terminal. diff --git a/news/1 Enhancements/1316.md b/news/1 Enhancements/1316.md new file mode 100644 index 000000000000..ca6c6a80eb77 --- /dev/null +++ b/news/1 Enhancements/1316.md @@ -0,0 +1 @@ +Save the python file before running it in the terminal using the command/menu `Run Python File in Terminal`. diff --git a/news/1 Enhancements/1349.md b/news/1 Enhancements/1349.md new file mode 100644 index 000000000000..7bcbcd8f1a88 --- /dev/null +++ b/news/1 Enhancements/1349.md @@ -0,0 +1 @@ +Add `Ctrl+Enter` keyboard shortcut for `Run Selection/Line in Python Terminal`. diff --git a/news/2 Fixes/259.md b/news/2 Fixes/259.md new file mode 100644 index 000000000000..8579bc07f4cc --- /dev/null +++ b/news/2 Fixes/259.md @@ -0,0 +1 @@ +Add blank lines to seprate blocks of indented code (function defs, classes, and the like) to ensure the code can be run within a Python interactive prompt. diff --git a/package.json b/package.json index 906ad9e94b14..a6a9a8f4a1e5 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,12 @@ "path": "./snippets/python.json" } ], + "keybindings":[ + { + "command": "python.execSelectionInTerminal", + "key": "ctrl+enter" + } + ], "commands": [ { "command": "python.sortImports", diff --git a/src/client/common/configSettings.ts b/src/client/common/configSettings.ts index 62d75c0ec0ba..131228a28677 100644 --- a/src/client/common/configSettings.ts +++ b/src/client/common/configSettings.ts @@ -37,7 +37,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings { public formatting?: IFormattingSettings; public autoComplete?: IAutoCompleteSettings; public unitTest?: IUnitTestSettings; - public terminal?: ITerminalSettings; + public terminal!: ITerminalSettings; public sortImports?: ISortImportSettings; public workspaceSymbols?: IWorkspaceSymbolSettings; public disableInstallationChecks = false; diff --git a/src/client/common/types.ts b/src/client/common/types.ts index 5e16a4557786..240b6f5809e3 100644 --- a/src/client/common/types.ts +++ b/src/client/common/types.ts @@ -106,7 +106,7 @@ export interface IPythonSettings { readonly formatting?: IFormattingSettings; readonly unitTest?: IUnitTestSettings; readonly autoComplete?: IAutoCompleteSettings; - readonly terminal?: ITerminalSettings; + readonly terminal: ITerminalSettings; readonly sortImports?: ISortImportSettings; readonly workspaceSymbols?: IWorkspaceSymbolSettings; readonly envFile: string; diff --git a/src/client/terminals/codeExecution/codeExecutionManager.ts b/src/client/terminals/codeExecution/codeExecutionManager.ts index 5a6159a50aae..04ccf5e61c19 100644 --- a/src/client/terminals/codeExecution/codeExecutionManager.ts +++ b/src/client/terminals/codeExecution/codeExecutionManager.ts @@ -35,6 +35,7 @@ export class CodeExecutionManager implements ICodeExecutionManager { if (!fileToExecute) { return; } + await codeExecutionHelper.saveFileIfDirty(fileToExecute); const executionService = this.serviceContainer.get(ICodeExecutionService, 'standard'); await executionService.executeFile(fileToExecute); } @@ -59,11 +60,11 @@ export class CodeExecutionManager implements ICodeExecutionManager { } const codeExecutionHelper = this.serviceContainer.get(ICodeExecutionHelper); const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!); - const normalizedCode = codeExecutionHelper.normalizeLines(codeToExecute!); + const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!); if (!normalizedCode || normalizedCode.trim().length === 0) { return; } - await executionService.execute(codeToExecute!, activeEditor!.document.uri); + await executionService.execute(normalizedCode, activeEditor!.document.uri); } } diff --git a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts index 4fc230b6e21a..3066ec27fb71 100644 --- a/src/client/terminals/codeExecution/djangoShellCodeExecution.ts +++ b/src/client/terminals/codeExecution/djangoShellCodeExecution.ts @@ -32,7 +32,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi 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 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 : ''; diff --git a/src/client/terminals/codeExecution/helper.ts b/src/client/terminals/codeExecution/helper.ts index efa468dcbc95..f5c2e0baeb27 100644 --- a/src/client/terminals/codeExecution/helper.ts +++ b/src/client/terminals/codeExecution/helper.ts @@ -2,23 +2,34 @@ // 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 { IServiceContainer } from '../../ioc/types'; import { ICodeExecutionHelper } from '../types'; @injectable() export class CodeExecutionHelper implements ICodeExecutionHelper { - constructor( @inject(IDocumentManager) private documentManager: IDocumentManager, - @inject(IApplicationShell) private applicationShell: IApplicationShell) { - + private readonly documentManager: IDocumentManager; + private readonly applicationShell: IApplicationShell; + constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) { + this.documentManager = serviceContainer.get(IDocumentManager); + this.applicationShell = serviceContainer.get(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 normalizeLines(code: string, resource?: Uri): Promise { + try { + if (code.trim().length === 0) { + return ''; + } + const regex = /(\n)([ \t]*\r?\n)([ \t]+\S+)/gm; + return code.replace(regex, (_, a, b, c) => { + return `${a}${c}`; + }); + } catch (ex) { + console.error(ex, 'Python: Failed to normalize code for execution in terminal'); + return code; + } } public async getFileToExecute(): Promise { @@ -35,6 +46,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { this.applicationShell.showErrorMessage('The active file is not a Python source file'); return; } + if (activeEditor.document.isDirty) { + await activeEditor.document.save(); + } return activeEditor.document.uri; } @@ -53,4 +67,10 @@ export class CodeExecutionHelper implements ICodeExecutionHelper { } return code; } + public async saveFileIfDirty(file: Uri): Promise { + const docs = this.documentManager.textDocuments.filter(d => d.uri.path === file.path); + if (docs.length === 1 && docs[0].isDirty) { + await docs[0].save(); + } + } } diff --git a/src/client/terminals/codeExecution/terminalCodeExecution.ts b/src/client/terminals/codeExecution/terminalCodeExecution.ts index d6ab0601c885..7e541ee12126 100644 --- a/src/client/terminals/codeExecution/terminalCodeExecution.ts +++ b/src/client/terminals/codeExecution/terminalCodeExecution.ts @@ -10,16 +10,15 @@ import { IWorkspaceService } from '../../common/application/types'; import '../../common/extensions'; import { IPlatformService } from '../../common/platform/types'; import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types'; -import { IConfigurationService } from '../../common/types'; -import { IDisposableRegistry } from '../../common/types'; +import { IConfigurationService, IDisposableRegistry } from '../../common/types'; import { ICodeExecutionService } from '../../terminals/types'; @injectable() export class TerminalCodeExecutionProvider implements ICodeExecutionService { - protected terminalTitle: string; - private _terminalService: ITerminalService; + protected terminalTitle!: string; + private _terminalService!: ITerminalService; private replActive?: Promise; - constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory, + 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[], @@ -60,7 +59,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService { await this.replActive; } - public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } { + 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(); diff --git a/src/client/terminals/types.ts b/src/client/terminals/types.ts index 67c50d4b887b..cd9e1bc96a9b 100644 --- a/src/client/terminals/types.ts +++ b/src/client/terminals/types.ts @@ -14,8 +14,9 @@ export interface ICodeExecutionService { export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper'); export interface ICodeExecutionHelper { - normalizeLines(code: string): string; + normalizeLines(code: string): Promise; getFileToExecute(): Promise; + saveFileIfDirty(file: Uri): Promise; getSelectedTextToExecute(textEditor: TextEditor): Promise; } diff --git a/src/test/pythonFiles/terminalExec/sample1_normalized.py b/src/test/pythonFiles/terminalExec/sample1_normalized.py new file mode 100644 index 000000000000..0896de65d22f --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_normalized.py @@ -0,0 +1,22 @@ +# Sample block 1 +def square(x): + return x**2 + +print('hello') +# Sample block 2 +a = 2 +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') + +# Sample block 3 +for i in range(5): + print(i) + print(i) + print(i) + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample1_raw.py b/src/test/pythonFiles/terminalExec/sample1_raw.py new file mode 100644 index 000000000000..fe050c7af289 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample1_raw.py @@ -0,0 +1,24 @@ +# Sample block 1 +def square(x): + return x**2 + +print('hello') +# Sample block 2 +a = 2 +if a < 2: + print('less than 2') +else: + print('more than 2') + +print('hello') + +# Sample block 3 +for i in range(5): + print(i) + + print(i) + print(i) + + print(i) + +print('complete') diff --git a/src/test/pythonFiles/terminalExec/sample2_normalized.py b/src/test/pythonFiles/terminalExec/sample2_normalized.py new file mode 100644 index 000000000000..a333d4e0daae --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_normalized.py @@ -0,0 +1,7 @@ +def add(x, y): + """Adds x to y""" + # Some comment + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample2_raw.py b/src/test/pythonFiles/terminalExec/sample2_raw.py new file mode 100644 index 000000000000..6ab7e67637f4 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample2_raw.py @@ -0,0 +1,8 @@ +def add(x, y): + """Adds x to y""" + # Some comment + + return x + y + +v = add(1, 7) +print(v) diff --git a/src/test/pythonFiles/terminalExec/sample3_normalized.py b/src/test/pythonFiles/terminalExec/sample3_normalized.py new file mode 100644 index 000000000000..e4f028b0b778 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_normalized.py @@ -0,0 +1,4 @@ +if True: + print(1) + print(2) +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample3_raw.py b/src/test/pythonFiles/terminalExec/sample3_raw.py new file mode 100644 index 000000000000..5865e6d2cbde --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample3_raw.py @@ -0,0 +1,5 @@ +if True: + print(1) + + print(2) +print(3) diff --git a/src/test/pythonFiles/terminalExec/sample4_normalized.py b/src/test/pythonFiles/terminalExec/sample4_normalized.py new file mode 100644 index 000000000000..2c49d10253ff --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_normalized.py @@ -0,0 +1,6 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample4_raw.py b/src/test/pythonFiles/terminalExec/sample4_raw.py new file mode 100644 index 000000000000..fbf0d68fe5f8 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample4_raw.py @@ -0,0 +1,7 @@ +class pc(object): + def __init__(self, pcname, model): + self.pcname = pcname + self.model = model + + def print_name(self): + print('Workstation name is', self.pcname, 'model is', self.model) diff --git a/src/test/pythonFiles/terminalExec/sample5_normalized.py b/src/test/pythonFiles/terminalExec/sample5_normalized.py new file mode 100644 index 000000000000..822d51bd15d9 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_normalized.py @@ -0,0 +1,8 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + print('b2') + for k in range(2): + print('c') + print('done with first loop') diff --git a/src/test/pythonFiles/terminalExec/sample5_raw.py b/src/test/pythonFiles/terminalExec/sample5_raw.py new file mode 100644 index 000000000000..19caa9cf26a6 --- /dev/null +++ b/src/test/pythonFiles/terminalExec/sample5_raw.py @@ -0,0 +1,11 @@ +for i in range(10): + print('a') + for j in range(5): + print('b') + + print('b2') + + for k in range(2): + print('c') + + print('done with first loop') diff --git a/src/test/terminals/codeExecution/codeExecutionManager.test.ts b/src/test/terminals/codeExecution/codeExecutionManager.test.ts index 8dec4d7f5025..05c395a1e571 100644 --- a/src/test/terminals/codeExecution/codeExecutionManager.test.ts +++ b/src/test/terminals/codeExecution/codeExecutionManager.test.ts @@ -69,7 +69,7 @@ suite('Terminal - Code Execution Manager', () => { serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); await commandHandler!(); - helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.once()); + helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use provided file', async () => { @@ -96,8 +96,8 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); await commandHandler!(fileToExecute); - helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.never()); - executionService.verify(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.never()); + executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); test('Ensure executeFileInterTerminal will use active file', async () => { @@ -119,12 +119,12 @@ suite('Terminal - Code Execution Manager', () => { const fileToExecute = Uri.file('x'); const helper = TypeMoq.Mock.ofType(); serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object); - helper.setup(async h => await h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute)); + helper.setup(async 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(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); + executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once()); }); async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) { @@ -150,7 +150,7 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => undefined); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => { @@ -186,7 +186,7 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; }); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); + executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never()); } test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => { @@ -218,7 +218,7 @@ suite('Terminal - Code Execution Manager', () => { 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); + helper.setup(h => h.normalizeLines).returns(() => () => Promise.resolve(textSelected)).verifiable(TypeMoq.Times.once()); 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(); @@ -228,7 +228,8 @@ suite('Terminal - Code Execution Manager', () => { documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object); await commandHandler!(); - executionService.verify(async e => await e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + executionService.verify(async e => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once()); + helper.verifyAll(); } test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => { await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Terminal, 'standard'); diff --git a/src/test/terminals/codeExecution/helper.test.ts b/src/test/terminals/codeExecution/helper.test.ts index 064ca0d55e72..a9344344d548 100644 --- a/src/test/terminals/codeExecution/helper.test.ts +++ b/src/test/terminals/codeExecution/helper.test.ts @@ -4,14 +4,19 @@ // tslint:disable:no-multiline-string no-trailing-whitespace import { expect } from 'chai'; +import * as fs from 'fs-extra'; import { EOL } from 'os'; +import * as path from 'path'; 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 { EXTENSION_ROOT_DIR, PythonLanguage } from '../../../client/common/constants'; +import { IServiceContainer } from '../../../client/ioc/types'; import { CodeExecutionHelper } from '../../../client/terminals/codeExecution/helper'; import { ICodeExecutionHelper } from '../../../client/terminals/types'; +const TEST_FILES_PATH = path.join(EXTENSION_ROOT_DIR, 'src', 'test', 'pythonFiles', 'terminalExec'); + // tslint:disable-next-line:max-func-body-length suite('Terminal - Code Execution Helper', () => { let documentManager: TypeMoq.IMock; @@ -20,20 +25,43 @@ suite('Terminal - Code Execution Helper', () => { let document: TypeMoq.IMock; let editor: TypeMoq.IMock; setup(() => { + const serviceContainer = TypeMoq.Mock.ofType(); documentManager = TypeMoq.Mock.ofType(); applicationShell = TypeMoq.Mock.ofType(); - helper = new CodeExecutionHelper(documentManager.object, applicationShell.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IDocumentManager), TypeMoq.It.isAny())).returns(() => documentManager.object); + serviceContainer.setup(c => c.get(TypeMoq.It.isValue(IApplicationShell), TypeMoq.It.isAny())).returns(() => applicationShell.object); + helper = new CodeExecutionHelper(serviceContainer.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); + async function ensureBlankLinesAreRemoved(source: string, expectedSource: string) { + const normalizedZCode = await helper.normalizeLines(source); + expect(normalizedZCode).to.be.equal(expectedSource); + } + test('Ensure blank lines are NOT removed when code is not indented (simple)', async () => { + const code = ['import sys', '', 'print(sys.executable)', '', 'print("1234")', '', 'print(1)', 'print(2)']; + const expectedCode = code.join(EOL); + await ensureBlankLinesAreRemoved(code.join(EOL), expectedCode); + }); + ['sample1', 'sample2', 'sample3', 'sample4', 'sample5'].forEach(fileName => { + test(`Ensure blank lines are removed (${fileName})`, async () => { + const code = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_raw.py`), 'utf8'); + const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_normalized.py`), 'utf8'); + await ensureBlankLinesAreRemoved(code, expectedCode); + }); + // test(`Ensure blank lines are removed, including leading empty lines (${fileName})`, async () => { + // const code = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_raw.py`), 'utf8'); + // const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, `${fileName}_normalized.py`), 'utf8'); + // await ensureBlankLinesAreRemoved(['', '', ''].join(EOL) + EOL + code, expectedCode); + // }); + }); + test('Ensure blank lines are removed (sample2)', async () => { + const code = await fs.readFile(path.join(TEST_FILES_PATH, 'sample2_raw.py'), 'utf8'); + const expectedCode = await fs.readFile(path.join(TEST_FILES_PATH, 'sample2_normalized.py'), 'utf8'); + await ensureBlankLinesAreRemoved(code, expectedCode); }); test('Display message if there\s no active file', async () => { documentManager.setup(doc => doc.activeTextEditor).returns(() => undefined); @@ -73,6 +101,45 @@ suite('Terminal - Code Execution Helper', () => { expect(uri).to.be.deep.equal(expectedUri); }); + test('Returns file uri even if saving fails', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + document.setup(doc => doc.languageId).returns(() => PythonLanguage.language); + document.setup(doc => doc.save()).returns(() => Promise.resolve(false)); + 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('Dirty files are saved', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + 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); + document.verify(doc => doc.save(), TypeMoq.Times.once()); + }); + + test('Non-Dirty files are not-saved', async () => { + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).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); + document.verify(doc => doc.save(), TypeMoq.Times.never()); + }); + 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)); @@ -94,4 +161,37 @@ suite('Terminal - Code Execution Helper', () => { const content = await helper.getSelectedTextToExecute(editor.object); expect(content).to.be.equal('3.0.10.5'); }); + + test('saveFileIfDirty will not fail if file is not opened', async () => { + documentManager.setup(d => d.textDocuments).returns(() => []).verifiable(TypeMoq.Times.once()); + + await helper.saveFileIfDirty(Uri.file(`${__filename}.py`)); + documentManager.verifyAll(); + }); + + test('File will be saved if file is dirty', async () => { + documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => true); + document.setup(doc => doc.languageId).returns(() => PythonLanguage.language); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify(doc => doc.save(), TypeMoq.Times.once()); + }); + + test('File will be not saved if file is not dirty', async () => { + documentManager.setup(d => d.textDocuments).returns(() => [document.object]).verifiable(TypeMoq.Times.once()); + document.setup(doc => doc.isUntitled).returns(() => false); + document.setup(doc => doc.isDirty).returns(() => false); + document.setup(doc => doc.languageId).returns(() => PythonLanguage.language); + const expectedUri = Uri.file('one.py'); + document.setup(doc => doc.uri).returns(() => expectedUri); + + await helper.saveFileIfDirty(expectedUri); + documentManager.verifyAll(); + document.verify(doc => doc.save(), TypeMoq.Times.never()); + }); });