diff --git a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs index f2ee1cc0096..ec378f277aa 100644 --- a/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/CompletionTests.cs @@ -4128,19 +4128,9 @@ public async Task ModuleRegistryReferenceCompletions_GetCompletionsForFolderInsi [DataRow("using 'br/public:|", BicepSourceFileKind.ParamsFile)] public async Task ModuleRegistryReferenceCompletions_GetPathCompletions(string inputWithCursors, BicepSourceFileKind kind) { - var testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); - var (text, cursor) = ParserHelper.GetFileWithSingleCursor(inputWithCursors, '|'); - - var fileName = kind switch - { - BicepSourceFileKind.BicepFile => "main.bicep", - BicepSourceFileKind.ParamsFile => "main.bicepparam", - _ => throw new InvalidOperationException(), - }; - var mainBicepFilePath = FileHelper.SaveResultFile(TestContext, fileName, text, testOutputPath); - var mainUri = DocumentUri.FromFileSystemPath(mainBicepFilePath); - - FileHelper.SaveResultFile(TestContext, "groups.bicep", string.Empty, Path.Combine(testOutputPath, "br")); + var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; + var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(inputWithCursors, '|'); + var fileUri = new Uri($"file:///{Guid.NewGuid():D}/{TestContext.TestName}/main.{extension}"); var settingsProvider = StrictMock.Of(); settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); @@ -4151,11 +4141,10 @@ public async Task ModuleRegistryReferenceCompletions_GetPathCompletions(string i using var helper = await MultiFileLanguageServerHelper.StartLanguageServer( TestContext, services => services - .AddSingleton(publicRegistryModuleMetadataProvider.Object) - .AddSingleton(settingsProvider.Object) - .WithFileResolver(new FileResolver(new LocalFileSystem()))); + .AddSingleton(publicRegistryModuleMetadataProvider.Object) + .AddSingleton(settingsProvider.Object)); - var file = await new ServerRequestHelper(TestContext, helper).OpenFile(mainUri.ToUriEncoded(), text); + var file = await new ServerRequestHelper(TestContext, helper).OpenFile(fileUri, fileText); var completions = await file.RequestCompletion(cursor); completions.Count().Should().Be(2); @@ -4174,32 +4163,25 @@ public async Task ModuleRegistryReferenceCompletions_GetPathCompletions(string i [DataRow("using 'br:mcr.microsoft.com/bicep/app/dapr-containerapp:|", BicepSourceFileKind.ParamsFile)] public async Task ModuleRegistryReferenceCompletions_GetVersionCompletions(string inputWithCursors, BicepSourceFileKind kind) { - var testOutputPath = FileHelper.GetUniqueTestOutputPath(TestContext); - var (text, cursor) = ParserHelper.GetFileWithSingleCursor(inputWithCursors, '|'); - - var fileName = kind switch - { - BicepSourceFileKind.BicepFile => "main.bicep", - BicepSourceFileKind.ParamsFile => "main.bicepparam", - _ => throw new InvalidOperationException(), - }; - var mainBicepFilePath = FileHelper.SaveResultFile(TestContext, fileName, text, testOutputPath); - var mainUri = DocumentUri.FromFileSystemPath(mainBicepFilePath); + var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; + var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(inputWithCursors, '|'); + var fileUri = new Uri($"file:///{Guid.NewGuid():D}/{TestContext.TestName}/main.{extension}"); var settingsProvider = StrictMock.Of(); settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); var publicRegistryModuleMetadataProvider = StrictMock.Of(); + publicRegistryModuleMetadataProvider.Setup(x => x.GetModulesMetadata()).Returns([new("app/dapr-containerapp", "d1", "contoso.com/help1")]); + publicRegistryModuleMetadataProvider.Setup(x => x.GetModuleVersionsMetadata("app/dapr-containerapp")).Returns([new("1.0.2", "d1", "contoso.com/help1"), new("1.0.1", null, null)]); publicRegistryModuleMetadataProvider.Setup(x => x.GetModuleVersionsMetadata("app/dapr-containerapp")).Returns([new("1.0.2", "d1", "contoso.com/help1"), new("1.0.1", null, null)]); using var helper = await MultiFileLanguageServerHelper.StartLanguageServer( TestContext, services => services - .AddSingleton(publicRegistryModuleMetadataProvider.Object) - .AddSingleton(settingsProvider.Object) - .WithFileResolver(new FileResolver(new LocalFileSystem()))); + .AddSingleton(publicRegistryModuleMetadataProvider.Object) + .AddSingleton(settingsProvider.Object)); - var file = await new ServerRequestHelper(TestContext, helper).OpenFile(mainUri.ToUriEncoded(), text); + var file = await new ServerRequestHelper(TestContext, helper).OpenFile(fileUri, fileText); var completions = await file.RequestCompletion(cursor); completions.Count().Should().Be(2); @@ -4207,6 +4189,41 @@ public async Task ModuleRegistryReferenceCompletions_GetVersionCompletions(strin completions.Should().Contain(x => x.Label == "1.0.2" && x.SortText == "0000" && x.Kind == CompletionItemKind.Snippet && x.Detail == "d1" && x.Documentation!.MarkupContent!.Value == "[View Documentation](contoso.com/help1)"); } + [TestMethod] + [DataRow("module test 'br:mcr.microsoft.com/bicep/foo|'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br:mcr.microsoft.com/bicep/foo|", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:foo|'", BicepSourceFileKind.BicepFile)] + [DataRow("module test 'br/public:foo|", BicepSourceFileKind.BicepFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/foo|'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br:mcr.microsoft.com/bicep/foo|", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:foo|'", BicepSourceFileKind.ParamsFile)] + [DataRow("using 'br/public:foo|", BicepSourceFileKind.ParamsFile)] + public async Task Public_registry_completions_support_prefix_matching(string text, BicepSourceFileKind kind) + { + var extension = kind == BicepSourceFileKind.ParamsFile ? "bicepparam" : "bicep"; + var (fileText, cursor) = ParserHelper.GetFileWithSingleCursor(text, '|'); + var fileUri = new Uri($"file:///{Guid.NewGuid():D}/{TestContext.TestName}/main.{extension}"); + + var settingsProvider = StrictMock.Of(); + settingsProvider.Setup(x => x.GetSetting(LangServerConstants.GetAllAzureContainerRegistriesForCompletionsSetting)).Returns(false); + + var publicRegistryModuleMetadataProvider = StrictMock.Of(); + publicRegistryModuleMetadataProvider.Setup(x => x.GetModulesMetadata()).Returns([new("foo/bar", "d1", "contoso.com/help1"), new("food/bar", "d2", "contoso.com/help2"), new("bar/bar", "d2", "contoso.com/help2")]); + + using var helper = await MultiFileLanguageServerHelper.StartLanguageServer( + TestContext, + services => services + .AddSingleton(publicRegistryModuleMetadataProvider.Object) + .AddSingleton(settingsProvider.Object)); + + var file = await new ServerRequestHelper(TestContext, helper).OpenFile(fileUri, fileText); + var completions = await file.RequestCompletion(cursor); + + completions.Count().Should().Be(2); + completions.Should().Contain(x => x.Label == "foo/bar"); + completions.Should().Contain(x => x.Label == "food/bar"); + } + [DataTestMethod] [DataRow("var arr1 = [|]")] [DataRow("param arr array = [|]")] diff --git a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs index 38881c2883d..f09b127de6a 100644 --- a/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/ModuleReferenceCompletionProvider.cs @@ -297,22 +297,11 @@ private IEnumerable GetOciModulePathCompletions(BicepCompletionC return []; } - if (replacementText == "'br/public:'" || - replacementText == $"'br:{PublicMCRRegistry}/bicep/'" || - replacementText == "'br/public:" || - replacementText == $"'br:{PublicMCRRegistry}/bicep/") - { - return GetPublicModuleCompletions(replacementText, context, sourceFileUri); - } - else - { - List completions = new(); - - completions.AddRange(GetACRPartialPathCompletionsFromBicepConfig(replacementText, context, sourceFileUri)); - completions.AddRange(GetMCRPathCompletionFromBicepConfig(replacementText, context, sourceFileUri)); - - return completions; - } + return [ + .. GetPublicModuleCompletions(replacementText, context), + .. GetACRPartialPathCompletionsFromBicepConfig(replacementText, context, sourceFileUri), + .. GetMCRPathCompletionFromBicepConfig(replacementText, context, sourceFileUri), + ]; } @@ -503,16 +492,30 @@ private IEnumerable GetACRPartialPathCompletionsFromBicepConfig( // br/public: // or // br:mcr.microsoft.com/bicep/: - private IEnumerable GetPublicModuleCompletions(string replacementText, BicepCompletionContext context, Uri sourceUri) + private IEnumerable GetPublicModuleCompletions(string replacementText, BicepCompletionContext context) { - List completions = new(); + var (prefix, suffix) = replacementText switch { + {} x when x.StartsWith("'br/public:", StringComparison.Ordinal) => ("'br/public:", x["'br/public:".Length..].TrimEnd('\'')), + {} x when x.StartsWith($"'br:{PublicMCRRegistry}/bicep/", StringComparison.Ordinal) => ($"'br:{PublicMCRRegistry}/bicep/", x[$"'br:{PublicMCRRegistry}/bicep/".Length..].TrimEnd('\'')), + _ => (null, null), + }; - var replacementTextWithTrimmedEnd = replacementText.TrimEnd('\''); + if (prefix is null || suffix is null) + { + return []; + } + + List completions = new(); var modules = publicRegistryModuleMetadataProvider.GetModulesMetadata(); foreach (var (moduleName, description, documentationUri) in modules) { - var insertText = $"{replacementTextWithTrimmedEnd}{moduleName}:$0'"; + if (!moduleName.StartsWith(suffix, StringComparison.Ordinal)) + { + continue; + } + + var insertText = $"{prefix}{moduleName}:$0'"; var completionItem = CompletionItemBuilder.Create(CompletionItemKind.Snippet, moduleName) .WithSnippetEdit(context.ReplacementRange, insertText)