public async Task GoToDefinitionOnUnboundSyntaxNodeShouldReturnEmptyResponse(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithText(dataSet.Bicep, uri); var compilation = new Compilation(SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var unboundNodes = SyntaxAggregator.Aggregate( source: compilation.ProgramSyntax, seed: new List <SyntaxBase>(), function: (accumulated, syntax) => { if (symbolTable.ContainsKey(syntax) == false && !(syntax is ProgramSyntax)) { // only collect unbound nodes non-program nodes accumulated.Add(syntax); } return(accumulated); }, resultSelector: accumulated => accumulated, // visit children only if current node is not bound continuationFunction: (accumulated, syntax) => symbolTable.ContainsKey(syntax) == false); foreach (var syntax in unboundNodes) { var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position) }); // go to definition on a syntax node that isn't bound to a symbol should produce an empty response response.Should().BeEmpty(); } }
public async Task GoToDefinitionRequestOnValidSymbolReferenceShouldReturnLocationOfDeclaredSymbol(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithText(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); // filter out symbols that don't have locations var declaredSymbolBindings = symbolTable .Where(pair => pair.Value is DeclaredSymbol) .Select(pair => new KeyValuePair <SyntaxBase, DeclaredSymbol>(pair.Key, (DeclaredSymbol)pair.Value)); foreach (var(syntax, symbol) in declaredSymbolBindings) { var response = await client.RequestDefinition(new DefinitionParams { TextDocument = new TextDocumentIdentifier(uri), Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position) }); var link = ValidateDefinitionResponse(response); // document should match the requested document link.TargetUri.Should().Be(uri); // target range should be the whole span of the symbol link.TargetRange.Should().Be(symbol.DeclaringSyntax.Span.ToRange(lineStarts)); // selection range should be the span of the identifier of the symbol link.TargetSelectionRange.Should().Be(symbol.NameSyntax.Span.ToRange(lineStarts)); // origin selection range should be the span of the syntax node that references the symbol link.OriginSelectionRange.Should().Be(syntax.ToRange(lineStarts)); } }
public async Task RequestingHighlightsForWrongNodeShouldProduceNoHighlights(DataSet dataSet) { // local function bool IsWrongNode(SyntaxBase node) => !(node is ISymbolReference) && !(node is IDeclarationSyntax) && !(node is Token); var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var wrongNodes = SyntaxAggregator.Aggregate( compilation.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (IsWrongNode(node) && !(node is ProgramSyntax)) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated, (accumulated, node) => IsWrongNode(node)); foreach (var syntax in wrongNodes) { var highlights = await client.RequestDocumentHighlight(new DocumentHighlightParams { TextDocument = new TextDocumentIdentifier(uri), Position = IntegrationTestHelper.GetPosition(lineStarts, syntax) }); highlights.Should().BeEmpty(); } }
public async Task FindReferencesWithoutDeclarationsShouldProduceCorrectResults(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); using var client = await IntegrationTestHelper.StartServerWithText(dataSet.Bicep, uri); var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var symbolToSyntaxLookup = symbolTable .Where(pair => pair.Value.Kind != SymbolKind.Error) .ToLookup(pair => pair.Value, pair => pair.Key); foreach (var(syntax, symbol) in symbolTable.Where(s => s.Value.Kind != SymbolKind.Error)) { var locations = await client.RequestReferences(new ReferenceParams { TextDocument = new TextDocumentIdentifier(uri), Context = new ReferenceContext { IncludeDeclaration = false }, Position = PositionHelper.GetPosition(lineStarts, syntax.Span.Position) }); // all URIs should be the same in the results locations.Select(r => r.Uri).Should().AllBeEquivalentTo(uri); // exclude declarations when calculating expected ranges var expectedRanges = symbolToSyntaxLookup[symbol] .Where(node => !(node is IDeclarationSyntax)) .Select(node => PositionHelper.GetNameRange(lineStarts, node)); // ranges should match what we got from our own symbol table locations.Select(l => l.Range).Should().BeEquivalentTo(expectedRanges); } }
public async Task RequestingCodeActionWithNonFixableDiagnosticsShouldProduceEmptyQuickFixes(DataSet dataSet) { var uri = DocumentUri.From($"/{dataSet.Name}"); var client = await IntegrationTestHelper.StartServerWithTextAsync(dataSet.Bicep, uri); // construct a parallel compilation var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxFactory.CreateFromText(dataSet.Bicep)); var lineStarts = TextCoordinateConverter.GetLineStarts(dataSet.Bicep); var nonFixables = compilation.GetSemanticModel().GetAllDiagnostics().Where(diagnostic => !(diagnostic is IFixable)); foreach (var nonFixable in nonFixables) { CommandOrCodeActionContainer?quickFixes = await client.RequestCodeAction(new CodeActionParams { TextDocument = new TextDocumentIdentifier(uri), Range = nonFixable.Span.ToRange(lineStarts) }); // Assert. quickFixes.Should().NotBeNull(); quickFixes.Should().BeEmpty(); } }
public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers(DataSet dataSet) { var(compilation, _, fileUri) = await dataSet.SetupPrerequisitesAndCreateCompilation(TestContext); var uri = DocumentUri.From(fileUri); var client = await IntegrationTestHelper.StartServerWithTextAsync(this.TestContext, dataSet.Bicep, uri, creationOptions : new LanguageServer.Server.CreationOptions(NamespaceProvider: BicepTestConstants.NamespaceProvider, FileResolver: BicepTestConstants.FileResolver)); var symbolTable = compilation.ReconstructSymbolTable(); var lineStarts = compilation.SourceFileGrouping.EntryPoint.LineStarts; var symbolReferences = SyntaxAggregator.Aggregate( compilation.SourceFileGrouping.EntryPoint.ProgramSyntax, new List <SyntaxBase>(), (accumulated, node) => { if (node is ISymbolReference || node is ITopLevelNamedDeclarationSyntax) { accumulated.Add(node); } return(accumulated); }, accumulated => accumulated); foreach (var symbolReference in symbolReferences) { // by default, request a hover on the first character of the syntax, but for certain syntaxes, this doesn't make sense. // for example on an instance function call 'az.resourceGroup()', it only makes sense to request a hover on the 3rd character. var nodeForHover = symbolReference switch { ITopLevelDeclarationSyntax d => d.Keyword, ResourceAccessSyntax r => r.ResourceName, FunctionCallSyntaxBase f => f.Name, _ => symbolReference, }; var hover = await client.RequestHover(new HoverParams { TextDocument = new TextDocumentIdentifier(uri), Position = TextCoordinateConverter.GetPosition(lineStarts, nodeForHover.Span.Position) }); // fancy method to give us some annotated source code to look at if any assertions fail :) using (new AssertionScope().WithVisualCursor(compilation.SourceFileGrouping.EntryPoint, nodeForHover.Span.ToZeroLengthSpan())) { if (!symbolTable.TryGetValue(symbolReference, out var symbol)) { if (symbolReference is InstanceFunctionCallSyntax && compilation.GetEntrypointSemanticModel().GetSymbolInfo(symbolReference) is FunctionSymbol ifcSymbol) { ValidateHover(hover, ifcSymbol); break; } // symbol ref not bound to a symbol hover.Should().BeNull(); continue; } switch (symbol !.Kind) {
// If the insertion path already exists, or can't be added (eg array instead of object exists on the path), returns null public (int line, int column, string insertText)? InsertIfNotExist(string[] propertyPaths, object valueIfNotExist) { if (propertyPaths.Length == 0) { throw new ArgumentException($"{nameof(propertyPaths)} must not be empty"); } if (string.IsNullOrWhiteSpace(_json)) { return(AppendToEndOfJson(Stringify(PropertyPathToObject(propertyPaths, valueIfNotExist)))); } TextReader textReader = new StringReader(_json); JsonReader jsonReader = new JsonTextReader(textReader); JObject?jObject = null; try { jObject = JObject.Load(jsonReader, new JsonLoadSettings { LineInfoHandling = LineInfoHandling.Load, CommentHandling = CommentHandling.Load, DuplicatePropertyNameHandling = DuplicatePropertyNameHandling.Ignore, }); } catch (Exception) { } if (jObject is null) { // Just append to existing text return(AppendToEndOfJson(Stringify(PropertyPathToObject(propertyPaths, valueIfNotExist)))); } JObject? currentObject = jObject; List <string> remainingPaths = new(propertyPaths); while (remainingPaths.Count > 0) { string path = PopFromLeft(remainingPaths); JToken?nextLevel = currentObject[path]; if (nextLevel is null) { int line = ((IJsonLineInfo)currentObject).LineNumber - 1; // 1-indexed to 0-indexed int column = ((IJsonLineInfo)currentObject).LinePosition - 1; object insertionValue = valueIfNotExist; remainingPaths.Reverse(); foreach (string propertyName in remainingPaths) { Dictionary <string, object> newObject = new(); newObject[propertyName] = insertionValue; insertionValue = newObject; } remainingPaths.Reverse(); string newPath = string.Join('.', remainingPaths); string insertionValueAsString = Stringify(insertionValue); int insertLine; int insertColumn; int currentIndent; bool hasSiblings = currentObject.Children().Any(child => child.Type != JTokenType.Comment); insertLine = line; insertColumn = column + 1; currentIndent = GetIndentationOfLine(line) + _indent; // use indent of line with the starting "{" as the nested indent level // We will insert before the first sibling string propertyInsertion = "\n" + IndentEachLine( $"\"{path}\": {insertionValueAsString}", currentIndent); if (hasSiblings) { propertyInsertion += ","; } // Need a newline after the insertion if there's anything else on the line var lineStarts = TextCoordinateConverter.GetLineStarts(_json); int offset = TextCoordinateConverter.GetOffset(lineStarts, line, column); char?charAfterInsertion = _json.Length > offset ? _json[offset + 1] : null; if (charAfterInsertion != '\n' && charAfterInsertion != '\r') { propertyInsertion += '\n'; } return(insertLine, insertColumn, propertyInsertion); } if (remainingPaths.Count == 0) { // We found matches all the way to the leaf, doesn't matter what the leaf value is, we will leave it alone return(null); } if (nextLevel is JObject nextObject) { currentObject = nextObject; } else { return(null); } } return(null); }
public JsonEditor(string json, int indent = 2) { _json = json; _indent = indent; _lineStarts = TextCoordinateConverter.GetLineStarts(json); }
public static string PrintWithAnnotations(BicepFile bicepFile, IEnumerable <Annotation> annotations, int context, bool includeLineNumbers) { if (!annotations.Any()) { return(""); } var output = new StringBuilder(); var programLines = GetProgramTextLines(bicepFile); var annotationPositions = annotations.ToDictionary( x => x, x => TextCoordinateConverter.GetPosition(bicepFile.LineStarts, x.Span.Position)); var annotationsByLine = annotationPositions.ToLookup(x => x.Value.line, x => x.Key); var minLine = annotationPositions.Values.Aggregate(int.MaxValue, (min, curr) => Math.Min(curr.line, min)); var maxLine = annotationPositions.Values.Aggregate(0, (max, curr) => Math.Max(curr.line, max)) + 1; minLine = Math.Max(0, minLine - context); maxLine = Math.Min(bicepFile.LineStarts.Length, maxLine + context); var digits = maxLine.ToString().Length; for (var i = minLine; i < maxLine; i++) { var gutterOffset = 0; if (includeLineNumbers) { var lineNumber = i + 1; // to match VSCode's line numbering (starting at 1) output.Append(lineNumber.ToString().PadLeft(digits, '0')); output.Append("| "); gutterOffset = digits + 2; } output.Append(programLines[i]); output.Append('\n'); var annotationsToDisplay = annotationsByLine[i].OrderBy(x => annotationPositions[x].character); foreach (var annotation in annotationsToDisplay) { var position = annotationPositions[annotation]; output.Append(new String(' ', gutterOffset + position.character)); switch (annotation.Span.Length) { case 0: output.Append("^"); break; case int x: // TODO handle annotation spanning multiple lines output.Append(new String('~', x)); break; } output.Append(" "); output.Append(annotation.Message); output.Append('\n'); } } return(output.ToString()); }
public async Task VerifyResourceBodyCompletionWithDiscriminatedObjectTypeContainsRequiredPropertiesSnippet() { string text = @"resource deploymentScripts 'Microsoft.Resources/deploymentScripts@2020-10-01'="; var syntaxTree = SyntaxTree.Create(new Uri("file:///main.bicep"), text); using var client = await IntegrationTestHelper.StartServerWithTextAsync(text, syntaxTree.FileUri, resourceTypeProvider : TypeProvider); var completions = await client.RequestCompletion(new CompletionParams { TextDocument = new TextDocumentIdentifier(syntaxTree.FileUri), Position = TextCoordinateConverter.GetPosition(syntaxTree.LineStarts, text.Length), }); completions.Should().SatisfyRespectively( c => { c.Label.Should().Be("{}"); }, c => { c.InsertTextFormat.Should().Be(InsertTextFormat.Snippet); c.Label.Should().Be("required-properties-AzureCLI"); c.Detail.Should().Be("Required properties"); c.TextEdit?.NewText?.Should().BeEquivalentToIgnoringNewlines(@"{ name: $1 location: $2 kind: 'AzureCLI' properties: { azCliVersion: $3 retentionInterval: $4 } $0 }"); }, c => { c.Label.Should().Be("required-properties-AzurePowerShell"); c.Detail.Should().Be("Required properties"); c.TextEdit?.NewText?.Should().BeEquivalentToIgnoringNewlines(@"{ name: $1 location: $2 kind: 'AzurePowerShell' properties: { azPowerShellVersion: $3 retentionInterval: $4 } $0 }"); }, c => { c.Label.Should().Be("if"); }, c => { c.Label.Should().Be("for"); }, c => { c.Label.Should().Be("for-indexed"); }, c => { c.Label.Should().Be("for-filtered"); }); }
public void GetPosition_EmptyLineStarts_ThrowsArgumentException() { Action sut = () => TextCoordinateConverter.GetPosition(new List <int>().AsReadOnly(), 10); sut.Should().Throw <ArgumentException>().WithMessage("*must not be empty*"); }