Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ public static void AddCompletionServices(this IServiceCollection services)
services.AddSingleton<CompletionItemResolver, DelegatedCompletionItemResolver>();
services.AddSingleton<ITagHelperCompletionService, TagHelperCompletionService>();
services.AddSingleton<IRazorCompletionFactsService, LspRazorCompletionFactsService>();
services.AddSingleton<IRazorCompletionItemProvider, CSharpRazorKeywordCompletionItemProvider>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my hypothesis is correct, and this issue is caused by semantic tokens, then I think putting this in the non-cohosting editor would actually result in doubling up of completion items for these, as Roslyn will add them as well, so I think perhaps this line is not desirable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can I have you try out cohosting off and see if you see the keywords in completion after typing just '@'? (I'm not seeing it without my changes)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well now I'm really confused. Using Version: 18.3.0 Insiders [11220.113.main]

Without cohosting:
image

With cohosting:
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remember I'll pull this branch down tomorrow and see what I get with these changes

Copy link
Contributor Author

@ToddGrun ToddGrun Nov 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh, behavior seems to vary across more axes than I tested. I'll do a more comprehensive run through of with/without my changes, at various locations, forced completion versus typed trigger, cohosting on/off and report back probably early tomorrow.

services.AddSingleton<IRazorCompletionItemProvider, DirectiveCompletionItemProvider>();
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeCompletionItemProvider>();
services.AddSingleton<IRazorCompletionItemProvider, DirectiveAttributeEventParameterCompletionItemProvider>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal sealed class CSharpRazorKeywordCompletionDescription(string description) : CompletionDescription
{
public override string Description { get; } = string.Format(SR.KeywordDescription, description);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Collections.Immutable;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Syntax;

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal class CSharpRazorKeywordCompletionItemProvider : IRazorCompletionItemProvider
{
internal static readonly ImmutableArray<RazorCommitCharacter> KeywordCommitCharacters = RazorCommitCharacter.CreateArray([" "]);

// internal for testing
internal static readonly ImmutableArray<string> CSharpRazorKeywords =
[
"do", "for", "foreach", "if", "lock", "switch", "try", "while"
];

// Internal for testing
internal static readonly ImmutableArray<RazorCompletionItem> CSharpRazorKeywordCompletionItems = GetCSharpRazorKeywordCompletionItems();

public ImmutableArray<RazorCompletionItem> GetCompletionItems(RazorCompletionContext context)
{
return ShouldProvideCompletions(context)
? CSharpRazorKeywordCompletionItems
: [];
}

// Internal for testing
internal static bool ShouldProvideCompletions(RazorCompletionContext context)
{
var owner = context.Owner;
if (owner is null)
{
return false;
}

// Do not provide IntelliSense for explicit expressions. Explicit expressions will usually look like:
// @(DateTime.Now)
var implicitExpression = owner.FirstAncestorOrSelf<CSharpImplicitExpressionSyntax>();
if (implicitExpression is null)
{
return false;
}

if (implicitExpression.Width > 2 && context.Reason != CompletionReason.Invoked)
{
// We only want to provide razor csharp keyword completions if the implicit expression is empty "@|" or at the beginning of a word "@i|", this ensures
// we're consistent with how C# typically provides completion items.
return false;
}

if (owner.ChildNodesAndTokens().Any(static x => !x.AsToken(out var token) || !IsCSharpRazorKeywordCompletableToken(token)))
{
// Implicit expression contains nodes or tokens that aren't completable by a csharp razor keyword
return false;
}

return true;
}

private static bool IsCSharpRazorKeywordCompletableToken(AspNetCore.Razor.Language.Syntax.SyntaxToken token)
{
return token is { Kind: SyntaxKind.Identifier or SyntaxKind.Marker or SyntaxKind.Keyword }
or { Kind: SyntaxKind.Transition, Parent.Kind: SyntaxKind.CSharpTransition };
}

private static ImmutableArray<RazorCompletionItem> GetCSharpRazorKeywordCompletionItems()
{
var completionItems = new RazorCompletionItem[CSharpRazorKeywords.Length];

for (var i = 0; i < CSharpRazorKeywords.Length; i++)
{
var keyword = CSharpRazorKeywords[i];

var keywordCompletionItem = RazorCompletionItem.CreateKeyword(
displayText: keyword,
insertText: keyword,
KeywordCommitCharacters);

completionItems[i] = keywordCompletionItem;
}

return ImmutableCollectionsMarshal.AsImmutableArray(completionItems);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal class DirectiveCompletionDescription : CompletionDescription
internal sealed class DirectiveCompletionDescription(string description) : CompletionDescription
{
public override string Description { get; }

public DirectiveCompletionDescription(string description)
{
if (description is null)
{
throw new ArgumentNullException(nameof(description));
}

Description = description;
}
public override string Description { get; } = description;
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;

namespace Microsoft.CodeAnalysis.Razor.Completion;

internal class MarkupTransitionCompletionDescription : CompletionDescription
internal sealed class MarkupTransitionCompletionDescription(string description) : CompletionDescription
{
public override string Description { get; }

public MarkupTransitionCompletionDescription(string description)
{
if (description is null)
{
throw new ArgumentNullException(nameof(description));
}

Description = description;
}
public override string Description { get; } = description;
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,9 @@ public static RazorCompletionItem CreateAttribute(
AttributeDescriptionInfo descriptionInfo,
ImmutableArray<RazorCommitCharacter> commitCharacters, bool isSnippet)
=> new(RazorCompletionItemKind.Attribute, displayText, insertText, sortText: null, descriptionInfo, commitCharacters, isSnippet);

public static RazorCompletionItem CreateKeyword(
string displayText, string insertText,
ImmutableArray<RazorCommitCharacter> commitCharacters)
=> new(RazorCompletionItemKind.CSharpRazorKeyword, displayText, insertText, sortText: null, new CSharpRazorKeywordCompletionDescription(displayText), commitCharacters, isSnippet: false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ namespace Microsoft.CodeAnalysis.Razor.Completion;

internal enum RazorCompletionItemKind
{
CSharpRazorKeyword,
Directive,
DirectiveAttribute,
DirectiveAttributeParameter,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ internal class RazorCompletionItemResolver : CompletionItemResolver
.ConfigureAwait(false);
}

break;
}
case RazorCompletionItemKind.CSharpRazorKeyword:
{
if (associatedRazorCompletion.DescriptionInfo is CSharpRazorKeywordCompletionDescription descriptionInfo)
{
completionItem.Documentation = descriptionInfo.Description;
}

break;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,23 @@ internal static bool TryConvert(
completionItem = attributeCompletionItem;
return true;
}
case RazorCompletionItemKind.CSharpRazorKeyword:
{
var csharpRazorKeywordCompletionItem = new VSInternalCompletionItem()
{
Label = razorCompletionItem.DisplayText,
InsertText = razorCompletionItem.InsertText,
FilterText = razorCompletionItem.DisplayText,
SortText = razorCompletionItem.SortText,
InsertTextFormat = insertTextFormat,
Kind = CompletionItemKind.Keyword,
};

csharpRazorKeywordCompletionItem.UseCommitCharactersFrom(razorCompletionItem, clientCapabilities);

completionItem = csharpRazorKeywordCompletionItem;
return true;
}
}

completionItem = null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,4 +230,7 @@
<value>{0} (and req'd attributes...)</value>
<comment>The term "req'd" is an abbreviation for "required"</comment>
</data>
<data name="KeywordDescription" xml:space="preserve">
<value>{0} Keyword</value>
</data>
</root>

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

namespace Microsoft.CodeAnalysis.Remote.Razor.Completion;

[Export(typeof(IRazorCompletionItemProvider)), Shared]
internal sealed class OOPCSharpRazorKeywordCompletionItemProvider : CSharpRazorKeywordCompletionItemProvider;

[Export(typeof(IRazorCompletionItemProvider)), Shared]
internal sealed class OOPDirectiveCompletionItemProvider : DirectiveCompletionItemProvider;

Expand Down
Loading