Language feature: go to Definition

This commit is contained in:
全卓 2022-11-10 15:59:22 +08:00
parent 6bf06473dd
commit 99156d3ed5
5 changed files with 287 additions and 80 deletions

View File

@ -21,8 +21,14 @@ import antlr4 from './parser/antlr4/index.js';
import CMakeLexer from './parser/CMakeLexer.js';
import CMakeParser from './parser/CMakeParser.js';
import { SymbolListener } from './docSymbols';
import { Entries, getBuiltinEntries } from './utils';
import { DefinationListener } from './symbolTable/goToDefination';
import { Entries, getBuiltinEntries, getCMakeVersion, getFileContext } from './utils';
import { DefinationListener, refToDef, topScope } from './symbolTable/goToDefination';
type Word = {
text: string,
line: number,
col: number
};
const entries: Entries = getBuiltinEntries();
const modules = entries[0].split('\n');
@ -68,7 +74,7 @@ connection.onInitialized(() => {
connection.onHover((params: HoverParams) => {
const document: TextDocument = documents.get(params.textDocument.uri);
const word = getWordAtPosition(document, params.position);
const word = getWordAtPosition(document, params.position).text;
if (word.length === 0) {
return null;
}
@ -125,7 +131,7 @@ connection.onHover((params: HoverParams) => {
connection.onCompletion(async (params: CompletionParams) => {
const document = documents.get(params.textDocument.uri);
const word = getWordAtPosition(document, params.position);
const word = getWordAtPosition(document, params.position).text;
if (word.length === 0) {
return null;
}
@ -150,7 +156,7 @@ connection.onSignatureHelp((params: SignatureHelpParams) => {
character: params.position.character - 1
};
const word: string = getWordAtPosition(document, posBeforeLParen);
const word: string = getWordAtPosition(document, posBeforeLParen).text;
if (word.length === 0 || !(word in builtinCmds)) {
return null;
}
@ -169,7 +175,7 @@ connection.onSignatureHelp((params: SignatureHelpParams) => {
});
}
} else if (params.context.triggerKind === SignatureHelpTriggerKind.ContentChange) {
const word: string = getWordAtPosition(document, params.position);
const word: string = getWordAtPosition(document, params.position).text;
if (word.length === 0) {
return null;
}
@ -246,42 +252,30 @@ connection.onDocumentSymbol((params: DocumentSymbolParams) => {
});
connection.onDefinition((params: DefinitionParams) => {
if (workspaceFolders === null || workspaceFolders.length === 0 ) {
if (workspaceFolders === null || workspaceFolders.length === 0) {
return null;
}
if (workspaceFolders.length > 1) {
connection.window.showInformationMessage("CMake IntelliSence doesn't support multi-root workspace now");
return null;
}
const uri: string = params.textDocument.uri;
const document = documents.get(uri);
const word: string = getWordAtPosition(document, params.position);
const dir = uri.slice(0, uri.lastIndexOf('/'));
const subdir = dir + '/' + word;
const cmakeLists = subdir + uri.slice(uri.lastIndexOf('/'));
const input = antlr4.CharStreams.fromString(document.getText());
const lexer = new CMakeLexer(input);
const tokenStream = new antlr4.CommonTokenStream(lexer);
const parser = new CMakeParser(tokenStream);
const tree = parser.file();
const fileScope: FileScope = new FileScope(null);
// let currentScope: Scope = null;
const definationListener = new DefinationListener(fileScope);
antlr4.tree.ParseTreeWalker.DEFAULT.walk(definationListener, tree);
return new Promise((resolve, reject) => {
resolve({
uri: cmakeLists,
range: {
start: {
line: 0,
character: 0
},
end: {
line: Number.MAX_VALUE,
character: Number.MAX_VALUE
}
}
});
const uri: string = params.textDocument.uri;
const tree = getFileContext(uri);
const definationListener = new DefinationListener(uri, topScope);
antlr4.tree.ParseTreeWalker.DEFAULT.walk(definationListener, tree);
const document = documents.get(uri);
const word: Word = getWordAtPosition(document, params.position);
const wordPos: string = uri + '_' + params.position.line + '_' +
word.col + '_' + word.text;
if (refToDef.has(wordPos)) {
resolve(refToDef.get(wordPos));
} else {
return null;
}
});
});
@ -291,7 +285,7 @@ documents.onDidChangeContent(change => {
console.log('content changed');
});
function getWordAtPosition(textDocument: TextDocument, position: Position): string {
function getWordAtPosition(textDocument: TextDocument, position: Position): Word {
const lineRange: Range = {
start: { line: position.line, character: 0 },
end: { line: position.line, character: Number.MAX_VALUE }
@ -305,7 +299,11 @@ function getWordAtPosition(textDocument: TextDocument, position: Position): stri
const startWord = start.match(startReg)[0],
endWord = end.match(endReg)[0];
return startWord + endWord;
return {
text: startWord + endWord,
line: position.line,
col: position.character - startWord.length
};
}
function getCommandProposals(word: string): Thenable<CompletionItem[]> {

View File

@ -1,53 +1,145 @@
import CMakeListener from "../parser/CMakeListener";
import CMakeLexer from "../parser/CMakeLexer";
import CMakeParser from "../parser/CMakeParser";
import antlr4 from '../parser/antlr4/index.js';
import { documents } from "../server";
import * as path from "path";
import { Location } from "vscode-languageserver-types";
import antlr4 from '../parser/antlr4/index.js';
import Token from "../parser/antlr4/Token";
import CMakeListener from "../parser/CMakeListener";
import { getFileContext, getIncludeFileUri } from "../utils";
import { FileScope, FunctionScope, Scope } from "./scope";
import { Sym, Type } from "./symbol";
export const definations: Map<string, Location> = new Map();
export const topScope: FileScope = new FileScope(null);
/**
* key: <uri>_<line>_<column>_<word>
* value: Location
*
* NOTE: line and column are numbers start from zero
*/
export const refToDef: Map<string, Location> = new Map();
export class DefinationListener extends CMakeListener {
private fileScope: FileScope;
private currentScope: Scope;
private inFunction = false;
private uri: string;
constructor(parentScope) {
constructor(uri: string, scope: Scope) {
super();
this.fileScope = parentScope;
this.currentScope = this.fileScope;
this.uri = uri;
this.currentScope = scope;
}
enterFile(ctx: any): void {
}
enterFunctionCmd(ctx: any): void {
this.inFunction = true;
this.inFunction = true;
// create a function symbol
const funcToken: Token = ctx.argument(0).start;
const funcSymbol: Sym = new Sym(funcToken.text, Type.Function,
this.uri, funcToken.line - 1, funcToken.column);
// add to current scope
this.currentScope.define(funcSymbol);
// create a new function scope
const funcScope: Scope = new FunctionScope(this.currentScope);
this.currentScope = funcScope;
// add all remain arguments to function scope
ctx.argument().slice(1).forEach(element => {
const argToken = element.start;
const varSymbol: Sym = new Sym(argToken.text, Type.Variable,
this.uri, argToken.line - 1, argToken.column);
this.currentScope.define(varSymbol);
});
}
exitEndFunctionCmd(ctx: any): void {
this.inFunction = false;
// restore the parent scope
this.currentScope = this.currentScope.getEnclosingScope();
}
enterSetCmd(ctx: any): void {
// create a variable symbol
const varToken: Token = ctx.argument(0).start;
const varSymbol: Sym = new Sym(varToken.text, Type.Variable,
this.uri, varToken.line - 1, varToken.column);
// add variable to current scope
this.currentScope.define(varSymbol);
}
enterIncludeCmd(ctx: any): void {
// FIXME: placeholders, please fix the include fileUri
const fileUri: string = "include-filename";
const document = documents.get(fileUri);
const input = antlr4.CharStreams.fromString(document.getText());
const lexer = new CMakeLexer(input);
const tokenStream = new antlr4.CommonTokenStream(lexer);
const parser = new CMakeParser(tokenStream);
const tree = parser.file();
const nameToken = ctx.argument(0).start;
const fileUri: string = getIncludeFileUri(this.uri, nameToken.text);
if (!fileUri) {
return;
}
const tree = getFileContext(fileUri);
const definationListener = new DefinationListener(fileUri, this.currentScope);
antlr4.tree.ParseTreeWalker.DEFAULT.walk(definationListener, tree);
}
enterAddSubDirCmd(ctx: any): void {
const dirToken: Token = ctx.argument(0).start;
const fileUri: string = this.uri + path.sep + dirToken.text;
const tree = getFileContext(fileUri);
const subDirScope: Scope = new FileScope(this.currentScope);
const definationListener = new DefinationListener(fileUri, subDirScope);
antlr4.tree.ParseTreeWalker.DEFAULT.walk(definationListener, tree);
}
}
exitAddSubDirCmd(ctx: any): void {
this.currentScope = this.currentScope.getEnclosingScope();
}
enterOtherCmd(ctx: any): void {
// command reference, resolve the defination
const cmdToken: Token = ctx.ID().symbol;
const symbol: Sym = this.currentScope.resolve(cmdToken.text, Type.Function);
if (symbol === null) {
return;
}
// token.line start from 1, so - 1 first
const refPos: string = this.uri + '_' + (cmdToken.line - 1) + '_' +
cmdToken.column + '_' + cmdToken.text;
// add to refToDef
refToDef.set(refPos, symbol.getLocation());
}
enterArgument(ctx: any): void {
const count: number = ctx.getChildCount();
if (count !== 1) {
return;
}
if (ctx.BracketArgument() !== null) {
return;
}
// find all variable reference, resolve the defination, add to refToDef
const argToken: Token = ctx.start;
const regexp: RegExp = /\${(.*?)}/g;
const matches = argToken.text.matchAll(regexp);
for (let match of matches) {
const varRef: string = match[1];
const symbol: Sym = this.currentScope.resolve(varRef, Type.Variable);
if (symbol === null) {
continue;
}
// token.line start from 1, so - 1 first
const refPos: string = this.uri + '_' + (argToken.line - 1) + '_' +
(argToken.column + match.index + 2) + '_' + varRef;
refToDef.set(refPos, symbol.getLocation());
}
// TODO: UnquotedArgument
}
}

View File

@ -1,48 +1,52 @@
class Scope {
import { Sym, Type } from './symbol';
export class Scope {
private enclosingScope: Scope;
private symbols: Map<string, Sym>;
private variables: Map<string, Sym>;
private commands: Map<string, Sym>;
constructor(enclosingScope: Scope) {
this.enclosingScope = enclosingScope;
this.symbols = new Map<string, Sym>();
this.variables = new Map<string, Sym>();
this.commands = new Map<string, Sym>();
}
resolve(name: string): Sym {
const s = this.symbols.get(name);
resolve(name: string, type: Type): Sym {
const symbols = type === Type.Variable ? this.variables : this.commands;
const s = symbols.get(name);
if (s !== undefined) {
return s;
}
if (this.enclosingScope) {
return this.enclosingScope.resolve(name);
return this.enclosingScope.resolve(name, type);
}
return null;
}
define(symbol: Sym): void {
this.symbols.set(symbol.getName(), symbol);
if (symbol.getType() === Type.Variable) {
this.variables.set(symbol.getName(), symbol);
} else {
this.commands.set(symbol.getName(), symbol);
}
symbol.setScope(this);
}
getEnclosingScope(): Scope {
return this.enclosingScope;
}
// toString(): string {
// return getScope
// }
}
class FileScope extends Scope {
export class FileScope extends Scope {
constructor(encolsingScope: Scope) {
super(encolsingScope);
}
}
class FunctionScope extends Scope {
export class FunctionScope extends Scope {
constructor(enclosingScope: Scope) {
super(enclosingScope);
}
}
}

View File

@ -1,4 +1,7 @@
enum Type {
import { Location } from 'vscode-languageserver-types';
import { Scope } from './scope';
export enum Type {
// eslint-disable-next-line @typescript-eslint/naming-convention
Function,
// eslint-disable-next-line @typescript-eslint/naming-convention
@ -7,14 +10,20 @@ enum Type {
Macro
}
class Sym {
export class Sym {
private type: Type;
private scope: Scope; // all symbols know what scope contains them
private name: string;
private uri: string;
private line: number;
private column: number;
constructor(name: string, type: Type) {
constructor(name: string, type: Type, uri: string, line: number, column: number) {
this.name = name;
this.type = type;
this.uri = uri;
this.line = line;
this.column = column;
}
getName() {
@ -28,4 +37,20 @@ class Sym {
setScope(scope: Scope) {
this.scope = scope;
}
}
getLocation(): Location {
return {
uri: this.uri,
range: {
start: {
line: this.line,
character: this.column,
},
end: {
line: this.line,
character: this.column + this.name.length
}
}
};
}
}

View File

@ -1,11 +1,99 @@
import * as cp from 'child_process';
import { documents } from './server';
import { existsSync, fstat } from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath, pathToFileURL } from 'url';
import antlr4 from './parser/antlr4/index.js';
import CMakeLexer from "./parser/CMakeLexer";
import CMakeParser from "./parser/CMakeParser";
import InputStream from './parser/antlr4/InputStream';
export type Entries = [string, string, string, string];
export type CMakeVersion = {
version: string,
major: number,
minor: number,
patch: number
};
export let cmakeVersion: CMakeVersion = getCMakeVersion();
export function getBuiltinEntries(): Entries {
const args = ['cmake', '--help-module-list', '--help-policy-list',
'--help-variable-list', '--help-property-list'];
const cmd: string = args.join(' ');
const output = cp.execSync(cmd, { encoding: 'utf-8' });
return output.trim().split('\n\n\n') as Entries;
}
}
export function getCMakeVersion(): CMakeVersion {
const args = ['cmake', '--version'];
const output: string = cp.execSync(args.join(' '), { encoding: 'utf-8' });
const regexp: RegExp = /(\d+)\.(\d+)\.(\d+)/;
const res = output.match(regexp);
return {
version: res[0],
major: parseInt(res[1]),
minor: parseInt(res[2]),
patch: parseInt(res[3])
};
}
export function getFileContext(uri: string) {
const document = documents.get(uri);
let text: string;
if (document) {
text = document.getText();
} else {
text = fs.readFileSync(fileURLToPath(uri), { encoding: 'utf-8' });
}
const input: InputStream = antlr4.CharStreams.fromString(text);
const lexer = new CMakeLexer(input);
const tokenStream = new antlr4.CommonTokenStream(lexer);
const parser = new CMakeParser(tokenStream);
return parser.file();
}
export function getIncludeFileUri(currentFileUri: string, includeFileName: string): string {
const currentFilePath: string = fileURLToPath(currentFileUri);
const includeFilePath: string = path.dirname(currentFilePath) + path.sep + includeFileName;
// const includeFileUri: string = filePathToURL;
if (existsSync(includeFilePath)) {
const index = currentFileUri.lastIndexOf('/');
const includeFileUri = currentFileUri.slice(0, index) + path.sep + includeFileName;
return includeFileUri;
}
// name is a cmake module
const cmakePath: string = which('cmake');
if (cmakePath === null) {
return null;
}
const moduleDir = 'cmake-' + cmakeVersion.major + '.' + cmakeVersion.minor;
const resPath = path.join(cmakePath, '..', 'share', moduleDir, 'Modules', includeFileName) + '.cmake';
return pathToFileURL(resPath).toString();
}
function which(cmd: string): string {
let command: string;
if (os.type() === 'Windows_NT') {
command = cmd + ".exe";
} else {
command = cmd;
}
for (const dir of process.env.PATH.split(path.sep)) {
const absPath: string = dir + path.sep + command;
if (existsSync(absPath)) {
return absPath;
}
}
return null;
}