Skip to content

Commit e06c1ed

Browse files
committed
Move to a listener model to invert the relationship between the capability check and notifier
1 parent 5a9344a commit e06c1ed

File tree

4 files changed

+54
-29
lines changed

4 files changed

+54
-29
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.VisualStudio.Razor;
5+
6+
internal interface IProjectCapabilityListener
7+
{
8+
void OnProjectCapabilityMatched(string projectFilePath, string capability, bool isMatch);
9+
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/IProjectCapabilityResolver.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,4 @@ internal interface IProjectCapabilityResolver
99
/// Determines whether the project associated with the specified document has the given <paramref name="capability"/>.
1010
/// </summary>
1111
bool ResolveCapability(string capability, string documentFilePath);
12-
13-
/// <summary>
14-
/// Tries to return a cached value for the capability check, if a previous call to <see cref="ResolveCapability(string, string)" /> has been made for the same project and capability.
15-
/// </summary>
16-
/// <remarks>
17-
/// This method is intended purely for performance optimization. It should not be used to determine if a capability is supported, as it may return false negatives in many circumstances.
18-
/// </remarks>
19-
bool TryGetCachedCapabilityMatch(string projectFilePath, string capability, out bool isMatch);
2012
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/LanguageClient/Cohost/IncompatibleProjectNotifier.cs

Lines changed: 39 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,33 +15,64 @@
1515

1616
namespace Microsoft.VisualStudio.Razor.LanguageClient.Cohost;
1717

18+
[Export(typeof(IProjectCapabilityListener))]
1819
[Export(typeof(IIncompatibleProjectNotifier))]
1920
[method: ImportingConstructor]
2021
internal sealed class IncompatibleProjectNotifier(
21-
IProjectCapabilityResolver projectCapabilityResolver,
22-
ILoggerFactory loggerFactory) : IIncompatibleProjectNotifier
22+
ILoggerFactory loggerFactory) : IIncompatibleProjectNotifier, IProjectCapabilityListener
2323
{
24-
private readonly IProjectCapabilityResolver _projectCapabilityResolver = projectCapabilityResolver;
2524
private readonly ILogger _logger = loggerFactory.GetOrCreateLogger<IncompatibleProjectNotifier>();
2625

26+
private readonly HashSet<string> _frameworkProjects = new(PathUtilities.OSSpecificPathComparer);
27+
2728
public void NotifyMiscFilesDocument(TextDocument textDocument)
2829
{
2930
_logger.Log(LogLevel.Error, $"{WorkspacesSR.FormatIncompatibleProject_MiscFiles(Path.GetFileName(textDocument.FilePath))}");
3031
}
3132

3233
public void NotifyMissingDocument(Project project, string filePath)
3334
{
34-
// When this document was opened, we will have checked if it was a .NET Framework project. If so, then we can avoid
35-
// notifying the user because they are not using the LSP editor, even though we get the odd request.
36-
// If this check returns a false positive, the fallout is only one log message, so nothing to be concerned about.
37-
if (_projectCapabilityResolver.TryGetCachedCapabilityMatch(project.FilePath.AssumeNotNull(), WellKnownProjectCapabilities.DotNetCoreCSharp, out var isMatch) && !isMatch)
35+
// When this document was opened, we will have checked if it was a .NET Framework project, and we listened for that below.
36+
// Since this method is only called when we receive an LSP request for a document, and LSP only works on open documents,
37+
// we know that the capability check must have happened before this method was called, so our cache is as up to date as
38+
// possible for the specific file being asked about.
39+
lock (_frameworkProjects)
3840
{
39-
return;
41+
if (_frameworkProjects.Contains(project.FilePath.AssumeNotNull()))
42+
{
43+
// This project doesn't have the .NET Core C# capability, so it's a .NET Framework project and we don't want
44+
// to notify the user, as those projects use a different editor.
45+
return;
46+
}
4047
}
4148

4249
_logger.Log(LogLevel.Error, $"{(
4350
project.AdditionalDocuments.Any(d => d.FilePath is not null && d.FilePath.IsRazorFilePath())
4451
? WorkspacesSR.FormatIncompatibleProject_NotAnAdditionalFile(Path.GetFileName(filePath), project.Name)
4552
: WorkspacesSR.FormatIncompatibleProject_NoAdditionalFiles(Path.GetFileName(filePath), project.Name))}");
4653
}
54+
55+
public void OnProjectCapabilityMatched(string projectFilePath, string capability, bool isMatch)
56+
{
57+
// We only track the .NET Core capability
58+
if (capability != WellKnownProjectCapabilities.DotNetCoreCSharp)
59+
{
60+
return;
61+
}
62+
63+
lock (_frameworkProjects)
64+
{
65+
if (isMatch)
66+
{
67+
// The project is a .NET Core project, so we don't care, but just in case it used to be .NET Framework,
68+
// let's clean up.
69+
_frameworkProjects.Remove(projectFilePath);
70+
}
71+
else
72+
{
73+
// The project is not a .NET Core project, so add it to our list of framework projects.
74+
_frameworkProjects.Add(projectFilePath);
75+
}
76+
}
77+
}
4778
}

src/Razor/src/Microsoft.VisualStudio.LanguageServices.Razor/ProjectCapabilityResolver.cs

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,22 +22,22 @@ namespace Microsoft.VisualStudio.Razor;
2222
internal sealed class ProjectCapabilityResolver : IProjectCapabilityResolver, IDisposable
2323
{
2424
private readonly ILiveShareSessionAccessor _liveShareSessionAccessor;
25+
private readonly IEnumerable<IProjectCapabilityListener> _projectCapabilityListeners;
2526
private readonly AsyncLazy<IVsUIShellOpenDocument> _lazyVsUIShellOpenDocument;
2627
private readonly ILogger _logger;
2728
private readonly JoinableTaskFactory _jtf;
2829
private readonly CancellationTokenSource _disposeTokenSource;
2930

30-
private readonly Dictionary<(string ProjectFilePath, string Capability), bool> _cachedCapabilities = [];
31-
private readonly object _gate = new();
32-
3331
[ImportingConstructor]
3432
public ProjectCapabilityResolver(
3533
ILiveShareSessionAccessor liveShareSessionAccessor,
3634
IVsService<SVsUIShellOpenDocument, IVsUIShellOpenDocument> vsUIShellOpenDocumentService,
35+
[ImportMany] IEnumerable<IProjectCapabilityListener> projectCapabilityListeners,
3736
ILoggerFactory loggerFactory,
3837
JoinableTaskContext joinableTaskContext)
3938
{
4039
_liveShareSessionAccessor = liveShareSessionAccessor;
40+
_projectCapabilityListeners = projectCapabilityListeners;
4141
_jtf = joinableTaskContext.Factory;
4242
_logger = loggerFactory.GetOrCreateLogger<ProjectCapabilityResolver>();
4343
_disposeTokenSource = new();
@@ -131,9 +131,10 @@ private bool ContainingProjectHasCapability(string capability, string documentFi
131131

132132
if (vsHierarchy.GetProjectFilePath(_jtf) is { } projectFilePath)
133133
{
134-
lock (_gate)
134+
foreach (var listener in _projectCapabilityListeners)
135135
{
136-
_cachedCapabilities[(projectFilePath, capability)] = isMatch;
136+
// Notify all listeners of the capability match.
137+
listener.OnProjectCapabilityMatched(projectFilePath, capability, isMatch);
137138
}
138139
}
139140
}
@@ -149,12 +150,4 @@ private bool ContainingProjectHasCapability(string capability, string documentFi
149150

150151
return isMatch;
151152
}
152-
153-
public bool TryGetCachedCapabilityMatch(string projectFilePath, string capability, out bool isMatch)
154-
{
155-
lock (_gate)
156-
{
157-
return _cachedCapabilities.TryGetValue((projectFilePath, capability), out isMatch);
158-
}
159-
}
160153
}

0 commit comments

Comments
 (0)