Skip to content
Merged
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
1 change: 1 addition & 0 deletions .github/instructions/IDE.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Comment thread
JoeRobich marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public Task OnInitializedAsync(ClientCapabilities clientCapabilities, RequestCon
return Task.CompletedTask;
}

private void OnWorkspaceFoldersChanged(ImmutableHashSet<string> _)
private void OnWorkspaceFoldersChanged()
=> _discoveryQueue.AddWork();

public void Dispose()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Microsoft.CodeAnalysis.LanguageServer.Handler;

internal interface IWorkspaceFolderTracker : ILspService
{
event Action<ImmutableHashSet<string>>? WorkspaceFoldersChanged;
event Action? WorkspaceFoldersChanged;
Comment thread
JoeRobich marked this conversation as resolved.

ImmutableHashSet<string> GetRequiredWorkspaceFolderPaths();

Expand Down
17 changes: 7 additions & 10 deletions src/LanguageServer/Protocol/Handler/WorkspaceFolderTracker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// <summary>
/// Mutations are serialized by the request queue, but non-mutating requests may read the current folders concurrently.
/// </summary>
Comment thread
JoeRobich marked this conversation as resolved.
private readonly object _gate = new();
private ImmutableHashSet<string> _workspaceFolderPaths = ImmutableHashSet.Create(PathUtilities.Comparer);
private volatile ImmutableHashSet<string> _workspaceFolderPaths = ImmutableHashSet.Create(PathUtilities.Comparer);

public event Action<ImmutableHashSet<string>>? WorkspaceFoldersChanged;
public event Action? WorkspaceFoldersChanged;

public void Update(WorkspaceFolder[]? addedFolders, WorkspaceFolder[]? removedFolders)
{
Expand Down Expand Up @@ -52,16 +54,11 @@ public void Update(WorkspaceFolder[]? addedFolders, WorkspaceFolder[]? removedFo
_workspaceFolderPaths = updatedWorkspaceFolderPaths;
}

WorkspaceFoldersChanged?.Invoke(updatedWorkspaceFolderPaths);
WorkspaceFoldersChanged?.Invoke();
}

public ImmutableHashSet<string> GetRequiredWorkspaceFolderPaths()
{
lock (_gate)
{
return _workspaceFolderPaths;
}
}
=> _workspaceFolderPaths;

private static string? GetNormalizedFilePath(WorkspaceFolder workspaceFolder)
=> workspaceFolder.DocumentUri.ParsedDocumentUri?.IsFile == true
Expand Down
2 changes: 1 addition & 1 deletion src/LanguageServer/ProtocolUnitTests/HandlerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,9 +26,9 @@ public ProjectFileExtensionRegistry(DiagnosticReporter diagnosticReporter, IFile

_extensionToLanguageMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "csproj", LanguageNames.CSharp },
{ "vbproj", LanguageNames.VisualBasic },
{ "fsproj", LanguageNames.FSharp }
{ ".csproj", LanguageNames.CSharp },
{ ".vbproj", LanguageNames.VisualBasic },
{ ".fsproj", LanguageNames.FSharp }
};

_dataGuard = new NonReentrantLock();
Expand All @@ -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)
/// <summary>
/// Gets the registered project file extensions with a leading '.'.
/// </summary>
public ImmutableArray<string> GetRegisteredProjectFileExtensions()
Comment thread
JoeRobich marked this conversation as resolved.
Comment thread
JoeRobich marked this conversation as resolved.
{
return TryGetLanguageNameFromProjectPath(projectFilePath, mode, out languageName, out _);
using (_dataGuard.DisposableWait())
{
return [.. _extensionToLanguageMap.Keys];
Comment thread
JoeRobich marked this conversation as resolved.
}
Comment thread
JoeRobich marked this conversation as resolved.
}
Comment thread
Copilot marked this conversation as resolved.

public bool TryGetLanguageNameFromProjectPath(string? projectFilePath, DiagnosticReportingMode mode, [NotNullWhen(true)] out string? languageName, out bool isFileBasedApp)
/// <summary>
/// Tries to get the language registered for an extension, with or without a leading '.'.
/// </summary>
public bool TryGetLanguageNameFromExtension(string extension, [NotNullWhen(true)] out string? languageName)
Comment thread
JoeRobich marked this conversation as resolved.
{
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)
Expand All @@ -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;
}
}
25 changes: 24 additions & 1 deletion src/Workspaces/MSBuild/Test/NetCoreTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Comment thread
JoeRobich marked this conversation as resolved.
await workspace.OpenProjectAsync(sourceFilePath);

// [Failure] Msbuild failed when processing the file 'Program.cs' with message:
Expand All @@ -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);
Comment thread
JoeRobich marked this conversation as resolved.

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)]
Expand Down
Loading