diff --git a/server/src/logging.ts b/server/src/logging.ts new file mode 100644 index 0000000..16db54b --- /dev/null +++ b/server/src/logging.ts @@ -0,0 +1,183 @@ +/** + * Logging utilities + * copy from vscode-cmake-tools + */ + +import * as node_fs from 'fs'; +import * as path from 'path'; + +/** Logging levels */ +export enum LogLevel { + Debug, + Info, + Warning, + Error, + Off, +} + +type LogLevelKey = 'debug' | 'info' | 'warning' | 'error' | 'off'; + +/** + * Get the name of a logging level + * @param level A logging level + */ +function levelName(level: LogLevel): LogLevelKey { + switch (level) { + case LogLevel.Debug: + return 'debug'; + case LogLevel.Info: + return 'info'; + case LogLevel.Warning: + return 'warning'; + case LogLevel.Error: + return 'error'; + case LogLevel.Off: + return 'off'; + } +} + +/** + * Determine if logging is enabled for the given LogLevel + * @param level The log level to check + */ +function levelEnabled(level: LogLevel): boolean { + // const strlevel = vscode.workspace.getConfiguration('cmake').get('loggingLevel', 'info'); + const strLevel = extSettings.loggingLevel; + switch (strLevel) { + case 'debug': + return level >= LogLevel.Debug; + case 'info': + return level >= LogLevel.Info; + case 'warning': + return level >= LogLevel.Warning; + case 'error': + return level >= LogLevel.Error; + case 'off': + return level >= LogLevel.Off; + default: + console.error('Invalid logging level in settings.json'); + return true; + } +} + +export interface Stringable { + toString(): string; + toLocaleString(): string; +} + +let _LOGGER: NodeJS.WritableStream; + +export function logFilePath(): string { + return path.join(paths.dataDir, 'log.txt'); +} + +async function _openLogFile() { + if (!_LOGGER) { + const fpath = logFilePath(); + await mkdir_p(path.dirname(fpath)); + if (await exists(fpath)) { + _LOGGER = node_fs.createWriteStream(fpath, { flags: 'r+' }); + } else { + _LOGGER = node_fs.createWriteStream(fpath, { flags: 'w' }); + } + } + return _LOGGER; +} + +/** + * Manages and controls logging + */ +class SingletonLogger { + private readonly _logStream = _openLogFile(); + + private _log(level: LogLevel, ...args: Stringable[]) { + + if (!levelEnabled(level)) { + return; + } + + const user_message = args.map(a => a.toString()).join(' '); + const prefix = new Date().toISOString() + ` [${levelName(level)}]`; + const raw_message = `${prefix} ${user_message}`; + switch (level) { + // case LogLevel.Trace: + case LogLevel.Debug: + case LogLevel.Info: + // case LogLevel.Note: + if (process.env['CMT_QUIET_CONSOLE'] !== '1') { + console.info('[cmake-intellisence]', raw_message); + } + break; + case LogLevel.Warning: + console.warn('[cmake-intellisence]', raw_message); + break; + case LogLevel.Error: + console.error('[cmake-intellisence]', raw_message); + break; + } + // Write to the logfile asynchronously. + this._logStream.then(strm => strm.write(raw_message + '\n')).catch(e => { + console.error('Unhandled error while writing cmake-intellisence log file', e); + }); + } + + debug(...args: Stringable[]) { + this._log(LogLevel.Debug, ...args); + } + + info(...args: Stringable[]) { + this._log(LogLevel.Info, ...args); + } + + warning(...args: Stringable[]) { + this._log(LogLevel.Warning, ...args); + } + + error(...args: Stringable[]) { + this._log(LogLevel.Error, ...args); + } + + private static _inst: SingletonLogger | null = null; + + static instance(): SingletonLogger { + if (SingletonLogger._inst === null) { + SingletonLogger._inst = new SingletonLogger(); + } + return SingletonLogger._inst; + } +} + +export class Logger { + constructor(readonly _tag: string) { } + get tag() { + return `[${this._tag}]`; + } + + debug(...args: Stringable[]) { + SingletonLogger.instance().debug(this.tag, ...args); + } + + info(...args: Stringable[]) { + SingletonLogger.instance().info(this.tag, ...args); + } + + warning(...args: Stringable[]) { + SingletonLogger.instance().warning(this.tag, ...args); + } + + error(...args: Stringable[]) { + SingletonLogger.instance().error(this.tag, ...args); + } + + static logTestName(suite?: string, test?: string) { + SingletonLogger.instance().info('-----------------------------------------------------------------------'); + SingletonLogger.instance().info(`Beginning test: ${suite ?? 'unknown suite'} - ${test ?? 'unknown test'}`); + } +} + +export function createLogger(tag: string) { + return new Logger(tag); +} + +import paths, { mkdir_p, exists } from './paths'; +import { extSettings } from './settings'; diff --git a/server/src/paths.ts b/server/src/paths.ts new file mode 100644 index 0000000..589f7a8 --- /dev/null +++ b/server/src/paths.ts @@ -0,0 +1,191 @@ +/** + * This module defines important directories and paths to the extension + * copy from vscode-cmake-tools + */ + +import * as path from 'path'; +import * as fs from 'fs'; +import * as util from 'util'; + +const promisify = util.promisify; +export const stat = promisify(fs.stat); +export const mkdir = promisify(fs.mkdir); + +/** + * Try and stat() a file/folder. If stat() fails for *any reason*, returns `null`. + * @param filePath The file to try and stat() + */ +export async function tryStat(filePath: fs.PathLike): Promise { + try { + return await stat(filePath); + } catch (_e) { + // Don't even bother with the error. Any number of things might have gone + // wrong. Probably one of: Non-existing file, bad permissions, bad path. + return null; + } +} + +export async function exists(filePath: string): Promise { + const stat = await tryStat(filePath); + return stat !== null; +} + +/** + * Creates a directory and all parent directories recursively. If the file + * already exists, and is not a directory, just return. + * @param fspath The directory to create + */ +export async function mkdir_p(fspath: string): Promise { + const parent = path.dirname(fspath); + if (!await exists(parent)) { + await mkdir_p(parent); + } else { + if (!(await stat(parent)).isDirectory()) { + throw new Error(`cannot.create.path', 'Cannot create ${fspath}: ${fspath} is a non-directory`); + } + } + if (!await exists(fspath)) { + await mkdir(fspath); + } else { + if (!(await stat(fspath)).isDirectory()) { + throw new Error(`cannot.create.directory', 'Cannot create directory ${fspath}. It exists, and is not a directory!`); + } + } +} + +class WindowsEnvironment { + get AppData(): string | undefined { + return process.env['APPDATA']; + } + + get LocalAppData(): string | undefined { + return process.env['LOCALAPPDATA']; + } + + get AllUserProfile(): string | undefined { + return process.env['ProgramData']; + } + + get ComSpec(): string { + let comSpec = process.env['ComSpec']; + + if (undefined === comSpec) { + comSpec = this.SystemRoot! + '\\system32\\cmd.exe'; + } + + return comSpec; + } + + get HomeDrive(): string | undefined { + return process.env['HOMEDRIVE']; + } + + get HomePath(): string | undefined { + return process.env['HOMEPATH']; + } + + get ProgramFilesX86(): string | undefined { + return process.env['ProgramFiles(x86)']; + } + + get ProgramFiles(): string | undefined { + return process.env['ProgramFiles']; + } + + get SystemDrive(): string | undefined { + return process.env['SystemDrive']; + } + + get SystemRoot(): string | undefined { + return process.env['SystemRoot']; + } + + get Temp(): string | undefined { + return process.env['TEMP']; + } +} + +/** + * Directory class. + */ +class Paths { + private _ninjaPath?: string; + + readonly windows: WindowsEnvironment = new WindowsEnvironment(); + + /** + * The current user's home directory + */ + get userHome(): string { + if (process.platform === 'win32') { + return path.join(process.env['HOMEDRIVE'] || 'C:', process.env['HOMEPATH'] || 'Users\\Public'); + } else { + return process.env['HOME'] || process.env['PROFILE']!; + } + } + + /** + * The user-local data directory. This is where user-specific persistent + * application data should be stored. + */ + get userLocalDir(): string { + + if (process.platform === 'win32') { + return this.windows.LocalAppData!; + } else { + const xdg_dir = process.env['XDG_DATA_HOME']; + if (xdg_dir) { + return xdg_dir; + } + const home = this.userHome; + return path.join(home, '.local/share'); + } + } + + get userRoamingDir(): string { + if (process.platform === 'win32') { + return this.windows.AppData!; + } else { + const xdg_dir = process.env['XDG_CONFIG_HOME']; + if (xdg_dir) { + return xdg_dir; + } + const home = this.userHome; + return path.join(home, '.config'); + } + } + + /** + * The directory where CMake Tools should store user-specific persistent + * data. + */ + get dataDir(): string { + return path.join(this.userLocalDir, 'cmake-intellisence'); + } + + /** + * The "roaming" directory where CMake Tools stores roaming configuration + * data. + */ + get roamingDataDir(): string { + return path.join(this.userRoamingDir, 'cmake-intellisence'); + } + + /** + * Get the platform-specific temporary directory + */ + get tmpDir(): string { + if (process.platform === 'win32') { + return this.windows.Temp!; + } else { + return '/tmp'; + } + } + + get ninjaPath() { + return this._ninjaPath; + } +} + +const paths = new Paths(); +export default paths; diff --git a/server/src/server.ts b/server/src/server.ts index e4146a5..b45b68b 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -29,6 +29,7 @@ import { getTokenBuilder, getTokenModifiers, getTokenTypes, SemanticListener, to import { extSettings } from './settings'; import { DefinationListener, incToBaseDir, refToDef, topScope } from './symbolTable/goToDefination'; import { getFileContext } from './utils'; +import { createLogger } from './logging'; type Word = { text: string, @@ -47,6 +48,7 @@ export const connection = createConnection(ProposedFeatures.all); // Create a simple text document manager. export const documents: TextDocuments = new TextDocuments(TextDocument); +export const logger = createLogger('server'); connection.onInitialize(async (params: InitializeParams) => { initParams = params; @@ -312,6 +314,7 @@ connection.onDefinition((params: DefinitionParams) => { if (refToDef.has(wordPos)) { resolve(refToDef.get(wordPos)); } else { + logger.warning(`can't find defination, word: ${word.text}, wordPos: ${wordPos}`); return null; } }); @@ -481,6 +484,7 @@ function getProposals(word: string, kind: CompletionItemKind, dataSource: string // Make the text document manager listen on the connection // for open, change and close text document events documents.listen(connection); +logger.info('listen on connection'); // Listen on the connection connection.listen(); diff --git a/server/src/utils.ts b/server/src/utils.ts index 60dea40..54d8d2f 100644 --- a/server/src/utils.ts +++ b/server/src/utils.ts @@ -7,7 +7,7 @@ import antlr4 from './parser/antlr4/index.js'; import InputStream from './parser/antlr4/InputStream'; import CMakeLexer from "./parser/CMakeLexer"; import CMakeParser from "./parser/CMakeParser"; -import { documents } from './server'; +import { documents, logger } from './server'; export function getFileContext(uri: URI) { const document = documents.get(uri.toString()); @@ -28,6 +28,8 @@ export function getSubCMakeListsUri(baseDir: URI, subDir: string): URI { const subCMakeListsUri: URI = Utils.joinPath(baseDir, subDir, 'CMakeLists.txt'); if (existsSync(subCMakeListsUri.fsPath)) { return subCMakeListsUri; + } else { + logger.error('getSubCMakeListsUri:', subCMakeListsUri.fsPath, 'not exist'); } return null; @@ -37,6 +39,8 @@ export function getIncludeFileUri(baseDir: URI, includeFileName: string): URI { const incFileUri: URI = Utils.joinPath(baseDir, includeFileName); if (existsSync(incFileUri.fsPath)) { return incFileUri; + } else { + logger.error('getIncludeFileUri:', incFileUri.fsPath, 'not exist'); } const cmakePath: string = which('cmake'); @@ -50,6 +54,8 @@ export function getIncludeFileUri(baseDir: URI, includeFileName: string): URI { if (existsSync(resPath)) { // return pathToFileURL(resPath).toString(); return URI.file(resPath); + } else { + logger.error('getIncludeFileUri:', resPath, 'not exist'); } return null;