diff --git a/.vscode/launch.json b/.vscode/launch.json index 8ad01ac358d..7fad751c624 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,23 @@ { - "version": "0.2.0", + "version": "2.0.0", "configurations": [ + { + "name": "Bicep Extension", + "type": "extensionHost", + "request": "launch", + "runtimeExecutable": "${execPath}", + "args": [ + "--enable-proposed-api", + "--extensionDevelopmentPath=${workspaceRoot}/src/vscode-bicep" + ], + "env": { + "bicepLanguageServerPath": "${workspaceRoot}/src/Bicep.LangServer/bin/Debug/netcoreapp3.1/Bicep.LangServer.dll" + }, + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": ["${workspaceRoot}/out/src/**/*.js"], + "preLaunchTask": "vscodeext-build" + }, { "name": "Bicep Web Demo", "type": "coreclr", @@ -17,7 +34,7 @@ }, }, { - "name": "Bicep.Cli", + "name": "Bicep Cli", "type": "coreclr", "request": "launch", "preLaunchTask": "build", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index afeee579e4d..8acd8e3db0f 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -13,6 +13,19 @@ ], "problemMatcher": "$msCompile" }, + { + "label": "vscodeext-build", + "command": "npm", + "args": ["run", "compile", "--loglevel", "silent"], + "type": "shell", + "problemMatcher": "$tsc-watch", + "options": { + "cwd": "${workspaceFolder}/src/vscode-bicep" + }, + "dependsOn": [ + "build" + ] + }, { "label": "watch", "command": "dotnet", diff --git a/src/Bicep.Core.IntegrationTests/PrintVisitor.cs b/src/Bicep.Core.IntegrationTests/PrintVisitor.cs index 991fa22715e..8eaddaa7d6a 100644 --- a/src/Bicep.Core.IntegrationTests/PrintVisitor.cs +++ b/src/Bicep.Core.IntegrationTests/PrintVisitor.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Text; using Bicep.Core.Parser; using Bicep.Core.Syntax; @@ -15,9 +16,17 @@ public PrintVisitor(StringBuilder buffer) public override void VisitToken(Token token) { - buffer.Append(token.LeadingTrivia); + WriteTrivia(token.LeadingTrivia); buffer.Append(token.Text); - buffer.Append(token.TrailingTrivia); + WriteTrivia(token.TrailingTrivia); + } + + private void WriteTrivia(IEnumerable triviaList) + { + foreach (var trivia in triviaList) + { + buffer.Append(trivia.Text); + } } } } \ No newline at end of file diff --git a/src/Bicep.Core.Samples/Bicep.Core.Samples.csproj b/src/Bicep.Core.Samples/Bicep.Core.Samples.csproj index d90fa7e1523..69fedf899bb 100644 --- a/src/Bicep.Core.Samples/Bicep.Core.Samples.csproj +++ b/src/Bicep.Core.Samples/Bicep.Core.Samples.csproj @@ -90,6 +90,7 @@ + diff --git a/src/Bicep.Core.Samples/DataSet.cs b/src/Bicep.Core.Samples/DataSet.cs index f0112e0b022..0f7ecc225be 100644 --- a/src/Bicep.Core.Samples/DataSet.cs +++ b/src/Bicep.Core.Samples/DataSet.cs @@ -57,7 +57,7 @@ private Lazy CreateRequired(string fileName) private string ReadDataSetFile(string fileName) => ReadFile($"{Prefix}{this.Name}.{fileName}"); - private static string ReadFile(string streamName) + public static string ReadFile(string streamName) { using Stream? stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(streamName); stream.Should().NotBeNull($"because stream '{streamName}' should exist"); diff --git a/src/Bicep.Core.Samples/SnippetTests.cs b/src/Bicep.Core.Samples/SnippetTests.cs new file mode 100644 index 00000000000..895d7bea006 --- /dev/null +++ b/src/Bicep.Core.Samples/SnippetTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text.RegularExpressions; +using Bicep.Core.Parser; +using Bicep.Core.Syntax; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json; + +namespace Bicep.Core.Samples +{ + [TestClass] + public class SnippetTests + { + public class SnippetModel + { + public string? Prefix { get; set; } + + public IEnumerable? Body { get; set; } + + public static string GetDisplayName(MethodInfo info, object[] data) => ((SnippetModel)data[0]).Prefix!; + } + + private static IDictionary> SnippetValidations = new Dictionary> + { + ["resource"] = body => ValidateSnippet(body, "myResource", "myProvider", "myType", "2020-01-01", "name: 'myResource'"), + ["variable"] = body => ValidateSnippet(body, "myVariable", "'stringVal'"), + ["parameter"] = body => ValidateSnippet(body, "myParam", "string"), + ["output"] = body => ValidateSnippet(body, "myOutput", "string", "'stringVal'"), + }; + + private static IEnumerable GetSnippets() + { + var data = JsonConvert.DeserializeObject>(DataSet.ReadFile("vscode-bicep.snippets.bicep.json")); + + return data.Values.Select(snippet => new object[] { snippet }); + } + + [DataTestMethod] + [DynamicData(nameof(GetSnippets), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(SnippetModel), DynamicDataDisplayName = nameof(SnippetModel.GetDisplayName))] + public void SnippetIsValid(SnippetModel snippet) + { + var snippetBody = string.Join('\n', snippet.Body!); + var prefix = snippet.Prefix!; + + SnippetValidations.Should().ContainKey(prefix, "validation has not been defined for this snippet"); + SnippetValidations[prefix].Invoke(snippetBody); + } + + private static void ValidateSnippet(string body, params string[] replacements) + { + var holes = new Dictionary(); + var currentMatch = Regex.Match(body, @"\$({(?\d+):\w+}|(?\d+))"); + while (currentMatch.Success) + { + var index = int.Parse(currentMatch.Groups["index"].Value); + + holes.Should().NotContainKey(index, "there should only be one entry per index"); + holes[index] = currentMatch; + + currentMatch = currentMatch.NextMatch(); + } + + holes.Should().HaveCount(replacements.Length, "the number of replacements should match the number of holes"); + if (holes.ContainsKey(0)) + { + holes.Should().HaveCount(holes.Keys.Max() + 1, "there should be a consecutive range of numbered holes"); + holes.Keys.Min().Should().Be(0, "the numbered holes should start at 0"); + } + else if (holes.Any()) + { + holes.Should().HaveCount(holes.Keys.Max(), "there should be a consecutive range of numbered holes"); + holes.Keys.Min().Should().Be(1, "the numbered holes should start at 1"); + } + + var orderedKeys = holes.Keys.OrderBy(i => i > 0 ? i : int.MaxValue).ToArray(); // VSCode puts $0 (if present) at the end, hence the strange ordering. + var replacementPairs = orderedKeys.Select((holeIndex, i) => (holes[holeIndex], replacements[i])); + + // replace backwards so we don't have to recompute the index each iteration + foreach (var (match, replacement) in replacementPairs.OrderByDescending(t => t.Item1.Index)) + { + body = body.Substring(0, match.Index) + replacement + body.Substring(match.Index + match.Length); + } + + var program = SyntaxFactory.CreateFromText(body + "\n"); + program.GetParseDiagnostics().Should().BeEmpty($"compilation failed: {body}"); + } + } +} diff --git a/src/Bicep.Core.UnitTests/Parser/LexerTests.cs b/src/Bicep.Core.UnitTests/Parser/LexerTests.cs index d177cc53d09..f1ed23e42e3 100644 --- a/src/Bicep.Core.UnitTests/Parser/LexerTests.cs +++ b/src/Bicep.Core.UnitTests/Parser/LexerTests.cs @@ -2,6 +2,7 @@ using System.Collections.Immutable; using System.Linq; using Bicep.Core.Parser; +using Bicep.Core.Syntax; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -22,7 +23,7 @@ public class LexerTests [DataRow("'First line\\nSecond\\ttabbed\\tline'", "First line\nSecond\ttabbed\tline")] public void GetStringValue_ValidStringLiteralToken_ShouldCalculateValueCorrectly(string literalText, string expectedValue) { - var token = new Token(TokenType.String, new TextSpan(0, literalText.Length), literalText, string.Empty, string.Empty); + var token = new Token(TokenType.String, new TextSpan(0, literalText.Length), literalText, Enumerable.Empty(), Enumerable.Empty()); var actual = Lexer.GetStringValue(token); @@ -32,7 +33,7 @@ public void GetStringValue_ValidStringLiteralToken_ShouldCalculateValueCorrectly [TestMethod] public void GetStringValue_WrongTokenType_ShouldThrow() { - var token = new Token(TokenType.Number, new TextSpan(0, 2), "12", string.Empty, string.Empty); + var token = new Token(TokenType.Number, new TextSpan(0, 2), "12", Enumerable.Empty(), Enumerable.Empty()); Action wrongType = () => Lexer.GetStringValue(token); wrongType.Should().Throw().WithMessage("The specified token must be of type 'String' but is of type 'Number'."); @@ -45,7 +46,7 @@ public void GetStringValue_WrongTokenType_ShouldThrow() [DataRow(@"'test\!'", "String token contains an invalid escape character. Text = 'test\\!'")] public void GetStringValue_InvalidStringLiteralToken_ShouldThrow(string literalText, string expectedExceptionMessage) { - var token = new Token(TokenType.String, new TextSpan(0, literalText.Length), literalText, string.Empty, string.Empty); + var token = new Token(TokenType.String, new TextSpan(0, literalText.Length), literalText, Enumerable.Empty(), Enumerable.Empty()); Action invalidLiteral = () => Lexer.GetStringValue(token); invalidLiteral.Should().Throw().WithMessage(expectedExceptionMessage); diff --git a/src/Bicep.Core.UnitTests/Utils/TestSyntaxFactory.cs b/src/Bicep.Core.UnitTests/Utils/TestSyntaxFactory.cs index 49b93559c05..d0ccf70065e 100644 --- a/src/Bicep.Core.UnitTests/Utils/TestSyntaxFactory.cs +++ b/src/Bicep.Core.UnitTests/Utils/TestSyntaxFactory.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.Immutable; using System.Linq; using Bicep.Core.Parser; using Bicep.Core.Syntax; @@ -31,7 +32,7 @@ public static class TestSyntaxFactory public static ObjectPropertySyntax CreateProperty(IdentifierSyntax name, SyntaxBase value) => new ObjectPropertySyntax(name, CreateToken(TokenType.Colon), value, CreateNewLines()); - private static Token CreateToken(TokenType type, string text = "") => new Token(type, new TextSpan(0, 0), text, String.Empty, String.Empty); + private static Token CreateToken(TokenType type, string text = "") => new Token(type, new TextSpan(0, 0), text, ImmutableArray.Create(), ImmutableArray.Create()); private static Token[] CreateNewLines() => new[] {CreateToken(TokenType.NewLine)}; } diff --git a/src/Bicep.Core.UnitTests/Utils/TokenWriter.cs b/src/Bicep.Core.UnitTests/Utils/TokenWriter.cs index 88ed4a04c62..62dac0d9d24 100644 --- a/src/Bicep.Core.UnitTests/Utils/TokenWriter.cs +++ b/src/Bicep.Core.UnitTests/Utils/TokenWriter.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Text; using Bicep.Core.Parser; +using Bicep.Core.Syntax; namespace Bicep.Core.UnitTests.Utils { @@ -23,9 +24,17 @@ public void WriteTokens(IEnumerable tokens) public void WriteToken(Token token) { - this.buffer.Append(token.LeadingTrivia); + WriteTrivia(token.LeadingTrivia); this.buffer.Append(token.Text); - this.buffer.Append(token.TrailingTrivia); + WriteTrivia(token.TrailingTrivia); + } + + private void WriteTrivia(IEnumerable triviaList) + { + foreach (var trivia in triviaList) + { + this.buffer.Append(trivia.Text); + } } } } diff --git a/src/Bicep.Core/Parser/Lexer.cs b/src/Bicep.Core/Parser/Lexer.cs index c93a5c5d3ef..bf9e99cef3f 100644 --- a/src/Bicep.Core/Parser/Lexer.cs +++ b/src/Bicep.Core/Parser/Lexer.cs @@ -5,6 +5,7 @@ using System.Text; using Bicep.Core.Diagnostics; using Bicep.Core.Extensions; +using Bicep.Core.Syntax; namespace Bicep.Core.Parser { @@ -169,42 +170,46 @@ public static string GetStringValue(Token stringToken) return buffer.ToString(); } - private void ScanTrailingTrivia() + private IEnumerable ScanTrailingTrivia() { - ScanWhitespace(); + if (IsWhiteSpace(textWindow.Peek())) + { + yield return ScanWhitespace(); + } if (textWindow.Peek() == '/' && textWindow.Peek(1) == '/') { - ScanSingleLineComment(); - return; + yield return ScanSingleLineComment(); + yield break; } if (textWindow.Peek() == '/' && textWindow.Peek(1) == '*') { - ScanMultiLineComment(); - return; + yield return ScanMultiLineComment(); + yield break; } } - private void ScanLeadingTrivia() + private IEnumerable ScanLeadingTrivia() { while (true) { if (IsWhiteSpace(textWindow.Peek())) { - ScanWhitespace(); + yield return ScanWhitespace(); } else if (textWindow.Peek() == '/' && textWindow.Peek(1) == '/') { - ScanSingleLineComment(); + yield return ScanSingleLineComment(); } else if (textWindow.Peek() == '/' && textWindow.Peek(1) == '*') { - ScanMultiLineComment(); + yield return ScanMultiLineComment(); + yield break; } else { - return; + yield break; } } } @@ -213,7 +218,8 @@ private void LexToken() { textWindow.Reset(); ScanLeadingTrivia(); - var leadingTrivia = textWindow.GetText(); + // important to force enum evaluation here via .ToImmutableArray()! + var leadingTrivia = ScanLeadingTrivia().ToImmutableArray(); textWindow.Reset(); var tokenType = ScanToken(); @@ -226,15 +232,17 @@ private void LexToken() } textWindow.Reset(); - ScanTrailingTrivia(); - var trailingTrivia = textWindow.GetText(); + // important to force enum evaluation here via .ToImmutableArray()! + var trailingTrivia = ScanTrailingTrivia().ToImmutableArray(); var token = new Token(tokenType, tokenSpan, tokenText, leadingTrivia, trailingTrivia); this.tokens.Add(token); } - private void ScanWhitespace() + private SyntaxTrivia ScanWhitespace() { + textWindow.Reset(); + while (!textWindow.IsAtEnd()) { var nextChar = textWindow.Peek(); @@ -243,15 +251,20 @@ private void ScanWhitespace() case ' ': case '\t': textWindow.Advance(); - break; - default: - return; + continue; } + + break; } + + return new SyntaxTrivia(SyntaxTriviaType.Whitespace, textWindow.GetSpan(), textWindow.GetText()); } - private void ScanSingleLineComment() + private SyntaxTrivia ScanSingleLineComment() { + textWindow.Reset(); + textWindow.Advance(2); + while (!textWindow.IsAtEnd()) { var nextChar = textWindow.Peek(); @@ -259,21 +272,26 @@ private void ScanSingleLineComment() // make sure we don't include the newline in the comment trivia if (IsNewLine(nextChar)) { - return; + break; } textWindow.Advance(); } + + return new SyntaxTrivia(SyntaxTriviaType.SingleLineComment, textWindow.GetSpan(), textWindow.GetText()); } - private void ScanMultiLineComment() + private SyntaxTrivia ScanMultiLineComment() { + textWindow.Reset(); + textWindow.Advance(2); + while (true) { if (textWindow.IsAtEnd()) { AddError(b => b.UnterminatedMultilineComment()); - return; + break; } var nextChar = textWindow.Peek(); @@ -287,7 +305,7 @@ private void ScanMultiLineComment() if (textWindow.IsAtEnd()) { AddError(b => b.UnterminatedMultilineComment()); - return; + break; } nextChar = textWindow.Peek(); @@ -295,9 +313,11 @@ private void ScanMultiLineComment() if (nextChar == '/') { - return; + break; } } + + return new SyntaxTrivia(SyntaxTriviaType.MultiLineComment, textWindow.GetSpan(), textWindow.GetText()); } private void ScanNewLine() diff --git a/src/Bicep.Core/Parser/Token.cs b/src/Bicep.Core/Parser/Token.cs index f7d83ce981c..5e16dc580fb 100644 --- a/src/Bicep.Core/Parser/Token.cs +++ b/src/Bicep.Core/Parser/Token.cs @@ -1,17 +1,20 @@ +using System.Collections.Generic; +using System.Collections.Immutable; using System.Diagnostics; +using Bicep.Core.Syntax; namespace Bicep.Core.Parser { [DebuggerDisplay("{Type} = {Text}")] public class Token : IPositionable { - public Token(TokenType type, TextSpan span, string text, string leadingTrivia, string trailingTrivia) + public Token(TokenType type, TextSpan span, string text, IEnumerable leadingTrivia, IEnumerable trailingTrivia) { Type = type; Span = span; Text = text; - LeadingTrivia = leadingTrivia; - TrailingTrivia = trailingTrivia; + LeadingTrivia = leadingTrivia.ToImmutableArray(); + TrailingTrivia = trailingTrivia.ToImmutableArray(); } public TokenType Type { get; } @@ -20,8 +23,8 @@ public Token(TokenType type, TextSpan span, string text, string leadingTrivia, s public string Text { get; } - public string LeadingTrivia { get; } + public ImmutableArray LeadingTrivia { get; } - public string TrailingTrivia { get; } + public ImmutableArray TrailingTrivia { get; } } } \ No newline at end of file diff --git a/src/Bicep.Core/Syntax/SyntaxTrivia.cs b/src/Bicep.Core/Syntax/SyntaxTrivia.cs new file mode 100644 index 00000000000..18497abea81 --- /dev/null +++ b/src/Bicep.Core/Syntax/SyntaxTrivia.cs @@ -0,0 +1,20 @@ +using Bicep.Core.Parser; + +namespace Bicep.Core.Syntax +{ + public class SyntaxTrivia : IPositionable + { + public SyntaxTrivia(SyntaxTriviaType type, TextSpan span, string text) + { + Type = type; + Span = span; + Text = text; + } + + public SyntaxTriviaType Type { get; } + + public TextSpan Span { get; } + + public string Text { get; } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Syntax/SyntaxTriviaType.cs b/src/Bicep.Core/Syntax/SyntaxTriviaType.cs new file mode 100644 index 00000000000..eafb3958155 --- /dev/null +++ b/src/Bicep.Core/Syntax/SyntaxTriviaType.cs @@ -0,0 +1,9 @@ +namespace Bicep.Core.Syntax +{ + public enum SyntaxTriviaType + { + SingleLineComment, + MultiLineComment, + Whitespace, + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Syntax/SyntaxVisitor.cs b/src/Bicep.Core/Syntax/SyntaxVisitor.cs index aa3c8571d32..69e2326725c 100644 --- a/src/Bicep.Core/Syntax/SyntaxVisitor.cs +++ b/src/Bicep.Core/Syntax/SyntaxVisitor.cs @@ -12,6 +12,20 @@ public virtual void Visit(SyntaxBase node) public virtual void VisitToken(Token token) { + foreach (var syntaxTrivia in token.LeadingTrivia) + { + this.VisitSyntaxTrivia(syntaxTrivia); + } + + foreach (var syntaxTrivia in token.TrailingTrivia) + { + this.VisitSyntaxTrivia(syntaxTrivia); + } + } + + public virtual void VisitSyntaxTrivia(SyntaxTrivia syntaxTrivia) + { + } public virtual void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) diff --git a/src/Bicep.LangServer/BicepDocumentSymbolHandler.cs b/src/Bicep.LangServer/BicepDocumentSymbolHandler.cs index 60bf32ed2fc..6d0e6070957 100644 --- a/src/Bicep.LangServer/BicepDocumentSymbolHandler.cs +++ b/src/Bicep.LangServer/BicepDocumentSymbolHandler.cs @@ -8,6 +8,7 @@ using Bicep.LanguageServer.Extensions; using Microsoft.Extensions.Logging; using OmniSharp.Extensions.LanguageServer.Protocol.Document; +using OmniSharp.Extensions.LanguageServer.Protocol.Document.Proposals; using OmniSharp.Extensions.LanguageServer.Protocol.Models; using SymbolKind = OmniSharp.Extensions.LanguageServer.Protocol.Models.SymbolKind; diff --git a/src/Bicep.LangServer/BicepSemanticTokensHandler.cs b/src/Bicep.LangServer/BicepSemanticTokensHandler.cs new file mode 100644 index 00000000000..1e49bb141ca --- /dev/null +++ b/src/Bicep.LangServer/BicepSemanticTokensHandler.cs @@ -0,0 +1,57 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Bicep.LanguageServer.CompilationManager; +using Microsoft.Extensions.Logging; +using OmniSharp.Extensions.LanguageServer.Protocol.Client.Capabilities; +using OmniSharp.Extensions.LanguageServer.Protocol.Document.Proposals; +using OmniSharp.Extensions.LanguageServer.Protocol.Models; +using OmniSharp.Extensions.LanguageServer.Protocol.Models.Proposals; + +namespace Bicep.LanguageServer +{ + [Obsolete] // proposed LSP feature must be marked 'obsolete' to access + public class BicepSemanticTokensHandler : SemanticTokensHandlerBase + { + private readonly ILogger logger; + private readonly ICompilationManager compilationManager; + + public BicepSemanticTokensHandler(ILogger logger, ICompilationManager compilationManager) + : base(GetSemanticTokensRegistrationOptions()) + { + this.logger = logger; + this.compilationManager = compilationManager; + } + + protected override Task GetSemanticTokensDocument(ITextDocumentIdentifierParams @params, CancellationToken cancellationToken) + { + return Task.FromResult(new SemanticTokensDocument(GetRegistrationOptions().Legend)); + } + + protected override Task Tokenize(SemanticTokensBuilder builder, ITextDocumentIdentifierParams identifier, CancellationToken cancellationToken) + { + var compilationContext = this.compilationManager.GetCompilation(identifier.TextDocument.Uri); + + if (compilationContext != null) + { + SemanticTokenVisitor.BuildSemanticTokens(builder, compilationContext); + } + + return Task.CompletedTask; + } + + private static SemanticTokensRegistrationOptions GetSemanticTokensRegistrationOptions() + { + return new SemanticTokensRegistrationOptions + { + DocumentSelector = DocumentSelector.ForLanguage(LanguageServerConstants.LanguageId), + Legend = new SemanticTokensLegend(), + Full = new SemanticTokensCapabilityRequestFull + { + Delta = true + }, + Range = true + }; + } + } +} \ No newline at end of file diff --git a/src/Bicep.LangServer/CompilationManager/CompilationContext.cs b/src/Bicep.LangServer/CompilationManager/CompilationContext.cs index a8c43aefe64..bac91edf82f 100644 --- a/src/Bicep.LangServer/CompilationManager/CompilationContext.cs +++ b/src/Bicep.LangServer/CompilationManager/CompilationContext.cs @@ -1,4 +1,5 @@ using System.Collections.Immutable; +using Bicep.Core.Parser; using Bicep.Core.SemanticModel; namespace Bicep.LanguageServer.CompilationManager diff --git a/src/Bicep.LangServer/Extensions/TextSpanExtensions.cs b/src/Bicep.LangServer/Extensions/TextSpanExtensions.cs index 009691cf681..d008a6e9a6a 100644 --- a/src/Bicep.LangServer/Extensions/TextSpanExtensions.cs +++ b/src/Bicep.LangServer/Extensions/TextSpanExtensions.cs @@ -1,4 +1,5 @@ -using System.Collections.Immutable; +using System.Collections.Generic; +using System.Collections.Immutable; using Bicep.Core.Parser; using Bicep.LanguageServer.Utils; using OmniSharp.Extensions.LanguageServer.Protocol.Models; @@ -15,5 +16,23 @@ public static class TextSpanExtensions Start = PositionHelper.GetPosition(lineStarts, span.Position), End = PositionHelper.GetPosition(lineStarts, span.Position + span.Length) }; + + public static IEnumerable ToRangeSpanningLines(this IPositionable positionable, ImmutableArray lineStarts) => positionable.Span.ToRangeSpanningLines(lineStarts); + + public static IEnumerable ToRangeSpanningLines(this TextSpan span, ImmutableArray lineStarts) + { + var start = PositionHelper.GetPosition(lineStarts, span.Position); + var end = PositionHelper.GetPosition(lineStarts, span.Position + span.Length); + + while (start.Line < end.Line) + { + var lineEnd = PositionHelper.GetPosition(lineStarts, lineStarts[start.Line + 1] - 1); + yield return new Range(start, lineEnd); + + start = new Position(start.Line + 1, 0); + } + + yield return new Range(start, end); + } } } diff --git a/src/Bicep.LangServer/Program.cs b/src/Bicep.LangServer/Program.cs index 3d9a0d427a7..7466f0de5c1 100644 --- a/src/Bicep.LangServer/Program.cs +++ b/src/Bicep.LangServer/Program.cs @@ -20,6 +20,9 @@ public static async Task Main(string[] args) .WithOutput(Console.OpenStandardOutput()) .WithHandler() .WithHandler() +#pragma warning disable 0612 // disable 'obsolete' warning for proposed LSP feature + .WithHandler() +#pragma warning restore 0612 .WithServices(RegisterServices)); server.TextDocument.PublishDiagnostics(new PublishDiagnosticsParams()); diff --git a/src/Bicep.LangServer/SemanticTokenVisitor.cs b/src/Bicep.LangServer/SemanticTokenVisitor.cs new file mode 100644 index 00000000000..2009d91a86d --- /dev/null +++ b/src/Bicep.LangServer/SemanticTokenVisitor.cs @@ -0,0 +1,219 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bicep.Core.Parser; +using Bicep.Core.Syntax; +using Bicep.LanguageServer.CompilationManager; +using Bicep.LanguageServer.Extensions; +using OmniSharp.Extensions.LanguageServer.Protocol.Document.Proposals; +using OmniSharp.Extensions.LanguageServer.Protocol.Models.Proposals; + +namespace Bicep.LanguageServer +{ + [Obsolete] // proposed LSP feature must be marked 'obsolete' to access + public class SemanticTokenVisitor : SyntaxVisitor + { + private readonly List<(IPositionable positionable, SemanticTokenType tokenType)> tokens; + + private SemanticTokenVisitor() + { + this.tokens = new List<(IPositionable, SemanticTokenType)>(); + } + + public static void BuildSemanticTokens(SemanticTokensBuilder builder, CompilationContext compilationContext) + { + var visitor = new SemanticTokenVisitor(); + + visitor.Visit(compilationContext.Compilation.ProgramSyntax); + + // the builder is fussy about ordering. tokens are visited out of order, we need to call build after visiting everything + foreach (var (positionable, tokenType) in visitor.tokens.OrderBy(t => t.positionable.Span.Position)) + { + var tokenRanges = positionable.ToRangeSpanningLines(compilationContext.LineStarts); + foreach (var tokenRange in tokenRanges) + { + builder.Push(tokenRange.Start.Line, tokenRange.Start.Character, tokenRange.End.Character - tokenRange.Start.Character, tokenType as SemanticTokenType?); + } + } + } + + private void AddTokenType(IPositionable positionable, SemanticTokenType tokenType) + { + tokens.Add((positionable, tokenType)); + } + + public override void VisitArrayAccessSyntax(ArrayAccessSyntax syntax) + { + base.VisitArrayAccessSyntax(syntax); + } + + public override void VisitArrayItemSyntax(ArrayItemSyntax syntax) + { + base.VisitArrayItemSyntax(syntax); + } + + public override void VisitArraySyntax(ArraySyntax syntax) + { + base.VisitArraySyntax(syntax); + } + + public override void VisitBinaryOperationSyntax(BinaryOperationSyntax syntax) + { + AddTokenType(syntax.OperatorToken, SemanticTokenType.Operator); + base.VisitBinaryOperationSyntax(syntax); + } + + public override void VisitBooleanLiteralSyntax(BooleanLiteralSyntax syntax) + { + AddTokenType(syntax.Literal, SemanticTokenType.Number); + base.VisitBooleanLiteralSyntax(syntax); + } + + public override void VisitFunctionArgumentSyntax(FunctionArgumentSyntax syntax) + { + base.VisitFunctionArgumentSyntax(syntax); + } + + public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax) + { + AddTokenType(syntax.FunctionName, SemanticTokenType.Function); + base.VisitFunctionCallSyntax(syntax); + } + + public override void VisitIdentifierSyntax(IdentifierSyntax syntax) + { + base.VisitIdentifierSyntax(syntax); + } + + public override void VisitNoOpDeclarationSyntax(NoOpDeclarationSyntax syntax) + { + base.VisitNoOpDeclarationSyntax(syntax); + } + + public override void VisitNullLiteralSyntax(NullLiteralSyntax syntax) + { + AddTokenType(syntax.NullKeyword, SemanticTokenType.Number); + base.VisitNullLiteralSyntax(syntax); + } + + public override void VisitNumericLiteralSyntax(NumericLiteralSyntax syntax) + { + AddTokenType(syntax.Literal, SemanticTokenType.Number); + base.VisitNumericLiteralSyntax(syntax); + } + + public override void VisitObjectPropertySyntax(ObjectPropertySyntax syntax) + { + AddTokenType(syntax.Identifier, SemanticTokenType.Member); + base.VisitObjectPropertySyntax(syntax); + } + + public override void VisitObjectSyntax(ObjectSyntax syntax) + { + base.VisitObjectSyntax(syntax); + } + + public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax) + { + AddTokenType(syntax.OutputKeyword, SemanticTokenType.Keyword); + AddTokenType(syntax.Name, SemanticTokenType.Variable); + base.VisitOutputDeclarationSyntax(syntax); + } + + public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) + { + AddTokenType(syntax.ParameterKeyword, SemanticTokenType.Keyword); + AddTokenType(syntax.Name, SemanticTokenType.Variable); + AddTokenType(syntax.Type, SemanticTokenType.Type); + base.VisitParameterDeclarationSyntax(syntax); + } + + public override void VisitParameterDefaultValueSyntax(ParameterDefaultValueSyntax syntax) + { + AddTokenType(syntax.DefaultKeyword, SemanticTokenType.Keyword); + base.VisitParameterDefaultValueSyntax(syntax); + } + + public override void VisitParenthesizedExpressionSyntax(ParenthesizedExpressionSyntax syntax) + { + base.VisitParenthesizedExpressionSyntax(syntax); + } + + public override void VisitProgramSyntax(ProgramSyntax syntax) + { + base.VisitProgramSyntax(syntax); + } + + public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) + { + AddTokenType(syntax.PropertyName, SemanticTokenType.Member); + base.VisitPropertyAccessSyntax(syntax); + } + + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) + { + AddTokenType(syntax.ResourceKeyword, SemanticTokenType.Keyword); + AddTokenType(syntax.Name, SemanticTokenType.Variable); + base.VisitResourceDeclarationSyntax(syntax); + } + + public override void VisitSkippedTokensTriviaSyntax(SkippedTokensTriviaSyntax syntax) + { + base.VisitSkippedTokensTriviaSyntax(syntax); + } + + public override void VisitStringSyntax(StringSyntax syntax) + { + AddTokenType(syntax.StringToken, SemanticTokenType.String); + base.VisitStringSyntax(syntax); + } + + public override void VisitTernaryOperationSyntax(TernaryOperationSyntax syntax) + { + AddTokenType(syntax.Colon, SemanticTokenType.Operator); + AddTokenType(syntax.Question, SemanticTokenType.Operator); + base.VisitTernaryOperationSyntax(syntax); + } + + public override void VisitToken(Token token) + { + base.VisitToken(token); + } + + public override void VisitSyntaxTrivia(SyntaxTrivia syntaxTrivia) + { + switch (syntaxTrivia.Type) + { + case SyntaxTriviaType.SingleLineComment: + case SyntaxTriviaType.MultiLineComment: + AddTokenType(syntaxTrivia, SemanticTokenType.Comment); + break; + } + } + + public override void VisitTypeSyntax(TypeSyntax syntax) + { + AddTokenType(syntax.Identifier, SemanticTokenType.Type); + base.VisitTypeSyntax(syntax); + } + + public override void VisitUnaryOperationSyntax(UnaryOperationSyntax syntax) + { + AddTokenType(syntax.OperatorToken, SemanticTokenType.Operator); + base.VisitUnaryOperationSyntax(syntax); + } + + public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) + { + AddTokenType(syntax.Name, SemanticTokenType.Variable); + base.VisitVariableAccessSyntax(syntax); + } + + public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax) + { + AddTokenType(syntax.VariableKeyword, SemanticTokenType.Keyword); + AddTokenType(syntax.Name, SemanticTokenType.Variable); + base.VisitVariableDeclarationSyntax(syntax); + } + } +} \ No newline at end of file diff --git a/src/vscode-bicep/bicep.grammar.json b/src/vscode-bicep/bicep.grammar.json deleted file mode 100644 index fcb3f3b9cb2..00000000000 --- a/src/vscode-bicep/bicep.grammar.json +++ /dev/null @@ -1,162 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", - "name": "Bicep", - "patterns": [ - { - "include": "#keywords" - }, - { - "include": "#strings" - }, - { - "include": "#properties" - }, - { - "include": "#functions" - }, - { - "include": "#comments" - } - ], - "repository": { - "keywords": { - "patterns": [ - { - "name": "keyword.control.bicep", - "match": "\\b(resource|variable|parameter|output)\\b" - }, - { - "name": "keyword.bundle.bicep", - "match": "\\b(azrm)\\b" - }, - { - "name": "keyword.type.bicep", - "match": "\\b(string|int|bool|object|array|null|true|false)\\b" - } - ] - }, - "strings": { - "name": "string.quoted.single.bicep", - "begin": "'", - "end": "'", - "patterns": [ - { - "name": "constant.character.escape.bicep", - "match": "(?x)\\\\(?:[\\\\nrt'$]|[0-9a-fA-F]{4})" - }, - { - "include": "#stringinterp" - }, - { - "match": "\\\\.", - "name": "invalid.illegal.unrecognized-string-escape.bicep" - } - ] - }, - "stringinterp": { - "begin": "\\$\\{", - "end": "\\}", - "beginCaptures": { - "0": { - "name": "punctuation.definition.template-expression.begin.bicep" - } - }, - "endCaptures": { - "0": { - "name": "punctuation.definition.template-expression.end.bicep" - } - }, - "name": "meta.embedded.block", - "patterns": [ - { - "include": "#functions" - }, - { - "include": "#identifiers" - }, - { - "name": "invalid.illegal.expected-expression.bicep", - "match": "[^\\s]" - } - ] - }, - "identifiers": { - "match": "\\b([a-zA-Z_][a-zA-Z0-9_]*)\\w*\\b", - "captures": { - "1": { - "name": "variable.other.identifier.bicep" - } - } - }, - "properties": { - "match": "\\b([a-zA-Z][a-zA-Z0-9]*)\\w*\\b\\s*:", - "captures": { - "1": { - "name": "support.type.property-name.bicep" - } - } - }, - "functions": { - "begin": "\\b([a-zA-Z][a-zA-Z0-9]*)\\w*\\(\\b", - "end": "\\)", - "beginCaptures": { - "1": { - "name": "support.function.bicep" - } - }, - "patterns": [ - { - "include": "#functions" - }, - { - "include": "#identifiers" - }, - { - "include": "#strings" - }, - { - "match": ",", - "name": "punctuation.separator.function.bicep" - }, - { - "name": "invalid.illegal.expected-expression.bicep", - "match": "[^\\s]" - } - ] - }, - "comments": { - "patterns": [ - { - "begin": "/\\*\\*(?!/)", - "captures": { - "0": { - "name": "punctuation.definition.comment.bicep" - } - }, - "end": "\\*/", - "name": "comment.block.documentation.bicep" - }, - { - "begin": "/\\*", - "captures": { - "0": { - "name": "punctuation.definition.comment.bicep" - } - }, - "end": "\\*/", - "name": "comment.block.bicep" - }, - { - "captures": { - "1": { - "name": "punctuation.definition.comment.bicep" - } - }, - "match": "(//).*$\\n?", - "name": "comment.line.double-slash.js" - } - ] - } - }, - "scopeName": "source.bicep" -} \ No newline at end of file diff --git a/src/vscode-bicep/icons/azure-resource-manager.png b/src/vscode-bicep/icons/azure-resource-manager.png new file mode 100644 index 00000000000..e23fc23e634 Binary files /dev/null and b/src/vscode-bicep/icons/azure-resource-manager.png differ diff --git a/src/vscode-bicep/package.json b/src/vscode-bicep/package.json index cb82047f310..3866d8f63c9 100644 --- a/src/vscode-bicep/package.json +++ b/src/vscode-bicep/package.json @@ -1,16 +1,38 @@ { "name": "vscode-bicep", - "description": "VSCode part of a language server", + "displayName": "Bicep", + "description": "Bicep language support for Visual Studio Code", "author": "Microsoft Corporation", "license": "MIT", "version": "0.0.1", - "publisher": "vscode", + "publisher": "Azure", + "icon": "icons/azure-resource-manager.png", + "preview": true, "engines": { "vscode": "^1.8.0" }, "categories": [ - "Other" + "Azure", + "Programming Languages" ], + "keywords": [ + "Bicep", + "Azure Resource Manager", + "ARM Template", + "Azure" + ], + "bugs": { + "url": "https://github.com/Azure/bicep/issues" + }, + "repository": { + "type": "git", + "url": "https://github.com/Azure/bicep.git" + }, + "galleryBanner": { + "color": "E7F1FA", + "theme": "light" + }, + "homepage": "https://github.com/Azure/bicep/blob/master/README.md", "activationEvents": [ "onLanguage:bicep" ], @@ -19,24 +41,8 @@ "contributes": { "configuration": { "type": "object", - "title": "Example configuration", - "properties": { - "bicep.maxNumberOfProblems": { - "type": "number", - "default": 100, - "description": "Controls the maximum number of problems produced by the server." - }, - "bicep.trace.server": { - "type": "string", - "enum": [ - "off", - "messages", - "verbose" - ], - "default": "verbose", - "description": "Traces the communication between VSCode and the bicep service." - } - } + "title": "Bicep Configuration", + "properties": {} }, "languages": [ { @@ -51,11 +57,10 @@ "configuration": "./language-configuration.json" } ], - "grammars": [ + "snippets": [ { "language": "bicep", - "scopeName": "source.bicep", - "path": "./bicep.grammar.json" + "path": "./snippets/bicep.json" } ] }, @@ -79,4 +84,4 @@ "extensionDependencies": [ "ms-dotnettools.vscode-dotnet-runtime" ] -} +} \ No newline at end of file diff --git a/src/vscode-bicep/snippets/bicep.json b/src/vscode-bicep/snippets/bicep.json new file mode 100644 index 00000000000..1a005af9480 --- /dev/null +++ b/src/vscode-bicep/snippets/bicep.json @@ -0,0 +1,32 @@ +{ + "Resource": { + "prefix": "resource", + "body": [ + "resource ${1:Identifier} 'Microsoft.${2:Provider}/${3:Type}@${4:Version}' = {", + " $0", + "}" + ], + "description": "Resource statement" + }, + "Variable": { + "prefix": "variable", + "body": [ + "variable ${1:Identifier} = $0" + ], + "description": "Variable statement" + }, + "Parameter": { + "prefix": "parameter", + "body": [ + "parameter ${1:Identifier} ${2:Type}" + ], + "description": "Parameter statement" + }, + "Output": { + "prefix": "output", + "body": [ + "output ${1:Identifier} ${2:Type} = $0" + ], + "description": "Output statement" + } +} \ No newline at end of file diff --git a/src/vscode-bicep/src/extension.ts b/src/vscode-bicep/src/extension.ts index bf50bcc2586..62cd855d64d 100644 --- a/src/vscode-bicep/src/extension.ts +++ b/src/vscode-bicep/src/extension.ts @@ -121,7 +121,7 @@ function startLanguageServer(context: ExtensionContext, languageServerPath: stri clientOptions ); client.registerProposedFeatures(); - client.trace = Trace.Verbose; + client.trace = Trace.Off; let disposable = client.start(); // Push the disposable to the context's subscriptions so that the