Skip to content

Commit 951600d

Browse files
committed
Add real-time null-safety diagnostics to Monaco editor
- Integrated clang compiler to provide live error/warning feedback - Added onDidChangeModelContent listener with 500ms debounce - Parse clang diagnostic output and display as Monaco markers - Users now see red squiggles for errors as they type - Powered by null-safe clang running in WASM worker
1 parent 5116b27 commit 951600d

File tree

6 files changed

+1328
-151
lines changed

6 files changed

+1328
-151
lines changed

nullsafe-playground/clangd.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

nullsafe-playground/clangd.wasm

41.2 MB
Binary file not shown.

nullsafe-playground/index.html

Lines changed: 143 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -198,37 +198,50 @@
198198
}
199199

200200
.loading-spinner {
201-
display: inline-flex;
202-
gap: 3px;
203-
align-items: flex-end;
201+
display: inline-block;
202+
width: 14px;
204203
height: 14px;
204+
position: relative;
205+
transform: rotate(-90deg);
205206
}
206207

207-
.loading-bar {
208-
width: 3px;
209-
background: currentColor;
210-
border-radius: 2px;
211-
animation: loading-bounce 0.8s ease-in-out infinite;
212-
}
213-
214-
.loading-bar:nth-child(1) {
215-
animation-delay: 0s;
216-
}
217-
218-
.loading-bar:nth-child(2) {
219-
animation-delay: 0.15s;
220-
}
221-
222-
.loading-bar:nth-child(3) {
223-
animation-delay: 0.3s;
208+
.loading-spinner::before {
209+
content: '';
210+
position: absolute;
211+
top: 0;
212+
left: 0;
213+
right: 0;
214+
bottom: 0;
215+
border: 2px solid rgba(128, 128, 128, 0.25);
216+
border-radius: 50%;
224217
}
225218

226-
@keyframes loading-bounce {
227-
0%, 60%, 100% {
228-
height: 6px;
219+
.loading-spinner::after {
220+
content: '';
221+
position: absolute;
222+
top: 0;
223+
left: 0;
224+
width: 100%;
225+
height: 100%;
226+
border-radius: 50%;
227+
background: conic-gradient(
228+
from 0deg,
229+
currentColor 0deg,
230+
currentColor var(--progress, 0deg),
231+
transparent var(--progress, 0deg),
232+
transparent 360deg
233+
);
234+
mask: radial-gradient(circle, transparent 4px, black 4px);
235+
-webkit-mask: radial-gradient(circle, transparent 4px, black 4px);
236+
transition: --progress 0.2s ease-out;
237+
}
238+
239+
@keyframes spinner-rotate {
240+
0% {
241+
transform: rotate(0deg);
229242
}
230-
30% {
231-
height: 14px;
243+
100% {
244+
transform: rotate(360deg);
232245
}
233246
}
234247

@@ -514,11 +527,7 @@ <h1>Null-Safe Clang Playground</h1>
514527
</button>
515528

516529
<span id="status" class="status">
517-
<span class="loading-spinner">
518-
<span class="loading-bar"></span>
519-
<span class="loading-bar"></span>
520-
<span class="loading-bar"></span>
521-
</span>
530+
<span class="loading-spinner"></span>
522531
Initializing compiler...
523532
</span>
524533
</div>
@@ -645,6 +654,29 @@ <h1>Null-Safe Clang Playground</h1>
645654
}
646655
});
647656

657+
// Real-time diagnostics on content change
658+
let diagnosticsTimeout;
659+
editor.onDidChangeModelContent(() => {
660+
clearTimeout(diagnosticsTimeout);
661+
diagnosticsTimeout = setTimeout(async () => {
662+
if (!scriptUrl || isCompiling) {
663+
console.log('[Diagnostics] Skipped - scriptUrl:', !!scriptUrl, 'isCompiling:', isCompiling);
664+
return;
665+
}
666+
try {
667+
const code = getEditorValue();
668+
console.log('[Diagnostics] Running clang on code...');
669+
const result = await compileCode(code, []);
670+
console.log('[Diagnostics] stderr:', result.stderr);
671+
const diagnostics = parseDiagnostics(result.stderr);
672+
console.log('[Diagnostics] Parsed markers:', diagnostics);
673+
monaco.editor.setModelMarkers(editor.getModel(), 'clang', diagnostics);
674+
} catch (error) {
675+
console.error('[Diagnostics] Error:', error);
676+
}
677+
}, 500);
678+
});
679+
648680
resolve();
649681
});
650682
});
@@ -661,6 +693,31 @@ <h1>Null-Safe Clang Playground</h1>
661693
}
662694
}
663695

696+
// Parse clang diagnostics into Monaco markers
697+
function parseDiagnostics(stderr) {
698+
const markers = [];
699+
const lines = stderr.split('\n');
700+
const diagnosticRegex = /^input\.c:(\d+):(\d+):\s+(error|warning|note):\s+(.+)$/;
701+
702+
for (const line of lines) {
703+
const match = line.match(diagnosticRegex);
704+
if (match) {
705+
const [, lineNum, colNum, severity, message] = match;
706+
markers.push({
707+
startLineNumber: parseInt(lineNum),
708+
startColumn: parseInt(colNum),
709+
endLineNumber: parseInt(lineNum),
710+
endColumn: parseInt(colNum) + 1,
711+
message: message,
712+
severity: severity === 'error' ? monaco.MarkerSeverity.Error
713+
: severity === 'warning' ? monaco.MarkerSeverity.Warning
714+
: monaco.MarkerSeverity.Info
715+
});
716+
}
717+
}
718+
return markers;
719+
}
720+
664721
let clangVersion = '';
665722
let isCompiling = false;
666723
let isDragging = false;
@@ -677,11 +734,33 @@ <h1>Null-Safe Clang Playground</h1>
677734
}
678735

679736
// Status update helper
680-
function setStatus(message, showSpinner = false) {
737+
function setStatus(message, showSpinner = false, progressPercent = null) {
681738
if (showSpinner) {
682-
status.innerHTML = `<span class="loading-spinner"><span class="loading-bar"></span><span class="loading-bar"></span><span class="loading-bar"></span></span> ${message}`;
739+
const spinner = `<span class="loading-spinner" style="${progressPercent !== null ? `--progress: ${progressPercent}` : ''}"></span>`;
740+
status.innerHTML = `${spinner} ${message}`;
741+
if (progressPercent !== null) {
742+
status.title = `${progressPercent}% downloaded`;
743+
const spinnerEl = status.querySelector('.loading-spinner::after');
744+
if (spinnerEl) {
745+
const degrees = (progressPercent / 100) * 360;
746+
spinnerEl.style.transform = `rotate(${degrees}deg)`;
747+
}
748+
} else {
749+
status.title = '';
750+
}
683751
} else {
684752
status.textContent = message;
753+
status.title = '';
754+
}
755+
}
756+
757+
// Update progress spinner
758+
function updateProgress(percent) {
759+
const spinner = status.querySelector('.loading-spinner');
760+
if (spinner) {
761+
const degrees = (percent / 100) * 360;
762+
spinner.style.setProperty('--progress', `${degrees}deg`);
763+
status.title = `${Math.round(percent)}% downloaded`;
685764
}
686765
}
687766

@@ -752,7 +831,7 @@ <h1>Null-Safe Clang Playground</h1>
752831
}
753832

754833
async function initCompiler() {
755-
setStatus('Loading compiler (64MB)...', true);
834+
setStatus('Loading compiler (64MB)...', true, 0);
756835
status.className = 'status';
757836
loadingBar.classList.add('active');
758837

@@ -767,10 +846,37 @@ <h1>Null-Safe Clang Playground</h1>
767846
? './clang.wasm'
768847
: 'https://github.com/cs01/llvm-project/releases/latest/download/clang-nullsafe.wasm';
769848

770-
// Pre-load the WASM binary ONCE
849+
// Pre-load the WASM binary ONCE with progress tracking
771850
console.log('Downloading WASM binary...');
772851
const wasmResponse = await fetch(wasmUrl);
773-
wasmBinary = await wasmResponse.arrayBuffer();
852+
const contentLength = wasmResponse.headers.get('content-length');
853+
const total = parseInt(contentLength, 10);
854+
855+
let loaded = 0;
856+
const reader = wasmResponse.body.getReader();
857+
const chunks = [];
858+
859+
while (true) {
860+
const { done, value } = await reader.read();
861+
if (done) break;
862+
863+
chunks.push(value);
864+
loaded += value.length;
865+
866+
if (total) {
867+
const percent = (loaded / total) * 100;
868+
updateProgress(percent);
869+
}
870+
}
871+
872+
// Combine chunks into final array buffer
873+
const chunksAll = new Uint8Array(loaded);
874+
let position = 0;
875+
for (const chunk of chunks) {
876+
chunksAll.set(chunk, position);
877+
position += chunk.length;
878+
}
879+
wasmBinary = chunksAll.buffer;
774880
console.log('WASM binary loaded:', wasmBinary.byteLength, 'bytes');
775881

776882
// Now test with a worker to get the version
@@ -785,6 +891,7 @@ <h1>Null-Safe Clang Playground</h1>
785891

786892
status.textContent = 'Ready to compile';
787893
status.className = 'status ready';
894+
status.title = '';
788895
compileBtn.disabled = false;
789896
loadingBar.classList.remove('active');
790897
showToast('Compiler loaded successfully!');
@@ -794,6 +901,7 @@ <h1>Null-Safe Clang Playground</h1>
794901
} catch (error) {
795902
status.textContent = 'Failed to load compiler';
796903
status.className = 'status error';
904+
status.title = '';
797905
loadingBar.classList.remove('active');
798906
showToast('Failed to load compiler: ' + error.message, 4000);
799907
console.error('Failed to load Clang WASM:', error);

0 commit comments

Comments
 (0)