Browse Source

Exclude leading indentation from highlight position ranges

MarkNodeStart and BeginSpan captured builder.Length to anchor a node range
or highlight span, but indentation is written lazily on the first token of a
line. A node or span opened at the start of an indented line therefore
recorded its start before the leading tabs, so the debug-step highlight (and
any span) extended back across the indentation to column 0.

Flush the pending indent in both before capturing the offset, matching the
WPF AvalonEditTextOutput.BeginSpan the Avalonia port derived from. The emitted
text is unchanged -- the indent is written either way, in the same place; only
the recorded start moves to the first real character.

Assisted-by: Claude:claude-opus-4-8:Claude Code
pull/3847/head
Siegfried Pammer 1 week ago committed by Siegfried Pammer
parent
commit
7eed5704dc
  1. 43
      ILSpy.Tests/Views/DebugStepsTests.cs
  2. 20
      ILSpy/TextView/AvaloniaEditTextOutput.cs

43
ILSpy.Tests/Views/DebugStepsTests.cs

@ -288,6 +288,49 @@ public class DebugStepsTests @@ -288,6 +288,49 @@ public class DebugStepsTests
return Task.CompletedTask;
}
[AvaloniaTest]
public Task MarkNodeStart_Excludes_Leading_Indentation()
{
// A node opened at the start of an indented line must record its range from the first real
// character, so the debug-step highlight does not extend across the indentation to column 0.
var output = new AvaloniaEditTextOutput();
output.Indent();
output.WriteLine();
var node = new object();
output.MarkNodeStart(node);
output.Write("statement;");
output.MarkNodeEnd(node);
output.NodeLookup.TryGetRange(node, out var range).Should().BeTrue();
output.GetText().Substring(range.Start, range.Length).Should().Be("statement;");
return Task.CompletedTask;
}
[AvaloniaTest]
public Task MarkNodeEnd_Records_Nodes_Regardless_Of_Close_Order()
{
// Node spans are keyed by identity, so closing an outer node before the inner one it still
// contains must not discard either range. A stack that popped by position would lose both.
var output = new AvaloniaEditTextOutput();
var outer = new object();
var inner = new object();
output.MarkNodeStart(outer);
output.Write("a(");
output.MarkNodeStart(inner);
output.Write("b");
output.MarkNodeEnd(outer);
output.Write(")");
output.MarkNodeEnd(inner);
output.NodeLookup.TryGetRange(outer, out var outerRange).Should().BeTrue();
output.GetText().Substring(outerRange.Start, outerRange.Length).Should().Be("a(b");
output.NodeLookup.TryGetRange(inner, out var innerRange).Should().BeTrue();
output.GetText().Substring(innerRange.Start, innerRange.Length).Should().Be("b)");
return Task.CompletedTask;
}
[AvaloniaTest]
public Task Pane_Reports_Not_Available_For_Languages_Without_Debug_Steps()
{

20
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -54,7 +54,10 @@ namespace ICSharpCode.ILSpy.TextView @@ -54,7 +54,10 @@ namespace ICSharpCode.ILSpy.TextView
public int LengthLimit { get; set; } = int.MaxValue;
readonly Stack<(int Offset, HighlightingColor Color)> openSpans = new();
readonly Stack<(object Node, int Offset)> openNodes = new();
// Keyed by node rather than a stack: node writes are strictly nested today, but keying by
// identity means a stray or out-of-order MarkNodeEnd is a clean no-op that can't desync the
// remaining open nodes. Each node is written exactly once, so there is no self-nesting.
readonly Dictionary<object, int> openNodeStarts = new(ReferenceEqualityComparer.Instance);
readonly Stack<(NewFolding Folding, int StartLine)> openFoldings = new();
readonly List<NewFolding> foldings = new();
int indent;
@ -290,6 +293,7 @@ namespace ICSharpCode.ILSpy.TextView @@ -290,6 +293,7 @@ namespace ICSharpCode.ILSpy.TextView
public void BeginSpan(HighlightingColor highlightingColor)
{
WriteIndentIfNeeded();
openSpans.Push((builder.Length, highlightingColor));
}
@ -308,17 +312,17 @@ namespace ICSharpCode.ILSpy.TextView @@ -308,17 +312,17 @@ namespace ICSharpCode.ILSpy.TextView
public void MarkNodeStart(object node)
{
openNodes.Push((node, builder.Length));
// Flush a pending indent before capturing the offset so a node opened at the start of a
// line records its range from the first real character, not from column 0 across the
// leading indentation.
WriteIndentIfNeeded();
openNodeStarts[node] = builder.Length;
}
public void MarkNodeEnd(object node)
{
if (openNodes.Count == 0)
return;
var (currentNode, start) = openNodes.Pop();
if (!ReferenceEquals(currentNode, node))
return;
NodeLookup.AddNode(node, start, builder.Length - start);
if (openNodeStarts.Remove(node, out var start))
NodeLookup.AddNode(node, start, builder.Length - start);
}
}
}

Loading…
Cancel
Save