Commit: e21669c
Parent: 592fafe

Nicer labels

Mårten Åsberg committed on 2026-10-29 at 20:02
Assisted-by: GitHub Copilot:gpt-6.1-sol
ui/src/Graph/GraphLayout.cs +17 -7
diff --git a/ui/src/Graph/GraphLayout.cs b/ui/src/Graph/GraphLayout.cs
index dd2c935..f1f8cb6 100644
@@ -11,12 +11,8 @@ public static class GraphLayout
{
private const double SiblingGap = 32;
private static readonly SymbolDisplayFormat LabelFormat = new(
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameOnly,
typeQualificationStyle: SymbolDisplayTypeQualificationStyle.NameAndContainingTypes,
genericsOptions: SymbolDisplayGenericsOptions.IncludeTypeParameters,
memberOptions: SymbolDisplayMemberOptions.IncludeParameters,
parameterOptions: SymbolDisplayParameterOptions.IncludeType
| SymbolDisplayParameterOptions.IncludeName
| SymbolDisplayParameterOptions.IncludeParamsRefOut,
miscellaneousOptions: SymbolDisplayMiscellaneousOptions.UseSpecialTypes
| SymbolDisplayMiscellaneousOptions.EscapeKeywordIdentifiers
| SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
@@ -77,7 +73,7 @@ public static class GraphLayout
var node = new GraphNode(
nodes.Count,
symbol,
symbol.ToDisplayString(LabelFormat),
FormatLabel(symbol),
sourceLink,
size,
isOrigin,
@@ -198,7 +194,7 @@ public static class GraphLayout
/// <summary>Events and wider reach add prominence; repeats shrink within readable bounds. Origin size is independent.</summary>
public static Size CalculateNodeSize(CallAnalysis call, uint appearances)
{
var labelWidth = Math.Clamp(call.Target.ToDisplayString(LabelFormat).Length * 7.5 + 36, 180, 360);
var labelWidth = Math.Clamp(FormatLabel(call.Target).Length * 7.5 + 36, 180, 360);
var prominence =
1
+ (call.LinkKind == LinkKind.Event ? 0.3 : 0)
@@ -219,6 +215,20 @@ public static class GraphLayout
);
}
private static string FormatLabel(ISymbol symbol)
{
var type = symbol.ContainingType.ToDisplayString(LabelFormat);
return symbol switch
{
IMethodSymbol { MethodKind: MethodKind.Constructor or MethodKind.StaticConstructor } => "new "
+ type
+ "()",
IMethodSymbol => type + "." + symbol.ToDisplayString(LabelFormat) + "()",
IPropertySymbol { IsIndexer: true } => type + ".this[]",
_ => type + "." + symbol.ToDisplayString(LabelFormat),
};
}
public static double CalculateThickness(CallAnalysis call) =>
call.LinkKind switch
{
ui/tests/GraphLayoutTests/BuildShould.cs +38 -9
diff --git a/ui/tests/GraphLayoutTests/BuildShould.cs b/ui/tests/GraphLayoutTests/BuildShould.cs
index e6808c1..7fbb193 100644
@@ -115,6 +115,7 @@ public sealed class BuildShould
var references = scene.Nodes.Where(n => n.IsCycleReference).ToArray();
references.Length.ShouldBe(2);
references.ShouldAllBe(n => n.Symbol.Name == "Root" && !n.IsOrigin);
references.ShouldAllBe(n => n.Label == "Methods.Root()");
references.ShouldAllBe(n => !scene.Edges.Any(e => ReferenceEquals(e.Source, n)));
scene.Nodes.Count(n => n.Symbol.Name == "Left").ShouldBe(2);
scene.Edges.ShouldAllBe(e => !e.IsLoopback);
@@ -349,7 +350,7 @@ public sealed class BuildShould
nodes.Length.ShouldBe(2);
nodes[0].Symbol.ShouldBeSameAs(first);
nodes[1].Symbol.ShouldBeSameAs(second);
nodes[0].Label.ShouldBe("Shared(int value)");
nodes[0].Label.ShouldBe("Linked.Shared()");
nodes[1].Label.ShouldBe(nodes[0].Label);
nodes[0].SourceLink.ShouldStartWith("file:///project/Linked.cs#1:");
nodes[1].SourceLink.ShouldBe(nodes[0].SourceLink);
@@ -357,7 +358,7 @@ public sealed class BuildShould
}
[Fact]
public void FormatSignaturesAndOneBasedSourceLinksWithoutContainingNames()
public void IncludeTheTypeWithoutParametersOrNamespacesAndPreserveSourceLinks()
{
// Arrange
var tree = CSharpSyntaxTree.ParseText(
@@ -377,27 +378,46 @@ public sealed class BuildShould
var node = GraphLayout.Build(analysis).Nodes.Single();
// Assert
node.Label.ShouldBe("Root(int value, string text)");
node.Label.ShouldBe("Container.Root()");
var start = tree.GetLineSpan(symbol.DeclaringSyntaxReferences[0].Span).StartLinePosition;
node.SourceLink.ShouldBe($"file:///project/Example.cs#{start.Line + 1}:{start.Character + 1}");
node.Label.ShouldNotContain("Container");
node.Label.ShouldNotContain("Example");
}
[Fact]
public void FormatConstructorsAndPropertiesWithSimpleNames()
public void QualifyConstructorsPropertiesAndIndexersWithAllContainingTypes()
{
// Arrange
var tree = CSharpSyntaxTree.ParseText(
"namespace Example { class Container { public Container(int id) { } public string Name { get; set; } public string this[int index] => Name; } }"
"""
namespace Example
{
class Outer<T>
{
class Middle
{
class Container<U>
{
public Container(int id) { }
public string Name { get; set; }
public string this[int index] => Name;
public void Run<V>(int value) { }
public void Run<V>(string value) { }
}
}
}
}
"""
);
var type = CSharpCompilation.Create("Members", [tree]).GetTypeByMetadataName("Example.Container")!;
var type = CSharpCompilation
.Create("Members", [tree])
.GetTypeByMetadataName("Example.Outer`1+Middle+Container`1")!;
var members = new[]
{
type.InstanceConstructors.Single(),
type.GetMembers("Name").Single(),
type.GetMembers().OfType<IPropertySymbol>().Single(p => p.IsIndexer),
};
}.Concat(type.GetMembers("Run"));
// Act
var labels = members
@@ -417,7 +437,16 @@ public sealed class BuildShould
.ToArray();
// Assert
labels.ShouldBe(new[] { "Container(int id)", "Name", "this[int index]" });
labels.ShouldBe(
new[]
{
"new Outer<T>.Middle.Container<U>()",
"Outer<T>.Middle.Container<U>.Name",
"Outer<T>.Middle.Container<U>.this[]",
"Outer<T>.Middle.Container<U>.Run<V>()",
"Outer<T>.Middle.Container<U>.Run<V>()",
}
);
}
[Fact]
ui/tests/MainWindowTests/InteractShould.cs +3 -0
diff --git a/ui/tests/MainWindowTests/InteractShould.cs b/ui/tests/MainWindowTests/InteractShould.cs
index 376f66f..20d0903 100644
@@ -251,6 +251,9 @@ public sealed class InteractShould
.Where(button => button.DataContext is GraphNode)
.ToArray();
buttons.Length.ShouldBe(scene.Nodes.Count);
buttons.ShouldAllBe(button =>
Equals(button.Content, "Flow." + ((GraphNode)button.DataContext!).Symbol.Name + "()")
);
buttons.ShouldAllBe(button => button.Bounds.Width > 0 && button.Bounds.Height > 0);
var editor = window.FindControl<NodifyEditor>("Editor")!;
var origin = buttons.Single(button => ((GraphNode)button.DataContext!).IsOrigin);