diff --git a/src/MIDebugEngine/AD7.Impl/AD7Property.cs b/src/MIDebugEngine/AD7.Impl/AD7Property.cs
index 422318a3a..0da96f5ab 100644
--- a/src/MIDebugEngine/AD7.Impl/AD7Property.cs
+++ b/src/MIDebugEngine/AD7.Impl/AD7Property.cs
@@ -72,7 +72,7 @@ public DEBUG_PROPERTY_INFO ConstructDebugPropertyInfo(enum_DEBUGPROP_INFO_FLAGS
if ((dwFields & enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE) != 0)
{
- (propertyInfo.bstrValue, _uiVisualizers) = _engine.DebuggedProcess.Natvis.FormatDisplayString(variable);
+ (propertyInfo.bstrValue, _uiVisualizers) = _engine.DebuggedProcess.Natvis.FormatDisplayString(variable, variable.NatvisView);
propertyInfo.dwFields |= enum_DEBUGPROP_INFO_FLAGS.DEBUGPROP_INFO_VALUE;
}
@@ -154,7 +154,7 @@ public int EnumChildren(enum_DEBUGPROP_INFO_FLAGS dwFields, uint dwRadix, ref Gu
try
{
_engine.DebuggedProcess.Natvis.WaitDialog.ShowWaitDialog(_variableInformation.Name);
- var children = _engine.DebuggedProcess.Natvis.Expand(_variableInformation);
+ var children = _engine.DebuggedProcess.Natvis.Expand(_variableInformation, _variableInformation.NatvisView);
// Count number of children that fit filter (results saved in "fitsFilter")
int propertyCount = children.Length;
diff --git a/src/MIDebugEngine/Engine.Impl/Variables.cs b/src/MIDebugEngine/Engine.Impl/Variables.cs
index 031dd40c8..0b4f1dc81 100644
--- a/src/MIDebugEngine/Engine.Impl/Variables.cs
+++ b/src/MIDebugEngine/Engine.Impl/Variables.cs
@@ -35,6 +35,7 @@ internal interface IVariableInformation : IDisposable
VariableInformation FindChildByName(string name);
string EvalDependentExpression(string expr);
bool IsVisualized { get; }
+ string NatvisView { get; }
bool IsReadOnly();
bool IsNullPointer();
enum_DEBUGPROP_INFO_FLAGS PropertyInfoFlags { get; set; }
@@ -375,6 +376,7 @@ public VariableInformation FindChildByName(string name)
private DeferedFormatExpression _deferedFormatExpression;
private IVariableInformation _parent;
private string _format;
+ private string _natvisView; // view name from a "view(name)" format specifier
private string _strippedName; // "Name" stripped of format specifiers
private string _fullname;
@@ -392,6 +394,8 @@ public enum NodeType
public NodeType VariableNodeType { get; private set; }
+ public string NatvisView { get { return _natvisView; } }
+
private static readonly string[] s_stringTypes = new string[] {
@"^char *\*$",
@"^char *\[[0-9]*\]$",
@@ -417,6 +421,17 @@ private string ProcessFormatSpecifiers(string exp, out string formatSpecifier)
// Find the format specifier expression
string expFS = exp.Substring(lastComma + 1).Trim();
+ // A view() specifier (e.g. "obj,view(simple)") selects a named natvis view; it is
+ // consumed by natvis formatting/expansion rather than the debugger. Handle it before
+ // the modifier stripping below, which could corrupt a view name that contains one of
+ // the modifier letter pairs (e.g. "view(second)").
+ string viewName = Natvis.Natvis.ExtractViewName(expFS);
+ if (viewName != null)
+ {
+ _natvisView = viewName;
+ return exp.Substring(0, lastComma);
+ }
+
// 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
// contain any of these characters we can fix this the simple way and remove them.
diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs
index 5e7e30d05..145ddcedb 100755
--- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs
+++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs
@@ -43,6 +43,7 @@ public SimpleWrapper(string name, AD7Engine engine, IVariableInformation underly
public bool IsStringType { get { return Parent.IsStringType; } }
public ThreadContext ThreadContext { get { return Parent.ThreadContext; } }
public virtual bool IsVisualized { get { return Parent.IsVisualized; } }
+ public string NatvisView { get { return Parent.NatvisView; } }
public virtual enum_DEBUGPROP_INFO_FLAGS PropertyInfoFlags { get; set; }
public virtual bool IsReadOnly() => Parent.IsReadOnly();
public bool IsNullPointer() => Parent.IsNullPointer();
@@ -73,6 +74,56 @@ public uint Size()
}
}
+ ///
+ /// A synthetic, non-evaluatable child row that displays a fixed diagnostic message as an
+ /// error value, e.g. when a CustomListItems loop cannot continue.
+ ///
+ internal class MessageVariableInformation : IVariableInformation
+ {
+ public MessageVariableInformation(string name, string message, AD7Engine engine, IVariableInformation parent)
+ {
+ Name = name;
+ Value = message;
+ _engine = engine;
+ _parent = parent;
+ }
+
+ private readonly AD7Engine _engine;
+ private readonly IVariableInformation _parent;
+
+ public string Name { get; }
+ public string Value { get; }
+ public string TypeName { get { return null; } }
+ public bool IsParameter { get { return false; } }
+ public VariableInformation[] Children { get { return null; } }
+ public AD7Thread Client { get { return _parent.Client; } }
+ public bool Error { get { return true; } }
+ public uint CountChildren { get { return 0; } }
+ public bool IsChild { get; set; }
+ public enum_DBG_ATTRIB_FLAGS Access { get { return enum_DBG_ATTRIB_FLAGS.DBG_ATTRIB_ACCESS_NONE; } }
+ public bool IsStringType { get { return false; } }
+ public ThreadContext ThreadContext { get { return _parent.ThreadContext; } }
+ public bool IsVisualized { get { return false; } }
+ public string NatvisView { get { return null; } }
+ public enum_DEBUGPROP_INFO_FLAGS PropertyInfoFlags { get; set; }
+ public bool IsPreformatted { get { return true; } set { } }
+ public string FullName() { return null; }
+ public void EnsureChildren() { }
+ public void AsyncEval(IDebugEventCallback2 pExprCallback) { }
+ public void AsyncError(IDebugEventCallback2 pExprCallback, IDebugProperty2 error)
+ {
+ VariableInformation.AsyncErrorImpl(pExprCallback != null ? new EngineCallback(_engine, pExprCallback) : _engine.Callback, this, error);
+ }
+ public void SyncEval(enum_EVALFLAGS dwFlags, DAPEvalFlags dwDAPFlags) { }
+ public VariableInformation FindChildByName(string name) { return null; }
+ public string EvalDependentExpression(string expr) { return null; }
+ public bool IsReadOnly() { return true; }
+ public bool IsNullPointer() { return false; }
+ public string Address() { return null; }
+ public uint Size() { return 0; }
+ public void Dispose() { }
+ }
+
internal class VisualizerWrapper : SimpleWrapper
{
public readonly Natvis.VisualizerInfo Visualizer;
@@ -117,6 +168,24 @@ public LinkedListContinueWrapper(string name, AD7Engine engine, IVariableInforma
}
}
+ ///
+ /// Represents the continuation of a CustomListItemsType: the saved natvis local-variable
+ /// table and loop position let the "[More...]" expansion resume where the previous page
+ /// stopped instead of re-walking the list from the start. StartIndex (from the base class)
+ /// is the global item index to continue numbering from.
+ ///
+ internal sealed class CustomListContinueWrapper : PaginatedVisualizerWrapper
+ {
+ public readonly Dictionary SavedLocalVars;
+ public readonly int LoopIndex;
+ public CustomListContinueWrapper(string name, AD7Engine engine, IVariableInformation underlyingVariable, Natvis.VisualizerInfo viz, bool isVisualizerView, Dictionary savedLocalVars, int loopIndex, uint startIndex)
+ : base(name, engine, underlyingVariable, viz, isVisualizerView, startIndex)
+ {
+ SavedLocalVars = savedLocalVars;
+ LoopIndex = loopIndex;
+ }
+ }
+
internal class Node
{
public enum ScanState
@@ -249,6 +318,23 @@ public VisualizerInfo(VisualizerType viz, TypeName name)
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+");
+ // Matches "varName = rhs" in a CustomListItems expression to detect local-variable assignments.
+ // The negative look-ahead (?!=) prevents matching "==" comparison operators.
+ private static readonly Regex s_execAssignment = new Regex(@"^\s*(\w+)\s*=(?!=)\s*(.+)$", RegexOptions.Singleline | RegexOptions.Compiled);
+ // Matches the bare "$i" token (word boundary) in a CustomListItems Name template.
+ // The {$i} form is matched first with a plain Replace; this regex handles bare "$i"
+ // with a word-boundary guard so that e.g. "$item" is not corrupted.
+ private static readonly Regex s_dollarI = new Regex(@"\$i\b", RegexOptions.Compiled);
+ // Matches increment/decrement shorthand in : ++i, i++, --i, i--
+ // Groups: (1) prefix-op (2) prefix-varname | (3) postfix-varname (4) postfix-op
+ private static readonly Regex s_execIncrDecr = new Regex(@"^\s*(?:(\+\+|--)(\w+)|(\w+)(\+\+|--))\s*$", RegexOptions.Compiled);
+ // Matches a plain (optionally signed) decimal integer literal.
+ private static readonly Regex s_integerLiteral = new Regex(@"^\s*-?\d+\s*$", RegexOptions.Compiled);
+ // Matches a hex integer literal — how integers render when the engine radix is 16.
+ private static readonly Regex s_hexIntegerLiteral = new Regex(@"^0x[0-9a-fA-F]+$", RegexOptions.Compiled);
+ // Matches the leading hex address of a raw pointer value. GDB may append a symbol or
+ // string ("0x5555 ", "0x5555 \"text\""), so only the leading token is captured.
+ private static readonly Regex s_leadingHexAddress = new Regex(@"^\s*(0x[0-9a-fA-F]+)\b", RegexOptions.Compiled);
private List _typeVisualizers;
private DebuggedProcess _process;
private HostConfigurationStore _configStore;
@@ -453,7 +539,7 @@ private bool LoadFile(string path)
}
}
- internal (string value, VisualizerId[] uiVisualizers) FormatDisplayString(IVariableInformation variable)
+ internal (string value, VisualizerId[] uiVisualizers) FormatDisplayString(IVariableInformation variable, string currentView = null)
{
VisualizerInfo visualizer = null;
try
@@ -479,6 +565,15 @@ private bool LoadFile(string path)
{
DisplayStringType display = item as DisplayStringType;
// e.g. {{ size={_Mypair._Myval2._Mylast - _Mypair._Myval2._Myfirst} }}
+
+ // IncludeView: only use this DisplayString when the named view is active.
+ if (!IsIncludeViewMatch(display.IncludeView, currentView))
+ continue;
+
+ // ExcludeView: skip this DisplayString when the current view is in the excluded list.
+ if (IsExcludeViewMatch(display.ExcludeView, currentView))
+ continue;
+
if (!EvalCondition(display.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics))
{
continue;
@@ -522,7 +617,7 @@ private IVariableInformation GetVisualizationWrapper(IVariableInformation variab
return new VisualizerWrapper(ResourceStrings.VisualizedView, _process.Engine, variable, visualizer, isVisualizerView: true);
}
- internal IVariableInformation[] Expand(IVariableInformation variable)
+ internal IVariableInformation[] Expand(IVariableInformation variable, string currentView = null)
{
try
{
@@ -530,7 +625,7 @@ internal IVariableInformation[] Expand(IVariableInformation variable)
if (variable.IsVisualized
|| ((ShowDisplayStrings == DisplayStringsState.On) && !(variable is VisualizerWrapper))) // visualize right away if DisplayStringsState.On, but only if not dummy var ([Raw View])
{
- return ExpandVisualized(variable);
+ return ExpandVisualized(variable, currentView);
}
IVariableInformation visView = GetVisualizationWrapper(variable);
if (visView == null)
@@ -587,7 +682,7 @@ internal string GetUIVisualizerName(string serviceId, int id)
private delegate IVariableInformation Traverse(IVariableInformation node);
- private IVariableInformation[] ExpandVisualized(IVariableInformation variable)
+ private IVariableInformation[] ExpandVisualized(IVariableInformation variable, string currentView = null)
{
VisualizerInfo visualizer = FindType(variable);
if (visualizer == null)
@@ -605,6 +700,10 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable)
if (i is ItemType && !(variable is PaginatedVisualizerWrapper)) // we do not want to repeatedly display other ItemTypes when expanding the "[More...]" node
{
ItemType item = (ItemType)i;
+ if (!IsIncludeViewMatch(item.IncludeView, currentView))
+ continue;
+ if (IsExcludeViewMatch(item.ExcludeView, currentView))
+ continue;
if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics))
{
continue;
@@ -938,6 +1037,13 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable)
// _Myptr
//
//
+
+ // IncludeView/ExcludeView: skip this ExpandedItem if the current view doesn't match.
+ if (!IsIncludeViewMatch(item.IncludeView, currentView))
+ continue;
+ if (IsExcludeViewMatch(item.ExcludeView, currentView))
+ continue;
+
if (item.Condition != null)
{
if (!EvalCondition(item.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics))
@@ -949,13 +1055,92 @@ private IVariableInformation[] ExpandVisualized(IVariableInformation variable)
{
continue;
}
- var expand = GetExpression(item.Value, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics);
- var eChildren = Expand(expand);
+
+ // A view() specifier on the ExpandedItem expression (e.g. "inner(),view(myview)")
+ // means: expand the result but show its children in the named view.
+ // Strip the specifier before evaluating the expression, then pass the
+ // view name into the recursive Expand call so that IncludeView guards
+ // on the target's Expand elements (including CustomListItems) match.
+ string rawExpr = item.Value.Trim();
+ string spec = ExtractFormatSpecifier(rawExpr);
+ string viewName = ExtractViewName(spec);
+ string exprToEval = viewName != null ? StripFormatSpecifier(rawExpr) : rawExpr;
+ string childView = viewName ?? currentView;
+
+ var expand = GetExpression(exprToEval, variable, visualizer.ScopedNames, intrinsics: visualizer.Intrinsics);
+ var eChildren = Expand(expand, childView);
if (eChildren != null)
{
children.AddRange(eChildren);
}
}
+ else if (i is CustomListItemsType)
+ {
+ CustomListItemsType customList = (CustomListItemsType)i;
+ if (!IsIncludeViewMatch(customList.IncludeView, currentView)) continue;
+ if (IsExcludeViewMatch(customList.ExcludeView, currentView)) continue;
+ if (!EvalCondition(customList.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) continue;
+ if (customList.Loop == null || customList.Loop.Length == 0) continue;
+
+ // Build the natvis local-variable table from elements.
+ // Each entry maps the declared name to its current expression string; expressions are
+ // substituted in-place whenever the name appears in subsequent loop-body expressions.
+ // A "[More...]" continuation instead restores the table saved when the previous page
+ // filled, so the walk resumes where it stopped rather than re-running from the start.
+ var localVars = new Dictionary(StringComparer.Ordinal);
+ var continueCLI = variable as CustomListContinueWrapper;
+ if (continueCLI != null)
+ {
+ foreach (var savedVar in continueCLI.SavedLocalVars)
+ localVars[savedVar.Key] = savedVar.Value;
+ }
+ else if (customList.Items != null)
+ {
+ foreach (var v in customList.Items)
+ {
+ if (string.IsNullOrEmpty(v.Name)) continue;
+ string initVal = v.InitialValue ?? "0";
+ // An InitialValue may reference earlier-declared s (per natvis.xsd),
+ // e.g. . Substitute those first
+ // (the table is built in document order, so earlier names are already present),
+ // then resolve field names, template parameters and intrinsics.
+ string withLocals = SubstituteLocalVars(initVal, localVars);
+ localVars[v.Name] = ReplaceNamesInExpression(withLocals, variable, visualizer.ScopedNames, visualizer.Intrinsics);
+ }
+ }
+
+ // Optional element provides an upper bound for children (and drives pagination).
+ uint totalSize = uint.MaxValue;
+ if (customList.Items1 != null)
+ {
+ foreach (var sz in customList.Items1)
+ {
+ if (!EvalCondition(sz.Condition, variable, visualizer.ScopedNames, visualizer.Intrinsics)) continue;
+ try
+ {
+ string szExpr = SubstituteLocalVars(sz.Value?.Trim() ?? "0", localVars);
+ string szVal = GetExpressionValue(szExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics);
+ totalSize = MICore.Debugger.ParseUint(szVal, throwOnError: true);
+ }
+ catch (Exception) { /* leave totalSize as MaxValue so Break drives termination */ }
+ break;
+ }
+ }
+
+ uint startIndex = 0;
+ if (variable is PaginatedVisualizerWrapper pvwCLI)
+ startIndex = pvwCLI.StartIndex;
+
+ var ctx = new CustomListLoopContext(startIndex, totalSize);
+ if (continueCLI != null)
+ ctx.GlobalIndex = startIndex; // resume numbering where the previous page stopped
+
+ for (int loopIndex = continueCLI?.LoopIndex ?? 0; loopIndex < customList.Loop.Length; loopIndex++)
+ {
+ if (ctx.Done) break;
+ DriveLoop(customList.Loop[loopIndex], ctx, variable, visualizer, localVars, children, loopIndex);
+ }
+ }
}
if (!(variable is VisualizerWrapper) && !expandType.HideRawView) // don't stack wrappers, and respect HideRawView
{
@@ -1122,12 +1307,31 @@ private bool EvalCondition(string condition, IVariableInformation variable, IDic
bool res = true;
if (!String.IsNullOrWhiteSpace(condition))
{
- string exprValue = GetExpressionValue(condition, variable, scopedNames, intrinsics);
+ try
+ {
+ string exprValue = GetExpressionValue(condition, variable, scopedNames, intrinsics);
- bool exprBool = false;
- int exprInt = 0;
- res = !String.IsNullOrEmpty(exprValue) &&
- ((bool.TryParse(exprValue, out exprBool) && exprBool) || (int.TryParse(exprValue, out exprInt) && exprInt > 0));
+ bool exprBool = false;
+ int exprInt = 0;
+ res = !String.IsNullOrEmpty(exprValue) &&
+ ((bool.TryParse(exprValue, out exprBool) && exprBool) || (int.TryParse(exprValue, out exprInt) && exprInt > 0));
+ }
+ catch (MICore.MIException e)
+ {
+ // Expected failure path: the debugger rejected the expression
+ // (e.g. expression too long, unknown symbol).
+ // Treat as false so the next DisplayString is tried as a fallback.
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Verbose, "EvalCondition failed: " + e.Message);
+ res = false;
+ }
+ catch (Exception e)
+ {
+ // Unexpected failure (e.g. NullReferenceException in the evaluation path).
+ // Still return false to avoid surfacing natvis errors as debug session failures,
+ // but log at Warning so unexpected exceptions are not silently swallowed.
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, "EvalCondition unexpected exception: " + e.Message);
+ res = false;
+ }
}
return res;
}
@@ -1282,13 +1486,31 @@ private string FormatValue(string format, IVariableInformation variable, IDictio
Match m = s_expression.Match(format.Substring(i));
if (m.Success)
{
- string rawExpr = format.Substring(i + 1, m.Length - 2);
+ // Trim whitespace (including newlines from multi-line XML blocks) so that
+ // the expression never starts with \n, which would break LLDB MI's line-based protocol.
+ string rawExpr = format.Substring(i + 1, m.Length - 2).Trim();
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);
+ string exprValue;
+ string viewName = ExtractViewName(spec);
+ if (viewName != null)
+ {
+ // {expr,view(name)} -- format expr using the named view's DisplayString.
+ // Any other specifiers combined with view() (e.g. "na", "sub", "sb") are
+ // intentionally ignored: specifiers like sub/sb exist to post-process raw
+ // debugger output (stripping address prefixes and quotes), but view() already
+ // produces fully-formatted text via FormatDisplayString — applying those
+ // post-processors on top would corrupt the result.
+ string strippedExpr = StripFormatSpecifier(rawExpr);
+ exprValue = GetExpressionValue(strippedExpr, variable, scopedNames, intrinsics, viewName);
+ }
+ else
+ {
+ 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;
}
@@ -1488,6 +1710,58 @@ private static int FindLastTopLevelComma(string expression)
return lastTopLevelComma;
}
+ ///
+ /// Strips a NatVis format specifier (e.g. ",sub", ",d", ",view(name)na") from the end of
+ /// an expression, returning the bare expression. The specifier boundary is the last
+ /// top-level comma (not nested inside any parentheses or square brackets).
+ ///
+ internal static string StripFormatSpecifier(string expression)
+ {
+ int commaPos = FindLastTopLevelComma(expression);
+ return commaPos >= 0
+ ? expression.Substring(0, commaPos).TrimEnd()
+ : expression;
+ }
+
+ ///
+ /// Returns true when is listed in the semicolon-separated
+ /// IncludeView attribute, i.e. the DisplayString should only be shown in one of those views.
+ /// An empty or null includeView means "show in all views" (returns true for any currentView).
+ /// Both IncludeView and ExcludeView are defined as semicolon-delimited lists in the natvis XSD.
+ ///
+ internal static bool IsIncludeViewMatch(string includeView, string currentView)
+ {
+ if (string.IsNullOrEmpty(includeView)) return true;
+ if (currentView == null) return false;
+ return includeView.Split(';').Any(v => string.Equals(v.Trim(), currentView, StringComparison.Ordinal));
+ }
+
+ ///
+ /// Returns true when is listed in the semicolon-separated
+ /// ExcludeView attribute, i.e. the DisplayString should be skipped in this view.
+ /// An empty/null excludeView or a null currentView never excludes.
+ ///
+ internal static bool IsExcludeViewMatch(string excludeView, string currentView)
+ {
+ if (string.IsNullOrEmpty(excludeView) || currentView == null) return false;
+ return excludeView.Split(';').Any(v => string.Equals(v.Trim(), currentView, StringComparison.Ordinal));
+ }
+
+ ///
+ /// If is a view specifier of the form "view(name)" or
+ /// "view(name)na", returns the view name. Otherwise returns null.
+ ///
+ internal static string ExtractViewName(string spec)
+ {
+ if (spec == null) return null;
+ if (!spec.StartsWith("view(", StringComparison.Ordinal)) return null;
+ int closeParen = spec.IndexOf(')');
+ if (closeParen < 0) return null;
+ string name = spec.Substring(5, closeParen - 5);
+ // view() with an empty name is not a valid specifier; treat as absent.
+ return name.Length > 0 ? name : null;
+ }
+
///
/// Returns the format specifier from a NatVis expression (the part after the last
/// top-level comma), normalized the same way as
@@ -1665,13 +1939,16 @@ internal static string ResolveIntrinsicCalls(string expression, IDictionary scopedNames, IDictionary intrinsics = null)
{
+ // Expand intrinsic calls FIRST so that dll!-qualified type names that appear
+ // inside intrinsic bodies (e.g. "(Foo.dll!MyType*)ptr") are also stripped
+ // in the next step.
+ expression = ResolveIntrinsicCalls(expression, intrinsics);
+
// Strip Windows dll!-qualified type prefixes (e.g. Qt6Cored.dll!)
- // for GDB/LLDB compatibility — meaningless outside Windows
+ // for GDB/LLDB compatibility — meaningless outside Windows.
+ // Must run AFTER intrinsic expansion so intrinsic-body dll! references are caught.
expression = s_moduleQualifiedPrefix.Replace(expression, "");
- // Expand intrinsic calls (e.g. day(), memberOffset(3)) into plain C++ expressions
- expression = ResolveIntrinsicCalls(expression, intrinsics);
-
return ProcessNamesInString(expression, new Substitute[] {
(m)=>
{
@@ -1715,19 +1992,36 @@ private IVariableInformation GetExpression(string expression, IVariableInformati
return expressionVariable;
}
- private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null)
+ private string GetExpressionValue(string expression, IVariableInformation variable, IDictionary scopedNames, IDictionary intrinsics = null, string view = null)
{
- string processedExpr = ReplaceNamesInExpression(expression, variable, scopedNames, intrinsics);
+ // Strip any format specifier (e.g. ",d", ",x") BEFORE name/intrinsic substitution.
+ // If we don't do this, an identifier that happens to appear in the specifier — most
+ // commonly the "d" in ",d" — will be matched by ProcessNamesInString and replaced
+ // with the full expression for the child variable of that name (e.g. a member "d"),
+ // turning "1234,d" into "1234,(obj.d)" which the debugger evaluates as a C
+ // comma-operator expression returning the struct field instead of the integer.
+ string spec = ExtractFormatSpecifier(expression);
+ string exprNoSpec = spec != null ? StripFormatSpecifier(expression) : expression;
+
+ string processedExpr = ReplaceNamesInExpression(exprNoSpec, variable, scopedNames, intrinsics);
+
+ // Re-attach the format specifier so that VariableInformation.ProcessFormatSpecifiers
+ // can apply the correct display format (decimal, hex, etc.) via -var-set-format.
+ if (spec != null)
+ processedExpr = processedExpr + "," + spec;
+
IVariableInformation expressionVariable = new VariableInformation(processedExpr, variable, _process.Engine, null);
expressionVariable.SyncEval();
- // Avoid recursive natvis formatting when expression is 'this'
- if (expression.Trim() == "this")
+ // Avoid recursive natvis formatting when expression is 'this' and no view is requested.
+ // With a view, {this,view(name)} must go through FormatDisplayString to select the right
+ // IncludeView DisplayString for the named view.
+ if (expression.Trim() == "this" && view == null)
{
return expressionVariable.Value;
}
- return FormatDisplayString(expressionVariable).value;
+ return FormatDisplayString(expressionVariable, view).value;
}
private string GetDisplayNameFromArrayIndex(uint arrayIndex, int rank, uint[] dimensions, bool isForward)
@@ -1771,6 +2065,484 @@ private string GetDisplayNameFromArrayIndex(uint arrayIndex, int rank, uint[] di
return displayName.ToString();
}
+ // ---- CustomListItems execution helpers ----------------------------------
+
+ ///
+ /// Mutable state shared across one invocation of the CustomListItems loop engine.
+ ///
+ private sealed class CustomListLoopContext
+ {
+ /// Total children emitted across all iterations ($i counter).
+ public uint GlobalIndex;
+ /// Children added to the current page.
+ public uint Emitted;
+ /// Pagination start (0 for the first page).
+ public readonly uint StartIndex;
+ /// Maximum total children expected (from <Size>, or uint.MaxValue).
+ public readonly uint TotalSize;
+ /// Set to true when a <Break> fires or the page limit is reached.
+ public bool Done;
+ /// <Exec> steps whose updated variable could not be stored compactly
+ /// (not an integer, bool or pointer), so its expression grew by one step.
+ public int NonCompactSteps;
+
+ public CustomListLoopContext(uint startIndex, uint totalSize)
+ {
+ StartIndex = startIndex;
+ TotalSize = totalSize;
+ }
+ }
+
+ ///
+ /// How many expression-growing <Exec> steps are tolerated before the loop stops with
+ /// an error row. Kept small: each such step nests the variable's expression one level
+ /// deeper, so evaluation cost grows quadratically and eventually exceeds the debugger's
+ /// expression limits (observed as a hang around 250 nested levels with lldb).
+ ///
+ private const int MaxNonCompactExecSteps = 10;
+
+ ///
+ /// Drives a single <Loop> element: runs its body once per iteration until a <Break>
+ /// fires (ctx.Done), the optional while-Condition becomes false, the size limit is reached,
+ /// a pass makes no progress, or the iteration cap is hit. Shared by the top-level loop and
+ /// nested <Loop> elements. Returns true if any pass made progress.
+ ///
+ private bool DriveLoop(
+ LoopType loop,
+ CustomListLoopContext ctx,
+ IVariableInformation variable,
+ VisualizerInfo visualizer,
+ Dictionary localVars,
+ List children,
+ int topLevelLoopIndex = -1)
+ {
+ bool progress = false;
+ if (loop?.Items == null)
+ return progress;
+
+ // Cap iterations to cover fast-forwarding through any remaining StartIndex items plus
+ // one page of MAX_EXPAND, with a hard ceiling of 10 000 guarding against malformed
+ // natvis where no / bounds the loop.
+ long remaining = ctx.StartIndex > ctx.GlobalIndex ? ctx.StartIndex - ctx.GlobalIndex : 0;
+ long maxIter = Math.Min(remaining + MAX_EXPAND + 1, 10000);
+ for (long iter = 0; !ctx.Done && ctx.GlobalIndex < ctx.TotalSize && iter < maxIter; iter++)
+ {
+ // While-guard: stop if the loop's Condition evaluates to false.
+ if (!string.IsNullOrEmpty(loop.Condition))
+ {
+ string loopCond = SubstituteLocalVars(loop.Condition, localVars);
+ if (!EvalCondition(loopCond, variable, visualizer.ScopedNames, visualizer.Intrinsics))
+ break;
+ }
+ // A full page is cut here, between passes, so the loop state is consistent (the
+ // pass that filled the page has run to completion, including its trailing
+ // steps). The continuation saves the local-variable table — compact literals
+ // after normalization — and the position, letting the "[More...]" node
+ // resume the walk here instead of re-running it from the start. Only the
+ // top-level loop takes snapshots; a nested runs within its pass.
+ if (topLevelLoopIndex >= 0 && ctx.Emitted >= MAX_EXPAND)
+ {
+ children.Add(new CustomListContinueWrapper(
+ ResourceStrings.MoreView, _process.Engine, variable, visualizer,
+ isVisualizerView: true,
+ new Dictionary(localVars, StringComparer.Ordinal),
+ topLevelLoopIndex, ctx.GlobalIndex));
+ ctx.Done = true;
+ break;
+ }
+ bool passProgress = ExecuteCustomListBody(loop.Items, loop.ItemsElementName, ctx, variable, visualizer, localVars, children);
+ if (!passProgress && !ctx.Done)
+ break; // no items emitted and no break — avoid an infinite loop
+ progress = true;
+ }
+ return progress;
+ }
+
+ ///
+ /// Executes one pass through a loop-body element sequence (Break / Item / Exec / If / Else).
+ /// Returns true if at least one Item or nested body was processed, false if the pass produced
+ /// no observable effect (used to detect infinite-loop conditions).
+ ///
+ private bool ExecuteCustomListBody(
+ object[] body,
+ ItemsChoiceType[] choices,
+ CustomListLoopContext ctx,
+ IVariableInformation variable,
+ VisualizerInfo visualizer,
+ Dictionary localVars,
+ List children)
+ {
+ bool progress = false;
+
+ // and both deserialize to IfType; the parallel choice array records the
+ // original element name so the two can be told apart (defaults to If if unavailable).
+ ItemsChoiceType ChoiceAt(int i) =>
+ (choices != null && i < choices.Length) ? choices[i] : ItemsChoiceType.If;
+
+ for (int idx = 0; idx < body.Length && !ctx.Done; idx++)
+ {
+ var elem = body[idx];
+
+ if (elem is BreakType br)
+ {
+ // : stop the loop when the condition holds.
+ if (string.IsNullOrEmpty(br.Condition))
+ {
+ ctx.Done = true;
+ break;
+ }
+ string condExpr = SubstituteLocalVars(br.Condition, localVars);
+ if (EvalCondition(condExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics))
+ ctx.Done = true;
+ }
+ else if (elem is CustomListItemType li)
+ {
+ // - expr
: emit a child variable.
+ if (li.Condition != null)
+ {
+ string condExpr = SubstituteLocalVars(li.Condition, localVars);
+ if (!EvalCondition(condExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics))
+ continue;
+ }
+
+ // A pass always runs to completion, even when it fills the page mid-body:
+ // stopping between two - s of the same pass would lose the later ones,
+ // since the pagination continuation resumes at a pass boundary. A page can
+ // therefore slightly exceed MAX_EXPAND when a pass emits several items.
+ if (ctx.GlobalIndex >= ctx.StartIndex)
+ {
+ string rawExpr = SubstituteLocalVars(li.Value?.Trim() ?? "", localVars);
+ string processedExpr = ReplaceNamesInExpression(rawExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics);
+ string name = FormatCustomListItemName(li.Name, ctx.GlobalIndex, localVars);
+ var childVar = new VariableInformation(processedExpr, variable, _process.Engine, name);
+ childVar.SyncEval();
+ children.Add(childVar);
+ ctx.Emitted++;
+ }
+ ctx.GlobalIndex++;
+ progress = true;
+ }
+ else if (elem is ExecType exec)
+ {
+ // var = expr (or "++var", or several
+ // comma-separated assignments): update one or more local variables.
+ if (exec.Condition != null)
+ {
+ string condExpr = SubstituteLocalVars(exec.Condition, localVars);
+ if (!EvalCondition(condExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics))
+ continue;
+ }
+ // Pass the raw text: ApplyExecToLocalVars detects the assigned variable
+ // name(s) and substitutes local vars on the right-hand side itself. A single
+ // may update several variables, comma-separated (e.g. "++idx, ++statptr").
+ var updatedVars = ApplyExecToLocalVars(exec.Value?.Trim() ?? "", localVars, out List unhandledExec);
+ if (unhandledExec.Count > 0)
+ {
+ // A segment we could not apply (unsupported form, or an undeclared
+ // left-hand side) means the loop state did not advance the way the
+ // visualizer intended; continuing would emit wrong items or run to
+ // the iteration cap. Log the segments and stop the loop.
+ foreach (string seg in unhandledExec)
+ {
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, "CustomListItems segment not applied (unsupported expression or undeclared variable); stopping loop: " + seg);
+ }
+ ctx.Done = true;
+ break;
+ }
+ foreach (string updatedVar in updatedVars)
+ {
+ // Keep each updated variable's stored expression compact, or it grows by
+ // one step every iteration ("(((0)+1)+1)+1...", "((node)->next)->next...")
+ // making evaluation quadratic and, past a few hundred steps, exceeding the
+ // debugger's expression limits. The variable is evaluated raw (no natvis
+ // formatting) and stored back as a literal when possible: integers/bools
+ // as-is, pointers as the address cast to their type ("(MyStruct *)0x1234",
+ // stable while the process is stopped). Other types (e.g. a by-value
+ // iterator) cannot be stored back as C++ text; a few such steps are
+ // tolerated, then an error row is emitted and the loop stops rather than
+ // degrade item by item.
+ try
+ {
+ IVariableInformation updated = GetExpression(localVars[updatedVar], variable, visualizer.ScopedNames, null, visualizer.Intrinsics);
+ string compact = updated.Error ? null : MakeCompactLiteral(updated.Value, updated.TypeName);
+ if (compact != null)
+ {
+ localVars[updatedVar] = compact;
+ }
+ else if (++ctx.NonCompactSteps > MaxNonCompactExecSteps)
+ {
+ string message = "CustomListItems: the value of loop variable '" + updatedVar + "' is not an integer, bool or pointer; the loop cannot continue efficiently and was stopped.";
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, message);
+ children.Add(new MessageVariableInformation("[Error]", message, _process.Engine, variable));
+ ctx.Done = true;
+ break;
+ }
+ }
+ catch (Exception e)
+ {
+ // The normalization evaluates a variable we just updated. If that throws,
+ // the variable can't be evaluated -- and since the loop body and its
+ // conditions use the same variable, the iteration can't continue meaningfully.
+ // Log and stop the loop rather than emit error items or run to the cap.
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, "CustomListItems normalization failed; stopping loop: " + e.Message);
+ ctx.Done = true;
+ break;
+ }
+ }
+ progress = true; // Exec advances loop state (e.g. iSpan++) even when no Item is emitted
+ }
+ else if (elem is IfType ifElem)
+ {
+ // optionally followed by any number of s and a final .
+ // Execute the first branch whose condition holds; consume all siblings.
+ // shares IfType with , so they are distinguished via the choice
+ // array. A standalone (no preceding ) is malformed: warn and skip.
+ if (ChoiceAt(idx) == ItemsChoiceType.Elseif)
+ {
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, "CustomListItems: without a preceding .");
+ continue;
+ }
+
+ string condExpr = SubstituteLocalVars(ifElem.Condition ?? "", localVars);
+ bool taken = !string.IsNullOrEmpty(condExpr) &&
+ EvalCondition(condExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics);
+
+ if (taken && ifElem.Items != null)
+ progress |= ExecuteCustomListBody(ifElem.Items, ifElem.ItemsElementName, ctx, variable, visualizer, localVars, children);
+
+ // Consume any immediately following elements (IfType with choice == Elseif).
+ while (idx + 1 < body.Length && body[idx + 1] is IfType elseIfElem && ChoiceAt(idx + 1) == ItemsChoiceType.Elseif)
+ {
+ idx++;
+ if (!taken)
+ {
+ string eiCond = SubstituteLocalVars(elseIfElem.Condition ?? "", localVars);
+ bool eiTaken = !string.IsNullOrEmpty(eiCond) &&
+ EvalCondition(eiCond, variable, visualizer.ScopedNames, visualizer.Intrinsics);
+ if (eiTaken && elseIfElem.Items != null)
+ {
+ progress |= ExecuteCustomListBody(elseIfElem.Items, elseIfElem.ItemsElementName, ctx, variable, visualizer, localVars, children);
+ taken = true;
+ }
+ }
+ }
+
+ // Consume an immediately following element.
+ if (idx + 1 < body.Length && body[idx + 1] is ElseType elseElem)
+ {
+ idx++;
+ if (!taken && elseElem.Items != null)
+ progress |= ExecuteCustomListBody(elseElem.Items, elseElem.ItemsElementName, ctx, variable, visualizer, localVars, children);
+ }
+ }
+ else if (elem is LoopType nestedLoop)
+ {
+ // Nested : drive it exactly like the top-level loop.
+ progress |= DriveLoop(nestedLoop, ctx, variable, visualizer, localVars, children);
+ }
+ else
+ {
+ // Unrecognised loop-body element. This should not occur for natvis that
+ // validates against the schema (a stray without a preceding
+ // also lands here); log so unsupported/malformed elements are visible.
+ _process.Logger.NatvisLogger?.WriteLine(LogLevel.Warning, "CustomListItems loop body contains an unsupported element: " + elem?.GetType().Name);
+ }
+ }
+
+ return progress;
+ }
+
+ ///
+ /// Substitutes natvis local variable names in with their
+ /// current expression strings, using word-boundary matching to avoid partial replacements.
+ /// Each substituted value is wrapped in parentheses to preserve operator precedence.
+ ///
+ internal static string SubstituteLocalVars(string expression, Dictionary localVars)
+ {
+ if (string.IsNullOrEmpty(expression) || localVars == null || localVars.Count == 0)
+ return expression;
+ foreach (var kv in localVars)
+ {
+ expression = Regex.Replace(
+ expression,
+ @"\b" + Regex.Escape(kv.Key) + @"\b",
+ "(" + kv.Value + ")");
+ }
+ return expression;
+ }
+
+ ///
+ /// Attempts to parse as "varName = rhs" and, when the left-hand
+ /// side is a declared natvis local variable, updates its entry to the substituted RHS.
+ /// Expressions that do not match this pattern are silently ignored.
+ ///
+ ///
+ /// The name of the local variable that was updated, or null if nothing changed.
+ /// The caller can use this to normalise the stored expression (evaluate it and replace
+ /// with the scalar result) so that repeated increments do not cause unbounded growth.
+ ///
+ internal static List ApplyExecToLocalVars(string execExpr, Dictionary localVars)
+ => ApplyExecToLocalVars(execExpr, localVars, out _);
+
+ internal static List ApplyExecToLocalVars(string execExpr, Dictionary localVars, out List unhandled)
+ {
+ var updated = new List();
+ unhandled = new List();
+ if (string.IsNullOrEmpty(execExpr)) return updated;
+
+ // A single may update several variables, comma-separated, e.g.
+ // "++idx, ++statptr" (common in the .natvis files shipped with VS). Split on
+ // top-level commas (not those inside parentheses/brackets) and apply each in order.
+ // Non-empty segments we cannot apply (unsupported form, or an undeclared left-hand
+ // side) are collected so the caller can log a warning rather than drop them silently.
+ foreach (string part in SplitTopLevelCommas(execExpr))
+ {
+ if (string.IsNullOrWhiteSpace(part))
+ continue;
+ string varName = ApplySingleExec(part, localVars);
+ if (varName != null)
+ updated.Add(varName);
+ else
+ unhandled.Add(part.Trim());
+ }
+ return updated;
+ }
+
+ ///
+ /// Applies one assignment from an <Exec> block to the local-variable table:
+ /// "varName = rhs", or the "++"/"--" shorthand. Returns the updated variable name,
+ /// or null when the segment is empty or its left-hand side is not a declared local.
+ ///
+ private static string ApplySingleExec(string execExpr, Dictionary localVars)
+ {
+ if (string.IsNullOrWhiteSpace(execExpr)) return null;
+
+ // Check for increment/decrement shorthand: ++i, i++, --i, i--
+ var mIncr = s_execIncrDecr.Match(execExpr);
+ if (mIncr.Success)
+ {
+ // prefix form: groups 1 (op) + 2 (varname); postfix form: groups 3 (varname) + 4 (op)
+ string varName = mIncr.Groups[2].Success ? mIncr.Groups[2].Value : mIncr.Groups[3].Value;
+ string op = mIncr.Groups[1].Success ? mIncr.Groups[1].Value : mIncr.Groups[4].Value;
+ if (localVars.ContainsKey(varName))
+ {
+ localVars[varName] = SubstituteLocalVars(varName, localVars) + (op == "++" ? " + 1" : " - 1");
+ return varName;
+ }
+ return null;
+ }
+
+ // Check for simple assignment: varName = rhs
+ var m = s_execAssignment.Match(execExpr);
+ if (m.Success && localVars.ContainsKey(m.Groups[1].Value))
+ {
+ string varName = m.Groups[1].Value;
+ string rhs = m.Groups[2].Value.Trim();
+ localVars[varName] = SubstituteLocalVars(rhs, localVars);
+ return varName;
+ }
+ return null;
+ }
+
+ ///
+ /// Returns a compact C++ expression equivalent to a loop variable's current value, or null
+ /// when none exists. Pointers are returned as the address cast to their type
+ /// ("(MyStruct *)0x1234", the same simplification
+ /// applies to long parent expressions);
+ /// integers (decimal or hex, depending on the engine radix) and bools are returned as
+ /// literals. Keeping the stored expression constant-size is what stops it growing by one
+ /// step per iteration ("((node)->next)->next...") until evaluation becomes quadratic and
+ /// finally exceeds the debugger's expression limits.
+ ///
+ internal static string MakeCompactLiteral(string rawValue, string typeName)
+ {
+ if (string.IsNullOrEmpty(rawValue))
+ return null;
+ string value = rawValue.Trim();
+ // Pointers are recognized by type, not value shape: a pointer must be stored with its
+ // type cast (a bare number would break later "->" accesses), so a pointer whose value
+ // has no leading address is not compactable at all.
+ if (!string.IsNullOrEmpty(typeName) && typeName.TrimEnd().EndsWith("*", StringComparison.Ordinal))
+ {
+ Match m = s_leadingHexAddress.Match(value);
+ return m.Success ? "(" + typeName + ")" + m.Groups[1].Value : null;
+ }
+ if (s_integerLiteral.IsMatch(value) || value == "true" || value == "false")
+ return value;
+ // A hex integer (how integers render when the engine radix is 16) is stored with its
+ // type cast: a bare hex literal is unsigned, so "(int)0xffffffd6" is needed to keep a
+ // negative value negative in later comparisons. Without a known type it cannot be
+ // stored back safely.
+ if (s_hexIntegerLiteral.IsMatch(value) && !string.IsNullOrEmpty(typeName))
+ return "(" + typeName + ")" + value;
+ return null;
+ }
+
+ ///
+ /// Splits an expression on top-level commas — commas not nested inside parentheses
+ /// or square brackets — so a multi-assignment <Exec> like "++idx, ++statptr" is
+ /// separated while a comma inside a call such as "f(a, b)" is left intact.
+ ///
+ internal static IEnumerable SplitTopLevelCommas(string expression)
+ {
+ if (string.IsNullOrEmpty(expression))
+ yield break;
+
+ int depth = 0;
+ int start = 0;
+ for (int i = 0; i < expression.Length; i++)
+ {
+ char c = expression[i];
+ if (c == '(' || c == '[')
+ depth++;
+ else if (c == ')' || c == ']')
+ {
+ if (depth > 0) depth--;
+ }
+ else if (c == ',' && depth == 0)
+ {
+ yield return expression.Substring(start, i - start);
+ start = i + 1;
+ }
+ }
+ yield return expression.Substring(start);
+ }
+
+ ///
+ /// Formats the display name for a CustomListItems child. Replaces {$i} and
+ /// bare $i with , then substitutes any local variable
+ /// names. Falls back to [index] when is null.
+ ///
+ ///
+ /// The condition-passing item counter (ctx.GlobalIndex), which starts at 0 and
+ /// increments for every Item whose Condition passes, across all pages. This matches
+ /// the Visual Studio behaviour where $i is the absolute loop-item index, not a
+ /// page-relative offset.
+ ///
+ internal static string FormatCustomListItemName(string nameTemplate, uint index, Dictionary localVars)
+ {
+ if (string.IsNullOrEmpty(nameTemplate))
+ return "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
+
+ string indexStr = index.ToString(CultureInfo.InvariantCulture);
+ // Replace the {$i} token first (complete braced form), then bare $i with a
+ // word-boundary guard so that e.g. "$item" in a Name template is not corrupted.
+ string name = s_dollarI.Replace(
+ nameTemplate.Replace("{$i}", indexStr),
+ indexStr);
+ name = SubstituteLocalVars(name, localVars);
+
+ // If {expr} tokens remain after substitution they would require evaluating against
+ // the debugger, which is not supported here. Fall back to [index] to avoid surfacing
+ // a failed expression evaluation string as the child name.
+ if (name.Contains('{'))
+ return "[" + index.ToString(CultureInfo.InvariantCulture) + "]";
+
+ return name;
+ }
+
+ // ---- End CustomListItems execution helpers ------------------------------
+
public void Dispose()
{
GC.SuppressFinalize(this);
diff --git a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs
index c3fdb38e0..fcc69526e 100644
--- a/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs
+++ b/src/MIDebugEngine/Natvis.Impl/NatvisXsdTypes.cs
@@ -514,19 +514,25 @@ public string Value {
public partial class CustomListItemsType {
private VariableType[] itemsField;
-
+
private CustomListSizeType[] items1Field;
-
+
private SkipType itemField;
-
+
+ private LoopType[] loopField;
+
private bool optionalField;
-
+
private bool optionalFieldSpecified;
-
+
private string conditionField;
-
+
+ private string includeViewField;
+
+ private string excludeViewField;
+
private uint maxItemsPerViewField;
-
+
private bool maxItemsPerViewFieldSpecified;
///
@@ -561,7 +567,18 @@ public SkipType Item {
this.itemField = value;
}
}
-
+
+ ///
+ [System.Xml.Serialization.XmlElementAttribute("Loop")]
+ public LoopType[] Loop {
+ get {
+ return this.loopField;
+ }
+ set {
+ this.loopField = value;
+ }
+ }
+
///
[System.Xml.Serialization.XmlAttributeAttribute()]
public bool Optional {
@@ -572,7 +589,7 @@ public bool Optional {
this.optionalField = value;
}
}
-
+
///
[System.Xml.Serialization.XmlIgnoreAttribute()]
public bool OptionalSpecified {
@@ -583,7 +600,7 @@ public bool OptionalSpecified {
this.optionalFieldSpecified = value;
}
}
-
+
///
[System.Xml.Serialization.XmlAttributeAttribute()]
public string Condition {
@@ -594,7 +611,29 @@ public string Condition {
this.conditionField = value;
}
}
-
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string IncludeView {
+ get {
+ return this.includeViewField;
+ }
+ set {
+ this.includeViewField = value;
+ }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string ExcludeView {
+ get {
+ return this.excludeViewField;
+ }
+ set {
+ this.excludeViewField = value;
+ }
+ }
+
///
[System.Xml.Serialization.XmlAttributeAttribute()]
public uint MaxItemsPerView {
@@ -618,6 +657,266 @@ public bool MaxItemsPerViewSpecified {
}
}
+ // ---- CustomListItems loop-body types ------------------------------------
+ //
+ // These types represent the body of a element inside .
+ // The loop body is an ordered sequence of Item, Break, Exec, If, Elseif, Else and
+ // Loop elements. Nested Loop elements are also supported inside If/Elseif/Else bodies.
+ //
+ // Per the natvis schema, shares IfType with (it has the same content
+ // model). The XmlSerializer cannot map two element names to one type on its own, so the
+ // Items arrays use an XmlChoiceIdentifier (the parallel ItemsElementName array of
+ // ItemsChoiceType) to record which element name produced each entry; the loop engine
+ // reads it to tell an from an .
+
+ ///
+ /// Represents an <Item> element inside a <CustomListItems> loop body.
+ /// Produces one child in the expanded view when its optional condition is true.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class CustomListItemType {
+ private string nameField;
+ private string conditionField;
+ private string valueField;
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Name {
+ get { return this.nameField; }
+ set { this.nameField = value; }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Condition {
+ get { return this.conditionField; }
+ set { this.conditionField = value; }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlTextAttribute()]
+ public string Value {
+ get { return this.valueField; }
+ set { this.valueField = value; }
+ }
+ }
+
+ ///
+ /// Represents a <Break> element inside a <CustomListItems> loop body.
+ /// The loop stops when the condition evaluates to true.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class BreakType {
+ private string conditionField;
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Condition {
+ get { return this.conditionField; }
+ set { this.conditionField = value; }
+ }
+ }
+
+ ///
+ /// Represents an <Exec> element inside a <CustomListItems> loop body.
+ /// The expression is parsed as an assignment to a declared natvis local variable
+ /// (e.g. "ptr = ptr->next") and updates that variable's current expression.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class ExecType {
+ private string conditionField;
+ private string valueField;
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Condition {
+ get { return this.conditionField; }
+ set { this.conditionField = value; }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlTextAttribute()]
+ public string Value {
+ get { return this.valueField; }
+ set { this.valueField = value; }
+ }
+ }
+
+ ///
+ /// Represents an <If> element inside a <CustomListItems> loop body.
+ /// The body is executed only when Condition is true; immediately following
+ /// <Elseif> and <Else> elements provide alternative branches.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class IfType {
+ private string conditionField;
+ private object[] itemsField;
+
+ private ItemsChoiceType[] itemsElementNameField;
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Condition {
+ get { return this.conditionField; }
+ set { this.conditionField = value; }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListItemType))]
+ [System.Xml.Serialization.XmlElementAttribute("Break", typeof(BreakType))]
+ [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(ExecType))]
+ [System.Xml.Serialization.XmlElementAttribute("If", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Elseif", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Else", typeof(ElseType))]
+ [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(LoopType))]
+ [System.Xml.Serialization.XmlChoiceIdentifierAttribute("ItemsElementName")]
+ public object[] Items {
+ get { return this.itemsField; }
+ set { this.itemsField = value; }
+ }
+
+ ///
+ // Parallel to Items: records which element name (If vs Elseif, etc.) produced each
+ // entry, since the schema maps both and to the same IfType.
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ public ItemsChoiceType[] ItemsElementName {
+ get { return this.itemsElementNameField; }
+ set { this.itemsElementNameField = value; }
+ }
+ }
+
+ ///
+ /// Represents an <Else> element that immediately follows an <If> or
+ /// <Elseif> element. Its body is executed when all preceding branch conditions
+ /// were false.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class ElseType {
+ private object[] itemsField;
+
+ private ItemsChoiceType[] itemsElementNameField;
+
+ ///
+ [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListItemType))]
+ [System.Xml.Serialization.XmlElementAttribute("Break", typeof(BreakType))]
+ [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(ExecType))]
+ [System.Xml.Serialization.XmlElementAttribute("If", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Elseif", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Else", typeof(ElseType))]
+ [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(LoopType))]
+ [System.Xml.Serialization.XmlChoiceIdentifierAttribute("ItemsElementName")]
+ public object[] Items {
+ get { return this.itemsField; }
+ set { this.itemsField = value; }
+ }
+
+ ///
+ // Parallel to Items: records which element name (If vs Elseif, etc.) produced each
+ // entry, since the schema maps both and to the same IfType.
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ public ItemsChoiceType[] ItemsElementName {
+ get { return this.itemsElementNameField; }
+ set { this.itemsElementNameField = value; }
+ }
+ }
+
+ ///
+ /// Represents a <Loop> element inside <CustomListItems> (or nested in If/Elseif/Else).
+ /// The optional Condition attribute acts as a while-guard: the loop stops as soon as it
+ /// evaluates to false. The body is also stopped by a <Break> element or when the
+ /// size limit is reached.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Diagnostics.DebuggerStepThroughAttribute()]
+ [System.ComponentModel.DesignerCategoryAttribute("code")]
+ [System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public partial class LoopType {
+ private string conditionField;
+ private object[] itemsField;
+
+ private ItemsChoiceType[] itemsElementNameField;
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string Condition {
+ get { return this.conditionField; }
+ set { this.conditionField = value; }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListItemType))]
+ [System.Xml.Serialization.XmlElementAttribute("Break", typeof(BreakType))]
+ [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(ExecType))]
+ [System.Xml.Serialization.XmlElementAttribute("If", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Elseif", typeof(IfType))]
+ [System.Xml.Serialization.XmlElementAttribute("Else", typeof(ElseType))]
+ [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(LoopType))]
+ [System.Xml.Serialization.XmlChoiceIdentifierAttribute("ItemsElementName")]
+ public object[] Items {
+ get { return this.itemsField; }
+ set { this.itemsField = value; }
+ }
+
+ ///
+ // Parallel to Items: records which element name (If vs Elseif, etc.) produced each
+ // entry, since the schema maps both and to the same IfType.
+ [System.Xml.Serialization.XmlIgnoreAttribute()]
+ public ItemsChoiceType[] ItemsElementName {
+ get { return this.itemsElementNameField; }
+ set { this.itemsElementNameField = value; }
+ }
+ }
+
+ ///
+ /// Element-name discriminator for the loop-body Items arrays. Because the schema
+ /// maps both <If> and <Elseif> to IfType, the deserializer can no longer be
+ /// told apart by runtime type; this parallel enum (populated via XmlChoiceIdentifier)
+ /// records the original element name for each entry. Member names must match the element
+ /// names exactly.
+ ///
+ [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
+ [System.SerializableAttribute()]
+ [System.Xml.Serialization.XmlTypeAttribute(IncludeInSchema=false, Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
+ public enum ItemsChoiceType {
+ ///
+ Item,
+ ///
+ Break,
+ ///
+ Exec,
+ ///
+ If,
+ ///
+ Elseif,
+ ///
+ Else,
+ ///
+ Loop,
+ }
+
+ // ---- End CustomListItems loop-body types ---------------------------------
+
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
@@ -717,13 +1016,17 @@ public string Value {
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
public partial class ExpandedItemType {
-
+
private bool optionalField;
-
+
private bool optionalFieldSpecified;
-
+
private string conditionField;
-
+
+ private string includeViewField;
+
+ private string excludeViewField;
+
private string valueField;
///
@@ -758,7 +1061,29 @@ public string Condition {
this.conditionField = value;
}
}
-
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string IncludeView {
+ get {
+ return this.includeViewField;
+ }
+ set {
+ this.includeViewField = value;
+ }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string ExcludeView {
+ get {
+ return this.excludeViewField;
+ }
+ set {
+ this.excludeViewField = value;
+ }
+ }
+
///
[System.Xml.Serialization.XmlTextAttribute()]
public string Value {
@@ -770,7 +1095,7 @@ public string Value {
}
}
}
-
+
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
@@ -852,11 +1177,11 @@ public string Condition {
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
public partial class IndexNodeType {
-
+
private string conditionField;
-
+
private string valueField;
-
+
///
[System.Xml.Serialization.XmlAttributeAttribute()]
public string Condition {
@@ -867,7 +1192,7 @@ public string Condition {
this.conditionField = value;
}
}
-
+
///
[System.Xml.Serialization.XmlTextAttribute()]
public string Value {
@@ -879,7 +1204,7 @@ public string Value {
}
}
}
-
+
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
@@ -887,15 +1212,19 @@ public string Value {
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
public partial class ItemType {
-
+
private string nameField;
-
+
private bool optionalField;
-
+
private bool optionalFieldSpecified;
-
+
private string conditionField;
-
+
+ private string includeViewField;
+
+ private string excludeViewField;
+
private string valueField;
///
@@ -941,7 +1270,29 @@ public string Condition {
this.conditionField = value;
}
}
-
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string IncludeView {
+ get {
+ return this.includeViewField;
+ }
+ set {
+ this.includeViewField = value;
+ }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string ExcludeView {
+ get {
+ return this.excludeViewField;
+ }
+ set {
+ this.excludeViewField = value;
+ }
+ }
+
///
[System.Xml.Serialization.XmlTextAttribute()]
public string Value {
@@ -953,7 +1304,7 @@ public string Value {
}
}
}
-
+
///
[System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")]
[System.SerializableAttribute()]
@@ -1263,17 +1614,21 @@ public string Condition {
[System.ComponentModel.DesignerCategoryAttribute("code")]
[System.Xml.Serialization.XmlTypeAttribute(Namespace="http://schemas.microsoft.com/vstudio/debugger/natvis/2010")]
public partial class DisplayStringType {
-
+
private bool optionalField;
-
+
private bool optionalFieldSpecified;
-
+
private string conditionField;
-
+
private string legacyAddinField;
-
+
private string exportField;
-
+
+ private string includeViewField;
+
+ private string excludeViewField;
+
private string valueField;
///
@@ -1330,7 +1685,29 @@ public string Export {
this.exportField = value;
}
}
-
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string IncludeView {
+ get {
+ return this.includeViewField;
+ }
+ set {
+ this.includeViewField = value;
+ }
+ }
+
+ ///
+ [System.Xml.Serialization.XmlAttributeAttribute()]
+ public string ExcludeView {
+ get {
+ return this.excludeViewField;
+ }
+ set {
+ this.excludeViewField = value;
+ }
+ }
+
///
[System.Xml.Serialization.XmlTextAttribute()]
public string Value {
diff --git a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
index 287be0072..f603635bd 100644
--- a/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
+++ b/src/MIDebugEngineUnitTests/NatvisFormatSpecifierTest.cs
@@ -44,6 +44,128 @@ public void ExtractFormatSpecifier_NaModifierStripped()
Assert.Equal("view(RecZone)", Natvis.ExtractFormatSpecifier("this,view(RecZone)na"));
}
+ [Fact]
+ public void ExtractFormatSpecifier_ViewSpecifierNoModifier_Extracted()
+ {
+ // View specifier with no trailing modifier (e.g. no "na")
+ Assert.Equal("view(arr)", Natvis.ExtractFormatSpecifier("foo(),view(arr)"));
+ }
+
+ // -- IsIncludeViewMatch -----------------------------------------------
+
+ [Fact]
+ public void IsIncludeViewMatch_NullIncludeView_AlwaysMatches()
+ {
+ Assert.True(Natvis.IsIncludeViewMatch(null, "RecZone"));
+ Assert.True(Natvis.IsIncludeViewMatch(null, null));
+ }
+
+ [Fact]
+ public void IsIncludeViewMatch_EmptyIncludeView_AlwaysMatches()
+ {
+ Assert.True(Natvis.IsIncludeViewMatch("", "RecZone"));
+ }
+
+ [Fact]
+ public void IsIncludeViewMatch_MatchingView_ReturnsTrue()
+ {
+ Assert.True(Natvis.IsIncludeViewMatch("RecZone", "RecZone"));
+ }
+
+ [Fact]
+ public void IsIncludeViewMatch_DifferentView_ReturnsFalse()
+ {
+ Assert.False(Natvis.IsIncludeViewMatch("RecZone", "other"));
+ }
+
+ [Fact]
+ public void IsIncludeViewMatch_NullCurrentView_ReturnsFalse()
+ {
+ Assert.False(Natvis.IsIncludeViewMatch("RecZone", null));
+ }
+
+ [Fact]
+ public void IsIncludeViewMatch_MultipleViews_MatchesAny()
+ {
+ // IncludeView is a semicolon-delimited list per the natvis XSD — same as ExcludeView.
+ Assert.True(Natvis.IsIncludeViewMatch("RecZone;RecZoneAbs", "RecZone"));
+ Assert.True(Natvis.IsIncludeViewMatch("RecZone;RecZoneAbs", "RecZoneAbs"));
+ Assert.False(Natvis.IsIncludeViewMatch("RecZone;RecZoneAbs", "other"));
+ }
+
+ // -- IsExcludeViewMatch -----------------------------------------------
+
+ [Fact]
+ public void IsExcludeViewMatch_NullExcludeView_ReturnsFalse()
+ {
+ Assert.False(Natvis.IsExcludeViewMatch(null, "RecZone"));
+ }
+
+ [Fact]
+ public void IsExcludeViewMatch_NullCurrentView_ReturnsFalse()
+ {
+ Assert.False(Natvis.IsExcludeViewMatch("RecZone;RecZoneAbs", null));
+ }
+
+ [Fact]
+ public void IsExcludeViewMatch_ViewInList_ReturnsTrue()
+ {
+ Assert.True(Natvis.IsExcludeViewMatch("RecZone;RecZoneAbs", "RecZone"));
+ Assert.True(Natvis.IsExcludeViewMatch("RecZone;RecZoneAbs", "RecZoneAbs"));
+ }
+
+ [Fact]
+ public void IsExcludeViewMatch_ViewNotInList_ReturnsFalse()
+ {
+ Assert.False(Natvis.IsExcludeViewMatch("RecZone;RecZoneAbs", "other"));
+ }
+
+ [Fact]
+ public void IsExcludeViewMatch_SingleEntry_Matches()
+ {
+ Assert.True(Natvis.IsExcludeViewMatch("simple", "simple"));
+ }
+
+ // -- ExtractViewName --------------------------------------------------
+
+ [Fact]
+ public void ExtractViewName_ViewSpecifier_ReturnsName()
+ {
+ Assert.Equal("RecZone", Natvis.ExtractViewName("view(RecZone)"));
+ }
+
+ [Fact]
+ public void ExtractViewName_ViewSpecifierWithTrailingNa_ReturnsName()
+ {
+ Assert.Equal("RecZone", Natvis.ExtractViewName("view(RecZone)na"));
+ }
+
+ [Fact]
+ public void ExtractViewName_ShortName_ReturnsName()
+ {
+ Assert.Equal("arr", Natvis.ExtractViewName("view(arr)"));
+ }
+
+ [Fact]
+ public void ExtractViewName_NotViewSpecifier_ReturnsNull()
+ {
+ Assert.Null(Natvis.ExtractViewName("d"));
+ Assert.Null(Natvis.ExtractViewName("sub"));
+ }
+
+ [Fact]
+ public void ExtractViewName_Null_ReturnsNull()
+ {
+ Assert.Null(Natvis.ExtractViewName(null));
+ }
+
+ [Fact]
+ public void ExtractViewName_EmptyViewName_ReturnsNull()
+ {
+ // view() with no name is not a valid specifier — treat as absent.
+ Assert.Null(Natvis.ExtractViewName("view()"));
+ }
+
// -- CleanUtf16StringValue --------------------------------------------
[Fact]
@@ -113,5 +235,372 @@ public void CleanAsciiStringValue_NoPrefix_Unchanged()
{
Assert.Equal("42", Natvis.CleanAsciiStringValue("42"));
}
+
+ // -- SubstituteLocalVars ---------------------------------------------
+
+ [Fact]
+ public void SubstituteLocalVars_EmptyExpression_Unchanged()
+ {
+ Assert.Equal("", Natvis.SubstituteLocalVars("", new System.Collections.Generic.Dictionary { ["ptr"] = "head" }));
+ }
+
+ [Fact]
+ public void SubstituteLocalVars_NoVars_Unchanged()
+ {
+ Assert.Equal("ptr->next", Natvis.SubstituteLocalVars("ptr->next", new System.Collections.Generic.Dictionary()));
+ }
+
+ [Fact]
+ public void SubstituteLocalVars_SingleVar_Substituted()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Assert.Equal("(m_head)->next", Natvis.SubstituteLocalVars("ptr->next", vars));
+ }
+
+ [Fact]
+ public void SubstituteLocalVars_WordBoundary_PartialNameNotReplaced()
+ {
+ // "ptrr" must not be replaced when the variable is "ptr"
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Assert.Equal("ptrr->next", Natvis.SubstituteLocalVars("ptrr->next", vars));
+ }
+
+ [Fact]
+ public void SubstituteLocalVars_MultipleVars_BothSubstituted()
+ {
+ var vars = new System.Collections.Generic.Dictionary
+ {
+ ["lo"] = "start",
+ ["hi"] = "end"
+ };
+ Assert.Equal("(start) + (end)", Natvis.SubstituteLocalVars("lo + hi", vars));
+ }
+
+ [Fact]
+ public void SubstituteLocalVars_ParensPreservesPrecedence()
+ {
+ // Replacement is wrapped in () so "ptr->val * 2" becomes "((m_head)->val) * 2"
+ // after two substitution passes aren't needed here — single pass is enough.
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Assert.Equal("(m_head)->val", Natvis.SubstituteLocalVars("ptr->val", vars));
+ }
+
+ // -- ApplyExecToLocalVars --------------------------------------------
+
+ [Fact]
+ public void ApplyExecToLocalVars_SimpleAssignment_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Natvis.ApplyExecToLocalVars("ptr = ptr->next", vars);
+ Assert.Equal("(m_head)->next", vars["ptr"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_UnknownLhs_NoChange()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Natvis.ApplyExecToLocalVars("other = 0", vars);
+ // "other" is not a declared variable; dict should be unchanged
+ Assert.Equal("m_head", vars["ptr"]);
+ Assert.False(vars.ContainsKey("other"));
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_EmptyExpression_NoChange()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["ptr"] = "m_head" };
+ Natvis.ApplyExecToLocalVars("", vars);
+ Assert.Equal("m_head", vars["ptr"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_CounterIncrement_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ Natvis.ApplyExecToLocalVars("i = i + 1", vars);
+ Assert.Equal("(0) + 1", vars["i"]);
+ }
+
+ // Assignment must not match == comparison operator (regression guard)
+ [Fact]
+ public void ApplyExecToLocalVars_EqualityComparison_NoChange()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "5" };
+ Natvis.ApplyExecToLocalVars("i == 1", vars);
+ Assert.Equal("5", vars["i"]);
+ }
+
+ // -- ApplyExecToLocalVars — increment/decrement -----------------------
+
+ [Fact]
+ public void ApplyExecToLocalVars_PrefixIncrement_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "3" };
+ Natvis.ApplyExecToLocalVars("++i", vars);
+ Assert.Equal("(3) + 1", vars["i"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_PostfixIncrement_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "3" };
+ Natvis.ApplyExecToLocalVars("i++", vars);
+ Assert.Equal("(3) + 1", vars["i"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_PrefixDecrement_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "3" };
+ Natvis.ApplyExecToLocalVars("--i", vars);
+ Assert.Equal("(3) - 1", vars["i"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_PostfixDecrement_UpdatesVar()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "3" };
+ Natvis.ApplyExecToLocalVars("i--", vars);
+ Assert.Equal("(3) - 1", vars["i"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_IncrUnknownVar_NoChange()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "3" };
+ Natvis.ApplyExecToLocalVars("++j", vars);
+ Assert.Equal("3", vars["i"]);
+ Assert.False(vars.ContainsKey("j"));
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_IncrWithSpaces_UpdatesVar()
+ {
+ // Whitespace around the operator/operand must be tolerated.
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ Natvis.ApplyExecToLocalVars(" i++ ", vars);
+ Assert.Equal("(0) + 1", vars["i"]);
+ }
+
+ // -- FormatCustomListItemName ----------------------------------------
+
+ [Fact]
+ public void FormatCustomListItemName_NullTemplate_ReturnsBracketedIndex()
+ {
+ Assert.Equal("[0]", Natvis.FormatCustomListItemName(null, 0, new System.Collections.Generic.Dictionary()));
+ Assert.Equal("[42]", Natvis.FormatCustomListItemName(null, 42, new System.Collections.Generic.Dictionary()));
+ }
+
+ [Fact]
+ public void FormatCustomListItemName_BracedDollarI_ReplacedWithIndex()
+ {
+ Assert.Equal("[7]", Natvis.FormatCustomListItemName("[{$i}]", 7, new System.Collections.Generic.Dictionary()));
+ }
+
+ [Fact]
+ public void FormatCustomListItemName_BareDollarI_ReplacedWithIndex()
+ {
+ Assert.Equal("item_3", Natvis.FormatCustomListItemName("item_$i", 3, new System.Collections.Generic.Dictionary()));
+ }
+
+ [Fact]
+ public void FormatCustomListItemName_NoSpecialTokens_Unchanged()
+ {
+ Assert.Equal("key", Natvis.FormatCustomListItemName("key", 5, new System.Collections.Generic.Dictionary()));
+ }
+
+ [Fact]
+ public void FormatCustomListItemName_LocalVar_Substituted()
+ {
+ // A local variable name that appears in the Name template is substituted.
+ var vars = new System.Collections.Generic.Dictionary { ["node"] = "m_head" };
+ Assert.Equal("[(m_head)]", Natvis.FormatCustomListItemName("[node]", 0, vars));
+ }
+
+ [Fact]
+ public void FormatCustomListItemName_ExprToken_FallsBackToIndex()
+ {
+ // {expr} tokens that survive local-var substitution require debugger evaluation,
+ // which is not available here. The method must fall back to [index] rather than
+ // surfacing the raw expression text (or a debugger error string) as the child name.
+ var vars = new System.Collections.Generic.Dictionary { ["iSpan"] = "0" };
+ // After substituting iSpan, "[{getKey((0), 0)}]" still contains '{' -- fall back.
+ Assert.Equal("[2]", Natvis.FormatCustomListItemName("[{getKey(iSpan, 0)}]", 2, vars));
+ }
+
+ // -- ApplyExecToLocalVars — multiple assignments ----------------------
+
+ [Fact]
+ public void ApplyExecToLocalVars_MultipleIncrements_UpdatesAll()
+ {
+ // A single can update several variables, e.g. "++idx, ++statptr".
+ var vars = new System.Collections.Generic.Dictionary { ["idx"] = "0", ["statptr"] = "5" };
+ Natvis.ApplyExecToLocalVars("++idx, ++statptr", vars);
+ Assert.Equal("(0) + 1", vars["idx"]);
+ Assert.Equal("(5) + 1", vars["statptr"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_MultipleAssignments_UpdatesAllInOrder()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0", ["j"] = "10" };
+ Natvis.ApplyExecToLocalVars("i = i + 1, j = j - 1", vars);
+ Assert.Equal("(0) + 1", vars["i"]);
+ Assert.Equal("(10) - 1", vars["j"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_MultipleAssignments_ReturnsAllUpdatedNames()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["idx"] = "0", ["statptr"] = "5" };
+ var updated = Natvis.ApplyExecToLocalVars("++idx, ++statptr", vars);
+ Assert.Equal(2, updated.Count);
+ Assert.Contains("idx", updated);
+ Assert.Contains("statptr", updated);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_CommaInsideParens_NotSplit()
+ {
+ // The comma inside max(...) must not split the assignment into two.
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ var updated = Natvis.ApplyExecToLocalVars("i = max(i, 5)", vars);
+ Assert.Single(updated);
+ Assert.Equal("max((0), 5)", vars["i"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_TrailingComma_EmptySegmentIgnored()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ var updated = Natvis.ApplyExecToLocalVars("++i,", vars);
+ Assert.Single(updated);
+ Assert.Equal("(0) + 1", vars["i"]);
+ }
+
+ // -- SplitTopLevelCommas ----------------------------------------------
+
+ [Fact]
+ public void SplitTopLevelCommas_TopLevel_Splits()
+ {
+ Assert.Equal(new[] { "a", "b", "c" }, System.Linq.Enumerable.ToArray(Natvis.SplitTopLevelCommas("a,b,c")));
+ }
+
+ [Fact]
+ public void SplitTopLevelCommas_InsideParens_NotSplit()
+ {
+ Assert.Equal(new[] { "f(a,b)", "c" }, System.Linq.Enumerable.ToArray(Natvis.SplitTopLevelCommas("f(a,b),c")));
+ }
+
+ [Fact]
+ public void SplitTopLevelCommas_InsideBrackets_NotSplit()
+ {
+ Assert.Equal(new[] { "arr[i,j]", "k" }, System.Linq.Enumerable.ToArray(Natvis.SplitTopLevelCommas("arr[i,j],k")));
+ }
+
+ [Fact]
+ public void SplitTopLevelCommas_NoComma_SingleSegment()
+ {
+ Assert.Equal(new[] { "ptr->next" }, System.Linq.Enumerable.ToArray(Natvis.SplitTopLevelCommas("ptr->next")));
+ }
+
+ // -- ApplyExecToLocalVars — unhandled segments are reported ------------
+
+ [Fact]
+ public void ApplyExecToLocalVars_UnsupportedSegment_ReportedAsUnhandled()
+ {
+ // A segment we cannot apply (here a function call) is reported as unhandled,
+ // while the recognised increment in the same is still applied.
+ var vars = new System.Collections.Generic.Dictionary { ["idx"] = "0" };
+ var updated = Natvis.ApplyExecToLocalVars("++idx, frobnicate()", vars, out var unhandled);
+ Assert.Equal(new[] { "idx" }, System.Linq.Enumerable.ToArray(updated));
+ Assert.Equal(new[] { "frobnicate()" }, System.Linq.Enumerable.ToArray(unhandled));
+ Assert.Equal("(0) + 1", vars["idx"]);
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_UndeclaredLhs_ReportedAsUnhandled()
+ {
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ Natvis.ApplyExecToLocalVars("other = 5", vars, out var unhandled);
+ Assert.Equal(new[] { "other = 5" }, System.Linq.Enumerable.ToArray(unhandled));
+ }
+
+ [Fact]
+ public void ApplyExecToLocalVars_TrailingComma_NotReportedAsUnhandled()
+ {
+ // An empty segment from a trailing comma must not be flagged as unhandled.
+ var vars = new System.Collections.Generic.Dictionary { ["i"] = "0" };
+ Natvis.ApplyExecToLocalVars("++i,", vars, out var unhandled);
+ Assert.Empty(unhandled);
+ }
+
+ // -- MakeCompactLiteral (Exec normalization) --------------------------
+
+ [Fact]
+ public void MakeCompactLiteral_Integer_ReturnsLiteral()
+ {
+ Assert.Equal("42", Natvis.MakeCompactLiteral("42", "int"));
+ Assert.Equal("-5", Natvis.MakeCompactLiteral(" -5 ", "long"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_Bool_ReturnsLiteral()
+ {
+ Assert.Equal("true", Natvis.MakeCompactLiteral("true", "bool"));
+ Assert.Equal("false", Natvis.MakeCompactLiteral("false", "bool"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_Pointer_ReturnsCastAddress()
+ {
+ Assert.Equal("(MyStruct *)0x1234", Natvis.MakeCompactLiteral("0x1234", "MyStruct *"));
+ Assert.Equal("(Container::Node *)0x0", Natvis.MakeCompactLiteral("0x0", "Container::Node *"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_PointerWithGdbAnnotation_UsesLeadingAddressOnly()
+ {
+ // GDB appends symbol or string text after the address.
+ Assert.Equal("(char *)0x5555", Natvis.MakeCompactLiteral("0x5555 \"hello\"", "char *"));
+ Assert.Equal("(Node *)0x7fff12", Natvis.MakeCompactLiteral("0x7fff12 ", "Node *"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_HexInteger_ReturnsCastLiteral()
+ {
+ // With the engine radix set to 16, integers render as hex. The type cast keeps the
+ // value's signedness: a bare hex literal is unsigned, so "(int)0xffffffd6" is needed
+ // for -42 to stay negative in later comparisons.
+ Assert.Equal("(int)0x2a", Natvis.MakeCompactLiteral("0x2a", "int"));
+ Assert.Equal("(unsigned int)0x1234", Natvis.MakeCompactLiteral("0x1234", "unsigned int"));
+ Assert.Equal("(int)0xffffffd6", Natvis.MakeCompactLiteral("0xffffffd6", "int"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_HexIntegerWithoutType_Null()
+ {
+ // Without a type the cast cannot be emitted, and a bare hex literal could change
+ // signedness, so the value is not stored back.
+ Assert.Null(Natvis.MakeCompactLiteral("0x2a", null));
+ Assert.Null(Natvis.MakeCompactLiteral("0x2a", ""));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_PointerWithoutAddress_Null()
+ {
+ // A pointer needs its type cast, so a value without a leading address (e.g. GDB's
+ // "") cannot be stored back.
+ Assert.Null(Natvis.MakeCompactLiteral("", "Node *"));
+ }
+
+ [Fact]
+ public void MakeCompactLiteral_NonCompactableValues_Null()
+ {
+ Assert.Null(Natvis.MakeCompactLiteral("{ size=3 }", "Container"));
+ Assert.Null(Natvis.MakeCompactLiteral("1.5", "double"));
+ Assert.Null(Natvis.MakeCompactLiteral("", "int"));
+ Assert.Null(Natvis.MakeCompactLiteral(null, "int"));
+ }
}
}
diff --git a/test/CppTests/Tests/NatvisTests.cs b/test/CppTests/Tests/NatvisTests.cs
index 46f31299c..8c9fbefb2 100644
--- a/test/CppTests/Tests/NatvisTests.cs
+++ b/test/CppTests/Tests/NatvisTests.cs
@@ -38,8 +38,8 @@ public NatvisTests(ITestOutputHelper outputHelper) : base(outputHelper)
private const string NatvisSourceName = "main.cpp";
// These line numbers will need to change if src/natvis/main.cpp changes
- private const int SimpleClassAssignmentLine = 66;
- private const int ReturnSourceLine = 90;
+ private const int SimpleClassAssignmentLine = 87;
+ private const int ReturnSourceLine = 125;
[Theory]
[RequiresTestSettings]
@@ -720,6 +720,126 @@ public void TestLinkedListItemsCondition(ITestSettings settings)
}
}
+ [Theory]
+ [DependsOnTest(nameof(CompileNatvisDebuggee))]
+ [RequiresTestSettings]
+ public void TestCustomListItems(ITestSettings settings)
+ {
+ this.TestPurpose("This test checks that a visualizer expands via its /
- / body.");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, NatvisName, DebuggeeMonikers.Natvis.Default);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ string visFile = Path.Join(debuggee.SourceRoot, "visualizer_files", "Simple.natvis");
+
+ LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath, visFile, false);
+ runner.RunCommand(launch);
+
+ this.Comment("Set Breakpoint");
+ SourceBreakpoints writerBreakpoints = debuggee.Breakpoints(NatvisSourceName, ReturnSourceLine);
+ runner.SetBreakpoints(writerBreakpoints);
+
+ runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone();
+
+ using (IThreadInspector threadInspector = runner.GetThreadInspector())
+ {
+ IFrameInspector currentFrame = threadInspector.Stack.First();
+
+ this.Comment("Verifying walks the list via its loop body");
+ var customList = currentFrame.GetVariable("customList");
+ Assert.Equal("3", customList.GetVariable("Count").Value);
+
+ // The node = node->next step must advance every iteration, so all
+ // three nodes appear as distinct, in-order children [0]..[2].
+ Assert.Equal("10", customList.GetVariable("[0]").Value);
+ Assert.Equal("20", customList.GetVariable("[1]").Value);
+ Assert.Equal("30", customList.GetVariable("[2]").Value);
+
+ this.Comment("Verifying a 500-element list pages correctly (pointer loop variables are stored compactly, so the walk stays evaluable)");
+ var customList500 = currentFrame.GetVariable("customList500");
+ Assert.Equal("500", customList500.GetVariable("Count").Value);
+ Assert.Equal("0", customList500.GetVariable("[0]").Value);
+ Assert.Equal("49", customList500.GetVariable("[49]").Value);
+ var more = customList500.GetVariable("[More...]");
+ Assert.Equal("50", more.GetVariable("[50]").Value);
+ Assert.Equal("99", more.GetVariable("[99]").Value);
+ }
+
+ runner.Expects.ExitedEvent(exitCode: 0).TerminatedEvent().AfterContinue();
+ runner.DisconnectAndVerify();
+ }
+ }
+
+ [Theory]
+ [DependsOnTest(nameof(CompileNatvisDebuggee))]
+ [RequiresTestSettings]
+ public void TestViewFiltering(ITestSettings settings)
+ {
+ this.TestPurpose("This test checks that IncludeView/ExcludeView filter the DisplayString and Expand items, and that a ',view(name)' specifier in a natvis expression propagates into the expansion.");
+ this.WriteSettings(settings);
+
+ IDebuggee debuggee = Debuggee.Open(this, settings.CompilerSettings, NatvisName, DebuggeeMonikers.Natvis.Default);
+
+ using (IDebuggerRunner runner = CreateDebugAdapterRunner(settings))
+ {
+ this.Comment("Configure launch");
+ string visFile = Path.Join(debuggee.SourceRoot, "visualizer_files", "Simple.natvis");
+
+ LaunchCommand launch = new LaunchCommand(settings.DebuggerSettings, debuggee.OutputPath, visFile, false);
+ runner.RunCommand(launch);
+
+ this.Comment("Set Breakpoint");
+ SourceBreakpoints writerBreakpoints = debuggee.Breakpoints(NatvisSourceName, ReturnSourceLine);
+ runner.SetBreakpoints(writerBreakpoints);
+
+ runner.Expects.StoppedEvent(StoppedReason.Breakpoint).AfterConfigurationDone();
+
+ using (IThreadInspector threadInspector = runner.GetThreadInspector())
+ {
+ IFrameInspector currentFrame = threadInspector.Stack.First();
+
+ this.Comment("Default view: IncludeView=\"simple\" is skipped everywhere.");
+ // DisplayString: the IncludeView="simple" line is skipped, so the plain one is used.
+ var vo = currentFrame.GetVariable("vo");
+ Assert.Equal("full 1 2 3", vo.Value);
+ // Expand: X (untagged) and Y (ExcludeView="simple", inactive here) show; Z (IncludeView="simple") is hidden.
+ Assert.True(vo.Variables.ContainsKey("X"), "X (untagged) should show in the default view");
+ Assert.True(vo.Variables.ContainsKey("Y"), "Y (ExcludeView=simple) should show outside the simple view");
+ Assert.False(vo.Variables.ContainsKey("Z"), "Z (IncludeView=simple) should be hidden outside the simple view");
+ Assert.Equal("1", vo.GetVariable("X").Value);
+ Assert.Equal("2", vo.GetVariable("Y").Value);
+
+ this.Comment("A DisplayString embedding {inner,view(simple)} renders the inner object's IncludeView=\"simple\" DisplayString.");
+ var holder = currentFrame.GetVariable("holder");
+ Assert.Equal("holder compact 1", holder.Value);
+
+ this.Comment("inner,view(simple) propagates the view: X and Z show, Y (ExcludeView=simple) is filtered out.");
+ Assert.True(holder.Variables.ContainsKey("X"), "X (untagged) should show in the simple view");
+ Assert.True(holder.Variables.ContainsKey("Z"), "Z (IncludeView=simple) should show in the simple view");
+ Assert.False(holder.Variables.ContainsKey("Y"), "Y (ExcludeView=simple) should be hidden in the simple view");
+ Assert.Equal("1", holder.GetVariable("X").Value);
+ Assert.Equal("3", holder.GetVariable("Z").Value);
+
+ this.Comment("A ',view(simple)' specifier typed on a watch expression selects the IncludeView DisplayString.");
+ Assert.Equal("compact 1", currentFrame.Evaluate("vo,view(simple)", EvaluateContext.Watch));
+
+ this.Comment("Watch ',view(simple)' expansion: X and Z show, Y (ExcludeView=simple) is filtered out.");
+ IDictionary watchItems = currentFrame.EvaluateChildren("vo,view(simple)", EvaluateContext.Watch);
+ Assert.True(watchItems.ContainsKey("X"), "X (untagged) should show in the simple view");
+ Assert.True(watchItems.ContainsKey("Z"), "Z (IncludeView=simple) should show in the simple view");
+ Assert.False(watchItems.ContainsKey("Y"), "Y (ExcludeView=simple) should be hidden in the simple view");
+ Assert.Equal("1", watchItems["X"].Value);
+ Assert.Equal("3", watchItems["Z"].Value);
+ }
+
+ runner.Expects.ExitedEvent(exitCode: 0).TerminatedEvent().AfterContinue();
+ runner.DisconnectAndVerify();
+ }
+ }
+
#endregion
}
}
diff --git a/test/CppTests/debuggees/natvis/src/CustomListContainer.h b/test/CppTests/debuggees/natvis/src/CustomListContainer.h
new file mode 100644
index 000000000..99e14e4fa
--- /dev/null
+++ b/test/CppTests/debuggees/natvis/src/CustomListContainer.h
@@ -0,0 +1,32 @@
+#include
+
+// A simple NULL-terminated singly-linked list, visualized via
+// (the loop engine: ///
- /). Nodes are appended
+// to the tail so that iteration order (head -> next) matches insertion order.
+class CustomListContainer
+{
+private:
+ struct Node {
+ int value;
+ Node* next;
+ Node(int v) : value(v), next(NULL) {}
+ };
+
+ Node* head;
+ int count;
+
+public:
+ CustomListContainer() : head(NULL), count(0) {}
+
+ void Append(int v) {
+ Node* n = new Node(v);
+ if (head == NULL) {
+ head = n;
+ } else {
+ Node* cur = head;
+ while (cur->next != NULL) cur = cur->next;
+ cur->next = n;
+ }
+ count++;
+ }
+};
diff --git a/test/CppTests/debuggees/natvis/src/main.cpp b/test/CppTests/debuggees/natvis/src/main.cpp
index 1216a3e13..34bf5ff37 100644
--- a/test/CppTests/debuggees/natvis/src/main.cpp
+++ b/test/CppTests/debuggees/natvis/src/main.cpp
@@ -7,6 +7,7 @@
#include "SimpleTemplated.h"
#include "DataPoint.h"
#include "ConditionalLinkedList.h"
+#include "CustomListContainer.h"
class SimpleDisplayObject
{
@@ -30,6 +31,26 @@ class ShowRawViewObject
ShowRawViewObject() : a(30), b(40) {}
};
+// Visualized with IncludeView/ExcludeView on both the DisplayString and the Expand items,
+// so a ",view(simple)" specifier changes the summary and which rows are shown.
+class ViewObject
+{
+public:
+ int x;
+ int y;
+ int z;
+ ViewObject(int xVal, int yVal, int zVal) : x(xVal), y(yVal), z(zVal) {}
+};
+
+// Invokes the "simple" view from inside the natvis: {inner,view(simple)} in the
+// DisplayString and inner,view(simple) in the Expand.
+class ViewHolder
+{
+public:
+ ViewObject inner;
+ ViewHolder() : inner(1, 2, 3) {}
+};
+
int main(int argc, char** argv)
{
SimpleDisplayObject obj_1;
@@ -87,5 +108,19 @@ int main(int argc, char** argv)
inactiveList.Add(100);
inactiveList.Add(200);
+ CustomListContainer customList;
+ customList.Append(10);
+ customList.Append(20);
+ customList.Append(30);
+
+ CustomListContainer customList500;
+ for (int i = 0; i < 500; i++)
+ {
+ customList500.Append(i);
+ }
+
+ ViewObject vo(1, 2, 3);
+ ViewHolder holder;
+
return 0;
}
diff --git a/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis b/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis
index 0c833dde5..1f3cae92d 100644
--- a/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis
+++ b/test/CppTests/debuggees/natvis/src/visualizer_files/Simple.natvis
@@ -135,4 +135,37 @@
+
+ compact {x}
+ full {x} {y} {z}
+
+
- x
+ - y
+ - z
+
+
+
+
+ holder {inner,view(simple)}
+
+ inner,view(simple)
+
+
+
+
+ {{ size={count} }}
+
+ - count
+
+
+ count
+
+
+ - node->value
+ node = node->next
+
+
+
+
+
\ No newline at end of file
diff --git a/test/DebuggerTesting/OpenDebug/Commands/Responses/EvaluateResponseValue.cs b/test/DebuggerTesting/OpenDebug/Commands/Responses/EvaluateResponseValue.cs
index 27e8e9295..ec9dee6d7 100644
--- a/test/DebuggerTesting/OpenDebug/Commands/Responses/EvaluateResponseValue.cs
+++ b/test/DebuggerTesting/OpenDebug/Commands/Responses/EvaluateResponseValue.cs
@@ -11,6 +11,9 @@ public sealed class Body
{
[JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
public string result;
+
+ [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)]
+ public int? variablesReference;
}
public Body body = new Body();
diff --git a/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs b/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs
index ce1e5b06b..7ab55feee 100644
--- a/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs
+++ b/test/DebuggerTesting/OpenDebug/Extensions/FrameInspector.cs
@@ -169,6 +169,16 @@ public string Evaluate(string expression, EvaluateContext context = EvaluateCont
return response.body.result;
}
+ public IDictionary EvaluateChildren(string expression, EvaluateContext context = EvaluateContext.None)
+ {
+ this.VerifyNotDisposed();
+ EvaluateResponseValue response = this.DebuggerRunner.RunCommand(new EvaluateCommand(expression, this.Id, context));
+ int? childReference = response.body.variablesReference;
+ if (childReference == null || childReference == 0)
+ return new Dictionary();
+ return VariableInspector.GetChildVariables(this.DebuggerRunner, childReference.Value);
+ }
+
public string GetSourceContent()
{
this.VerifyNotDisposed();
diff --git a/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs b/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs
index caf6b2aff..37161ea82 100644
--- a/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs
+++ b/test/DebuggerTesting/OpenDebug/Extensions/IInspectors.cs
@@ -77,6 +77,14 @@ public interface IFrameInspector : IVariableExpander, IInspector
///
string Evaluate(string expression, EvaluateContext context = EvaluateContext.None);
+ ///
+ /// Evaluates an expression on this frame and returns the child variables of the result
+ /// (the rows shown when the result is expanded), keyed by name; empty when the result has
+ /// no children. Unlike , this follows the result's variablesReference,
+ /// so it can inspect a view-filtered expansion such as "expr,view(name)".
+ ///
+ IDictionary EvaluateChildren(string expression, EvaluateContext context = EvaluateContext.None);
+
///
/// Gets the source associated with this frame
///