diff --git a/.github/instructions/IDE.instructions.md b/.github/instructions/IDE.instructions.md index 12a3115d48505..12a8150f95ab1 100644 --- a/.github/instructions/IDE.instructions.md +++ b/.github/instructions/IDE.instructions.md @@ -104,3 +104,4 @@ var methodDecl = generator.MethodDeclaration("MyMethod", ...); - **ImportingConstructor must be marked `[Obsolete]`** with `MefConstruction.ImportingConstructorMessage` - **Language services must be exported with a specific language name** — don't use generic exports for both C#/VB - **Workspace changes must use immutable updates** — `Workspace.SetCurrentSolution()` +- **MSBuild project extensions are stored with a leading `.`.** `ProjectFileExtensionRegistry` accepts registration and lookup values with or without the dot, but its enumeration API returns the canonical dot-prefixed form. diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs index acc7533cf36ea..0d2e737ff48fa 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/FileBasedProgramsEntryPointDiscovery.cs @@ -77,7 +77,7 @@ public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestCon return Task.CompletedTask; } - private void OnWorkspaceFoldersChanged(ImmutableHashSet _) + private void OnWorkspaceFoldersChanged() => _discoveryQueue.AddWork(); public void Dispose() diff --git a/src/LanguageServer/Protocol/Handler/IWorkspaceFolderTracker.cs b/src/LanguageServer/Protocol/Handler/IWorkspaceFolderTracker.cs index c9732f35d138e..584cfcd67995b 100644 --- a/src/LanguageServer/Protocol/Handler/IWorkspaceFolderTracker.cs +++ b/src/LanguageServer/Protocol/Handler/IWorkspaceFolderTracker.cs @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; internal interface IWorkspaceFolderTracker : ILspService { - event Action>? WorkspaceFoldersChanged; + event Action? WorkspaceFoldersChanged; ImmutableHashSet GetRequiredWorkspaceFolderPaths(); diff --git a/src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs b/src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs index c0d7a92bc69f0..fa1787e529b44 100644 --- a/src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs +++ b/src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs @@ -12,11 +12,13 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler; internal sealed class WorkspaceFolderTracker : IWorkspaceFolderTracker { - // Mutations are serialized by the request queue, but non-mutating requests may read the current folders concurrently. + /// + /// Mutations are serialized by the request queue, but non-mutating requests may read the current folders concurrently. + /// private readonly object _gate = new(); - private ImmutableHashSet _workspaceFolderPaths = ImmutableHashSet.Create(PathUtilities.Comparer); + private volatile ImmutableHashSet _workspaceFolderPaths = ImmutableHashSet.Create(PathUtilities.Comparer); - public event Action>? WorkspaceFoldersChanged; + public event Action? WorkspaceFoldersChanged; public void Update(WorkspaceFolder[]? addedFolders, WorkspaceFolder[]? removedFolders) { @@ -52,16 +54,11 @@ public void Update(WorkspaceFolder[]? addedFolders, WorkspaceFolder[]? removedFo _workspaceFolderPaths = updatedWorkspaceFolderPaths; } - WorkspaceFoldersChanged?.Invoke(updatedWorkspaceFolderPaths); + WorkspaceFoldersChanged?.Invoke(); } public ImmutableHashSet GetRequiredWorkspaceFolderPaths() - { - lock (_gate) - { - return _workspaceFolderPaths; - } - } + => _workspaceFolderPaths; private static string? GetNormalizedFilePath(WorkspaceFolder workspaceFolder) => workspaceFolder.DocumentUri.ParsedDocumentUri?.IsFile == true diff --git a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs index b223bf7cd610c..447c059d011e3 100644 --- a/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs +++ b/src/LanguageServer/ProtocolUnitTests/HandlerTests.cs @@ -49,7 +49,7 @@ public void WorkspaceFolderTrackerPreservesSetForEquivalentUpdate() var workspaceFolder = new WorkspaceFolder { DocumentUri = new("file:///Workspace"), Name = "Workspace" }; var equivalentWorkspaceFolder = new WorkspaceFolder { DocumentUri = new("file:///Workspace/"), Name = "Workspace" }; var eventCount = 0; - tracker.WorkspaceFoldersChanged += _ => eventCount++; + tracker.WorkspaceFoldersChanged += () => eventCount++; tracker.Update([workspaceFolder], removedFolders: null); var workspaceFolders = tracker.GetRequiredWorkspaceFolderPaths(); diff --git a/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs b/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs index 9d6fa1cae7ae7..fc7f72c399be4 100644 --- a/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs +++ b/src/Workspaces/MSBuild/Core/MSBuild/ProjectFileExtensionRegistry.cs @@ -4,7 +4,7 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.IO; using Microsoft.CodeAnalysis.FileBasedPrograms; @@ -26,9 +26,9 @@ public ProjectFileExtensionRegistry(DiagnosticReporter diagnosticReporter, IFile _extensionToLanguageMap = new Dictionary(StringComparer.OrdinalIgnoreCase) { - { "csproj", LanguageNames.CSharp }, - { "vbproj", LanguageNames.VisualBasic }, - { "fsproj", LanguageNames.FSharp } + { ".csproj", LanguageNames.CSharp }, + { ".vbproj", LanguageNames.VisualBasic }, + { ".fsproj", LanguageNames.FSharp } }; _dataGuard = new NonReentrantLock(); @@ -41,38 +41,58 @@ public void AssociateFileExtensionWithLanguage(string fileExtension, string lang { using (_dataGuard.DisposableWait()) { - _extensionToLanguageMap[fileExtension] = language; + _extensionToLanguageMap[AddLeadingDot(fileExtension)] = language; } } - public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName) + /// + /// Gets the registered project file extensions with a leading '.'. + /// + public ImmutableArray GetRegisteredProjectFileExtensions() { - return TryGetLanguageNameFromProjectPath(projectFilePath, mode, out languageName, out _); + using (_dataGuard.DisposableWait()) + { + return [.. _extensionToLanguageMap.Keys]; + } } - public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName, out bool isFileBasedApp) + /// + /// Tries to get the language registered for an extension, with or without a leading '.'. + /// + public bool TryGetLanguageNameFromExtension(string extension, [NotNullWhen(true)] out string? languageName) { - var extension = Path.GetExtension(projectFilePath); - if (extension is null) + using (_dataGuard.DisposableWait()) + { + return _extensionToLanguageMap.TryGetValue(AddLeadingDot(extension), out languageName); + } + } + + private static string AddLeadingDot(string extension) + => extension.Length == 0 || extension[0] == '.' ? extension : "." + extension; + + public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName) + => TryGetLanguageNameFromProjectPath(projectFilePath, mode, out languageName, out _); + + public bool TryGetLanguageNameFromProjectPath( + string? projectFilePath, + DiagnosticReportingMode mode, + [NotNullWhen(true)] out string? languageName, + out bool isFileBasedApp) + { + if (projectFilePath is null) { languageName = null; isFileBasedApp = false; - _diagnosticReporter.Report(mode, $"Project file path was 'null'"); + _diagnosticReporter.Report(mode, "Project file path is null."); return false; } - Debug.Assert(projectFilePath != null); - - if (extension is ['.', .. var rest]) - extension = rest; + var projectFileExtension = Path.GetExtension(projectFilePath); - using (_dataGuard.DisposableWait()) + if (TryGetLanguageNameFromExtension(projectFileExtension, out languageName)) { - if (_extensionToLanguageMap.TryGetValue(extension, out languageName)) - { - isFileBasedApp = false; - return true; - } + isFileBasedApp = false; + return true; } if (_fileBasedProgramService?.IsValidEntryPointPath(projectFilePath) == true) @@ -83,7 +103,7 @@ public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, Diagnosti } isFileBasedApp = false; - _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, Path.GetExtension(projectFilePath))); + _diagnosticReporter.Report(mode, string.Format(WorkspacesResources.Cannot_open_project_0_because_the_file_extension_1_is_not_associated_with_a_language, projectFilePath, projectFileExtension)); return false; } } diff --git a/src/Workspaces/MSBuild/Test/NetCoreTests.cs b/src/Workspaces/MSBuild/Test/NetCoreTests.cs index 5d70704e34c62..695308d42ddb8 100644 --- a/src/Workspaces/MSBuild/Test/NetCoreTests.cs +++ b/src/Workspaces/MSBuild/Test/NetCoreTests.cs @@ -747,7 +747,7 @@ public async Task TestOpenProject_FileBasedApp_AssociateFileExtensionWithLanguag var sourceFilePath = GetSolutionFileName("Program.cs"); using var workspace = CreateMSBuildWorkspace(); - workspace.AssociateFileExtensionWithLanguage("cs", LanguageNames.CSharp); + workspace.AssociateFileExtensionWithLanguage(".cs", LanguageNames.CSharp); await workspace.OpenProjectAsync(sourceFilePath); // [Failure] Msbuild failed when processing the file 'Program.cs' with message: @@ -756,6 +756,29 @@ public async Task TestOpenProject_FileBasedApp_AssociateFileExtensionWithLanguag Assert.Contains("Program.cs", diagnostic.Message); } + [Fact] + public void ProjectFileExtensionRegistryUsesLeadingDots() + { + using var workspace = new AdhocWorkspace(); + var registry = new ProjectFileExtensionRegistry(new DiagnosticReporter(workspace), fileBasedProgramService: null); + + var registeredExtensions = registry.GetRegisteredProjectFileExtensions(); + Assert.Equal(3, registeredExtensions.Length); + Assert.Contains(".csproj", registeredExtensions); + Assert.Contains(".vbproj", registeredExtensions); + Assert.Contains(".fsproj", registeredExtensions); + + Assert.True(registry.TryGetLanguageNameFromExtension(".csproj", out var languageName)); + Assert.Equal(LanguageNames.CSharp, languageName); + Assert.True(registry.TryGetLanguageNameFromExtension("csproj", out languageName)); + Assert.Equal(LanguageNames.CSharp, languageName); + + registry.AssociateFileExtensionWithLanguage(".", "Dot"); + Assert.True(registry.TryGetLanguageNameFromExtension(".", out languageName)); + Assert.Equal("Dot", languageName); + Assert.False(registry.TryGetLanguageNameFromExtension("", out _)); + } + [ConditionalFact(typeof(DotNetSdkMSBuildInstalled))] [Trait(Traits.Feature, Traits.Features.MSBuildWorkspace)] [Trait(Traits.Feature, Traits.Features.NetCore)]