Skip to content

Commit b541492

Browse files
committed
Include computed node title in NodeInfo
1 parent b4426e9 commit b541492

File tree

14 files changed

+161
-74
lines changed

14 files changed

+161
-74
lines changed

YarnSpinner.Compiler/Utility.cs

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ namespace Yarn.Compiler
1111
using System.Globalization;
1212
using System.Linq;
1313
using System.Text;
14+
using Yarn.Utility;
1415

1516
/// <summary>
1617
/// Utility methods for working with line tags.
@@ -582,6 +583,81 @@ public static string GetCompiledCodeAsString(Program program, Library? l = null,
582583
return null;
583584
}
584585
}
586+
587+
/// <summary>
588+
/// Gets the title for a node as defined in the source code, along with
589+
/// its unique title (which may be different to the source title.)
590+
/// </summary>
591+
/// <param name="sourceFileName">The name of the file in which the node
592+
/// is defined, or <see langword="null"/> if not available.</param>
593+
/// <param name="nodeContext">The parsed node's context.</param>
594+
/// <param name="sourceTitle">On return, contains the title of the node, as it
595+
/// appears in the source code.</param>
596+
/// <param name="uniqueTitle">On return, contains the unique title of
597+
/// the node, as stored in the output program.</param>
598+
/// <returns><see langword="true"/> if the <paramref name="sourceTitle"/> and
599+
/// <paramref name="uniqueTitle"/> could be determined; <see
600+
/// langword="false"/> otherwise.</returns>
601+
public static bool TryGetNodeTitle(
602+
string? sourceFileName,
603+
YarnSpinnerParser.NodeContext nodeContext,
604+
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
605+
out string? sourceTitle,
606+
[System.Diagnostics.CodeAnalysis.NotNullWhen(true)]
607+
out string? uniqueTitle
608+
)
609+
{
610+
// Try and find the current title, if present.
611+
uniqueTitle = nodeContext.title_header()?.FirstOrDefault()?.title?.Text;
612+
613+
if (string.IsNullOrEmpty(uniqueTitle))
614+
{
615+
// No title was found.
616+
uniqueTitle = sourceTitle = null;
617+
return false;
618+
}
619+
620+
// The node group name of a node is its title, as it appears in the
621+
// source.
622+
sourceTitle = uniqueTitle;
623+
624+
if (nodeContext.GetWhenHeaders().Any())
625+
{
626+
// The node is in a node group. Its real title (as stored in the
627+
// Program) will be different, to uniquely identify it. This
628+
// value depends on whether it has an explicit subtitle or not.
629+
630+
var subtitle = nodeContext.GetHeader("subtitle")?.header_value?.Text;
631+
632+
// Calculate a new unique title for this node and update its title header
633+
if (subtitle != null && string.IsNullOrEmpty(subtitle) == false)
634+
{
635+
// The node's unique title is derived from its original
636+
// title and its subtitle.
637+
uniqueTitle = $"{uniqueTitle}.{subtitle}";
638+
}
639+
else
640+
{
641+
// The node's unique title is derived from its original
642+
// title, the name of the file it's in, and the position
643+
// it's in in the file.
644+
string checksum = CRC32.GetChecksumString(
645+
(sourceFileName ?? "")
646+
+ uniqueTitle
647+
+ nodeContext.Start.Line.ToString());
648+
649+
uniqueTitle = $"{uniqueTitle}.{checksum}";
650+
}
651+
}
652+
653+
if (uniqueTitle == null || sourceTitle == null)
654+
{
655+
uniqueTitle = sourceTitle = null;
656+
return false;
657+
}
658+
659+
return true;
660+
}
585661
}
586662

587663
public struct GraphingNode

YarnSpinner.Compiler/Visitors/NodeGroupVisitor.cs

Lines changed: 31 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
using Antlr4.Runtime;
22
using Antlr4.Runtime.Misc;
3-
using System;
43
using System.Linq;
5-
using Yarn.Utility;
64

75
namespace Yarn.Compiler
86
{
@@ -17,46 +15,45 @@ public NodeGroupVisitor(string sourceFile)
1715

1816
public override int VisitNode([NotNull] YarnSpinnerParser.NodeContext context)
1917
{
20-
var titleHeader = context.title_header().FirstOrDefault();
18+
if (!context.GetWhenHeaders().Any())
19+
{
20+
// The node does not contain any 'when' headers. It's not part
21+
// of a node group, and doesn't need to be modified.
22+
return base.VisitNode(context);
23+
}
2124

22-
if (titleHeader == null || titleHeader.title?.Text == null)
25+
// Get the node's title information.
26+
if (!Utility.TryGetNodeTitle(SourceFile, context, out var sourceTitle, out var uniqueTitle))
2327
{
24-
// The node doesn't have a title. It can't be part of a node group.
28+
// The node's title or nodegroup name can't be determined.
2529
return base.VisitNode(context);
2630
}
2731

28-
if (context.GetWhenHeaders().Any())
32+
var titleHeader = context.title_header().FirstOrDefault();
33+
34+
if (titleHeader == null || !context.GetWhenHeaders().Any())
2935
{
30-
// This node contains at least one 'when' header.
31-
var title = titleHeader.title.Text;
32-
33-
// Add a new header to mark which group it's from.
34-
var groupHeader = new YarnSpinnerParser.HeaderContext(context, 0)
35-
{
36-
header_key = new CommonToken(YarnSpinnerParser.ID, Node.NodeGroupHeader),
37-
header_value = new CommonToken(YarnSpinnerParser.REST_OF_LINE, title)
38-
};
39-
40-
context.AddChild(groupHeader);
41-
42-
var subtitle = context.GetHeader("subtitle")?.header_value ?? null;
43-
44-
// Calculate a new unique title for this node and update its title header
45-
string? newTitle;
46-
if (String.IsNullOrEmpty(subtitle?.Text) == false)
47-
{
48-
newTitle = $"{title}.{subtitle?.Text}";
49-
}
50-
else
51-
{
52-
newTitle = $"{title}.{CRC32.GetChecksumString(SourceFile + title + context.Start.Line.ToString())}";
53-
}
54-
55-
56-
// Update the title header to the new 'actual' title.
57-
titleHeader.title = new CommonToken(YarnSpinnerParser.REST_OF_LINE, newTitle);
36+
// The node either lacks a title header, or lacks 'when'
37+
// headers.
38+
return base.VisitNode(context);
5839
}
5940

41+
// This node contains a title header and at least one 'when' header.
42+
// It's in a node group. We need to mark it as belonging to a node
43+
// group and update its title.
44+
45+
// Add a new header to mark which group it's from.
46+
var groupHeader = new YarnSpinnerParser.HeaderContext(context, 0)
47+
{
48+
header_key = new CommonToken(YarnSpinnerParser.ID, Node.NodeGroupHeader),
49+
header_value = new CommonToken(YarnSpinnerParser.REST_OF_LINE, sourceTitle)
50+
};
51+
52+
context.AddChild(groupHeader);
53+
54+
// Update the title header to the new 'actual' title.
55+
titleHeader.title = new CommonToken(YarnSpinnerParser.REST_OF_LINE, uniqueTitle);
56+
6057
return base.VisitNode(context);
6158
}
6259
}

YarnSpinner.LanguageServer.Tests/CodeActionTests.cs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ public async Task Server_FixesJumpDestinationTypo()
4444
var codeActionHandler = new CodeActionHandler(workspace);
4545
var CodeActionParams = new CodeActionParams
4646
{
47-
Context = new CodeActionContext { Diagnostics = Container.From(jumpToWarning!)},
48-
TextDocument = new TextDocumentIdentifier(DocumentUri.FromFileSystemPath(filePath))
47+
Context = new CodeActionContext { Diagnostics = Container.From(jumpToWarning!) },
48+
TextDocument = new TextDocumentIdentifier(DocumentUri.FromFileSystemPath(filePath))
4949
};
5050

5151
var commandOrCodeActions = await codeActionHandler.Handle(CodeActionParams, default);
@@ -66,9 +66,9 @@ public async Task Server_FixesJumpDestinationTypo()
6666

6767
nodeInfo = await nodesChangedAfterRemovingNode;
6868

69-
var jumpToNode = nodeInfo.Nodes.Find(n => n.Title == "JumpToTest");
69+
var jumpToNode = nodeInfo.Nodes.Find(n => n.UniqueTitle == "JumpToTest");
7070
nodeInfo.Nodes.Should().HaveCount(nodeCount, "because didn't change any nodes");
71-
jumpToNode!.Jumps.Where(j=>j.DestinationTitle == "JumpToTest").Should().HaveCount(1, "because we fixed the typo and are jumping to the existing JumpToTest node");
71+
jumpToNode!.Jumps.Where(j => j.DestinationTitle == "JumpToTest").Should().HaveCount(1, "because we fixed the typo and are jumping to the existing JumpToTest node");
7272
}
7373

7474
[Fact]
@@ -97,7 +97,7 @@ public async Task Server_CreatesNewNodeBasedOnJumpTarget()
9797
};
9898
var commandOrCodeActions = await codeActionHandler.Handle(CodeActionParams, default);
9999

100-
var generateNodeFix = commandOrCodeActions.FirstOrDefault(c =>
100+
var generateNodeFix = commandOrCodeActions.FirstOrDefault(c =>
101101
c.CodeAction?.Title?.Contains("Generate node 'Jump2Test'") ?? false);
102102

103103
generateNodeFix.Should().NotBeNull("Expecting a code action to generate a new node");
@@ -109,7 +109,7 @@ public async Task Server_CreatesNewNodeBasedOnJumpTarget()
109109
var nodeCount = nodeInfo.Nodes.Count;
110110

111111
// Remember how many jumps we had to JumpToTest we had before making the change
112-
var jumpCount = nodeInfo.Nodes.Find(n => n.Title == "JumpToTest")?.Jumps.Count ?? 0;
112+
var jumpCount = nodeInfo.Nodes.Find(n => n.UniqueTitle == "JumpToTest")?.Jumps.Count ?? 0;
113113

114114
// Expect to receive a 'nodes changed' notification
115115
Task<NodesChangedParams> nodesChangedAfterRemovingNode = GetNodesChangedNotificationAsync(n => n.Uri.ToString().Contains(filePath));
@@ -118,12 +118,12 @@ public async Task Server_CreatesNewNodeBasedOnJumpTarget()
118118

119119
nodeInfo = await nodesChangedAfterRemovingNode;
120120

121-
var jumpToNode = nodeInfo.Nodes.Find(n => n.Title == "JumpToTest");
122-
var jump2Node = nodeInfo.Nodes.Find(n => n.Title == "Jump2Test");
121+
var jumpToNode = nodeInfo.Nodes.Find(n => n.UniqueTitle == "JumpToTest");
122+
var jump2Node = nodeInfo.Nodes.Find(n => n.UniqueTitle == "Jump2Test");
123123
jump2Node.Should().NotBeNull("because we have created a new node");
124124
nodeInfo.Nodes.Should().HaveCount(nodeCount + 1, "because we have added a single new node");
125-
jumpToNode!.Jumps.Where(j=>j.DestinationTitle == "JumpToTest").Should().HaveCount(0, "because we are jumping to the new generated node, not the exisiting JumpToTest node");
126-
jumpToNode!.Jumps.Where(j=>j.DestinationTitle == "Jump2Test").Should().HaveCount(1, "because we are jumping to the new generated node, not the exisiting JumpToTest node");
125+
jumpToNode!.Jumps.Where(j => j.DestinationTitle == "JumpToTest").Should().HaveCount(0, "because we are jumping to the new generated node, not the exisiting JumpToTest node");
126+
jumpToNode!.Jumps.Where(j => j.DestinationTitle == "Jump2Test").Should().HaveCount(1, "because we are jumping to the new generated node, not the exisiting JumpToTest node");
127127
}
128128

129129
}

YarnSpinner.LanguageServer.Tests/CommandTests.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,13 @@ public async Task Server_CanListNodesInFile()
3939

4040
foreach (var node in result)
4141
{
42-
node.Title.Should().NotBeNullOrEmpty("because all nodes have a title");
42+
node.UniqueTitle.Should().NotBeNullOrEmpty("because all nodes have a title");
4343
node.Headers.Should().NotBeNullOrEmpty("because all nodes have headers");
4444
node.BodyStartLine.Should().NotBe(0, "because bodies never start on the first line");
4545
node.HeaderStartLine.Should().NotBe(node.BodyStartLine, "because bodies never start at the same line as their headers");
4646

4747
node.Headers.Should().Contain(h => h.Key == "title", "because all nodes have a header named 'title'")
48-
.Which.Value.Should().Be(node.Title, "because the 'title' header populates the Title property");
48+
.Which.Value.Should().Be(node.UniqueTitle, "because the 'title' header populates the Title property");
4949

5050
if (node == result.First())
5151
{
@@ -57,13 +57,13 @@ public async Task Server_CanListNodesInFile()
5757
}
5858
}
5959

60-
result.Should().Contain(n => n.Title == "Node2")
60+
result.Should().Contain(n => n.UniqueTitle == "Node2")
6161
.Which
6262
.Headers.Should().Contain(h => h.Key == "tags", "because Node2 has a 'tags' header")
6363
.Which
6464
.Value.Should().Be("wow incredible", "because Node2's 'tags' header has this value");
6565

66-
result.Should().Contain(n => n.Title == "Start")
66+
result.Should().Contain(n => n.UniqueTitle == "Start")
6767
.Which
6868
.Jumps.Should().NotBeNullOrEmpty("because the Start node contains jumps")
6969
.And
@@ -107,7 +107,7 @@ public async Task Server_OnAddNodeCommand_ReturnsTextEdit()
107107

108108
nodeInfo.Nodes.Should().HaveCount(count + 1, "because we added a node");
109109
nodeInfo.Nodes.Should()
110-
.Contain(n => n.Title == "Node",
110+
.Contain(n => n.UniqueTitle == "Node",
111111
"because the new node should be called Title")
112112
.Which.Headers.Should()
113113
.Contain(h => h.Key == "position" && h.Value == "100,100",
@@ -165,7 +165,7 @@ public async Task Server_OnUpdateHeaderCommand_ReturnsTextEditCreatingHeader()
165165

166166
nodeInfo
167167
.Nodes.Should()
168-
.Contain(n => n.Title == "Start")
168+
.Contain(n => n.UniqueTitle == "Start")
169169
.Which.Headers.Should()
170170
.NotContain(n => n.Key == "position",
171171
"because this node doesn't have this header");
@@ -193,7 +193,7 @@ public async Task Server_OnUpdateHeaderCommand_ReturnsTextEditCreatingHeader()
193193
nodeInfo = await nodesChangedAfterChangingText;
194194

195195
nodeInfo.Nodes.Should()
196-
.Contain(n => n.Title == "Start")
196+
.Contain(n => n.UniqueTitle == "Start")
197197
.Which.Headers.Should()
198198
.Contain(n => n.Key == "position",
199199
"because we added this new header")
@@ -219,7 +219,7 @@ public async Task Server_OnUpdateHeaderCommand_ReturnsTextEditModifyingHeader()
219219

220220
nodeInfo
221221
.Nodes.Should()
222-
.Contain(n => n.Title == "Node2")
222+
.Contain(n => n.UniqueTitle == "Node2")
223223
.Which.Headers.Should()
224224
.HaveCount(2)
225225
.And
@@ -248,7 +248,7 @@ public async Task Server_OnUpdateHeaderCommand_ReturnsTextEditModifyingHeader()
248248
nodeInfo = await nodesChangedAfterChangingText;
249249

250250
nodeInfo.Nodes.Should()
251-
.Contain(n => n.Title == "Node2")
251+
.Contain(n => n.UniqueTitle == "Node2")
252252
.Which.Headers.Should()
253253
.HaveCount(2, "because we added no new headers")
254254
.And.Contain(n => n.Key == headerName,

YarnSpinner.LanguageServer.Tests/HoverTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ public async Task Server_OnJumpToDefinition_GivesExpectedRange()
105105
var workspace = server.Services.GetService<Workspace>()!;
106106
var project = workspace.GetProjectsForUri(filePath).Single();
107107
var node = project.Nodes.Should()
108-
.ContainSingle(n => n.Title == "Node2", "the project should contain a single node called Node2")
108+
.ContainSingle(n => n.UniqueTitle == "Node2", "the project should contain a single node called Node2")
109109
.Subject;
110110

111111
var definition = definitionsResult.Should().ContainSingle().Subject;

YarnSpinner.LanguageServer.Tests/LanguageServerTests.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -85,11 +85,11 @@ public async Task Server_OnOpeningDocument_SendsNodesChangedNotification()
8585
nodeInfo.Should().NotBeNull("because this notification always carries a parameters object");
8686
nodeInfo.Nodes.Should().NotBeNullOrEmpty("because this notification always contains a list of node infos, even if it's empty");
8787

88-
nodeInfo.Nodes.Should().Contain(ni => ni.Title == "Start", "because this file contains a node with this title");
88+
nodeInfo.Nodes.Should().Contain(ni => ni.UniqueTitle == "Start", "because this file contains a node with this title");
8989

9090
nodeInfo.Nodes.Should()
9191
.Contain(
92-
ni => ni.Title == "Node2",
92+
ni => ni.UniqueTitle == "Node2",
9393
"because this file contains a node with this title")
9494
.Which.Headers.Should()
9595
.Contain(
@@ -125,7 +125,7 @@ public async Task Server_OnChangingDocument_SendsNodesChangedNotification()
125125
nodeInfo = await nodesChanged;
126126

127127
nodeInfo.Nodes.Should().HaveCount(6, "because we added a new node");
128-
nodeInfo.Nodes.Should().Contain(n => n.Title == "Node3", "because the new node we added has this title");
128+
nodeInfo.Nodes.Should().Contain(n => n.UniqueTitle == "Node3", "because the new node we added has this title");
129129
}
130130

131131
[Fact(Timeout = 2000)]

YarnSpinner.LanguageServer.Tests/ReferenceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ public async Task Workspace_FindsReferencesToNodes()
3535
(IEnumerable<Location> References, IEnumerable<NodeJump> Jumps) GetReferencesAndJumps(string name)
3636
{
3737
IEnumerable<Location> references = ReferencesHandler.GetReferences(project, name, YarnSymbolType.Node);
38-
IEnumerable<NodeJump> jumps = project.Nodes.Single(n => n.Title == name).Jumps;
38+
IEnumerable<NodeJump> jumps = project.Nodes.Single(n => n.UniqueTitle == name).Jumps;
3939
return (references, jumps);
4040
}
4141

YarnSpinner.LanguageServer.Tests/WorkspaceTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public void Workspaces_CanOpen()
5151
// The node NotIncludedInProject is inside a file that is not
5252
// included in a .yarnproject; because we have opened a workspace
5353
// that includes .yarnprojects, the file will not be included
54-
workspace.Projects.Should().AllSatisfy(p => p.Nodes.Should().NotContain(n => n.Title == "NotIncludedInProject"));
54+
workspace.Projects.Should().AllSatisfy(p => p.Nodes.Should().NotContain(n => n.UniqueTitle == "NotIncludedInProject"));
5555

5656
workspace.Projects.Should().AllSatisfy(p => p.Uri!.Should().NotBeNull());
5757

@@ -76,7 +76,7 @@ public void Workspaces_WithNoProjects_HaveImplicitProject()
7676
// Then
7777
var project = workspace.Projects.Should().ContainSingle().Subject;
7878
var file = project.Files.Should().ContainSingle().Subject;
79-
file.NodeInfos.Should().Contain(n => n.Title == "NotIncludedInProject");
79+
file.NodeInfos.Should().Contain(n => n.UniqueTitle == "NotIncludedInProject");
8080
project.Diagnostics.Should().NotContain(d => d.Severity == Yarn.Compiler.Diagnostic.DiagnosticSeverity.Error);
8181

8282
}

YarnSpinner.LanguageServer/src/Server/Handlers/CompletionHandler.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -371,19 +371,19 @@ private static void GetNodeNameCompletions(Project project, CompletionParams req
371371
{
372372
foreach (var node in project.Nodes)
373373
{
374-
if (node.Title == null || node.File == null)
374+
if (node.UniqueTitle == null || node.File == null)
375375
{
376376
continue;
377377
}
378378

379379
results.Add(new CompletionItem
380380
{
381-
Label = node.Title,
381+
Label = node.UniqueTitle,
382382
Kind = CompletionItemKind.Method,
383383
Detail = System.IO.Path.GetFileName(node.File.Uri.AbsolutePath),
384384
TextEdit = new TextEditOrInsertReplaceEdit(new TextEdit
385385
{
386-
NewText = node.Title,
386+
NewText = node.UniqueTitle,
387387
Range = new Range
388388
{
389389
Start = indexTokenRange.Start,

0 commit comments

Comments
 (0)