Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions js/transpiler/editor/monaco_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ function initializeMonacoEditor(monaco, containerId, options = {}) {
renderWhitespace: 'selection',
tabSize: 2,
insertSpaces: true,
glyphMargin: true, // Enable gutter for active LC highlighting decorations
wordBasedSuggestions: 'off', // Disable word-based suggestions (use string "off", not boolean)
suggest: {
showWords: false,
Expand Down
138 changes: 138 additions & 0 deletions js/transpiler/lc_highlighting.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* Logic Condition Active Highlighting Module
*
* Provides visual feedback in the Monaco editor showing which Logic Conditions
* are currently TRUE (green checkmarks) or FALSE (gray circles).
*/

'use strict';

/**
* Categorize Logic Conditions by their current status (TRUE/FALSE)
*
* @param {Array<number>} lcStatus - Array of LC status values (0=FALSE, non-zero=TRUE)
* @param {Array<Object>} lcConditions - Array of LC condition objects
* @param {Object} lcToLineMapping - Map of LC index to editor line number
* @returns {Object} { trueLCs: number[], falseLCs: number[] }
*/
export function categorizeLCsByStatus(lcStatus, lcConditions, lcToLineMapping) {
const trueLCs = [];
const falseLCs = [];

for (let lcIndex = 0; lcIndex < lcStatus.length; lcIndex++) {
const status = lcStatus[lcIndex];
const condition = lcConditions[lcIndex];

// Only process enabled LCs that are in our mapping (i.e., visible in the editor)
if (condition && condition.getEnabled && condition.getEnabled() !== 0 && lcToLineMapping[lcIndex] !== undefined) {
if (status !== 0) {
trueLCs.push(lcIndex);
} else {
falseLCs.push(lcIndex);
}
}
}

return { trueLCs, falseLCs };
}

/**
* Map LC indices to editor line numbers with their combined status
*
* Handles cases where multiple LCs map to the same line (shows "mixed" if both TRUE and FALSE exist)
*
* @param {number[]} trueLCs - Array of TRUE LC indices
* @param {number[]} falseLCs - Array of FALSE LC indices
* @param {Object} lcToLineMapping - Map of LC index to editor line number
* @returns {Object} Map of line number to status ('true'|'false'|'mixed')
*/
export function mapLCsToLines(trueLCs, falseLCs, lcToLineMapping) {
const lineStatus = {}; // { lineNum: 'true'|'false'|'mixed' }

// Process TRUE LCs
for (const lcIndex of trueLCs) {
const line = lcToLineMapping[lcIndex];
if (line !== undefined) {
if (lineStatus[line] === 'false') {
lineStatus[line] = 'mixed'; // Both true and false LCs on same line
} else if (lineStatus[line] !== 'mixed') {
lineStatus[line] = 'true';
}
}
}

// Process FALSE LCs
for (const lcIndex of falseLCs) {
const line = lcToLineMapping[lcIndex];
if (line !== undefined) {
if (lineStatus[line] === 'true') {
lineStatus[line] = 'mixed'; // Both true and false LCs on same line
} else if (lineStatus[line] !== 'mixed') {
lineStatus[line] = 'false';
}
}
}

return lineStatus;
}

/**
* Create Monaco editor decorations from line status
*
* @param {Object} lineStatus - Map of line number to status ('true'|'false'|'mixed')
* @param {Object} monaco - Monaco editor instance (passed from caller)
* @returns {Array<Object>} Array of Monaco decoration objects
*/
export function createMonacoDecorations(lineStatus, monaco) {
return Object.entries(lineStatus).map(([lineNum, status]) => {
// For mixed status, show green checkmark (at least one condition is true)
const className = (status === 'true' || status === 'mixed') ? 'lc-active-true' : 'lc-active-false';
const message = status === 'mixed'
? 'Multiple logic conditions: at least one is TRUE'
: (status === 'true' ? 'Logic condition is TRUE' : 'Logic condition is FALSE');

return {
range: new monaco.Range(parseInt(lineNum), 1, parseInt(lineNum), 1),
options: {
glyphMarginClassName: className,
glyphMarginHoverMessage: {
value: message
}
}
};
});
}

/**
* Apply decorations to Monaco editor
*
* @param {Object} editor - Monaco editor instance
* @param {Array<Object>} currentDecorations - Current decoration IDs
* @param {Array<Object>} newDecorations - New decorations to apply
* @returns {Array<Object>} Updated decoration IDs
*/
export function applyDecorations(editor, currentDecorations, newDecorations) {
if (!editor || !editor.deltaDecorations) {
return currentDecorations || [];
}

return editor.deltaDecorations(
currentDecorations || [],
newDecorations
);
}

/**
* Clear all decorations from Monaco editor
*
* @param {Object} editor - Monaco editor instance
* @param {Array<Object>} currentDecorations - Current decoration IDs to clear
* @returns {Array<Object>} Empty array (no decorations)
*/
export function clearDecorations(editor, currentDecorations) {
if (!editor || !editor.deltaDecorations || !currentDecorations) {
return [];
}

return editor.deltaDecorations(currentDecorations, []);
}
67 changes: 45 additions & 22 deletions js/transpiler/transpiler/codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ class INAVCodeGenerator {
constructor(variableHandler = null) {
this.lcIndex = 0; // Current logic condition index
this.commands = [];
this.lcToLineMapping = {}; // Map LC index -> source line number for highlighting
this.currentSourceLine = null; // Current source line being processed (for line tracking)
this.lineOffset = 0; // Line offset from auto-added imports (set by transpiler)
this.errorHandler = new ErrorHandler(); // Error and warning collection
this.operandMapping = buildForwardMapping(apiDefinitions);
this.arrowHelper = new ArrowFunctionHelper(this);
Expand Down Expand Up @@ -150,28 +153,42 @@ class INAVCodeGenerator {
*/
generateStatement(stmt) {
if (!stmt) return;
switch (stmt.type) {
case 'EventHandler':
this.generateEventHandler(stmt);
break;
case 'Assignment':
// Top-level assignment (e.g., gvar[0] = value) - runs unconditionally
this.generateTopLevelAssignment(stmt);
break;
case 'StickyAssignment':
// latch1 = sticky({on: ..., off: ...})
this.generateStickyAssignment(stmt);
break;
case 'LetDeclaration':
case 'VarDeclaration':
// Skip - declarations handled separately
break;
default:
this.errorHandler.addError(
`Unsupported statement type: ${stmt.type}. Only assignments and event handlers are supported`,
stmt,
'unsupported_statement'
);

// Set current source line for LC-to-line tracking
const previousSourceLine = this.currentSourceLine;
if (stmt.loc && stmt.loc.start) {
// Acorn line numbers include auto-added import lines at the top
// Subtract lineOffset to match Monaco editor line numbers
this.currentSourceLine = stmt.loc.start.line - this.lineOffset;
}

try {
switch (stmt.type) {
case 'EventHandler':
this.generateEventHandler(stmt);
break;
case 'Assignment':
// Top-level assignment (e.g., gvar[0] = value) - runs unconditionally
this.generateTopLevelAssignment(stmt);
break;
case 'StickyAssignment':
// latch1 = sticky({on: ..., off: ...})
this.generateStickyAssignment(stmt);
break;
case 'LetDeclaration':
case 'VarDeclaration':
// Skip - declarations handled separately
break;
default:
this.errorHandler.addError(
`Unsupported statement type: ${stmt.type}. Only assignments and event handlers are supported`,
stmt,
'unsupported_statement'
);
}
} finally {
// Restore previous source line context
this.currentSourceLine = previousSourceLine;
}
}

Expand Down Expand Up @@ -248,6 +265,12 @@ class INAVCodeGenerator {
this.commands.push(
`logic ${lcIndex} 1 ${activatorId} ${operation} ${operandA.type} ${operandA.value} ${operandB.type} ${operandB.value} ${flags}`
);

// Track source line mapping for transpiler-side highlighting
if (this.currentSourceLine !== null) {
this.lcToLineMapping[lcIndex] = this.currentSourceLine;
}

this.lcIndex++;
return lcIndex;
}
Expand Down
Loading