Skip to content

Commit e46c5bb

Browse files
committed
feat: implement Monaco editor error markers and validate programs before saving
1 parent 7acc143 commit e46c5bb

8 files changed

Lines changed: 165 additions & 26 deletions

animated-transformer/src/app/logic-explorer/logic-explorer.component.html

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ <h2>Logic Program Source</h2>
4242
<label>Selected Program</label>
4343
<button
4444
[matMenuTriggerFor]="presetsMenu"
45-
class="presets-trigger-btn"
45+
class="menu-trigger-btn"
4646
>
4747
<mat-icon class="preset-btn-icon">snippet_folder</mat-icon>
4848
<span class="selected-preset-name">{{ selectedPresetName() }}</span>
@@ -564,7 +564,7 @@ <h3>Probabilistic Population Simulator</h3>
564564

565565
<div class="sim-controls">
566566
<div class="control-row">
567-
<div class="control-item">
567+
<div class="control-item steps-item">
568568
<label for="sim-steps">Steps</label>
569569
<input
570570
id="sim-steps"
@@ -578,19 +578,27 @@ <h3>Probabilistic Population Simulator</h3>
578578
</div>
579579

580580
<div class="control-item">
581-
<label for="sim-mode">Probability Mode</label>
582-
<select
583-
id="sim-mode"
584-
[value]="simMode()"
585-
(change)="onSimModeChange($event)"
586-
class="styled-select"
581+
<label>Probability Mode</label>
582+
<button
583+
[matMenuTriggerFor]="simModeMenu"
584+
class="menu-trigger-btn"
587585
>
588-
<option value="proportional">Proportional Rates</option>
589-
<option value="softmax">Softmax Temperature</option>
590-
</select>
586+
<span>{{ simMode() === 'softmax' ? 'Softmax Temperature' : 'Proportional Rates' }}</span>
587+
<mat-icon class="arrow-icon">arrow_drop_down</mat-icon>
588+
</button>
589+
<mat-menu #simModeMenu="matMenu" class="sim-mode-menu-panel">
590+
<button mat-menu-item (click)="simMode.set('proportional')" [class.active-mode]="simMode() === 'proportional'">
591+
<mat-icon>trending_up</mat-icon>
592+
<span>Proportional Rates</span>
593+
</button>
594+
<button mat-menu-item (click)="simMode.set('softmax')" [class.active-mode]="simMode() === 'softmax'">
595+
<mat-icon>thermostat</mat-icon>
596+
<span>Softmax Temperature</span>
597+
</button>
598+
</mat-menu>
591599
</div>
592600

593-
<div class="control-item" *ngIf="simMode() === 'softmax'">
601+
<div class="control-item temp-item" *ngIf="simMode() === 'softmax'">
594602
<label for="sim-temp">Temperature</label>
595603
<input
596604
id="sim-temp"

animated-transformer/src/app/logic-explorer/logic-explorer.component.scss

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,7 @@ limitations under the License.
231231
letter-spacing: 0.05em;
232232
}
233233

234-
.presets-trigger-btn {
234+
.menu-trigger-btn {
235235
background: #ffffff;
236236
color: #0f172a;
237237
border: 1px solid #cbd5e1;
@@ -263,7 +263,7 @@ limitations under the License.
263263
justify-content: center;
264264
}
265265

266-
.selected-preset-name {
266+
span {
267267
flex-grow: 1;
268268
overflow: hidden;
269269
text-overflow: ellipsis;
@@ -286,12 +286,13 @@ limitations under the License.
286286

287287
/* Material Menu Styling */
288288
::ng-deep {
289-
.presets-menu-panel.mat-mdc-menu-panel {
289+
.presets-menu-panel.mat-mdc-menu-panel,
290+
.sim-mode-menu-panel.mat-mdc-menu-panel {
290291
border-radius: 8px !important;
291292
border: 1px solid #cbd5e1 !important;
292293
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1) !important;
293294
background: #ffffff !important;
294-
min-width: 260px !important;
295+
min-width: 200px !important;
295296
padding: 4px 0 !important;
296297

297298
.presets-menu-header {
@@ -323,7 +324,7 @@ limitations under the License.
323324
margin-right: 10px !important;
324325
}
325326

326-
&.active-preset {
327+
&.active-preset, &.active-mode {
327328
background: #f0fdf4 !important; /* light green for active */
328329
color: #15803d !important;
329330
font-weight: 600 !important;
@@ -333,11 +334,15 @@ limitations under the License.
333334
}
334335
}
335336

336-
&:hover:not(.active-preset) {
337+
&:hover:not(.active-preset):not(.active-mode) {
337338
background: #f8fafc !important;
338339
}
339340
}
340341
}
342+
343+
.presets-menu-panel.mat-mdc-menu-panel {
344+
min-width: 260px !important;
345+
}
341346
}
342347

343348
.editor-wrapper {
@@ -1372,6 +1377,16 @@ h3 {
13721377
.btn-item {
13731378
flex: 0;
13741379
min-width: max-content;
1380+
1381+
.btn {
1382+
height: 38px;
1383+
box-sizing: border-box;
1384+
}
1385+
}
1386+
1387+
.steps-item, .temp-item {
1388+
flex: 0 0 90px;
1389+
min-width: 90px;
13751390
}
13761391

13771392
.styled-input {
@@ -1384,6 +1399,7 @@ h3 {
13841399
outline: none;
13851400
transition: all 0.15s ease;
13861401
width: 100%;
1402+
height: 38px;
13871403
box-sizing: border-box;
13881404

13891405
&:focus {

animated-transformer/src/app/logic-explorer/logic-explorer.component.ts

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
MonacoJavaScriptEditorComponent,
2020
CodeStrUpdate,
2121
CodeStrUpdateKind,
22+
EditorError,
2223
} from '../monaco-js-editor/monaco-js-editor.component';
2324
import { updateLinearLogicTokens, updateLogicTheme, DEFAULT_THEME_CONFIG, LogicThemeConfig } from '../monaco-editor-loader';
2425
import { D3LineChartComponent, NamedChartPoint, CurveKind, ScalingKind, defaultChartConfig } from '../d3-line-chart/d3-line-chart.component';
@@ -337,12 +338,55 @@ export class LogicExplorerComponent implements OnInit {
337338
return [];
338339
}
339340

341+
private parseErrorString(errorMessage: string): EditorError {
342+
let line = 1;
343+
let column = 1;
344+
345+
const lineColRegex = /(?:line|at)\s+(\d+)(?:,?\s+col(?:umn)?\s+(\d+))?/i;
346+
const colonRegex = /(\d+):(\d+)/;
347+
348+
let match = errorMessage.match(lineColRegex);
349+
if (match) {
350+
line = parseInt(match[1], 10);
351+
if (match[2]) column = parseInt(match[2], 10);
352+
} else {
353+
match = errorMessage.match(colonRegex);
354+
if (match) {
355+
line = parseInt(match[1], 10);
356+
column = parseInt(match[2], 10);
357+
}
358+
}
359+
360+
return {
361+
message: errorMessage,
362+
start: {
363+
line,
364+
column
365+
}
366+
};
367+
}
368+
340369
saveProgram(saveAs = false) {
341370
const isCustom = this.isCustomPreset();
342371
const currentName = this.selectedPresetName();
343372

344373
// Get latest code text directly from Monaco
345-
const currentSrc = this.monacoEditor() ? this.monacoEditor()!.editor.getValue() : this.rawSource();
374+
const currentSrc = this.monacoEditor()?.editor ? this.monacoEditor()!.editor.getValue() : this.rawSource();
375+
376+
// Compile check to validate before saving
377+
try {
378+
const ctxt = parseContext(currentSrc);
379+
registerDefaultTSFunctions(ctxt);
380+
this.errorMessage.set(null);
381+
this.monacoEditor()?.setEditorError(null);
382+
} catch (e) {
383+
const errMsg = (e as Error).message;
384+
this.errorMessage.set(errMsg);
385+
const parsedError = this.parseErrorString(errMsg);
386+
this.monacoEditor()?.setEditorError(parsedError);
387+
alert(`Cannot save program: compilation failed.\nError: ${errMsg}`);
388+
return;
389+
}
346390

347391
// Find current config if available to copy it over
348392
const preset = this.presetsList().find(p => p.name === currentName);
@@ -519,6 +563,9 @@ export class LogicExplorerComponent implements OnInit {
519563

520564
this.refreshApplicableActions();
521565

566+
// Clear any errors in the Monaco editor
567+
this.monacoEditor()?.setEditorError(null);
568+
522569
// Dynamically extract registered names to keep editor highlights synchronized perfectly!
523570
const rawData = ctxt.getRawData();
524571
const constructors = Object.keys(rawData.constructors);
@@ -527,10 +574,15 @@ export class LogicExplorerComponent implements OnInit {
527574
const types = Object.keys(rawData.types);
528575
updateLinearLogicTokens(constructors, functions, actions, types);
529576
} catch (e) {
530-
this.errorMessage.set((e as Error).message);
577+
const errMsg = (e as Error).message;
578+
this.errorMessage.set(errMsg);
531579
this.story.set(null);
532580
this.currentContext.set(null);
533581
this.applicableActions.set([]);
582+
583+
// Draw red squiggly lines in Monaco editor!
584+
const parsedError = this.parseErrorString(errMsg);
585+
this.monacoEditor()?.setEditorError(parsedError);
534586
}
535587
}
536588

@@ -555,7 +607,8 @@ export class LogicExplorerComponent implements OnInit {
555607
}
556608

557609
onCompileClick() {
558-
this.compileSource(this.rawSource());
610+
const currentSrc = this.monacoEditor()?.editor ? this.monacoEditor()!.editor.getValue() : this.rawSource();
611+
this.compileSource(currentSrc);
559612
}
560613

561614
/**
@@ -896,13 +949,12 @@ export class LogicExplorerComponent implements OnInit {
896949
return isNaN(val) ? 1.0 : val;
897950
}
898951

899-
onSimModeChange(event: Event) {
900-
this.simMode.set((event.target as HTMLSelectElement).value as any);
901-
}
952+
902953

903954
runSimulation() {
904-
if (this.rawSource() !== this.compiledSource) {
905-
this.compileSource(this.rawSource());
955+
const currentSrc = this.monacoEditor()?.editor ? this.monacoEditor()!.editor.getValue() : this.rawSource();
956+
if (currentSrc !== this.compiledSource) {
957+
this.compileSource(currentSrc);
906958
}
907959

908960
const startContext = this.currentContext();

animated-transformer/src/app/monaco-js-editor/monaco-js-editor.component.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ export type CodeStrUpdate =
5050
close: true;
5151
};
5252

53+
export interface EditorPosition {
54+
line?: number;
55+
column?: number;
56+
}
57+
58+
export interface EditorError {
59+
message: string;
60+
start?: EditorPosition;
61+
end?: EditorPosition;
62+
}
63+
5364
@Component({
5465
selector: 'app-monaco-js-editor',
5566
templateUrl: './monaco-js-editor.component.html',
@@ -151,6 +162,13 @@ export class MonacoJavaScriptEditorComponent implements OnInit, AfterViewInit, O
151162
if (this.changed() !== changedNow) {
152163
this.changed.set(changedNow);
153164
}
165+
// Clear editor markers on user typing
166+
loadMonaco().then((monaco) => {
167+
const model = this.editor.getModel();
168+
if (model) {
169+
monaco.editor.setModelMarkers(model, 'logic-explorer', []);
170+
}
171+
});
154172
});
155173
}).catch(err => {
156174
console.error('Failed to initialize Monaco JS editor:', err);
@@ -237,4 +255,49 @@ export class MonacoJavaScriptEditorComponent implements OnInit, AfterViewInit, O
237255
};
238256
this.update.emit(configUpdate);
239257
}
258+
259+
setEditorError(error: EditorError | null) {
260+
if (!this.editor) return;
261+
262+
loadMonaco().then((monaco) => {
263+
const model = this.editor.getModel();
264+
if (!model) return;
265+
266+
if (!error) {
267+
monaco.editor.setModelMarkers(model, 'logic-explorer', []);
268+
return;
269+
}
270+
271+
const lineCount = model.getLineCount();
272+
273+
let startLine = error.start?.line ?? 1;
274+
let startColumn = error.start?.column ?? 1;
275+
if (startLine > lineCount) {
276+
startLine = 1;
277+
startColumn = 1;
278+
}
279+
280+
let endLine = error.end?.line ?? startLine;
281+
let endColumn = error.end?.column;
282+
if (endLine > lineCount) {
283+
endLine = startLine;
284+
}
285+
286+
if (endColumn === undefined) {
287+
const maxCol = model.getLineMaxColumn(endLine);
288+
endColumn = Math.min(startColumn + 5, maxCol);
289+
}
290+
291+
const marker: any = {
292+
severity: monaco.MarkerSeverity.Error,
293+
message: error.message,
294+
startLineNumber: startLine,
295+
startColumn: startColumn,
296+
endLineNumber: endLine,
297+
endColumn: endColumn,
298+
};
299+
300+
monaco.editor.setModelMarkers(model, 'logic-explorer', [marker]);
301+
});
302+
}
240303
}

0 commit comments

Comments
 (0)