Skip to content

Commit 55f5ca5

Browse files
authored
Enhancements to running code in a terminal (#1432)
Fixes #1207 Fixes #1316 Fixes #1349 Fixes #259
1 parent 2ba4e6c commit 55f5ca5

24 files changed

+270
-36
lines changed

news/1 Enhancements/1207.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Remove empty spaces from the selected text of the active editor when executing in a terminal.

news/1 Enhancements/1316.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Save the python file before running it in the terminal using the command/menu `Run Python File in Terminal`.

news/1 Enhancements/1349.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add `Ctrl+Enter` keyboard shortcut for `Run Selection/Line in Python Terminal`.

news/2 Fixes/259.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
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.

package.json

+6
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@
9494
"path": "./snippets/python.json"
9595
}
9696
],
97+
"keybindings":[
98+
{
99+
"command": "python.execSelectionInTerminal",
100+
"key": "ctrl+enter"
101+
}
102+
],
97103
"commands": [
98104
{
99105
"command": "python.sortImports",

src/client/common/configSettings.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class PythonSettings extends EventEmitter implements IPythonSettings {
3737
public formatting?: IFormattingSettings;
3838
public autoComplete?: IAutoCompleteSettings;
3939
public unitTest?: IUnitTestSettings;
40-
public terminal?: ITerminalSettings;
40+
public terminal!: ITerminalSettings;
4141
public sortImports?: ISortImportSettings;
4242
public workspaceSymbols?: IWorkspaceSymbolSettings;
4343
public disableInstallationChecks = false;

src/client/common/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export interface IPythonSettings {
106106
readonly formatting?: IFormattingSettings;
107107
readonly unitTest?: IUnitTestSettings;
108108
readonly autoComplete?: IAutoCompleteSettings;
109-
readonly terminal?: ITerminalSettings;
109+
readonly terminal: ITerminalSettings;
110110
readonly sortImports?: ISortImportSettings;
111111
readonly workspaceSymbols?: IWorkspaceSymbolSettings;
112112
readonly envFile: string;

src/client/terminals/codeExecution/codeExecutionManager.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export class CodeExecutionManager implements ICodeExecutionManager {
3535
if (!fileToExecute) {
3636
return;
3737
}
38+
await codeExecutionHelper.saveFileIfDirty(fileToExecute);
3839
const executionService = this.serviceContainer.get<ICodeExecutionService>(ICodeExecutionService, 'standard');
3940
await executionService.executeFile(fileToExecute);
4041
}
@@ -59,11 +60,11 @@ export class CodeExecutionManager implements ICodeExecutionManager {
5960
}
6061
const codeExecutionHelper = this.serviceContainer.get<ICodeExecutionHelper>(ICodeExecutionHelper);
6162
const codeToExecute = await codeExecutionHelper.getSelectedTextToExecute(activeEditor!);
62-
const normalizedCode = codeExecutionHelper.normalizeLines(codeToExecute!);
63+
const normalizedCode = await codeExecutionHelper.normalizeLines(codeToExecute!);
6364
if (!normalizedCode || normalizedCode.trim().length === 0) {
6465
return;
6566
}
6667

67-
await executionService.execute(codeToExecute!, activeEditor!.document.uri);
68+
await executionService.execute(normalizedCode, activeEditor!.document.uri);
6869
}
6970
}

src/client/terminals/codeExecution/djangoShellCodeExecution.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class DjangoShellCodeExecutionProvider extends TerminalCodeExecutionProvi
3232
public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } {
3333
const pythonSettings = this.configurationService.getSettings(resource);
3434
const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath;
35-
const args = pythonSettings.terminal!.launchArgs.slice();
35+
const args = pythonSettings.terminal.launchArgs.slice();
3636

3737
const workspaceUri = resource ? this.workspace.getWorkspaceFolder(resource) : undefined;
3838
const defaultWorkspace = Array.isArray(this.workspace.workspaceFolders) && this.workspace.workspaceFolders.length > 0 ? this.workspace.workspaceFolders[0].uri.fsPath : '';

src/client/terminals/codeExecution/helper.ts

+28-8
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,34 @@
22
// Licensed under the MIT License.
33

44
import { inject, injectable } from 'inversify';
5-
import { EOL } from 'os';
65
import { Range, TextEditor, Uri } from 'vscode';
76
import { IApplicationShell, IDocumentManager } from '../../common/application/types';
87
import { PythonLanguage } from '../../common/constants';
98
import '../../common/extensions';
9+
import { IServiceContainer } from '../../ioc/types';
1010
import { ICodeExecutionHelper } from '../types';
1111

1212
@injectable()
1313
export class CodeExecutionHelper implements ICodeExecutionHelper {
14-
constructor( @inject(IDocumentManager) private documentManager: IDocumentManager,
15-
@inject(IApplicationShell) private applicationShell: IApplicationShell) {
16-
14+
private readonly documentManager: IDocumentManager;
15+
private readonly applicationShell: IApplicationShell;
16+
constructor(@inject(IServiceContainer) serviceContainer: IServiceContainer) {
17+
this.documentManager = serviceContainer.get<IDocumentManager>(IDocumentManager);
18+
this.applicationShell = serviceContainer.get<IApplicationShell>(IApplicationShell);
1719
}
18-
public normalizeLines(code: string): string {
19-
const codeLines = code.splitLines({ trim: false, removeEmptyEntries: false });
20-
const codeLinesWithoutEmptyLines = codeLines.filter(line => line.trim().length > 0);
21-
return codeLinesWithoutEmptyLines.join(EOL);
20+
public async normalizeLines(code: string, resource?: Uri): Promise<string> {
21+
try {
22+
if (code.trim().length === 0) {
23+
return '';
24+
}
25+
const regex = /(\n)([ \t]*\r?\n)([ \t]+\S+)/gm;
26+
return code.replace(regex, (_, a, b, c) => {
27+
return `${a}${c}`;
28+
});
29+
} catch (ex) {
30+
console.error(ex, 'Python: Failed to normalize code for execution in terminal');
31+
return code;
32+
}
2233
}
2334

2435
public async getFileToExecute(): Promise<Uri | undefined> {
@@ -35,6 +46,9 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
3546
this.applicationShell.showErrorMessage('The active file is not a Python source file');
3647
return;
3748
}
49+
if (activeEditor.document.isDirty) {
50+
await activeEditor.document.save();
51+
}
3852
return activeEditor.document.uri;
3953
}
4054

@@ -53,4 +67,10 @@ export class CodeExecutionHelper implements ICodeExecutionHelper {
5367
}
5468
return code;
5569
}
70+
public async saveFileIfDirty(file: Uri): Promise<void> {
71+
const docs = this.documentManager.textDocuments.filter(d => d.uri.path === file.path);
72+
if (docs.length === 1 && docs[0].isDirty) {
73+
await docs[0].save();
74+
}
75+
}
5676
}

src/client/terminals/codeExecution/terminalCodeExecution.ts

+5-6
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,15 @@ import { IWorkspaceService } from '../../common/application/types';
1010
import '../../common/extensions';
1111
import { IPlatformService } from '../../common/platform/types';
1212
import { ITerminalService, ITerminalServiceFactory } from '../../common/terminal/types';
13-
import { IConfigurationService } from '../../common/types';
14-
import { IDisposableRegistry } from '../../common/types';
13+
import { IConfigurationService, IDisposableRegistry } from '../../common/types';
1514
import { ICodeExecutionService } from '../../terminals/types';
1615

1716
@injectable()
1817
export class TerminalCodeExecutionProvider implements ICodeExecutionService {
19-
protected terminalTitle: string;
20-
private _terminalService: ITerminalService;
18+
protected terminalTitle!: string;
19+
private _terminalService!: ITerminalService;
2120
private replActive?: Promise<boolean>;
22-
constructor( @inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory,
21+
constructor(@inject(ITerminalServiceFactory) protected readonly terminalServiceFactory: ITerminalServiceFactory,
2322
@inject(IConfigurationService) protected readonly configurationService: IConfigurationService,
2423
@inject(IWorkspaceService) protected readonly workspace: IWorkspaceService,
2524
@inject(IDisposableRegistry) protected readonly disposables: Disposable[],
@@ -60,7 +59,7 @@ export class TerminalCodeExecutionProvider implements ICodeExecutionService {
6059

6160
await this.replActive;
6261
}
63-
public getReplCommandArgs(resource?: Uri): { command: string, args: string[] } {
62+
public getReplCommandArgs(resource?: Uri): { command: string; args: string[] } {
6463
const pythonSettings = this.configurationService.getSettings(resource);
6564
const command = this.platformService.isWindows ? pythonSettings.pythonPath.replace(/\\/g, '/') : pythonSettings.pythonPath;
6665
const args = pythonSettings.terminal.launchArgs.slice();

src/client/terminals/types.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ export interface ICodeExecutionService {
1414
export const ICodeExecutionHelper = Symbol('ICodeExecutionHelper');
1515

1616
export interface ICodeExecutionHelper {
17-
normalizeLines(code: string): string;
17+
normalizeLines(code: string): Promise<string>;
1818
getFileToExecute(): Promise<Uri | undefined>;
19+
saveFileIfDirty(file: Uri): Promise<void>;
1920
getSelectedTextToExecute(textEditor: TextEditor): Promise<string | undefined>;
2021
}
2122

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Sample block 1
2+
def square(x):
3+
return x**2
4+
5+
print('hello')
6+
# Sample block 2
7+
a = 2
8+
if a < 2:
9+
print('less than 2')
10+
else:
11+
print('more than 2')
12+
13+
print('hello')
14+
15+
# Sample block 3
16+
for i in range(5):
17+
print(i)
18+
print(i)
19+
print(i)
20+
print(i)
21+
22+
print('complete')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Sample block 1
2+
def square(x):
3+
return x**2
4+
5+
print('hello')
6+
# Sample block 2
7+
a = 2
8+
if a < 2:
9+
print('less than 2')
10+
else:
11+
print('more than 2')
12+
13+
print('hello')
14+
15+
# Sample block 3
16+
for i in range(5):
17+
print(i)
18+
19+
print(i)
20+
print(i)
21+
22+
print(i)
23+
24+
print('complete')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
def add(x, y):
2+
"""Adds x to y"""
3+
# Some comment
4+
return x + y
5+
6+
v = add(1, 7)
7+
print(v)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def add(x, y):
2+
"""Adds x to y"""
3+
# Some comment
4+
5+
return x + y
6+
7+
v = add(1, 7)
8+
print(v)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
if True:
2+
print(1)
3+
print(2)
4+
print(3)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
if True:
2+
print(1)
3+
4+
print(2)
5+
print(3)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
class pc(object):
2+
def __init__(self, pcname, model):
3+
self.pcname = pcname
4+
self.model = model
5+
def print_name(self):
6+
print('Workstation name is', self.pcname, 'model is', self.model)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class pc(object):
2+
def __init__(self, pcname, model):
3+
self.pcname = pcname
4+
self.model = model
5+
6+
def print_name(self):
7+
print('Workstation name is', self.pcname, 'model is', self.model)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
for i in range(10):
2+
print('a')
3+
for j in range(5):
4+
print('b')
5+
print('b2')
6+
for k in range(2):
7+
print('c')
8+
print('done with first loop')
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
for i in range(10):
2+
print('a')
3+
for j in range(5):
4+
print('b')
5+
6+
print('b2')
7+
8+
for k in range(2):
9+
print('c')
10+
11+
print('done with first loop')

src/test/terminals/codeExecution/codeExecutionManager.test.ts

+10-9
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ suite('Terminal - Code Execution Manager', () => {
6969
serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object);
7070

7171
await commandHandler!();
72-
helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.once());
72+
helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.once());
7373
});
7474

7575
test('Ensure executeFileInterTerminal will use provided file', async () => {
@@ -96,8 +96,8 @@ suite('Terminal - Code Execution Manager', () => {
9696

9797
const fileToExecute = Uri.file('x');
9898
await commandHandler!(fileToExecute);
99-
helper.verify(async h => await h.getFileToExecute(), TypeMoq.Times.never());
100-
executionService.verify(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once());
99+
helper.verify(async h => h.getFileToExecute(), TypeMoq.Times.never());
100+
executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once());
101101
});
102102

103103
test('Ensure executeFileInterTerminal will use active file', async () => {
@@ -119,12 +119,12 @@ suite('Terminal - Code Execution Manager', () => {
119119
const fileToExecute = Uri.file('x');
120120
const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>();
121121
serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object);
122-
helper.setup(async h => await h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute));
122+
helper.setup(async h => h.getFileToExecute()).returns(() => Promise.resolve(fileToExecute));
123123
const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>();
124124
serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue('standard'))).returns(() => executionService.object);
125125

126126
await commandHandler!(fileToExecute);
127-
executionService.verify(async e => await e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once());
127+
executionService.verify(async e => e.executeFile(TypeMoq.It.isValue(fileToExecute)), TypeMoq.Times.once());
128128
});
129129

130130
async function testExecutionOfSelectionWithoutAnyActiveDocument(commandId: string, executionSericeId: string) {
@@ -150,7 +150,7 @@ suite('Terminal - Code Execution Manager', () => {
150150
documentManager.setup(d => d.activeTextEditor).returns(() => undefined);
151151

152152
await commandHandler!();
153-
executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never());
153+
executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never());
154154
}
155155

156156
test('Ensure executeSelectionInTerminal will do nothing if theres no active document', async () => {
@@ -186,7 +186,7 @@ suite('Terminal - Code Execution Manager', () => {
186186
documentManager.setup(d => d.activeTextEditor).returns(() => { return {} as any; });
187187

188188
await commandHandler!();
189-
executionService.verify(async e => await e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never());
189+
executionService.verify(async e => e.execute(TypeMoq.It.isAny()), TypeMoq.Times.never());
190190
}
191191

192192
test('Ensure executeSelectionInTerminal will do nothing if no text is selected', async () => {
@@ -218,7 +218,7 @@ suite('Terminal - Code Execution Manager', () => {
218218
const helper = TypeMoq.Mock.ofType<ICodeExecutionHelper>();
219219
serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionHelper))).returns(() => helper.object);
220220
helper.setup(h => h.getSelectedTextToExecute).returns(() => () => Promise.resolve(textSelected));
221-
helper.setup(h => h.normalizeLines).returns(() => () => textSelected);
221+
helper.setup(h => h.normalizeLines).returns(() => () => Promise.resolve(textSelected)).verifiable(TypeMoq.Times.once());
222222
const executionService = TypeMoq.Mock.ofType<ICodeExecutionService>();
223223
serviceContainer.setup(s => s.get(TypeMoq.It.isValue(ICodeExecutionService), TypeMoq.It.isValue(executionServiceId))).returns(() => executionService.object);
224224
const document = TypeMoq.Mock.ofType<TextDocument>();
@@ -228,7 +228,8 @@ suite('Terminal - Code Execution Manager', () => {
228228
documentManager.setup(d => d.activeTextEditor).returns(() => activeEditor.object);
229229

230230
await commandHandler!();
231-
executionService.verify(async e => await e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once());
231+
executionService.verify(async e => e.execute(TypeMoq.It.isValue(textSelected), TypeMoq.It.isValue(activeDocumentUri)), TypeMoq.Times.once());
232+
helper.verifyAll();
232233
}
233234
test('Ensure executeSelectionInTerminal will normalize selected text and send it to the terminal', async () => {
234235
await testExecutionOfSelectionIsSentToTerminal(Commands.Exec_Selection_In_Terminal, 'standard');

0 commit comments

Comments
 (0)