Skip to content

Commit 6150e84

Browse files
committed
File-based programs IDE support
1 parent 680ed2a commit 6150e84

File tree

13 files changed

+428
-57
lines changed

13 files changed

+428
-57
lines changed

.vscode/tasks.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@
169169
"type": "process",
170170
"options": {
171171
"env": {
172-
"DOTNET_ROSLYN_SERVER_PATH": "${workspaceRoot}/artifacts/bin/Microsoft.CodeAnalysis.LanguageServer/Debug/net8.0/Microsoft.CodeAnalysis.LanguageServer.dll"
172+
"DOTNET_ROSLYN_SERVER_PATH": "${workspaceRoot}/artifacts/bin/Microsoft.CodeAnalysis.LanguageServer/Debug/net9.0/Microsoft.CodeAnalysis.LanguageServer.dll"
173173
}
174174
},
175175
"dependsOn": [ "build language server" ]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# File-based programs VS Code support
2+
3+
See also [dotnet-run-file.md](https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md).
4+
5+
## Feature overview
6+
7+
A file-based program embeds a subset of MSBuild project capabilities into C# code, allowing single files to stand alone as ordinary projects.
8+
9+
The following is a file-based program:
10+
11+
```cs
12+
Console.WriteLine("Hello World!");
13+
```
14+
15+
So is the following:
16+
17+
```cs
18+
#!/usr/bin/env dotnet run
19+
#:sdk Microsoft.Net.Sdk
20+
#:package Newtonsoft.Json@13.0.3
21+
#:property LangVersion=preview
22+
23+
using Newtonsoft.Json;
24+
25+
Main();
26+
27+
void Main()
28+
{
29+
if (args is not [_, var jsonPath, ..])
30+
{
31+
Console.Error.WriteLine("Usage: app <json-file>");
32+
return;
33+
}
34+
35+
var json = File.ReadAllText(jsonPath);
36+
var data = JsonConvert.DeserializeObject<Data>(json);
37+
// ...
38+
}
39+
40+
record Data(string field1, int field2);
41+
```
42+
43+
This basically works by having the `dotnet` command line interpret the `#:` directives found at the top (and only at the top) of source files, produce a C# project XML document in memory, and pass it off to MSBuild. The in-memory project is sometimes called a "virtual project".
44+
45+
## Miscellaneous files changes
46+
47+
There is a long-standing backlog item to enhance the experience of working with miscellaneous files ("loose files" not associated with any project). We think that as part of the "file-based program" work, we can enable the following in such files without substantial issues:
48+
- Syntax diagnostics.
49+
- Intellisense for the "default" set of references. e.g. those references which are included in the project created by `dotnet new console` with the current SDK.
50+
51+
## High-level IDE layer changes
52+
53+
The following outline is for "single-file" scenarios. We are interested in expanding to multi-file as well, and this document will be expanded in the future with additional explanation of how to handle that scenario.
54+
55+
### Prerequisite work
56+
- MSBuildHost is updated to allow passing project content along with a file path. (https://github.com/dotnet/roslyn/pull/78303)
57+
- LanguageServerProjectSystem has a base type extracted so that a FileBasedProgramProjectSystem can be added. The latter handles loading file-based program projects, and makes the appropriate call to obtain the file-based program project XML. (https://github.com/dotnet/roslyn/pull/78329)
58+
59+
### Heuristic
60+
The IDE considers a file to be a file-based program, if:
61+
- It has a `#!` directive such as `#!/usr/bin/env dotnet run`, or,
62+
- It has any `#:` directives which configure the file-based program project, or,
63+
- It has any top-level statements.
64+
- Any of the above is met, and, the file is not included in an ordinary `.csproj` project (i.e. it is not part of any ordinary project's list of `Compile` items).
65+
66+
### Opt-out
67+
68+
Before we ship this feature in any stable release, we want to ensure an opt-out flag is in place. Keeping the existing `LspMiscellaneousFilesWorkspace` may be a convenient point of control for this--if user has enabled the opt-out flag, just use the old workspace. Otherwise, use the new one.

src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/WorkspaceProjectFactoryServiceTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public async Task CreateProjectAndBatch()
4848
await batch.ApplyAsync(CancellationToken.None);
4949

5050
// Verify it actually did something; we won't exclusively test each method since those are tested at lower layers
51-
var project = workspaceFactory.Workspace.CurrentSolution.Projects.Single();
51+
var project = workspaceFactory.HostWorkspace.CurrentSolution.Projects.Single();
5252

5353
var document = Assert.Single(project.Documents);
5454
Assert.Equal(sourceFilePath, document.FilePath);
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Collections.Immutable;
6+
using System.Security;
7+
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
8+
using Microsoft.CodeAnalysis.MetadataAsSource;
9+
using Microsoft.CodeAnalysis.MSBuild;
10+
using Microsoft.CodeAnalysis.Options;
11+
using Microsoft.CodeAnalysis.ProjectSystem;
12+
using Microsoft.CodeAnalysis.Shared.Extensions;
13+
using Microsoft.CodeAnalysis.Shared.TestHooks;
14+
using Microsoft.CodeAnalysis.Text;
15+
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem;
16+
using Microsoft.CommonLanguageServerProtocol.Framework;
17+
using Microsoft.Extensions.Logging;
18+
using Microsoft.VisualStudio.Composition;
19+
using Roslyn.Utilities;
20+
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager;
21+
22+
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
23+
24+
internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider
25+
{
26+
private readonly ILspServices _lspServices;
27+
private readonly ILogger<FileBasedProgramsProjectSystem> _logger;
28+
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService;
29+
30+
public FileBasedProgramsProjectSystem(
31+
ILspServices lspServices,
32+
IMetadataAsSourceFileService metadataAsSourceFileService,
33+
LanguageServerWorkspaceFactory workspaceFactory,
34+
IFileChangeWatcher fileChangeWatcher,
35+
IGlobalOptionService globalOptionService,
36+
ILoggerFactory loggerFactory,
37+
IAsynchronousOperationListenerProvider listenerProvider,
38+
ProjectLoadTelemetryReporter projectLoadTelemetry,
39+
ServerConfigurationFactory serverConfigurationFactory,
40+
BinlogNamer binlogNamer)
41+
: base(
42+
workspaceFactory.FileBasedProgramsProjectFactory,
43+
workspaceFactory.TargetFrameworkManager,
44+
workspaceFactory.ProjectSystemHostInfo,
45+
fileChangeWatcher,
46+
globalOptionService,
47+
loggerFactory,
48+
listenerProvider,
49+
projectLoadTelemetry,
50+
serverConfigurationFactory,
51+
binlogNamer)
52+
{
53+
_lspServices = lspServices;
54+
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>();
55+
_metadataAsSourceFileService = metadataAsSourceFileService;
56+
}
57+
58+
public Workspace Workspace => ProjectFactory.Workspace;
59+
60+
public async Task<TextDocument?> AddMiscellaneousDocumentAsync(Uri uri, SourceText documentText, string languageId, ILspLogger logger)
61+
{
62+
var documentPath = ProtocolConversions.GetDocumentFilePathFromUri(uri);
63+
var container = documentText.Container;
64+
if (_metadataAsSourceFileService.TryAddDocumentToWorkspace(documentPath, container, out var documentId))
65+
{
66+
var metadataWorkspace = _metadataAsSourceFileService.TryGetWorkspace();
67+
Contract.ThrowIfNull(metadataWorkspace);
68+
var metadataDoc = metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId);
69+
return metadataDoc;
70+
}
71+
72+
var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>();
73+
if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation))
74+
{
75+
// Only log here since throwing here could take down the LSP server.
76+
logger.LogError($"Could not find language information for {uri} with absolute path {documentPath}");
77+
return null;
78+
}
79+
80+
// For Razor files we need to override the language name to C# as thats what code is generated
81+
var isRazor = languageInformation.LanguageName == "Razor";
82+
var languageName = isRazor ? LanguageNames.CSharp : languageInformation.LanguageName;
83+
var loadedProject = await CreateAndTrackInitialProjectAsync(documentPath, language: languageName);
84+
var documentFileInfo = new DocumentFileInfo(documentPath, logicalPath: documentPath, isLinked: false, isGenerated: false, folders: default);
85+
var projectFileInfo = new ProjectFileInfo()
86+
{
87+
Language = languageName,
88+
FilePath = uri.IsFile ? VirtualProject.GetVirtualProjectPath(documentPath) : documentPath,
89+
CommandLineArgs = uri.IsFile ? ["/langversion:preview", "/features:FileBasedProgram=true"] : ["/langversion:preview"],
90+
Documents = isRazor ? [] : [documentFileInfo],
91+
AdditionalDocuments = isRazor ? [documentFileInfo] : [],
92+
AnalyzerConfigDocuments = [],
93+
ProjectReferences = [],
94+
PackageReferences = [],
95+
ProjectCapabilities = [],
96+
ContentFilePaths = [],
97+
FileGlobs = []
98+
};
99+
await loadedProject.UpdateWithNewProjectInfoAsync(projectFileInfo, _logger);
100+
var workspaceProject = ProjectFactory.Workspace.CurrentSolution.GetRequiredProject(loadedProject.ProjectId);
101+
var document = isRazor ? workspaceProject.AdditionalDocuments.Single() : workspaceProject.Documents.Single();
102+
103+
if (uri.IsFile && languageInformation.LanguageName == LanguageNames.CSharp)
104+
{
105+
// light up the proper file-based program experience.
106+
ProjectsToLoadAndReload.AddWork(new ProjectToLoad(documentPath, ProjectGuid: null, ReportTelemetry: true));
107+
loadedProject.NeedsReload += (_, _) => ProjectsToLoadAndReload.AddWork(new ProjectToLoad(documentPath, ProjectGuid: null, ReportTelemetry: false));
108+
109+
_ = Task.Run(async () =>
110+
{
111+
await ProjectsToLoadAndReload.WaitUntilCurrentBatchCompletesAsync();
112+
await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync();
113+
});
114+
}
115+
116+
Contract.ThrowIfFalse(document.FilePath == documentPath);
117+
return document;
118+
}
119+
120+
public void TryRemoveMiscellaneousDocument(Uri uri, bool removeFromMetadataWorkspace)
121+
{
122+
// support unloading
123+
}
124+
125+
protected override async Task<(RemoteProjectFile? projectFile, BuildHostProcessKind preferred, BuildHostProcessKind actual)> TryLoadProjectAsync(
126+
BuildHostProcessManager buildHostProcessManager, ProjectToLoad projectToLoad, CancellationToken cancellationToken)
127+
{
128+
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore;
129+
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken);
130+
var documentPath = projectToLoad.Path;
131+
Contract.ThrowIfFalse(Path.GetExtension(documentPath) == ".cs");
132+
133+
var fakeProjectPath = VirtualProject.GetVirtualProjectPath(documentPath);
134+
var contentToLoad = VirtualProject.MakeVirtualProjectContent(documentPath);
135+
136+
var loadedFile = await buildHost.LoadProjectAsync(fakeProjectPath, contentToLoad, languageName: LanguageNames.CSharp, cancellationToken);
137+
return (loadedFile, preferred: buildHostKind, actual: buildHostKind);
138+
}
139+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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+
// See the LICENSE file in the project root for more information.
4+
5+
using System.Composition;
6+
using Microsoft.CodeAnalysis.Host;
7+
using Microsoft.CodeAnalysis.Host.Mef;
8+
using Microsoft.CodeAnalysis.LanguageServer.Handler;
9+
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry;
10+
using Microsoft.CodeAnalysis.MetadataAsSource;
11+
using Microsoft.CodeAnalysis.Options;
12+
using Microsoft.CodeAnalysis.ProjectSystem;
13+
using Microsoft.CodeAnalysis.Shared.TestHooks;
14+
using Microsoft.CommonLanguageServerProtocol.Framework;
15+
using Microsoft.Extensions.Logging;
16+
17+
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace;
18+
19+
/// <summary>
20+
/// Service to create <see cref="LspMiscellaneousFilesWorkspaceProvider"/> instances.
21+
/// This is not exported as a <see cref="ILspServiceFactory"/> as it requires
22+
/// special base language server dependencies such as the <see cref="HostServices"/>
23+
/// </summary>
24+
[ExportCSharpVisualBasicStatelessLspService(typeof(ILspMiscellaneousFilesWorkspaceProviderFactory), WellKnownLspServerKinds.CSharpVisualBasicLspServer), Shared]
25+
[method: ImportingConstructor]
26+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
27+
internal sealed class FileBasedProgramsWorkspaceProviderFactory(
28+
IMetadataAsSourceFileService metadataAsSourceFileService,
29+
LanguageServerWorkspaceFactory workspaceFactory,
30+
IFileChangeWatcher fileChangeWatcher,
31+
IGlobalOptionService globalOptionService,
32+
ILoggerFactory loggerFactory,
33+
IAsynchronousOperationListenerProvider listenerProvider,
34+
ProjectLoadTelemetryReporter projectLoadTelemetry,
35+
ServerConfigurationFactory serverConfigurationFactory,
36+
BinlogNamer binlogNamer) : ILspMiscellaneousFilesWorkspaceProviderFactory
37+
{
38+
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices)
39+
{
40+
// TODO(fbp): check feature flag and maybe give base misc workspace
41+
return new FileBasedProgramsProjectSystem(lspServices, metadataAsSourceFileService, workspaceFactory, fileChangeWatcher, globalOptionService, loggerFactory, listenerProvider, projectLoadTelemetry, serverConfigurationFactory, binlogNamer);
42+
}
43+
}

0 commit comments

Comments
 (0)