diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index fdd8a8001..5e7e30d05 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -247,6 +247,8 @@ public VisualizerInfo(VisualizerType viz, TypeName name) private static Regex s_expression = new Regex(@"^\{[^\}]*\}"); private static readonly Regex s_moduleQualifiedPrefix = new Regex(@"\w+(?:\.\w+)*\.(?:dll|exe)!", RegexOptions.IgnoreCase); private static readonly Regex s_intrinsicCallPattern = new Regex(@"\b(\w+)\s*\("); + // Matches the leading "0x " address that GDB/LLDB prepends when displaying a string pointer value. + private static readonly Regex s_addressPrefix = new Regex(@"^0x[0-9a-fA-F]+\s+"); private List _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; @@ -1280,7 +1282,13 @@ private string FormatValue(string format, IVariableInformation variable, IDictio Match m = s_expression.Match(format.Substring(i)); if (m.Success) { - string exprValue = GetExpressionValue(format.Substring(i + 1, m.Length - 2), variable, scopedNames, intrinsics); + string rawExpr = format.Substring(i + 1, m.Length - 2); + string spec = ExtractFormatSpecifier(rawExpr); + string exprValue = GetExpressionValue(rawExpr, variable, scopedNames, intrinsics); + if (spec == "sub" || spec == "su") + exprValue = CleanUtf16StringValue(exprValue); + else if (spec == "sb") + exprValue = CleanAsciiStringValue(exprValue); value.Append(exprValue); i += m.Length - 1; } @@ -1460,6 +1468,88 @@ internal static List SplitArguments(string argsText) return result; } + /// + /// Returns the index of the last top-level comma in , + /// i.e. a comma not nested inside any parentheses or square brackets. + /// Returns -1 when no such comma exists. + /// + private static int FindLastTopLevelComma(string expression) + { + int depth = 0; + int lastTopLevelComma = -1; + for (int i = 0; i < expression.Length; i++) + { + char c = expression[i]; + if (c == '(' || c == '[') depth++; + else if (c == ')' || c == ']') depth--; + else if (c == ',' && depth == 0) + lastTopLevelComma = i; + } + return lastTopLevelComma; + } + + /// + /// Returns the format specifier from a NatVis expression (the part after the last + /// top-level comma), normalized the same way as + /// : modifiers "nvo", "na", + /// "nr", "nd" are stripped before returning. Returns null when no specifier is present. + /// + internal static string ExtractFormatSpecifier(string expression) + { + int commaPos = FindLastTopLevelComma(expression); + if (commaPos < 0) return null; + return expression.Substring(commaPos + 1).Trim() + .Replace("nvo", "").Replace("na", "").Replace("nr", "").Replace("nd", ""); + } + + /// + /// 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. + /// + 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, ""); + // Strip surrounding u"..." or U"..." quotes. + 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) + : value.Substring(2); + } + return value; + } + + /// + /// Cleans up the raw value that GDB/LLDB returns for a char* expression + /// (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). + /// + 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, ""); + // Strip surrounding "..." quotes. + if (value.Length >= 2 && value.StartsWith("\"", StringComparison.Ordinal)) + { + value = value.EndsWith("\"", StringComparison.Ordinal) + ? value.Substring(1, value.Length - 2) + : value.Substring(1); + } + return value; + } + /// /// Substitute named parameters in an intrinsic expression with the supplied argument /// values. Each parameter name is replaced as a whole word so that e.g. "val" inside diff --git a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs new file mode 100644 index 000000000..287be0072 --- /dev/null +++ b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs @@ -0,0 +1,117 @@ +using Xunit; +using Microsoft.MIDebugEngine.Natvis; + +namespace MIDebugEngineUnitTests +{ + /// + /// Unit tests for , + /// and + /// . + /// + public class NatvisFormatSpecifierTest + { + // -- ExtractFormatSpecifier ------------------------------------------- + + [Fact] + public void ExtractFormatSpecifier_Sub_Extracted() + { + Assert.Equal("sub", Natvis.ExtractFormatSpecifier("schemeStr(),sub")); + } + + [Fact] + public void ExtractFormatSpecifier_Decimal_Extracted() + { + Assert.Equal("d", Natvis.ExtractFormatSpecifier("year(),d")); + } + + [Fact] + public void ExtractFormatSpecifier_NoSpecifier_ReturnsNull() + { + Assert.Null(Natvis.ExtractFormatSpecifier("cspec == 1")); + } + + [Fact] + public void ExtractFormatSpecifier_NvoModifierStripped() + { + // "nvoXb": strip "nvo" modifier, result is "Xb" + Assert.Equal("Xb", Natvis.ExtractFormatSpecifier("data1,nvoXb")); + } + + [Fact] + public void ExtractFormatSpecifier_NaModifierStripped() + { + // "view(RecZone)na": strip "na", result is "view(RecZone)" + Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na")); + } + + // -- CleanUtf16StringValue -------------------------------------------- + + [Fact] + public void CleanUtf16StringValue_AddressAndQuotes_Stripped() + { + Assert.Equal("Hello World", Natvis.CleanUtf16StringValue("0x00007fff5fbff6c0 u\"Hello World\"")); + } + + [Fact] + public void CleanUtf16StringValue_NoAddress_QuotesStripped() + { + Assert.Equal("Hello", Natvis.CleanUtf16StringValue("u\"Hello\"")); + } + + [Fact] + public void CleanUtf16StringValue_UpperCaseU_QuotesStripped() + { + Assert.Equal("Hello", Natvis.CleanUtf16StringValue("U\"Hello\"")); + } + + [Fact] + public void CleanUtf16StringValue_TruncatedNoClosingQuote_PrefixStripped() + { + Assert.Equal("Hello...", Natvis.CleanUtf16StringValue("0x00007fff u\"Hello...")); + } + + [Fact] + public void CleanUtf16StringValue_Empty_ReturnsEmpty() + { + Assert.Equal("", Natvis.CleanUtf16StringValue("")); + } + + [Fact] + public void CleanUtf16StringValue_NoPrefix_Unchanged() + { + Assert.Equal("42", Natvis.CleanUtf16StringValue("42")); + } + + // -- CleanAsciiStringValue -------------------------------------------- + + [Fact] + public void CleanAsciiStringValue_AddressAndQuotes_Stripped() + { + Assert.Equal("Hello World", Natvis.CleanAsciiStringValue("0x00007fff5fbff6c0 \"Hello World\"")); + } + + [Fact] + public void CleanAsciiStringValue_NoAddress_QuotesStripped() + { + Assert.Equal("Hello", Natvis.CleanAsciiStringValue("\"Hello\"")); + } + + [Fact] + public void CleanAsciiStringValue_TruncatedNoClosingQuote_PrefixAndOpenQuoteStripped() + { + Assert.Equal("Hello...", Natvis.CleanAsciiStringValue("0x00007fff \"Hello...")); + } + + [Fact] + public void CleanAsciiStringValue_Empty_ReturnsEmpty() + { + Assert.Equal("", Natvis.CleanAsciiStringValue("")); + } + + [Fact] + public void CleanAsciiStringValue_NoPrefix_Unchanged() + { + Assert.Equal("42", Natvis.CleanAsciiStringValue("42")); + } + } +}