diff --git a/src/LibraryManager.Vsix/Json/Completion/FileMappingRootCompletionProvider.cs b/src/LibraryManager.Vsix/Json/Completion/FileMappingRootCompletionProvider.cs new file mode 100644 index 00000000..5be8341c --- /dev/null +++ b/src/LibraryManager.Vsix/Json/Completion/FileMappingRootCompletionProvider.cs @@ -0,0 +1,111 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.ComponentModel.Composition; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.Imaging; +using Microsoft.VisualStudio.Utilities; +using Microsoft.Web.LibraryManager.Contracts; +using Microsoft.Web.LibraryManager.Vsix.Contracts; +using Microsoft.WebTools.Languages.Json.Editor.Completion; +using Microsoft.WebTools.Languages.Json.Parser.Nodes; + +namespace Microsoft.Web.LibraryManager.Vsix.Json.Completion +{ + [Export(typeof(IJsonCompletionListProvider))] + [Name(nameof(FileMappingRootCompletionProvider))] + internal class FileMappingRootCompletionProvider : BaseCompletionProvider + { + private readonly IDependenciesFactory _dependenciesFactory; + + [ImportingConstructor] + internal FileMappingRootCompletionProvider(IDependenciesFactory dependenciesFactory) + { + _dependenciesFactory = dependenciesFactory; + } + + public override JsonCompletionContextType ContextType => JsonCompletionContextType.PropertyValue; + + [SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Checked completion first")] + protected override IEnumerable GetEntries(JsonCompletionContext context) + { + MemberNode member = context.ContextNode.FindType(); + + // This provides completions for libraries/[n]/fileMappings/[m]/root + if (member == null || member.UnquotedNameText != ManifestConstants.Root) + yield break; + + MemberNode possibleFileMappingsNode = member.Parent.FindType(); + bool isInFileMapping = possibleFileMappingsNode?.UnquotedNameText == ManifestConstants.FileMappings; + if (!isInFileMapping) + yield break; + + ObjectNode parent = possibleFileMappingsNode.Parent as ObjectNode; + + if (!JsonHelpers.TryGetInstallationState(parent, out ILibraryInstallationState state)) + yield break; + + if (string.IsNullOrEmpty(state.Name)) + yield break; + + IDependencies dependencies = _dependenciesFactory.FromConfigFile(ConfigFilePath); + IProvider provider = dependencies.GetProvider(state.ProviderId); + ILibraryCatalog catalog = provider?.GetCatalog(); + + if (catalog is null) + { + yield break; + } + + Task task = catalog.GetLibraryAsync(state.Name, state.Version, CancellationToken.None); + + if (task.IsCompleted) + { + if (task.Result is ILibrary library) + { + foreach (JsonCompletionEntry item in GetRootCompletions(context, library)) + { + yield return item; + } + } + } + else + { + yield return new SimpleCompletionEntry(Resources.Text.Loading, string.Empty, KnownMonikers.Loading, context.Session); + _ = task.ContinueWith(async (t) => + { + await VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (!(t.Result is ILibrary library)) + return; + + if (!context.Session.IsDismissed) + { + IEnumerable completions = GetRootCompletions(context, library); + + UpdateListEntriesSync(context, completions); + } + }, TaskScheduler.Default); + } + } + + private IEnumerable GetRootCompletions(JsonCompletionContext context, ILibrary library) + { + HashSet libraryFolders = []; + foreach (string file in library.Files.Keys) + { + int sepIndex = file.LastIndexOf('/'); + if (sepIndex >= 0) + { + libraryFolders.Add(file.Substring(0, file.LastIndexOf('/'))); + } + } + + return libraryFolders.Select(folder => new SimpleCompletionEntry(folder, KnownMonikers.FolderClosed, context.Session)); + } + } +} diff --git a/src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs b/src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs index 25f1991f..4e6d8983 100644 --- a/src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs +++ b/src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.ComponentModel.Composition; using System.Diagnostics.CodeAnalysis; @@ -9,7 +10,6 @@ using System.Threading.Tasks; using System.Windows; using System.Windows.Data; -using System.Windows.Media; using Microsoft.VisualStudio.Imaging; using Microsoft.VisualStudio.Imaging.Interop; using Microsoft.VisualStudio.PlatformUI; @@ -19,6 +19,7 @@ using Microsoft.Web.LibraryManager.Vsix.Shared; using Microsoft.WebTools.Languages.Json.Editor.Completion; using Microsoft.WebTools.Languages.Json.Parser.Nodes; +using Microsoft.WebTools.Languages.Shared.Parser; using Microsoft.WebTools.Languages.Shared.Parser.Nodes; namespace Microsoft.Web.LibraryManager.Vsix.Json.Completion @@ -45,10 +46,20 @@ protected override IEnumerable GetEntries(JsonCompletionCon { MemberNode member = context.ContextNode.FindType(); + // We can show completions for "files". This could be libraries/[n]/files or + // libraries/[n]/fileMappings/[m]/files. if (member == null || member.UnquotedNameText != "files") yield break; - var parent = member.Parent as ObjectNode; + // If the current member is "files", then it is either: + // - a library "files" property + // - a fileMapping "files" property + MemberNode possibleFileMappingsNode = member.Parent.FindType(); + bool isFileMapping = possibleFileMappingsNode?.UnquotedNameText == "fileMappings"; + + ObjectNode parent = isFileMapping + ? possibleFileMappingsNode.Parent as ObjectNode + : member.Parent as ObjectNode; if (!JsonHelpers.TryGetInstallationState(parent, out ILibraryInstallationState state)) yield break; @@ -67,18 +78,23 @@ protected override IEnumerable GetEntries(JsonCompletionCon FrameworkElement presenter = GetPresenter(context); IEnumerable usedFiles = GetUsedFiles(context); + string rootPathPrefix = isFileMapping ? GetRootValue(member) : string.Empty; + static string GetRootValue(MemberNode fileMappingNode) + { + FindFileMappingRootVisitor visitor = new FindFileMappingRootVisitor(); + fileMappingNode.Parent?.Accept(visitor); + return visitor.FoundNode?.UnquotedValueText ?? string.Empty; + } + if (task.IsCompleted) { if (!(task.Result is ILibrary library)) yield break; - foreach (string file in library.Files.Keys) + IEnumerable completions = GetFileCompletions(context, usedFiles, library, rootPathPrefix); + foreach (JsonCompletionEntry item in completions) { - if (!usedFiles.Contains(file)) - { - ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file); - yield return new SimpleCompletionEntry(file, glyph, context.Session); - } + yield return item; } } else @@ -94,23 +110,40 @@ protected override IEnumerable GetEntries(JsonCompletionCon if (!context.Session.IsDismissed) { - var results = new List(); - - foreach (string file in library.Files.Keys) - { - if (!usedFiles.Contains(file)) - { - ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file); - results.Add(new SimpleCompletionEntry(file, glyph, context.Session)); - } - } - - UpdateListEntriesSync(context, results); + IEnumerable completions = GetFileCompletions(context, usedFiles, library, rootPathPrefix); + + UpdateListEntriesSync(context, completions); } }, TaskScheduler.Default); } } + private static IEnumerable GetFileCompletions(JsonCompletionContext context, IEnumerable usedFiles, ILibrary library, string root) + { + static bool alwaysInclude(string s) => true; + bool includeIfUnderRoot(string s) => FileHelpers.IsUnderRootDirectory(s, root); + + Func filter = string.IsNullOrEmpty(root) + ? alwaysInclude + : includeIfUnderRoot; + + bool rootHasTrailingSlash = string.IsNullOrEmpty(root) || root.EndsWith("/") || root.EndsWith("\\"); + int nameOffset = rootHasTrailingSlash ? root.Length : root.Length + 1; + + foreach (string file in library.Files.Keys) + { + if (filter(file)) + { + string fileSubPath = file.Substring(nameOffset); + if (!usedFiles.Contains(fileSubPath)) + { + ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file); + yield return new SimpleCompletionEntry(fileSubPath, glyph, context.Session); + } + } + } + } + private static IEnumerable GetUsedFiles(JsonCompletionContext context) { ArrayNode array = context.ContextNode.FindType(); @@ -139,5 +172,31 @@ private FrameworkElement GetPresenter(JsonCompletionContext context) return presenter; } + + private class FindFileMappingRootVisitor : INodeVisitor + { + public MemberNode FoundNode { get; private set; } + + public VisitNodeResult Visit(Node node) + { + if (node is ObjectNode) + { + return VisitNodeResult.Continue; + } + // we only look at the object and it's members, this is not a recursive search + if (node is not MemberNode mn) + { + return VisitNodeResult.SkipChildren; + } + + if (mn.UnquotedNameText == ManifestConstants.Root) + { + FoundNode = mn; + return VisitNodeResult.Cancel; + } + + return VisitNodeResult.SkipChildren; + } + } } } diff --git a/src/LibraryManager.Vsix/Microsoft.Web.LibraryManager.Vsix.csproj b/src/LibraryManager.Vsix/Microsoft.Web.LibraryManager.Vsix.csproj index 74f2ebe3..35bd8227 100644 --- a/src/LibraryManager.Vsix/Microsoft.Web.LibraryManager.Vsix.csproj +++ b/src/LibraryManager.Vsix/Microsoft.Web.LibraryManager.Vsix.csproj @@ -48,6 +48,7 @@ + @@ -417,4 +418,4 @@ - + \ No newline at end of file