Skip to content
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

Add Razor C# semantic tokens support in VS Code #6489

Merged
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
169 changes: 168 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5175,6 +5175,168 @@
"markupCommentPunctuation": [
"punctuation.definition.comment.html",
"comment.block.html"
],
"keyword": [
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is needed to override the default colors VS Code uses since they can differ compared to what C# uses. For example, keyword is colored purple by VS Code, but colored blue by C#.

Thanks @dibarbet for the tip!

Copy link
Contributor

Choose a reason for hiding this comment

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

Interesting, so "keyword.cs" is already blue for VS Code and it just... knows?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm guessing C# sets the color somewhere, although I'm not sure where. Maybe @dibarbet or @JoeRobich would know?

Copy link
Member

Choose a reason for hiding this comment

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

I don't quite remember - it was potentially that the lsp token type for keyword mapped to keyword.control but keyword.cs falls back to keyword instead

Copy link
Member

Choose a reason for hiding this comment

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

"keyword.cs"
],
"excludedCode": [
"support.other.excluded.cs"
],
"controlKeyword": [
"keyword.control.cs"
],
"operatorOverloaded": [
"entity.name.function.member.overload.cs"
],
"preprocessorText": [
"meta.preprocessor.string.cs"
],
"punctuation": [
"punctuation.cs"
],
"stringVerbatim": [
"string.verbatim.cs"
],
"stringEscapeCharacter": [
"constant.character.escape.cs"
],
"delegate": [
"entity.name.type.delegate.cs"
],
"module": [
"entity.name.type.module.cs"
],
"field": [
"entity.name.variable.field.cs"
],
"constant": [
"variable.other.constant"
],
"extensionMethod": [
"entity.name.function.extension.cs"
],
"xmlDocCommentAttributeName": [
"comment.documentation.attribute.name.cs"
],
"xmlDocCommentAttributeQuotes": [
"comment.documentation.attribute.quotes.cs"
],
"xmlDocCommentAttributeValue": [
"comment.documentation.attribute.value.cs"
],
"xmlDocCommentCDataSection": [
"comment.documentation.cdata.cs"
],
"xmlDocCommentComment": [
"comment.documentation.comment.cs"
],
"xmlDocCommentDelimiter": [
"comment.documentation.delimiter.cs"
],
"xmlDocCommentEntityReference": [
"comment.documentation.entityReference.cs"
],
"xmlDocCommentName": [
"comment.documentation.name.cs"
],
"xmlDocCommentProcessingInstruction": [
"comment.documentation.processingInstruction.cs"
],
"xmlDocCommentText": [
"comment.documentation.cs"
],
"xmlLiteralAttributeName": [
"entity.other.attribute-name.localname.xml"
],
"xmlLiteralAttributeQuotes": [
"string.quoted.double.xml"
],
"xmlLiteralAttributeValue": [
"meta.tag.xml"
],
"xmlLiteralCDataSection": [
"string.quoted.double.xml"
],
"xmlLiteralComment": [
"comment.block.xml"
],
"xmlLiteralDelimiter": [
"text.xml"
],
"xmlLiteralEmbeddedExpression": [
"meta.tag.xml"
],
"xmlLiteralEntityReference": [
"meta.tag.xml"
],
"xmlLiteralName": [
"entity.name.tag.localname.xml"
],
"xmlLiteralProcessingInstruction": [
"meta.tag.xml"
],
"xmlLiteralText": [
"text.xml"
],
"regexComment": [
"string.regexp.comment.cs"
],
"regexCharacterClass": [
"constant.character.character-class.regexp.cs"
],
"regexAnchor": [
"keyword.control.anchor.regexp.cs"
],
"regexQuantifier": [
"keyword.operator.quantifier.regexp.cs"
],
"regexGrouping": [
"punctuation.definition.group.regexp.cs"
],
"regexAlternation": [
"keyword.operator.or.regexp.cs"
],
"regexText": [
"string.regexp"
],
"regexSelfEscapedCharacter": [
"string.regexp.self-escaped-character.cs"
],
"regexOtherEscape": [
"string.regexp.other-escape.cs"
],
"jsonComment": [
"comment.line.double-slash.js"
],
"jsonNumber": [
"constant.numeric.json"
],
"jsonString": [
"string.quoted.double.json"
],
"jsonKeyword": [
"constant.language.json"
],
"jsonText": [
"string.quoted.double.json"
],
"jsonOperator": [
"string.quoted.double.json"
],
"jsonPunctuation": [
"punctuation.separator.dictionary.key-value.json"
],
"jsonArray": [
"punctuation.definition.array.begin.json"
],
"jsonObject": [
"punctuation.definition.dictionary.begin.json"
],
"jsonPropertyName": [
"support.type.property-name.json"
],
"jsonConstructorName": [
"support.type.property-name.json"
]
}
},
Expand Down Expand Up @@ -5369,7 +5531,12 @@
{
"language": "aspnetcorerazor",
"scopeName": "text.aspnetcorerazor",
"path": "./src/razor/syntaxes/aspnetcorerazor.tmLanguage.json"
"path": "./src/razor/syntaxes/aspnetcorerazor.tmLanguage.json",
"unbalancedBracketScopes": [
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Needed to address this scenario:
image

Otherwise, VS Code will think there is an unclosed bracket and color it red.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure if this is the only one - you might want to test less than / greather than as well

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah you're right, and I was missing bit shifting operators as well. Thanks!

"keyword.operator.arrow.cs",
"keyword.operator.bitwise.shift.cs",
"keyword.operator.relational.cs"
]
}
],
"menus": {
Expand Down
10 changes: 5 additions & 5 deletions src/lsptoolshost/razorCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const provideCodeActionsCommand = 'roslyn.provideCodeActions';
export const resolveCodeActionCommand = 'roslyn.resolveCodeAction';
export const provideCompletionsCommand = 'roslyn.provideCompletions';
export const resolveCompletionsCommand = 'roslyn.resolveCompletion';
export const provideSemanticTokensRangeCommand = 'roslyn.provideSemanticTokensRange';
export const roslynSimplifyMethodCommand = 'roslyn.simplifyMethod';
export const roslynFormatNewFileCommand = 'roslyn.formatNewFile';
export const razorInitializeCommand = 'razor.initialize';
Expand Down Expand Up @@ -72,15 +73,14 @@ export function registerRazorCommands(context: vscode.ExtensionContext, language
}
)
);

const formatNewFileRequestType = new RequestType<SerializableFormatNewFileParams, string | undefined, any>(
'roslyn/formatNewFile'
);
context.subscriptions.push(
vscode.commands.registerCommand(
roslynFormatNewFileCommand,
async (request: SerializableFormatNewFileParams) => {
const formatNewFileRequestType = new RequestType<
SerializableFormatNewFileParams,
string | undefined,
any
>('roslyn/formatNewFile');
return await languageServer.sendRequest(formatNewFileRequestType, request, CancellationToken.None);
}
)
Expand Down
7 changes: 6 additions & 1 deletion src/razor/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,12 @@ export async function activate(
languageServiceClient,
logger
);
const semanticTokenHandler = new SemanticTokensRangeHandler(languageServerClient);
const semanticTokenHandler = new SemanticTokensRangeHandler(
documentManager,
documentSynchronizer,
languageServerClient,
logger
);
const colorPresentationHandler = new ColorPresentationHandler(
documentManager,
languageServerClient,
Expand Down
2 changes: 1 addition & 1 deletion src/razor/src/semantic/provideSemanticTokensResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@

export class ProvideSemanticTokensResponse {
// tslint:disable-next-line: variable-name
constructor(public Tokens: Array<Array<number>>, public HostDocumentSyncVersion: number) {}
constructor(public Tokens: number[], public HostDocumentSyncVersion: number) {}
}
67 changes: 57 additions & 10 deletions src/razor/src/semantic/semanticTokensRangeHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import { RequestType } from 'vscode-languageclient';
import { RazorLanguageServerClient } from '../razorLanguageServerClient';
import { ProvideSemanticTokensResponse } from './provideSemanticTokensResponse';
import { SerializableSemanticTokensParams } from './serializableSemanticTokensParams';
import { RazorDocumentManager } from '../document/razorDocumentManager';
import { RazorDocumentSynchronizer } from '../document/razorDocumentSynchronizer';
import { RazorLogger } from '../razorLogger';

export class SemanticTokensRangeHandler {
private static readonly getSemanticTokensRangeEndpoint = 'razor/provideSemanticTokensRange';
Expand All @@ -16,9 +19,14 @@ export class SemanticTokensRangeHandler {
ProvideSemanticTokensResponse,
any
> = new RequestType(SemanticTokensRangeHandler.getSemanticTokensRangeEndpoint);
private emptyTokensInResponse: Array<Array<number>> = new Array<Array<number>>();
private emptyTokensResponse: number[] = new Array<number>();

constructor(private readonly serverClient: RazorLanguageServerClient) {}
constructor(
private readonly documentManager: RazorDocumentManager,
private readonly documentSynchronizer: RazorDocumentSynchronizer,
private readonly serverClient: RazorLanguageServerClient,
private readonly logger: RazorLogger
) {}

public async register() {
await this.serverClient.onRequestWithParams<
Expand All @@ -33,16 +41,55 @@ export class SemanticTokensRangeHandler {
}

private async getSemanticTokens(
_semanticTokensParams: SerializableSemanticTokensParams,
_cancellationToken: vscode.CancellationToken
semanticTokensParams: SerializableSemanticTokensParams,
cancellationToken: vscode.CancellationToken
): Promise<ProvideSemanticTokensResponse> {
// This is currently a no-op since (1) the default C# semantic tokens experience is already powerful and
// (2) there seems to be an issue with the semantic tokens execute command - possibly either O# not
// returning tokens, or an issue with the command itself:
// https://github.com/dotnet/razor/issues/6922
try {
const razorDocumentUri = vscode.Uri.parse(semanticTokensParams.textDocument.uri, true);
const razorDocument = await this.documentManager.getDocument(razorDocumentUri);
if (razorDocument === undefined) {
this.logger.logWarning(
`Could not find Razor document ${razorDocumentUri}; returning semantic tokens information.`
);

return new ProvideSemanticTokensResponse(
this.emptyTokensResponse,
semanticTokensParams.requiredHostDocumentVersion
);
}

const textDocument = await vscode.workspace.openTextDocument(razorDocumentUri);
const synchronized = await this.documentSynchronizer.trySynchronizeProjectedDocument(
textDocument,
razorDocument.csharpDocument,
semanticTokensParams.requiredHostDocumentVersion,
cancellationToken
);

if (!synchronized) {
return new ProvideSemanticTokensResponse(
this.emptyTokensResponse,
semanticTokensParams.requiredHostDocumentVersion
);
}

const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
'vscode.provideDocumentRangeSemanticTokens',
razorDocument.csharpDocument.uri,
semanticTokensParams.ranges[0]
);

return new ProvideSemanticTokensResponse(
Array.from(tokens.data),
semanticTokensParams.requiredHostDocumentVersion
);
} catch (error) {
this.logger.logWarning(`${SemanticTokensRangeHandler.getSemanticTokensRangeEndpoint} failed with ${error}`);
}

return new ProvideSemanticTokensResponse(
this.emptyTokensInResponse,
_semanticTokensParams.requiredHostDocumentVersion
this.emptyTokensResponse,
semanticTokensParams.requiredHostDocumentVersion
);
}
}