Skip to content

Commit

Permalink
debug: allow filter/search on debug values (#236768)
Browse files Browse the repository at this point in the history
* debug: allow filter/search on debug values

I think this is something that never worked, or at least not for a long
while. Implementing match highlighting in values in the era of
linkification and ANSI support was a little more complex, but this works
well now.

![](https://memes.peet.io/img/24-12-b1f699dc-f20c-4c93-a5ce-f768473fff62.png)

Fixes #230945

* fix test
  • Loading branch information
connor4312 authored Dec 21, 2024
1 parent d6a59b7 commit d953d84
Show file tree
Hide file tree
Showing 9 changed files with 208 additions and 39 deletions.
30 changes: 29 additions & 1 deletion src/vs/workbench/contrib/debug/browser/baseDebugView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,21 @@ import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';
import { HighlightedLabel, IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
import { IInputValidationOptions, InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
import { IKeyboardNavigationLabelProvider } from '../../../../base/browser/ui/list/list.js';
import { IAsyncDataSource, ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { FuzzyScore, createMatches } from '../../../../base/common/filters.js';
import { createSingleCallFunction } from '../../../../base/common/functional.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { localize } from '../../../../nls.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { IContextViewService } from '../../../../platform/contextview/browser/contextView.js';
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
import { defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { IDebugService, IExpression } from '../common/debug.js';
import { IDebugService, IExpression, IScope } from '../common/debug.js';
import { Variable } from '../common/debugModel.js';
import { IDebugVisualizerService } from '../common/debugVisualizers.js';
import { LinkDetector } from './linkDetector.js';
Expand Down Expand Up @@ -78,6 +80,32 @@ export interface IExpressionTemplateData {
currentElement: IExpression | undefined;
}

/** Splits highlights based on matching of the {@link expressionAndScopeLabelProvider} */
export const splitExpressionOrScopeHighlights = (e: IExpression | IScope, highlights: IHighlight[]) => {
const nameEndsAt = e.name.length;
const labelBeginsAt = e.name.length + 2;
const name: IHighlight[] = [];
const value: IHighlight[] = [];
for (const hl of highlights) {
if (hl.start < nameEndsAt) {
name.push({ start: hl.start, end: Math.min(hl.end, nameEndsAt) });
}
if (hl.end > labelBeginsAt) {
value.push({ start: Math.max(hl.start - labelBeginsAt, 0), end: hl.end - labelBeginsAt });
}
}

return { name, value };
};

/** Keyboard label provider for expression and scope tree elements. */
export const expressionAndScopeLabelProvider: IKeyboardNavigationLabelProvider<IExpression | IScope> = {
getKeyboardNavigationLabel(e) {
const stripAnsi = e.getSession()?.rememberedCapabilities?.supportsANSIStyling;
return `${e.name}: ${stripAnsi ? removeAnsiEscapeCodes(e.value) : e.value}`;
},
};

export abstract class AbstractExpressionDataSource<Input, Element extends IExpression> implements IAsyncDataSource<Input, Element> {
constructor(
@IDebugService protected debugService: IDebugService,
Expand Down
29 changes: 22 additions & 7 deletions src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';
import { Color, RGBA } from '../../../../base/common/color.js';
import { isDefined } from '../../../../base/common/types.js';
import { editorHoverBackground, listActiveSelectionBackground, listFocusBackground, listInactiveFocusBackground, listInactiveSelectionBackground } from '../../../../platform/theme/common/colorRegistry.js';
Expand All @@ -16,7 +17,7 @@ import { ILinkDetector } from './linkDetector.js';
* @param text The content to stylize.
* @returns An {@link HTMLSpanElement} that contains the potentially stylized text.
*/
export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined): HTMLSpanElement {
export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, highlights: IHighlight[] | undefined): HTMLSpanElement {

const root: HTMLSpanElement = document.createElement('span');
const textLength: number = text.length;
Expand All @@ -27,6 +28,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work
let customUnderlineColor: RGBA | string | undefined;
let colorsInverted: boolean = false;
let currentPos: number = 0;
let unprintedChars = 0;
let buffer: string = '';

while (currentPos < textLength) {
Expand Down Expand Up @@ -58,8 +60,10 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work

if (sequenceFound) {

unprintedChars += 2 + ansiSequence.length;

// Flush buffer with previous styles.
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length - unprintedChars);

buffer = '';

Expand Down Expand Up @@ -105,7 +109,7 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work

// Flush remaining text buffer if not empty.
if (buffer) {
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length);
}

return root;
Expand Down Expand Up @@ -395,22 +399,33 @@ export function handleANSIOutput(text: string, linkDetector: ILinkDetector, work
* @param customTextColor If provided, will apply custom color with inline style.
* @param customBackgroundColor If provided, will apply custom backgroundColor with inline style.
* @param customUnderlineColor If provided, will apply custom textDecorationColor with inline style.
* @param highlights The ranges to highlight.
* @param offset The starting index of the stringContent in the original text.
*/
export function appendStylizedStringToContainer(
root: HTMLElement,
stringContent: string,
cssClasses: string[],
linkDetector: ILinkDetector,
workspaceFolder: IWorkspaceFolder | undefined,
customTextColor?: RGBA | string,
customBackgroundColor?: RGBA | string,
customUnderlineColor?: RGBA | string,
customTextColor: RGBA | string | undefined,
customBackgroundColor: RGBA | string | undefined,
customUnderlineColor: RGBA | string | undefined,
highlights: IHighlight[] | undefined,
offset: number,
): void {
if (!root || !stringContent) {
return;
}

const container = linkDetector.linkify(stringContent, true, workspaceFolder);
const container = linkDetector.linkify(
stringContent,
true,
workspaceFolder,
undefined,
undefined,
highlights?.map(h => ({ start: h.start - offset, end: h.end - offset, extraClasses: h.extraClasses })),
);

container.className = cssClasses.join(' ');
if (customTextColor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { observableConfigValue } from '../../../../platform/observable/common/pl
import { IDebugSession, IExpressionValue } from '../common/debug.js';
import { Expression, ExpressionContainer, Variable } from '../common/debugModel.js';
import { ReplEvaluationResult } from '../common/replModel.js';
import { IVariableTemplateData } from './baseDebugView.js';
import { IVariableTemplateData, splitExpressionOrScopeHighlights } from './baseDebugView.js';
import { handleANSIOutput } from './debugANSIHandling.js';
import { COPY_EVALUATE_PATH_ID, COPY_VALUE_ID } from './debugCommands.js';
import { DebugLinkHoverBehavior, DebugLinkHoverBehaviorTypeData, ILinkDetector, LinkDetector } from './linkDetector.js';
Expand All @@ -32,6 +32,7 @@ export interface IRenderValueOptions {
/** If not false, a rich hover will be shown on the element. */
hover?: false | IValueHoverOptions;
colorize?: boolean;
highlights?: IHighlight[];

/**
* Indicates areas where VS Code implicitly always supported ANSI escape
Expand Down Expand Up @@ -90,6 +91,7 @@ export class DebugExpressionRenderer {

renderVariable(data: IVariableTemplateData, variable: Variable, options: IRenderVariableOptions = {}): IDisposable {
const displayType = this.displayType.get();
const highlights = splitExpressionOrScopeHighlights(variable, options.highlights || []);

if (variable.available) {
data.type.textContent = '';
Expand All @@ -103,7 +105,7 @@ export class DebugExpressionRenderer {
}
}

data.label.set(text, options.highlights, variable.type && !displayType ? variable.type : variable.name);
data.label.set(text, highlights.name, variable.type && !displayType ? variable.type : variable.name);
data.name.classList.toggle('virtual', variable.presentationHint?.kind === 'virtual');
data.name.classList.toggle('internal', variable.presentationHint?.visibility === 'internal');
} else if (variable.value && typeof variable.name === 'string' && variable.name) {
Expand All @@ -122,6 +124,7 @@ export class DebugExpressionRenderer {
showChanged: options.showChanged,
maxValueLength: MAX_VALUE_RENDER_LENGTH_IN_VIEWLET,
hover: { commands },
highlights: highlights.value,
colorize: true,
session: variable.getSession(),
});
Expand Down Expand Up @@ -184,9 +187,9 @@ export class DebugExpressionRenderer {
}

if (supportsANSI) {
container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined));
container.appendChild(handleANSIOutput(value, linkDetector, session ? session.root : undefined, options.highlights));
} else {
container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior));
container.appendChild(linkDetector.linkify(value, false, session?.root, true, hoverBehavior, options.highlights));
}

if (options.hover !== false) {
Expand All @@ -199,7 +202,7 @@ export class DebugExpressionRenderer {
if (supportsANSI) {
// note: intentionally using `this.linkDetector` so we don't blindly linkify the
// entire contents and instead only link file paths that it contains.
hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined));
hoverContentsPre.appendChild(handleANSIOutput(value, this.linkDetector, session ? session.root : undefined, options.highlights));
} else {
hoverContentsPre.textContent = value;
}
Expand Down
Loading

0 comments on commit d953d84

Please sign in to comment.