Skip to content

Commit

Permalink
Add command to create a python terminal (#625)
Browse files Browse the repository at this point in the history
Fixes #622
  • Loading branch information
DonJayamanne committed Jan 25, 2018
1 parent 2876e19 commit 5a1dc67
Show file tree
Hide file tree
Showing 9 changed files with 224 additions and 2 deletions.
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@
"title": "%python.command.python.startREPL.title%",
"category": "Python"
},
{
"command": "python.createTerminal",
"title": "%python.command.python.createTerminal.title%",
"category": "Python"
},
{
"command": "python.buildWorkspaceSymbols",
"title": "%python.command.python.buildWorkspaceSymbols.title%",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"python.command.python.sortImports.title": "Sort Imports",
"python.command.python.startREPL.title": "Start REPL",
"python.command.python.createTerminal.title": "Create Terminal",
"python.command.python.buildWorkspaceSymbols.title": "Build Workspace Symbols",
"python.command.python.runtests.title": "Run All Unit Tests",
"python.command.python.debugtests.title": "Debug All Unit Tests",
Expand Down
1 change: 1 addition & 0 deletions src/client/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export namespace Commands {
export const Update_SparkLibrary = 'python.updateSparkLibrary';
export const Build_Workspace_Symbols = 'python.buildWorkspaceSymbols';
export const Start_REPL = 'python.startREPL';
export const Create_Terminal = 'python.createTerminal';
export const Set_Linter = 'python.setLinter';
export const Enable_Linter = 'python.enableLinting';
}
Expand Down
4 changes: 4 additions & 0 deletions src/client/common/terminal/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ export class TerminalServiceFactory implements ITerminalServiceFactory {

return this.terminalServices.get(id)!;
}
public createTerminalService(resource?: Uri, title?: string): ITerminalService {
const terminalTitle = typeof title === 'string' && title.trim().length > 0 ? title.trim() : 'Python';
return new TerminalService(this.serviceContainer, resource, terminalTitle);
}
private getTerminalId(title: string, resource?: Uri): string {
if (!resource) {
return title;
Expand Down
3 changes: 2 additions & 1 deletion src/client/common/terminal/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export interface ITerminalService {
readonly onDidCloseTerminal: Event<void>;
sendCommand(command: string, args: string[]): Promise<void>;
sendText(text: string): Promise<void>;
show(): void;
show(): Promise<void>;
}

export const ITerminalServiceFactory = Symbol('ITerminalServiceFactory');
Expand All @@ -33,6 +33,7 @@ export interface ITerminalServiceFactory {
* @memberof ITerminalServiceFactory
*/
getTerminalService(resource?: Uri, title?: string): ITerminalService;
createTerminalService(resource?: Uri, title?: string): ITerminalService;
}

export const ITerminalHelper = Symbol('ITerminalHelper');
Expand Down
4 changes: 3 additions & 1 deletion src/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ if ((Reflect as any).metadata === undefined) {
}
import { Container } from 'inversify';
import * as os from 'os';
import * as vscode from 'vscode';
import { Disposable, Memento, OutputChannel, window } from 'vscode';
import * as vscode from 'vscode';
import { BannerService } from './banner';
import { PythonSettings } from './common/configSettings';
import * as settings from './common/configSettings';
Expand Down Expand Up @@ -47,6 +47,7 @@ import { ReplProvider } from './providers/replProvider';
import { PythonSignatureProvider } from './providers/signatureProvider';
import { activateSimplePythonRefactorProvider } from './providers/simpleRefactorProvider';
import { PythonSymbolProvider } from './providers/symbolProvider';
import { TerminalProvider } from './providers/terminalProvider';
import { activateUpdateSparkLibraryProvider } from './providers/updateSparkLibraryProvider';
import * as sortImports from './sortImports';
import { sendTelemetryEvent } from './telemetry';
Expand Down Expand Up @@ -118,6 +119,7 @@ export async function activate(context: vscode.ExtensionContext) {
context.subscriptions.push(...activateGoToObjectDefinitionProvider(jediFactory));

context.subscriptions.push(new ReplProvider(serviceContainer));
context.subscriptions.push(new TerminalProvider(serviceContainer));

// Enable indentAction
// tslint:disable-next-line:no-non-null-assertion
Expand Down
37 changes: 37 additions & 0 deletions src/client/providers/terminalProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { Disposable, Uri } from 'vscode';
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../common/application/types';
import { Commands } from '../common/constants';
import { ITerminalServiceFactory } from '../common/terminal/types';
import { IServiceContainer } from '../ioc/types';

export class TerminalProvider implements Disposable {
private disposables: Disposable[] = [];
constructor(private serviceContainer: IServiceContainer) {
this.registerCommands();
}
public dispose() {
this.disposables.forEach(disposable => disposable.dispose());
}
private registerCommands() {
const commandManager = this.serviceContainer.get<ICommandManager>(ICommandManager);
const disposable = commandManager.registerCommand(Commands.Create_Terminal, this.onCreateTerminal, this);

this.disposables.push(disposable);
}
private async onCreateTerminal() {
const terminalService = this.serviceContainer.get<ITerminalServiceFactory>(ITerminalServiceFactory);
const activeResource = this.getActiveResource();
await terminalService.createTerminalService(activeResource, 'Python').show();
}
private getActiveResource(): Uri | undefined {
const documentManager = this.serviceContainer.get<IDocumentManager>(IDocumentManager);
if (documentManager.activeTextEditor && !documentManager.activeTextEditor.document.isUntitled) {
return documentManager.activeTextEditor.document.uri;
}
const workspace = this.serviceContainer.get<IWorkspaceService>(IWorkspaceService);
return Array.isArray(workspace.workspaceFolders) && workspace.workspaceFolders.length > 0 ? workspace.workspaceFolders[0].uri : undefined;
}
}
19 changes: 19 additions & 0 deletions src/test/common/terminals/factory.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as TypeMoq from 'typemoq';
import { Disposable, Uri, WorkspaceFolder } from 'vscode';
import { ITerminalManager, IWorkspaceService } from '../../../client/common/application/types';
import { TerminalServiceFactory } from '../../../client/common/terminal/factory';
import { TerminalService } from '../../../client/common/terminal/service';
import { ITerminalHelper, ITerminalServiceFactory } from '../../../client/common/terminal/types';
import { IDisposableRegistry } from '../../../client/common/types';
import { IInterpreterService } from '../../../client/interpreter/contracts';
Expand Down Expand Up @@ -54,6 +55,8 @@ suite('Terminal Service Factory', () => {

test('Ensure different instance of terminal service is returned when title is provided', () => {
const defaultInstance = factory.getTerminalService();
expect(defaultInstance instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service');

const notSameAsDefaultInstance = factory.getTerminalService(undefined, 'New Title') === defaultInstance;
expect(notSameAsDefaultInstance).to.not.equal(true, 'Instances are the same as default instance');

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

test('Ensure different instance of terminal services are created', () => {
const instance1 = factory.createTerminalService();
expect(instance1 instanceof TerminalService).to.equal(true, 'Not an instance of Terminal service');

const notSameAsFirstInstance = factory.createTerminalService() === instance1;
expect(notSameAsFirstInstance).to.not.equal(true, 'Instances are the same');

const instance2 = factory.createTerminalService(Uri.file('a'), 'Title');
const notSameAsSecondInstance = instance1 === instance2;
expect(notSameAsSecondInstance).to.not.equal(true, 'Instances are the same');

const instance3 = factory.createTerminalService(Uri.file('a'), 'Title');
const notSameAsThirdInstance = instance2 === instance3;
expect(notSameAsThirdInstance).to.not.equal(true, 'Instances are the same');
});

test('Ensure same terminal is returned when using resources from the same workspace', () => {
const file1A = Uri.file('1a');
const file2A = Uri.file('2a');
Expand Down
152 changes: 152 additions & 0 deletions src/test/providers/terminal.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

import { expect } from 'chai';
import * as TypeMoq from 'typemoq';
import { Disposable, TextDocument, TextEditor, Uri, WorkspaceFolder } from 'vscode';
import { ICommandManager, IDocumentManager, IWorkspaceService } from '../../client/common/application/types';
import { Commands } from '../../client/common/constants';
import { TerminalService } from '../../client/common/terminal/service';
import { ITerminalServiceFactory } from '../../client/common/terminal/types';
import { IServiceContainer } from '../../client/ioc/types';
import { TerminalProvider } from '../../client/providers/terminalProvider';

// tslint:disable-next-line:max-func-body-length
suite('Terminal Provider', () => {
let serviceContainer: TypeMoq.IMock<IServiceContainer>;
let commandManager: TypeMoq.IMock<ICommandManager>;
let workspace: TypeMoq.IMock<IWorkspaceService>;
let documentManager: TypeMoq.IMock<IDocumentManager>;
let terminalProvider: TerminalProvider;
setup(() => {
serviceContainer = TypeMoq.Mock.ofType<IServiceContainer>();
commandManager = TypeMoq.Mock.ofType<ICommandManager>();
workspace = TypeMoq.Mock.ofType<IWorkspaceService>();
documentManager = TypeMoq.Mock.ofType<IDocumentManager>();
serviceContainer.setup(c => c.get(ICommandManager)).returns(() => commandManager.object);
serviceContainer.setup(c => c.get(IWorkspaceService)).returns(() => workspace.object);
serviceContainer.setup(c => c.get(IDocumentManager)).returns(() => documentManager.object);
});
teardown(() => {
try {
terminalProvider.dispose();
// tslint:disable-next-line:no-empty
} catch { }
});

test('Ensure command is registered', () => {
terminalProvider = new TerminalProvider(serviceContainer.object);
commandManager.verify(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once());
});

test('Ensure command handler is disposed', () => {
const disposable = TypeMoq.Mock.ofType<Disposable>();
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns(() => disposable.object);

terminalProvider = new TerminalProvider(serviceContainer.object);
terminalProvider.dispose();

disposable.verify(d => d.dispose(), TypeMoq.Times.once());
});

test('Ensure terminal is created and displayed when command is invoked', () => {
const disposable = TypeMoq.Mock.ofType<Disposable>();
let commandHandler: undefined | (() => void);
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
commandHandler = callback;
return disposable.object;
});
documentManager.setup(d => d.activeTextEditor).returns(() => undefined);
workspace.setup(w => w.workspaceFolders).returns(() => undefined);

terminalProvider = new TerminalProvider(serviceContainer.object);
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');

const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);

commandHandler!.call(terminalProvider);
terminalService.verify(t => t.show(), TypeMoq.Times.once());
});

test('Ensure terminal creation does not use uri of the active documents which is untitled', () => {
const disposable = TypeMoq.Mock.ofType<Disposable>();
let commandHandler: undefined | (() => void);
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
commandHandler = callback;
return disposable.object;
});
const editor = TypeMoq.Mock.ofType<TextEditor>();
documentManager.setup(d => d.activeTextEditor).returns(() => editor.object);
const document = TypeMoq.Mock.ofType<TextDocument>();
document.setup(d => d.isUntitled).returns(() => true);
editor.setup(e => e.document).returns(() => document.object);
workspace.setup(w => w.workspaceFolders).returns(() => undefined);

terminalProvider = new TerminalProvider(serviceContainer.object);
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');

const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(undefined), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);

commandHandler!.call(terminalProvider);
terminalService.verify(t => t.show(), TypeMoq.Times.once());
});

test('Ensure terminal creation uses uri of active document', () => {
const disposable = TypeMoq.Mock.ofType<Disposable>();
let commandHandler: undefined | (() => void);
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
commandHandler = callback;
return disposable.object;
});
const editor = TypeMoq.Mock.ofType<TextEditor>();
documentManager.setup(d => d.activeTextEditor).returns(() => editor.object);
const document = TypeMoq.Mock.ofType<TextDocument>();
const documentUri = Uri.file('a');
document.setup(d => d.isUntitled).returns(() => false);
document.setup(d => d.uri).returns(() => documentUri);
editor.setup(e => e.document).returns(() => document.object);
workspace.setup(w => w.workspaceFolders).returns(() => undefined);

terminalProvider = new TerminalProvider(serviceContainer.object);
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');

const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(documentUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);

commandHandler!.call(terminalProvider);
terminalService.verify(t => t.show(), TypeMoq.Times.once());
});

test('Ensure terminal creation uses uri of active workspace', () => {
const disposable = TypeMoq.Mock.ofType<Disposable>();
let commandHandler: undefined | (() => void);
commandManager.setup(c => c.registerCommand(TypeMoq.It.isValue(Commands.Create_Terminal), TypeMoq.It.isAny(), TypeMoq.It.isAny())).returns((_cmd, callback) => {
commandHandler = callback;
return disposable.object;
});
documentManager.setup(d => d.activeTextEditor).returns(() => undefined);
const workspaceUri = Uri.file('a');
const workspaceFolder = TypeMoq.Mock.ofType<WorkspaceFolder>();
workspaceFolder.setup(w => w.uri).returns(() => workspaceUri);
workspace.setup(w => w.workspaceFolders).returns(() => [workspaceFolder.object]);

terminalProvider = new TerminalProvider(serviceContainer.object);
expect(commandHandler).not.to.be.equal(undefined, 'Handler not set');

const terminalServiceFactory = TypeMoq.Mock.ofType<ITerminalServiceFactory>();
serviceContainer.setup(c => c.get(TypeMoq.It.isValue(ITerminalServiceFactory))).returns(() => terminalServiceFactory.object);
const terminalService = TypeMoq.Mock.ofType<TerminalService>();
terminalServiceFactory.setup(t => t.createTerminalService(TypeMoq.It.isValue(workspaceUri), TypeMoq.It.isValue('Python'))).returns(() => terminalService.object);

commandHandler!.call(terminalProvider);
terminalService.verify(t => t.show(), TypeMoq.Times.once());
});
});

0 comments on commit 5a1dc67

Please sign in to comment.