diff --git a/Extension/src/Debugger/evaluatableExpression.ts b/Extension/src/Debugger/evaluatableExpression.ts new file mode 100644 index 000000000..adccc4491 --- /dev/null +++ b/Extension/src/Debugger/evaluatableExpression.ts @@ -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); + + // 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) }; +} diff --git a/Extension/src/Debugger/evaluatableExpressionProvider.ts b/Extension/src/Debugger/evaluatableExpressionProvider.ts new file mode 100755 index 000000000..fd04d1da7 --- /dev/null +++ b/Extension/src/Debugger/evaluatableExpressionProvider.ts @@ -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 { + 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); + } +} diff --git a/Extension/src/Debugger/extension.ts b/Extension/src/Debugger/extension.ts index f784c1382..780fa121b 100644 --- a/Extension/src/Debugger/extension.ts +++ b/Extension/src/Debugger/extension.ts @@ -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. @@ -82,6 +83,9 @@ export async function initialize(context: vscode.ExtensionContext): Promise { + 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'); + }); +});