diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 73aeae4a6eb..a252e748184 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -340,6 +340,11 @@ "from": "preserve@>=0.2.0 <0.3.0", "resolved": "https://registry.npmjs.org/preserve/-/preserve-0.2.0.tgz" }, + "pty.js": { + "version": "0.3.0", + "from": "https://github.com/platformio/pty.js/tarball/22e84bd6ef177c7c211a767564ffe64426cb8a69", + "resolved": "https://github.com/platformio/pty.js/tarball/22e84bd6ef177c7c211a767564ffe64426cb8a69" + }, "randomatic": { "version": "1.1.5", "from": "randomatic@>=1.1.3 <2.0.0", @@ -390,6 +395,11 @@ "from": "string_decoder@>=0.10.0 <0.11.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" }, + "term.js": { + "version": "0.0.7", + "from": "https://github.com/jeremyramin/term.js/tarball/master", + "resolved": "https://github.com/jeremyramin/term.js/tarball/master" + }, "typechecker": { "version": "2.0.8", "from": "typechecker@>=2.0.1 <2.1.0", diff --git a/package.json b/package.json index 4066a293192..97158298980 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,10 @@ "iconv-lite": "0.4.13", "minimist": "^1.2.0", "native-keymap": "0.1.2", + "pty.js": "https://github.com/platformio/pty.js/tarball/22e84bd6ef177c7c211a767564ffe64426cb8a69", "sax": "1.1.2", "semver": "4.3.6", + "term.js": "https://github.com/jeremyramin/term.js/tarball/master", "vscode-debugprotocol": "1.8.0", "vscode-textmate": "1.0.11", "winreg": "1.2.0", diff --git a/src/vs/workbench/parts/terminal/common/terminal.ts b/src/vs/workbench/parts/terminal/common/terminal.ts new file mode 100644 index 00000000000..66b820c11c2 --- /dev/null +++ b/src/vs/workbench/parts/terminal/common/terminal.ts @@ -0,0 +1,53 @@ +/*--------------------------------------------------------------------------------------------- + * 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 {createDecorator, ServiceIdentifier} from 'vs/platform/instantiation/common/instantiation'; +import path = require('path'); +import platform = require('vs/base/common/platform'); + +export const TERMINAL_PANEL_ID = 'workbench.panel.terminal'; + +export const TERMINAL_SERVICE_ID = 'terminalService'; + +export const TERMINAL_DEFAULT_SHELL_UNIX_LIKE = process.env.SHELL || 'sh'; +export const TERMINAL_DEFAULT_SHELL_WINDOWS = platform.isWindows ? path.resolve(process.env.SystemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe') : ''; + +export var ITerminalService = createDecorator(TERMINAL_SERVICE_ID); + +export interface ITerminalConfiguration { + integratedTerminal: { + shell: { + unixLike: string, + windows: string + }, + fontFamily: string, + ansiColors: { + black: string, + red: string, + green: string, + yellow: string, + blue: string, + magenta: string, + cyan: string, + white: string, + brightBlack: string, + brightRed: string, + brightGreen: string, + brightYellow: string, + brightBlue: string, + brightMagenta: string, + brightCyan: string, + brightWhite: string, + } + }; +} + +export interface ITerminalService { + serviceId: ServiceIdentifier; + + show(): TPromise; +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/terminal.contribution.css b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.contribution.css new file mode 100644 index 00000000000..ff499f7f13d --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/media/terminal.contribution.css @@ -0,0 +1,46 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.monaco-workbench .integrated-terminal { + align-items: flex-end; + display: flex; +} + +.monaco-workbench .integrated-terminal { + background-color: #1e1e1e!important; + color: #CCC; + -webkit-user-select: initial; + overflow: hidden; /* prevents the terminal output being incorrectly placed over the title */ +} + +.monaco-workbench .integrated-terminal .terminal { + background-color: #1e1e1e!important; +} + +.monaco-workbench .integrated-terminal .terminal span { + display: inline-block; /* make terminal characters cover height of line */ +} + +.monaco-workbench .integrated-terminal .terminal-cursor { + background-color: #CCC; +} + +.monaco-workbench .integrated-terminal .reverse-video { + color: #1e1e1e; +} + +/* High Contrast Theming */ + +.hc-dark .monaco-workbench .integrated-terminal .terminal { + background-color: black!important; +} + +.hc-dark .monaco-workbench .integrated-terminal .terminal-cursor { + background-color: white; +} + +.hc-dark .monaco-workbench .integrated-terminal .reverse-video { + color: black; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/pty.js.d.ts b/src/vs/workbench/parts/terminal/electron-browser/pty.js.d.ts new file mode 100644 index 00000000000..cc726238aa2 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/pty.js.d.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'pty.js' { + export function fork(file: string, args: string[], options: any): Terminal; + export function spawn(file: string, args: string[], options: any): Terminal; + export function createTerminal(file: string, args: string[], options: any): Terminal; + + export interface Terminal { + /** + * The title of the active process. + */ + process: string; + + on(event: string, callback: (data: any) => void): void; + + resize(columns: number, rows: number): void; + + write(data: string): void; + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/term.js.d.ts b/src/vs/workbench/parts/terminal/electron-browser/term.js.d.ts new file mode 100644 index 00000000000..faa6d112d7a --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/term.js.d.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'term.js' { + function init(options: any): TermJsTerminal; + + // There seems to be no way to export this so it can be referenced outside of this file when a + // module is a function. + interface TermJsTerminal { + on(event: string, callback: (data: any) => void): void; + resize(columns: number, rows: number): void; + } + + export = init; +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts new file mode 100644 index 00000000000..f805c9d9714 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminal.contribution.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import 'vs/css!./media/terminal.contribution'; +import nls = require('vs/nls'); +import product from 'vs/platform/product'; +//import {KeyMod, KeyCode} from 'vs/base/common/keyCodes'; +import {SyncActionDescriptor} from 'vs/platform/actions/common/actions'; +import {registerSingleton} from 'vs/platform/instantiation/common/extensions'; +import {IWorkbenchActionRegistry, Extensions as ActionExtensions} from 'vs/workbench/common/actionRegistry'; +import {TerminalService} from 'vs/workbench/parts/terminal/electron-browser/terminalService'; +import {ToggleTerminalAction} from 'vs/workbench/parts/terminal/electron-browser/terminalActions'; +import {ITerminalService, TERMINAL_PANEL_ID, TERMINAL_DEFAULT_SHELL_UNIX_LIKE, TERMINAL_DEFAULT_SHELL_WINDOWS} from 'vs/workbench/parts/terminal/common/terminal'; +import * as panel from 'vs/workbench/browser/panel'; +import {Registry} from 'vs/platform/platform'; +import {Extensions, IConfigurationRegistry} from 'vs/platform/configuration/common/configurationRegistry'; + +// Only enable in the alpha channel until more stable +if (product.quality === 'alpha') { + let configurationRegistry = Registry.as(Extensions.Configuration); + configurationRegistry.registerConfiguration({ + 'id': 'terminal', + 'order': 100, + 'title': nls.localize('integratedTerminalConfigurationTitle', "(Experimental) Integrated terminal configuration"), + 'type': 'object', + 'properties': { + 'integratedTerminal.shell.unixLike': { + 'description': nls.localize('integratedTerminal.shell.unixLike', "The path of the shell that the terminal uses on Linux and OS X."), + 'type': 'string', + 'default': TERMINAL_DEFAULT_SHELL_UNIX_LIKE + }, + 'integratedTerminal.shell.windows': { + 'description': nls.localize('integratedTerminal.shell.windows', "The path of the shell that the terminal uses on Windows."), + 'type': 'string', + 'default': TERMINAL_DEFAULT_SHELL_WINDOWS + }, + 'integratedTerminal.fontFamily': { + 'description': nls.localize('integratedTerminal.fontFamily', "The font family used by the terminal (CSS font-family format)."), + 'type': 'string', + 'default': 'Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"' + }, + 'integratedTerminal.ansiColors.black': { + 'description': nls.localize('integratedTerminal.ansiColors.black', "Black color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#000000' + }, + 'integratedTerminal.ansiColors.red': { + 'description': nls.localize('integratedTerminal.ansiColors.red', "Red color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#cd3131' + }, + 'integratedTerminal.ansiColors.green': { + 'description': nls.localize('integratedTerminal.ansiColors.green', "Green color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#09885a' + }, + 'integratedTerminal.ansiColors.yellow': { + 'description': nls.localize('integratedTerminal.ansiColors.yellow', "Yellow color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#e5e510' + }, + 'integratedTerminal.ansiColors.blue': { + 'description': nls.localize('integratedTerminal.ansiColors.blue', "Blue color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#0451a5' + }, + 'integratedTerminal.ansiColors.magenta': { + 'description': nls.localize('integratedTerminal.ansiColors.magenta', "Magenta color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#bc05bc' + }, + 'integratedTerminal.ansiColors.cyan': { + 'description': nls.localize('integratedTerminal.ansiColors.cyan', "Cyan color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#0598bc' + }, + 'integratedTerminal.ansiColors.white': { + 'description': nls.localize('integratedTerminal.ansiColors.white', "White color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#e5e5e5' + }, + 'integratedTerminal.ansiColors.brightBlack': { + 'description': nls.localize('integratedTerminal.ansiColors.brightBlack', "Bright black color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#000000' + }, + 'integratedTerminal.ansiColors.brightRed': { + 'description': nls.localize('integratedTerminal.ansiColors.brightRed', "Bright red color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#cd3131' + }, + 'integratedTerminal.ansiColors.brightGreen': { + 'description': nls.localize('integratedTerminal.ansiColors.brightGreen', "Bright green color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#09885a' + }, + 'integratedTerminal.ansiColors.brightYellow': { + 'description': nls.localize('integratedTerminal.ansiColors.brightYellow', "Bright yellow color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#e5e510' + }, + 'integratedTerminal.ansiColors.brightBlue': { + 'description': nls.localize('integratedTerminal.ansiColors.brightBlue', "Bright blue color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#0451a5' + }, + 'integratedTerminal.ansiColors.brightMagenta': { + 'description': nls.localize('integratedTerminal.ansiColors.brightMagenta', "Bright magenta color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#bc05bc' + }, + 'integratedTerminal.ansiColors.brightCyan': { + 'description': nls.localize('integratedTerminal.ansiColors.brightCyan', "Bright cyan color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#0598bc' + }, + 'integratedTerminal.ansiColors.brightWhite': { + 'description': nls.localize('integratedTerminal.ansiColors.brightWhite', "Bright white color for terminals that support ANSI escape sequences (format: #rrggbb)"), + 'type': 'string', + 'default': '#e5e5e5' + } + } + }); + + // Register Service + registerSingleton(ITerminalService, TerminalService); + + // Register Output Panel + (Registry.as(panel.Extensions.Panels)).registerPanel(new panel.PanelDescriptor( + 'vs/workbench/parts/terminal/electron-browser/terminalPanel', + 'TerminalPanel', + TERMINAL_PANEL_ID, + nls.localize('terminal', "Terminal"), + 'terminal' + )); + + // Register toggle output action globally + let actionRegistry = Registry.as(ActionExtensions.WorkbenchActions); + // { primary: KeyMod.CtrlCmd | KeyCode.US_BACKTICK } + actionRegistry.registerWorkbenchAction(new SyncActionDescriptor(ToggleTerminalAction, ToggleTerminalAction.ID, ToggleTerminalAction.LABEL), 'View: ' + ToggleTerminalAction.LABEL, nls.localize('viewCategory', "View")); +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts new file mode 100644 index 00000000000..bda26557661 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalActions.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {TPromise} from 'vs/base/common/winjs.base'; +import nls = require('vs/nls'); +import {Action} from 'vs/base/common/actions'; +import {ITerminalService} from 'vs/workbench/parts/terminal/common/terminal'; + +export class ToggleTerminalAction extends Action { + + public static ID = 'workbench.action.terminal.toggleTerminal'; + public static LABEL = nls.localize('toggleTerminal', "(Experimental) Toggle Terminal"); + + constructor( + id: string, label: string, + @ITerminalService private terminalService: ITerminalService + ) { + super(id, label); + } + + public run(event?: any): TPromise { + return this.terminalService.show(); + } +} \ No newline at end of file diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts new file mode 100644 index 00000000000..8f9927186b9 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalPanel.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import termJs = require('term.js'); +import lifecycle = require('vs/base/common/lifecycle'); +import fs = require('fs'); +import DOM = require('vs/base/browser/dom'); +import {fork, Terminal} from 'pty.js'; +import platform = require('vs/base/common/platform'); +import {TPromise} from 'vs/base/common/winjs.base'; +import {Builder, Dimension} from 'vs/base/browser/builder'; +import {IConfigurationService} from 'vs/platform/configuration/common/configuration'; +import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry'; +import {IWorkspaceContextService} from 'vs/platform/workspace/common/workspace'; +import {ITerminalConfiguration, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/common/terminal'; +import {Panel} from 'vs/workbench/browser/panel'; +import {DomScrollableElement} from 'vs/base/browser/ui/scrollbar/scrollableElement'; +import {ScrollbarVisibility} from 'vs/base/browser/ui/scrollbar/scrollableElementOptions'; + +const TERMINAL_CHAR_WIDTH = 8; +const TERMINAL_CHAR_HEIGHT = 18; + +export class TerminalPanel extends Panel { + + private toDispose: lifecycle.IDisposable[]; + private ptyProcess: Terminal; + private parentDomElement: HTMLElement; + private terminal; + private terminalDomElement: HTMLDivElement; + + constructor( + @IConfigurationService private configurationService: IConfigurationService, + @ITelemetryService telemetryService: ITelemetryService, + @IWorkspaceContextService private contextService: IWorkspaceContextService + ) { + super(TERMINAL_PANEL_ID, telemetryService); + this.toDispose = []; + } + + public layout(dimension: Dimension): void { + let cols = Math.floor(this.parentDomElement.offsetWidth / TERMINAL_CHAR_WIDTH); + let rows = Math.floor(this.parentDomElement.offsetHeight / TERMINAL_CHAR_HEIGHT); + this.terminal.resize(cols, rows); + this.ptyProcess.resize(cols, rows); + } + + public create(parent: Builder): TPromise { + super.create(parent); + + this.parentDomElement = parent.getHTMLElement(); + + return this.createTerminal(); + } + + private createTerminal(): TPromise { + return new TPromise(resolve => { + this.parentDomElement.innerHTML = ''; + this.ptyProcess = fork(this.getShell(), [], { + name: fs.existsSync('/usr/share/terminfo/x/xterm-256color') ? 'xterm-256color' : 'xterm', + cwd: this.contextService.getWorkspace() ? this.contextService.getWorkspace().resource.fsPath : process.env.HOME + }); + this.terminalDomElement = document.createElement('div'); + this.parentDomElement.classList.add('integrated-terminal'); + let terminalScrollbar = new DomScrollableElement(this.terminalDomElement, { + canUseTranslate3d: false, + horizontal: ScrollbarVisibility.Hidden, + vertical: ScrollbarVisibility.Auto + }); + this.toDispose.push(terminalScrollbar); + this.terminal = termJs({ + cursorBlink: false // term.js' blinking cursor breaks selection + }); + + this.ptyProcess.on('data', (data) => { + this.terminal.write(data); + }); + this.terminal.on('data', (data) => { + this.ptyProcess.write(data); + return false; + }); + this.ptyProcess.on('exit', (data) => { + this.terminal.destroy(); + // TODO: When multiple terminals are supported this should do something smarter. There is + // also a weird bug here at leasy on Ubuntu 15.10 where the new terminal text does not + // repaint correctly. + this.createTerminal(); + }); + this.toDispose.push(DOM.addDisposableListener(this.parentDomElement, 'mousedown', (event) => { + // Drop selection and focus terminal on Linux to enable middle button paste when click + // occurs on the selection itself. + if (event.which === 2 && platform.isLinux) { + this.focusTerminal(true); + } + })); + this.toDispose.push(DOM.addDisposableListener(this.parentDomElement, 'mouseup', (event) => { + if (event.which !== 3) { + this.focusTerminal(); + } + })); + + this.terminal.open(this.terminalDomElement); + this.parentDomElement.appendChild(terminalScrollbar.getDomNode()); + + let config = this.configurationService.getConfiguration(); + this.terminalDomElement.style.fontFamily = config.integratedTerminal.fontFamily; + this.terminal.colors = this.getTerminalColors(); + resolve(void 0); + }); + } + + private focusTerminal(force?: boolean): void { + let text = window.getSelection().toString(); + if (!text || force) { + this.terminal.focus(); + if (this.terminal._textarea) { + this.terminal._textarea.focus(); + } + } + } + + private getShell(): string { + let config = this.configurationService.getConfiguration(); + if (platform.isWindows) { + return config.integratedTerminal.shell.windows; + } + return config.integratedTerminal.shell.unixLike; + } + + private getTerminalColors(): string[] { + let config = this.configurationService.getConfiguration().integratedTerminal.ansiColors; + let colors = [ + config.black, + config.red, + config.green, + config.yellow, + config.blue, + config.magenta, + config.cyan, + config.white, + config.brightBlack, + config.brightRed, + config.brightGreen, + config.brightYellow, + config.brightBlue, + config.brightMagenta, + config.brightCyan, + config.brightWhite + ]; + return colors; + } + + public dispose(): void { + this.toDispose = lifecycle.dispose(this.toDispose); + super.dispose(); + } +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts new file mode 100644 index 00000000000..b834d580627 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import {TPromise} from 'vs/base/common/winjs.base'; +import {IPanelService} from 'vs/workbench/services/panel/common/panelService'; +import {IPartService} from 'vs/workbench/services/part/common/partService'; +import {ITerminalService, TERMINAL_PANEL_ID} from 'vs/workbench/parts/terminal/common/terminal'; + +export class TerminalService implements ITerminalService { + public serviceId = ITerminalService; + + constructor( + @IPanelService private panelService: IPanelService, + @IPartService private partService: IPartService + ) { + } + + public show(): TPromise { + const panel = this.panelService.getActivePanel(); + if (panel && panel.getId() === TERMINAL_PANEL_ID) { + this.partService.setPanelHidden(true); + + return TPromise.as(null); + } + + return this.panelService.openPanel(TERMINAL_PANEL_ID, true); + } +} \ No newline at end of file diff --git a/src/vs/workbench/workbench.main.js b/src/vs/workbench/workbench.main.js index 9b970c02b4e..45bbb116fc7 100644 --- a/src/vs/workbench/workbench.main.js +++ b/src/vs/workbench/workbench.main.js @@ -58,6 +58,8 @@ define([ 'vs/workbench/parts/output/browser/output.contribution', + 'vs/workbench/parts/terminal/electron-browser/terminal.contribution', + 'vs/workbench/parts/markdown/browser/markdown.contribution', 'vs/workbench/parts/markdown/browser/markdownActions.contribution',