Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions Extension/src/Debugger/evaluatableExpression.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */

// The column range and text of the expression a debug data-tip should evaluate.
export interface EvaluatableExpressionInfo {
readonly startColumn: number;
readonly endColumn: number;
readonly expression: string;
}

// Computes the expression a debug data-tip should evaluate for the token at `character` in `line`,
// or undefined when the cursor is not on an expression token.
//
// Registering an EvaluatableExpressionProvider replaces VS Code's built-in data-tip expression
// detection, so this reproduces that detection for ordinary tokens and additionally resolves access
// chains involving a leading `*`/`&` or array subscripts, which the built-in detection mishandles:
// - A leading `*`/`&` applies to the whole access chain (the postfix `.`, `->`, `[]` operators
// bind tighter). It is dropped for an interior member of a `.` chain, where e.g. `*a.b` would
// dereference the struct `a.b`, and kept on the final segment and before `->`.
// - Array subscripts are part of the chain, so `[...]` is kept in the token; hovering `c` in
// `a.b[i].c` evaluates `a.b[i].c` rather than a fragment after the `]`.
//
// This has no vscode dependency so it can be unit tested directly.
export function computeEvaluatableExpression(line: string, character: number): EvaluatableExpressionInfo | undefined {
// An optional leading run of `*`/`&`, then a chain of identifiers, `.`, `->`, `::` and non-nested
// `[...]` subscripts.
const tokenRegExp: RegExp = /(?:[*&]+)?(?:[\p{L}\p{N}_]+|->|::|\.|\[[^\][]*\])+/gu;
let token: RegExpExecArray | null = null;
for (let m: RegExpExecArray | null = tokenRegExp.exec(line); m !== null; m = tokenRegExp.exec(line)) {
// Upper bound is inclusive to match VS Code's built-in detection, which selects a token when
// the cursor is at its trailing edge.
if (m.index <= character && character <= m.index + m[0].length) {
token = m;
break;
}
}
if (token === null) {
return undefined;
}
const tokenStart: number = token.index;
const tokenEnd: number = token.index + token[0].length;
const leading: RegExpMatchArray | null = token[0].match(/^[*&]+/);
const exprStart: number = tokenStart + (leading !== null ? leading[0].length : 0);
Comment on lines +42 to +45

// On a subscript bracket, evaluate the element through that subscript without a leading `*`/`&`,
// i.e. the indexed element itself.
const cursorChar: string = line.charAt(character);
if (cursorChar === '[' || cursorChar === ']') {
let end: number = character;
if (cursorChar === '[') {
while (end < tokenEnd && line.charAt(end) !== ']') {
end++;
}
}
const subEnd: number = Math.min(end + 1, tokenEnd);
return { startColumn: exprStart, endColumn: subEnd, expression: line.substring(exprStart, subEnd) };
}

// Locate the identifier under the cursor and the offset just past it.
let clipEnd: number = tokenEnd;
let wordStart: number = tokenStart;
let word: string = '';
const wordRegExp: RegExp = /[\p{L}\p{N}_]+/gu;
for (let w: RegExpExecArray | null = wordRegExp.exec(token[0]); w !== null; w = wordRegExp.exec(token[0])) {
clipEnd = tokenStart + w.index + w[0].length;
wordStart = tokenStart + w.index;
word = w[0];
if (clipEnd >= character) {
break;
}
}

// An identifier inside a `[...]` is the index; it is evaluated on its own, not as part of the
// surrounding access chain.
const beforeCursor: string = line.substring(tokenStart, character);
const openCount: number = (beforeCursor.match(/\[/g) || []).length;
const closeCount: number = (beforeCursor.match(/\]/g) || []).length;
if (openCount > closeCount) {
return { startColumn: wordStart, endColumn: clipEnd, expression: word };
}

// The leading `*`/`&` is dropped only for an interior member directly followed by `.`. On the
// final member, or before `->`, it applies to the whole expression and is kept.
if (leading === null || clipEnd >= tokenEnd || clipEnd <= exprStart || line.charAt(clipEnd) !== '.') {
return { startColumn: tokenStart, endColumn: clipEnd, expression: line.substring(tokenStart, clipEnd) };
}
return { startColumn: exprStart, endColumn: clipEnd, expression: line.substring(exprStart, clipEnd) };
}
18 changes: 18 additions & 0 deletions Extension/src/Debugger/evaluatableExpressionProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */
import * as vscode from 'vscode';
import { computeEvaluatableExpression, EvaluatableExpressionInfo } from './evaluatableExpression';

// Provides the expression a C/C++ debug data-tip evaluates when hovering a variable. The actual
// computation lives in `evaluatableExpression.ts` (no vscode dependency) so it can be unit tested.
export class EvaluatableExpressionProvider implements vscode.EvaluatableExpressionProvider {
public provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult<vscode.EvaluatableExpression> {
Comment thread
sean-mcmanus marked this conversation as resolved.
const info: EvaluatableExpressionInfo | undefined = computeEvaluatableExpression(document.lineAt(position.line).text, position.character);
if (info === undefined) {
return undefined;
}
return new vscode.EvaluatableExpression(new vscode.Range(position.line, info.startColumn, position.line, info.endColumn), info.expression);
}
}
6 changes: 5 additions & 1 deletion Extension/src/Debugger/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ import { SshTargetsProvider, getActiveSshTarget, initializeSshTargets, selectSsh
import { TargetLeafNode, setActiveSshTarget } from '../SSH/TargetsView/targetNodes';
import { sshCommandToConfig } from '../SSH/sshCommandToConfig';
import { getSshConfiguration, getSshConfigurationFiles, parseFailures, writeSshConfiguration } from '../SSH/sshHosts';
import { pathAccessible } from '../common';
import { documentSelector, pathAccessible } from '../common';
import { instrument } from '../instrumentation';
import { getSshChannel } from '../logger';
import { AttachItemsProvider, AttachPicker, RemoteAttachPicker } from './attachToProcess';
import { ConfigurationAssetProviderFactory, ConfigurationSnippetProvider, DebugConfigurationProvider, IConfigurationAssetProvider } from './configurationProvider';
import { DebuggerType } from './configurations';
import { CppdbgDebugAdapterDescriptorFactory, CppvsdbgDebugAdapterDescriptorFactory } from './debugAdapterDescriptorFactory';
import { EvaluatableExpressionProvider } from './evaluatableExpressionProvider';
import { NativeAttachItemsProviderFactory } from './nativeAttach';

// The extension deactivate method is asynchronous, so we handle the disposables ourselves instead of using extensionContext.subscriptions.
Expand Down Expand Up @@ -82,6 +83,9 @@ export async function initialize(context: vscode.ExtensionContext): Promise<void
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppvsdbg, new CppvsdbgDebugAdapterDescriptorFactory(context)));
disposables.push(vscode.debug.registerDebugAdapterDescriptorFactory(DebuggerType.cppdbg, new CppdbgDebugAdapterDescriptorFactory(context)));

// Supplies the expression evaluated by debug data-tips when hovering C/C++ source.
disposables.push(vscode.languages.registerEvaluatableExpressionProvider(documentSelector, instrument(new EvaluatableExpressionProvider())));

// SSH Targets View
await initializeSshTargets();
const sshTargetsProvider: SshTargetsProvider = new SshTargetsProvider();
Expand Down
67 changes: 67 additions & 0 deletions Extension/test/unit/evaluatableExpression.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/* --------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved.
* See 'LICENSE' in the project root for license information.
* ------------------------------------------------------------------------------------------ */

import { describe, it } from 'mocha';
import { strictEqual } from 'node:assert';
import { computeEvaluatableExpression } from '../../src/Debugger/evaluatableExpression';

// In each input the `|` marks the cursor; it is removed before evaluating.
function evaluate(marked: string): string | undefined {
const character: number = marked.indexOf('|');
const line: string = marked.replace('|', '');

Check failure

Code scanning / CodeQL

Incomplete string escaping or encoding High test

This replaces only the first occurrence of '|'.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CodeQL js/incomplete-sanitization (test helper)

It's a benign warning on test code, but trivially silenced and made more robust by slicing at the known marker index instead of String.replace, which also documents the single-marker assumption:

function evaluate(marked: string): string | undefined {
    const character: number = marked.indexOf('|');
    const line: string = marked.slice(0, character) + marked.slice(character + 1);
    return computeEvaluatableExpression(line, character)?.expression;
}

return computeEvaluatableExpression(line, character)?.expression;
}

describe('computeEvaluatableExpression', () => {
it('returns undefined when the cursor is not on a token', () => {
strictEqual(evaluate('a + | b'), undefined);
});

it('evaluates a plain identifier', () => {
strictEqual(evaluate('|x'), 'x');
});

it('drops a leading * for an interior member of a dot chain', () => {
strictEqual(evaluate('*|a.b.c'), 'a');
strictEqual(evaluate('*a.|b.c'), 'a.b');
});

it('keeps a leading * on the final member', () => {
strictEqual(evaluate('*a.b.|c'), '*a.b.c');
});

it('keeps a leading * before -> (binds to the whole chain)', () => {
strictEqual(evaluate('*|ptr->member'), '*ptr');
strictEqual(evaluate('*ptr->|member'), '*ptr->member');
});

it('leaves -> chains without a leading operator unchanged', () => {
strictEqual(evaluate('p->|q->r'), 'p->q');
strictEqual(evaluate('p->q->|r'), 'p->q->r');
});

it('keeps array subscripts in the chain', () => {
strictEqual(evaluate('a.|b[i].c'), 'a.b');
strictEqual(evaluate('a.b[i].|c'), 'a.b[i].c');
strictEqual(evaluate('dbbolz.dbbolz_anst[out_idx].|anw_dig'), 'dbbolz.dbbolz_anst[out_idx].anw_dig');
strictEqual(evaluate('dbbolz.dbbolz_anst[out_idx].anw_dig.|stsdig'), 'dbbolz.dbbolz_anst[out_idx].anw_dig.stsdig');
});

it('evaluates the element when on a subscript bracket, without the leading operator', () => {
strictEqual(evaluate('a.b|[i].c'), 'a.b[i]');
strictEqual(evaluate('a.b[i|].c'), 'a.b[i]');
strictEqual(evaluate('&dbbolz.dbbolz_anst[out_idx].fg|[kanal_idx]'), 'dbbolz.dbbolz_anst[out_idx].fg[kanal_idx]');
});

it('evaluates the index on its own when inside a subscript', () => {
strictEqual(evaluate('a.b[|i].c'), 'i');
strictEqual(evaluate('&dbbolz.dbbolz_anst[out_idx].fg[|kanal_idx]'), 'kanal_idx');
});

it('keeps :: scoped names together', () => {
strictEqual(evaluate('ns::|var'), 'ns::var');
strictEqual(evaluate('ns::var::|z'), 'ns::var::z');
});
});
Comment on lines +63 to +67
Loading