Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
172 changes: 172 additions & 0 deletions js/transpiler/gvar_display.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/**
* Global Variable Display Module
*
* Provides inline display of non-zero global variable values in the Monaco editor.
* Shows values as subtle hints (e.g., "// = 150") next to gvar references in code.
* Uses Monaco Content Widgets for inline text display.
*/

'use strict';

/**
* Find all gvar references in editor content
* @param {string} code - Editor content
* @returns {Array} Array of {index, line, column} objects
*/
export function findGvarReferences(code) {
const references = [];
const gvarRegex = /inav\.gvar\[(\d+)\]/g;
const lines = code.split('\n');

lines.forEach((line, lineIndex) => {
let match;
gvarRegex.lastIndex = 0;

while ((match = gvarRegex.exec(line)) !== null) {
const gvarIndex = parseInt(match[1], 10);
references.push({
index: gvarIndex,
line: lineIndex + 1, // Monaco uses 1-based line numbers
column: line.length + 3 // Position at end of line with extra space
});
}
});

return references;
}

/**
* GvarHintWidget - Monaco Content Widget for displaying gvar values inline
*/
class GvarHintWidget {
constructor(editor, line, column, gvarIndex, value, widgetId) {
this.editor = editor;
this.line = line;
this.column = column;
this.gvarIndex = gvarIndex;
this.value = value;
this._id = widgetId;
this._domNode = null;
}

getId() {
return this._id;
}

getDomNode() {
if (!this._domNode) {
this._domNode = document.createElement('span');
this._domNode.className = 'gvar-hint';
this._domNode.textContent = ` // gvar[${this.gvarIndex}] = ${this.value}`;
}
return this._domNode;
}

getPosition() {
return {
position: {
lineNumber: this.line,
column: this.column
},
preference: [0] // EXACT
};
}
}

/**
* Create Monaco content widgets for non-zero gvar values
* Only creates widget for first occurrence of each gvar index
* @param {object} editor - Monaco editor instance
* @param {Array} gvarRefs - Array of gvar references from findGvarReferences()
* @param {Array} gvarValues - Array of current gvar values from FC
* @returns {Array} Array of widget instances
*/
export function createGvarWidgets(editor, gvarRefs, gvarValues) {
const widgets = [];
const seenGvars = new Set();

if (!editor || !Array.isArray(gvarValues)) {
return widgets;
}

gvarRefs.forEach((ref, index) => {
// Skip if we've already shown this gvar
if (seenGvars.has(ref.index)) {
return;
}

const value = gvarValues[ref.index];

if (value !== undefined && value !== 0) {
seenGvars.add(ref.index);
const widgetId = `gvar-hint-${ref.line}-${ref.column}-${index}`;
const widget = new GvarHintWidget(editor, ref.line, ref.column, ref.index, value, widgetId);
widgets.push(widget);
}
});

return widgets;
}

/**
* Apply gvar widgets to editor
* @param {object} editor - Monaco editor instance
* @param {Array} oldWidgets - Previous widgets to remove
* @param {Array} newWidgets - New widgets to add
* @returns {Array} New widget instances
*/
export function applyWidgets(editor, oldWidgets, newWidgets) {
if (!editor) {
console.warn('[GvarDisplay] Cannot apply widgets - editor not initialized');
return [];
}

// Remove old widgets
if (oldWidgets && oldWidgets.length > 0) {
oldWidgets.forEach(widget => {
try {
editor.removeContentWidget(widget);
} catch (error) {
console.error('[GvarDisplay] Failed to remove widget:', widget.getId(), error);
}
});
}

// Add new widgets
if (newWidgets && newWidgets.length > 0) {
newWidgets.forEach(widget => {
try {
editor.addContentWidget(widget);
} catch (error) {
console.error('[GvarDisplay] Failed to add widget at line', widget.line, ':', error);
}
});
}

return newWidgets;
}

/**
* Clear all gvar widgets
* @param {object} editor - Monaco editor instance
* @param {Array} widgets - Widgets to remove
* @returns {Array} Empty widget array
*/
export function clearWidgets(editor, widgets) {
if (!editor) {
console.warn('[GvarDisplay] Cannot clear widgets - editor not initialized');
return [];
}

if (widgets && widgets.length > 0) {
widgets.forEach(widget => {
try {
editor.removeContentWidget(widget);
} catch (error) {
console.error('[GvarDisplay] Failed to clear widget:', widget.getId(), error);
}
});
}

return [];
}
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, []);
}
Loading