Skip to content

Commit d17489e

Browse files
Initial spike for file-based programs editor features (#80410)
Co-authored-by: Joey Robichaud <[email protected]>
1 parent fbd9982 commit d17489e

22 files changed

+998
-1
lines changed
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
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;
6+
using System.Collections.Generic;
7+
using System.Linq;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Completion;
10+
using Microsoft.CodeAnalysis.Completion.Providers;
11+
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
12+
using Microsoft.CodeAnalysis.Editor.CSharp.UnitTests.Completion.CompletionProviders;
13+
using Microsoft.CodeAnalysis.Options;
14+
using Microsoft.CodeAnalysis.Test.Utilities;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Xunit;
17+
18+
namespace Microsoft.CodeAnalysis.CSharp.UnitTests;
19+
20+
public sealed class SdkAppDirectiveCompletionProviderTests : AbstractAppDirectiveCompletionProviderTests
21+
{
22+
protected override string DirectiveKind => "sdk";
23+
24+
internal override Type GetCompletionProviderType()
25+
=> typeof(SdkAppDirectiveCompletionProvider);
26+
}
27+
28+
public sealed class PropertyAppDirectiveCompletionProviderTests : AbstractAppDirectiveCompletionProviderTests
29+
{
30+
protected override string DirectiveKind => "property";
31+
32+
internal override Type GetCompletionProviderType()
33+
=> typeof(PropertyAppDirectiveCompletionProvider);
34+
}
35+
36+
public sealed class PackageAppDirectiveCompletionProviderTests : AbstractAppDirectiveCompletionProviderTests
37+
{
38+
protected override string DirectiveKind => "package";
39+
40+
internal override Type GetCompletionProviderType()
41+
=> typeof(PackageAppDirectiveCompletionProvider);
42+
}
43+
44+
public sealed class ProjectAppDirectiveCompletionProviderTests : AbstractAppDirectiveCompletionProviderTests
45+
{
46+
protected override string DirectiveKind => "project";
47+
48+
internal override Type GetCompletionProviderType()
49+
=> typeof(ProjectAppDirectiveCompletionProvider);
50+
}
51+
52+
public abstract class AbstractAppDirectiveCompletionProviderTests : AbstractCSharpCompletionProviderTests
53+
{
54+
/// <summary>The directive kind. For example, `package` in `#:package MyNugetPackage@Version`.</summary>
55+
/// <remarks>Term defined in feature doc: https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md#directives-for-project-metadata</remarks>
56+
protected abstract string DirectiveKind { get; }
57+
58+
protected static string GetMarkup(string code, string features = "FileBasedProgram=true") => $$"""
59+
<Workspace>
60+
<Project Language="C#" CommonReferences="true" AssemblyName="Test1" Features="{{features}}">
61+
<Document><![CDATA[{{code}}]]></Document>
62+
</Project>
63+
</Workspace>
64+
""";
65+
66+
[Fact]
67+
public Task AfterHashColon()
68+
=> VerifyItemExistsAsync(GetMarkup("""
69+
#:$$
70+
"""), expectedItem: DirectiveKind);
71+
72+
[Fact]
73+
public Task AfterHashColonQuote()
74+
=> VerifyItemIsAbsentAsync(GetMarkup("""
75+
#:"$$
76+
"""), expectedItem: DirectiveKind);
77+
78+
[Fact]
79+
public Task NotWhenFileBasedProgramIsDisabled()
80+
=> VerifyItemIsAbsentAsync(GetMarkup("""
81+
#:$$
82+
""", features: ""), expectedItem: DirectiveKind);
83+
84+
[Fact]
85+
public Task AfterHashColonSpace()
86+
=> VerifyItemExistsAsync(GetMarkup("""
87+
#: $$
88+
"""), expectedItem: DirectiveKind);
89+
90+
[Fact]
91+
public Task NotAfterHashColonSpaceColon()
92+
=> VerifyItemIsAbsentAsync(GetMarkup("""
93+
#: :$$
94+
"""), expectedItem: DirectiveKind);
95+
96+
[Fact]
97+
public Task NotAfterHashColonWord()
98+
=> VerifyItemIsAbsentAsync(GetMarkup("""
99+
#:word$$
100+
"""), expectedItem: DirectiveKind);
101+
102+
[Fact]
103+
public Task AfterHashColonBeforeWord()
104+
=> VerifyItemExistsAsync(GetMarkup("""
105+
#:$$word
106+
"""), expectedItem: DirectiveKind);
107+
108+
[Fact]
109+
public Task AfterHashColonBeforeNameEqualsValue()
110+
=> VerifyItemExistsAsync(GetMarkup("""
111+
#:$$ Name=Value
112+
"""), expectedItem: DirectiveKind);
113+
114+
[Fact]
115+
public Task NotAfterHashOnly()
116+
=> VerifyItemIsAbsentAsync(GetMarkup("""
117+
#$$
118+
"""), expectedItem: DirectiveKind);
119+
120+
[Fact]
121+
public Task NotAfterColonOnly()
122+
=> VerifyItemIsAbsentAsync(GetMarkup("""
123+
:$$
124+
"""), expectedItem: DirectiveKind);
125+
126+
[Fact]
127+
public Task NotAfterStatement()
128+
=> VerifyItemIsAbsentAsync(GetMarkup("""
129+
Console.WriteLine();
130+
$$
131+
"""), expectedItem: DirectiveKind);
132+
}

src/EditorFeatures/CSharpTest/Completion/CompletionProviders/CompletionProviderOrderTests.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ public void TestCompletionProviderOrder()
7171
typeof(LoadDirectiveCompletionProvider),
7272
typeof(ReferenceDirectiveCompletionProvider),
7373

74+
// File-based programs providers
75+
typeof(SdkAppDirectiveCompletionProvider),
76+
typeof(PropertyAppDirectiveCompletionProvider),
77+
typeof(PackageAppDirectiveCompletionProvider),
78+
typeof(ProjectAppDirectiveCompletionProvider),
79+
7480
// Marker for end of built-in completion providers
7581
typeof(LastBuiltInCompletionProvider),
7682
};

src/Features/CSharp/Portable/CSharpFeaturesResources.resx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,4 +587,36 @@
587587
<data name="Convert_all_extension_methods_in_0_to_extension" xml:space="preserve">
588588
<value>Convert all extension methods in '{0}' to extension</value>
589589
</data>
590+
<data name="Build_property_value" xml:space="preserve">
591+
<value>Value</value>
592+
<comment>'Value' is a placeholder for a build property value in a directive like '#:property Name=Value'.</comment>
593+
</data>
594+
<data name="Build_property_name" xml:space="preserve">
595+
<value>Name</value>
596+
<comment>'Name' is a placeholder for a property name in a directive like '#:property Name=Value'.</comment>
597+
</data>
598+
<data name="Package_name" xml:space="preserve">
599+
<value>Name</value>
600+
<comment>'Name' is a placeholder for a package or sdk name in a directive like '#:package Name@Version'.</comment>
601+
</data>
602+
<data name="Package_version" xml:space="preserve">
603+
<value>Version</value>
604+
<comment>'Version' is a placeholder for a package or sdk version in a directive like '#:package Name@Version'.</comment>
605+
</data>
606+
<data name="Project_directive_file_path" xml:space="preserve">
607+
<value>path</value>
608+
<comment>'path' is a placeholder for a project file or directory path in a directive like '#:project path'.</comment>
609+
</data>
610+
<data name="Adds_an_SDK_reference" xml:space="preserve">
611+
<value>Adds an SDK reference.</value>
612+
</data>
613+
<data name="Defines_a_build_property" xml:space="preserve">
614+
<value>Defines a build property.</value>
615+
</data>
616+
<data name="Adds_a_NuGet_package_reference" xml:space="preserve">
617+
<value>Adds a NuGet package reference.</value>
618+
</data>
619+
<data name="Adds_a_project_reference" xml:space="preserve">
620+
<value>Adds a project reference.</value>
621+
</data>
590622
</root>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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;
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Completion;
10+
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Options;
14+
using Microsoft.CodeAnalysis.Shared.Extensions;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Microsoft.CodeAnalysis.Host.Mef;
17+
18+
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
19+
20+
/// <summary>
21+
/// Base type for completion of "app directives" used in file-based programs.
22+
/// See also https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md#directives-for-project-metadata
23+
/// Examples:
24+
/// - '#:property LangVersion=preview'
25+
/// - '#:project path/to/OtherProject.csproj'
26+
/// - '#:package MyNugetPackage@Version'
27+
/// </summary>
28+
internal abstract class AbstractAppDirectiveCompletionProvider : LSPCompletionProvider
29+
{
30+
/// <summary>The directive kind. For example, `package` in `#:package MyNugetPackage@Version`.</summary>
31+
/// <remarks>Term defined in feature doc: https://github.com/dotnet/sdk/blob/main/documentation/general/dotnet-run-file.md#directives-for-project-metadata</remarks>
32+
protected abstract string DirectiveKind { get; }
33+
34+
public sealed override bool IsInsertionTrigger(SourceText text, int characterPosition, CompletionOptions options)
35+
{
36+
return TriggerCharacters.Contains(text[characterPosition])
37+
|| (options.TriggerOnTypingLetters && CompletionUtilities.IsStartingNewWord(text, characterPosition));
38+
}
39+
40+
public override ImmutableHashSet<char> TriggerCharacters { get; } = [':'];
41+
42+
internal sealed override string Language => LanguageNames.CSharp;
43+
44+
public override async Task ProvideCompletionsAsync(CompletionContext context)
45+
{
46+
var tree = await context.Document.GetRequiredSyntaxTreeAsync(context.CancellationToken).ConfigureAwait(false);
47+
if (!tree.Options.Features.ContainsKey("FileBasedProgram"))
48+
return;
49+
50+
var token = tree.GetRoot(context.CancellationToken).FindTokenOnLeftOfPosition(context.Position, includeDirectives: true);
51+
if (token.Parent is not IgnoredDirectiveTriviaSyntax ignoredDirective)
52+
return;
53+
54+
// Note that in the `#: $$` case, the whitespace is trailing trivia on the colon-token.
55+
if (token == ignoredDirective.ColonToken)
56+
{
57+
AddDirectiveKindCompletion(context);
58+
}
59+
else if (token == ignoredDirective.Content)
60+
{
61+
// Consider a test case like '#: pro$$ Name=Value', where we may want to offer 'property' as a completion item:
62+
// We know that 'token.Text == "pro Name=Value"', and, the below expressions correspond to text positions as shown:
63+
// #: pro Name=Value
64+
// │ │
65+
// │ └─context.Position
66+
// └────token.SpanStart
67+
var textLeftOfCaret = token.Text.AsSpan(start: 0, length: context.Position - token.SpanStart);
68+
if (DirectiveKind.StartsWith(textLeftOfCaret))
69+
{
70+
AddDirectiveKindCompletion(context);
71+
}
72+
}
73+
}
74+
75+
protected abstract void AddDirectiveKindCompletion(CompletionContext context);
76+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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;
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Completion;
10+
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Options;
14+
using Microsoft.CodeAnalysis.Shared.Extensions;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Microsoft.CodeAnalysis.Host.Mef;
17+
18+
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
19+
20+
[ExportCompletionProvider(nameof(PackageAppDirectiveCompletionProvider), LanguageNames.CSharp)]
21+
[ExtensionOrder(After = nameof(PropertyAppDirectiveCompletionProvider))]
22+
[Shared]
23+
[method: ImportingConstructor]
24+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
25+
internal sealed class PackageAppDirectiveCompletionProvider() : AbstractAppDirectiveCompletionProvider
26+
{
27+
protected override string DirectiveKind => "package";
28+
29+
protected sealed override void AddDirectiveKindCompletion(CompletionContext context)
30+
{
31+
context.AddItem(CommonCompletionItem.Create(DirectiveKind, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Keyword,
32+
description: [
33+
new(SymbolDisplayPartKind.Keyword, symbol: null, "#:package"),
34+
new(SymbolDisplayPartKind.Space, symbol: null, " "),
35+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Package_name),
36+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, "@"),
37+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Package_version),
38+
new(SymbolDisplayPartKind.LineBreak, symbol: null, ""),
39+
new(SymbolDisplayPartKind.Text, symbol: null, CSharpFeaturesResources.Adds_a_NuGet_package_reference),
40+
]));
41+
}
42+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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;
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Completion;
10+
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Options;
14+
using Microsoft.CodeAnalysis.Shared.Extensions;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Microsoft.CodeAnalysis.Host.Mef;
17+
18+
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
19+
20+
[ExportCompletionProvider(nameof(ProjectAppDirectiveCompletionProvider), LanguageNames.CSharp)]
21+
[ExtensionOrder(After = nameof(PackageAppDirectiveCompletionProvider))]
22+
[Shared]
23+
[method: ImportingConstructor]
24+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
25+
internal sealed class ProjectAppDirectiveCompletionProvider() : AbstractAppDirectiveCompletionProvider
26+
{
27+
protected override string DirectiveKind => "project";
28+
29+
protected sealed override void AddDirectiveKindCompletion(CompletionContext context)
30+
{
31+
context.AddItem(CommonCompletionItem.Create(DirectiveKind, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Keyword,
32+
description: [
33+
new(SymbolDisplayPartKind.Keyword, symbol: null, "#:project"),
34+
new(SymbolDisplayPartKind.Space, symbol: null, " "),
35+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Project_directive_file_path),
36+
new(SymbolDisplayPartKind.LineBreak, symbol: null, ""),
37+
new(SymbolDisplayPartKind.Text, symbol: null, CSharpFeaturesResources.Adds_a_project_reference),
38+
]));
39+
}
40+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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;
6+
using System.Collections.Immutable;
7+
using System.Composition;
8+
using System.Threading.Tasks;
9+
using Microsoft.CodeAnalysis.Completion;
10+
using Microsoft.CodeAnalysis.CSharp.Completion.Providers;
11+
using Microsoft.CodeAnalysis.CSharp;
12+
using Microsoft.CodeAnalysis.CSharp.Syntax;
13+
using Microsoft.CodeAnalysis.Options;
14+
using Microsoft.CodeAnalysis.Shared.Extensions;
15+
using Microsoft.CodeAnalysis.Text;
16+
using Microsoft.CodeAnalysis.Host.Mef;
17+
18+
namespace Microsoft.CodeAnalysis.CSharp.Completion.Providers;
19+
20+
[ExportCompletionProvider(nameof(PropertyAppDirectiveCompletionProvider), LanguageNames.CSharp)]
21+
[ExtensionOrder(After = nameof(SdkAppDirectiveCompletionProvider))]
22+
[Shared]
23+
[method: ImportingConstructor]
24+
[method: Obsolete(MefConstruction.ImportingConstructorMessage, error: true)]
25+
internal sealed class PropertyAppDirectiveCompletionProvider() : AbstractAppDirectiveCompletionProvider
26+
{
27+
protected override string DirectiveKind => "property";
28+
29+
protected sealed override void AddDirectiveKindCompletion(CompletionContext context)
30+
{
31+
context.AddItem(CommonCompletionItem.Create(DirectiveKind, displayTextSuffix: "", CompletionItemRules.Default, glyph: Glyph.Keyword,
32+
description: [
33+
new(SymbolDisplayPartKind.Keyword, symbol: null, "#:property"),
34+
new(SymbolDisplayPartKind.Space, symbol: null, " "),
35+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Build_property_name),
36+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, "="),
37+
new(SymbolDisplayPartKind.StringLiteral, symbol: null, CSharpFeaturesResources.Build_property_value),
38+
new(SymbolDisplayPartKind.LineBreak, symbol: null, ""),
39+
new(SymbolDisplayPartKind.Text, symbol: null, CSharpFeaturesResources.Defines_a_build_property),
40+
]));
41+
}
42+
}

0 commit comments

Comments
 (0)