Skip to content

Commit 5a1dc67

Browse files
authored
Add command to create a python terminal (#625)
Fixes #622
1 parent 2876e19 commit 5a1dc67

File tree

9 files changed

+224
-2
lines changed

9 files changed

+224
-2
lines changed

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,11 @@
106106
"title": "%python.command.python.startREPL.title%",
107107
"category": "Python"
108108
},
109+
{
110+
"command": "python.createTerminal",
111+
"title": "%python.command.python.createTerminal.title%",
112+
"category": "Python"
113+
},
109114
{
110115
"command": "python.buildWorkspaceSymbols",
111116
"title": "%python.command.python.buildWorkspaceSymbols.title%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"python.command.python.sortImports.title": "Sort Imports",
33
"python.command.python.startREPL.title": "Start REPL",
4+
"python.command.python.createTerminal.title": "Create Terminal",
45
"python.command.python.buildWorkspaceSymbols.title": "Build Workspace Symbols",
56
"python.command.python.runtests.title": "Run All Unit Tests",
67
"python.command.python.debugtests.title": "Debug All Unit Tests",

src/client/common/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export namespace Commands {
2828
export const Update_SparkLibrary = 'python.updateSparkLibrary';
2929
export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols';
3030
export const Start_REPL = 'python.startREPL';
31+
export const Create_Terminal = 'python.createTerminal';
3132
export const Set_Linter = 'python.setLinter';
3233
export const Enable_Linter = 'python.enableLinting';
3334
}

src/client/common/terminal/factory.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export class TerminalServiceFactory implements ITerminalServiceFactory {
2727

2828
return this.terminalServices.get(id)!;
2929
}
30+
public createTerminalService(resource?: Uri, title?: string): ITerminalService {
31+
const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python';
32+
return new TerminalService(this.serviceContainer, resource, terminalTitle);
33+
}
3034
private getTerminalId(title: string, resource?: Uri): string {
3135
if (!resource) {
3236
return title;

src/client/common/terminal/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export interface ITerminalService {
1818
readonly onDidCloseTerminal: Event<void>;
1919
sendCommand(command: string, args: string[]): Promise<void>;
2020
sendText(text: string): Promise<void>;
21-
show(): void;
21+
show(): Promise<void>;
2222
}
2323

2424
export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory');
@@ -33,6 +33,7 @@ export interface ITerminalServiceFactory {
3333
* @memberof ITerminalServiceFactory
3434
*/
3535
getTerminalService(resource?: Uri, title?: string): ITerminalService;
36+
createTerminalService(resource?: Uri, title?: string): ITerminalService;
3637
}
3738

3839
export const ITerminalHelper = Symbol('ITerminalHelper');

src/client/extension.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ if ((Reflect as any).metadata === undefined) {
77
}
88
import { Container } from 'inversify';
99
import * as os from 'os';
10-
import * as vscode from 'vscode';
1110
import { Disposable, Memento, OutputChannel, window } from 'vscode';
11+
import * as vscode from 'vscode';
1212
import { BannerService } from './banner';
1313
import { PythonSettings } from './common/configSettings';
1414
import * as settings from './common/configSettings';
@@ -47,6 +47,7 @@ import { ReplProvider } from './providers/replProvider';
4747
import { PythonSignatureProvider } from './providers/signatureProvider';
4848
import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider';
4949
import { PythonSymbolProvider } from './providers/symbolProvider';
50+
import { TerminalProvider } from './providers/terminalProvider';
5051
import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider';
5152
import * as sortImports from './sortImports';
5253
import { sendTelemetryEvent } from './telemetry';
@@ -118,6 +119,7 @@ export async function activate(context: vscode.ExtensionContext) {
118119
context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory));
119120

120121
context.subscriptions.push(new ReplProvider(serviceContainer));
122+
context.subscriptions.push(new TerminalProvider(serviceContainer));
121123

122124
// Enable indentAction
123125
// tslint:disable-next-line:no-non-null-assertion
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { Disposable, Uri } from 'vscode';
5+
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types';
6+
import { Commands } from '../common/constants';
7+
import { ITerminalServiceFactory } from '../common/terminal/types';
8+
import { IServiceContainer } from '../ioc/types';
9+
10+
export class TerminalProvider implements Disposable {
11+
private disposables: Disposable[] = [];
12+
constructor(private serviceContainer: IServiceContainer) {
13+
this.registerCommands();
14+
}
15+
public dispose() {
16+
this.disposables.forEach(disposable => disposable.dispose());
17+
}
18+
private registerCommands() {
19+
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
20+
const disposable = commandManager.registerCommand(Commands.Create_Terminal, this.onCreateTerminal, this);
21+
22+
this.disposables.push(disposable);
23+
}
24+
private async onCreateTerminal() {
25+
const terminalService = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory);
26+
const activeResource = this.getActiveResource();
27+
await terminalService.createTerminalService(activeResource, 'Python').show();
28+
}
29+
private getActiveResource(): Uri | undefined {
30+
const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager);
31+
if (documentManager.activeTextEditor && !documentManager.activeTextEditor.document.isUntitled) {
32+
return documentManager.activeTextEditor.document.uri;
33+
}
34+
const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
35+
return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0].uri : undefined;
36+
}
37+
}

src/test/common/terminals/factory.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as TypeMoq from 'typemoq';
66
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
77
import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types';
88
import { TerminalServiceFactory } from '../../../client/common/terminal/factory';
9+
import { TerminalService } from '../../../client/common/terminal/service';
910
import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common/terminal/types';
1011
import { IDisposableRegistry } from '../../../client/common/types';
1112
import { IInterpreterService } from '../../../client/interpreter/contracts';
@@ -54,6 +55,8 @@ suite('Terminal Service Factory', () => {
5455

5556
test('Ensure different instance of terminal service is returned when title is provided', () => {
5657
const defaultInstance = factory.getTerminalService();
58+
expect(defaultInstance instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service');
59+
5760
const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance;
5861
expect(notSameAsDefaultInstance).to.not.equal(true, 'Instances are the same as default instance');
5962

@@ -66,6 +69,22 @@ suite('Terminal Service Factory', () => {
6669
expect(notTheSameInstance).not.to.equal(true, 'Instances are the same');
6770
});
6871

72+
test('Ensure different instance of terminal services are created', () => {
73+
const instance1 = factory.createTerminalService();
74+
expect(instance1 instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service');
75+
76+
const notSameAsFirstInstance = factory.createTerminalService() === instance1;
77+
expect(notSameAsFirstInstance).to.not.equal(true, 'Instances are the same');
78+
79+
const instance2 = factory.createTerminalService(Uri.file('a'), 'Title');
80+
const notSameAsSecondInstance = instance1 === instance2;
81+
expect(notSameAsSecondInstance).to.not.equal(true, 'Instances are the same');
82+
83+
const instance3 = factory.createTerminalService(Uri.file('a'), 'Title');
84+
const notSameAsThirdInstance = instance2 === instance3;
85+
expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same');
86+
});
87+
6988
test('Ensure same terminal is returned when using resources from the same workspace', () => {
7089
const file1A = Uri.file('1a');
7190
const file2A = Uri.file('2a');

src/test/providers/terminal.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
import { expect } from 'chai';
5+
import * as TypeMoq from 'typemoq';
6+
import { Disposable, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode';
7+
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types';
8+
import { Commands } from '../../client/common/constants';
9+
import { TerminalService } from '../../client/common/terminal/service';
10+
import { ITerminalServiceFactory } from '../../client/common/terminal/types';
11+
import { IServiceContainer } from '../../client/ioc/types';
12+
import { TerminalProvider } from '../../client/providers/terminalProvider';
13+
14+
// tslint:disable-next-line:max-func-body-length
15+
suite('Terminal Provider', () => {
16+
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
17+
let commandManager: TypeMoq.IMock<ICommandManager>;
18+
let workspace: TypeMoq.IMock<IWorkspaceService>;
19+
let documentManager: TypeMoq.IMock<IDocumentManager>;
20+
let terminalProvider: TerminalProvider;
21+
setup(() => {
22+
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
23+
commandManager = TypeMoq.Mock.ofType<ICommandManager>();
24+
workspace = TypeMoq.Mock.ofType<IWorkspaceService>();
25+
documentManager = TypeMoq.Mock.ofType<IDocumentManager>();
26+
serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object);
27+
serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object);
28+
serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object);
29+
});
30+
teardown(() => {
31+
try {
32+
terminalProvider.dispose();
33+
// tslint:disable-next-line:no-empty
34+
} catch { }
35+
});
36+
37+
test('Ensure command is registered', () => {
38+
terminalProvider = new TerminalProvider(serviceContainer.object);
39+
commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
40+
});
41+
42+
test('Ensure command handler is disposed', () => {
43+
const disposable = TypeMoq.Mock.ofType<Disposable>();
44+
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object);
45+
46+
terminalProvider = new TerminalProvider(serviceContainer.object);
47+
terminalProvider.dispose();
48+
49+
disposable.verify(d => d.dispose(), TypeMoq.Times.once());
50+
});
51+
52+
test('Ensure terminal is created and displayed when command is invoked', () => {
53+
const disposable = TypeMoq.Mock.ofType<Disposable>();
54+
let commandHandler: undefined | (() => void);
55+
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
56+
commandHandler = callback;
57+
return disposable.object;
58+
});
59+
documentManager.setup(d => d.activeTextEditor).returns(() => undefined);
60+
workspace.setup(w => w.workspaceFolders).returns(() => undefined);
61+
62+
terminalProvider = new TerminalProvider(serviceContainer.object);
63+
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');
64+
65+
const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
66+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
67+
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
68+
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);
69+
70+
commandHandler!.call(terminalProvider);
71+
terminalService.verify(t => t.show(), TypeMoq.Times.once());
72+
});
73+
74+
test('Ensure terminal creation does not use uri of the active documents which is untitled', () => {
75+
const disposable = TypeMoq.Mock.ofType<Disposable>();
76+
let commandHandler: undefined | (() => void);
77+
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
78+
commandHandler = callback;
79+
return disposable.object;
80+
});
81+
const editor = TypeMoq.Mock.ofType<TextEditor>();
82+
documentManager.setup(d => d.activeTextEditor).returns(() => editor.object);
83+
const document = TypeMoq.Mock.ofType<TextDocument>();
84+
document.setup(d => d.isUntitled).returns(() => true);
85+
editor.setup(e => e.document).returns(() => document.object);
86+
workspace.setup(w => w.workspaceFolders).returns(() => undefined);
87+
88+
terminalProvider = new TerminalProvider(serviceContainer.object);
89+
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');
90+
91+
const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
92+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
93+
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
94+
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);
95+
96+
commandHandler!.call(terminalProvider);
97+
terminalService.verify(t => t.show(), TypeMoq.Times.once());
98+
});
99+
100+
test('Ensure terminal creation uses uri of active document', () => {
101+
const disposable = TypeMoq.Mock.ofType<Disposable>();
102+
let commandHandler: undefined | (() => void);
103+
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
104+
commandHandler = callback;
105+
return disposable.object;
106+
});
107+
const editor = TypeMoq.Mock.ofType<TextEditor>();
108+
documentManager.setup(d => d.activeTextEditor).returns(() => editor.object);
109+
const document = TypeMoq.Mock.ofType<TextDocument>();
110+
const documentUri = Uri.file('a');
111+
document.setup(d => d.isUntitled).returns(() => false);
112+
document.setup(d => d.uri).returns(() => documentUri);
113+
editor.setup(e => e.document).returns(() => document.object);
114+
workspace.setup(w => w.workspaceFolders).returns(() => undefined);
115+
116+
terminalProvider = new TerminalProvider(serviceContainer.object);
117+
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');
118+
119+
const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
120+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
121+
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
122+
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(documentUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);
123+
124+
commandHandler!.call(terminalProvider);
125+
terminalService.verify(t => t.show(), TypeMoq.Times.once());
126+
});
127+
128+
test('Ensure terminal creation uses uri of active workspace', () => {
129+
const disposable = TypeMoq.Mock.ofType<Disposable>();
130+
let commandHandler: undefined | (() => void);
131+
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
132+
commandHandler = callback;
133+
return disposable.object;
134+
});
135+
documentManager.setup(d => d.activeTextEditor).returns(() => undefined);
136+
const workspaceUri = Uri.file('a');
137+
const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>();
138+
workspaceFolder.setup(w => w.uri).returns(() => workspaceUri);
139+
workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder.object]);
140+
141+
terminalProvider = new TerminalProvider(serviceContainer.object);
142+
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');
143+
144+
const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
145+
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
146+
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
147+
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);
148+
149+
commandHandler!.call(terminalProvider);
150+
terminalService.verify(t => t.show(), TypeMoq.Times.once());
151+
});
152+
});

0 commit comments

Comments
 (0)