提交 e1dbce60 编写于 作者: D Daniel Imms 提交者: GitHub

Merge pull request #22182 from Microsoft/tyriar/21934

Add hints for terminal links without ctrl/cmd
......@@ -432,7 +432,7 @@
"xterm": {
"version": "2.4.0",
"from": "Tyriar/xterm.js#vscode-release/1.11",
"resolved": "git+https://github.com/Tyriar/xterm.js.git#432393d53661fe5160ef3deb3ba58b77e4ca3baf"
"resolved": "git+https://github.com/Tyriar/xterm.js.git#f9378fe3c635f4dfdee011a140ddb8cd744426eb"
},
"yauzl": {
"version": "2.3.1",
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ITerminalConfigHelper } from 'vs/workbench/parts/terminal/common/terminal';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
export class TerminalWidgetManager {
private _container: HTMLElement;
private _xtermViewport: HTMLElement;
private _messageWidget: MessageWidget;
private _messageListeners: IDisposable[] = [];
constructor(
private _configHelper: ITerminalConfigHelper,
terminalWrapper: HTMLElement
) {
this._container = document.createElement('div');
this._container.classList.add('terminal-widget-overlay');
terminalWrapper.appendChild(this._container);
this._initTerminalHeightWatcher(terminalWrapper);
}
private _initTerminalHeightWatcher(terminalWrapper: HTMLElement) {
// Watch the xterm.js viewport for style changes and do a layout if it changes
this._xtermViewport = <HTMLElement>terminalWrapper.querySelector('.xterm-viewport');
const mutationObserver = new MutationObserver(() => this._refreshHeight());
mutationObserver.observe(this._xtermViewport, { attributes: true, attributeFilter: ['style'] });
}
public showMessage(left: number, top: number, text: string): void {
dispose(this._messageWidget);
this._messageListeners = dispose(this._messageListeners);
this._messageWidget = new MessageWidget(this._container, left, top, text);
}
public closeMessage(): void {
this._messageListeners = dispose(this._messageListeners);
if (this._messageWidget) {
this._messageListeners.push(MessageWidget.fadeOut(this._messageWidget));
}
}
private _refreshHeight(): void {
this._container.style.height = this._xtermViewport.style.height;
}
}
class MessageWidget {
private _domNode: HTMLDivElement;
public get left(): number { return this._left; }
public get top(): number { return this._top; }
public get text(): string { return this._text; }
public get domNode(): HTMLElement { return this._domNode; }
public static fadeOut(messageWidget: MessageWidget): IDisposable {
let handle: number;
const dispose = () => {
messageWidget.dispose();
clearTimeout(handle);
messageWidget.domNode.removeEventListener('animationend', dispose);
};
handle = setTimeout(dispose, 110);
messageWidget.domNode.addEventListener('animationend', dispose);
messageWidget.domNode.classList.add('fadeOut');
return { dispose };
}
constructor(
private _container: HTMLElement,
private _left: number,
private _top: number,
private _text: string
) {
this._domNode = document.createElement('div');
this._domNode.style.position = 'absolute';
this._domNode.style.left = `${_left}px`;
this._domNode.style.bottom = `${_container.offsetHeight - _top}px`;
this._domNode.classList.add('terminal-message-widget', 'fadeIn');
this._domNode.textContent = _text;
this._container.appendChild(this._domNode);
}
public dispose(): void {
if (this.domNode.parentElement === this._container) {
this._container.removeChild(this.domNode);
}
}
}
......@@ -180,7 +180,7 @@ export interface ITerminalInstance {
* added to the DOM.
* @return The ID of the new matcher, this can be used to deregister.
*/
registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number;
registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void): number;
/**
* Deregisters a link matcher if it has been registered.
......
......@@ -33,6 +33,11 @@
top: 0;
}
.monaco-workbench .panel.integrated-terminal .xterm a:not(.xterm-invalid-link) {
/* To support message box sizing */
position: relative;
}
.monaco-workbench .panel.integrated-terminal .terminal-wrapper > div {
height: 100%;
}
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-workbench .terminal-widget-overlay {
position: absolute;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
}
.monaco-workbench .terminal-message-widget {
/* This font list is sourced from a .monaco-shell style */
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif;
font-size: 14px;
line-height: 19px;
padding: 4px 5px;
animation: fadein 100ms linear;
background-color: #F3F3F3;
border: 1px solid #CCC;
}
.vs-dark .monaco-workbench .terminal-message-widget {
background-color: #2D2D30;
border-color: #555;
}
.hc-black .monaco-workbench .terminal-message-widget {
background-color: #0C141F;
}
\ No newline at end of file
......@@ -6,6 +6,7 @@
import 'vs/css!./media/scrollbar';
import 'vs/css!./media/terminal';
import 'vs/css!./media/xterm';
import 'vs/css!./media/widgets';
import * as panel from 'vs/workbench/browser/panel';
import * as platform from 'vs/base/common/platform';
import nls = require('vs/nls');
......
......@@ -23,10 +23,12 @@ import { ITerminalInstance, KEYBINDING_CONTEXT_TERMINAL_TEXT_SELECTED, 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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
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';
import { TerminalWidgetManager } from 'vs/workbench/parts/terminal/browser/terminalWidgetManager';
/** The amount of time to consider terminal errors to be related to the launch */
const LAUNCHING_DURATION = 500;
......@@ -69,6 +71,9 @@ export class TerminalInstance implements ITerminalInstance {
private _cols: number;
private _rows: number;
private _widgetManager: TerminalWidgetManager;
private _linkHandler: TerminalLinkHandler;
public get id(): number { return this._id; }
public get processId(): number { return this._processId; }
public get onDisposed(): Event<ITerminalInstance> { return this._onDisposed.event; }
......@@ -80,7 +85,6 @@ 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,
......@@ -88,7 +92,8 @@ export class TerminalInstance implements ITerminalInstance {
@IMessageService private _messageService: IMessageService,
@IPanelService private _panelService: IPanelService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService,
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IInstantiationService private _instantiationService: IInstantiationService
) {
this._instanceDisposables = [];
this._processDisposables = [];
......@@ -206,8 +211,6 @@ export class TerminalInstance implements ITerminalInstance {
this._xtermElement = document.createElement('div');
this._xterm.open(this._xtermElement);
this._linkHandler.initialize(this._xterm);
this._linkHandler.registerLocalLinkHandler(this._xterm);
this._xterm.attachCustomKeydownHandler((event: KeyboardEvent) => {
// Disable all input if the terminal is exiting
if (this._isExiting) {
......@@ -277,6 +280,9 @@ export class TerminalInstance implements ITerminalInstance {
}));
this._wrapperElement.appendChild(this._xtermElement);
this._widgetManager = new TerminalWidgetManager(this._configHelper, this._wrapperElement);
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, this._widgetManager, this._xterm, platform.platform);
this._linkHandler.registerLocalLinkHandler();
this._container.appendChild(this._wrapperElement);
const computedStyle = window.getComputedStyle(this._container);
......@@ -287,8 +293,8 @@ export class TerminalInstance implements ITerminalInstance {
this.updateConfig();
}
public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void): number {
return this._linkHandler.registerCustomLinkHandler(this._xterm, regex, handler, matchIndex, validationCallback);
public registerLinkMatcher(regex: RegExp, handler: (url: string) => void, matchIndex?: number, validationCallback?: (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void): number {
return this._linkHandler.registerCustomLinkHandler(regex, handler, matchIndex, validationCallback);
}
public deregisterLinkMatcher(linkMatcherId: number): void {
......
......@@ -3,12 +3,14 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as path from 'path';
import * as platform from 'vs/base/common/platform';
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 { TerminalWidgetManager } from 'vs/workbench/parts/terminal/browser/terminalWidgetManager';
import { TPromise } from 'vs/base/common/winjs.base';
const pathPrefix = '(\\.\\.?|\\~)';
......@@ -30,36 +32,38 @@ const CUSTOM_LINK_PRIORITY = -1;
const LOCAL_LINK_PRIORITY = -2;
export type XtermLinkMatcherHandler = (event: MouseEvent, uri: string) => boolean | void;
export type XtermLinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void;
export type XtermLinkMatcherValidationCallback = (uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => void;
export class TerminalLinkHandler {
constructor(
private _platform: Platform,
private _widgetManager: TerminalWidgetManager,
private _xterm: any,
private _platform: platform.Platform,
@IWorkbenchEditorService private _editorService: IWorkbenchEditorService,
@IWorkspaceContextService private _contextService: IWorkspaceContextService
) {
this._xterm.setHypertextLinkHandler(this._wrapLinkHandler(() => true));
this._xterm.setHypertextValidationCallback((uri: string, element: HTMLElement, callback: (isValid: boolean) => void) => {
this._validateWebLink(uri, element, callback);
});
}
public initialize(xterm: any) {
xterm.attachHypertextLinkHandler(this._wrapLinkHandler(() => true));
}
public registerCustomLinkHandler(xterm: any, regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
return xterm.registerLinkMatcher(regex, this._wrapLinkHandler(handler), {
public registerCustomLinkHandler(regex: RegExp, handler: (uri: string) => void, matchIndex?: number, validationCallback?: XtermLinkMatcherValidationCallback): number {
return this._xterm.registerLinkMatcher(regex, this._wrapLinkHandler(handler), {
matchIndex,
validationCallback,
priority: CUSTOM_LINK_PRIORITY
});
}
public registerLocalLinkHandler(xterm: any): number {
public registerLocalLinkHandler(): number {
const wrappedHandler = this._wrapLinkHandler(url => {
this._handleLocalLink(url);
return;
});
return xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
return this._xterm.registerLinkMatcher(this._localLinkRegex, wrappedHandler, {
matchIndex: 1,
validationCallback: (link: string, callback: (isValid: boolean) => void) => this._validateLocalLink(link, callback),
validationCallback: (link: string, element: HTMLElement, callback: (isValid: boolean) => void) => this._validateLocalLink(link, element, callback),
priority: LOCAL_LINK_PRIORITY
});
}
......@@ -67,8 +71,7 @@ export class TerminalLinkHandler {
private _wrapLinkHandler(handler: (uri: string) => boolean | void): XtermLinkMatcherHandler {
return (event: MouseEvent, uri: string) => {
// Require ctrl/cmd on click
if (this._platform === Platform.Mac ? !event.metaKey : !event.ctrlKey) {
// TODO: Show hint on fail
if (this._platform === platform.Platform.Mac ? !event.metaKey : !event.ctrlKey) {
event.preventDefault();
return false;
}
......@@ -77,7 +80,7 @@ export class TerminalLinkHandler {
}
protected get _localLinkRegex(): RegExp {
if (this._platform === Platform.Windows) {
if (this._platform === platform.Platform.Windows) {
return WINDOWS_LOCAL_LINK_REGEX;
}
return UNIX_LIKE_LOCAL_LINK_REGEX;
......@@ -93,14 +96,41 @@ export class TerminalLinkHandler {
});
}
private _validateLocalLink(link: string, callback: (isValid: boolean) => void): void {
private _validateLocalLink(link: string, element: HTMLElement, callback: (isValid: boolean) => void): void {
this._resolvePath(link).then(resolvedLink => {
if (resolvedLink) {
this._addTooltipEventListeners(element);
}
callback(!!resolvedLink);
});
}
private _validateWebLink(link: string, element: HTMLElement, callback: (isValid: boolean) => void): void {
this._addTooltipEventListeners(element);
callback(true);
}
private _addTooltipEventListeners(element: HTMLElement) {
let timeout = null;
element.addEventListener('mouseenter', () => {
timeout = setTimeout(() => {
let message: string;
if (platform.isMacintosh) {
message = nls.localize('terminalLinkHandler.followLinkCmd', 'Cmd + click to follow link');
} else {
message = nls.localize('terminalLinkHandler.followLinkCtrl', 'Ctrl + click to follow link');
}
this._widgetManager.showMessage(element.offsetLeft, element.offsetTop, message);
}, 500);
});
element.addEventListener('mouseleave', () => {
clearTimeout(timeout);
this._widgetManager.closeMessage();
});
}
private _resolvePath(link: string): TPromise<string> {
if (this._platform === Platform.Windows) {
if (this._platform === platform.Platform.Windows) {
// Resolve ~ -> %HOMEDRIVE%\%HOMEPATH%
if (link.charAt(0) === '~') {
if (!process.env.HOMEDRIVE || !process.env.HOMEPATH) {
......
......@@ -17,11 +17,9 @@ import { ITerminalInstance, ITerminalService, IShellLaunchConfig, ITerminalConfi
import { TerminalService as AbstractTerminalService } from 'vs/workbench/parts/terminal/common/terminalService';
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 extends AbstractTerminalService implements ITerminalService {
private _configHelper: TerminalConfigHelper;
private _linkHandler: TerminalLinkHandler;
public get configHelper(): ITerminalConfigHelper { return this._configHelper; };
......@@ -37,14 +35,12 @@ export class TerminalService extends AbstractTerminalService implements ITermina
super(_contextKeyService, _configurationService, _panelService, _partService, _lifecycleService);
this._configHelper = this._instantiationService.createInstance(TerminalConfigHelper, platform.platform);
this._linkHandler = this._instantiationService.createInstance(TerminalLinkHandler, platform.platform);
}
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));
......
......@@ -15,10 +15,15 @@ class TestTerminalLinkHandler extends TerminalLinkHandler {
}
}
class TestXterm {
public setHypertextLinkHandler() { }
public setHypertextValidationCallback() { }
}
suite('Workbench - TerminalLinkHandler', () => {
suite('localLinkRegex', () => {
test('Windows', () => {
const regex = new TestTerminalLinkHandler(Platform.Windows, null, null).localLinkRegex;
const regex = new TestTerminalLinkHandler(null, new TestXterm(), Platform.Windows, null, null).localLinkRegex;
function testLink(link: string) {
assert.equal(` ${link} `.match(regex)[1], link);
assert.equal(`:${link}:`.match(regex)[1], link);
......@@ -39,7 +44,7 @@ suite('Workbench - TerminalLinkHandler', () => {
});
test('Linux', () => {
const regex = new TestTerminalLinkHandler(Platform.Linux, null, null).localLinkRegex;
const regex = new TestTerminalLinkHandler(null, new TestXterm(), Platform.Linux, null, null).localLinkRegex;
function testLink(link: string) {
assert.equal(` ${link} `.match(regex)[1], link);
assert.equal(`:${link}:`.match(regex)[1], link);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册