Language feature: go to Definition
This commit is contained in:
parent
6bf06473dd
commit
99156d3ed5
|
@ -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[]> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue