diff --git a/src/MIDebugEngine/Natvis.Impl/Natvis.cs b/src/MIDebugEngine/Natvis.Impl/Natvis.cs index 5e7e30d05..061c8f457 100755 --- a/src/MIDebugEngine/Natvis.Impl/Natvis.cs +++ b/src/MIDebugEngine/Natvis.Impl/Natvis.cs @@ -249,6 +249,16 @@ 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); private List _typeVisualizers; private DebuggedProcess _process; private HostConfigurationStore _configStore; @@ -453,7 +463,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 +489,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 +541,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 +549,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 +606,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 +624,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 +961,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 +979,96 @@ 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. + var localVars = new Dictionary(StringComparer.Ordinal); + if (customList.Items != null) + { + foreach (var v in customList.Items) + { + if (string.IsNullOrEmpty(v.Name)) continue; + string initVal = v.InitialValue ?? "0"; + // Resolve field names, template parameters and intrinsics in the initial value. + localVars[v.Name] = ReplaceNamesInExpression(initVal, 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: false); + } + 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); + + foreach (var loop in customList.Loop) + { + if (loop?.Items == null || ctx.Done) continue; + + // Drive the loop: each call to ExecuteCustomListBody runs one full pass + // through the loop body (Break -> Item(s) -> Exec, in document order). + // The limit must cover fast-forwarding through startIndex items plus one + // page of MAX_EXPAND items, capped to avoid runaway loops. + long maxIter = Math.Min((long)startIndex + MAX_EXPAND + 1, 10000); + for (long iter = 0; !ctx.Done && ctx.GlobalIndex < ctx.TotalSize && iter < maxIter; iter++) + { + // While-guard: stop if the loop condition is false. + if (!string.IsNullOrEmpty(loop.Condition)) + { + string loopCond = SubstituteLocalVars(loop.Condition, localVars); + if (!EvalCondition(loopCond, variable, visualizer.ScopedNames, visualizer.Intrinsics)) + break; + } + bool progress = ExecuteCustomListBody(loop.Items, ctx, variable, visualizer, localVars, children); + if (!progress && !ctx.Done) + break; // no items emitted and no break — avoid infinite loop + } + } + } } if (!(variable is VisualizerWrapper) && !expandType.HideRawView) // don't stack wrappers, and respect HideRawView { @@ -1122,12 +1235,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 +1414,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 +1638,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 +1867,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 +1920,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 +1993,287 @@ 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; + + public CustomListLoopContext(uint startIndex, uint totalSize) + { + StartIndex = startIndex; + TotalSize = totalSize; + } + } + + /// + /// 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, + CustomListLoopContext ctx, + IVariableInformation variable, + VisualizerInfo visualizer, + Dictionary localVars, + List children) + { + bool progress = false; + + for (int idx = 0; idx < body.Length && !ctx.Done; idx++) + { + var elem = body[idx]; + + if (elem is CustomListBreakType 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 CustomListLoopItemType 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; + } + + if (ctx.GlobalIndex >= ctx.StartIndex && ctx.Emitted < MAX_EXPAND) + { + 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; + + // Check whether the page is now full. + if (ctx.Emitted >= MAX_EXPAND && ctx.GlobalIndex < ctx.TotalSize) + { + children.Add(new PaginatedVisualizerWrapper( + ResourceStrings.MoreView, _process.Engine, variable, + visualizer, isVisualizerView: true, ctx.StartIndex + MAX_EXPAND)); + ctx.Done = true; + } + } + else if (elem is CustomListExecType exec) + { + // varName = expr: update a local variable. + if (exec.Condition != null) + { + string condExpr = SubstituteLocalVars(exec.Condition, localVars); + if (!EvalCondition(condExpr, variable, visualizer.ScopedNames, visualizer.Intrinsics)) + continue; + } + string execExpr = SubstituteLocalVars(exec.Value?.Trim() ?? "", localVars); + string updatedVar = ApplyExecToLocalVars(execExpr, localVars); + // Normalise the stored expression to prevent unbounded growth across iterations. + // After each i++, the expression would otherwise grow as "(((0)+1)+1)+1...". + // Evaluate the new expression and replace it with the scalar result so that + // each iteration starts from a compact literal (same principle as intrinsic-eval + // caching). Skip normalisation for pointer values (starts with "0x") — those + // must remain as expressions, not substituted as address literals. + if (updatedVar != null) + { + try + { + string normalized = GetExpressionValue(localVars[updatedVar], variable, visualizer.ScopedNames, visualizer.Intrinsics); + if (!string.IsNullOrEmpty(normalized) && + !normalized.TrimStart().StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + localVars[updatedVar] = normalized; + } + } + catch (Exception) { /* keep the expression as-is if evaluation fails */ } + } + progress = true; // Exec advances loop state (e.g. iSpan++) even when no Item is emitted + } + else if (elem is CustomListIfType ifElem) + { + // optionally followed by any number of s and a final . + // Execute the first branch whose condition holds; consume all siblings. + 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, ctx, variable, visualizer, localVars, children); + + // Consume any immediately following elements. + while (idx + 1 < body.Length && body[idx + 1] is CustomListElseIfType elseIfElem) + { + 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, ctx, variable, visualizer, localVars, children); + taken = true; + } + } + } + + // Consume an immediately following element. + if (idx + 1 < body.Length && body[idx + 1] is CustomListElseType elseElem) + { + idx++; + if (!taken && elseElem.Items != null) + progress |= ExecuteCustomListBody(elseElem.Items, ctx, variable, visualizer, localVars, children); + } + } + else if (elem is CustomListLoopType nestedLoop) + { + // Nested : drive it like the top-level loop. + if (nestedLoop.Items != null) + { + // Cap iterations at ctx.StartIndex + MAX_EXPAND + 1: that is the highest + // GlobalIndex at which we could still emit or detect the page-full sentinel, + // so any further iteration would be dead work. The hard cap of 10 000 guards + // against infinite loops in malformed natvis where TotalSize is not set. + long maxInner = Math.Min((long)ctx.StartIndex + MAX_EXPAND + 1, 10000); + for (long iter = 0; !ctx.Done && ctx.GlobalIndex < ctx.TotalSize && iter < maxInner; iter++) + { + // While-guard: stop if the loop condition is false. + if (!string.IsNullOrEmpty(nestedLoop.Condition)) + { + string loopCond = SubstituteLocalVars(nestedLoop.Condition, localVars); + if (!EvalCondition(loopCond, variable, visualizer.ScopedNames, visualizer.Intrinsics)) + break; + } + bool innerProgress = ExecuteCustomListBody(nestedLoop.Items, ctx, variable, visualizer, localVars, children); + if (!innerProgress && !ctx.Done) break; + progress = true; + } + } + } + } + + 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 string ApplyExecToLocalVars(string execExpr, Dictionary localVars) + { + if (string.IsNullOrEmpty(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; + } + + /// + /// 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..b6084182b 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 CustomListLoopType[] 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 CustomListLoopType[] 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,232 @@ 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, and Else elements. + // Nested Loop elements are also supported inside If/Else bodies. + + /// + /// 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 CustomListLoopItemType { + 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 CustomListBreakType { + 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 CustomListExecType { + 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 CustomListIfType { + private string conditionField; + private object[] itemsField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Condition { + get { return this.conditionField; } + set { this.conditionField = value; } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListLoopItemType))] + [System.Xml.Serialization.XmlElementAttribute("Break", typeof(CustomListBreakType))] + [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(CustomListExecType))] + [System.Xml.Serialization.XmlElementAttribute("If", typeof(CustomListIfType))] + [System.Xml.Serialization.XmlElementAttribute("ElseIf", typeof(CustomListElseIfType))] + [System.Xml.Serialization.XmlElementAttribute("Else", typeof(CustomListElseType))] + [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(CustomListLoopType))] + public object[] Items { + get { return this.itemsField; } + set { this.itemsField = value; } + } + } + + /// + /// Represents an <ElseIf> element that immediately follows an <If> or + /// another <ElseIf>. Its body is executed when all preceding branch conditions + /// were false and this Condition holds. + /// + [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 CustomListElseIfType { + private string conditionField; + private object[] itemsField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Condition { + get { return this.conditionField; } + set { this.conditionField = value; } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListLoopItemType))] + [System.Xml.Serialization.XmlElementAttribute("Break", typeof(CustomListBreakType))] + [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(CustomListExecType))] + [System.Xml.Serialization.XmlElementAttribute("If", typeof(CustomListIfType))] + [System.Xml.Serialization.XmlElementAttribute("ElseIf", typeof(CustomListElseIfType))] + [System.Xml.Serialization.XmlElementAttribute("Else", typeof(CustomListElseType))] + [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(CustomListLoopType))] + public object[] Items { + get { return this.itemsField; } + set { this.itemsField = 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 CustomListElseType { + private object[] itemsField; + + /// + [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListLoopItemType))] + [System.Xml.Serialization.XmlElementAttribute("Break", typeof(CustomListBreakType))] + [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(CustomListExecType))] + [System.Xml.Serialization.XmlElementAttribute("If", typeof(CustomListIfType))] + [System.Xml.Serialization.XmlElementAttribute("ElseIf", typeof(CustomListElseIfType))] + [System.Xml.Serialization.XmlElementAttribute("Else", typeof(CustomListElseType))] + [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(CustomListLoopType))] + public object[] Items { + get { return this.itemsField; } + set { this.itemsField = 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 CustomListLoopType { + private string conditionField; + private object[] itemsField; + + /// + [System.Xml.Serialization.XmlAttributeAttribute()] + public string Condition { + get { return this.conditionField; } + set { this.conditionField = value; } + } + + /// + [System.Xml.Serialization.XmlElementAttribute("Item", typeof(CustomListLoopItemType))] + [System.Xml.Serialization.XmlElementAttribute("Break", typeof(CustomListBreakType))] + [System.Xml.Serialization.XmlElementAttribute("Exec", typeof(CustomListExecType))] + [System.Xml.Serialization.XmlElementAttribute("If", typeof(CustomListIfType))] + [System.Xml.Serialization.XmlElementAttribute("ElseIf", typeof(CustomListElseIfType))] + [System.Xml.Serialization.XmlElementAttribute("Else", typeof(CustomListElseType))] + [System.Xml.Serialization.XmlElementAttribute("Loop", typeof(CustomListLoopType))] + public object[] Items { + get { return this.itemsField; } + set { this.itemsField = value; } + } + } + + // ---- End CustomListItems loop-body types --------------------------------- + /// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] [System.SerializableAttribute()] @@ -717,13 +982,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 +1027,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 +1061,7 @@ public string Value { } } } - + /// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] [System.SerializableAttribute()] @@ -852,11 +1143,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 +1158,7 @@ public string Condition { this.conditionField = value; } } - + /// [System.Xml.Serialization.XmlTextAttribute()] public string Value { @@ -879,7 +1170,7 @@ public string Value { } } } - + /// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] [System.SerializableAttribute()] @@ -887,15 +1178,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 +1236,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 +1270,7 @@ public string Value { } } } - + /// [System.CodeDom.Compiler.GeneratedCodeAttribute("xsd", "4.8.3928.0")] [System.SerializableAttribute()] @@ -1263,17 +1580,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 +1651,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..186f72ab9 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,197 @@ 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)); + } } }