Skip to content

Commit

Permalink
Add support for overriding semantic token types (#1600)
Browse files Browse the repository at this point in the history
  • Loading branch information
aabounegm authored Aug 22, 2024
1 parent ceba584 commit bfca81f
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 7 deletions.
5 changes: 3 additions & 2 deletions packages/langium/src/lsp/language-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import type { ConfigurationInitializedParams } from '../workspace/configuration.
import { DocumentState, type LangiumDocument } from '../workspace/documents.js';
import { mergeCompletionProviderOptions } from './completion/completion-provider.js';
import type { LangiumSharedServices, PartialLangiumLSPServices } from './lsp-services.js';
import { DefaultSemanticTokenOptions } from './semantic-token-provider.js';
import { mergeSemanticTokenProviderOptions } from './semantic-token-provider.js';
import { mergeSignatureHelpOptions } from './signature-help-provider.js';

export interface LanguageServer {
Expand Down Expand Up @@ -105,6 +105,7 @@ export class DefaultLanguageServer implements LanguageServer {
const formattingOnTypeOptions = allServices.map(e => e.lsp?.Formatter?.formatOnTypeOptions).find(e => Boolean(e));
const hasCodeActionProvider = this.hasService(e => e.lsp?.CodeActionProvider);
const hasSemanticTokensProvider = this.hasService(e => e.lsp?.SemanticTokenProvider);
const semanticTokensOptions = mergeSemanticTokenProviderOptions(allServices.map(e => e.lsp?.SemanticTokenProvider?.semanticTokensOptions));
const commandNames = this.services.lsp?.ExecuteCommandHandler?.commands;
const hasDocumentLinkProvider = this.hasService(e => e.lsp?.DocumentLinkProvider);
const signatureHelpOptions = mergeSignatureHelpOptions(allServices.map(e => e.lsp?.SignatureHelp?.signatureHelpOptions));
Expand Down Expand Up @@ -160,7 +161,7 @@ export class DefaultLanguageServer implements LanguageServer {
prepareProvider: true
} : undefined,
semanticTokensProvider: hasSemanticTokensProvider
? DefaultSemanticTokenOptions
? semanticTokensOptions
: undefined,
signatureHelpProvider: signatureHelpOptions,
implementationProvider: hasGoToImplementationProvider,
Expand Down
77 changes: 73 additions & 4 deletions packages/langium/src/lsp/semantic-token-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,54 @@ export interface SemanticTokenProvider {
semanticHighlight(document: LangiumDocument, params: SemanticTokensParams, cancelToken?: CancellationToken): MaybePromise<SemanticTokens>
semanticHighlightRange(document: LangiumDocument, params: SemanticTokensRangeParams, cancelToken?: CancellationToken): MaybePromise<SemanticTokens>
semanticHighlightDelta(document: LangiumDocument, params: SemanticTokensDeltaParams, cancelToken?: CancellationToken): MaybePromise<SemanticTokens | SemanticTokensDelta>
readonly tokenTypes: Record<string, number>
readonly tokenModifiers: Record<string, number>
readonly semanticTokensOptions: SemanticTokensOptions
}

export function mergeSemanticTokenProviderOptions(options: Array<SemanticTokensOptions | undefined>): SemanticTokensOptions {
const tokenTypes: string[] = [];
const tokenModifiers: string[] = [];
let full = true;
let delta = true;
let range = true;
for (const option of options) {
if (!option) {
continue;
}
option.legend.tokenTypes.forEach((tokenType, index) => {
const existing = tokenTypes[index];
if (existing && existing !== tokenType) {
throw new Error(`Cannot merge '${existing}' and '${tokenType}' token types. They use the same index ${index}.`);
} else {
tokenTypes[index] = tokenType;
}
});
option.legend.tokenModifiers.forEach((tokenModifier, index) => {
const existing = tokenModifiers[index];
if (existing && existing !== tokenModifier) {
throw new Error(`Cannot merge '${existing}' and '${tokenModifier}' token modifier. They use the same index ${index}.`);
} else {
tokenModifiers[index] = tokenModifier;
}
});
if (!option.full) {
full = false;
} else if (typeof option.full === 'object' && !option.full.delta) {
delta = false;
}
if (!option.range) {
range = false;
}
}
return {
legend: {
tokenTypes,
tokenModifiers,
},
full: full && { delta },
range,
};
}

export interface SemanticToken {
Expand Down Expand Up @@ -209,6 +257,27 @@ export abstract class AbstractSemanticTokenProvider implements SemanticTokenProv
this.clientCapabilities = clientCapabilities;
}

get tokenTypes(): Record<string, number> {
return AllSemanticTokenTypes;
}

get tokenModifiers(): Record<string, number> {
return AllSemanticTokenModifiers;
}

get semanticTokensOptions(): SemanticTokensOptions {
return {
legend: {
tokenTypes: Object.keys(this.tokenTypes),
tokenModifiers: Object.keys(this.tokenModifiers),
},
full: {
delta: true
},
range: true,
};
}

async semanticHighlight(document: LangiumDocument, _params: SemanticTokensParams, cancelToken = CancellationToken.None): Promise<SemanticTokens> {
this.currentRange = undefined;
this.currentDocument = document;
Expand Down Expand Up @@ -307,14 +376,14 @@ export abstract class AbstractSemanticTokenProvider implements SemanticTokenProv
if ((this.currentRange && !inRange(range, this.currentRange)) || !this.currentDocument || !this.currentTokensBuilder) {
return;
}
const intType = AllSemanticTokenTypes[type];
const intType = this.tokenTypes[type];
let totalModifier = 0;
if (modifiers !== undefined) {
if (typeof modifiers === 'string') {
modifiers = [modifiers];
}
for (const modifier of modifiers) {
const intModifier = AllSemanticTokenModifiers[modifier];
const intModifier = this.tokenModifiers[modifier];
totalModifier |= intModifier;
}
}
Expand Down Expand Up @@ -432,9 +501,9 @@ export namespace SemanticTokensDecoder {
text: string;
}

export function decode<T extends AstNode = AstNode>(tokens: SemanticTokens, document: LangiumDocument<T>): DecodedSemanticToken[] {
export function decode<T extends AstNode = AstNode>(tokens: SemanticTokens, tokenTypes: Record<string, number>, document: LangiumDocument<T>): DecodedSemanticToken[] {
const typeMap = new Map<number, SemanticTokenTypes>();
Object.entries(AllSemanticTokenTypes).forEach(([type, index]) => typeMap.set(index, type as SemanticTokenTypes));
Object.entries(tokenTypes).forEach(([type, index]) => typeMap.set(index, type as SemanticTokenTypes));
let line = 0;
let character = 0;
return sliceIntoChunks(tokens.data, 5).map(t => {
Expand Down
2 changes: 1 addition & 1 deletion packages/langium/src/test/langium-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ export function highlightHelper<T extends AstNode = AstNode>(services: LangiumSe
const document = await parse(input, options);
const params: SemanticTokensParams = { textDocument: { uri: document.textDocument.uri } };
const tokens = await tokenProvider.semanticHighlight(document, params);
return { tokens: SemanticTokensDecoder.decode(tokens, document), ranges };
return { tokens: SemanticTokensDecoder.decode(tokens, tokenProvider.tokenTypes, document), ranges };
};
}

Expand Down

0 comments on commit bfca81f

Please sign in to comment.