Skip to content

Commit

Permalink
Merge pull request #20582 from Microsoft/tyriar/7321
Browse files Browse the repository at this point in the history
Terminal links
  • Loading branch information
Tyriar authored Feb 14, 2017
2 parents 62383be + 1a76f05 commit 27c5f64
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 5 deletions.
4 changes: 2 additions & 2 deletions npm-shrinkwrap.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/vs/workbench/parts/terminal/electron-browser/media/xterm.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@
* 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';
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';
Expand All @@ -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;
Expand Down Expand Up @@ -74,13 +76,15 @@ export class TerminalInstance implements ITerminalInstance {
public constructor(
private _terminalFocusContextKey: IContextKey<boolean>,
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 = [];
Expand Down Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> {
if (this._platform === Platform.Windows) {
return this._handleWindowsLocalLink(link);
}
return this._handleUnixLikeLocalLink(link);
}

private _handleUnixLikeLocalLink(link: string): TPromise<void> {
// 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<void> {
// 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<void> {
// Resolve workspace path . / .. -> <path>/. / <path/..
if (link.charAt(0) === '.') {
if (!this._contextService.hasWorkspace) {
// Abort if no workspace is open
return TPromise.as(void 0);
}
link = path.join(this._contextService.getWorkspace().resource.fsPath, link);
}

// Clean up the path
const resource = Uri.file(path.normalize(path.resolve(link)));

// Open an editor if the path exists
return pfs.fileExists(link).then(isFile => {
if (!isFile) {
return void 0;
}
return this._editorService.openEditor({ resource }).then(() => void 0);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
private _onInstanceDisposed: Emitter<ITerminalInstance>;
private _onInstanceProcessIdReady: Emitter<ITerminalInstance>;
Expand Down Expand Up @@ -57,14 +59,16 @@ export class TerminalService implements ITerminalService {

this._configurationService.onDidUpdateConfiguration(() => this.updateConfig());
this._terminalFocusContextKey = KEYBINDING_CONTEXT_TERMINAL_FOCUS.bindTo(this._contextKeyService);
this._configHelper = <TerminalConfigHelper>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); });
}

public createInstance(shell: IShellLaunchConfig = {}): ITerminalInstance {
let terminalInstance = this._instantiationService.createInstance(TerminalInstance,
this._terminalFocusContextKey,
this._configHelper,
this._linkHandler,
this._terminalContainer,
shell);
terminalInstance.addDisposable(terminalInstance.onTitleChanged(this._onInstanceTitleChanged.fire, this._onInstanceTitleChanged));
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
});

0 comments on commit 27c5f64

Please sign in to comment.