diff --git a/server/src/server.ts b/server/src/server.ts index 3c050e1..44252c3 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -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 { diff --git a/server/src/symbolTable/goToDefination.ts b/server/src/symbolTable/goToDefination.ts index 852373e..f14a369 100644 --- a/server/src/symbolTable/goToDefination.ts +++ b/server/src/symbolTable/goToDefination.ts @@ -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 = new Map(); +export const topScope: FileScope = new FileScope(null); + +/** + * key: ___ + * value: Location + * + * NOTE: line and column are numbers start from zero + */ +export const refToDef: Map = 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); } -} \ No newline at end of file + + 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 + } +} diff --git a/server/src/symbolTable/scope.ts b/server/src/symbolTable/scope.ts index fc7c6f2..9e30f91 100644 --- a/server/src/symbolTable/scope.ts +++ b/server/src/symbolTable/scope.ts @@ -1,48 +1,52 @@ - -class Scope { +import { Sym, Type } from './symbol'; +export class Scope { private enclosingScope: Scope; - private symbols: Map; + private variables: Map; + private commands: Map; constructor(enclosingScope: Scope) { this.enclosingScope = enclosingScope; - this.symbols = new Map(); + this.variables = new Map(); + this.commands = new Map(); } - 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); } -} \ No newline at end of file +} diff --git a/server/src/symbolTable/symbol.ts b/server/src/symbolTable/symbol.ts index a64531d..76b5812 100644 --- a/server/src/symbolTable/symbol.ts +++ b/server/src/symbolTable/symbol.ts @@ -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; } -} \ No newline at end of file + + getLocation(): Location { + return { + uri: this.uri, + range: { + start: { + line: this.line, + character: this.column, + }, + end: { + line: this.line, + character: this.column + this.name.length + } + } + }; + } +} diff --git a/server/src/utils.ts b/server/src/utils.ts index 8199707..f5dd27b 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -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; -} \ No newline at end of file +} + +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; +}