diff --git a/src/MIDebugEngine/Engine.Impl/Variables.cs b/src/MIDebugEngine/Engine.Impl/Variables.cs index b6dd867bb..9524d677c 100644 --- a/src/MIDebugEngine/Engine.Impl/Variables.cs +++ b/src/MIDebugEngine/Engine.Impl/Variables.cs @@ -5,6 +5,7 @@ using Microsoft.VisualStudio.Debugger.Interop; using Microsoft.VisualStudio.Debugger.Interop.DAP; using System; +using Microsoft.DebugEngineHost; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; @@ -92,6 +93,18 @@ internal sealed class VariableInformation : IVariableInformation static readonly Lazy s_addressPattern = new Lazy(() => new Regex(@"^(0x[0-9a-fA-F]+)\b")); + static readonly Regex s_naPattern = new Regex(@"^0x[0-9a-fA-F]+\s+(.)"); + + internal static string StripLeadingAddress(string value) + { + if (string.IsNullOrEmpty(value)) + { + return value; + } + + return s_naPattern.Replace(value, "$1"); + } + public string Address() { // ask GDB to evaluate "&expression" @@ -238,7 +251,7 @@ internal VariableInformation(string displayName, string expr, ThreadContext ctx, : this(ctx, engine, thread) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = displayName; IsParameter = isParameter; _parent = null; @@ -250,7 +263,7 @@ internal VariableInformation(string expr, IVariableInformation parent, AD7Engine : this(parent.ThreadContext, engine, parent.Client) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = displayName ?? expr; _parent = parent; VariableNodeType = NodeType.Synthetic; @@ -261,7 +274,7 @@ internal VariableInformation(string expr, VariableInformation parent) : this(parent._ctx, parent._engine, parent.Client) { // strip off formatting string - _strippedName = ProcessFormatSpecifiers(expr, out _format); + _strippedName = ProcessFormatSpecifiers(expr, out _format, out _formatHasNa); Name = expr; VariableNodeType = NodeType.Root; } @@ -272,6 +285,12 @@ private VariableInformation(TupleValue results, VariableInformation parent, stri { TypeName = results.TryFindString("type"); Value = results.TryFindString("value"); + _formatHasNa = parent._formatHasNa; + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } + Name = name ?? results.FindString("exp"); if (results.Contains("dynamic")) { @@ -375,6 +394,9 @@ public VariableInformation FindChildByName(string name) private DeferedFormatExpression _deferedFormatExpression; private IVariableInformation _parent; private string _format; + // Indicates the original format specifier included the natvis "na" modifier + // (used to decide whether to strip MI's leading address prefix from string values) + private bool _formatHasNa = false; private string _strippedName; // "Name" stripped of format specifiers private string _fullname; @@ -401,8 +423,9 @@ public enum NodeType private static Regex s_isFunction = new Regex(@".+\(.*\).*"); - private string ProcessFormatSpecifiers(string exp, out string formatSpecifier) + private string ProcessFormatSpecifiers(string exp, out string formatSpecifier, out bool formatNa) { + formatNa = false; formatSpecifier = null; // will be used with -var-set-format if (EngineUtils.IsConsoleExecCmd(exp, out string _, out string _)) @@ -416,6 +439,9 @@ private string ProcessFormatSpecifiers(string exp, out string formatSpecifier) // Find the format specifier expression string expFS = exp.Substring(lastComma + 1).Trim(); + // Detect whether the natvis 'na' modifier is present in the original format specifier. + // We must detect this before we strip modifiers below. + formatNa = expFS.IndexOf("na", StringComparison.Ordinal) >= 0; // Strip off modifiers that may be included together with another format specifier, e.g. 'nvoXb' is a valid format specifier, but we only care about the 'Xb' part // This is not quite the right fix -- really the below switch statement should be a series of if statements. But since none of the supported format specifiers @@ -698,6 +724,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw _attribsFetched = true; } Value = results.TryFindString("value"); + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } if ((string.IsNullOrEmpty(Value) || _format != null) && !string.IsNullOrEmpty(_internalName)) { if (_format != null) @@ -711,6 +741,10 @@ internal async Task Eval(uint radix, enum_EVALFLAGS dwFlags = 0, DAPEvalFlags dw if (results.ResultClass == ResultClass.done) { Value = results.FindString("value"); + if (_formatHasNa) + { + Value = StripLeadingAddress(Value); + } } else if (results.ResultClass == ResultClass.error) { diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 5e7e30d05..9ad2a437e 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -1283,12 +1283,23 @@ private string FormatValue(string format, IVariableInformation variable, IDictio if (m.Success) { string rawExpr = format.Substring(i + 1, m.Length - 2); - string spec = ExtractFormatSpecifier(rawExpr); + // Substitute template parameter macros ($T1, $T2, ...) inside the whole brace + // expression (this covers both the expression and any trailing format specifier) + if (scopedNames != null) + { + rawExpr = Regex.Replace(rawExpr, @"\$T\d+", (Match mt) => + { + if (scopedNames.TryGetValue(mt.Value, out string replacement)) + return replacement; + return mt.Value; + }); + } + ExtractFormatSpecifier(rawExpr, out bool hasNa); string exprValue = GetExpressionValue(rawExpr, variable, scopedNames, intrinsics); - if (spec == "sub" || spec == "su") - exprValue = CleanUtf16StringValue(exprValue); - else if (spec == "sb") - exprValue = CleanAsciiStringValue(exprValue); + if (hasNa) + { + exprValue = VariableInformation.StripLeadingAddress(exprValue); + } value.Append(exprValue); i += m.Length - 1; } @@ -1494,30 +1505,40 @@ private static int FindLastTopLevelComma(string expression) /// : modifiers "nvo", "na", /// "nr", "nd" are stripped before returning. Returns null when no specifier is present. /// - internal static string ExtractFormatSpecifier(string expression) + internal static string ExtractFormatSpecifier(string expression, out bool hasNa) { + hasNa = false; int commaPos = FindLastTopLevelComma(expression); if (commaPos < 0) return null; + + string tail = expression.Substring(commaPos + 1).Trim(); + hasNa = tail.IndexOf("na", StringComparison.Ordinal) >= 0; + return expression.Substring(commaPos + 1).Trim() .Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", ""); } + /// + /// Returns true if the NatVis expression's trailing format specifier (the part after the + /// last top-level comma) contains the "na" modifier. This intentionally inspects the + /// raw specifier text and does not normalize/remove modifiers so callers can detect + /// whether the original expression asked for the "na" behavior. + /// /// /// Cleans up the raw value that GDB/LLDB returns for a const char16_t* /// expression (i.e. one evaluated with the ,sub / ,su format specifier). /// GDB and LLDB both prefix the string with the pointer address, e.g. /// 0x00007fff5fbff6c0 u"Hello" - /// This method strips the address and the surrounding u"…" quotes so that - /// the NatVis DisplayString shows just the string content. + /// This method strips the leading address prefix that GDB/LLDB emits ("0x... "). + /// It does NOT remove surrounding quotes or the leading character-width prefix (u/U). /// internal static string CleanUtf16StringValue(string value) { if (string.IsNullOrEmpty(value)) return value; // Strip leading "0x " address prefix emitted by GDB/LLDB. - value = s_addressPrefix.Replace(value, ""); + value = VariableInformation.StripLeadingAddress(value); // Strip surrounding u"..." or U"..." quotes. - if (value.Length >= 3 && - (value.StartsWith("u\"", StringComparison.Ordinal) || value.StartsWith("U\"", StringComparison.Ordinal))) + if (value.Length >= 3 && (value.StartsWith("u\"", StringComparison.Ordinal) || value.StartsWith("U\"", StringComparison.Ordinal))) { value = value.EndsWith("\"", StringComparison.Ordinal) ? value.Substring(2, value.Length - 3) @@ -1531,15 +1552,13 @@ internal static string CleanUtf16StringValue(string value) /// (i.e. one evaluated with the ,sb format specifier). /// GDB and LLDB prefix the string with the pointer address, e.g. /// 0x00007fff5fbff6c0 "Hello" - /// This method strips the address and the surrounding "…" quotes so that - /// the NatVis DisplayString shows just the string content (matching VS behaviour, - /// where {ptr,sb} evaluates to bare text without quotes). + /// This method strips the leading address prefix that GDB/LLDB emits ("0x... "). /// internal static string CleanAsciiStringValue(string value) { if (string.IsNullOrEmpty(value)) return value; // Strip leading "0x " address prefix emitted by GDB/LLDB. - value = s_addressPrefix.Replace(value, ""); + value = VariableInformation.StripLeadingAddress(value); // Strip surrounding "..." quotes. if (value.Length >= 2 && value.StartsWith("\"", StringComparison.Ordinal)) { diff --git a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs index 287be0072..02e9a94b0 100644 --- a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs +++ b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs @@ -15,33 +15,34 @@ public class NatvisFormatSpecifierTest [Fact] public void ExtractFormatSpecifier_Sub_Extracted() { - Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub")); + Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub", out _)); } [Fact] public void ExtractFormatSpecifier_Decimal_Extracted() { - Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d")); + Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d", out _)); } [Fact] public void ExtractFormatSpecifier_NoSpecifier_ReturnsNull() { - Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1")); + Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1", out _)); } [Fact] public void ExtractFormatSpecifier_NvoModifierStripped() { // "nvoXb": strip "nvo" modifier, result is "Xb" - Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb")); + Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb", out _)); } [Fact] public void ExtractFormatSpecifier_NaModifierStripped() { // "view(RecZone)na": strip "na", result is "view(RecZone)" - Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na")); + Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na", out bool hasNa)); + Assert.True(hasNa); } // -- CleanUtf16StringValue --------------------------------------------