Skip to content

Commit

Permalink
Support completions for import file paths (Azure#14659)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin authored Jul 26, 2024
1 parent aa4bdb7 commit 3863bf7
Show file tree
Hide file tree
Showing 4 changed files with 60 additions and 84 deletions.
9 changes: 9 additions & 0 deletions src/Bicep.Core/Extensions/IPositionableExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,14 @@ public static bool IsOverlapping(this IPositionable positionable, int position)

public static bool IsEnclosing(this IPositionable positionable, int position)
=> positionable.GetPosition() < position && position < positionable.GetEndPosition();

public static bool IsBefore(this IPositionable positionable, int offset)
=> positionable.GetEndPosition() < offset;

public static bool IsAfter(this IPositionable positionable, int offset)
=> positionable.GetPosition() > offset;

public static bool IsOnOrAfter(this IPositionable positionable, int offset)
=> positionable.GetPosition() >= offset;
}
}
3 changes: 3 additions & 0 deletions src/Bicep.LangServer.IntegrationTests/CompletionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4025,6 +4025,9 @@ public async Task LoadFunctionsPathArgument_returnsSymbolsAndFilePathsInCompleti
[DataRow("module foo oth|", "other.bicep", "module foo 'other.bicep'|")]
[DataRow("module foo 'ot|h'", "other.bicep", "module foo 'other.bicep'|")]
[DataRow("module foo '../to2/|'", "main.bicep", "module foo '../to2/main.bicep'|")]
[DataRow("import {} from |", "other.bicep", "import {} from 'other.bicep'|")]
[DataRow("import {} from 'oth|'", "other.bicep", "import {} from 'other.bicep'|")]
[DataRow("import {} from oth|", "other.bicep", "import {} from 'other.bicep'|")]
public async Task Module_path_completions_are_offered(string fileWithCursors, string expectedLabel, string expectedResult)
{
var fileUri = InMemoryFileResolver.GetFileUri("/path/to/main.bicep");
Expand Down
89 changes: 47 additions & 42 deletions src/Bicep.LangServer/Completions/BicepCompletionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ public static BicepCompletionContext Create(IFeatureProvider featureProvider, Co
ConvertFlag(IsTargetScopeContext(matchingNodes, offset), BicepCompletionContextKind.TargetScope) |
ConvertFlag(IsDecoratorNameContext(matchingNodes, offset), BicepCompletionContextKind.DecoratorName) |
ConvertFlag(functionArgumentContext is not null, BicepCompletionContextKind.FunctionArgument | BicepCompletionContextKind.Expression) |
ConvertFlag(IsUsingDeclarationContext(matchingNodes, offset), BicepCompletionContextKind.UsingFilePath) |
ConvertFlag(IsUsingPathContext(matchingNodes, offset), BicepCompletionContextKind.UsingFilePath) |
ConvertFlag(IsTestPathContext(matchingNodes, offset), BicepCompletionContextKind.TestPath) |
ConvertFlag(IsModulePathContext(matchingNodes, offset), BicepCompletionContextKind.ModulePath) |
ConvertFlag(IsImportPathContext(matchingNodes, offset), BicepCompletionContextKind.ModulePath) |
ConvertFlag(IsParameterIdentifierContext(matchingNodes, offset), BicepCompletionContextKind.ParamIdentifier) |
ConvertFlag(IsParameterValueContext(matchingNodes, offset), BicepCompletionContextKind.ParamValue) |
ConvertFlag(IsObjectTypePropertyValueContext(matchingNodes, offset), BicepCompletionContextKind.ObjectTypePropertyValue) |
Expand All @@ -221,7 +224,6 @@ public static BicepCompletionContext Create(IFeatureProvider featureProvider, Co
ConvertFlag(IsImportedSymbolListItemContext(matchingNodes, offset), BicepCompletionContextKind.ImportedSymbolIdentifier) |
ConvertFlag(ExpectingContextualAsKeyword(matchingNodes, offset), BicepCompletionContextKind.ExpectingImportAsKeyword) |
ConvertFlag(ExpectingContextualFromKeyword(matchingNodes, offset), BicepCompletionContextKind.ExpectingImportFromKeyword) |
ConvertFlag(IsImportTargetContext(matchingNodes, offset), BicepCompletionContextKind.ModulePath) |
ConvertFlag(IsAfterSpreadTokenContext(matchingNodes, offset), BicepCompletionContextKind.Expression);

if (featureProvider.ExtensibilityEnabled)
Expand Down Expand Up @@ -402,9 +404,7 @@ output.Type is ResourceTypeSyntax type &&
return BicepCompletionContextKind.ResourceType;
}

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))
if (IsResourceTypeContext(matchingNodes, offset))
{
// the most specific matching node is a resource declaration
// the declaration syntax is "resource <identifier> '<type>' ..."
Expand All @@ -416,30 +416,6 @@ output.Type is ResourceTypeSyntax type &&
return BicepCompletionContextKind.ResourceType;
}

if (SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax>(matchingNodes, module => CheckTypeIsExpected(module.Name, module.Path)) ||
SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax, StringSyntax, Token>(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) ||
SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (module, skipped, _) => module.Path == skipped))
{
// the most specific matching node is a module declaration
// the declaration syntax is "module <identifier> '<path>' ..."
// the cursor position is on the type if we have an identifier (non-zero length span) and the offset matches the path position
// OR
// we are in a token that is inside a StringSyntax node, which is inside a module declaration
return BicepCompletionContextKind.ModulePath;
}

if (SyntaxMatcher.IsTailMatch<TestDeclarationSyntax>(matchingNodes, test => CheckTypeIsExpected(test.Name, test.Path)) ||
SyntaxMatcher.IsTailMatch<TestDeclarationSyntax, StringSyntax, Token>(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) ||
SyntaxMatcher.IsTailMatch<TestDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (test, skipped, _) => test.Path == skipped))
{
// the most specific matching node is a test declaration
// the declaration syntax is "test <identifier> '<path>' ..."
// the cursor position is on the type if we have an identifier (non-zero length span) and the offset matches the path position
// OR
// we are in a token that is inside a StringSyntax node, which is inside a module declaration
return BicepCompletionContextKind.TestPath;
}

return BicepCompletionContextKind.None;
}

Expand Down Expand Up @@ -845,10 +821,48 @@ private static bool ExpectingContextualFromKeyword(List<SyntaxBase> matchingNode
statement.FromClause is SkippedTriviaSyntax &&
statement.FromClause.Span.ContainsInclusive(offset));

private static bool IsImportTargetContext(List<SyntaxBase> matchingNodes, int offset) =>
// import {} | or import * as foo |
SyntaxMatcher.IsTailMatch<CompileTimeImportFromClauseSyntax>(matchingNodes, (fromClause) => offset > fromClause.Keyword.GetEndPosition()) ||
SyntaxMatcher.IsTailMatch<CompileTimeImportFromClauseSyntax, StringSyntax>(matchingNodes);
private static bool IsBetweenNodes(int offset, IPositionable first, IPositionable second)
=> first.Span.Length > 0 && first.IsBefore(offset) && second.IsOnOrAfter(offset);

private static bool IsImportPathContext(List<SyntaxBase> matchingNodes, int offset) =>
// import {} from |
SyntaxMatcher.IsTailMatch<CompileTimeImportFromClauseSyntax>(matchingNodes, (fromClause) => IsBetweenNodes(offset, fromClause.Keyword, fromClause.Path)) ||
// import {} from 'f|oo'
SyntaxMatcher.IsTailMatch<CompileTimeImportFromClauseSyntax, StringSyntax, Token>(matchingNodes, (fromClause, @string, _) => fromClause.Path == @string) ||
// import {} from fo|o
SyntaxMatcher.IsTailMatch<CompileTimeImportFromClauseSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (fromClause, skipped, _) => fromClause.Path == skipped);

private static bool IsModulePathContext(IList<SyntaxBase> matchingNodes, int offset) =>
// module foo | =
SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax>(matchingNodes, module => IsBetweenNodes(offset, module.Name, module.Path)) ||
// module foo 'f|oo'
SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax, StringSyntax, Token>(matchingNodes, (module, @string, _) => module.Path == @string) ||
// module foo fo|o
SyntaxMatcher.IsTailMatch<ModuleDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (module, skipped, _) => module.Path == skipped);

private static bool IsResourceTypeContext(IList<SyntaxBase> matchingNodes, int offset) =>
// resource foo | =
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax>(matchingNodes, resource => IsBetweenNodes(offset, resource.Name, resource.Type)) ||
// resource foo 'f|oo'
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax, StringSyntax, Token>(matchingNodes, (resource, @string, _) => resource.Type == @string) ||
// resource foo fo|o
SyntaxMatcher.IsTailMatch<ResourceDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (resource, skipped, _) => resource.Type == skipped);

private static bool IsTestPathContext(IList<SyntaxBase> matchingNodes, int offset) =>
// test foo | =
SyntaxMatcher.IsTailMatch<TestDeclarationSyntax>(matchingNodes, test => IsBetweenNodes(offset, test.Name, test.Path)) ||
// test foo 'f|oo'
SyntaxMatcher.IsTailMatch<TestDeclarationSyntax, StringSyntax, Token>(matchingNodes, (test, @string, _) => test.Path == @string) ||
// test foo fo|o
SyntaxMatcher.IsTailMatch<TestDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (test, skipped, _) => test.Path == skipped);

private static bool IsUsingPathContext(IList<SyntaxBase> matchingNodes, int offset) =>
// using |
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax>(matchingNodes, usingClause => usingClause.Keyword.IsBefore(offset)) ||
// using 'f|oo'
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax, StringSyntax, Token>(matchingNodes, (@using, @string, _) => @using.Path == @string) ||
// using fo|o
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (@using, skipped, _) => @using.Path == skipped);

private static bool IsResourceBodyContext(List<SyntaxBase> matchingNodes, int offset) =>
// resources only allow {} as the body so we don't need to worry about
Expand Down Expand Up @@ -1226,15 +1240,6 @@ TokenType.StringMiddlePiece when IsOffsetImmediatlyAfterNode(offset, token) => f
(operatorToken.Type == TokenType.Colon && operatorToken.GetEndPosition() == offset && ternaryOperation.FalseExpression is not SkippedTriviaSyntax)));
}

private static bool IsUsingDeclarationContext(List<SyntaxBase> matchingNodes, int offset) =>
// using |
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax>(matchingNodes) ||
// using '|'
// using 'f|oo'
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax, StringSyntax, Token>(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) ||
// using fo|o
SyntaxMatcher.IsTailMatch<UsingDeclarationSyntax, SkippedTriviaSyntax, Token>(matchingNodes, (_, _, token) => token.Type == TokenType.Identifier);

private static bool IsParameterIdentifierContext(List<SyntaxBase> matchingNodes, int offset) =>
// param |
SyntaxMatcher.IsTailMatch<ParameterAssignmentSyntax>(matchingNodes, (paramAssignment => paramAssignment.Name.IdentifierName == LanguageConstants.MissingName)) ||
Expand Down
43 changes: 1 addition & 42 deletions src/Bicep.LangServer/Completions/BicepCompletionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@ public async Task<IEnumerable<CompletionItem>> GetFilteredCompletions(Compilatio
.Concat(GetResourceTypeCompletions(model, context))
.Concat(GetResourceTypeFollowerCompletions(context))
.Concat(GetLocalModulePathCompletions(model, context))
.Concat(GetLocalTestPathCompletions(model, context))
.Concat(GetModuleBodyCompletions(model, context))
.Concat(GetTestBodyCompletions(model, context))
.Concat(GetResourceBodyCompletions(model, context))
Expand Down Expand Up @@ -608,6 +607,7 @@ private IEnumerable<CompletionItem> CreateDirectoryCompletionItems(Range replace
private IEnumerable<CompletionItem> GetLocalModulePathCompletions(SemanticModel model, BicepCompletionContext context)
{
if (!context.Kind.HasFlag(BicepCompletionContextKind.ModulePath) &&
!context.Kind.HasFlag(BicepCompletionContextKind.TestPath) &&
!context.Kind.HasFlag(BicepCompletionContextKind.UsingFilePath))
{
return [];
Expand Down Expand Up @@ -683,47 +683,6 @@ sourceFile is ArmTemplateFile &&
}
}

private IEnumerable<CompletionItem> GetLocalTestPathCompletions(SemanticModel model, BicepCompletionContext context)
{
if (!context.Kind.HasFlag(BicepCompletionContextKind.TestPath))
{
return [];
}

// To provide intellisense before the quotes are typed
if (context.EnclosingDeclaration is not TestDeclarationSyntax declarationSyntax
|| declarationSyntax.Path is not StringSyntax stringSyntax
|| stringSyntax.TryGetLiteralValue() is not string entered)
{
entered = "";
}

try
{
// These should only fail if we're not able to resolve cwd path or the entered string
if (TryGetFilesForPathCompletions(model.SourceFile.FileUri, entered) is not { } fileCompletionInfo)
{
return [];
}

var replacementRange = context.EnclosingDeclaration is TestDeclarationSyntax test ? test.Path.ToRange(model.SourceFile.LineStarts) : context.ReplacementRange;

// Prioritize .bicep files higher than other files.
var bicepFileItems = CreateFileCompletionItems(model.SourceFile.FileUri, replacementRange, fileCompletionInfo, IsBicepFile, CompletionPriority.High);
var dirItems = CreateDirectoryCompletionItems(replacementRange, fileCompletionInfo);

return bicepFileItems;
}
catch (DirectoryNotFoundException)
{
return [];
}

// Local functions.

bool IsBicepFile(Uri fileUri) => PathHelper.HasBicepExtension(fileUri);
}

private bool IsOciArtifactRegistryReference(BicepCompletionContext context)
{
return context.ReplacementTarget is Token token &&
Expand Down

0 comments on commit 3863bf7

Please sign in to comment.