Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
946f90f
Allow MSBuildWorkspace to open file-based apps
jjonescz Jun 15, 2026
a0e4f7a
Add RemoteProjectInstance
jjonescz Jun 15, 2026
715a38f
Support `#:ref`s
jjonescz Jun 17, 2026
c850439
Merge branch 'main' into 78887-sprint-MSBuildWorkspace
jjonescz Jun 17, 2026
8a8efd0
Fixup use of ImmutableArray
jjonescz Jun 17, 2026
daf0e3a
Update to fix a leak
jjonescz Jun 17, 2026
70c8202
Remove unnecessary items
jjonescz Jun 17, 2026
ad8d824
Use VirtualProjectBuilder in FileBasedProgramsProjectSystem
jjonescz Jun 17, 2026
4f7e092
Test `#:ref` in LSP
jjonescz Jun 17, 2026
9d7ceee
Fixup tfm condition
jjonescz Jun 18, 2026
fb1d7eb
Merge branch 'main' into 78887-sprint-MSBuildWorkspace
jjonescz Jun 24, 2026
9fc0b90
Improve tests
jjonescz Jun 24, 2026
b66509c
Fixup net472
jjonescz Jun 24, 2026
6d97213
Improve global.json in tests
jjonescz Jun 24, 2026
52f1c2e
Test diagnostics
jjonescz Jun 25, 2026
b0cfe02
Remove CSharp dependency
jjonescz Jun 25, 2026
c4224f3
Move the whole builder to the service
jjonescz Jun 26, 2026
9a2c671
Improve code ordering
jjonescz Jun 26, 2026
a8cd5ba
Promote to workspace service
jjonescz Jun 26, 2026
95f50a3
Implement more BuildService APIs
jjonescz Jun 26, 2026
dba38e8
Improve BuildService implementation
jjonescz Jun 26, 2026
19a524a
Remove run-api usage
jjonescz Jun 26, 2026
3da4bd8
Extract TFM
jjonescz Jun 26, 2026
b62c59c
Share global build properties
jjonescz Jun 26, 2026
c31639f
Bump SDK to 10.0.301
jjonescz Jun 26, 2026
4b3e00d
Remove custom `global.json`s from tests
jjonescz Jun 26, 2026
a7b8321
Merge branch 'main' into 78887-sprint-MSBuildWorkspace
jjonescz Jun 26, 2026
7a3b860
Fix SourceRoot test
tmat Jun 24, 2026
f9d95e8
Remove unnecessary `using`
jjonescz Jun 29, 2026
1a104e8
Support extensionless file-based apps
jjonescz Jun 29, 2026
68bd559
Remove copied virtual project helpers
jjonescz Jun 29, 2026
4900670
Disable MSBuild server temporarily
jjonescz Jun 29, 2026
57ca53e
Fixup implementation of GetDiscoveryCacheRootDirectory
jjonescz Jun 29, 2026
1ad218f
Ensure fuzz repro is up-to-date
jjonescz Jun 29, 2026
06b57f1
Move the source package to Workspaces layer
jjonescz Jun 29, 2026
8cb6ea2
Prefer NetCore for file-based apps
jjonescz Jun 29, 2026
97a1252
Propagate cancellation token
jjonescz Jun 30, 2026
87968dd
Use `GetAwaiter().GetResult()`
jjonescz Jun 30, 2026
b5ca6df
Avoid loading non-existent DLLs in a test
jjonescz Jun 30, 2026
27c8141
Improve assert in a test
jjonescz Jun 30, 2026
8fef535
Merge branch 'main' into 78887-sprint-MSBuildWorkspace
jjonescz Jul 1, 2026
f04f656
Use physical file path in project map
jjonescz Jul 1, 2026
5c8f512
Simplify physical/virtual path split
jjonescz Jul 1, 2026
18522a2
Pass options to XmlReader
jjonescz Jul 1, 2026
aee4b35
Merge branch 'main' into 78887-sprint-MSBuildWorkspace
jjonescz Jul 21, 2026
f084b1f
Dispose remote project instances
jjonescz Jul 21, 2026
1fc2f1d
Use BundledNETCoreAppTargetFrameworkVersion
jjonescz Jul 21, 2026
84abddb
Share DeferDeleteCacheDirectory call
jjonescz Jul 21, 2026
06488a7
Improve how default TFM is specified
jjonescz Jul 22, 2026
be34fb2
Improve code
jjonescz Jul 22, 2026
b27925e
Avoid hang in self-referencing test
jjonescz Jul 22, 2026
3ade14a
Improve compilation references in a test
jjonescz Jul 22, 2026
75a1f82
Move ProjectRootElement cache outside VirtualProjectBuilder
jjonescz Jul 22, 2026
ae74a50
Handle global properties
jjonescz Jul 22, 2026
29f58c4
Add comments
jjonescz Jul 27, 2026
74f01cf
Ensure MSBuildUtilities are still included
jjonescz Jul 27, 2026
51442eb
Get all item metadata values at once
jjonescz Jul 27, 2026
2d3aeaf
Check disposal like the other entrypoints
jjonescz Jul 27, 2026
54cf60f
Make IBuildService async
jjonescz Jul 27, 2026
6bc96ff
Clarify RPC API
jjonescz Jul 27, 2026
9943ae8
Add `ConfigureAwait(false)`
jjonescz Jul 27, 2026
6dc5b9e
Narrow data guard hold
jjonescz Jul 27, 2026
371a034
Test AssociateFileExtensionWithLanguage
jjonescz Jul 29, 2026
c9b40e0
Document file-based app support
jjonescz Jul 29, 2026
d7cfdbb
Update to a merged commit hash
jjonescz Jul 29, 2026
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
16 changes: 12 additions & 4 deletions eng/ensure-sources-synced.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
using System.Net.Http.Headers;
using System.Text.Json;

// Verifies or updates the shared source files under `src/Features/CSharp/Portable/SyncedSource/FileBasedPrograms`
// using files from dotnet/sdk at the commit specified in `src/Features/CSharp/Portable/SyncedSource/commitid.txt`.
// Verifies or updates the shared source files under `src/Workspaces/CSharp/Portable/SyncedSource/FileBasedPrograms`
// using files from dotnet/sdk at the commit specified in `src/Workspaces/CSharp/Portable/SyncedSource/commitid.txt`.
//
// Usage:
// dotnet run --file eng/ensure-sources-synced.cs
Expand All @@ -34,13 +34,13 @@ static async Task MainAsync(string[] args)

var mode = ParseMode(args);

var commitIdPath = Path.Combine(root, "src", "Features", "CSharp", "Portable", "SyncedSource", "commitid.txt");
var commitIdPath = Path.Combine(root, "src", "Workspaces", "CSharp", "Portable", "SyncedSource", "commitid.txt");
if (!File.Exists(commitIdPath)) throw new InvalidOperationException($"'{commitIdPath}' not found.");

var sdkCommit = File.ReadAllText(commitIdPath).Trim();
if (string.IsNullOrWhiteSpace(sdkCommit)) throw new InvalidOperationException($"'{commitIdPath}' is empty.");

var localSourceDir = Path.Combine(root, "src", "Features", "CSharp", "Portable", "SyncedSource", "FileBasedPrograms");
var localSourceDir = Path.Combine(root, "src", "Workspaces", "CSharp", "Portable", "SyncedSource", "FileBasedPrograms");

var httpClient = CreateHttpClient();

Expand All @@ -54,6 +54,13 @@ static async Task MainAsync(string[] args)
name.EndsWith(".resx", StringComparison.OrdinalIgnoreCase),
mapRelativePath: static name => name).ConfigureAwait(false);

var commonFiles = await GetDirectoryFilesAsync(
httpClient,
sdkCommit,
githubDirectoryPath: "src/Common",
includeFile: static name => string.Equals(name, "MSBuildUtilities.cs", StringComparison.OrdinalIgnoreCase),
mapRelativePath: static name => name).ConfigureAwait(false);

var editorConfigFiles = await GetDirectoryFilesAsync(
httpClient,
sdkCommit,
Expand All @@ -62,6 +69,7 @@ static async Task MainAsync(string[] args)
mapRelativePath: static _ => ".editorconfig").ConfigureAwait(false);

var sourcePackageFiles = sourceFiles
.Concat(commonFiles)
.Concat(editorConfigFiles)
.ToList();
if (sourcePackageFiles.Count == 0) throw new InvalidOperationException("No source files found in dotnet/sdk.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,6 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="CSharpFeaturesResources.resx" GenerateSource="true" />
<EmbeddedResource
Update="SyncedSource\FileBasedPrograms\FileBasedProgramsResources.resx"
GenerateSource="true"
Namespace="Microsoft.DotNet.FileBasedPrograms" />
</ItemGroup>
<ItemGroup>
<PublicAPI Include="PublicAPI.Shipped.txt" />
Expand All @@ -74,9 +70,6 @@
<ItemGroup>
<PackageReference Include="Humanizer.Core" PrivateAssets="compile" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);FILE_BASED_PROGRAMS_SOURCE_PACKAGE_GRACEFUL_EXCEPTION</DefineConstants>
</PropertyGroup>
<Import Project="..\..\..\Analyzers\CSharp\Analyzers\CSharpAnalyzers.projitems" Label="Shared" />
<Import Project="..\..\..\Analyzers\CSharp\CodeFixes\CSharpCodeFixes.projitems" Label="Shared" />
<Import Project="..\..\..\Compilers\CSharp\CSharpAnalyzerDriver\CSharpAnalyzerDriver.projitems" Label="Shared" />
Expand Down
1 change: 0 additions & 1 deletion src/Features/CSharp/Portable/SyncedSource/commitid.txt

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Basic.Reference.Assemblies.Net100" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp.Analyzer.Testing" />
</ItemGroup>

Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Collections.Immutable;
using System.Text;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.LanguageServer.FileBasedPrograms;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
Expand Down Expand Up @@ -62,6 +63,51 @@ public async Task TestFileBasedProgram_Simple(bool mutatingLspWorkspace)
Assert.Empty(syntaxTree.GetDiagnostics(CancellationToken.None));
}

[Theory, CombinatorialData]
public async Task TestFileBasedProgram_RefDirective(bool mutatingLspWorkspace)
{
await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer });

Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer));
var tempDir = CreateTempDirectoryWithGlobalJson();
tempDir.CreateFile("Util.cs").WriteAllText("""
#:property TargetFramework=net10.0
#:property OutputType=Library
public static class Util
{
public static string M() => "Util";
}
""");
var sourceText = """
#:property TargetFramework=net10.0
#:property ExperimentalFileBasedProgramEnableRefDirective=true
#:ref Util.cs
Console.WriteLine($"Hello {Util.M()}!");
""";
var sourceFile = tempDir.CreateFile("SomeFile.cs").WriteAllText(sourceText);

// Until we can discover the `#:ref`erenced project, build it so it works as metadata reference.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 I've added only basic support for #:ref.

  1. Support for translating #:ref -> <ProjectReference/> - this comes for free from the updated VirtualProjectBuilder that is source-synced here from dotnet/sdk repo.
  2. Support for loading those <ProjectReference/>s which point to virtual project - small changes only, e.g., in src\Workspaces\MSBuild\Core\MSBuild\MSBuildProjectLoader.Worker_ResolveReferences.cs.

This results in the project reference being loaded as metadata reference (not source reference), i.e., equivalent to having a reference to a project that's not part of the solution.
Better than nothing, but a follow-up PR (since it's orthogonal to this PR and would require more code changed) should probably improve on this to discover these projects and add them to the virtual solution.
Similarly, completion, classification, etc. is orthogonal as well and already being worked on in PR #83985.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm. Do you think discovery should parse directives when we have a positive entry point match and walk the #:refs? (not for this PR obviously.) That feels viable since we would only pay the cost after identifying the #!.

@jjonescz jjonescz Jul 29, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, some component should probably take the loaded Project instance and add all its ProjectReference items automatically to the solution. (It can only limit that to ProjectReference items that correspond to #:ref directives, those can be recognized via a special metadata item which the virtual project emits for them. #:project directives refer to normal projects, so users already have a way to make those part of the solution.) This component can be the discovery if it has access to the Project instance, or it could be just the ProjectSystem or something.

var dotnetCliHelper = testLspServer.GetRequiredLspService<DotnetCliHelper>();
using (var process = dotnetCliHelper.Run(["build", sourceFile.Path], workingDirectory: tempDir.Path, shouldLocalizeOutput: true))
{
var sb = new StringBuilder();
process.OutputDataReceived += (sender, args) => sb.AppendLine($"> {args.Data}");
process.ErrorDataReceived += (sender, args) => sb.AppendLine($"! {args.Data}");
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
Assert.True(process.ExitCode == 0, sb.ToString());
}

var looseFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(sourceFile.Path);
await testLspServer.OpenDocumentAsync(looseFileUri, sourceText).ConfigureAwait(false);
await WaitForProjectLoad(looseFileUri, testLspServer);
var (workspace, document) = await GetRequiredLspWorkspaceAndDocumentAsync(looseFileUri, testLspServer).ConfigureAwait(false);
Assert.NotEmpty(document.Project.MetadataReferences);
var model = await document.GetRequiredSemanticModelAsync(CancellationToken.None);
model.GetDiagnostics().Verify();
}

[Theory, CombinatorialData]
public async Task TestDirectiveWithoutTopLevelStatements_IsMiscellaneousFile(bool mutatingLspWorkspace)
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private async Task<ImmutableArray<ProjectFileInfo>> LoadCanonicalProjectAsync(Ca
binaryLogPathProvider: null,
loggerFactory: _loggerFactory);
var buildHost = await buildHostProcessManager.GetBuildHostAsync(BuildHostProcessKind.NetCore, virtualProjectPath, dotnetPath: null, cancellationToken);
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectXml, languageName: LanguageNames.CSharp, cancellationToken);
var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, physicalFilePath: null, virtualProjectXml, languageName: LanguageNames.CSharp, globalProperties: null, cancellationToken);
return await loadedFile.GetProjectFileInfosAsync(cancellationToken);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,21 @@
using System.Buffers;
using System.Collections.Immutable;
using System.Composition;
using System.Diagnostics;
using System.IO.Enumeration;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.CodeAnalysis.Collections;
using Microsoft.CodeAnalysis.ErrorReporting;
using Microsoft.CodeAnalysis.Features.Workspaces;
using Microsoft.CodeAnalysis.FileBasedPrograms;
using Microsoft.CodeAnalysis.Host;
using Microsoft.CodeAnalysis.Host.Mef;
using Microsoft.CodeAnalysis.LanguageServer.Handler;
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
using Microsoft.CodeAnalysis.Options;
using Microsoft.CodeAnalysis.PooledObjects;
using Microsoft.CodeAnalysis.Shared.TestHooks;
using Microsoft.CodeAnalysis.Shared.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.Extensions.Logging;
using Roslyn.LanguageServer.Protocol;
using Roslyn.Utilities;
Expand All @@ -35,12 +34,21 @@ internal sealed class FileBasedProgramsEntryPointDiscoveryFactory(IGlobalOptionS
{
public ILspService CreateILspService(LspServices lspServices, WellKnownLspServerKinds serverKind)
{
return new FileBasedProgramsEntryPointDiscovery(globalOptionService, listenerProvider.GetListener(FeatureAttribute.Workspace), lspServices.GetRequiredService<ILoggerFactory>(), lspServices);
return new FileBasedProgramsEntryPointDiscovery(
globalOptionService,
listenerProvider.GetListener(FeatureAttribute.Workspace),
lspServices.GetRequiredService<IHostWorkspaceProvider>().Workspace.Services.GetRequiredService<IFileBasedProgramService>(),
lspServices.GetRequiredService<ILoggerFactory>(),
lspServices);
}
}

internal sealed partial class FileBasedProgramsEntryPointDiscovery(
IGlobalOptionService globalOptionService, IAsynchronousOperationListener listener, ILoggerFactory loggerFactory, LspServices lspServices) : ILspService, IOnInitialized
IGlobalOptionService globalOptionService,
IAsynchronousOperationListener listener,
IFileBasedProgramService fileBasedProgramService,
ILoggerFactory loggerFactory,
LspServices lspServices) : ILspService, IOnInitialized
{
private static readonly StringComparer s_pathComparer = StringComparer.OrdinalIgnoreCase;

Expand Down Expand Up @@ -114,16 +122,16 @@ internal async Task FindAndLoadEntryPointsAsync()
// Discovery pass done. Find and delete old caches.
IOUtilities.PerformIO(() =>
{
using var enumerator = new OldCacheEnumerator();
using var enumerator = new OldCacheEnumerator(fileBasedProgramService);
while (enumerator.MoveNext())
{
IOUtilities.PerformIO(() => Directory.Delete(enumerator.Current, recursive: true));
}
});
}

private sealed class OldCacheEnumerator() : FileSystemEnumerator<string>(
directory: VirtualProjectXmlProvider.GetDiscoveryCacheRootDirectory(),
private sealed class OldCacheEnumerator(IFileBasedProgramService fileBasedProgramService) : FileSystemEnumerator<string>(
directory: fileBasedProgramService.GetDiscoveryCacheRootDirectory(),
options: new() { RecurseSubdirectories = false })
{
// Yield cache directories that have not been modified in 30 days (indicates they are stale and should be deleted)
Expand All @@ -140,7 +148,7 @@ protected override bool ShouldIncludeEntry(ref FileSystemEntry entry)
internal ImmutableArray<string> FindEntryPoints(string workspaceFolder)
{
var stopwatch = SharedStopwatch.StartNew();
var cacheDirectory = VirtualProjectXmlProvider.GetDiscoveryCacheDirectory(workspaceFolder);
var cacheDirectory = fileBasedProgramService.GetDiscoveryCacheDirectory(workspaceFolder);
var cacheFilePath = Path.Join(cacheDirectory, "cache.json");
Cache? cache = null;
try
Expand Down
Loading