diff --git a/src/Bicep.Core/Extensions/IPositionableExtensions.cs b/src/Bicep.Core/Extensions/IPositionableExtensions.cs index 79184d1adad..8017713739a 100644 --- a/src/Bicep.Core/Extensions/IPositionableExtensions.cs +++ b/src/Bicep.Core/Extensions/IPositionableExtensions.cs @@ -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; } } diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index e2d0ba2ec23..3dacee2b1f2 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -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"); diff --git a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs index 028eb224277..b602c7bd317 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs @@ -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) | @@ -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) @@ -402,9 +404,7 @@ output.Type is ResourceTypeSyntax type && return BicepCompletionContextKind.ResourceType; } - if (SyntaxMatcher.IsTailMatch(matchingNodes, resource => CheckTypeIsExpected(resource.Name, resource.Type)) || - SyntaxMatcher.IsTailMatch(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) || - SyntaxMatcher.IsTailMatch(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 '' ..." @@ -416,30 +416,6 @@ output.Type is ResourceTypeSyntax type && return BicepCompletionContextKind.ResourceType; } - if (SyntaxMatcher.IsTailMatch(matchingNodes, module => CheckTypeIsExpected(module.Name, module.Path)) || - SyntaxMatcher.IsTailMatch(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) || - SyntaxMatcher.IsTailMatch(matchingNodes, (module, skipped, _) => module.Path == skipped)) - { - // the most specific matching node is a module declaration - // the declaration syntax is "module '' ..." - // 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(matchingNodes, test => CheckTypeIsExpected(test.Name, test.Path)) || - SyntaxMatcher.IsTailMatch(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) || - SyntaxMatcher.IsTailMatch(matchingNodes, (test, skipped, _) => test.Path == skipped)) - { - // the most specific matching node is a test declaration - // the declaration syntax is "test '' ..." - // 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; } @@ -845,10 +821,48 @@ private static bool ExpectingContextualFromKeyword(List matchingNode statement.FromClause is SkippedTriviaSyntax && statement.FromClause.Span.ContainsInclusive(offset)); - private static bool IsImportTargetContext(List matchingNodes, int offset) => - // import {} | or import * as foo | - SyntaxMatcher.IsTailMatch(matchingNodes, (fromClause) => offset > fromClause.Keyword.GetEndPosition()) || - SyntaxMatcher.IsTailMatch(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 matchingNodes, int offset) => + // import {} from | + SyntaxMatcher.IsTailMatch(matchingNodes, (fromClause) => IsBetweenNodes(offset, fromClause.Keyword, fromClause.Path)) || + // import {} from 'f|oo' + SyntaxMatcher.IsTailMatch(matchingNodes, (fromClause, @string, _) => fromClause.Path == @string) || + // import {} from fo|o + SyntaxMatcher.IsTailMatch(matchingNodes, (fromClause, skipped, _) => fromClause.Path == skipped); + + private static bool IsModulePathContext(IList matchingNodes, int offset) => + // module foo | = + SyntaxMatcher.IsTailMatch(matchingNodes, module => IsBetweenNodes(offset, module.Name, module.Path)) || + // module foo 'f|oo' + SyntaxMatcher.IsTailMatch(matchingNodes, (module, @string, _) => module.Path == @string) || + // module foo fo|o + SyntaxMatcher.IsTailMatch(matchingNodes, (module, skipped, _) => module.Path == skipped); + + private static bool IsResourceTypeContext(IList matchingNodes, int offset) => + // resource foo | = + SyntaxMatcher.IsTailMatch(matchingNodes, resource => IsBetweenNodes(offset, resource.Name, resource.Type)) || + // resource foo 'f|oo' + SyntaxMatcher.IsTailMatch(matchingNodes, (resource, @string, _) => resource.Type == @string) || + // resource foo fo|o + SyntaxMatcher.IsTailMatch(matchingNodes, (resource, skipped, _) => resource.Type == skipped); + + private static bool IsTestPathContext(IList matchingNodes, int offset) => + // test foo | = + SyntaxMatcher.IsTailMatch(matchingNodes, test => IsBetweenNodes(offset, test.Name, test.Path)) || + // test foo 'f|oo' + SyntaxMatcher.IsTailMatch(matchingNodes, (test, @string, _) => test.Path == @string) || + // test foo fo|o + SyntaxMatcher.IsTailMatch(matchingNodes, (test, skipped, _) => test.Path == skipped); + + private static bool IsUsingPathContext(IList matchingNodes, int offset) => + // using | + SyntaxMatcher.IsTailMatch(matchingNodes, usingClause => usingClause.Keyword.IsBefore(offset)) || + // using 'f|oo' + SyntaxMatcher.IsTailMatch(matchingNodes, (@using, @string, _) => @using.Path == @string) || + // using fo|o + SyntaxMatcher.IsTailMatch(matchingNodes, (@using, skipped, _) => @using.Path == skipped); private static bool IsResourceBodyContext(List matchingNodes, int offset) => // resources only allow {} as the body so we don't need to worry about @@ -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 matchingNodes, int offset) => - // using | - SyntaxMatcher.IsTailMatch(matchingNodes) || - // using '|' - // using 'f|oo' - SyntaxMatcher.IsTailMatch(matchingNodes, (_, _, token) => token.Type == TokenType.StringComplete) || - // using fo|o - SyntaxMatcher.IsTailMatch(matchingNodes, (_, _, token) => token.Type == TokenType.Identifier); - private static bool IsParameterIdentifierContext(List matchingNodes, int offset) => // param | SyntaxMatcher.IsTailMatch(matchingNodes, (paramAssignment => paramAssignment.Name.IdentifierName == LanguageConstants.MissingName)) || diff --git a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index c05c32c0297..b1bf3f3173e 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -71,7 +71,6 @@ public async Task> 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)) @@ -608,6 +607,7 @@ private IEnumerable CreateDirectoryCompletionItems(Range replace private IEnumerable GetLocalModulePathCompletions(SemanticModel model, BicepCompletionContext context) { if (!context.Kind.HasFlag(BicepCompletionContextKind.ModulePath) && + !context.Kind.HasFlag(BicepCompletionContextKind.TestPath) && !context.Kind.HasFlag(BicepCompletionContextKind.UsingFilePath)) { return []; @@ -683,47 +683,6 @@ sourceFile is ArmTemplateFile && } } - private IEnumerable 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 &&