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