Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement integrated terminal extension API #10635

Merged
merged 15 commits into from
Aug 19, 2016
40 changes: 40 additions & 0 deletions src/vs/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2971,6 +2971,37 @@ declare namespace vscode {
dispose(): void;
}

/**
* An individual terminal instance within the integrated terminal.
*/
export interface Terminal {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add name as readonly so people can access that data they have passed in the create call

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added that and then reverted it, it never changes currently so they should be able to keep their old reference? Figured this would enable more flexibility later on if name becomes more dynamic (setName, getName or something.)

Copy link
Member

@jrieken jrieken Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For properties we use stuff like name: string and add a @readonly tag, like so: https://github.com/Microsoft/vscode/blob/master/src/vs/vscode.d.ts#L2843 or https://github.com/Microsoft/vscode/blob/master/src/vs/vscode.d.ts#L2751 (actually misses @readonly)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the interest of keeping the API slim, I don't think it's worth keeping this there since the name never changes after passed into createTerminal.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I put consistency against that

/**
* Send text to the terminal.
*
* @param addNewLine Whether to add a new line to the text being sent, this is normally
* required to run a command in the terminal. This defaults to `true`.
*/
sendText(text: string, addNewLine?: boolean);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this always the same sequence for new line or should addNewLine be something like \n|\r\n|\r?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing return type, comment for text


/**
* Reveal this channel in the UI.
*
* @param preserveFocus When `true` the channel will not take focus.
*/
show(preservceFocus?: boolean): void;

/**
* Hide this channel from the UI.
*/
hide(): void;

/**
* Dispose and free associated resources.
*/
dispose(): void;
}

/**
* Represents an extension.
*
Expand Down Expand Up @@ -3429,6 +3460,15 @@ declare namespace vscode {
* @return A new status bar item.
*/
export function createStatusBarItem(alignment?: StatusBarAlignment, priority?: number): StatusBarItem;

/**
* Creates a [Terminal](#Terminal).
*
* @param name The optional name of a terminal, this will override the label used in the
* terminal dropdown.
* @return A new Terminal.
*/
export function createTerminal(name?: string): Thenable<vscode.Terminal>;
Copy link
Member

@jrieken jrieken Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not return promises from there functions but already working objects (see createStatusBarItem). It should be createTerminal(name?: string): Terminal. We don't expose in the API that stuff is being created/executed in a different process.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vscode.window.showInputBox returns a Thenable though? The promise isn't due to the fact that it's executing in another process but that creating a terminal will show and focus it and potentially create the terminal panel before creating the terminal. While the first points can be worked around, I don't think the latter can be easily since the TerminalPanel may not be initialized when the API is called into?

Copy link
Member

@jrieken jrieken Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

showInputBox is a different thing because it waits for the user to type and hit enter etc. None of the other createXYZ function is async - that's the pattern. Create functions are to be compared with a new'ing an object.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify - making the create-call sync doesn't mean the logic behind it must be all sync. Since all functions of the terminal are async under the hood anyways (talking to the main thread) you can just make them all wait on the initial creation (promise).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ID of the terminal is currently the process ID of the terminal process which must be async, would you suggest I use some other ID to refer to the from the API then?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure - reusing that PID just seems like an implementation detail. Create an extension host side id which maps to Promise and you have it

}

/**
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/api/node/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {ExtHostQuickOpen} from 'vs/workbench/api/node/extHostQuickOpen';
import {ExtHostStatusBar} from 'vs/workbench/api/node/extHostStatusBar';
import {ExtHostCommands} from 'vs/workbench/api/node/extHostCommands';
import {ExtHostOutputService} from 'vs/workbench/api/node/extHostOutputService';
import {ExtHostTerminalService} from 'vs/workbench/api/node/extHostTerminalService';
import {ExtHostMessageService} from 'vs/workbench/api/node/extHostMessageService';
import {ExtHostEditors} from 'vs/workbench/api/node/extHostEditors';
import {ExtHostLanguages} from 'vs/workbench/api/node/extHostLanguages';
Expand Down Expand Up @@ -117,6 +118,7 @@ export class ExtHostAPIImplementation {
const extHostMessageService = new ExtHostMessageService(threadService);
const extHostStatusBar = new ExtHostStatusBar(threadService);
const extHostOutputService = new ExtHostOutputService(threadService);
const extHostTerminalService = new ExtHostTerminalService(threadService);
const workspacePath = contextService.getWorkspace() ? contextService.getWorkspace().resource.fsPath : undefined;
const extHostWorkspace = new ExtHostWorkspace(threadService, workspacePath);
const languages = new ExtHostLanguages(threadService);
Expand Down Expand Up @@ -253,6 +255,9 @@ export class ExtHostAPIImplementation {
},
createOutputChannel(name: string): vscode.OutputChannel {
return extHostOutputService.createOutputChannel(name);
},
createTerminal(name?: string): Thenable<vscode.Terminal> {
return extHostTerminalService.createTerminal(name);
}
};

Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/node/extHost.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {MainThreadQuickOpen} from './mainThreadQuickOpen';
import {MainThreadStatusBar} from './mainThreadStatusBar';
import {MainThreadStorage} from './mainThreadStorage';
import {MainThreadTelemetry} from './mainThreadTelemetry';
import {MainThreadTerminalService} from './mainThreadTerminalService';
import {MainThreadWorkspace} from './mainThreadWorkspace';
import {MainProcessExtensionService} from './mainThreadExtensionService';
import {MainThreadFileSystemEventService} from './mainThreadFileSystemEventService';
Expand Down Expand Up @@ -72,6 +73,7 @@ export class ExtHostContribution implements IWorkbenchContribution {
col.define(MainContext.MainThreadStatusBar).set(create(MainThreadStatusBar));
col.define(MainContext.MainThreadStorage).set(create(MainThreadStorage));
col.define(MainContext.MainThreadTelemetry).set(create(MainThreadTelemetry));
col.define(MainContext.MainThreadTerminalService).set(create(MainThreadTerminalService));
col.define(MainContext.MainThreadWorkspace).set(create(MainThreadWorkspace));
if (this.extensionService instanceof MainProcessExtensionService) {
col.define(MainContext.MainProcessExtensionService).set(<MainProcessExtensionService>this.extensionService);
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,14 @@ export abstract class MainThreadOutputServiceShape {
$close(channelId: string): TPromise<void> { throw ni(); }
}

export abstract class MainThreadTerminalServiceShape {
$createTerminal(name?: string): TPromise<number> { throw ni(); }
$dispose(terminalId: number): void { throw ni(); }
$hide(terminalId: number): void { throw ni(); }
$sendText(terminalId: number, text: string, addNewLine: boolean): void { throw ni(); }
$show(terminalId: number, preserveFocus: boolean): void { throw ni(); }
}

export interface MyQuickPickItems extends IPickOpenEntry {
handle: number;
}
Expand Down Expand Up @@ -294,6 +302,7 @@ export const MainContext = {
MainThreadStatusBar: createMainId<MainThreadStatusBarShape>('MainThreadStatusBar', MainThreadStatusBarShape),
MainThreadStorage: createMainId<MainThreadStorageShape>('MainThreadStorage', MainThreadStorageShape),
MainThreadTelemetry: createMainId<MainThreadTelemetryShape>('MainThreadTelemetry', MainThreadTelemetryShape),
MainThreadTerminalService: createMainId<MainThreadTerminalServiceShape>('MainThreadTerminalService', MainThreadTerminalServiceShape),
MainThreadWorkspace: createMainId<MainThreadWorkspaceShape>('MainThreadWorkspace', MainThreadWorkspaceShape),
MainProcessExtensionService: createMainId<MainProcessExtensionServiceShape>('MainProcessExtensionService', MainProcessExtensionServiceShape),
};
Expand Down
56 changes: 56 additions & 0 deletions src/vs/workbench/api/node/extHostTerminalService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import {TPromise} from 'vs/base/common/winjs.base';
import {IThreadService} from 'vs/workbench/services/thread/common/threadService';
import vscode = require('vscode');
import {MainContext, MainThreadTerminalServiceShape} from './extHost.protocol';

export class ExtHostTerminal implements vscode.Terminal {

private _id: number;
private _proxy: MainThreadTerminalServiceShape;
private _disposed: boolean;

constructor(proxy: MainThreadTerminalServiceShape, id: number) {
this._id = id;
this._proxy = proxy;
}

public sendText(text: string, addNewLine: boolean = true) {
this._proxy.$sendText(this._id, text, addNewLine);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throw if disposed?

}

public show(preserveFocus: boolean): void {
this._proxy.$show(this._id, preserveFocus);
}

public hide(): void {
this._proxy.$hide(this._id);
}

public dispose(): void {
if (!this._disposed) {
this._disposed = true;
this._proxy.$dispose(this._id);
}
}
}

export class ExtHostTerminalService {

private _proxy: MainThreadTerminalServiceShape;

constructor(threadService: IThreadService) {
this._proxy = threadService.get(MainContext.MainThreadTerminalService);
}

public createTerminal(name?: string): TPromise<vscode.Terminal> {
return this._proxy.$createTerminal(name).then((terminalId) => {
return new ExtHostTerminal(this._proxy, terminalId);
});
}
}
51 changes: 51 additions & 0 deletions src/vs/workbench/api/node/mainThreadTerminalService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';

import {TPromise} from 'vs/base/common/winjs.base';
import {ITerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminal';
import {MainThreadTerminalServiceShape} from './extHost.protocol';

export class MainThreadTerminalService extends MainThreadTerminalServiceShape {

private _terminalService: ITerminalService;

constructor(
@ITerminalService terminalService: ITerminalService
) {
super();
this._terminalService = terminalService;
}

public $createTerminal(name?: string): TPromise<number> {
return this._terminalService.createNew(name);
}

public $show(terminalId: number, preserveFocus: boolean): void {
this._terminalService.show(!preserveFocus).then((terminalPanel) => {
terminalPanel.setActiveTerminalById(terminalId);
});
}

public $hide(terminalId: number): void {
this._terminalService.hide();
}

public $dispose(terminalId: number): void {
// TODO: This could be improved by not first showing the terminal to be disposed
var self = this;
Copy link
Member

@jrieken jrieken Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self, really? it's a fat-arrow function

Copy link
Member Author

@Tyriar Tyriar Aug 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, didn't know TS worked like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's even JS these days ;-)

this._terminalService.show(false).then((terminalPanel) => {
terminalPanel.setActiveTerminalById(terminalId);
self._terminalService.close();
});;
}

public $sendText(terminalId: number, text: string, addNewLine: boolean): void {
this._terminalService.show(false).then((terminalPanel) => {
terminalPanel.setActiveTerminalById(terminalId);
terminalPanel.sendTextToActiveTerminal(text, addNewLine);
});
}
}
9 changes: 7 additions & 2 deletions src/vs/workbench/parts/terminal/electron-browser/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ export interface ITerminalService {

close(): TPromise<any>;
copySelection(): TPromise<any>;
createNew(): TPromise<any>;
focus(): TPromise<any>;
createNew(name?: string): TPromise<number>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a recommendation I'd return the ITerminalPanel here and move setActive, show, dispose etc in it - the services seems to become quite large.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring this to #10721

focusNext(): TPromise<any>;
focusPrevious(): TPromise<any>;
hide(): TPromise<any>;
paste(): TPromise<any>;
runSelectedText(): TPromise<any>;
scrollDown(): TPromise<any>;
scrollUp(): TPromise<any>;
show(focus: boolean): TPromise<ITerminalPanel>;
setActiveTerminal(index: number): TPromise<any>;
toggle(): TPromise<any>;

Expand All @@ -82,3 +82,8 @@ export interface ITerminalService {
initConfigHelper(panelContainer: Builder): void;
killTerminalProcess(terminalProcess: ITerminalProcess): void;
}

export interface ITerminalPanel {
sendTextToActiveTerminal(text: string, addNewLine: boolean): void;
setActiveTerminalById(terminalId: number): void;
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export class FocusTerminalAction extends Action {
}

public run(event?: any): TPromise<any> {
return this.terminalService.focus();
return this.terminalService.show(true);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {TabFocus} from 'vs/editor/common/config/commonEditorConfig';

export class TerminalInstance {

public id: number;

private static eolRegex = /\r?\n/g;

private isExiting: boolean = false;
Expand Down Expand Up @@ -53,6 +55,7 @@ export class TerminalInstance {
this.terminalDomElement = document.createElement('div');
this.xterm = xterm();

this.id = this.terminalProcess.process.pid;
this.terminalProcess.process.on('message', (message) => {
if (message.type === 'data') {
this.xterm.write(message.content);
Expand All @@ -63,6 +66,8 @@ export class TerminalInstance {
event: 'input',
data: this.sanitizeInput(data)
});

console.log('this.terminalProcess.process.pid=' + this.terminalProcess.process.pid);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💥

return false;
});
this.xterm.attachCustomKeydownHandler(function (event: KeyboardEvent) {
Expand Down Expand Up @@ -176,6 +181,16 @@ export class TerminalInstance {
});
}

public sendText(text: string, addNewLine: boolean): void {;
if (addNewLine && text.substr(text.length - os.EOL.length) !== os.EOL) {
text += os.EOL;
}
this.terminalProcess.process.send({
event: 'input',
data: text
});
}

public focus(force?: boolean): void {
if (!this.xterm) {
return;
Expand Down
28 changes: 23 additions & 5 deletions src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class TerminalPanel extends Panel {
return super.getActionItem(action);
}

public create(parent: Builder): TPromise<void> {
public create(parent: Builder): TPromise<any> {
super.create(parent);
this.parentDomElement = parent.getHTMLElement();
this.terminalService.initConfigHelper(parent);
Expand Down Expand Up @@ -174,10 +174,11 @@ export class TerminalPanel extends Panel {
}));
}

public createNewTerminalInstance(terminalProcess: ITerminalProcess, terminalFocusContextKey: IContextKey<boolean>): TPromise<void> {
return this.createTerminal(terminalProcess, terminalFocusContextKey).then(() => {
public createNewTerminalInstance(process: ITerminalProcess, focusContextKey: IContextKey<boolean>): TPromise<number> {
return this.createTerminal(process, focusContextKey).then((terminalInstance) => {
this.updateConfig();
this.focus();
return TPromise.as(terminalInstance.id);
});
}

Expand Down Expand Up @@ -206,6 +207,11 @@ export class TerminalPanel extends Panel {
return super.setVisible(visible);
}

public sendTextToActiveTerminal(text: string, addNewLine: boolean): void {
let terminalInstance = this.terminalInstances[this.terminalService.getActiveTerminalIndex()];
terminalInstance.sendText(text, addNewLine);
}

private createTerminal(terminalProcess: ITerminalProcess, terminalFocusContextKey: IContextKey<boolean>): TPromise<TerminalInstance> {
return new TPromise<TerminalInstance>(resolve => {
var terminalInstance = new TerminalInstance(
Expand All @@ -229,12 +235,24 @@ export class TerminalPanel extends Panel {
});
}

public setActiveTerminal(newActiveIndex: number) {
public setActiveTerminal(newActiveIndex: number): void {
this.terminalInstances.forEach((terminalInstance, i) => {
terminalInstance.toggleVisibility(i === newActiveIndex);
});
}

public setActiveTerminalById(terminalId: number): void {
let terminalIndex = -1;
this.terminalInstances.forEach((terminalInstance, i) => {
if (terminalInstance.id === terminalId) {
terminalIndex = i;
}
});
if (terminalIndex !== -1) {
this.setActiveTerminal(terminalIndex);
}
}

private onTerminalInstanceExit(terminalInstance: TerminalInstance): void {
let index = this.terminalInstances.indexOf(terminalInstance);
if (index !== -1) {
Expand All @@ -247,7 +265,7 @@ export class TerminalPanel extends Panel {
if (this.terminalInstances.length === 0) {
this.terminalService.hide();
} else {
this.terminalService.focus();
this.terminalService.show(true);
}
}

Expand Down
Loading