diff --git a/CHANGELOG.md b/CHANGELOG.md index 237abe834..ee9ab3c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,10 @@ - Protect against trying to read non-existant `.compiler.log`. https://github.com/rescript-lang/rescript-vscode/pull/1116 +#### :rocket: New Feature + +- Add status bar item tracking compilation state. https://github.com/rescript-lang/rescript-vscode/pull/1119 + ## 1.64.0 #### :rocket: New Feature diff --git a/README.md b/README.md index a46aa1cc5..3071788f9 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ You'll find all ReScript specific settings under the scope `rescript.settings`. | Inlay Hints (experimental) | This allows an editor to place annotations inline with text to display type hints. Enable using `rescript.settings.inlayHints.enable: true` | | Code Lens (experimental) | This tells the editor to add code lenses to function definitions, showing its full type above the definition. Enable using `rescript.settings.codeLens: true` | | Signature Help | This tells the editor to show signature help when you're writing function calls. Enable using `rescript.settings.signatureHelp.enabled: true` | +| Compile Status Indicator | Shows compile status in the status bar (Compiling, Errors, Warnings, Success). Toggle via `rescript.settings.compileStatus.enable`. Clicking in Error/Warning modes focuses the Problems view. | **Default settings:** @@ -126,6 +127,9 @@ You'll find all ReScript specific settings under the scope `rescript.settings`. // Enable (experimental) code lens for function definitions. "rescript.settings.codeLens": true + +// Show compile status in the status bar (compiling/errors/warnings/success) +"rescript.settings.compileStatus.enable": true ``` ## 🚀 Code Analyzer diff --git a/client/src/extension.ts b/client/src/extension.ts index 5778d0c7e..2c1c88b28 100644 --- a/client/src/extension.ts +++ b/client/src/extension.ts @@ -14,6 +14,7 @@ import { CodeActionKind, Diagnostic, } from "vscode"; +import { ThemeColor } from "vscode"; import { LanguageClient, @@ -188,6 +189,120 @@ export function activate(context: ExtensionContext) { StatusBarAlignment.Right, ); + let compilationStatusBarItem = window.createStatusBarItem( + StatusBarAlignment.Right, + ); + context.subscriptions.push(compilationStatusBarItem); + + let compileStatusEnabled: boolean = workspace + .getConfiguration("rescript.settings") + .get("compileStatus.enable", true); + + type ClientCompileStatus = { + status: "compiling" | "success" | "error" | "warning"; + project: string; + errorCount: number; + warningCount: number; + }; + const projectStatuses: Map = new Map(); + + const refreshCompilationStatusItem = () => { + if (!compileStatusEnabled) { + compilationStatusBarItem.hide(); + compilationStatusBarItem.tooltip = undefined; + compilationStatusBarItem.backgroundColor = undefined; + compilationStatusBarItem.command = undefined; + return; + } + const entries = [...projectStatuses.values()]; + const compiling = entries.filter((e) => e.status === "compiling"); + const errors = entries.filter((e) => e.status === "error"); + const warnings = entries.filter((e) => e.status === "warning"); + + if (compiling.length > 0) { + compilationStatusBarItem.text = `$(loading~spin) ReScript`; + compilationStatusBarItem.tooltip = compiling + .map((e) => e.project) + .join(", "); + compilationStatusBarItem.backgroundColor = undefined; + compilationStatusBarItem.command = undefined; + compilationStatusBarItem.show(); + return; + } + + if (errors.length > 0) { + compilationStatusBarItem.text = `$(alert) ReScript: Failed`; + compilationStatusBarItem.backgroundColor = new ThemeColor( + "statusBarItem.errorBackground", + ); + compilationStatusBarItem.command = "rescript-vscode.showProblems"; + const byProject = errors.map((e) => `${e.project} (${e.errorCount})`); + compilationStatusBarItem.tooltip = `Failed: ${byProject.join(", ")}`; + compilationStatusBarItem.show(); + return; + } + + if (warnings.length > 0) { + compilationStatusBarItem.text = `$(warning) ReScript: Warnings`; + compilationStatusBarItem.backgroundColor = undefined; + compilationStatusBarItem.color = new ThemeColor( + "statusBarItem.warningBackground", + ); + compilationStatusBarItem.command = "rescript-vscode.showProblems"; + const byProject = warnings.map((e) => `${e.project} (${e.warningCount})`); + compilationStatusBarItem.tooltip = `Warnings: ${byProject.join(", ")}`; + compilationStatusBarItem.show(); + return; + } + + const successes = entries.filter((e) => e.status === "success"); + if (successes.length > 0) { + // Compact success display: project label plus a green check emoji + compilationStatusBarItem.text = `$(check) ReScript: Ok`; + compilationStatusBarItem.backgroundColor = undefined; + compilationStatusBarItem.color = null; + compilationStatusBarItem.command = undefined; + const projects = successes.map((e) => e.project).join(", "); + compilationStatusBarItem.tooltip = projects + ? `Compilation Succeeded: ${projects}` + : `Compilation Succeeded`; + compilationStatusBarItem.show(); + return; + } + + compilationStatusBarItem.hide(); + compilationStatusBarItem.tooltip = undefined; + compilationStatusBarItem.backgroundColor = undefined; + compilationStatusBarItem.command = undefined; + }; + + context.subscriptions.push( + client.onDidChangeState(({ newState }) => { + if (newState === State.Running) { + context.subscriptions.push( + client.onNotification( + "rescript/compilationStatus", + (payload: { + project: string; + projectRootPath: string; + status: "compiling" | "success" | "error" | "warning"; + errorCount: number; + warningCount: number; + }) => { + projectStatuses.set(payload.projectRootPath, { + status: payload.status, + project: payload.project, + errorCount: payload.errorCount, + warningCount: payload.warningCount, + }); + refreshCompilationStatusItem(); + }, + ), + ); + } + }), + ); + let inCodeAnalysisState: { active: boolean; activatedFromDirectory: string | null; @@ -256,6 +371,14 @@ export function activate(context: ExtensionContext) { customCommands.dumpDebug(context, debugDumpStatusBarItem); }); + commands.registerCommand("rescript-vscode.showProblems", async () => { + try { + await commands.executeCommand("workbench.actions.view.problems"); + } catch { + outputChannel.show(); + } + }); + commands.registerCommand("rescript-vscode.debug-dump-retrigger", () => { customCommands.dumpDebugRetrigger(); }); @@ -346,6 +469,12 @@ export function activate(context: ExtensionContext) { ) { commands.executeCommand("rescript-vscode.restart_language_server"); } else { + if (affectsConfiguration("rescript.settings.compileStatus.enable")) { + compileStatusEnabled = workspace + .getConfiguration("rescript.settings") + .get("compileStatus.enable", true); + refreshCompilationStatusItem(); + } // Send a general message that configuration has updated. Clients // interested can then pull the new configuration as they see fit. client diff --git a/package.json b/package.json index 5811157e8..82e929196 100644 --- a/package.json +++ b/package.json @@ -206,6 +206,11 @@ ], "default": null, "description": "Path to the directory where platform-specific ReScript binaries are. You can use it if you haven't or don't want to use the installed ReScript from node_modules in your project." + }, + "rescript.settings.compileStatus.enable": { + "type": "boolean", + "default": true, + "description": "Show compile status in the status bar (compiling/errors/warnings/success)." } } }, diff --git a/server/src/server.ts b/server/src/server.ts index a386bcb10..3dc08eb90 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -60,6 +60,36 @@ let codeActionsFromDiagnostics: codeActions.filesCodeActions = {}; // will be properly defined later depending on the mode (stdio/node-rpc) let send: (msg: p.Message) => void = (_) => {}; +type ProjectCompilationState = { + active: boolean; + startAt: number | null; + lastSent: { + status: "compiling" | "success" | "error" | "warning"; + errorCount: number; + warningCount: number; + } | null; + timer: NodeJS.Timeout | null; +}; +const projectCompilationStates: Map = + new Map(); + +type CompilationStatusPayload = { + project: string; + projectRootPath: string; + status: "compiling" | "success" | "error" | "warning"; + errorCount: number; + warningCount: number; +}; + +const sendCompilationStatus = (payload: CompilationStatusPayload) => { + const message: p.NotificationMessage = { + jsonrpc: c.jsonrpcVersion, + method: "rescript/compilationStatus", + params: payload, + }; + send(message); +}; + let findRescriptBinary = async ( projectRootPath: p.DocumentUri | null, ): Promise => { @@ -168,6 +198,86 @@ let sendUpdatedDiagnostics = async () => { } }); } + + try { + const state = projectCompilationStates.get(projectRootPath) ?? { + active: false, + startAt: null, + lastSent: null, + timer: null, + }; + + const lastStart = content.lastIndexOf("#Start"); + const lastDone = content.lastIndexOf("#Done"); + const isActive = lastStart > lastDone; + + let errorCount = 0; + let warningCount = 0; + for (const [fileUri, diags] of Object.entries(filesAndErrors)) { + const filePath = fileURLToPath(fileUri); + if (filePath.startsWith(projectRootPath)) { + for (const d of diags as v.Diagnostic[]) { + if (d.severity === v.DiagnosticSeverity.Error) errorCount++; + else if (d.severity === v.DiagnosticSeverity.Warning) + warningCount++; + } + } + } + + const projectName = path.basename(projectRootPath); + + const sendIfChanged = ( + status: "compiling" | "success" | "error" | "warning", + ) => { + const last = state.lastSent; + if ( + last == null || + last.status !== status || + last.errorCount !== errorCount || + last.warningCount !== warningCount + ) { + sendCompilationStatus({ + project: projectName, + projectRootPath, + status, + errorCount, + warningCount, + }); + state.lastSent = { status, errorCount, warningCount }; + } + }; + + if (isActive) { + if (!state.active) { + state.active = true; + state.startAt = Date.now(); + if (state.timer) clearTimeout(state.timer); + state.timer = setTimeout(() => { + const cur = projectCompilationStates.get(projectRootPath); + if (cur && cur.active) { + sendIfChanged("compiling"); + } + }, 100); + } + } else { + if (state.timer) { + clearTimeout(state.timer); + state.timer = null; + } + state.active = false; + state.startAt = null; + + if (errorCount > 0) { + sendIfChanged("error"); + } else if (warningCount > 0) { + sendIfChanged("warning"); + } else { + sendIfChanged("success"); + } + } + + projectCompilationStates.set(projectRootPath, state); + } catch {} } }; @@ -188,6 +298,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => { }); projectsFiles.delete(projectRootPath); + projectCompilationStates.delete(projectRootPath); if (config.extensionConfiguration.incrementalTypechecking?.enable) { ic.removeIncrementalFileFolder(projectRootPath); }