Skip to content

Commit acc1ce7

Browse files
committed
Implement file completion for fileMappings
This provides: - Completion for the "root" property. This is a set of all the subdirectories within the package. - Completion for the "files" property within a "fileMapping". This is largely the same as the existing property, but needs to take into account the "root" as a prefix if specified.
1 parent e639b87 commit acc1ce7

File tree

3 files changed

+192
-21
lines changed

3 files changed

+192
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Collections.Generic;
5+
using System.ComponentModel.Composition;
6+
using System.Diagnostics.CodeAnalysis;
7+
using System.Linq;
8+
using System.Threading;
9+
using System.Threading.Tasks;
10+
using Microsoft.VisualStudio.Imaging;
11+
using Microsoft.VisualStudio.Utilities;
12+
using Microsoft.Web.LibraryManager.Contracts;
13+
using Microsoft.Web.LibraryManager.Vsix.Contracts;
14+
using Microsoft.WebTools.Languages.Json.Editor.Completion;
15+
using Microsoft.WebTools.Languages.Json.Parser.Nodes;
16+
17+
namespace Microsoft.Web.LibraryManager.Vsix.Json.Completion
18+
{
19+
[Export(typeof(IJsonCompletionListProvider))]
20+
[Name(nameof(FileMappingRootCompletionProvider))]
21+
internal class FileMappingRootCompletionProvider : BaseCompletionProvider
22+
{
23+
private readonly IDependenciesFactory _dependenciesFactory;
24+
25+
[ImportingConstructor]
26+
internal FileMappingRootCompletionProvider(IDependenciesFactory dependenciesFactory)
27+
{
28+
_dependenciesFactory = dependenciesFactory;
29+
}
30+
31+
public override JsonCompletionContextType ContextType => JsonCompletionContextType.PropertyValue;
32+
33+
[SuppressMessage("Usage", "VSTHRD002:Avoid problematic synchronous waits", Justification = "Checked completion first")]
34+
protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionContext context)
35+
{
36+
MemberNode member = context.ContextNode.FindType<MemberNode>();
37+
38+
// This provides completions for libraries/[n]/fileMappings/[m]/root
39+
if (member == null || member.UnquotedNameText != ManifestConstants.Root)
40+
yield break;
41+
42+
MemberNode possibleFileMappingsNode = member.Parent.FindType<MemberNode>();
43+
bool isInFileMapping = possibleFileMappingsNode?.UnquotedNameText == ManifestConstants.FileMappings;
44+
if (!isInFileMapping)
45+
yield break;
46+
47+
ObjectNode parent = possibleFileMappingsNode.Parent as ObjectNode;
48+
49+
if (!JsonHelpers.TryGetInstallationState(parent, out ILibraryInstallationState state))
50+
yield break;
51+
52+
if (string.IsNullOrEmpty(state.Name))
53+
yield break;
54+
55+
IDependencies dependencies = _dependenciesFactory.FromConfigFile(ConfigFilePath);
56+
IProvider provider = dependencies.GetProvider(state.ProviderId);
57+
ILibraryCatalog catalog = provider?.GetCatalog();
58+
59+
if (catalog is null)
60+
{
61+
yield break;
62+
}
63+
64+
Task<ILibrary> task = catalog.GetLibraryAsync(state.Name, state.Version, CancellationToken.None);
65+
66+
if (task.IsCompleted)
67+
{
68+
if (task.Result is ILibrary library)
69+
{
70+
foreach (JsonCompletionEntry item in GetRootCompletions(context, library))
71+
{
72+
yield return item;
73+
}
74+
}
75+
}
76+
else
77+
{
78+
yield return new SimpleCompletionEntry(Resources.Text.Loading, string.Empty, KnownMonikers.Loading, context.Session);
79+
_ = task.ContinueWith(async (t) =>
80+
{
81+
await VisualStudio.Shell.ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
82+
83+
if (!(t.Result is ILibrary library))
84+
return;
85+
86+
if (!context.Session.IsDismissed)
87+
{
88+
IEnumerable<JsonCompletionEntry> completions = GetRootCompletions(context, library);
89+
90+
UpdateListEntriesSync(context, completions);
91+
}
92+
}, TaskScheduler.Default);
93+
}
94+
}
95+
96+
private IEnumerable<JsonCompletionEntry> GetRootCompletions(JsonCompletionContext context, ILibrary library)
97+
{
98+
HashSet<string> libraryFolders = [];
99+
foreach (string file in library.Files.Keys)
100+
{
101+
int sepIndex = file.LastIndexOf('/');
102+
if (sepIndex >= 0)
103+
{
104+
libraryFolders.Add(file.Substring(0, file.LastIndexOf('/')));
105+
}
106+
}
107+
108+
return libraryFolders.Select(folder => new SimpleCompletionEntry(folder, KnownMonikers.FolderClosed, context.Session));
109+
}
110+
}
111+
}

src/LibraryManager.Vsix/Json/Completion/FilesCompletionProvider.cs

+79-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.ComponentModel.Composition;
67
using System.Diagnostics.CodeAnalysis;
@@ -9,7 +10,6 @@
910
using System.Threading.Tasks;
1011
using System.Windows;
1112
using System.Windows.Data;
12-
using System.Windows.Media;
1313
using Microsoft.VisualStudio.Imaging;
1414
using Microsoft.VisualStudio.Imaging.Interop;
1515
using Microsoft.VisualStudio.PlatformUI;
@@ -19,6 +19,7 @@
1919
using Microsoft.Web.LibraryManager.Vsix.Shared;
2020
using Microsoft.WebTools.Languages.Json.Editor.Completion;
2121
using Microsoft.WebTools.Languages.Json.Parser.Nodes;
22+
using Microsoft.WebTools.Languages.Shared.Parser;
2223
using Microsoft.WebTools.Languages.Shared.Parser.Nodes;
2324

2425
namespace Microsoft.Web.LibraryManager.Vsix.Json.Completion
@@ -45,10 +46,20 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon
4546
{
4647
MemberNode member = context.ContextNode.FindType<MemberNode>();
4748

49+
// We can show completions for "files". This could be libraries/[n]/files or
50+
// libraries/[n]/fileMappings/[m]/files.
4851
if (member == null || member.UnquotedNameText != "files")
4952
yield break;
5053

51-
var parent = member.Parent as ObjectNode;
54+
// If the current member is "files", then it is either:
55+
// - a library "files" property
56+
// - a fileMapping "files" property
57+
MemberNode possibleFileMappingsNode = member.Parent.FindType<MemberNode>();
58+
bool isFileMapping = possibleFileMappingsNode?.UnquotedNameText == "fileMappings";
59+
60+
ObjectNode parent = isFileMapping
61+
? possibleFileMappingsNode.Parent as ObjectNode
62+
: member.Parent as ObjectNode;
5263

5364
if (!JsonHelpers.TryGetInstallationState(parent, out ILibraryInstallationState state))
5465
yield break;
@@ -67,18 +78,23 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon
6778
FrameworkElement presenter = GetPresenter(context);
6879
IEnumerable<string> usedFiles = GetUsedFiles(context);
6980

81+
string rootPathPrefix = isFileMapping ? GetRootValue(member) : string.Empty;
82+
static string GetRootValue(MemberNode fileMappingNode)
83+
{
84+
FindFileMappingRootVisitor visitor = new FindFileMappingRootVisitor();
85+
fileMappingNode.Parent?.Accept(visitor);
86+
return visitor.FoundNode?.UnquotedValueText ?? string.Empty;
87+
}
88+
7089
if (task.IsCompleted)
7190
{
7291
if (!(task.Result is ILibrary library))
7392
yield break;
7493

75-
foreach (string file in library.Files.Keys)
94+
IEnumerable<JsonCompletionEntry> completions = GetFileCompletions(context, usedFiles, library, rootPathPrefix);
95+
foreach (JsonCompletionEntry item in completions)
7696
{
77-
if (!usedFiles.Contains(file))
78-
{
79-
ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file);
80-
yield return new SimpleCompletionEntry(file, glyph, context.Session);
81-
}
97+
yield return item;
8298
}
8399
}
84100
else
@@ -94,23 +110,40 @@ protected override IEnumerable<JsonCompletionEntry> GetEntries(JsonCompletionCon
94110

95111
if (!context.Session.IsDismissed)
96112
{
97-
var results = new List<JsonCompletionEntry>();
98-
99-
foreach (string file in library.Files.Keys)
100-
{
101-
if (!usedFiles.Contains(file))
102-
{
103-
ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file);
104-
results.Add(new SimpleCompletionEntry(file, glyph, context.Session));
105-
}
106-
}
107-
108-
UpdateListEntriesSync(context, results);
113+
IEnumerable<JsonCompletionEntry> completions = GetFileCompletions(context, usedFiles, library, rootPathPrefix);
114+
115+
UpdateListEntriesSync(context, completions);
109116
}
110117
}, TaskScheduler.Default);
111118
}
112119
}
113120

121+
private static IEnumerable<JsonCompletionEntry> GetFileCompletions(JsonCompletionContext context, IEnumerable<string> usedFiles, ILibrary library, string root)
122+
{
123+
static bool alwaysInclude(string s) => true;
124+
bool includeIfUnderRoot(string s) => FileHelpers.IsUnderRootDirectory(s, root);
125+
126+
Func<string, bool> filter = string.IsNullOrEmpty(root)
127+
? alwaysInclude
128+
: includeIfUnderRoot;
129+
130+
bool rootHasTrailingSlash = string.IsNullOrEmpty(root) || root.EndsWith("/") || root.EndsWith("\\");
131+
int nameOffset = rootHasTrailingSlash ? root.Length : root.Length + 1;
132+
133+
foreach (string file in library.Files.Keys)
134+
{
135+
if (filter(file))
136+
{
137+
string fileSubPath = file.Substring(nameOffset);
138+
if (!usedFiles.Contains(fileSubPath))
139+
{
140+
ImageMoniker glyph = WpfUtil.GetImageMonikerForFile(file);
141+
yield return new SimpleCompletionEntry(fileSubPath, glyph, context.Session);
142+
}
143+
}
144+
}
145+
}
146+
114147
private static IEnumerable<string> GetUsedFiles(JsonCompletionContext context)
115148
{
116149
ArrayNode array = context.ContextNode.FindType<ArrayNode>();
@@ -139,5 +172,31 @@ private FrameworkElement GetPresenter(JsonCompletionContext context)
139172

140173
return presenter;
141174
}
175+
176+
private class FindFileMappingRootVisitor : INodeVisitor
177+
{
178+
public MemberNode FoundNode { get; private set; }
179+
180+
public VisitNodeResult Visit(Node node)
181+
{
182+
if (node is ObjectNode)
183+
{
184+
return VisitNodeResult.Continue;
185+
}
186+
// we only look at the object and it's members, this is not a recursive search
187+
if (node is not MemberNode mn)
188+
{
189+
return VisitNodeResult.SkipChildren;
190+
}
191+
192+
if (mn.UnquotedNameText == ManifestConstants.Root)
193+
{
194+
FoundNode = mn;
195+
return VisitNodeResult.Cancel;
196+
}
197+
198+
return VisitNodeResult.SkipChildren;
199+
}
200+
}
142201
}
143202
}

src/LibraryManager.Vsix/Microsoft.Web.LibraryManager.Vsix.csproj

+2-1
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
<Compile Include="Commands\InstallLibraryCommand.cs" />
4949
<Compile Include="Contracts\DependenciesFactory.cs" />
5050
<Compile Include="Contracts\IDependenciesFactory.cs" />
51+
<Compile Include="Json\Completion\FileMappingRootCompletionProvider.cs" />
5152
<Compile Include="Search\ISearchService.cs" />
5253
<Compile Include="Search\LocationSearchService.cs" />
5354
<Compile Include="Search\ProviderCatalogSearchService.cs" />
@@ -417,4 +418,4 @@
417418
<VSIXSourceItem Remove="@(_VsixSourceItemsFromNuGet)" />
418419
</ItemGroup>
419420
</Target>
420-
</Project>
421+
</Project>

0 commit comments

Comments
 (0)