-
Notifications
You must be signed in to change notification settings - Fork 4.2k
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 all 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,59 @@ | ||
| # 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 Newtonsoft.Json@13.0.3 | ||
| #: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. | ||
|
|
||
| ### 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). | ||
|
|
||
| ### 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. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -46,6 +46,12 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( | |
| compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath); | ||
| } | ||
|
|
||
| if (parseOptions != null && fileExtension != languageInformation.ScriptExtension) | ||
| { | ||
| // Any non-script misc file should not complain about usage of '#:' ignored directives. | ||
| parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]); | ||
|
Comment on lines
+51
to
+52
Member
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. This should probably move into the CSharpSyntaxTreeFactoryService's GetDefaultParseOptionsWithLatestLanguageVersion(). That's also used by metadata as source, which if that also gets used for Source Link, we'd want this since we don't want errors if a source linked file had this too. (As it stands this would also be setting this for VB, which may not be ideal.)
Member
Author
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. I think setting it for VB doesn't affect anything. It is just not recognized/used by the VB compiler. It also seems like no diagnostics are expected for MetadataAsSource or SourceLink documents, since diagnostics are not actionable in those contexts. I am happy to make the suggested change, though. I will plan to do it in a follow-up because we are almost thru CI now. |
||
| } | ||
|
|
||
| var projectId = ProjectId.CreateNewId(debugName: $"{workspace.GetType().Name} Files Project for {filePath}"); | ||
| var documentId = DocumentId.CreateNewId(projectId, debugName: filePath); | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| // 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; | ||
|
|
||
| /// <summary>Handles loading both miscellaneous files and file-based program projects.</summary> | ||
| 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; | ||
|
|
||
| private string GetDocumentFilePath(DocumentUri uri) => uri.ParsedUri is { } parsedUri ? ProtocolConversions.GetDocumentFilePathFromUri(parsedUri) : uri.UriString; | ||
|
|
||
| public async ValueTask<TextDocument?> AddMiscellaneousDocumentAsync(DocumentUri uri, SourceText documentText, string languageId, ILspLogger logger) | ||
| { | ||
| var documentFilePath = GetDocumentFilePath(uri); | ||
|
|
||
| // https://github.com/dotnet/roslyn/issues/78421: MetadataAsSource should be its own workspace | ||
| if (_metadataAsSourceFileService.TryAddDocumentToWorkspace(documentFilePath, documentText.Container, out var documentId)) | ||
| { | ||
| var metadataWorkspace = _metadataAsSourceFileService.TryGetWorkspace(); | ||
| Contract.ThrowIfNull(metadataWorkspace); | ||
| return metadataWorkspace.CurrentSolution.GetRequiredDocument(documentId); | ||
| } | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| var primordialDoc = AddPrimordialDocument(uri, documentText, languageId); | ||
| Contract.ThrowIfNull(primordialDoc.FilePath); | ||
|
|
||
| var doDesignTimeBuild = uri.ParsedUri?.IsFile is true | ||
| && primordialDoc.Project.Language == LanguageNames.CSharp | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| && GlobalOptionService.GetOption(LanguageServerProjectSystemOptionsStorage.EnableFileBasedPrograms); | ||
| await BeginLoadingProjectWithPrimordialAsync(primordialDoc.FilePath, primordialProjectId: primordialDoc.Project.Id, doDesignTimeBuild); | ||
|
|
||
| return primordialDoc; | ||
|
|
||
| TextDocument AddPrimordialDocument(DocumentUri uri, SourceText documentText, string languageId) | ||
| { | ||
| var languageInfoProvider = _lspServices.GetRequiredService<ILanguageInfoProvider>(); | ||
| if (!languageInfoProvider.TryGetLanguageInformation(uri, languageId, out var languageInformation)) | ||
| { | ||
| Contract.Fail($"Could not find language information for {uri} with absolute path {documentFilePath}"); | ||
RikkiGibson marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| var workspace = Workspace; | ||
| var sourceTextLoader = new SourceTextLoader(documentText, documentFilePath); | ||
| var projectInfo = MiscellaneousFileUtilities.CreateMiscellaneousProjectInfoForDocument( | ||
| workspace, documentFilePath, sourceTextLoader, languageInformation, documentText.ChecksumAlgorithm, workspace.Services.SolutionServices, []); | ||
|
|
||
| ProjectFactory.ApplyChangeToWorkspace(workspace => workspace.OnProjectAdded(projectInfo)); | ||
|
|
||
| // https://github.com/dotnet/roslyn/pull/78267 | ||
| // Work around an issue where opening a Razor file in the misc workspace causes a crash. | ||
|
Comment on lines
+103
to
+104
Member
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. Not sure why this comment is here -- it's not a workaround...it's how the razor code works...?
Member
Author
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. This was motivated by #78488 (comment).
Member
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. Potentially what we should do in the future is map language information to kinds of documents explicitly. But that shouldn't be part of this PR. |
||
| if (languageInformation.LanguageName == LanguageInfoProvider.RazorLanguageName) | ||
| { | ||
| var docId = projectInfo.AdditionalDocuments.Single().Id; | ||
| return workspace.CurrentSolution.GetRequiredAdditionalDocument(docId); | ||
| } | ||
|
|
||
| var id = projectInfo.Documents.Single().Id; | ||
| return workspace.CurrentSolution.GetRequiredDocument(id); | ||
| } | ||
| } | ||
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 = GetDocumentFilePath(uri); | ||
| if (removeFromMetadataWorkspace && _metadataAsSourceFileService.TryRemoveDocumentFromWorkspace(documentPath)) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| await UnloadProjectAsync(documentPath); | ||
| } | ||
|
|
||
| protected override async Task<(RemoteProjectFile projectFile, bool hasAllInformation, BuildHostProcessKind preferred, BuildHostProcessKind actual)?> TryLoadProjectInMSBuildHostAsync( | ||
| 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); | ||
|
|
||
| var loader = ProjectFactory.CreateFileTextLoader(documentPath); | ||
| var textAndVersion = await loader.LoadTextAsync(new LoadTextOptions(SourceHashAlgorithms.Default), cancellationToken); | ||
| var (virtualProjectContent, isFileBasedProgram) = VirtualCSharpFileBasedProgramProject.MakeVirtualProjectContent(documentPath, textAndVersion.Text); | ||
|
|
||
| // When loading a virtual project, the path to the on-disk source file is not used. Instead the path is adjusted to end with .csproj. | ||
| // This is necessary in order to get msbuild to apply the standard c# props/targets to the project. | ||
| var virtualProjectPath = VirtualCSharpFileBasedProgramProject.GetVirtualProjectPath(documentPath); | ||
| var loadedFile = await buildHost.LoadProjectAsync(virtualProjectPath, virtualProjectContent, languageName: LanguageNames.CSharp, cancellationToken); | ||
|
Member
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. Definitely not asking for it in this PR - but we should followup on telemetry for file based projects. It might be tough to report on the server side, since server side telemetry is only sent when devkit is enabled. But we could send a notificiation to the client (/ update the existing project load telemetry) to include that. |
||
| 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); | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The 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 comment
The 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).