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
42 changes: 38 additions & 4 deletions src/MIDebugEngine/Engine.Impl/Variables.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -92,6 +93,18 @@ internal sealed class VariableInformation : IVariableInformation

static readonly Lazy<Regex> s_addressPattern = new Lazy<Regex>(() => 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"
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -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"))
{
Expand Down Expand Up @@ -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;

Expand All @@ -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 _))
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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)
{
Expand Down
36 changes: 24 additions & 12 deletions src/MIDebugEngine/Natvis.Impl/Natvis.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1283,12 +1283,16 @@ private string FormatValue(string format, IVariableInformation variable, IDictio
if (m.Success)
{
string rawExpr = format.Substring(i + 1, m.Length - 2);
string spec = ExtractFormatSpecifier(rawExpr);
string spec = ExtractFormatSpecifier(rawExpr, out bool hasNa);
string exprValue = GetExpressionValue(rawExpr, variable, scopedNames, intrinsics);
if (spec == "sub" || spec == "su")
if (spec == "sub")
exprValue = CleanUtf16StringValue(exprValue);
else if (spec == "sb")
exprValue = CleanAsciiStringValue(exprValue);

@gregg-miskelly gregg-miskelly Jul 22, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Did you mean to loose this sub/su/sb code?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Actually, is su needed? Variabel.cs line 477 already handles su correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This problem seems to still be here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This is still not addressed - you want to put back the old code for sub/sb

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

if (hasNa)
{
exprValue = VariableInformation.StripLeadingAddress(exprValue);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggestion: Make VariableInformation.StripLeadingAddress public so you can use it here

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

value.Append(exprValue);
i += m.Length - 1;
}
Expand Down Expand Up @@ -1494,30 +1498,40 @@ private static int FindLastTopLevelComma(string expression)
/// <see cref="VariableInformation.ProcessFormatSpecifiers"/>: modifiers "nvo", "na",
/// "nr", "nd" are stripped before returning. Returns null when no specifier is present.
/// </summary>
internal static string ExtractFormatSpecifier(string expression)
internal static string ExtractFormatSpecifier(string expression, out bool hasNa)
Comment thread
SachinM123 marked this conversation as resolved.
{
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", "");
}

/// <summary>
/// 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.
/// </summary>
/// <summary>
/// Cleans up the raw value that GDB/LLDB returns for a <c>const char16_t*</c>
/// expression (i.e. one evaluated with the <c>,sub</c> / <c>,su</c> format specifier).
/// GDB and LLDB both prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 u"Hello"</c>
/// This method strips the address and the surrounding <c>u"…"</c> 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).
/// </summary>
internal static string CleanUtf16StringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " 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)
Expand All @@ -1531,15 +1545,13 @@ internal static string CleanUtf16StringValue(string value)
/// (i.e. one evaluated with the <c>,sb</c> format specifier).
/// GDB and LLDB prefix the string with the pointer address, e.g.
/// <c>0x00007fff5fbff6c0 "Hello"</c>
/// This method strips the address and the surrounding <c>"…"</c> quotes so that
/// the NatVis DisplayString shows just the string content (matching VS behaviour,
/// where <c>{ptr,sb}</c> evaluates to bare text without quotes).
/// This method strips the leading address prefix that GDB/LLDB emits ("0x... ").
/// </summary>
internal static string CleanAsciiStringValue(string value)
{
if (string.IsNullOrEmpty(value)) return value;
// Strip leading "0x<hex> " 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))
{
Expand Down
11 changes: 6 additions & 5 deletions src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 --------------------------------------------
Expand Down
Loading