Skip to content

Commit ecf4be4

Browse files
authored
Add status bar compilation status item (#1119)
* add status bar compilation status item * changelog * mention in readme
1 parent 9d9bcf5 commit ecf4be4

File tree

5 files changed

+253
-0
lines changed

5 files changed

+253
-0
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@
1616

1717
- Protect against trying to read non-existant `.compiler.log`. https://github.com/rescript-lang/rescript-vscode/pull/1116
1818

19+
#### :rocket: New Feature
20+
21+
- Add status bar item tracking compilation state. https://github.com/rescript-lang/rescript-vscode/pull/1119
22+
1923
## 1.64.0
2024

2125
#### :rocket: New Feature

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ You'll find all ReScript specific settings under the scope `rescript.settings`.
105105
| Inlay Hints (experimental) | This allows an editor to place annotations inline with text to display type hints. Enable using `rescript.settings.inlayHints.enable: true` |
106106
| 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` |
107107
| Signature Help | This tells the editor to show signature help when you're writing function calls. Enable using `rescript.settings.signatureHelp.enabled: true` |
108+
| 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. |
108109

109110
**Default settings:**
110111

@@ -126,6 +127,9 @@ You'll find all ReScript specific settings under the scope `rescript.settings`.
126127

127128
// Enable (experimental) code lens for function definitions.
128129
"rescript.settings.codeLens": true
130+
131+
// Show compile status in the status bar (compiling/errors/warnings/success)
132+
"rescript.settings.compileStatus.enable": true
129133
```
130134

131135
## 🚀 Code Analyzer

client/src/extension.ts

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
CodeActionKind,
1515
Diagnostic,
1616
} from "vscode";
17+
import { ThemeColor } from "vscode";
1718

1819
import {
1920
LanguageClient,
@@ -188,6 +189,120 @@ export function activate(context: ExtensionContext) {
188189
StatusBarAlignment.Right,
189190
);
190191

192+
let compilationStatusBarItem = window.createStatusBarItem(
193+
StatusBarAlignment.Right,
194+
);
195+
context.subscriptions.push(compilationStatusBarItem);
196+
197+
let compileStatusEnabled: boolean = workspace
198+
.getConfiguration("rescript.settings")
199+
.get<boolean>("compileStatus.enable", true);
200+
201+
type ClientCompileStatus = {
202+
status: "compiling" | "success" | "error" | "warning";
203+
project: string;
204+
errorCount: number;
205+
warningCount: number;
206+
};
207+
const projectStatuses: Map<string, ClientCompileStatus> = new Map();
208+
209+
const refreshCompilationStatusItem = () => {
210+
if (!compileStatusEnabled) {
211+
compilationStatusBarItem.hide();
212+
compilationStatusBarItem.tooltip = undefined;
213+
compilationStatusBarItem.backgroundColor = undefined;
214+
compilationStatusBarItem.command = undefined;
215+
return;
216+
}
217+
const entries = [...projectStatuses.values()];
218+
const compiling = entries.filter((e) => e.status === "compiling");
219+
const errors = entries.filter((e) => e.status === "error");
220+
const warnings = entries.filter((e) => e.status === "warning");
221+
222+
if (compiling.length > 0) {
223+
compilationStatusBarItem.text = `$(loading~spin) ReScript`;
224+
compilationStatusBarItem.tooltip = compiling
225+
.map((e) => e.project)
226+
.join(", ");
227+
compilationStatusBarItem.backgroundColor = undefined;
228+
compilationStatusBarItem.command = undefined;
229+
compilationStatusBarItem.show();
230+
return;
231+
}
232+
233+
if (errors.length > 0) {
234+
compilationStatusBarItem.text = `$(alert) ReScript: Failed`;
235+
compilationStatusBarItem.backgroundColor = new ThemeColor(
236+
"statusBarItem.errorBackground",
237+
);
238+
compilationStatusBarItem.command = "rescript-vscode.showProblems";
239+
const byProject = errors.map((e) => `${e.project} (${e.errorCount})`);
240+
compilationStatusBarItem.tooltip = `Failed: ${byProject.join(", ")}`;
241+
compilationStatusBarItem.show();
242+
return;
243+
}
244+
245+
if (warnings.length > 0) {
246+
compilationStatusBarItem.text = `$(warning) ReScript: Warnings`;
247+
compilationStatusBarItem.backgroundColor = undefined;
248+
compilationStatusBarItem.color = new ThemeColor(
249+
"statusBarItem.warningBackground",
250+
);
251+
compilationStatusBarItem.command = "rescript-vscode.showProblems";
252+
const byProject = warnings.map((e) => `${e.project} (${e.warningCount})`);
253+
compilationStatusBarItem.tooltip = `Warnings: ${byProject.join(", ")}`;
254+
compilationStatusBarItem.show();
255+
return;
256+
}
257+
258+
const successes = entries.filter((e) => e.status === "success");
259+
if (successes.length > 0) {
260+
// Compact success display: project label plus a green check emoji
261+
compilationStatusBarItem.text = `$(check) ReScript: Ok`;
262+
compilationStatusBarItem.backgroundColor = undefined;
263+
compilationStatusBarItem.color = null;
264+
compilationStatusBarItem.command = undefined;
265+
const projects = successes.map((e) => e.project).join(", ");
266+
compilationStatusBarItem.tooltip = projects
267+
? `Compilation Succeeded: ${projects}`
268+
: `Compilation Succeeded`;
269+
compilationStatusBarItem.show();
270+
return;
271+
}
272+
273+
compilationStatusBarItem.hide();
274+
compilationStatusBarItem.tooltip = undefined;
275+
compilationStatusBarItem.backgroundColor = undefined;
276+
compilationStatusBarItem.command = undefined;
277+
};
278+
279+
context.subscriptions.push(
280+
client.onDidChangeState(({ newState }) => {
281+
if (newState === State.Running) {
282+
context.subscriptions.push(
283+
client.onNotification(
284+
"rescript/compilationStatus",
285+
(payload: {
286+
project: string;
287+
projectRootPath: string;
288+
status: "compiling" | "success" | "error" | "warning";
289+
errorCount: number;
290+
warningCount: number;
291+
}) => {
292+
projectStatuses.set(payload.projectRootPath, {
293+
status: payload.status,
294+
project: payload.project,
295+
errorCount: payload.errorCount,
296+
warningCount: payload.warningCount,
297+
});
298+
refreshCompilationStatusItem();
299+
},
300+
),
301+
);
302+
}
303+
}),
304+
);
305+
191306
let inCodeAnalysisState: {
192307
active: boolean;
193308
activatedFromDirectory: string | null;
@@ -256,6 +371,14 @@ export function activate(context: ExtensionContext) {
256371
customCommands.dumpDebug(context, debugDumpStatusBarItem);
257372
});
258373

374+
commands.registerCommand("rescript-vscode.showProblems", async () => {
375+
try {
376+
await commands.executeCommand("workbench.actions.view.problems");
377+
} catch {
378+
outputChannel.show();
379+
}
380+
});
381+
259382
commands.registerCommand("rescript-vscode.debug-dump-retrigger", () => {
260383
customCommands.dumpDebugRetrigger();
261384
});
@@ -346,6 +469,12 @@ export function activate(context: ExtensionContext) {
346469
) {
347470
commands.executeCommand("rescript-vscode.restart_language_server");
348471
} else {
472+
if (affectsConfiguration("rescript.settings.compileStatus.enable")) {
473+
compileStatusEnabled = workspace
474+
.getConfiguration("rescript.settings")
475+
.get<boolean>("compileStatus.enable", true);
476+
refreshCompilationStatusItem();
477+
}
349478
// Send a general message that configuration has updated. Clients
350479
// interested can then pull the new configuration as they see fit.
351480
client

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,11 @@
206206
],
207207
"default": null,
208208
"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."
209+
},
210+
"rescript.settings.compileStatus.enable": {
211+
"type": "boolean",
212+
"default": true,
213+
"description": "Show compile status in the status bar (compiling/errors/warnings/success)."
209214
}
210215
}
211216
},

server/src/server.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,36 @@ let codeActionsFromDiagnostics: codeActions.filesCodeActions = {};
6060
// will be properly defined later depending on the mode (stdio/node-rpc)
6161
let send: (msg: p.Message) => void = (_) => {};
6262

63+
type ProjectCompilationState = {
64+
active: boolean;
65+
startAt: number | null;
66+
lastSent: {
67+
status: "compiling" | "success" | "error" | "warning";
68+
errorCount: number;
69+
warningCount: number;
70+
} | null;
71+
timer: NodeJS.Timeout | null;
72+
};
73+
const projectCompilationStates: Map<string, ProjectCompilationState> =
74+
new Map();
75+
76+
type CompilationStatusPayload = {
77+
project: string;
78+
projectRootPath: string;
79+
status: "compiling" | "success" | "error" | "warning";
80+
errorCount: number;
81+
warningCount: number;
82+
};
83+
84+
const sendCompilationStatus = (payload: CompilationStatusPayload) => {
85+
const message: p.NotificationMessage = {
86+
jsonrpc: c.jsonrpcVersion,
87+
method: "rescript/compilationStatus",
88+
params: payload,
89+
};
90+
send(message);
91+
};
92+
6393
let findRescriptBinary = async (
6494
projectRootPath: p.DocumentUri | null,
6595
): Promise<string | null> => {
@@ -168,6 +198,86 @@ let sendUpdatedDiagnostics = async () => {
168198
}
169199
});
170200
}
201+
202+
try {
203+
const state = projectCompilationStates.get(projectRootPath) ?? {
204+
active: false,
205+
startAt: null,
206+
lastSent: null,
207+
timer: null,
208+
};
209+
210+
const lastStart = content.lastIndexOf("#Start");
211+
const lastDone = content.lastIndexOf("#Done");
212+
const isActive = lastStart > lastDone;
213+
214+
let errorCount = 0;
215+
let warningCount = 0;
216+
for (const [fileUri, diags] of Object.entries(filesAndErrors)) {
217+
const filePath = fileURLToPath(fileUri);
218+
if (filePath.startsWith(projectRootPath)) {
219+
for (const d of diags as v.Diagnostic[]) {
220+
if (d.severity === v.DiagnosticSeverity.Error) errorCount++;
221+
else if (d.severity === v.DiagnosticSeverity.Warning)
222+
warningCount++;
223+
}
224+
}
225+
}
226+
227+
const projectName = path.basename(projectRootPath);
228+
229+
const sendIfChanged = (
230+
status: "compiling" | "success" | "error" | "warning",
231+
) => {
232+
const last = state.lastSent;
233+
if (
234+
last == null ||
235+
last.status !== status ||
236+
last.errorCount !== errorCount ||
237+
last.warningCount !== warningCount
238+
) {
239+
sendCompilationStatus({
240+
project: projectName,
241+
projectRootPath,
242+
status,
243+
errorCount,
244+
warningCount,
245+
});
246+
state.lastSent = { status, errorCount, warningCount };
247+
}
248+
};
249+
250+
if (isActive) {
251+
if (!state.active) {
252+
state.active = true;
253+
state.startAt = Date.now();
254+
if (state.timer) clearTimeout(state.timer);
255+
state.timer = setTimeout(() => {
256+
const cur = projectCompilationStates.get(projectRootPath);
257+
if (cur && cur.active) {
258+
sendIfChanged("compiling");
259+
}
260+
}, 100);
261+
}
262+
} else {
263+
if (state.timer) {
264+
clearTimeout(state.timer);
265+
state.timer = null;
266+
}
267+
state.active = false;
268+
state.startAt = null;
269+
270+
if (errorCount > 0) {
271+
sendIfChanged("error");
272+
} else if (warningCount > 0) {
273+
sendIfChanged("warning");
274+
} else {
275+
sendIfChanged("success");
276+
}
277+
}
278+
279+
projectCompilationStates.set(projectRootPath, state);
280+
} catch {}
171281
}
172282
};
173283

@@ -188,6 +298,7 @@ let deleteProjectDiagnostics = (projectRootPath: string) => {
188298
});
189299

190300
projectsFiles.delete(projectRootPath);
301+
projectCompilationStates.delete(projectRootPath);
191302
if (config.extensionConfiguration.incrementalTypechecking?.enable) {
192303
ic.removeIncrementalFileFolder(projectRootPath);
193304
}

0 commit comments

Comments
 (0)