-
Notifications
You must be signed in to change notification settings - Fork 4.1k
File based programs IDE support #78488
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
b4fae68
32345c5
264c182
497cac6
4244329
23d702b
d61f603
6b410cc
e568dd3
b3d056d
9d80851
9f1d904
1daef89
86cdb92
06f08d4
a00fbd1
46b733a
aea78ca
9bcf18c
3db218c
7700246
2c8cce6
f3016bc
21c4e0e
b7b822d
d5691f0
c935b95
423e09c
913a882
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
# File-based programs VS Code support | ||
|
||
See also [dotnet-run-file.md](https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md). | ||
|
||
## Feature overview | ||
|
||
A file-based program embeds a subset of MSBuild project capabilities into C# code, allowing single files to stand alone as ordinary projects. | ||
|
||
The following is a file-based program: | ||
|
||
```cs | ||
Console.WriteLine("Hello World!"); | ||
``` | ||
|
||
So is the following: | ||
|
||
```cs | ||
#!/usr/bin/env dotnet run | ||
#:sdk Microsoft.Net.Sdk | ||
#:package [email protected] | ||
#:property LangVersion=preview | ||
|
||
using Newtonsoft.Json; | ||
|
||
Main(); | ||
|
||
void Main() | ||
{ | ||
if (args is not [_, var jsonPath, ..]) | ||
{ | ||
Console.Error.WriteLine("Usage: app <json-file>"); | ||
return; | ||
} | ||
|
||
var json = File.ReadAllText(jsonPath); | ||
var data = JsonConvert.DeserializeObject<Data>(json); | ||
// ... | ||
} | ||
|
||
record Data(string field1, int field2); | ||
``` | ||
|
||
This basically works by having the `dotnet` command line interpret the `#:` directives in 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". | ||
|
||
## Miscellaneous files changes | ||
|
||
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: | ||
- Syntax diagnostics. | ||
- 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. | ||
|
||
## High-level IDE layer changes | ||
|
||
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. | ||
|
||
### Prerequisite work | ||
- MSBuildHost is updated to allow passing project content along with a file path. (https://github.com/dotnet/roslyn/pull/78303) | ||
- 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) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Heuristic | ||
The IDE considers a file to be a file-based program, if: | ||
- It has any `#:` directives which configure the file-based program project, or, | ||
- It has any top-level statements. | ||
- 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). | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
### Opt-out | ||
|
||
We added an opt-out flag with option name `dotnet.projects.enableFileBasedPrograms`. If issues arise with the file-based program experience, then VS Code users should set the corresponding setting `"dotnet.projects.enableFileBasedPrograms": false` to revert back to the old miscellaneous files experience. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @dibarbet With this PR how do you feel about this being set to true or false by default? Do we have an easy way to default based on prerelease/release channels? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally fine with defaulting to true in prerelease. If we are able to get it in for a few prereleases, also fine with it defaulting to true in release. We can change the default per branch by changing the value in the package.json in the corresponding branch. Other than that the only other way would be to use a feature rollout (control tower). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Collections.Immutable; | ||
using System.Security; | ||
using Microsoft.CodeAnalysis.Features.Workspaces; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; | ||
using Microsoft.CodeAnalysis.MetadataAsSource; | ||
using Microsoft.CodeAnalysis.MSBuild; | ||
using Microsoft.CodeAnalysis.Options; | ||
using Microsoft.CodeAnalysis.ProjectSystem; | ||
using Microsoft.CodeAnalysis.Shared.Extensions; | ||
using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
using Microsoft.CodeAnalysis.Text; | ||
using Microsoft.CodeAnalysis.Workspaces.ProjectSystem; | ||
using Microsoft.CommonLanguageServerProtocol.Framework; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.VisualStudio.Composition; | ||
using Roslyn.LanguageServer.Protocol; | ||
using Roslyn.Utilities; | ||
using static Microsoft.CodeAnalysis.MSBuild.BuildHostProcessManager; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; | ||
|
||
internal sealed class FileBasedProgramsProjectSystem : LanguageServerProjectLoader, ILspMiscellaneousFilesWorkspaceProvider | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
private readonly ILspServices _lspServices; | ||
private readonly ILogger<FileBasedProgramsProjectSystem> _logger; | ||
private readonly IMetadataAsSourceFileService _metadataAsSourceFileService; | ||
|
||
public FileBasedProgramsProjectSystem( | ||
ILspServices lspServices, | ||
IMetadataAsSourceFileService metadataAsSourceFileService, | ||
LanguageServerWorkspaceFactory workspaceFactory, | ||
IFileChangeWatcher fileChangeWatcher, | ||
IGlobalOptionService globalOptionService, | ||
ILoggerFactory loggerFactory, | ||
IAsynchronousOperationListenerProvider listenerProvider, | ||
ProjectLoadTelemetryReporter projectLoadTelemetry, | ||
ServerConfigurationFactory serverConfigurationFactory, | ||
BinlogNamer binlogNamer) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
: base( | ||
workspaceFactory.FileBasedProgramsProjectFactory, | ||
workspaceFactory.TargetFrameworkManager, | ||
workspaceFactory.ProjectSystemHostInfo, | ||
fileChangeWatcher, | ||
globalOptionService, | ||
loggerFactory, | ||
listenerProvider, | ||
projectLoadTelemetry, | ||
serverConfigurationFactory, | ||
binlogNamer) | ||
{ | ||
_lspServices = lspServices; | ||
_logger = loggerFactory.CreateLogger<FileBasedProgramsProjectSystem>(); | ||
_metadataAsSourceFileService = metadataAsSourceFileService; | ||
} | ||
|
||
public Workspace Workspace => ProjectFactory.Workspace; | ||
|
||
public async Task<TextDocument?> AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) | ||
{ | ||
var (documentPath, isFile) = uri.ParsedUri is { } parsedUri | ||
? (ProtocolConversions.GetDocumentFilePathFromUri(parsedUri), isFile: parsedUri.IsFile) | ||
: (uri.UriString, isFile: false); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var container = documentText.Container; | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (_metadataAsSourceFileService.TryAddDocumentToWorkspace(documentPath, container, out var documentId)) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var metadataWorkspace = _metadataAsSourceFileService.TryGetWorkspace(); | ||
Contract.ThrowIfNull(metadataWorkspace); | ||
var metadataDoc = metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId); | ||
return metadataDoc; | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>(); | ||
if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) | ||
{ | ||
// Only log here since throwing here could take down the LSP server. | ||
logger.LogError($"Could not find language information for {uri} with absolute path {documentPath}"); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return null; | ||
} | ||
|
||
if (!isFile || !GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms)) | ||
{ | ||
// For now, we cannot provide intellisense etc on files which are not on disk or are not C#. | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var sourceTextLoader = new SourceTextLoader(documentText, documentPath); | ||
var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( | ||
Workspace, documentPath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, Workspace.CurrentSolution.Services, []); | ||
await ProjectFactory.ApplyChangeToWorkspaceAsync(ws => ws.OnProjectAdded(projectInfo), cancellationToken: default); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var newSolution = Workspace.CurrentSolution; | ||
if (languageInformation.LanguageName == "Razor") | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var docId = projectInfo.AdditionalDocuments.Single().Id; | ||
return newSolution.GetRequiredAdditionalDocument(docId); | ||
} | ||
|
||
var id = projectInfo.Documents.Single().Id; | ||
return newSolution.GetRequiredDocument(id); | ||
} | ||
|
||
// We have a file on disk. Light up the file-based program experience. | ||
// For Razor files we need to override the language name to C# as that's what code is generated | ||
var isRazor = languageInformation.LanguageName == "Razor"; | ||
var languageName = isRazor ? LanguageNames.CSharp : languageInformation.LanguageName; | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var documentFileInfo = new DocumentFileInfo(documentPath, logicalPath: documentPath, isLinked: false, isGenerated: false, folders: default); | ||
var projectFileInfo = new ProjectFileInfo() | ||
{ | ||
Language = languageName, | ||
FilePath = VirtualProject.GetVirtualProjectPath(documentPath), | ||
CommandLineArgs = ["/langversion:preview", "/features:FileBasedProgram=true"], | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Documents = isRazor ? [] : [documentFileInfo], | ||
AdditionalDocuments = isRazor ? [documentFileInfo] : [], | ||
AnalyzerConfigDocuments = [], | ||
ProjectReferences = [], | ||
PackageReferences = [], | ||
ProjectCapabilities = [], | ||
ContentFilePaths = [], | ||
FileGlobs = [] | ||
}; | ||
|
||
var projectSet = AddLoadedProjectSet(documentPath); | ||
Project workspaceProject; | ||
using (await projectSet.Semaphore.DisposableWaitAsync()) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
var loadedProject = await this.CreateAndTrackInitialProjectAsync_NoLock(projectSet, documentPath, language: languageName); | ||
await loadedProject.UpdateWithNewProjectInfoAsync(projectFileInfo, hasAllInformation: false, _logger); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
ProjectsToLoadAndReload.AddWork(new ProjectToLoad(documentPath, ProjectGuid: null, ReportTelemetry: true)); | ||
loadedProject.NeedsReload += (_, _) => ProjectsToLoadAndReload.AddWork(new ProjectToLoad(documentPath, ProjectGuid: null, ReportTelemetry: false)); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
workspaceProject = ProjectFactory.Workspace.CurrentSolution.GetRequiredProject(loadedProject.ProjectId); | ||
} | ||
|
||
var document = isRazor ? workspaceProject.AdditionalDocuments.Single() : workspaceProject.Documents.Single(); | ||
|
||
_ = Task.Run(async () => | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
await ProjectsToLoadAndReload.WaitUntilCurrentBatchCompletesAsync(); | ||
await ProjectInitializationHandler.SendProjectInitializationCompleteNotificationAsync(); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}); | ||
|
||
Contract.ThrowIfFalse(document.FilePath == documentPath); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return document; | ||
} | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
public async ValueTask TryRemoveMiscellaneousDocumentAsync(DocumentUri uri, bool removeFromMetadataWorkspace) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var documentPath = uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString; | ||
await TryUnloadProjectSetAsync(documentPath); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// also do an unload in case this was the non-file scenario | ||
if (removeFromMetadataWorkspace && uri.ParsedUri is not null && _metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(ProtocolConversions.GetDocumentFilePathFromUri(uri.ParsedUri))) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
return; | ||
} | ||
|
||
var matchingDocument = Workspace.CurrentSolution.GetDocumentIds(uri).SingleOrDefault(); | ||
if (matchingDocument != null) | ||
{ | ||
var project = Workspace.CurrentSolution.GetRequiredProject(matchingDocument.ProjectId); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Workspace.OnProjectRemoved(project.Id); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
} | ||
|
||
protected override async Task<(RemoteProjectFile? projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)> TryLoadProjectAsync( | ||
BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
const BuildHostProcessKind buildHostKind = BuildHostProcessKind.NetCore; | ||
var buildHost = await buildHostProcessManager.GetBuildHostAsync(buildHostKind, cancellationToken); | ||
Contract.ThrowIfFalse(Path.GetExtension(documentPath) == ".cs"); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
var fakeProjectPath = VirtualProject.GetVirtualProjectPath(documentPath); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var loader = ProjectFactory.CreateFileTextLoader(documentPath); | ||
var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken: default); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
var (contentToLoad, isFileBasedProgram) = VirtualProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text); | ||
|
||
var loadedFile = await buildHost.LoadProjectAsync(fakeProjectPath, contentToLoad, languageName: LanguageNames.CSharp, cancellationToken); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return (loadedFile, hasAllInformation: isFileBasedProgram, preferred: buildHostKind, actual: buildHostKind); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
// Licensed to the .NET Foundation under one or more agreements. | ||
// The .NET Foundation licenses this file to you under the MIT license. | ||
// See the LICENSE file in the project root for more information. | ||
|
||
using System.Composition; | ||
using Microsoft.CodeAnalysis.Host; | ||
using Microsoft.CodeAnalysis.Host.Mef; | ||
using Microsoft.CodeAnalysis.LanguageServer.Handler; | ||
using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace.ProjectTelemetry; | ||
using Microsoft.CodeAnalysis.MetadataAsSource; | ||
using Microsoft.CodeAnalysis.Options; | ||
using Microsoft.CodeAnalysis.ProjectSystem; | ||
using Microsoft.CodeAnalysis.Shared.TestHooks; | ||
using Microsoft.CommonLanguageServerProtocol.Framework; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; | ||
|
||
/// <summary> | ||
/// Service to create <see cref="LspMiscellaneousFilesWorkspaceProvider"/> instances. | ||
/// This is not exported as a <see cref="ILspServiceFactory"/> as it requires | ||
/// special base language server dependencies such as the <see cref="HostServices"/> | ||
/// </summary> | ||
[ExportCSharpVisualBasicStatelessLspService(typeof(ILspMiscellaneousFilesWorkspaceProviderFactory), WellKnownLspServerKinds.CSharpVisualBasicLspServer), Shared] | ||
[method: ImportingConstructor] | ||
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)] | ||
internal sealed class FileBasedProgramsWorkspaceProviderFactory( | ||
IMetadataAsSourceFileService metadataAsSourceFileService, | ||
LanguageServerWorkspaceFactory workspaceFactory, | ||
IFileChangeWatcher fileChangeWatcher, | ||
IGlobalOptionService globalOptionService, | ||
ILoggerFactory loggerFactory, | ||
IAsynchronousOperationListenerProvider listenerProvider, | ||
ProjectLoadTelemetryReporter projectLoadTelemetry, | ||
ServerConfigurationFactory serverConfigurationFactory, | ||
BinlogNamer binlogNamer) : ILspMiscellaneousFilesWorkspaceProviderFactory | ||
{ | ||
public ILspMiscellaneousFilesWorkspaceProvider CreateLspMiscellaneousFilesWorkspaceProvider(ILspServices lspServices, HostServices hostServices) | ||
{ | ||
return new FileBasedProgramsProjectSystem(lspServices, metadataAsSourceFileService, workspaceFactory, fileChangeWatcher, globalOptionService, loggerFactory, listenerProvider, projectLoadTelemetry, serverConfigurationFactory, binlogNamer); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.