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);