diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 9659a06a58c3d52be92aac6dd974f8f929155aff..9faed92d5a39db7ba29dc4f932d67b3c7284fbe8 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -425,9 +425,9 @@ "resolved": "https://registry.npmjs.org/winreg/-/winreg-1.2.0.tgz" }, "xterm": { - "version": "2.2.3", + "version": "2.3.0", "from": "Tyriar/xterm.js#vscode-release/1.10", - "resolved": "git+https://github.com/Tyriar/xterm.js.git#90cd66bf353b86ad52d7b650760d8d879dd1c7b8" + "resolved": "git+https://github.com/Tyriar/xterm.js.git#5513303451202b0135601a2f026602ed391b3906" }, "yauzl": { "version": "2.3.1", diff --git a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css index 9577483746262cfc73aa98dd57f24f4d90e1188a..5681459545495e6cc002dddc6feeb094e505c429 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css +++ b/src/vs/workbench/parts/terminal/electron-browser/media/xterm.css @@ -58,6 +58,16 @@ opacity: 0 !important; } +.monaco-workbench .panel.integrated-terminal .xterm a { + color: inherit; + text-decoration: none; +} + +.monaco-workbench .panel.integrated-terminal .xterm a:hover { + cursor: pointer; + text-decoration: underline; +} + .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .reverse-video, .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar):focus .reverse-video { color: #CCC; } .vs-dark .monaco-workbench .panel.integrated-terminal .xterm:not(.xterm-cursor-style-underline):not(.xterm-cursor-style-bar).focus .reverse-video, diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts index f5c87ee09d88f36f39a03aed52342adcc6c12769..ab7cba198388e6ba7298717d5cce683668123b39 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalInstance.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as path from 'path'; import DOM = require('vs/base/browser/dom'); import Event, { Emitter } from 'vs/base/common/event'; import URI from 'vs/base/common/uri'; @@ -10,7 +11,6 @@ import cp = require('child_process'); import lifecycle = require('vs/base/common/lifecycle'); import nls = require('vs/nls'); import os = require('os'); -import path = require('path'); import platform = require('vs/base/common/platform'); import xterm = require('xterm'); import { Dimension } from 'vs/base/browser/builder'; @@ -22,9 +22,11 @@ import { IStringDictionary } from 'vs/base/common/collections'; import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, TERMINAL_PANEL_ID, IShellLaunchConfig } from 'vs/workbench/parts/terminal/common/terminal'; import { ITerminalProcessFactory } from 'vs/workbench/parts/terminal/electron-browser/terminal'; import { IWorkspace, IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { TabFocus } from 'vs/editor/common/config/commonEditorConfig'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; +import { TerminalLinkHandler } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler'; /** The amount of time to consider terminal errors to be related to the launch */ const LAUNCHING_DURATION = 500; @@ -74,13 +76,15 @@ export class TerminalInstance implements ITerminalInstance { public constructor( private _terminalFocusContextKey: IContextKey, private _configHelper: TerminalConfigHelper, + private _linkHandler: TerminalLinkHandler, private _container: HTMLElement, private _shellLaunchConfig: IShellLaunchConfig, @IContextKeyService private _contextKeyService: IContextKeyService, @IKeybindingService private _keybindingService: IKeybindingService, @IMessageService private _messageService: IMessageService, @IPanelService private _panelService: IPanelService, - @IWorkspaceContextService private _contextService: IWorkspaceContextService + @IWorkspaceContextService private _contextService: IWorkspaceContextService, + @IWorkbenchEditorService private _editorService: IWorkbenchEditorService ) { this._instanceDisposables = []; this._processDisposables = []; @@ -139,6 +143,7 @@ export class TerminalInstance implements ITerminalInstance { this._xtermElement = document.createElement('div'); this._xterm.open(this._xtermElement); + this._xterm.registerLinkMatcher(this._linkHandler.localLinkRegex, (url) => this._linkHandler.handleLocalLink(url), 1); this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => { // Disable all input if the terminal is exiting if (this._isExiting) { diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..ebf09bf37b61b4a47d0521f9e10fec1a625c4597 --- /dev/null +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalLinkHandler.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as path from 'path'; +import * as pfs from 'vs/base/node/pfs'; +import Uri from 'vs/base/common/uri'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; +import { Platform } from 'vs/base/common/platform'; +import { TPromise } from 'vs/base/common/winjs.base'; + +const pathPrefix = '(\\.\\.?|\\~)'; +const pathSeparatorClause = '\\/'; +const excludedPathCharactersClause = '[^\\0\\s!$`&*()+\'":;]'; // '":; are allowed in paths but they are often separators so ignore them +const escapedExcludedPathCharactersClause = '(\\\\s|\\\\!|\\\\$|\\\\`|\\\\&|\\\\*|(|)|\\+)'; +/** A regex that matches paths in the form /path, ~/path, ./path, ../path */ +const UNIX_LIKE_LOCAL_LINK_REGEX = new RegExp('(' + pathPrefix + '?(' + pathSeparatorClause + '(' + excludedPathCharactersClause + '|' + escapedExcludedPathCharactersClause + ')+)+)'); + +const winPathPrefix = '([a-zA-Z]:|\\.\\.?|\\~)'; +const winPathSeparatorClause = '(\\\\|\\/)'; +const winExcludedPathCharactersClause = '[^\\0<>\\?\\|\\/\\s!$`&*()+\'":;]'; +/** A regex that matches paths in the form c:\path, ~\path, .\path */ +const WINDOWS_LOCAL_LINK_REGEX = new RegExp('(' + winPathPrefix + '?(' + winPathSeparatorClause + '(' + winExcludedPathCharactersClause + ')+)+)'); + +export class TerminalLinkHandler { + constructor( + private _platform: Platform, + @IWorkbenchEditorService private _editorService: IWorkbenchEditorService, + @IWorkspaceContextService private _contextService: IWorkspaceContextService + ) { + } + + public get localLinkRegex(): RegExp { + if (this._platform === Platform.Windows) { + return WINDOWS_LOCAL_LINK_REGEX; + } + return UNIX_LIKE_LOCAL_LINK_REGEX; + } + + public handleLocalLink(link: string): TPromise { + if (this._platform === Platform.Windows) { + return this._handleWindowsLocalLink(link); + } + return this._handleUnixLikeLocalLink(link); + } + + private _handleUnixLikeLocalLink(link: string): TPromise { + // Resolve ~ -> $HOME + if (link.charAt(0) === '~') { + if (!process.env.HOME) { + return TPromise.as(void 0); + } + link = process.env.HOME + link.substring(1); + } + return this._handleCommonLocalLink(link); + } + + private _handleWindowsLocalLink(link: string): TPromise { + // Resolve ~ -> %HOMEDRIVE%\%HOMEPATH% + if (link.charAt(0) === '~') { + if (!process.env.HOMEDRIVE || !process.env.HOMEPATH) { + return TPromise.as(void 0); + } + link = `${process.env.HOMEDRIVE}\\${process.env.HOMEPATH + link.substring(1)}`; + } + return this._handleCommonLocalLink(link); + } + + private _handleCommonLocalLink(link: string): TPromise { + // Resolve workspace path . / .. -> /. / { + if (!isFile) { + return void 0; + } + return this._editorService.openEditor({ resource }).then(() => void 0); + }); + } +} diff --git a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts index 5f7640cd8e7f695eb4c8c3f07160f91a4a924d40..0b418320fbde3088f9b092656f75a4da4189a961 100644 --- a/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts +++ b/src/vs/workbench/parts/terminal/electron-browser/terminalService.ts @@ -15,12 +15,14 @@ import { ITerminalInstance, ITerminalService, IShellLaunchConfig, KEYBINDING_CON import { TPromise } from 'vs/base/common/winjs.base'; import { TerminalConfigHelper } from 'vs/workbench/parts/terminal/electron-browser/terminalConfigHelper'; import { TerminalInstance } from 'vs/workbench/parts/terminal/electron-browser/terminalInstance'; +import { TerminalLinkHandler } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler'; export class TerminalService implements ITerminalService { public _serviceBrand: any; private _activeTerminalInstanceIndex: number; private _configHelper: TerminalConfigHelper; + private _linkHandler: TerminalLinkHandler; private _onActiveInstanceChanged: Emitter; private _onInstanceDisposed: Emitter; private _onInstanceProcessIdReady: Emitter; @@ -57,7 +59,8 @@ export class TerminalService implements ITerminalService { this._configurationService.onDidUpdateConfiguration(() => this.updateConfig()); this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService); - this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, platform.platform); + this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, platform.platform); + this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, platform.platform); this.onInstanceDisposed((terminalInstance) => { this._removeInstance(terminalInstance); }); } @@ -65,6 +68,7 @@ export class TerminalService implements ITerminalService { let terminalInstance = this._instantiationService.createInstance(TerminalInstance, this._terminalFocusContextKey, this._configHelper, + this._linkHandler, this._terminalContainer, shell); terminalInstance.addDisposable(terminalInstance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged)); diff --git a/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts new file mode 100644 index 0000000000000000000000000000000000000000..4b4c5ef51187effb67a7f99fc3c02c914b21d396 --- /dev/null +++ b/src/vs/workbench/parts/terminal/test/electron-browser/terminalLinkHandler.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as assert from 'assert'; +import { Platform } from 'vs/base/common/platform'; +import { TerminalLinkHandler } from 'vs/workbench/parts/terminal/electron-browser/terminalLinkHandler'; + +suite('Workbench - TerminalLinkHandler', () => { + suite('localLinkRegex', () => { + test('Windows', () => { + const regex = new TerminalLinkHandler(Platform.Windows, null, null).localLinkRegex; + function testLink(link: string) { + assert.equal(` ${link} `.match(regex)[1], link); + assert.equal(`:${link}:`.match(regex)[1], link); + assert.equal(`;${link};`.match(regex)[1], link); + assert.equal(`(${link})`.match(regex)[1], link); + } + testLink('c:\\foo'); + testLink('c:/foo'); + testLink('.\\foo'); + testLink('./foo'); + testLink('..\\foo'); + testLink('../foo'); + testLink('~\\foo'); + testLink('~/foo'); + testLink('c:/a/long/path'); + testLink('c:\\a\\long\\path'); + testLink('c:\\mixed/slash\\path'); + }); + + test('Linux', () => { + const regex = new TerminalLinkHandler(Platform.Linux, null, null).localLinkRegex; + function testLink(link: string) { + assert.equal(` ${link} `.match(regex)[1], link); + assert.equal(`:${link}:`.match(regex)[1], link); + assert.equal(`;${link};`.match(regex)[1], link); + assert.equal(`(${link})`.match(regex)[1], link); + } + testLink('/foo'); + testLink('~/foo'); + testLink('./foo'); + testLink('../foo'); + testLink('/a/long/path'); + }); + }); +}); \ No newline at end of file