diff --git a/CHANGELOG.md b/CHANGELOG.md index 758a5fd36..b61263b41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,10 +15,12 @@ #### :bug: Bug fix - Fix rewatch lockfile detection on Windows. https://github.com/rescript-lang/rescript-vscode/pull/1160 +- Override default `initialConfiguration` with user specific config. https://github.com/rescript-lang/rescript-vscode/pull/1162 #### :nail_care: Polish - Resolve symlinks when finding platform binaries. https://github.com/rescript-lang/rescript-vscode/pull/1154 +- Use `window/logMessage` in LSP Server for logging. https://github.com/rescript-lang/rescript-vscode/pull/1162 ## 1.70.0 diff --git a/package.json b/package.json index 5709e3b32..7dc17150e 100644 --- a/package.json +++ b/package.json @@ -195,11 +195,6 @@ "default": false, "description": "(beta/experimental) Enable incremental type checking across files, so that unsaved file A gets access to unsaved file B." }, - "rescript.settings.incrementalTypechecking.debugLogging": { - "type": "boolean", - "default": false, - "description": "(debug) Enable debug logging (ends up in the extension output)." - }, "rescript.settings.cache.projectConfig.enable": { "type": "boolean", "default": true, @@ -233,6 +228,17 @@ "type": "boolean", "default": true, "description": "Show compile status in the status bar (compiling/errors/warnings/success)." + }, + "rescript.settings.logLevel": { + "type": "string", + "enum": [ + "error", + "warn", + "info", + "log" + ], + "default": "info", + "description": "Verbosity of ReScript language server logs sent to the Output channel." } } }, diff --git a/server/src/bsc-args/rewatch.ts b/server/src/bsc-args/rewatch.ts index 4cd229d5a..e38b8d2bb 100644 --- a/server/src/bsc-args/rewatch.ts +++ b/server/src/bsc-args/rewatch.ts @@ -3,12 +3,10 @@ import * as utils from "../utils"; import * as cp from "node:child_process"; import * as p from "vscode-languageserver-protocol"; import semver from "semver"; -import { - debug, - IncrementallyCompiledFileInfo, -} from "../incrementalCompilation"; +import { IncrementallyCompiledFileInfo } from "../incrementalCompilation"; import type { projectFiles } from "../projectFiles"; import { jsonrpcVersion } from "../constants"; +import { getLogger } from "../logger"; export type RewatchCompilerArgs = { compiler_args: Array; @@ -68,15 +66,11 @@ export async function getRewatchBscArgs( if (rescriptRewatchPath != null) { rewatchPath = rescriptRewatchPath; - if (debug()) { - console.log( - `Found rewatch binary bundled with v12: ${rescriptRewatchPath}`, - ); - } + getLogger().log( + `Found rewatch binary bundled with v12: ${rescriptRewatchPath}`, + ); } else { - if (debug()) { - console.log("Did not find rewatch binary bundled with v12"); - } + getLogger().log("Did not find rewatch binary bundled with v12"); } const rewatchArguments = semver.satisfies( diff --git a/server/src/config.ts b/server/src/config.ts index 97b89985a..874b95696 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -4,6 +4,7 @@ export type send = (msg: Message) => void; export interface extensionConfiguration { askToStartBuild?: boolean; + logLevel?: "error" | "warn" | "info" | "log"; inlayHints?: { enable?: boolean; maxLength?: number | null; @@ -19,7 +20,6 @@ export interface extensionConfiguration { incrementalTypechecking?: { enable?: boolean; acrossFiles?: boolean; - debugLogging?: boolean; }; cache?: { projectConfig?: { @@ -28,33 +28,35 @@ export interface extensionConfiguration { }; } -// All values here are temporary, and will be overridden as the server is -// initialized, and the current config is received from the client. -let config: { extensionConfiguration: extensionConfiguration } = { - extensionConfiguration: { - askToStartBuild: true, - inlayHints: { - enable: false, - maxLength: 25, - }, - codeLens: false, - binaryPath: null, - platformPath: null, - signatureHelp: { - enabled: true, - forConstructorPayloads: true, - }, - incrementalTypechecking: { +export const initialConfiguration: extensionConfiguration = { + askToStartBuild: true, + logLevel: "info", + inlayHints: { + enable: false, + maxLength: 25, + }, + codeLens: false, + binaryPath: null, + platformPath: null, + signatureHelp: { + enabled: true, + forConstructorPayloads: true, + }, + incrementalTypechecking: { + enable: true, + acrossFiles: false, + }, + cache: { + projectConfig: { enable: true, - acrossFiles: false, - debugLogging: false, - }, - cache: { - projectConfig: { - enable: true, - }, }, }, }; +// All values here are temporary, and will be overridden as the server is +// initialized, and the current config is received from the client. +let config: { extensionConfiguration: extensionConfiguration } = { + extensionConfiguration: initialConfiguration, +}; + export default config; diff --git a/server/src/incrementalCompilation.ts b/server/src/incrementalCompilation.ts index 624b3d47e..030cdea8e 100644 --- a/server/src/incrementalCompilation.ts +++ b/server/src/incrementalCompilation.ts @@ -14,12 +14,7 @@ import { getRewatchBscArgs, RewatchCompilerArgs } from "./bsc-args/rewatch"; import { BsbCompilerArgs, getBsbBscArgs } from "./bsc-args/bsb"; import { getCurrentCompilerDiagnosticsForFile } from "./server"; import { NormalizedPath } from "./utils"; - -export function debug() { - return ( - config.extensionConfiguration.incrementalTypechecking?.debugLogging ?? false - ); -} +import { getLogger } from "./logger"; const INCREMENTAL_FOLDER_NAME = "___incremental"; const INCREMENTAL_FILE_FOLDER_LOCATION = path.join( @@ -96,13 +91,11 @@ export function incrementalCompilationFileChanged(changedPath: NormalizedPath) { if (filePath != null) { const entry = incrementallyCompiledFileInfo.get(filePath); if (entry != null) { - if (debug()) { - console.log("[watcher] Cleaning up incremental files for " + filePath); - } + getLogger().log( + "[watcher] Cleaning up incremental files for " + filePath, + ); if (entry.compilation != null) { - if (debug()) { - console.log("[watcher] Was compiling, killing"); - } + getLogger().log("[watcher] Was compiling, killing"); clearTimeout(entry.compilation.timeout); entry.killCompilationListeners.forEach((cb) => cb()); entry.compilation = null; @@ -129,9 +122,7 @@ export function removeIncrementalFileFolder( } export function recreateIncrementalFileFolder(projectRootPath: NormalizedPath) { - if (debug()) { - console.log("Recreating incremental file folder"); - } + getLogger().log("Recreating incremental file folder"); removeIncrementalFileFolder(projectRootPath, () => { fs.mkdir( path.resolve(projectRootPath, INCREMENTAL_FILE_FOLDER_LOCATION), @@ -153,9 +144,7 @@ export function cleanUpIncrementalFiles( ? `${fileNameNoExt}-${namespace.result}` : fileNameNoExt; - if (debug()) { - console.log("Cleaning up incremental file assets for: " + fileNameNoExt); - } + getLogger().log("Cleaning up incremental file assets for: " + fileNameNoExt); fs.unlink( path.resolve( @@ -252,15 +241,14 @@ function triggerIncrementalCompilationOfFile( // New file const projectRootPath = utils.findProjectRootOfFile(filePath); if (projectRootPath == null) { - if (debug()) - console.log("Did not find project root path for " + filePath); + getLogger().log("Did not find project root path for " + filePath); return; } // projectRootPath is already normalized (NormalizedPath) from findProjectRootOfFile // Use getProjectFile to verify the project exists const project = utils.getProjectFile(projectRootPath); if (project == null) { - if (debug()) console.log("Did not find open project for " + filePath); + getLogger().log("Did not find open project for " + filePath); return; } @@ -279,20 +267,19 @@ function triggerIncrementalCompilationOfFile( projectRootPath != null && utils.findProjectRootOfDir(projectRootPath) != null; - if (foundRewatchLockfileInProjectRoot && debug()) { - console.log( + if (foundRewatchLockfileInProjectRoot) { + getLogger().log( `Found rewatch/rescript lockfile in project root, treating as local package in workspace`, ); - } else if (!foundRewatchLockfileInProjectRoot && debug()) { - console.log( + } else { + getLogger().log( `Did not find rewatch/rescript lockfile in project root, assuming bsb`, ); } const bscBinaryLocation = project.bscBinaryLocation; if (bscBinaryLocation == null) { - if (debug()) - console.log("Could not find bsc binary location for " + filePath); + getLogger().log("Could not find bsc binary location for " + filePath); return; } const ext = filePath.endsWith(".resi") ? ".resi" : ".res"; @@ -401,12 +388,9 @@ async function figureOutBscArgs( ) { const project = projectsFiles.get(entry.project.rootPath); if (project?.rescriptVersion == null) { - if (debug()) { - console.log( - "Found no project (or ReScript version) for " + - entry.file.sourceFilePath, - ); - } + getLogger().log( + "Found no project (or ReScript version) for " + entry.file.sourceFilePath, + ); return null; } const res = await getBscArgs(send, entry); @@ -515,11 +499,9 @@ async function compileContents( callArgs = callArgsRetried; entry.project.callArgs = Promise.resolve(callArgsRetried); } else { - if (debug()) { - console.log( - "Could not figure out call args. Maybe build.ninja does not exist yet?", - ); - } + getLogger().log( + "Could not figure out call args. Maybe build.ninja does not exist yet?", + ); return; } } @@ -537,31 +519,27 @@ async function compileContents( entry.buildSystem === "bsb" ? entry.project.rootPath : path.resolve(entry.project.rootPath, c.compilerDirPartialPath); - if (debug()) { - console.log( - `About to invoke bsc from \"${cwd}\", used ${entry.buildSystem}`, - ); - console.log( - `${entry.project.bscBinaryLocation} ${callArgs.map((c) => `"${c}"`).join(" ")}`, - ); - } + getLogger().log( + `About to invoke bsc from \"${cwd}\", used ${entry.buildSystem}`, + ); + getLogger().log( + `${entry.project.bscBinaryLocation} ${callArgs.map((c) => `"${c}"`).join(" ")}`, + ); const process = cp.execFile( entry.project.bscBinaryLocation, callArgs, { cwd }, async (error, _stdout, stderr) => { if (!error?.killed) { - if (debug()) - console.log( - `Recompiled ${entry.file.sourceFileName} in ${ - (performance.now() - startTime) / 1000 - }s`, - ); + getLogger().log( + `Recompiled ${entry.file.sourceFileName} in ${ + (performance.now() - startTime) / 1000 + }s`, + ); } else { - if (debug()) - console.log( - `Compilation of ${entry.file.sourceFileName} was killed.`, - ); + getLogger().log( + `Compilation of ${entry.file.sourceFileName} was killed.`, + ); } let hasIgnoredErrorMessages = false; if ( @@ -569,9 +547,7 @@ async function compileContents( triggerToken != null && verifyTriggerToken(entry.file.sourceFilePath, triggerToken) ) { - if (debug()) { - console.log("Resetting compilation status."); - } + getLogger().log("Resetting compilation status."); // Reset compilation status as this compilation finished entry.compilation = null; const { result, codeActions } = await utils.parseCompilerLogOutput( @@ -706,9 +682,7 @@ export function handleUpdateOpenedFile( send: send, onCompilationFinished?: () => void, ) { - if (debug()) { - console.log("Updated: " + filePath); - } + getLogger().log("Updated: " + filePath); triggerIncrementalCompilationOfFile( filePath, fileContent, @@ -718,9 +692,7 @@ export function handleUpdateOpenedFile( } export function handleClosedFile(filePath: NormalizedPath) { - if (debug()) { - console.log("Closed: " + filePath); - } + getLogger().log("Closed: " + filePath); const entry = incrementallyCompiledFileInfo.get(filePath); if (entry == null) return; cleanUpIncrementalFiles(filePath, entry.project.rootPath); diff --git a/server/src/logger.ts b/server/src/logger.ts new file mode 100644 index 000000000..3653aa5e9 --- /dev/null +++ b/server/src/logger.ts @@ -0,0 +1,89 @@ +import * as p from "vscode-languageserver-protocol"; +import * as c from "./constants"; + +export type LogLevel = "error" | "warn" | "info" | "log"; + +const levelOrder: Record = { + log: 1, + info: 2, + warn: 3, + error: 4, +}; + +export interface Logger { + error(message: string): void; + warn(message: string): void; + info(message: string): void; + log(message: string): void; +} + +class NoOpLogger implements Logger { + error(_message: string): void {} + warn(_message: string): void {} + info(_message: string): void {} + log(_message: string): void {} +} + +class LSPLogger implements Logger { + private logLevel: LogLevel = "info"; + + constructor(private send: (msg: p.Message) => void) {} + + setLogLevel(level: LogLevel): void { + this.logLevel = level; + } + + private shouldLog(level: LogLevel): boolean { + return levelOrder[level] >= levelOrder[this.logLevel]; + } + + error(message: string): void { + if (this.shouldLog("error")) { + this.sendLogMessage(message, p.MessageType.Error); + } + } + + warn(message: string): void { + if (this.shouldLog("warn")) { + this.sendLogMessage(message, p.MessageType.Warning); + } + } + + info(message: string): void { + if (this.shouldLog("info")) { + this.sendLogMessage(message, p.MessageType.Info); + } + } + + log(message: string): void { + if (this.shouldLog("log")) { + this.sendLogMessage(message, p.MessageType.Log); + } + } + + private sendLogMessage(message: string, type: p.MessageType): void { + const notification: p.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "window/logMessage", + params: { type, message }, + }; + this.send(notification); + } +} + +// Default no-op instance +let instance: Logger = new NoOpLogger(); + +export function initializeLogger(send: (msg: p.Message) => void): void { + instance = new LSPLogger(send); +} + +export function setLogLevel(level: LogLevel): void { + if (instance instanceof LSPLogger) { + instance.setLogLevel(level); + } +} + +export function getLogger(): Logger { + return instance; +} diff --git a/server/src/server.ts b/server/src/server.ts index 77eaffcda..5eedee868 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -26,9 +26,30 @@ import { assert } from "console"; import { WorkspaceEdit } from "vscode-languageserver"; import { onErrorReported } from "./errorReporter"; import * as ic from "./incrementalCompilation"; -import config, { extensionConfiguration } from "./config"; +import config, { extensionConfiguration, initialConfiguration } from "./config"; import { projectsFiles } from "./projectFiles"; import { NormalizedPath } from "./utils"; +import { initializeLogger, getLogger, setLogLevel, LogLevel } from "./logger"; + +function applyUserConfiguration(configuration: extensionConfiguration) { + // We always want to spread the initial configuration to ensure all defaults are respected. + config.extensionConfiguration = Object.assign( + {}, + initialConfiguration, + configuration, + ); + + const level = config.extensionConfiguration.logLevel; + + if ( + level === "error" || + level === "warn" || + level === "info" || + level === "log" + ) { + setLogLevel(level); + } +} // Absolute paths to all the workspace folders // Configured during the initialize request @@ -322,31 +343,29 @@ let sendCompilationFinishedMessage = () => { send(notification); }; -let debug = false; - let syncProjectConfigCache = async (rootPath: utils.NormalizedPath) => { try { - if (debug) console.log("syncing project config cache for " + rootPath); + getLogger().log("syncing project config cache for " + rootPath); await utils.runAnalysisAfterSanityCheck(rootPath, [ "cache-project", rootPath, ]); - if (debug) console.log("OK - synced project config cache for " + rootPath); + getLogger().log("OK - synced project config cache for " + rootPath); } catch (e) { - if (debug) console.error(e); + getLogger().error(String(e)); } }; let deleteProjectConfigCache = async (rootPath: utils.NormalizedPath) => { try { - if (debug) console.log("deleting project config cache for " + rootPath); + getLogger().log("deleting project config cache for " + rootPath); await utils.runAnalysisAfterSanityCheck(rootPath, [ "cache-delete", rootPath, ]); - if (debug) console.log("OK - deleted project config cache for " + rootPath); + getLogger().log("OK - deleted project config cache for " + rootPath); } catch (e) { - if (debug) console.error(e); + getLogger().error(String(e)); } }; @@ -377,7 +396,7 @@ async function onWorkspaceDidChangeWatchedFiles( sendCodeLensRefresh(); } } catch { - console.log("Error while sending updated diagnostics"); + getLogger().error("Error while sending updated diagnostics"); } } else { ic.incrementalCompilationFileChanged( @@ -517,6 +536,9 @@ let closedFile = async (fileUri: utils.FileURI) => { }; let updateOpenedFile = (fileUri: utils.FileURI, fileContent: string) => { + getLogger().info( + `Updating opened file ${fileUri}, incremental TC enabled: ${config.extensionConfiguration.incrementalTypechecking?.enable}`, + ); let filePath = utils.uriToNormalizedPath(fileUri); assert(stupidFileContentCache.has(filePath)); stupidFileContentCache.set(filePath, fileContent); @@ -548,10 +570,12 @@ export default function listen(useStdio = false) { let reader = new rpc.StreamMessageReader(process.stdin); // proper `this` scope for writer send = (msg: p.Message) => writer.write(msg); + initializeLogger(send); reader.listen(onMessage); } else { // proper `this` scope for process send = (msg: p.Message) => process.send!(msg); + initializeLogger(send); process.on("message", onMessage); } } @@ -1453,6 +1477,7 @@ async function onMessage(msg: p.Message) { }; send(response); } else if (msg.method === "initialize") { + getLogger().info("Received initialize request from client."); // Save initial configuration, if present let initParams = msg.params as InitializeParams; for (const workspaceFolder of initParams.workspaceFolders || []) { @@ -1464,7 +1489,7 @@ async function onMessage(msg: p.Message) { ?.extensionConfiguration as extensionConfiguration | undefined; if (initialConfiguration != null) { - config.extensionConfiguration = initialConfiguration; + applyUserConfiguration(initialConfiguration); } // These are static configuration options the client can set to enable certain @@ -1674,7 +1699,7 @@ async function onMessage(msg: p.Message) { extensionConfiguration | null | undefined, ]; if (configuration != null) { - config.extensionConfiguration = configuration; + applyUserConfiguration(configuration); } } } else if (