Files
vscode/src/vs/workbench/contrib/debug/browser/baseDebugView.ts
T

236 lines
9.0 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { IExpression, IDebugService, IExpressionContainer } from 'vs/workbench/contrib/debug/common/debug';
import { Expression, Variable, ExpressionContainer } from 'vs/workbench/contrib/debug/common/debugModel';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IInputValidationOptions, InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
import { ITreeRenderer, ITreeNode } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachInputBoxStyler } from 'vs/platform/theme/common/styler';
import { KeyCode } from 'vs/base/common/keyCodes';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { HighlightedLabel, IHighlight } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { FuzzyScore, createMatches } from 'vs/base/common/filters';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { ReplEvaluationResult } from 'vs/workbench/contrib/debug/common/replModel';
export const MAX_VALUE_RENDER_LENGTH_IN_VIEWLET = 1024;
export const twistiePixels = 20;
const booleanRegex = /^true|false$/i;
const stringRegex = /^(['"]).*\1$/;
const $ = dom.$;
export interface IRenderValueOptions {
preserveWhitespace?: boolean;
showChanged?: boolean;
maxValueLength?: number;
showHover?: boolean;
colorize?: boolean;
linkDetector?: LinkDetector;
}
export interface IVariableTemplateData {
expression: HTMLElement;
name: HTMLElement;
value: HTMLElement;
label: HighlightedLabel;
}
export function renderViewTree(container: HTMLElement): HTMLElement {
const treeContainer = document.createElement('div');
dom.addClass(treeContainer, 'debug-view-content');
container.appendChild(treeContainer);
return treeContainer;
}
export function replaceWhitespace(value: string): string {
const map: { [x: string]: string } = { '\n': '\\n', '\r': '\\r', '\t': '\\t' };
return value.replace(/[\n\r\t]/g, char => map[char]);
}
export function renderExpressionValue(expressionOrValue: IExpressionContainer | string, container: HTMLElement, options: IRenderValueOptions): void {
let value = typeof expressionOrValue === 'string' ? expressionOrValue : expressionOrValue.value;
// remove stale classes
container.className = 'value';
// when resolving expressions we represent errors from the server as a variable with name === null.
if (value === null || ((expressionOrValue instanceof Expression || expressionOrValue instanceof Variable || expressionOrValue instanceof ReplEvaluationResult) && !expressionOrValue.available)) {
dom.addClass(container, 'unavailable');
if (value !== Expression.DEFAULT_VALUE) {
dom.addClass(container, 'error');
}
} else if ((expressionOrValue instanceof ExpressionContainer) && options.showChanged && expressionOrValue.valueChanged && value !== Expression.DEFAULT_VALUE) {
// value changed color has priority over other colors.
container.className = 'value changed';
expressionOrValue.valueChanged = false;
}
if (options.colorize && typeof expressionOrValue !== 'string') {
if (expressionOrValue.type === 'number' || expressionOrValue.type === 'boolean' || expressionOrValue.type === 'string') {
dom.addClass(container, expressionOrValue.type);
} else if (!isNaN(+value)) {
dom.addClass(container, 'number');
} else if (booleanRegex.test(value)) {
dom.addClass(container, 'boolean');
} else if (stringRegex.test(value)) {
dom.addClass(container, 'string');
}
}
if (options.maxValueLength && value && value.length > options.maxValueLength) {
value = value.substr(0, options.maxValueLength) + '...';
}
if (value && !options.preserveWhitespace) {
value = replaceWhitespace(value);
} else {
value = value || '';
}
if (options.linkDetector) {
container.textContent = '';
const session = (expressionOrValue instanceof ExpressionContainer) ? expressionOrValue.getSession() : undefined;
container.appendChild(options.linkDetector.linkify(value, false, session ? session.root : undefined));
} else {
container.textContent = value;
}
if (options.showHover) {
container.title = value || '';
}
}
export function renderVariable(variable: Variable, data: IVariableTemplateData, showChanged: boolean, highlights: IHighlight[], linkDetector?: LinkDetector): void {
if (variable.available) {
let text = replaceWhitespace(variable.name);
if (variable.value && typeof variable.name === 'string') {
text += ':';
}
data.label.set(text, highlights, variable.type ? variable.type : variable.name);
dom.toggleClass(data.name, 'virtual', !!variable.presentationHint && variable.presentationHint.kind === 'virtual');
} else if (variable.value && typeof variable.name === 'string') {
data.label.set(':');
}
renderExpressionValue(variable, data.value, {
showChanged,
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
preserveWhitespace: false,
showHover: true,
colorize: true,
linkDetector
});
}
export interface IInputBoxOptions {
initialValue: string;
ariaLabel: string;
placeholder?: string;
validationOptions?: IInputValidationOptions;
onFinish: (value: string, success: boolean) => void;
}
export interface IExpressionTemplateData {
expression: HTMLElement;
name: HTMLSpanElement;
value: HTMLSpanElement;
inputBoxContainer: HTMLElement;
enableInputBox(options: IInputBoxOptions): void;
toDispose: IDisposable[];
label: HighlightedLabel;
}
export abstract class AbstractExpressionsRenderer implements ITreeRenderer<IExpression, FuzzyScore, IExpressionTemplateData> {
constructor(
@IDebugService protected debugService: IDebugService,
@IContextViewService private readonly contextViewService: IContextViewService,
@IThemeService private readonly themeService: IThemeService
) { }
abstract get templateId(): string;
renderTemplate(container: HTMLElement): IExpressionTemplateData {
const expression = dom.append(container, $('.expression'));
const name = dom.append(expression, $('span.name'));
const value = dom.append(expression, $('span.value'));
const label = new HighlightedLabel(name, false);
const inputBoxContainer = dom.append(expression, $('.inputBoxContainer'));
const toDispose: IDisposable[] = [];
const enableInputBox = (options: IInputBoxOptions) => {
name.style.display = 'none';
value.style.display = 'none';
inputBoxContainer.style.display = 'initial';
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, options);
const styler = attachInputBoxStyler(inputBox, this.themeService);
inputBox.value = replaceWhitespace(options.initialValue);
inputBox.focus();
inputBox.select();
let disposed = false;
toDispose.push(inputBox);
toDispose.push(styler);
const wrapUp = (renamed: boolean) => {
if (!disposed) {
disposed = true;
this.debugService.getViewModel().setSelectedExpression(undefined);
options.onFinish(inputBox.value, renamed);
// need to remove the input box since this template will be reused.
inputBoxContainer.removeChild(inputBox.element);
name.style.display = 'initial';
value.style.display = 'initial';
inputBoxContainer.style.display = 'none';
dispose(toDispose);
}
};
toDispose.push(dom.addStandardDisposableListener(inputBox.inputElement, 'keydown', (e: IKeyboardEvent) => {
const isEscape = e.equals(KeyCode.Escape);
const isEnter = e.equals(KeyCode.Enter);
if (isEscape || isEnter) {
e.preventDefault();
e.stopPropagation();
wrapUp(isEnter);
}
}));
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'blur', () => {
wrapUp(true);
}));
toDispose.push(dom.addDisposableListener(inputBox.inputElement, 'click', e => {
// Do not expand / collapse selected elements
e.preventDefault();
e.stopPropagation();
}));
};
return { expression, name, value, label, enableInputBox, inputBoxContainer, toDispose };
}
renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
const { element } = node;
if (element === this.debugService.getViewModel().getSelectedExpression() || (element instanceof Variable && element.errorMessage)) {
const options = this.getInputBoxOptions(element);
if (options) {
data.enableInputBox(options);
return;
}
}
this.renderExpression(element, data, createMatches(node.filterData));
}
protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void;
protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined;
disposeTemplate(templateData: IExpressionTemplateData): void {
dispose(templateData.toDispose);
}
}