Skip to content

Commit

Permalink
Fix for partially-typed resource type completions (Azure#9158)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin committed Dec 1, 2022
1 parent 064cb8b commit 6f380fe
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 6 deletions.
56 changes: 56 additions & 0 deletions src/Bicep.LangServer.IntegrationTests/CompletionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,62 @@ static void AssertExistingKeywordCompletion(CompletionItem item)
'|');
}

[TestMethod]
public async Task Partial_identifier_resource_type_completions_work()
{
{
var fileWithCursors = @"
resource myRes Te|st
";

var (text, cursor) = ParserHelper.GetFileWithSingleCursor(fileWithCursors, '|');
var file = await new ServerRequestHelper(TestContext, ServerWithBuiltInTypes).OpenFile(text);

var completions = await file.RequestCompletion(cursor);

var updatedFile = file.ApplyCompletion(completions, "'Test.Rp/basicTests'");
updatedFile.Should().HaveSourceText(@"
resource myRes 'Test.Rp/basicTests@|'
");
}
}

[TestMethod]
public async Task Partial_string_resource_type_completions_work()
{
{
var fileWithCursors = @"
resource myRes 'Test.Rp/ba|si
";

var (text, cursor) = ParserHelper.GetFileWithSingleCursor(fileWithCursors, '|');
var file = await new ServerRequestHelper(TestContext, ServerWithBuiltInTypes).OpenFile(text);

var completions = await file.RequestCompletion(cursor);

var updatedFile = file.ApplyCompletion(completions, "'Test.Rp/basicTests'");
updatedFile.Should().HaveSourceText(@"
resource myRes 'Test.Rp/basicTests@|'
");
}

{
var fileWithCursors = @"
resource myRes 'Test.Rp/basicTests@|
";

var (text, cursor) = ParserHelper.GetFileWithSingleCursor(fileWithCursors, '|');
var file = await new ServerRequestHelper(TestContext, ServerWithBuiltInTypes).OpenFile(text);

var completions = await file.RequestCompletion(cursor);

var updatedFile = file.ApplyCompletion(completions, "2020-01-01");
updatedFile.Should().HaveSourceText(@"
resource myRes 'Test.Rp/basicTests@2020-01-01'|
");
}
}

[TestMethod]
public async Task ResourceTypeFollowerWithoCompletionsOffersEqualsAndExisting()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ public async Task<CompletionList> RequestCompletion(int cursor)

public BicepFile ApplyCompletion(CompletionList completions, string label, params string[] tabStops)
{
// Should().Contain is superfluous here, but it gives a better assertion message when it fails
completions.Should().Contain(x => x.Label == label);
completions.Should().ContainSingle(x => x.Label == label);

return ApplyCompletion(completions.Single(x => x.Label == label), tabStops);
Expand Down
2 changes: 1 addition & 1 deletion src/Bicep.LangServer/Completions/BicepCompletionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,7 @@ private static BicepCompletionContextKind GetDeclarationTypeFlags(IList<SyntaxBa

if (SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax>(matchingNodes, resource => CheckTypeIsExpected(resource.Name, resource.Type)) ||
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax, StringSyntax, Token>(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) ||
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (resource, skipped, token) => resource.Type == skipped && token.Type == TokenType.Identifier))
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (resource, skipped, token) => resource.Type == skipped))
{
// the most specific matching node is a resource declaration
// the declaration syntax is "resource <identifier> '<type>' ..."
Expand Down
49 changes: 44 additions & 5 deletions src/Bicep.LangServer/Completions/BicepCompletionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,43 @@ private static IEnumerable<CompletionItem> GetUserDefinedTypeCompletions(Semanti
null => null,
};

private static string? TryGetSkippedTokenText(SkippedTriviaSyntax skippedTrivia)
{
// This method attempts to obtain text from a skipped token - in cases where the user has partially-typed syntax
// but may be looking for completions.
if (skippedTrivia.Elements.Length != 1 ||
skippedTrivia.Elements[0] is not Token token)
{
return null;
}

switch (token.Type)
{
case TokenType.Identifier:
return token.Text;

case TokenType.StringComplete:
if (!token.Text.EndsWith("'", StringComparison.Ordinal))
{
// An unterminated string will result in skipped trivia containing an unterminated token.
// Compensate here by building the expected token before lexing it.
token = SyntaxFactory.CreateToken(token.Type, $"{token.Text}'");
}

return Lexer.TryGetStringValue(token);

default:
return null;
}
}

private static string? TryGetEnteredTextFromStringOrSkipped(SyntaxBase syntax)
=> syntax switch {
StringSyntax s => s.TryGetLiteralValue(),
SkippedTriviaSyntax s => TryGetSkippedTokenText(s),
_ => null,
};

private IEnumerable<CompletionItem> GetResourceTypeCompletions(SemanticModel model, BicepCompletionContext context)
{
if (!context.Kind.HasFlag(BicepCompletionContextKind.ResourceType))
Expand Down Expand Up @@ -398,9 +435,11 @@ private IEnumerable<CompletionItem> GetResourceTypeCompletions(SemanticModel mod
return items;
}

static string? TryGetFullyQualifiedType(StringSyntax? stringSyntax)
static string? TryGetFullyQualifiedType(SyntaxBase? syntax)
{
if (stringSyntax?.TryGetLiteralValue() is string entered && ResourceTypeReference.HasResourceTypePrefix(entered))
if (syntax is not null &&
TryGetEnteredTextFromStringOrSkipped(syntax) is {} entered &&
ResourceTypeReference.HasResourceTypePrefix(entered))
{
return entered;
}
Expand All @@ -412,9 +451,9 @@ private IEnumerable<CompletionItem> GetResourceTypeCompletions(SemanticModel mod
{
return enclosingDeclaration switch
{
ResourceDeclarationSyntax resourceSyntax => TryGetFullyQualifiedType(resourceSyntax.TypeString),
ParameterDeclarationSyntax parameterSyntax when parameterSyntax.Type is ResourceTypeSyntax resourceType => TryGetFullyQualifiedType(resourceType.TypeString),
OutputDeclarationSyntax outputSyntax when outputSyntax.Type is ResourceTypeSyntax resourceType => TryGetFullyQualifiedType(resourceType.TypeString),
ResourceDeclarationSyntax resourceSyntax => TryGetFullyQualifiedType(resourceSyntax.Type),
ParameterDeclarationSyntax parameterSyntax when parameterSyntax.Type is ResourceTypeSyntax resourceType => TryGetFullyQualifiedType(resourceType.Type),
OutputDeclarationSyntax outputSyntax when outputSyntax.Type is ResourceTypeSyntax resourceType => TryGetFullyQualifiedType(resourceType.Type),
_ => null,
};
}
Expand Down

0 comments on commit 6f380fe

Please sign in to comment.