Skip to content

Commit

Permalink
Use context information and correct replacement ranges when completin…
Browse files Browse the repository at this point in the history
…g <<jump>> commands
  • Loading branch information
desplesda committed Aug 8, 2023
1 parent ca9c2ea commit cf47b6c
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 42 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

- Fixed a bug in the language server that would cause it to crash when opening a workspace with no root (for example, creating a new window in Visual Studio Code and then creating a Yarn file, without ever saving anything to disk.)
- Language Server: Fixed an issue where workspaces where no Yarn Projects exist on disk would fail to attach Yarn files to the workspace's implicit Yarn Project.
- Language Server: Improved the code-completion behaviour to provide better filtering when offering command completions.
- Language Server: Improved the code-completion behaviour to provide better filtering when offering command completions, in both jump commands and custom commands.

### Removed

Expand Down
68 changes: 68 additions & 0 deletions YarnSpinner.LanguageServer.Tests/CompletionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,73 @@ public async Task Server_OnCompletingPartialCommand_ReturnsValidCompletionRange(
});
}

[Fact]
public async Task Server_OnCompletingJumpCommand_ReturnsNodeNames()
{
// Given
var (client, server) = await Initialize(ConfigureClient, ConfigureServer);
var filePath = Path.Combine(TestUtility.PathToTestWorkspace, "Project1", "Test.yarn");

var endOfJumpKeyword = new Position
{
Character = 7,
Line = 10
};

var expectedLineText = "<<jump Node2>>";
var lines = File.ReadAllLines(filePath).ElementAt(endOfJumpKeyword.Line).Should().Be(expectedLineText);

// When
var completionResults = await client.RequestCompletion(new CompletionParams
{
TextDocument = new TextDocumentIdentifier { Uri = filePath },
Position = endOfJumpKeyword
});

// Then
completionResults.Should().AllSatisfy(item =>
{
item.Kind.Should().Be(CompletionItemKind.Method, "node names are expected when completing a jump command");
item.TextEdit!.TextEdit!.Range.Start.Should().BeEquivalentTo(endOfJumpKeyword);
item.TextEdit.TextEdit.Range.End.Should().BeEquivalentTo(endOfJumpKeyword);
});
}

[Fact]
public async Task Server_OnCompletingPartialJumpCommand_ReturnsNodeNames()
{
// Given
var (client, server) = await Initialize(ConfigureClient, ConfigureServer);
var filePath = Path.Combine(TestUtility.PathToTestWorkspace, "Project1", "Test.yarn");

var endOfJumpKeyword = new Position
{
Character = 7,
Line = 10
};
var middleOfNodeName = endOfJumpKeyword with
{
Character = 9,
};

var expectedLineText = "<<jump Node2>>";
var lines = File.ReadAllLines(filePath).ElementAt(endOfJumpKeyword.Line).Should().Be(expectedLineText);

// When
var completionResults = await client.RequestCompletion(new CompletionParams
{
TextDocument = new TextDocumentIdentifier { Uri = filePath },
Position = middleOfNodeName
});

// Then
completionResults.Should().AllSatisfy(item =>
{
item.Kind.Should().Be(CompletionItemKind.Method, "node names are expected when completing a jump command");
item.TextEdit!.TextEdit!.Range.Start.Should().BeEquivalentTo(endOfJumpKeyword);
item.TextEdit.TextEdit.Range.End.Should().BeEquivalentTo(middleOfNodeName);
});
}

}
}
97 changes: 56 additions & 41 deletions YarnSpinner.LanguageServer/src/Server/Handlers/CompletionHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,7 @@ public Task<CompletionList> Handle(CompletionParams request, CancellationToken c
var vocabulary = yarnFile.Lexer.Vocabulary;
var tokenName = vocabulary.GetSymbolicName(tokenAtRequestPosition.Type);

void ExpandRangeToPreviousTokenOfType(int tokenType, int startIndex, ref Range range)
void ExpandRangeToEndOfPreviousTokenOfType(int tokenType, int startIndex, ref Range range)
{
var startToken = yarnFile.Tokens[startIndex];
var current = startToken;
Expand All @@ -272,54 +272,63 @@ void ExpandRangeToPreviousTokenOfType(int tokenType, int startIndex, ref Range r
}
}

switch (tokenAtRequestPosition.Type)
if (context?.IsChildOfContext<YarnSpinnerParser.Jump_statementContext>() ?? false)
{
case YarnSpinnerLexer.COMMAND_JUMP:
{
GetNodeNameCompletions(project, rangeOfTokenAtRequestPosition, results);

break;
}

case YarnSpinnerLexer.COMMAND_START:
{
// The token we're at is the start of a command
// statement. Collapse our replacement range to the end
// of that token.
rangeOfTokenAtRequestPosition = rangeOfTokenAtRequestPosition.CollapseToEnd();
GetCommandCompletions(request, rangeOfTokenAtRequestPosition, results);
// We're in the middle of a jump statement. Expand the
// replacement range to the end of the '<<jump ', and offer the
// list of nodes.
ExpandRangeToEndOfPreviousTokenOfType(YarnSpinnerLexer.COMMAND_JUMP, maybeTokenAtRequestPosition.Value, ref rangeOfTokenAtRequestPosition);

GetNodeNameCompletions(
project,
request,
rangeOfTokenAtRequestPosition,
results
);
}
else
{
switch (tokenAtRequestPosition.Type)
{
case YarnSpinnerLexer.COMMAND_START:
{
// The token we're at is the start of a command
// statement. Collapse our replacement range to the end
// of that token.
rangeOfTokenAtRequestPosition = rangeOfTokenAtRequestPosition.CollapseToEnd();
GetCommandCompletions(request, rangeOfTokenAtRequestPosition, results);

break;
}
break;
}

case YarnSpinnerLexer.COMMAND_TEXT:
{
// The token we're at is in the middle of a command
// statement. Expand our range to the end of our
// starting command token, so that the results we send
// back can be filtered.
ExpandRangeToPreviousTokenOfType(YarnSpinnerLexer.COMMAND_START, maybeTokenAtRequestPosition.Value, ref rangeOfTokenAtRequestPosition);
GetCommandCompletions(request, rangeOfTokenAtRequestPosition, results);

break;
}
case YarnSpinnerLexer.COMMAND_TEXT:
{
// The token we're at is in the middle of a command
// statement. Expand our range to the end of our
// starting command token, so that the results we send
// back can be filtered.
ExpandRangeToEndOfPreviousTokenOfType(YarnSpinnerLexer.COMMAND_START, maybeTokenAtRequestPosition.Value, ref rangeOfTokenAtRequestPosition);
GetCommandCompletions(request, rangeOfTokenAtRequestPosition, results);

break;
}

// inline expressions, if, and elseif are the same thing
case YarnSpinnerLexer.EXPRESSION_START:
case YarnSpinnerLexer.COMMAND_EXPRESSION_START:
case YarnSpinnerLexer.COMMAND_IF:
case YarnSpinnerLexer.COMMAND_ELSEIF:
{
GetVariableNameCompletions(project, rangeOfTokenAtRequestPosition, results);
// inline expressions, if, and elseif are the same thing
case YarnSpinnerLexer.EXPRESSION_START:
case YarnSpinnerLexer.COMMAND_EXPRESSION_START:
case YarnSpinnerLexer.COMMAND_IF:
case YarnSpinnerLexer.COMMAND_ELSEIF:
{
GetVariableNameCompletions(project, rangeOfTokenAtRequestPosition, results);

break;
}
break;
}
}
}

return Task.FromResult(new CompletionList(results));
}

private static void GetNodeNameCompletions(Project project, Range indexTokenRange, List<CompletionItem> results)
private static void GetNodeNameCompletions(Project project, CompletionParams request, Range indexTokenRange, List<CompletionItem> results)
{
foreach (var node in project.Nodes)
{
Expand All @@ -328,7 +337,13 @@ private static void GetNodeNameCompletions(Project project, Range indexTokenRang
Label = node.Title,
Kind = CompletionItemKind.Method,
Detail = System.IO.Path.GetFileName(node.File.Uri.AbsolutePath),
TextEdit = new TextEditOrInsertReplaceEdit(new TextEdit { NewText = node.Title, Range = indexTokenRange.CollapseToEnd() }),
TextEdit = new TextEditOrInsertReplaceEdit(new TextEdit {
NewText = node.Title,
Range = new Range {
Start = indexTokenRange.Start,
End = request.Position,
},
}),
});
}
}
Expand Down

0 comments on commit cf47b6c

Please sign in to comment.