From 4a4f876ac77e7452549dfbf21b9a862186dbc7ec Mon Sep 17 00:00:00 2001 From: tieo <65707274+tieo@users.noreply.github.com> Date: Tue, 23 Jun 2026 11:53:39 +0200 Subject: [PATCH 1/2] Add EvaluatableExpressionProvider to fix C/C++ debug hover on dereferenced members VS Code's default debug data-tip keeps a leading `*`/`&` and clips on the right, so hovering an intermediate member of e.g. `*a.b.c` evaluates `*a.b` (a dereference of the struct `a.b`) and shows no value. Register an EvaluatableExpressionProvider that, only for that case, returns the expression without the leading operator. Every other expression returns undefined so the default behavior is unchanged. --- .../evaluatableExpressionProvider.ts | 51 +++++++++++++++++++ Extension/src/LanguageServer/client.ts | 2 + 2 files changed, 53 insertions(+) create mode 100644 Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts diff --git a/Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts b/Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts new file mode 100644 index 000000000..b62cc7261 --- /dev/null +++ b/Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts @@ -0,0 +1,51 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import * as vscode from 'vscode'; + +// VS Code's default data-tip keeps a leading `*`/`&` and clips on the right, so hovering an +// intermediate member of e.g. `*a.b.c` evaluates `*a.b` (deref of the struct `a.b`, which is of course wrong) +// That's why the leading operator needs to be dropped for that case. +export class EvaluatableExpressionProvider implements vscode.EvaluatableExpressionProvider { + public provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { + const line: string = document.lineAt(position.line).text; + // The same token the default uses: a run of expression characters (brackets, operators + // and whitespace excluded), with `->` allowed. + const tokenRegExp: RegExp = /(?:[^()[\]{}<>\s+\-/%~#^;=|,`!]|->)+/g; + let token: RegExpExecArray | null = null; + for (let m: RegExpExecArray | null = tokenRegExp.exec(line); m !== null; m = tokenRegExp.exec(line)) { + if (m.index <= position.character && position.character <= m.index + m[0].length) { + token = m; + break; + } + } + if (token === null) { + return undefined; + } + const leading: RegExpMatchArray | null = token[0].match(/^[*&]+/); + if (leading === null) { + return undefined; // No leading dereference/address-of: use the default. + } + const tokenStart: number = token.index; + const tokenEnd: number = token.index + token[0].length; + // Right-clip to the identifier the cursor is on, mirroring the default. + let clipEnd: number = tokenEnd; + 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; + if (clipEnd >= position.character) { + break; + } + } + // Only act when the cursor is on an intermediate member (the clipped expression is a + // proper prefix). If it is on the final segment, the leading operator legitimately + // applies to the whole expression, so defer to the default. + const exprStart: number = tokenStart + leading[0].length; + if (clipEnd >= tokenEnd || clipEnd <= exprStart) { + return undefined; + } + const expression: string = line.substring(exprStart, clipEnd); + return new vscode.EvaluatableExpression(new vscode.Range(position.line, exprStart, position.line, clipEnd), expression); + } +} diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index 3410e7502..a27e458a4 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -13,6 +13,7 @@ import { CodeActionProvider } from './Providers/codeActionProvider'; import { DocumentFormattingEditProvider } from './Providers/documentFormattingEditProvider'; import { DocumentRangeFormattingEditProvider } from './Providers/documentRangeFormattingEditProvider'; import { DocumentSymbolProvider } from './Providers/documentSymbolProvider'; +import { EvaluatableExpressionProvider } from './Providers/evaluatableExpressionProvider'; import { FindAllReferencesProvider } from './Providers/findAllReferencesProvider'; import { FoldingRangeProvider } from './Providers/foldingRangeProvider'; import { CppInlayHint, InlayHintsProvider } from './Providers/inlayHintProvider'; @@ -1431,6 +1432,7 @@ export class DefaultClient implements Client { this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.copilotHoverProvider))); this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.hoverProvider))); + this.disposables.push(vscode.languages.registerEvaluatableExpressionProvider(util.documentSelector, instrument(new EvaluatableExpressionProvider()))); this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, instrument(this.inlayHintsProvider))); this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, instrument(new RenameProvider(this)))); this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, instrument(new FindAllReferencesProvider(this)))); From 5433ec8a9f35f06ed26089c3536b2c5b6a0498bf Mon Sep 17 00:00:00 2001 From: tieo <65707274+tieo@users.noreply.github.com> Date: Thu, 25 Jun 2026 09:32:27 +0200 Subject: [PATCH 2/2] Move the debug hover provider to the debugger and fix access chains Register the EvaluatableExpressionProvider during debugger activation instead of through the language client, so it also works when IntelliSense is disabled, and move it next to the other debugger code. Registering a provider replaces VS Code's built-in data-tip expression detection entirely, so reproduce that for ordinary tokens and additionally handle two cases the built-in detection gets wrong for C/C++: - A leading * or & binds to the whole access chain, not an interior member (the postfix operators bind tighter), so it is dropped on an interior member, where e.g. *a.b would dereference the struct a.b, and kept on the final segment. Cases like *ptr->member are handled accordingly. - Array subscripts are part of the chain, so [...] is kept (including nesting) rather than ending the token, and hovering an index evaluates the index on its own. Members accessed after a subscript such as a.b[i].c now resolve instead of producing a broken fragment. --- .../Debugger/evaluatableExpressionProvider.ts | 89 +++++++++++++++++++ Extension/src/Debugger/extension.ts | 6 +- .../evaluatableExpressionProvider.ts | 51 ----------- Extension/src/LanguageServer/client.ts | 2 - 4 files changed, 94 insertions(+), 54 deletions(-) create mode 100644 Extension/src/Debugger/evaluatableExpressionProvider.ts delete mode 100644 Extension/src/LanguageServer/Providers/evaluatableExpressionProvider.ts diff --git a/Extension/src/Debugger/evaluatableExpressionProvider.ts b/Extension/src/Debugger/evaluatableExpressionProvider.ts new file mode 100644 index 000000000..2979d05a3 --- /dev/null +++ b/Extension/src/Debugger/evaluatableExpressionProvider.ts @@ -0,0 +1,89 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All Rights Reserved. + * See 'LICENSE' in the project root for license information. + * ------------------------------------------------------------------------------------------ */ +import * as vscode from 'vscode'; + +// Determines the expression a debug data-tip evaluates for the token under the cursor. While a +// provider is registered VS Code does not apply its built-in expression detection, so an +// expression is returned for any token rather than undefined. +// +// Beyond the identifier under the cursor, two parts of a C/C++ access chain are handled: +// - A leading `*` or `&` applies to the whole chain. On an interior member or a subscript it is +// dropped, because e.g. `*a.b` dereferences the struct `a.b` instead of yielding the member. +// - Array subscripts are part of the path, so `[...]` is kept in the token; hovering `c` in +// `a.b[i].c` evaluates `a.b[i].c` rather than a fragment after the `]`. +export class EvaluatableExpressionProvider implements vscode.EvaluatableExpressionProvider { + public provideEvaluatableExpression(document: vscode.TextDocument, position: vscode.Position): vscode.ProviderResult { + const line: string = document.lineAt(position.line).text; + // 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)) { + if (m.index <= position.character && position.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(position.character); + if (cursorChar === '[' || cursorChar === ']') { + let end: number = position.character; + if (cursorChar === '[') { + while (end < tokenEnd && line.charAt(end) !== ']') { + end++; + } + } + const subEnd: number = Math.min(end + 1, tokenEnd); + return new vscode.EvaluatableExpression(new vscode.Range(position.line, exprStart, position.line, subEnd), 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 >= position.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, position.character); + const openCount: number = (beforeCursor.match(/\[/g) || []).length; + const closeCount: number = (beforeCursor.match(/\]/g) || []).length; + if (openCount > closeCount) { + return new vscode.EvaluatableExpression(new vscode.Range(position.line, wordStart, position.line, clipEnd), word); + } + + // The access chain from its start through the identifier under the cursor. + const defaultExpression = (): vscode.EvaluatableExpression => + new vscode.EvaluatableExpression(new vscode.Range(position.line, tokenStart, position.line, clipEnd), line.substring(tokenStart, clipEnd)); + + if (leading === null) { + return defaultExpression(); + } + // The leading `*`/`&` is dropped only for an interior member directly followed by `.`. On + // the final member, or before `->` or a subscript, it applies to the whole expression and + // is kept. + if (clipEnd >= tokenEnd || clipEnd <= exprStart || line.charAt(clipEnd) !== '.') { + return defaultExpression(); + } + return new vscode.EvaluatableExpression(new vscode.Range(position.line, exprStart, position.line, clipEnd), line.substring(exprStart, clipEnd)); + } +} 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 { - const line: string = document.lineAt(position.line).text; - // The same token the default uses: a run of expression characters (brackets, operators - // and whitespace excluded), with `->` allowed. - const tokenRegExp: RegExp = /(?:[^()[\]{}<>\s+\-/%~#^;=|,`!]|->)+/g; - let token: RegExpExecArray | null = null; - for (let m: RegExpExecArray | null = tokenRegExp.exec(line); m !== null; m = tokenRegExp.exec(line)) { - if (m.index <= position.character && position.character <= m.index + m[0].length) { - token = m; - break; - } - } - if (token === null) { - return undefined; - } - const leading: RegExpMatchArray | null = token[0].match(/^[*&]+/); - if (leading === null) { - return undefined; // No leading dereference/address-of: use the default. - } - const tokenStart: number = token.index; - const tokenEnd: number = token.index + token[0].length; - // Right-clip to the identifier the cursor is on, mirroring the default. - let clipEnd: number = tokenEnd; - 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; - if (clipEnd >= position.character) { - break; - } - } - // Only act when the cursor is on an intermediate member (the clipped expression is a - // proper prefix). If it is on the final segment, the leading operator legitimately - // applies to the whole expression, so defer to the default. - const exprStart: number = tokenStart + leading[0].length; - if (clipEnd >= tokenEnd || clipEnd <= exprStart) { - return undefined; - } - const expression: string = line.substring(exprStart, clipEnd); - return new vscode.EvaluatableExpression(new vscode.Range(position.line, exprStart, position.line, clipEnd), expression); - } -} diff --git a/Extension/src/LanguageServer/client.ts b/Extension/src/LanguageServer/client.ts index a27e458a4..3410e7502 100644 --- a/Extension/src/LanguageServer/client.ts +++ b/Extension/src/LanguageServer/client.ts @@ -13,7 +13,6 @@ import { CodeActionProvider } from './Providers/codeActionProvider'; import { DocumentFormattingEditProvider } from './Providers/documentFormattingEditProvider'; import { DocumentRangeFormattingEditProvider } from './Providers/documentRangeFormattingEditProvider'; import { DocumentSymbolProvider } from './Providers/documentSymbolProvider'; -import { EvaluatableExpressionProvider } from './Providers/evaluatableExpressionProvider'; import { FindAllReferencesProvider } from './Providers/findAllReferencesProvider'; import { FoldingRangeProvider } from './Providers/foldingRangeProvider'; import { CppInlayHint, InlayHintsProvider } from './Providers/inlayHintProvider'; @@ -1432,7 +1431,6 @@ export class DefaultClient implements Client { this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.copilotHoverProvider))); this.disposables.push(vscode.languages.registerHoverProvider(util.documentSelector, instrument(this.hoverProvider))); - this.disposables.push(vscode.languages.registerEvaluatableExpressionProvider(util.documentSelector, instrument(new EvaluatableExpressionProvider()))); this.disposables.push(vscode.languages.registerInlayHintsProvider(util.documentSelector, instrument(this.inlayHintsProvider))); this.disposables.push(vscode.languages.registerRenameProvider(util.documentSelector, instrument(new RenameProvider(this)))); this.disposables.push(vscode.languages.registerReferenceProvider(util.documentSelector, instrument(new FindAllReferencesProvider(this))));