diff --git a/src/Bicep.Core.Samples/Files/Completions/declarations.json b/src/Bicep.Core.Samples/Files/Completions/declarations.json index 469172fa2df..3e06c318a64 100644 --- a/src/Bicep.Core.Samples/Files/Completions/declarations.json +++ b/src/Bicep.Core.Samples/Files/Completions/declarations.json @@ -203,6 +203,24 @@ "newText": "resource automationAccount 'Microsoft.Automation/automationAccounts@2019-06-01' = {\n name: ${1:'automationAccount'}\n location: resourceGroup().location\n properties: {\n sku: {\n name: '${2|Free,Basic|}'\n }\n }\n}\n" } }, + { + "label": "res-automation-module", + "kind": "snippet", + "detail": "Automation Module", + "documentation": { + "kind": "markdown", + "value": "```bicep\nresource automationAccount_automationVariable 'Microsoft.Automation/automationAccounts/modules@2015-10-31' = {\n name: '${automationAccount.name}/automationVariable'\n properties: {\n contentLink: {\n uri: 'https://content-url.nupkg'\n }\n }\n}\n\nresource automationAccount 'Microsoft.Automation/automationAccounts@2015-10-31' = {\n name: 'automationAccount'\n}\n```" + }, + "deprecated": false, + "preselect": false, + "sortText": "2_res-automation-module", + "insertTextFormat": "snippet", + "insertTextMode": "adjustIndentation", + "textEdit": { + "range": {}, + "newText": "resource automationAccount_automationVariable 'Microsoft.Automation/automationAccounts/modules@2015-10-31' = {\n name: '${automationAccount.name}/${2:automationVariable}'\n properties: {\n contentLink: {\n uri: '${3:https://content-url.nupkg}'\n }\n }\n}\n\nresource automationAccount 'Microsoft.Automation/automationAccounts@2015-10-31' = {\n name: '${1:automationAccount}'\n}" + } + }, { "label": "res-availability-set", "kind": "snippet", diff --git a/src/Bicep.Core.Samples/Files/InvalidResources_CRLF/Completions/objectPlusFor.json b/src/Bicep.Core.Samples/Files/InvalidResources_CRLF/Completions/objectPlusFor.json index baf740ae54a..7a35a6c76fc 100644 --- a/src/Bicep.Core.Samples/Files/InvalidResources_CRLF/Completions/objectPlusFor.json +++ b/src/Bicep.Core.Samples/Files/InvalidResources_CRLF/Completions/objectPlusFor.json @@ -68,17 +68,21 @@ } }, { - "label": "{}", - "kind": "value", - "detail": "{}", + "label": "res-dns-zone", + "kind": "snippet", + "detail": "DNS Zone", + "documentation": { + "kind": "markdown", + "value": "```bicep\n{\n name: 'dnsZone'\n location: 'global'\n}\n```" + }, "deprecated": false, "preselect": true, - "sortText": "1_{}", + "sortText": "2_res-dns-zone", "insertTextFormat": "snippet", "insertTextMode": "adjustIndentation", "textEdit": { "range": {}, - "newText": "{\n\t$0\n}" + "newText": "{\n name: '${1:dnsZone}'\n location: 'global'\n}" } } ] \ No newline at end of file diff --git a/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/diagnostics.json b/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/diagnostics.json new file mode 100644 index 00000000000..0637a088a01 --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/diagnostics.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/main.bicep b/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/main.bicep new file mode 100644 index 00000000000..42d1fb58d50 --- /dev/null +++ b/src/Bicep.LangServer.IntegrationTests/Completions/SnippetTemplates/res-automation-module/main.bicep @@ -0,0 +1,5 @@ +// $1 = testAutomationModule +// $2 = testAutomationVariable +// $3 = https://test-content-url.nupkg + +// Insert snippet here diff --git a/src/Bicep.LangServer.UnitTests/Snippets/SnippetTests.cs b/src/Bicep.LangServer.UnitTests/Snippets/SnippetTests.cs index cc2751104bb..f42f95e22ec 100644 --- a/src/Bicep.LangServer.UnitTests/Snippets/SnippetTests.cs +++ b/src/Bicep.LangServer.UnitTests/Snippets/SnippetTests.cs @@ -119,6 +119,25 @@ public void SinglePropertySnippetShouldParseCorrectly() snippet.FormatDocumentation().Should().Be("name: ''"); } + [TestMethod] + public void SnippetPlaceholderTextWithUrlShouldParseCorrectly() + { + string text = @"var testIdentifier = '${1:http://test-content-url.nupkg}'"; + + var snippet = new Snippet(text); + snippet.Text.Should().Be(text); + + snippet.Placeholders.Should().SatisfyRespectively( + x => + { + x.Index.Should().Be(1); + x.Name.Should().Be("http://test-content-url.nupkg"); + x.Span.ToString().Should().Be("[22:56]"); + }); + + snippet.FormatDocumentation().Should().Be("var testIdentifier = 'http://test-content-url.nupkg'"); + } + [TestMethod] public void SnippetPlaceholderTextWithMultipleChoicesShouldReturnFirstOneByDefault() { diff --git a/src/Bicep.LangServer.UnitTests/Snippets/SnippetsProviderTests.cs b/src/Bicep.LangServer.UnitTests/Snippets/SnippetsProviderTests.cs deleted file mode 100644 index db1c58f8e6e..00000000000 --- a/src/Bicep.LangServer.UnitTests/Snippets/SnippetsProviderTests.cs +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; -using System.Linq; -using Bicep.LanguageServer.Completions; -using Bicep.LanguageServer.Snippets; -using Microsoft.VisualStudio.TestTools.UnitTesting; - -namespace Bicep.LangServer.UnitTests.Snippets -{ - [TestClass] - public class SnippetsProviderTests - { - [TestMethod] - public void GetDescriptionAndText_WithEmptyInput_ReturnsEmptyDescriptionAndText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - (string description, string text) = snippetsProvider.GetDescriptionAndText(string.Empty); - - Assert.IsTrue(description.Equals(string.Empty)); - Assert.IsTrue(text.Equals(string.Empty)); - } - - [TestMethod] - public void GetDescriptionAndText_WithNullInput_ReturnsEmptyDescriptionAndText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - (string description, string text) = snippetsProvider.GetDescriptionAndText(null); - - Assert.IsTrue(description.Equals(string.Empty)); - Assert.IsTrue(text.Equals(string.Empty)); - } - - [TestMethod] - public void GetDescriptionAndText_WithOnlyWhitespaceInput_ReturnsEmptyDescriptionAndText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - (string description, string text) = snippetsProvider.GetDescriptionAndText(" "); - - Assert.IsTrue(description.Equals(string.Empty)); - Assert.IsTrue(text.Equals(string.Empty)); - } - - [TestMethod] - public void GetDescriptionAndText_WithValidInput_ReturnsDescriptionAndText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - string template = @"// DNS Zone -resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { - name: '${1:dnsZone}' - location: 'global' - tags: { - displayName: '${1:dnsZone}' - } -}"; - - (string description, string text) = snippetsProvider.GetDescriptionAndText(template); - - string expectedText = @"resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { - name: '${1:dnsZone}' - location: 'global' - tags: { - displayName: '${1:dnsZone}' - } -}"; - - Assert.AreEqual("DNS Zone", description); - Assert.AreEqual(expectedText, text); - } - - [TestMethod] - public void GetDescriptionAndText_WithMissingCommentInInput_ReturnsEmptyDescriptionAndValidText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - string template = @"resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { - name: '${1:dnsZone}' - location: 'global' - tags: { - displayName: '${1:dnsZone}' - } -}"; - - (string description, string text) = snippetsProvider.GetDescriptionAndText(template); - - string expectedText = @"resource dnsZone 'Microsoft.Network/dnsZones@2018-05-01' = { - name: '${1:dnsZone}' - location: 'global' - tags: { - displayName: '${1:dnsZone}' - } -}"; - - Assert.IsTrue(description.Equals(string.Empty)); - Assert.AreEqual(expectedText, text); - } - - [TestMethod] - public void GetDescriptionAndText_WithCommentAndMissingDeclarations_ReturnsEmptyDescriptionAndText() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - string template = @"// DNS Zone"; - - (string description, string text) = snippetsProvider.GetDescriptionAndText(template); - - Assert.IsTrue(description.Equals(string.Empty)); - Assert.IsTrue(text.Equals(string.Empty)); - } - - [TestMethod] - public void CompletionPriorityOfResourceSnippets_ShouldBeHigh() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - IEnumerable snippets = snippetsProvider.GetTopLevelNamedDeclarationSnippets() - .Where(x => x.Prefix.StartsWith("resource")); - - foreach (Snippet snippet in snippets) - { - Assert.AreEqual(CompletionPriority.High, snippet.CompletionPriority); - } - } - - - [TestMethod] - public void CompletionPriorityOfNonResourceSnippets_ShouldBeMedium() - { - SnippetsProvider snippetsProvider = new SnippetsProvider(); - - IEnumerable snippets = snippetsProvider.GetTopLevelNamedDeclarationSnippets() - .Where(x => !x.Prefix.StartsWith("resource")); - - foreach (Snippet snippet in snippets) - { - Assert.AreEqual(CompletionPriority.Medium, snippet.CompletionPriority); - } - } - } -} diff --git a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index 249436621b1..9a3d156f25f 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -58,7 +58,8 @@ public IEnumerable GetFilteredCompletions(Compilation compilatio .Concat(GetResourceTypeCompletions(model, context)) .Concat(GetResourceTypeFollowerCompletions(context)) .Concat(GetModulePathCompletions(model, context)) - .Concat(GetResourceOrModuleBodyCompletions(context)) + .Concat(GetModuleBodyCompletions(context)) + .Concat(GetResourceBodyCompletions(context)) .Concat(GetParameterDefaultValueCompletions(model, context)) .Concat(GetVariableValueCompletions(context)) .Concat(GetOutputValueCompletions(model, context)) @@ -326,7 +327,7 @@ private IEnumerable GetParameterDefaultValueCompletions(Semantic private IEnumerable GetVariableValueCompletions(BicepCompletionContext context) { - if(!context.Kind.HasFlag(BicepCompletionContextKind.VariableValue)) + if (!context.Kind.HasFlag(BicepCompletionContextKind.VariableValue)) { return Enumerable.Empty(); } @@ -347,9 +348,51 @@ private IEnumerable GetOutputValueCompletions(SemanticModel mode return GetValueCompletionsForType(declaredType, context.ReplacementRange, model, context, loopsAllowed: true); } - private IEnumerable GetResourceOrModuleBodyCompletions(BicepCompletionContext context) + private IEnumerable GetResourceBodyCompletions(BicepCompletionContext context) { - if (context.Kind.HasFlag(BicepCompletionContextKind.ResourceBody) || context.Kind.HasFlag(BicepCompletionContextKind.ModuleBody)) + if (context.Kind.HasFlag(BicepCompletionContextKind.ResourceBody)) + { + StringSyntax? stringSyntax = (context.EnclosingDeclaration as ResourceDeclarationSyntax)?.TypeString; + + if (stringSyntax is not null) + { + string type = stringSyntax.StringTokens[0].Text; + + Snippet? snippet = SnippetsProvider.GetResourceCompletionSnippet(type); + + if (snippet is null) + { + yield return CompletionItemBuilder.Create(CompletionItemKind.Value) + .WithLabel("{}") + .WithSnippetEdit(context.ReplacementRange, "{\n\t$0\n}") + .WithDetail("{}") + .Preselect() + .WithSortText(GetSortText("{}", CompletionPriority.High)); + } + else + { + yield return CreateContextualSnippetCompletion(snippet.Prefix, + snippet.Detail, + snippet.Text, + context.ReplacementRange, + snippet.CompletionPriority, + preselect: true); + } + } + + yield return CreateResourceOrModuleConditionCompletion(context.ReplacementRange); + + // loops are always allowed in a resource/module + foreach (var completion in CreateLoopCompletions(context.ReplacementRange, LanguageConstants.Object, filtersAllowed: true)) + { + yield return completion; + } + } + } + + private IEnumerable GetModuleBodyCompletions(BicepCompletionContext context) + { + if (context.Kind.HasFlag(BicepCompletionContextKind.ModuleBody)) { yield return CreateObjectBodyCompletion(context.ReplacementRange); @@ -742,7 +785,7 @@ private static IEnumerable CreateLoopCompletions(Range replaceme yield return CreateContextualSnippetCompletion(loopLabel, loopLabel, itemSnippet, replacementRange, CompletionPriority.High); yield return CreateContextualSnippetCompletion(indexedLabel, indexedLabel, indexedSnippet, replacementRange, CompletionPriority.High); - if(filtersAllowed && assignableToObject && !assignableToArray) + if (filtersAllowed && assignableToObject && !assignableToArray) { yield return CreateContextualSnippetCompletion(filteredLabel, filteredLabel, "[for (${2:item}, ${3:index}) in ${1:list}: if (${4:condition}) {\n\t$0\n}]", replacementRange, CompletionPriority.High); } @@ -864,13 +907,14 @@ private static CompletionItem CreateModulePathCompletion(string name, string pat /// /// Creates a completion with a contextual snippet. This will look like a snippet to the user. /// - private static CompletionItem CreateContextualSnippetCompletion(string label, string detail, string snippet, Range replacementRange, CompletionPriority priority = CompletionPriority.Medium) => + private static CompletionItem CreateContextualSnippetCompletion(string label, string detail, string snippet, Range replacementRange, CompletionPriority priority = CompletionPriority.Medium, bool preselect = false) => CompletionItemBuilder.Create(CompletionItemKind.Snippet) .WithLabel(label) .WithSnippetEdit(replacementRange, snippet) .WithDetail(detail) .WithDocumentation($"```bicep\n{new Snippet(snippet).FormatDocumentation()}\n```") - .WithSortText(GetSortText(label, priority)); + .WithSortText(GetSortText(label, priority)) + .Preselect(preselect); /// /// Creates a completion with a contextual snippet. This will look like a snippet to the user. diff --git a/src/Bicep.LangServer/Completions/CompletionItemBuilder.cs b/src/Bicep.LangServer/Completions/CompletionItemBuilder.cs index 3052aeb08c3..ad8a4ec5187 100644 --- a/src/Bicep.LangServer/Completions/CompletionItemBuilder.cs +++ b/src/Bicep.LangServer/Completions/CompletionItemBuilder.cs @@ -86,9 +86,6 @@ public static CompletionItem WithSnippetEdit(this CompletionItem item, Range ran return item; } - - - public static CompletionItem WithSortText(this CompletionItem item, string sortText) { item.SortText = sortText; @@ -103,7 +100,6 @@ public static CompletionItem Preselect(this CompletionItem item, bool preselect) return item; } - public static CompletionItem WithCommand(this CompletionItem item, Command command) { item.Command = command; diff --git a/src/Bicep.LangServer/Snippets/ISnippetsProvider.cs b/src/Bicep.LangServer/Snippets/ISnippetsProvider.cs deleted file mode 100644 index ec36f794d52..00000000000 --- a/src/Bicep.LangServer/Snippets/ISnippetsProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System.Collections.Generic; - -namespace Bicep.LanguageServer.Snippets -{ - public interface ISnippetsProvider - { - IEnumerable GetTopLevelNamedDeclarationSnippets(); - } -} diff --git a/src/Bicep.LangServer/Snippets/SnippetsProvider.cs b/src/Bicep.LangServer/Snippets/SnippetsProvider.cs deleted file mode 100644 index 2d874d1eb12..00000000000 --- a/src/Bicep.LangServer/Snippets/SnippetsProvider.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using Bicep.Core.Parsing; -using Bicep.Core.Syntax; -using Bicep.LanguageServer.Completions; - -namespace Bicep.LanguageServer.Snippets -{ - public class SnippetsProvider : ISnippetsProvider - { - private HashSet topLevelNamedDeclarationSnippets = new HashSet(); - - public SnippetsProvider() - { - Initialize(); - } - - private void Initialize() - { - string pathPrefix = "Snippets/Templates/"; - Assembly assembly = typeof(SnippetsProvider).Assembly; - IEnumerable manifestResourceNames = assembly.GetManifestResourceNames().Where(p => p.StartsWith(pathPrefix, StringComparison.Ordinal)); - - foreach (var manifestResourceName in manifestResourceNames) - { - Stream? stream = assembly.GetManifestResourceStream(manifestResourceName); - StreamReader streamReader = new StreamReader(stream ?? throw new ArgumentNullException("Stream is null"), Encoding.Default); - - (string description, string snippetText) = GetDescriptionAndText(streamReader.ReadToEnd()); - string prefix = Path.GetFileNameWithoutExtension(manifestResourceName); - CompletionPriority completionPriority = CompletionPriority.Medium; - - if (prefix.StartsWith("resource")) - { - completionPriority = CompletionPriority.High; - } - - Snippet snippet = new Snippet(snippetText, completionPriority, prefix, description); - - topLevelNamedDeclarationSnippets.Add(snippet); - } - } - - public (string, string) GetDescriptionAndText(string? template) - { - string description = string.Empty; - string text = string.Empty; - - if (!string.IsNullOrWhiteSpace(template)) - { - Parser parser = new Parser(template); - ProgramSyntax programSyntax = parser.Program(); - IEnumerable declarations = programSyntax.Declarations; - - if (declarations.Any() && declarations.First() is StatementSyntax statementSyntax) - { - text = template.Substring(statementSyntax.Span.Position); - - ImmutableArray children = programSyntax.Children; - - if (children.Length > 0 && - children[0] is Token firstToken && - firstToken is not null && - firstToken.LeadingTrivia[0] is SyntaxTrivia syntaxTrivia && - syntaxTrivia.Type is SyntaxTriviaType.SingleLineComment) - { - description = syntaxTrivia.Text.Substring("// ".Length); - } - } - } - - return (description, text); - } - - public IEnumerable GetTopLevelNamedDeclarationSnippets() => topLevelNamedDeclarationSnippets; - } -} diff --git a/src/Bicep.LangServer/Snippets/Templates/res-automation-module.bicep b/src/Bicep.LangServer/Snippets/Templates/res-automation-module.bicep new file mode 100644 index 00000000000..f789d329df6 --- /dev/null +++ b/src/Bicep.LangServer/Snippets/Templates/res-automation-module.bicep @@ -0,0 +1,13 @@ +// Automation Module +resource automationAccount_automationVariable 'Microsoft.Automation/automationAccounts/modules@2015-10-31' = { + name: '${automationAccount.name}/${2:automationVariable}' + properties: { + contentLink: { + uri: '${3:https://content-url.nupkg}' + } + } +} + +resource automationAccount 'Microsoft.Automation/automationAccounts@2015-10-31' = { + name: '${1:automationAccount}' +} \ No newline at end of file