/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import { IAction } from 'vs/base/common/actions'; import { isFullWidthCharacter } from 'vs/base/common/strings'; import uri from 'vs/base/common/uri'; import { isMacintosh } from 'vs/base/common/platform'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { IActionItem, Separator } from 'vs/base/browser/ui/actionbar/actionbar'; import * as dom from 'vs/base/browser/dom'; import * as errors from 'vs/base/common/errors'; import severity from 'vs/base/common/severity'; import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; import { ITree, IAccessibilityProvider, IDataSource, IRenderer } from 'vs/base/parts/tree/browser/tree'; import { IActionProvider } from 'vs/base/parts/tree/browser/actionsRenderer'; import { ICancelableEvent } from 'vs/base/parts/tree/browser/treeDefaults'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import * as debug from 'vs/workbench/parts/debug/common/debug'; import { Model, KeyValueOutputElement, Expression, ValueOutputElement, Variable } from 'vs/workbench/parts/debug/common/debugModel'; import { renderVariable, renderExpressionValue, IVariableTemplateData, BaseDebugController } from 'vs/workbench/parts/debug/electron-browser/debugViewer'; import { AddToWatchExpressionsAction, ClearReplAction } from 'vs/workbench/parts/debug/browser/debugActions'; import { CopyAction } from 'vs/workbench/parts/debug/electron-browser/electronDebugActions'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace'; const $ = dom.$; export class ReplExpressionsDataSource implements IDataSource { constructor(private debugService: debug.IDebugService) { // noop } public getId(tree: ITree, element: any): string { return element.getId(); } public hasChildren(tree: ITree, element: any): boolean { return element instanceof Model || element.reference > 0 || (element instanceof KeyValueOutputElement && element.getChildren().length > 0); } public getChildren(tree: ITree, element: any): TPromise { if (element instanceof Model) { return TPromise.as(element.getReplElements()); } if (element instanceof KeyValueOutputElement) { return TPromise.as(element.getChildren()); } if (element instanceof ValueOutputElement) { return TPromise.as(null); } return (element).getChildren(this.debugService); } public getParent(tree: ITree, element: any): TPromise { return TPromise.as(null); } } interface IInputOutputPairTemplateData { input: HTMLElement; output: HTMLElement; value: HTMLElement; annotation: HTMLElement; } interface IValueOutputTemplateData { container: HTMLElement; counter: HTMLElement; value: HTMLElement; } interface IKeyValueOutputTemplateData { container: HTMLElement; expression: HTMLElement; key: HTMLElement; value: HTMLElement; annotation: HTMLElement; } export class ReplExpressionsRenderer implements IRenderer { private static VARIABLE_TEMPLATE_ID = 'variable'; private static INPUT_OUTPUT_PAIR_TEMPLATE_ID = 'inputOutputPair'; private static VALUE_OUTPUT_TEMPLATE_ID = 'outputValue'; private static KEY_VALUE_OUTPUT_TEMPLATE_ID = 'outputKeyValue'; private static FILE_LOCATION_PATTERNS: RegExp[] = [ // group 0: the full thing :) // group 1: absolute path // group 2: the root slash/drive letter, if present // group 3: line number // group 4: column number // eg: at Context. (c:\Users\someone\Desktop\mocha-runner\test\test.js:26:11) /(?:at |^|[\(<\'\"\[])(?:file:\/\/)?((?:(\/|[a-zA-Z]:)|[^\(\)<>\'\"\[\]:\s]+)(?:[\\/][^\(\)<>\'\"\[\]:]*)?):(\d+)(?::(\d+))?(?:$|[\)>\'\"\]])/ ]; private static LINE_HEIGHT_PX = 18; private width: number; private characterWidth: number; constructor( @IWorkbenchEditorService private editorService: IWorkbenchEditorService, @IWorkspaceContextService private contextService: IWorkspaceContextService ) { // noop } public getHeight(tree: ITree, element: any): number { return this.getHeightForString(element.value) + (element instanceof Expression ? this.getHeightForString(element.name) : 0); } private getHeightForString(s: string): number { if (!s || !s.length || !this.width || this.width <= 0 || !this.characterWidth || this.characterWidth <= 0) { return ReplExpressionsRenderer.LINE_HEIGHT_PX; } const lines = s.trim().split(/\r\n|\r|\n/g); const numLines = lines.reduce((lineCount: number, line: string) => { let lineLength = 0; for (let i = 0; i < line.length; i++) { lineLength += isFullWidthCharacter(line.charCodeAt(i)) ? 2 : 1; } return lineCount + Math.floor(lineLength * this.characterWidth / this.width); }, lines.length); return ReplExpressionsRenderer.LINE_HEIGHT_PX * numLines; } public setWidth(fullWidth: number, characterWidth: number): void { this.width = fullWidth; this.characterWidth = characterWidth; } public getTemplateId(tree: ITree, element: any): string { if (element instanceof Variable) { return ReplExpressionsRenderer.VARIABLE_TEMPLATE_ID; } if (element instanceof Expression) { return ReplExpressionsRenderer.INPUT_OUTPUT_PAIR_TEMPLATE_ID; } if (element instanceof ValueOutputElement) { return ReplExpressionsRenderer.VALUE_OUTPUT_TEMPLATE_ID; } if (element instanceof KeyValueOutputElement) { return ReplExpressionsRenderer.KEY_VALUE_OUTPUT_TEMPLATE_ID; } return null; } public renderTemplate(tree: ITree, templateId: string, container: HTMLElement): any { if (templateId === ReplExpressionsRenderer.VARIABLE_TEMPLATE_ID) { let data: IVariableTemplateData = Object.create(null); data.expression = dom.append(container, $('.expression')); data.name = dom.append(data.expression, $('span.name')); data.value = dom.append(data.expression, $('span.value')); return data; } if (templateId === ReplExpressionsRenderer.INPUT_OUTPUT_PAIR_TEMPLATE_ID) { let data: IInputOutputPairTemplateData = Object.create(null); dom.addClass(container, 'input-output-pair'); data.input = dom.append(container, $('.input.expression')); data.output = dom.append(container, $('.output.expression')); data.value = dom.append(data.output, $('span.value')); data.annotation = dom.append(data.output, $('span')); return data; } if (templateId === ReplExpressionsRenderer.VALUE_OUTPUT_TEMPLATE_ID) { let data: IValueOutputTemplateData = Object.create(null); dom.addClass(container, 'output'); let expression = dom.append(container, $('.output.expression')); data.container = container; data.counter = dom.append(expression, $('div.counter')); data.value = dom.append(expression, $('span.value')); return data; } if (templateId === ReplExpressionsRenderer.KEY_VALUE_OUTPUT_TEMPLATE_ID) { let data: IKeyValueOutputTemplateData = Object.create(null); dom.addClass(container, 'output'); data.container = container; data.expression = dom.append(container, $('.output.expression')); data.key = dom.append(data.expression, $('span.name')); data.value = dom.append(data.expression, $('span.value')); data.annotation = dom.append(data.expression, $('span')); return data; } } public renderElement(tree: ITree, element: any, templateId: string, templateData: any): void { if (templateId === ReplExpressionsRenderer.VARIABLE_TEMPLATE_ID) { renderVariable(tree, element, templateData, false); } else if (templateId === ReplExpressionsRenderer.INPUT_OUTPUT_PAIR_TEMPLATE_ID) { this.renderInputOutputPair(tree, element, templateData); } else if (templateId === ReplExpressionsRenderer.VALUE_OUTPUT_TEMPLATE_ID) { this.renderOutputValue(element, templateData); } else if (templateId === ReplExpressionsRenderer.KEY_VALUE_OUTPUT_TEMPLATE_ID) { this.renderOutputKeyValue(tree, element, templateData); } } private renderInputOutputPair(tree: ITree, expression: debug.IExpression, templateData: IInputOutputPairTemplateData): void { templateData.input.textContent = expression.name; renderExpressionValue(expression, templateData.value, false); if (expression.hasChildren) { templateData.annotation.className = 'annotation octicon octicon-info'; templateData.annotation.title = nls.localize('stateCapture', "Object state is captured from first evaluation"); } } private renderOutputValue(output: ValueOutputElement, templateData: IValueOutputTemplateData): void { // counter if (output.counter > 1) { templateData.counter.textContent = String(output.counter); templateData.counter.className = (output.severity === severity.Warning) ? 'counter warn' : (output.severity === severity.Error) ? 'counter error' : 'counter info'; } else { templateData.counter.textContent = ''; templateData.counter.className = 'counter'; } // value dom.clearNode(templateData.value); let result = this.handleANSIOutput(output.value); if (typeof result === 'string') { templateData.value.textContent = result; } else { templateData.value.appendChild(result); } templateData.value.className = (output.severity === severity.Warning) ? 'warn' : (output.severity === severity.Error) ? 'error' : 'info'; } private handleANSIOutput(text: string): HTMLElement | string { let tokensContainer: HTMLSpanElement; let currentToken: HTMLSpanElement; let buffer: string = ''; for (let i = 0, len = text.length; i < len; i++) { // start of ANSI escape sequence (see http://ascii-table.com/ansi-escape-sequences.php) if (text.charCodeAt(i) === 27) { let index = i; let chr = (++index < len ? text.charAt(index) : null); if (chr && chr === '[') { let code: string = null; chr = (++index < len ? text.charAt(index) : null); if (chr && chr >= '0' && chr <= '9') { code = chr; chr = (++index < len ? text.charAt(index) : null); } if (chr && chr >= '0' && chr <= '9') { code += chr; chr = (++index < len ? text.charAt(index) : null); } if (code === null) { code = '0'; } if (chr === 'm') { // set text color/mode. // only respect text-foreground ranges and ignore the values for "black" & "white" because those // only make sense in combination with text-background ranges which we currently not support let parsedMode = parseInt(code, 10); let token = document.createElement('span'); if ((parsedMode >= 30 && parsedMode <= 37) || (parsedMode >= 90 && parsedMode <= 97)) { token.className = 'code' + parsedMode; } else if (parsedMode === 1) { token.className = 'code-bold'; } // we need a tokens container now if (!tokensContainer) { tokensContainer = document.createElement('span'); } // flush text buffer if we have any if (buffer) { this.insert(this.handleLinks(buffer), currentToken || tokensContainer); buffer = ''; } currentToken = token; tokensContainer.appendChild(token); i = index; } } } // normal text else { buffer += text[i]; } } // flush remaining text buffer if we have any if (buffer) { let res = this.handleLinks(buffer); if (typeof res !== 'string' || currentToken) { if (!tokensContainer) { tokensContainer = document.createElement('span'); } this.insert(res, currentToken || tokensContainer); } } return tokensContainer || buffer; } private insert(arg: HTMLElement | string, target: HTMLElement): void { if (typeof arg === 'string') { target.textContent = arg; } else { target.appendChild(arg); } } private handleLinks(text: string): HTMLElement | string { let linkContainer: HTMLElement; for (let pattern of ReplExpressionsRenderer.FILE_LOCATION_PATTERNS) { pattern.lastIndex = 0; // the holy grail of software development const match = pattern.exec(text); let resource: uri = null; try { // If root slash / drive letter is present, resolve relative path resource = match && (match[2] ? uri.file(match[1]) : this.contextService.toResource(match[1])); } catch (e) { } if (resource) { linkContainer = document.createElement('span'); let textBeforeLink = text.substr(0, match.index); if (textBeforeLink) { let span = document.createElement('span'); span.textContent = textBeforeLink; linkContainer.appendChild(span); } const link = document.createElement('a'); link.textContent = text.substr(match.index, match[0].length); link.title = isMacintosh ? nls.localize('fileLinkMac', "Click to follow (Cmd + click opens to the side)") : nls.localize('fileLink', "Click to follow (Ctrl + click opens to the side)"); linkContainer.appendChild(link); link.onclick = (e) => this.onLinkClick(new StandardMouseEvent(e), resource, Number(match[3]), match[4] && Number(match[4])); let textAfterLink = text.substr(match.index + match[0].length); if (textAfterLink) { let span = document.createElement('span'); span.textContent = textAfterLink; linkContainer.appendChild(span); } break; // support one link per line for now } } return linkContainer || text; } private onLinkClick(event: IMouseEvent, resource: uri, line: number, column: number = 0): void { const selection = window.getSelection(); if (selection.type === 'Range') { return; // do not navigate when user is selecting } event.preventDefault(); this.editorService.openEditor({ resource, options: { selection: { startLineNumber: line, startColumn: column } } }, event.ctrlKey || event.metaKey).done(null, errors.onUnexpectedError); } private renderOutputKeyValue(tree: ITree, output: KeyValueOutputElement, templateData: IKeyValueOutputTemplateData): void { // key if (output.key) { templateData.key.textContent = `${output.key}:`; } else { templateData.key.textContent = ''; } // value renderExpressionValue(output.value, templateData.value, false); // annotation if any if (output.annotation) { templateData.annotation.className = 'annotation octicon octicon-info'; templateData.annotation.title = output.annotation; } else { templateData.annotation.className = ''; templateData.annotation.title = ''; } } public disposeTemplate(tree: ITree, templateId: string, templateData: any): void { // noop } } export class ReplExpressionsAccessibilityProvider implements IAccessibilityProvider { public getAriaLabel(tree: ITree, element: any): string { if (element instanceof Variable) { return nls.localize('replVariableAriaLabel', "Variable {0} has value {1}, read eval print loop, debug", (element).name, (element).value); } if (element instanceof Expression) { return nls.localize('replExpressionAriaLabel', "Expression {0} has value {1}, read eval print loop, debug", (element).name, (element).value); } if (element instanceof ValueOutputElement) { return nls.localize('replValueOutputAriaLabel', "{0}, read eval print loop, debug", (element).value); } if (element instanceof KeyValueOutputElement) { return nls.localize('replKeyValueOutputAriaLabel', "Output variable {0} has value {1}, read eval print loop, debug", (element).key, (element).value); } return null; } } export class ReplExpressionsActionProvider implements IActionProvider { constructor(private instantiationService: IInstantiationService) { // noop } public hasActions(tree: ITree, element: any): boolean { return false; } public getActions(tree: ITree, element: any): TPromise { return TPromise.as([]); } public hasSecondaryActions(tree: ITree, element: any): boolean { return true; } public getSecondaryActions(tree: ITree, element: any): TPromise { const actions: IAction[] = []; if (element instanceof Variable || element instanceof Expression) { actions.push(this.instantiationService.createInstance(AddToWatchExpressionsAction, AddToWatchExpressionsAction.ID, AddToWatchExpressionsAction.LABEL, element)); actions.push(new Separator()); } actions.push(new CopyAction(CopyAction.ID, CopyAction.LABEL)); actions.push(this.instantiationService.createInstance(ClearReplAction, ClearReplAction.ID, ClearReplAction.LABEL)); return TPromise.as(actions); } public getActionItem(tree: ITree, element: any, action: IAction): IActionItem { return null; } } export class ReplExpressionsController extends BaseDebugController { private lastSelectedString: string = null; constructor( debugService: debug.IDebugService, contextMenuService: IContextMenuService, actionProvider: IActionProvider, private replInput: ICodeEditor, focusOnContextMenu = true ) { super(debugService, contextMenuService, actionProvider, focusOnContextMenu); } protected onLeftClick(tree: ITree, element: any, eventish: ICancelableEvent, origin: string = 'mouse'): boolean { const mouseEvent = eventish; // input and output are one element in the tree => we only expand if the user clicked on the output. if ((element.reference > 0 || (element instanceof KeyValueOutputElement && element.getChildren().length > 0)) && mouseEvent.target.className.indexOf('input expression') === -1) { super.onLeftClick(tree, element, eventish, origin); tree.clearFocus(); tree.deselect(element); } const selection = window.getSelection(); if (selection.type !== 'Range' || this.lastSelectedString === selection.toString()) { // only focus the input if the user is not currently selecting. this.replInput.focus(); } this.lastSelectedString = selection.toString(); return true; } protected onDown(tree: ITree, event: IKeyboardEvent): boolean { if (tree.getFocus()) { return super.onDown(tree, event); } const payload = { origin: 'keyboard', originalEvent: event }; tree.focusLast(payload); tree.reveal(tree.getFocus()).done(null, errors.onUnexpectedError); return true; } }