semantic tokens

This commit is contained in:
全卓 2022-11-21 21:57:33 +08:00
parent 6d9caeff57
commit 2798856ce8
2 changed files with 289 additions and 9 deletions

View File

@ -0,0 +1,248 @@
import { SemanticTokens, SemanticTokensBuilder } from "vscode-languageserver";
import { URI } from "vscode-uri";
import Token from "./parser/antlr4/Token";
import CMakeListener from "./parser/CMakeListener";
import { initParams, variables } from "./server";
import * as builtinCmds from './builtin-cmds.json';
export let tokenTypes = [
'type',
'enum',
'parameter',
'variable',
'property',
'function',
'macro',
'keyword',
'modifier',
'comment',
'string',
'number',
'regexp',
'operator'
];
export let tokenModifiers = [
'definition',
'readonly',
'deprecated',
'documentation'
];
enum TokenModifiers {
definition = 2 ** 0,
readonly = 2 ** 1,
deprecated = 2 ** 2,
documentation = 2 ** 3
};
enum TokenTypes {
type = 'type',
enum = 'enum',
parameter = 'parameter',
variable = 'variable',
property = 'property',
function = 'function',
macro = 'macro',
keyword = 'keyword',
modifier = 'modifier',
comment = 'comment',
string = 'string',
number = 'number',
regexp = 'regexp',
operator = 'operator'
}
export const tokenBuilders: Map<string, SemanticTokensBuilder> = new Map();
export function getTokenTypes(): string[] {
tokenTypes = tokenTypes.filter((value, index, arr) => {
return initParams.capabilities.textDocument.semanticTokens.tokenTypes.includes(value);
});
return tokenTypes;
}
export function getTokenModifiers(): string[] {
tokenModifiers = tokenModifiers.filter((value, index, arr) => {
return initParams.capabilities.textDocument.semanticTokens.tokenModifiers.includes(value);
});
return tokenModifiers;
}
// export function getTokenBuilder(document: TextDocument): SemanticTokensBuilder {
// let result = tokenBuilders.get(document.uri);
// if (result !== undefined) {
// return result;
// }
// result = new SemanticTokensBuilder();
// tokenBuilders.set(document.uri, result);
// return result;
// }
export class SemanticListener extends CMakeListener {
private _data: number[] = [];
private _builder: SemanticTokensBuilder;
private _uri: URI;
private _operator: string[] = [
'EXISTS', 'COMMAND', 'DEFINED',
'EQUAL', 'LESS', 'LESS_EQUAL', 'GREATER', 'GREATER_EAUAL', 'STREQUAL',
'STRLESS', 'STRLESS_EQUAL', 'STRGREATER', 'STRGREATER_EQUAL',
'VERSION_EQUAL', 'VERSION_LESS', 'VERSION_LESS_EQUAL', 'VERSION_GREATER',
'VERSION_GREATER_EQUAL', 'PATH_EQUAL', 'MATCHES',
'AND', 'NOT', 'OR'
];
constructor(uri: URI) {
super();
this._uri = uri;
this._builder = this.getTokenBuilder(uri.toString());
}
private isOperator(token: string): boolean {
return this._operator.includes(token);
}
private isVariable(token: string): boolean {
return variables.includes(token);
}
private getModifiers(modifiers: TokenModifiers[]): number {
let result = 0;
for (let modifier of modifiers) {
result |= modifier;
}
return result;
}
private getTokenBuilder(uri: string): SemanticTokensBuilder {
let result = tokenBuilders.get(uri);
if (result !== undefined) {
return result;
}
result = new SemanticTokensBuilder();
tokenBuilders.set(uri, result);
return result;
}
private getCmdKeyWords(sigs: string[]): string[] {
let result: string[] = [];
sigs.forEach(sig => {
const keys = sig.match(/[A-Z_]+/g);
if (keys !== null) {
keys.forEach(key => {
result.push(key);
});
}
});
return result;
}
enterIfCmd(ctx: any): void {
ctx.argument().forEach(argCtx => {
if (argCtx.getChildCount() === 1) {
const argToken: Token = argCtx.start;
if (this.isOperator(argToken.text)) {
this._builder.push(argToken.line - 1, argToken.column,
argToken.text.length, tokenTypes.indexOf(TokenTypes.operator),
this.getModifiers([]));
} else if (this.isVariable(argToken.text)) {
this._builder.push(argToken.line - 1, argToken.column,
argToken.text.length, tokenTypes.indexOf(TokenTypes.variable),
this.getModifiers([]));
}
}
});
}
enterElseIfCmd(ctx: any): void {
this.enterIfCmd(ctx);
}
enterForeachCmd(ctx: any): void {
}
enterWhileCmd(ctx: any): void {
this.enterIfCmd(ctx);
}
enterFunctionCmd(ctx: any): void {
const argCount = ctx.argument().length;
if (argCount > 1) {
ctx.argument().slice(1).forEach(argCtx => {
if (argCtx.getChildCount() === 1) {
const argToken: Token = argCtx.start;
this._builder.push(argToken.line - 1, argToken.column, argToken.text.length,
tokenTypes.indexOf(TokenTypes.parameter), this.getModifiers([]));
}
});
}
}
enterMacroCmd(ctx: any): void {
this.enterFunctionCmd(ctx);
}
enterSetCmd(ctx: any): void {
const argCount = ctx.argument().length;
if (argCount > 0) {
const varCtx = ctx.argument(0);
const varToken: Token = varCtx.start;
this._builder.push(varToken.line - 1, varToken.column, varToken.text.length,
tokenTypes.indexOf(TokenTypes.variable),
this.getModifiers([TokenModifiers.definition]));
}
}
enterOptionCmd(ctx: any): void {
}
enterIncludeCmd(ctx: any): void {
}
enterAddSubDirCmd(ctx: any): void {
}
enterOtherCmd(ctx: any): void {
const cmdName: Token = ctx.ID().symbol;
if (cmdName.text in builtinCmds) {
if ('deprecated' in builtinCmds[cmdName.text]) {
this._builder.push(cmdName.line - 1, cmdName.column,
cmdName.text.length, tokenTypes.indexOf(TokenTypes.function),
this.getModifiers([TokenModifiers.deprecated]));
}
const sigs: string[] = builtinCmds[cmdName.text]['sig'];
const keywords = this.getCmdKeyWords(sigs);
if (ctx.argument().length > 0) {
ctx.argument().forEach(argCtx => {
if (argCtx.getChildCount() === 1) {
const argToken: Token = argCtx.start;
if (keywords.includes(argToken.text)) {
this._builder.push(argToken.line - 1, argToken.column,
argToken.text.length, tokenTypes.indexOf(TokenTypes.keyword),
this.getModifiers([]));
}
}
});
}
} else {
this._builder.push(cmdName.line - 1, cmdName.column,
cmdName.text.length, tokenTypes.indexOf(TokenTypes.function),
this.getModifiers([]));
}
}
public getSemanticTokens(): SemanticTokens {
return this._builder.build();
}
}

View File

@ -1,6 +1,6 @@
import {
createConnection, HoverParams, InitializeParams, InitializeResult,
ProposedFeatures, SignatureHelpParams, TextDocuments, TextDocumentSyncKind,
ProposedFeatures, SemanticTokensParams, SemanticTokensRequest, SignatureHelpParams, TextDocuments, TextDocumentSyncKind,
_RemoteWindow
} from 'vscode-languageserver/node';
@ -26,6 +26,7 @@ import { DefinationListener, incToBaseDir, refToDef, topScope } from './symbolTa
import { existsSync } from 'fs';
import { URI, Utils } from 'vscode-uri';
import path = require('path');
import { getTokenModifiers, getTokenTypes, SemanticListener, tokenBuilders } from './semanticTokens';
type Word = {
text: string,
@ -34,14 +35,16 @@ type Word = {
};
const entries: Entries = getBuiltinEntries();
const modules = entries[0].split('\n');
const policies = entries[1].split('\n');
const variables = entries[2].split('\n');
const properties = entries[3].split('\n');
export const modules = entries[0].split('\n');
export const policies = entries[1].split('\n');
export const variables = entries[2].split('\n');
export const properties = entries[3].split('\n');
let workspaceFolders: WorkspaceFolder[] | null;
let contentChanged = true;
export let initParams: InitializeParams;
// Craete a connection for the server, using Node's IPC as a transport.
// Also include all preview / proposed LSP features.
const connection = createConnection(ProposedFeatures.all);
@ -50,7 +53,7 @@ const connection = createConnection(ProposedFeatures.all);
export const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
connection.onInitialize((params: InitializeParams) => {
workspaceFolders = params.workspaceFolders;
initParams = params;
const result: InitializeResult = {
capabilities: {
textDocumentSync: TextDocumentSyncKind.Incremental,
@ -61,7 +64,15 @@ connection.onInitialize((params: InitializeParams) => {
completionProvider: {},
documentFormattingProvider: true,
documentSymbolProvider: true,
definitionProvider: true
definitionProvider: true,
semanticTokensProvider: {
legend: {
tokenTypes: getTokenTypes(),
tokenModifiers: getTokenModifiers()
},
range: false,
full: true
}
},
serverInfo: {
name: 'cmakels',
@ -256,6 +267,7 @@ connection.onDocumentSymbol((params: DocumentSymbolParams) => {
});
connection.onDefinition((params: DefinitionParams) => {
const workspaceFolders = initParams.workspaceFolders;
if (workspaceFolders === null || workspaceFolders.length === 0) {
return null;
}
@ -269,7 +281,7 @@ connection.onDefinition((params: DefinitionParams) => {
const word: Word = getWordAtPosition(document, params.position);
const wordPos: string = params.textDocument.uri + '_' + params.position.line + '_' +
word.col + '_' + word.text;
if (contentChanged) {
// clear refToDef and topScope first
refToDef.clear();
@ -299,12 +311,32 @@ connection.onDefinition((params: DefinitionParams) => {
});
});
connection.languages.semanticTokens.on(async (params: SemanticTokensParams) => {
const document = documents.get(params.textDocument.uri);
if (document === undefined) {
return { data: [] };
}
// const builder = getTokenBuilder(document);
const docUri: URI = URI.parse(params.textDocument.uri);
const tree = getFileContext(docUri);
const semanticListener = new SemanticListener(docUri);
antlr4.tree.ParseTreeWalker.DEFAULT.walk(semanticListener, tree);
// return builder.build();
return semanticListener.getSemanticTokens();
});
// The content of a text document has changed. This event is emitted
// when the text document first opened or when its content has changed.
documents.onDidChangeContent(change => {
contentChanged = true;
});
documents.onDidClose(event => {
tokenBuilders.delete(event.document.uri);
});
function getWordAtPosition(textDocument: TextDocument, position: Position): Word {
const lineRange: Range = {
start: { line: position.line, character: 0 },