Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement file completion for fileMappings #768

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<JsonCompletionEntry> GetEntries(JsonCompletionContext context)
{
MemberNode member = context.ContextNode.FindType<MemberNode>();

// This provides completions for libraries/[n]/fileMappings/[m]/root
if (member == null || member.UnquotedNameText != ManifestConstants.Root)
yield break;

MemberNode possibleFileMappingsNode = member.Parent.FindType<MemberNode>();
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<ILibrary> 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<JsonCompletionEntry> completions = GetRootCompletions(context, library);

UpdateListEntriesSync(context, completions);
}
}, TaskScheduler.Default);
}
}

private IEnumerable<JsonCompletionEntry> GetRootCompletions(JsonCompletionContext context, ILibrary library)
{
HashSet<string> 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));
}
}
}
99 changes: 79 additions & 20 deletions src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand All @@ -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
Expand All @@ -45,10 +46,20 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon
{
MemberNode member = context.ContextNode.FindType<MemberNode>();

// 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<MemberNode>();
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;
Expand All @@ -67,18 +78,23 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon
FrameworkElement presenter = GetPresenter(context);
IEnumerable<string> 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<JsonCompletionEntry> 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
Expand All @@ -94,23 +110,40 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon

if (!context.Session.IsDismissed)
{
var results = new List<JsonCompletionEntry>();

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<JsonCompletionEntry> completions = GetFileCompletions(context, usedFiles, library, rootPathPrefix);

UpdateListEntriesSync(context, completions);
}
}, TaskScheduler.Default);
}
}

private static IEnumerable<JsonCompletionEntry> GetFileCompletions(JsonCompletionContext context, IEnumerable<string> usedFiles, ILibrary library, string root)
{
static bool alwaysInclude(string s) => true;
bool includeIfUnderRoot(string s) => FileHelpers.IsUnderRootDirectory(s, root);

Func<string, bool> 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<string> GetUsedFiles(JsonCompletionContext context)
{
ArrayNode array = context.ContextNode.FindType<ArrayNode>();
Expand Down Expand Up @@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
<Compile Include="Commands\InstallLibraryCommand.cs" />
<Compile Include="Contracts\DependenciesFactory.cs" />
<Compile Include="Contracts\IDependenciesFactory.cs" />
<Compile Include="Json\Completion\FileMappingRootCompletionProvider.cs" />
<Compile Include="Search\ISearchService.cs" />
<Compile Include="Search\LocationSearchService.cs" />
<Compile Include="Search\ProviderCatalogSearchService.cs" />
Expand Down Expand Up @@ -417,4 +418,4 @@
<VSIXSourceItem Remove="@(_VsixSourceItemsFromNuGet)" />
</ItemGroup>
</Target>
</Project>
</Project>