Files
vscode/src/vs/workbench/contrib/debug/browser/baseDebugView.ts
T
2020-10-01 12:30:30 +02:00

235 lines
8.9 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, Disposable, toDisposable } 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';
import { once } from 'vs/base/common/functional';
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 {
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 = $('.');
treeContainer.classList.add('debug-view-content');
container.appendChild(treeContainer);
return treeContainer;
}
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)) {
container.classList.add('unavailable');
if (value !== Expression.DEFAULT_VALUE) {
container.classList.add('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') {
container.classList.add(expressionOrValue.type);
} else if (!isNaN(+value)) {
container.classList.add('number');
} else if (booleanRegex.test(value)) {
container.classList.add('boolean');
} else if (stringRegex.test(value)) {
container.classList.add('string');
}
}
if (options.maxValueLength && value && value.length > options.maxValueLength) {
value = value.substr(0, options.maxValueLength) + '...';
}
if (!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 = variable.name;
if (variable.value && typeof variable.name === 'string') {
text += ':';
}
data.label.set(text, highlights, variable.type ? variable.type : variable.name);
data.name.classList.toggle('virtual', !!variable.presentationHint && variable.presentationHint.kind === 'virtual');
} else if (variable.value && typeof variable.name === 'string' && variable.name) {
data.label.set(':');
}
renderExpressionValue(variable, data.value, {
showChanged,
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
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;
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'));
return { expression, name, value, label, inputBoxContainer, toDispose: Disposable.None };
}
renderElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, data: IExpressionTemplateData): void {
data.toDispose.dispose();
data.toDispose = Disposable.None;
const { element } = node;
this.renderExpression(element, data, createMatches(node.filterData));
if (element === this.debugService.getViewModel().getSelectedExpression() || (element instanceof Variable && element.errorMessage)) {
const options = this.getInputBoxOptions(element);
if (options) {
data.toDispose = this.renderInputBox(data.name, data.value, data.inputBoxContainer, options);
return;
}
}
}
renderInputBox(nameElement: HTMLElement, valueElement: HTMLElement, inputBoxContainer: HTMLElement, options: IInputBoxOptions): IDisposable {
nameElement.style.display = 'none';
valueElement.style.display = 'none';
inputBoxContainer.style.display = 'initial';
const inputBox = new InputBox(inputBoxContainer, this.contextViewService, options);
const styler = attachInputBoxStyler(inputBox, this.themeService);
inputBox.value = options.initialValue;
inputBox.focus();
inputBox.select();
const done = once((success: boolean, finishEditing: boolean) => {
nameElement.style.display = 'initial';
valueElement.style.display = 'initial';
inputBoxContainer.style.display = 'none';
const value = inputBox.value;
dispose(toDispose);
if (finishEditing) {
this.debugService.getViewModel().setSelectedExpression(undefined);
options.onFinish(value, success);
}
});
const toDispose = [
inputBox,
dom.addStandardDisposableListener(inputBox.inputElement, dom.EventType.KEY_DOWN, (e: IKeyboardEvent) => {
const isEscape = e.equals(KeyCode.Escape);
const isEnter = e.equals(KeyCode.Enter);
if (isEscape || isEnter) {
e.preventDefault();
e.stopPropagation();
done(isEnter, true);
}
}),
dom.addDisposableListener(inputBox.inputElement, dom.EventType.BLUR, () => {
done(true, true);
}),
dom.addDisposableListener(inputBox.inputElement, dom.EventType.CLICK, e => {
// Do not expand / collapse selected elements
e.preventDefault();
e.stopPropagation();
}),
styler
];
return toDisposable(() => {
done(false, false);
});
}
protected abstract renderExpression(expression: IExpression, data: IExpressionTemplateData, highlights: IHighlight[]): void;
protected abstract getInputBoxOptions(expression: IExpression): IInputBoxOptions | undefined;
disposeElement(node: ITreeNode<IExpression, FuzzyScore>, index: number, templateData: IExpressionTemplateData): void {
templateData.toDispose.dispose();
}
disposeTemplate(templateData: IExpressionTemplateData): void {
templateData.toDispose.dispose();
}
}