Skip to content

Commit

Permalink
VSCode extension improvements (#155)
Browse files Browse the repository at this point in the history
* Semantic token support

* Extension metadata and snippets

* Launch config for extension in main project

* (rough) support for semantic comment parsing

* Fix bug with deferred enumerable evaluation

* Address PR feedback
  • Loading branch information
anthony-c-martin committed Aug 8, 2020
1 parent 8a333ac commit fe6e870
Show file tree
Hide file tree
Showing 25 changed files with 612 additions and 229 deletions.
21 changes: 19 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -17,7 +34,7 @@
},
},
{
"name": "Bicep.Cli",
"name": "Bicep Cli",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
Expand Down
13 changes: 13 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 11 additions & 2 deletions src/Bicep.Core.IntegrationTests/PrintVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.Text;
using Bicep.Core.Parser;
using Bicep.Core.Syntax;
Expand All @@ -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<SyntaxTrivia> triviaList)
{
foreach (var trivia in triviaList)
{
buffer.Append(trivia.Text);
}
}
}
}
1 change: 1 addition & 0 deletions src/Bicep.Core.Samples/Bicep.Core.Samples.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@
<EmbeddedResource Include="Variables_LF\Compiled.json" />
<EmbeddedResource Include="Variables_LF\Errors.json" />
<EmbeddedResource Include="Variables_LF\Tokens.json" />
<EmbeddedResource Include="..\vscode-bicep\snippets\bicep.json" LogicalName="vscode-bicep.snippets.bicep.json" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/Bicep.Core.Samples/DataSet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ private Lazy<string> 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");
Expand Down
91 changes: 91 additions & 0 deletions src/Bicep.Core.Samples/SnippetTests.cs
Original file line number Diff line number Diff line change
@@ -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<string>? Body { get; set; }

public static string GetDisplayName(MethodInfo info, object[] data) => ((SnippetModel)data[0]).Prefix!;
}

private static IDictionary<string, Action<string>> SnippetValidations = new Dictionary<string, Action<string>>
{
["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<object[]> GetSnippets()
{
var data = JsonConvert.DeserializeObject<Dictionary<string, SnippetModel>>(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<int, Match>();
var currentMatch = Regex.Match(body, @"\$({(?<index>\d+):\w+}|(?<index>\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}");
}
}
}
7 changes: 4 additions & 3 deletions src/Bicep.Core.UnitTests/Parser/LexerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<SyntaxTrivia>(), Enumerable.Empty<SyntaxTrivia>());

var actual = Lexer.GetStringValue(token);

Expand All @@ -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<SyntaxTrivia>(), Enumerable.Empty<SyntaxTrivia>());

Action wrongType = () => Lexer.GetStringValue(token);
wrongType.Should().Throw<ArgumentException>().WithMessage("The specified token must be of type 'String' but is of type 'Number'.");
Expand All @@ -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<SyntaxTrivia>(), Enumerable.Empty<SyntaxTrivia>());

Action invalidLiteral = () => Lexer.GetStringValue(token);
invalidLiteral.Should().Throw<ArgumentException>().WithMessage(expectedExceptionMessage);
Expand Down
3 changes: 2 additions & 1 deletion src/Bicep.Core.UnitTests/Utils/TestSyntaxFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<SyntaxTrivia>(), ImmutableArray.Create<SyntaxTrivia>());

private static Token[] CreateNewLines() => new[] {CreateToken(TokenType.NewLine)};
}
Expand Down
13 changes: 11 additions & 2 deletions src/Bicep.Core.UnitTests/Utils/TokenWriter.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Text;
using Bicep.Core.Parser;
using Bicep.Core.Syntax;

namespace Bicep.Core.UnitTests.Utils
{
Expand All @@ -23,9 +24,17 @@ public void WriteTokens(IEnumerable<Token> 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<SyntaxTrivia> triviaList)
{
foreach (var trivia in triviaList)
{
this.buffer.Append(trivia.Text);
}
}
}
}
Loading

0 comments on commit fe6e870

Please sign in to comment.