diff --git a/src/Bicep.Core/Navigation/SyntaxTriviaExtensions.cs b/src/Bicep.Core/Navigation/SyntaxTriviaExtensions.cs new file mode 100644 index 00000000000..76e061069d1 --- /dev/null +++ b/src/Bicep.Core/Navigation/SyntaxTriviaExtensions.cs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System; +using Bicep.Core.Syntax; + +namespace Bicep.Core.Navigation +{ + public static class SyntaxTriviaExtensions + { + public static SyntaxTrivia? TryFindMostSpecificTriviaInclusive(this SyntaxBase root, int offset, Func predicate) => + TryFindMostSpecificTriviaInternal(root, offset, predicate, inclusive: true); + + public static SyntaxTrivia? TryFindMostSpecificTriviaExclusive(this SyntaxBase root, int offset, Func predicate) => + TryFindMostSpecificTriviaInternal(root, offset, predicate, inclusive: false); + + private static SyntaxTrivia? TryFindMostSpecificTriviaInternal(SyntaxBase root, int offset, Func predicate, bool inclusive) + { + var visitor = new NavigationSearchVisitor(offset, predicate, inclusive); + visitor.Visit(root); + + return visitor.Result; + } + + private sealed class NavigationSearchVisitor : SyntaxVisitor + { + private readonly int offset; + private readonly Func predicate; + private readonly bool inclusive; + + public NavigationSearchVisitor(int offset, Func predicate, bool inclusive) + { + this.offset = offset; + this.predicate = predicate; + this.inclusive = inclusive; + } + + public SyntaxTrivia? Result { get; private set; } + + public override void VisitSyntaxTrivia(SyntaxTrivia node) + { + // check if offset is inside the node's span + if (CheckNodeContainsOffset(node)) + { + // the node span contains the offset + // check the predicate + if (this.predicate(node)) + { + // store the potential result + this.Result = node; + } + } + } + + private bool CheckNodeContainsOffset(SyntaxTrivia node) => this.inclusive + ? node.Span.ContainsInclusive(offset) + : node.Span.Contains(offset); + } + } +} \ No newline at end of file diff --git a/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs b/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs index bdc5b2ef032..898343d122a 100644 --- a/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs +++ b/src/Bicep.LangServer.UnitTests/BicepCompletionProviderTests.cs @@ -369,6 +369,28 @@ public void ParameterTypeContextShouldReturnDeclarationTypeCompletions() }); } + [DataTestMethod] + [DataRow("// |")] + [DataRow("/* |")] + [DataRow("param foo // |")] + [DataRow("param foo /* |")] + [DataRow("param /*| */ foo")] + [DataRow(@"/* +* +* | +*/")] + public void CommentShouldNotGiveAnyCompletions(string codeFragment) + { + var grouping = SyntaxFactory.CreateFromText(codeFragment); + var compilation = new Compilation(TestResourceTypeProvider.Create(), grouping); + var provider = new BicepCompletionProvider(); + + var offset = codeFragment.IndexOf('|'); + + var completions = provider.GetFilteredCompletions(compilation, BicepCompletionContext.Create(grouping.EntryPoint, offset)); + + completions.Should().BeEmpty(); + } private static void AssertExpectedDeclarationTypeCompletions(List completions) { completions.Should().SatisfyRespectively( diff --git a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs index e362bddf2b2..8c401a154c5 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionContext.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionContext.cs @@ -74,6 +74,15 @@ public static BicepCompletionContext Create(SyntaxTree syntaxTree, int offset) // this indicates a bug throw new ArgumentException($"The specified offset {offset} is outside the span of the specified {nameof(ProgramSyntax)} node."); } + + // the check at the beginning guarantees we have at least 1 node + var replacementRange = GetReplacementRange(syntaxTree, matchingNodes[^1], offset); + + var matchingTriviaType = FindTriviaMatchingOffset(syntaxTree.ProgramSyntax, offset)?.Type; + if (matchingTriviaType is not null && (matchingTriviaType == SyntaxTriviaType.MultiLineComment || matchingTriviaType == SyntaxTriviaType.SingleLineComment)) { + //we're in a comment, no hints here + return new BicepCompletionContext(BicepCompletionContextKind.None, replacementRange, null, null, null, null, null, null, null); + } var declarationInfo = FindLastNodeOfType(matchingNodes); var objectInfo = FindLastNodeOfType(matchingNodes); @@ -102,9 +111,6 @@ public static BicepCompletionContext Create(SyntaxTree syntaxTree, int offset) kind |= ConvertFlag(IsInnerExpressionContext(matchingNodes), BicepCompletionContextKind.Expression); } - // the check at the beginning guarantees we have at least 1 node - var replacementRange = GetReplacementRange(syntaxTree, matchingNodes[^1], offset); - return new BicepCompletionContext(kind, replacementRange, declarationInfo.node, objectInfo.node, propertyInfo.node, arrayInfo.node, propertyAccessInfo.node, arrayAccessInfo.node, targetScopeInfo.node); } @@ -133,6 +139,16 @@ private static List FindNodesMatchingOffset(ProgramSyntax syntax, in return nodes; } + /// + /// Returnes trivia which span contains the specified offset. + /// + /// The program node + /// The offset + private static SyntaxTrivia? FindTriviaMatchingOffset(ProgramSyntax syntax, int offset) + { + return syntax.TryFindMostSpecificTriviaInclusive(offset, current => true); + } + private static BicepCompletionContextKind ConvertFlag(bool value, BicepCompletionContextKind flag) => value ? flag : BicepCompletionContextKind.None; private static BicepCompletionContextKind GetDeclarationTypeFlags(IList matchingNodes, int offset)