diff --git a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs index 74db428c43ba9..ecb5e4713889e 100644 --- a/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs +++ b/src/EditorFeatures/CSharpTest/Completion/CompletionProviders/AppDirectiveCompletionProviderTests.cs @@ -14,6 +14,7 @@ using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Test.Utilities; using Microsoft.CodeAnalysis.Text; +using Roslyn.Utilities; using Xunit; namespace Microsoft.CodeAnalysis.CSharp.UnitTests; @@ -100,6 +101,27 @@ public async Task PathRecommendation_02() await VerifyItemExistsAsync(markup, expectedItem: "Project.csproj"); } + [Fact] + public async Task PathRecommendation_03() + { + // Test a virtual file scenario (e.g. ctrl+N in VS Code or other cases where there is not an actual file on disk.) + var code = """ + #:project $$ + """; + + var markup = $""" + + + + + + """; + + // In this case, only stuff like drive roots would be recommended. + var expectedRoot = PlatformInformation.IsWindows ? "C:" : "/"; + await VerifyItemExistsAsync(markup, expectedRoot); + } + // Note: The editor uses a shared mechanism to filter out completion items which don't match the prefix of what the user is typing. // Therefore we do not have "negative tests" here for file names. } diff --git a/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/ProjectAppDirectiveCompletionProvider.cs b/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/ProjectAppDirectiveCompletionProvider.cs index 7627c8a5eb0d5..31a12e8b26bcf 100644 --- a/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/ProjectAppDirectiveCompletionProvider.cs +++ b/src/Features/CSharp/Portable/Completion/CompletionProviders/FileBasedPrograms/ProjectAppDirectiveCompletionProvider.cs @@ -45,11 +45,12 @@ protected override async Task AddDirectiveContentCompletionsAsync(CompletionCont // Suppose we have a directive '#:project path/to/pr$$' // In this case, 'contentPrefix' is 'path/to/pr'. + var documentDirectory = PathUtilities.GetDirectoryName(context.Document.FilePath); var fileSystemHelper = new FileSystemCompletionHelper( Glyph.OpenFolder, Glyph.CSharpProject, searchPaths: [], - baseDirectory: PathUtilities.GetDirectoryName(context.Document.FilePath), + baseDirectory: PathUtilities.IsAbsolute(documentDirectory) ? documentDirectory : null, [".csproj", ".vbproj"], CompletionItemRules.Default); diff --git a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs index 789b6e418b4a8..da99a0982af78 100644 --- a/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs +++ b/src/Features/Core/Portable/Workspace/MiscellaneousFileUtilities.cs @@ -45,19 +45,20 @@ internal static ProjectInfo CreateMiscellaneousProjectInfoForDocument( // https://devdiv.visualstudio.com/DevDiv/_workitems/edit/575761 var parseOptions = languageServices.GetService()?.GetDefaultParseOptionsWithLatestLanguageVersion(); - if (parseOptions != null && - compilationOptions != null && - languageInformation.ScriptExtension is not null && - fileExtension == languageInformation.ScriptExtension) + if (parseOptions != null) { - parseOptions = parseOptions.WithKind(SourceCodeKind.Script); - compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath); - } - - if (parseOptions != null && languageInformation.ScriptExtension is not 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")]); + if (compilationOptions != null && + languageInformation.ScriptExtension is not null && + fileExtension == languageInformation.ScriptExtension) + { + parseOptions = parseOptions.WithKind(SourceCodeKind.Script); + compilationOptions = GetCompilationOptionsWithScriptReferenceResolvers(services, compilationOptions, filePath); + } + else + { + // Any non-script misc file should not complain about usage of '#:' ignored directives. + parseOptions = parseOptions.WithFeatures([.. parseOptions.Features, new("FileBasedProgram", "true")]); + } } var projectId = ProjectId.CreateNewId(debugName: $"{workspace.GetType().Name} Files Project for {filePath}"); diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs index 8c45df7a2a1aa..ba62a390b7458 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer.UnitTests/FileBasedProgramsWorkspaceTests.cs @@ -4,7 +4,6 @@ using Microsoft.CodeAnalysis.LanguageServer.HostWorkspace; using Microsoft.CodeAnalysis.LanguageServer.UnitTests.Miscellaneous; -using Microsoft.CodeAnalysis.Options; using Microsoft.CodeAnalysis.Shared.Extensions; using Microsoft.CodeAnalysis.Shared.TestHooks; using Microsoft.CodeAnalysis.Test.Utilities; @@ -146,13 +145,9 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """ Assert.Equal(2, primordialDocument.Project.Documents.Count()); Assert.Empty(primordialDocument.Project.MetadataReferences); + // No errors for '#:' are expected. var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); - // TODO: we probably don't want to report syntax errors for '#:' in the primordial non-file document. - // The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted. - primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify( - // vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" - // #:sdk Microsoft.Net.Sdk - TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + Assert.Empty(primordialSyntaxTree.GetDiagnostics(CancellationToken.None)); // Wait for the canonical project to finish loading. await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); @@ -165,13 +160,47 @@ await testLspServer.OpenDocumentAsync(nonFileUri, """ // Should have the appropriate generated files now that we ran a design time build Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "Canonical.AssemblyInfo.cs"); + // No errors for '#:' are expected. var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); - // TODO: we probably don't want to report syntax errors for '#:' in the canonical non-file document. - // The logic which decides whether to add '-features:FileBasedProgram' probably needs to be adjusted. - canonicalSyntaxTree.GetDiagnostics(CancellationToken.None).Verify( - // vscode-notebook-cell://dev-container/test.cs(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" + Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None)); + } + + [Theory, CombinatorialData] + public async Task TestScriptsWithIgnoredDirectives(bool mutatingLspWorkspace) + { + // https://github.com/dotnet/roslyn/issues/81644: A csx script with '#:' directives should not be loaded as a file-based program + await using var testLspServer = await CreateTestLspServerAsync(string.Empty, mutatingLspWorkspace, new InitializationOptions { ServerKind = WellKnownLspServerKinds.CSharpVisualBasicLspServer }); + Assert.Null(await GetMiscellaneousDocumentAsync(testLspServer)); + + var nonFileUri = ProtocolConversions.CreateAbsoluteDocumentUri(@"C:\script.csx"); + await testLspServer.OpenDocumentAsync(nonFileUri, """ + #:sdk Microsoft.Net.Sdk + Console.WriteLine("Hello World"); + """).ConfigureAwait(false); + + // File is added to a miscellaneous project containing only the script. + var (_, primordialDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(1, primordialDocument.Project.Documents.Count()); + Assert.Empty(primordialDocument.Project.MetadataReferences); + + // FileBasedProgram feature flag is not passed, so an error is expected on '#:'. + var primordialSyntaxTree = await primordialDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); + primordialSyntaxTree.GetDiagnostics(CancellationToken.None).Verify( + // script.csx(1,2): error CS9298: '#:' directives can be only used in file-based programs ('-features:FileBasedProgram')" // #:sdk Microsoft.Net.Sdk TestHelpers.Diagnostic(code: 9298, squiggledText: ":").WithLocation(1, 2)); + + // Wait for project load to finish + await testLspServer.TestWorkspace.GetService().GetWaiter(FeatureAttribute.Workspace).ExpeditedWaitAsync(); + + var (miscWorkspace, canonicalDocument) = await GetRequiredLspWorkspaceAndDocumentAsync(nonFileUri, testLspServer).ConfigureAwait(false); + Assert.Equal(WorkspaceKind.Host, miscWorkspace.Kind); + Assert.NotNull(canonicalDocument); + Assert.NotEqual(primordialDocument, canonicalDocument); + Assert.Contains(canonicalDocument.Project.Documents, d => d.Name == "script.AssemblyInfo.cs"); + + var canonicalSyntaxTree = await canonicalDocument.GetRequiredSyntaxTreeAsync(CancellationToken.None); + Assert.Empty(canonicalSyntaxTree.GetDiagnostics(CancellationToken.None)); } [Theory, CombinatorialData] diff --git a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs index 18dafdc7e3ae0..1e77769b8b924 100644 --- a/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs +++ b/src/LanguageServer/Microsoft.CodeAnalysis.LanguageServer/FileBasedPrograms/CanonicalMiscFilesProjectLoader.cs @@ -244,12 +244,15 @@ private TextDocument CreatePrimordialProjectAndAddDocument_NoLock(string documen protected override async Task TryLoadProjectInMSBuildHostAsync( BuildHostProcessManager buildHostProcessManager, string documentPath, CancellationToken cancellationToken) { + // Set the FileBasedProgram feature flag so that '#:' is permitted without errors in rich misc files. + // This allows us to avoid spurious errors for files which contain '#:' directives yet are not treated as file-based programs (due to not being saved to disk, for example.) var virtualProjectXml = $""" net$(BundledNETCoreAppTargetFrameworkVersion) enable enable + $(Features);FileBasedProgram """; diff --git a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs index 1fb5f99d3a7cd..46a67fc2403dd 100644 --- a/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs +++ b/src/Workspaces/CoreTestUtilities/Workspaces/TestWorkspace_XmlConsumption.cs @@ -672,7 +672,8 @@ private TDocument CreateDocument( AssertEx.Fail($"The document attributes on file {fileName} conflicted"); } - var filePath = Path.Combine(TestWorkspace.RootDirectory, fileName); + var resolveFilePath = (bool?)documentElement.Attribute("ResolveFilePath"); + var filePath = resolveFilePath is null or true ? Path.Combine(TestWorkspace.RootDirectory, fileName) : fileName; return CreateDocument( exportProvider, languageServiceProvider, code, fileName, filePath, cursorPosition, spans, codeKind, folders, isLinkFile, documentServiceProvider);