From a8980096888c96b3e4a552164f862c0dcd5c8734 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 25 Jan 2026 20:12:47 -0600 Subject: [PATCH 01/14] Add LC-to-line mapping for active condition highlighting Augment decompiler to track which Logic Conditions map to which lines in generated JavaScript code. This enables real-time highlighting of active conditions in the JavaScript Programming tab. Key features: - 3-pass mapping algorithm handles simple, compound, and nested conditions - Compound conditions (a && b) correctly map all sub-LCs to same line - Activator chains propagate line numbers to child LCs - Hoisted variables and sticky/timer patterns tracked - Returns lcToLineMapping in decompile() result Part of Feature 1: Active LC Highlighting (Phase 1/5) --- js/transpiler/transpiler/decompiler.js | 104 +++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/js/transpiler/transpiler/decompiler.js b/js/transpiler/transpiler/decompiler.js index 6bf6f6fff..aca3f9fa4 100644 --- a/js/transpiler/transpiler/decompiler.js +++ b/js/transpiler/transpiler/decompiler.js @@ -195,6 +195,7 @@ class Decompiler { // Note: All INAV objects are always imported for user convenience this.inlineDeclaredVars = new Set(); // Track let variables declared inline in body this.hoistedVarCounters = new Map(); // Track counters for hoisted variable names (e.g., min, min2, min3) + this.lcToLineMapping = {}; // Track which LC maps to which line in final JavaScript (for active LC highlighting) // Create hoisting manager for activator-wrapped LCs this.hoistingManager = new ActivatorHoistingManager({ @@ -235,6 +236,7 @@ class Decompiler { return { success: true, code: this.generateBoilerplate('// No logic conditions found'), + lcToLineMapping: {}, warnings: this.warnings, stats: { total: logicConditions.length, enabled: 0, groups: 0 } }; @@ -248,9 +250,13 @@ class Decompiler { // Apply custom variable names from variable map as a final rename pass code = this.applyCustomVariableNames(code, enabled); + // Build LC-to-line mapping for active highlighting feature + this.buildLineMapping(code, enabled); + return { success: true, code, + lcToLineMapping: this.lcToLineMapping, warnings: this.warnings, stats: { total: logicConditions.length, @@ -1168,6 +1174,104 @@ class Decompiler { return declarations; } + /** + * Build mapping from LC indices to line numbers in the generated code + * This enables real-time highlighting of active logic conditions in JavaScript Programming tab + * + * For compound conditions like "if (a && b)", multiple LCs map to the same line: + * - LC#0 (a) → line N + * - LC#1 (b) → line N + * - LC#2 (AND(0,1)) → line N + * This is correct: when any of them is true, we highlight line N. + * + * @param {string} code - Final generated JavaScript code + * @param {Array} conditions - Enabled logic conditions + */ + buildLineMapping(code, conditions) { + const lines = code.split('\n'); + + // First pass: Map LCs that appear explicitly in the code + for (const lc of conditions) { + if (lc._gap) continue; + + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const line = lines[lineIdx]; + const lineNumber = lineIdx + 1; // 1-based line numbers for Monaco + + // Strategy 1: Look for LC index in comments (for external references) + const commentMatch = line.match(/\/\*\s*LC\s+(\d+)/); + if (commentMatch && parseInt(commentMatch[1]) === lc.index) { + this.lcToLineMapping[lc.index] = lineNumber; + break; + } + + // Strategy 2: For hoisted variables, find their declaration line + const hoistedVarName = this.hoistingManager?.getHoistedVarName(lc.index); + if (hoistedVarName) { + const constMatch = line.match(new RegExp(`const\\s+${hoistedVarName}\\s*=`)); + if (constMatch) { + this.lcToLineMapping[lc.index] = lineNumber; + break; + } + } + + // Strategy 3: For sticky/timer LCs, find their variable assignment + if (lc.operation === OPERATION.STICKY || lc.operation === OPERATION.TIMER) { + const varName = this.stickyVarNames.get(lc.index); + if (varName) { + const stickyMatch = line.match(new RegExp(`${varName}\\s*=\\s*(?:sticky|timer)`)); + if (stickyMatch) { + this.lcToLineMapping[lc.index] = lineNumber; + break; + } + } + } + } + } + + // Second pass: Map LCs based on their relationships + // For LCs with activators (child conditions), inherit the activator's line + // This handles compound conditions AND activator chains + for (const lc of conditions) { + if (lc._gap) continue; + + if (lc.activatorId !== -1) { + // This LC has an activator - it's part of a conditional chain + const activatorLine = this.lcToLineMapping[lc.activatorId]; + if (activatorLine) { + // Inherit the activator's line if we don't already have a mapping + if (!this.lcToLineMapping[lc.index]) { + this.lcToLineMapping[lc.index] = activatorLine; + } + } + } + } + + // Third pass: Handle LCs that are operands of other LCs (compound conditions) + // For example, in "if (a && b)", LC#0 (a) and LC#1 (b) are operands of LC#2 (AND) + for (const lc of conditions) { + if (lc._gap) continue; + + // Check if this LC uses other LCs as operands + const operandLCs = []; + if (lc.operandAType === OPERAND_TYPE.LC) { + operandLCs.push(lc.operandAValue); + } + if (lc.operandBType === OPERAND_TYPE.LC) { + operandLCs.push(lc.operandBValue); + } + + // If this LC has a line mapping, share it with its operand LCs + if (this.lcToLineMapping[lc.index]) { + for (const operandLcIndex of operandLCs) { + if (!this.lcToLineMapping[operandLcIndex]) { + this.lcToLineMapping[operandLcIndex] = this.lcToLineMapping[lc.index]; + } + } + } + } + } + /** * Generate boilerplate code with proper formatting * @param {string} body - Main code body From 744160fe490ed286138696d345353e3508308b20 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 25 Jan 2026 20:26:13 -0600 Subject: [PATCH 02/14] Add Monaco integration for active LC highlighting Implement real-time status polling and gutter decoration updates in the JavaScript Programming tab to show which Logic Conditions are currently active/true. Key features: - Status polling every 100ms using MSPChainer - updateActiveHighlighting() filters true LCs and applies gutter markers - clearActiveHighlighting() when code is dirty (unsaved changes) - Proper cleanup on tab switch/disconnect - isDirty check prevents highlighting stale code Part of Feature 1: Active LC Highlighting (Phase 2/5) --- tabs/javascript_programming.js | 121 +++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index 3cfcacf0a..5749afbcc 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -11,6 +11,7 @@ import mspHelper from './../js/msp/MSPHelper.js'; import { GUI, TABS } from './../js/gui.js'; import FC from './../js/fc.js'; import i18n from './../js/localization.js'; +import interval from './../js/intervals.js'; import { Transpiler } from './../js/transpiler/index.js'; import { Decompiler } from './../js/transpiler/transpiler/decompiler.js'; import * as MonacoLoader from './../js/transpiler/editor/monaco_loader.js'; @@ -38,6 +39,11 @@ TABS.javascript_programming = { decompiler: null, currentCode: '', + // Active LC highlighting state + lcToLineMapping: {}, + activeDecorations: [], + statusChainer: null, + analyticsChanges: {}, initialize: function (callback) { @@ -80,6 +86,9 @@ TABS.javascript_programming = { self.isDirty = true; }); + // Set up LC status polling for active highlighting + self.setupActiveHighlighting(); + // Localize i18n strings i18n.localize(); @@ -514,6 +523,10 @@ if (inav.flight.homeDistance > 100) { // Set the decompiled code self.editor.setValue(result.code); + // Store LC-to-line mapping for active highlighting + self.lcToLineMapping = result.lcToLineMapping || {}; + console.log('[JavaScript Programming] Stored LC-to-line mapping:', self.lcToLineMapping); + // Show stats if (result.stats) { GUI.log( @@ -735,9 +748,117 @@ if (inav.flight.homeDistance > 100) { }); }, + /** + * Set up active LC highlighting with status polling + */ + setupActiveHighlighting: function() { + const self = this; + + // Create MSP chainer for polling LC status + self.statusChainer = new MSPChainerClass(); + self.statusChainer.setChain([ + mspHelper.loadLogicConditionsStatus + ]); + self.statusChainer.setExitPoint(function() { + self.updateActiveHighlighting(); + }); + + // Start 100ms polling interval + interval.add('js_programming_lc_highlight', function() { + if (self.statusChainer) { + self.statusChainer.execute(); + } + }, 100); + + console.log('[JavaScript Programming] Active highlighting polling started'); + }, + + /** + * Update active LC highlighting based on current status + */ + updateActiveHighlighting: function() { + const self = this; + + // Don't highlight if code has been modified + if (self.isDirty) { + self.clearActiveHighlighting(); + return; + } + + // Don't highlight if no mapping available + if (!self.lcToLineMapping || Object.keys(self.lcToLineMapping).length === 0) { + return; + } + + // Find all LCs with true/non-zero status + const trueLCs = []; + const lcStatus = FC.LOGIC_CONDITIONS_STATUS.get(); + const lcConditions = FC.LOGIC_CONDITIONS.get(); + + for (let lcIndex = 0; lcIndex < lcStatus.length; lcIndex++) { + const status = lcStatus[lcIndex]; + const condition = lcConditions[lcIndex]; + + // Check if LC is enabled and has non-zero status (true) + if (condition && condition.getEnabled && condition.getEnabled() !== 0 && status !== 0) { + trueLCs.push(lcIndex); + } + } + + // Map LC indices to line numbers + const linesToHighlight = trueLCs + .map(lcIndex => self.lcToLineMapping[lcIndex]) + .filter(line => line !== undefined); + + // Remove duplicates (multiple LCs on same line) + const uniqueLines = [...new Set(linesToHighlight)]; + + // Create Monaco decorations + const decorations = uniqueLines.map(lineNum => ({ + range: new monaco.Range(lineNum, 1, lineNum, 1), + options: { + glyphMarginClassName: 'lc-active-true', + glyphMarginHoverMessage: { + value: 'Logic condition is TRUE' + } + } + })); + + // Apply decorations (Monaco efficiently handles diff) + if (self.editor && self.editor.deltaDecorations) { + self.activeDecorations = self.editor.deltaDecorations( + self.activeDecorations || [], + decorations + ); + } + }, + + /** + * Clear all active LC highlighting + */ + clearActiveHighlighting: function() { + const self = this; + + if (self.editor && self.editor.deltaDecorations && self.activeDecorations) { + self.activeDecorations = self.editor.deltaDecorations( + self.activeDecorations, + [] + ); + } + }, + cleanup: function (callback) { console.log('[JavaScript Programming] cleanup() - disposing editor'); + // Stop LC status polling + interval.remove('js_programming_lc_highlight'); + + // Clear active highlighting + this.clearActiveHighlighting(); + + // Clear status chainer + this.statusChainer = null; + // Dispose Monaco editor // Note: Unsaved changes are checked BEFORE cleanup() is called: // - For disconnect: in serial_backend.js From 426e6ab41f3593bdc5283c490f95fc439a2d93c4 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 25 Jan 2026 20:40:11 -0600 Subject: [PATCH 03/14] Add visual styling for active LC highlighting Implement CSS and Monaco editor configuration for green checkmark gutter markers that indicate active/true Logic Conditions. Key changes: - Add .lc-active-true CSS class with SVG green checkmark icon - Add Monaco gutter margin background styling - Enable glyphMargin: true in Monaco editor options - Set up proper cursor and sizing for gutter decorations Part of Feature 1: Active LC Highlighting (Phase 3/5) --- js/transpiler/editor/monaco_loader.js | 1 + tabs/javascript_programming.html | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/js/transpiler/editor/monaco_loader.js b/js/transpiler/editor/monaco_loader.js index d366d253b..301c7a3fb 100644 --- a/js/transpiler/editor/monaco_loader.js +++ b/js/transpiler/editor/monaco_loader.js @@ -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, diff --git a/tabs/javascript_programming.html b/tabs/javascript_programming.html index 60f9c5fbe..4bc2a568f 100644 --- a/tabs/javascript_programming.html +++ b/tabs/javascript_programming.html @@ -217,5 +217,23 @@
.note_spacer p { margin-bottom: 10px; } + +/* Active LC gutter marker - green checkmark for true conditions */ +.lc-active-true { + background: url('data:image/svg+xml;utf8,') no-repeat center; + background-size: 14px 14px; + width: 16px; + margin-left: 3px; + cursor: pointer; +} + +/* Monaco gutter margin styling */ +.monaco-editor .margin { + background-color: #f5f5f5; +} + +.monaco-editor .margin-view-overlays .lc-active-true { + cursor: pointer; +} From cb9ac217d3389bad1f711822436e6db3c1d3b934 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 25 Jan 2026 20:45:18 -0600 Subject: [PATCH 04/14] Add null checks to prevent errors when FC data not loaded Fix 'Cannot read properties of null' error in updateActiveHighlighting() by adding proper null checks for FC.LOGIC_CONDITIONS_STATUS and FC.LOGIC_CONDITIONS before accessing .get(). This prevents errors during tab initialization when FC data is still loading. --- tabs/javascript_programming.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index 5749afbcc..6f47296d1 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -790,11 +790,22 @@ if (inav.flight.homeDistance > 100) { return; } - // Find all LCs with true/non-zero status - const trueLCs = []; + // Check if FC data is available + if (!FC.LOGIC_CONDITIONS_STATUS || !FC.LOGIC_CONDITIONS) { + return; + } + const lcStatus = FC.LOGIC_CONDITIONS_STATUS.get(); const lcConditions = FC.LOGIC_CONDITIONS.get(); + // Verify data is loaded (not null) + if (!lcStatus || !lcConditions) { + return; + } + + // Find all LCs with true/non-zero status + const trueLCs = []; + for (let lcIndex = 0; lcIndex < lcStatus.length; lcIndex++) { const status = lcStatus[lcIndex]; const condition = lcConditions[lcIndex]; From f999e71ee1edf12535fc1a0a9ba503962ae4cd78 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Sun, 25 Jan 2026 21:17:15 -0600 Subject: [PATCH 05/14] Fix isDirty race condition preventing active LC highlighting The onChange handler was firing after setValue() during load, causing isDirty to be set to true even after we cleared it. This prevented the highlighting from ever appearing because updateActiveHighlighting() returns early when isDirty is true. Fixed by using setTimeout() to clear isDirty after the setValue() change event has been processed. --- tabs/javascript_programming.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index 6f47296d1..d64380a94 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -491,7 +491,10 @@ if (inav.flight.homeDistance > 100) { if (!logicConditions || logicConditions.length === 0) { GUI.log(i18n.getMessage('noLogicConditions') || 'No logic conditions found on FC'); self.editor.setValue(self.getDefaultCode()); - self.isDirty = false; + // Clear isDirty flag AFTER setValue completes + setTimeout(() => { + self.isDirty = false; + }, 0); if (callback) callback(); return; } @@ -542,7 +545,11 @@ if (inav.flight.homeDistance > 100) { $('#transpiler-warnings').hide(); } - self.isDirty = false; + // Clear isDirty flag AFTER setValue completes (setValue triggers onChange asynchronously) + setTimeout(() => { + self.isDirty = false; + console.log('[JavaScript Programming] isDirty cleared after load'); + }, 0); } else { // Decompilation failed GUI.log('Decompilation failed: ' + result.error); From 0a7c9586e034fbabc50d4d2c15a1a30fc145383c Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 15:55:46 -0600 Subject: [PATCH 06/14] Track LC-to-line mappings during code generation Previous approach tried to find LCs by parsing the generated code for /* LC X */ comments, but normal if-statements don't have these comments. New approach: - Track LC index and line content during code generation - After adding boilerplate, search for the tracked lines in final code - Map LC index to actual line numbers in final output This ensures all if-statements and their associated LCs get mapped correctly for the active highlighting feature. --- js/transpiler/transpiler/decompiler.js | 91 +++++++++++++++----------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/js/transpiler/transpiler/decompiler.js b/js/transpiler/transpiler/decompiler.js index aca3f9fa4..0ff6f01b2 100644 --- a/js/transpiler/transpiler/decompiler.js +++ b/js/transpiler/transpiler/decompiler.js @@ -196,6 +196,7 @@ class Decompiler { this.inlineDeclaredVars = new Set(); // Track let variables declared inline in body this.hoistedVarCounters = new Map(); // Track counters for hoisted variable names (e.g., min, min2, min3) this.lcToLineMapping = {}; // Track which LC maps to which line in final JavaScript (for active LC highlighting) + this._tempLcLines = []; // Temporary storage: [{lcIndex, relativeLineOffset}] during generation // Create hoisting manager for activator-wrapped LCs this.hoistingManager = new ActivatorHoistingManager({ @@ -251,7 +252,8 @@ class Decompiler { code = this.applyCustomVariableNames(code, enabled); // Build LC-to-line mapping for active highlighting feature - this.buildLineMapping(code, enabled); + // Convert tracked lines to actual line numbers in the final code + this.finalizeLcLineMapping(code, enabled); return { success: true, @@ -648,7 +650,11 @@ class Decompiler { } // Build if statement - lines.push(indentStr + `if (${condition}) {`); + const ifLine = indentStr + `if (${condition}) {`; + lines.push(ifLine); + + // Track this LC for line mapping (we'll calculate actual line numbers after boilerplate is added) + this._tempLcLines.push({ lcIndex: node.lc.index, lineContent: ifLine }); // First, output any actions at this level for (const actionNode of actions) { @@ -1187,41 +1193,55 @@ class Decompiler { * @param {string} code - Final generated JavaScript code * @param {Array} conditions - Enabled logic conditions */ - buildLineMapping(code, conditions) { + /** + * Finalize LC-to-line mapping after code generation + * Converts tracked LC lines to actual line numbers in final code + * @param {string} code - Final generated code with boilerplate + * @param {Array} conditions - All logic conditions + */ + finalizeLcLineMapping(code, conditions) { const lines = code.split('\n'); - // First pass: Map LCs that appear explicitly in the code - for (const lc of conditions) { - if (lc._gap) continue; + // First pass: Find tracked if-statements in the final code + for (const tracked of this._tempLcLines) { + const { lcIndex, lineContent } = tracked; + // Find this line content in the final code for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { - const line = lines[lineIdx]; - const lineNumber = lineIdx + 1; // 1-based line numbers for Monaco - - // Strategy 1: Look for LC index in comments (for external references) - const commentMatch = line.match(/\/\*\s*LC\s+(\d+)/); - if (commentMatch && parseInt(commentMatch[1]) === lc.index) { - this.lcToLineMapping[lc.index] = lineNumber; + if (lines[lineIdx].trim() === lineContent.trim()) { + this.lcToLineMapping[lcIndex] = lineIdx + 1; // Monaco uses 1-based line numbers break; } + } + } - // Strategy 2: For hoisted variables, find their declaration line - const hoistedVarName = this.hoistingManager?.getHoistedVarName(lc.index); - if (hoistedVarName) { - const constMatch = line.match(new RegExp(`const\\s+${hoistedVarName}\\s*=`)); - if (constMatch) { - this.lcToLineMapping[lc.index] = lineNumber; + // Second pass: Handle hoisted variables (const declarations) + for (const lc of conditions) { + if (lc._gap) continue; + + const hoistedVarName = this.hoistingManager?.getHoistedVarName(lc.index); + if (hoistedVarName && !this.lcToLineMapping[lc.index]) { + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const line = lines[lineIdx]; + if (line.match(new RegExp(`const\\s+${hoistedVarName}\\s*=`))) { + this.lcToLineMapping[lc.index] = lineIdx + 1; break; } } + } + } - // Strategy 3: For sticky/timer LCs, find their variable assignment - if (lc.operation === OPERATION.STICKY || lc.operation === OPERATION.TIMER) { - const varName = this.stickyVarNames.get(lc.index); - if (varName) { - const stickyMatch = line.match(new RegExp(`${varName}\\s*=\\s*(?:sticky|timer)`)); - if (stickyMatch) { - this.lcToLineMapping[lc.index] = lineNumber; + // Third pass: Handle sticky/timer variables + for (const lc of conditions) { + if (lc._gap) continue; + + if ((lc.operation === OPERATION.STICKY || lc.operation === OPERATION.TIMER) && !this.lcToLineMapping[lc.index]) { + const varName = this.stickyVarNames.get(lc.index); + if (varName) { + for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { + const line = lines[lineIdx]; + if (line.match(new RegExp(`${varName}\\s*=\\s*(?:sticky|timer)`))) { + this.lcToLineMapping[lc.index] = lineIdx + 1; break; } } @@ -1229,30 +1249,24 @@ class Decompiler { } } - // Second pass: Map LCs based on their relationships - // For LCs with activators (child conditions), inherit the activator's line - // This handles compound conditions AND activator chains + // Fourth pass: Map LCs based on activator relationships + // For LCs with activators (child conditions/actions), inherit the activator's line for (const lc of conditions) { if (lc._gap) continue; - if (lc.activatorId !== -1) { - // This LC has an activator - it's part of a conditional chain + if (lc.activatorId !== -1 && !this.lcToLineMapping[lc.index]) { const activatorLine = this.lcToLineMapping[lc.activatorId]; if (activatorLine) { - // Inherit the activator's line if we don't already have a mapping - if (!this.lcToLineMapping[lc.index]) { - this.lcToLineMapping[lc.index] = activatorLine; - } + this.lcToLineMapping[lc.index] = activatorLine; } } } - // Third pass: Handle LCs that are operands of other LCs (compound conditions) - // For example, in "if (a && b)", LC#0 (a) and LC#1 (b) are operands of LC#2 (AND) + // Fifth pass: Handle LCs that are operands of other LCs (compound conditions) + // For example, in "if (a && b)", LC#0 (a) and LC#1 (b) are operands of parent LC for (const lc of conditions) { if (lc._gap) continue; - // Check if this LC uses other LCs as operands const operandLCs = []; if (lc.operandAType === OPERAND_TYPE.LC) { operandLCs.push(lc.operandAValue); @@ -1261,7 +1275,6 @@ class Decompiler { operandLCs.push(lc.operandBValue); } - // If this LC has a line mapping, share it with its operand LCs if (this.lcToLineMapping[lc.index]) { for (const operandLcIndex of operandLCs) { if (!this.lcToLineMapping[operandLcIndex]) { From d058d0fa1da1a20bb0b894d617bedf85fa5704b2 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 16:22:45 -0600 Subject: [PATCH 07/14] Add debug logging to finalizeLcLineMapping Track what's being searched for and what's being found to diagnose why the LC-to-line mapping is empty. --- js/transpiler/transpiler/decompiler.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/js/transpiler/transpiler/decompiler.js b/js/transpiler/transpiler/decompiler.js index 0ff6f01b2..c93d85d01 100644 --- a/js/transpiler/transpiler/decompiler.js +++ b/js/transpiler/transpiler/decompiler.js @@ -1202,19 +1202,31 @@ class Decompiler { finalizeLcLineMapping(code, conditions) { const lines = code.split('\n'); + console.log('[Decompiler] finalizeLcLineMapping - tracked lines:', this._tempLcLines); + console.log('[Decompiler] finalizeLcLineMapping - total code lines:', lines.length); + // First pass: Find tracked if-statements in the final code for (const tracked of this._tempLcLines) { const { lcIndex, lineContent } = tracked; + console.log(`[Decompiler] Looking for LC${lcIndex}:`, lineContent.trim()); + // Find this line content in the final code for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) { if (lines[lineIdx].trim() === lineContent.trim()) { this.lcToLineMapping[lcIndex] = lineIdx + 1; // Monaco uses 1-based line numbers + console.log(`[Decompiler] Found LC${lcIndex} at line ${lineIdx + 1}`); break; } } + + if (!this.lcToLineMapping[lcIndex]) { + console.warn(`[Decompiler] Could not find LC${lcIndex} in final code`); + } } + console.log('[Decompiler] Final mapping:', this.lcToLineMapping); + // Second pass: Handle hoisted variables (const declarations) for (const lc of conditions) { if (lc._gap) continue; From 6d3e73d289adb789488100b504427aca41113bf8 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 16:40:17 -0600 Subject: [PATCH 08/14] Add debug logging to updateActiveHighlighting This will help us diagnose why the green checkmarks aren't appearing by logging: - When the function is called - If isDirty is blocking it - What the LC-to-line mapping is - If FC data is available - Which LCs are TRUE - What lines should be highlighted - If decorations are created --- tabs/javascript_programming.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index d64380a94..cf42ebda6 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -786,19 +786,26 @@ if (inav.flight.homeDistance > 100) { updateActiveHighlighting: function() { const self = this; + console.log('[JavaScript Programming] updateActiveHighlighting called'); + // Don't highlight if code has been modified if (self.isDirty) { + console.log('[JavaScript Programming] Skipping highlighting - code is dirty'); self.clearActiveHighlighting(); return; } // Don't highlight if no mapping available if (!self.lcToLineMapping || Object.keys(self.lcToLineMapping).length === 0) { + console.log('[JavaScript Programming] Skipping highlighting - no mapping'); return; } + console.log('[JavaScript Programming] LC-to-line mapping:', self.lcToLineMapping); + // Check if FC data is available if (!FC.LOGIC_CONDITIONS_STATUS || !FC.LOGIC_CONDITIONS) { + console.log('[JavaScript Programming] Skipping highlighting - FC data not available'); return; } @@ -807,6 +814,7 @@ if (inav.flight.homeDistance > 100) { // Verify data is loaded (not null) if (!lcStatus || !lcConditions) { + console.log('[JavaScript Programming] Skipping highlighting - FC data is null'); return; } @@ -820,17 +828,24 @@ if (inav.flight.homeDistance > 100) { // Check if LC is enabled and has non-zero status (true) if (condition && condition.getEnabled && condition.getEnabled() !== 0 && status !== 0) { trueLCs.push(lcIndex); + console.log(`[JavaScript Programming] LC${lcIndex} is TRUE (status: ${status})`); } } + console.log('[JavaScript Programming] True LCs:', trueLCs); + // Map LC indices to line numbers const linesToHighlight = trueLCs .map(lcIndex => self.lcToLineMapping[lcIndex]) .filter(line => line !== undefined); + console.log('[JavaScript Programming] Lines to highlight:', linesToHighlight); + // Remove duplicates (multiple LCs on same line) const uniqueLines = [...new Set(linesToHighlight)]; + console.log('[JavaScript Programming] Unique lines:', uniqueLines); + // Create Monaco decorations const decorations = uniqueLines.map(lineNum => ({ range: new monaco.Range(lineNum, 1, lineNum, 1), @@ -842,6 +857,8 @@ if (inav.flight.homeDistance > 100) { } })); + console.log('[JavaScript Programming] Creating', decorations.length, 'decorations'); + // Apply decorations (Monaco efficiently handles diff) if (self.editor && self.editor.deltaDecorations) { self.activeDecorations = self.editor.deltaDecorations( From 1de1e9a3492a253f66ab6956f82191aa2f59d9a6 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 16:50:17 -0600 Subject: [PATCH 09/14] Fix critical bug: use getAll() instead of get() for LC status FC.LOGIC_CONDITIONS_STATUS.get() requires a condition index parameter and returns a single value. We need getAll() to get the entire status array. This was causing 'FC data is null' errors and preventing any highlighting from appearing. --- tabs/javascript_programming.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index cf42ebda6..b404040ae 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -809,7 +809,7 @@ if (inav.flight.homeDistance > 100) { return; } - const lcStatus = FC.LOGIC_CONDITIONS_STATUS.get(); + const lcStatus = FC.LOGIC_CONDITIONS_STATUS.getAll(); const lcConditions = FC.LOGIC_CONDITIONS.get(); // Verify data is loaded (not null) From b12f9befc947a23898549d162d43fced70adb9e6 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 17:17:20 -0600 Subject: [PATCH 10/14] Improve active LC checkmark visibility with circle background and white stroke --- tabs/javascript_programming.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tabs/javascript_programming.html b/tabs/javascript_programming.html index 4bc2a568f..66b98872a 100644 --- a/tabs/javascript_programming.html +++ b/tabs/javascript_programming.html @@ -220,8 +220,8 @@
/* Active LC gutter marker - green checkmark for true conditions */ .lc-active-true { - background: url('data:image/svg+xml;utf8,') no-repeat center; - background-size: 14px 14px; + background: url('data:image/svg+xml;utf8,') no-repeat center; + background-size: 16px 16px; width: 16px; margin-left: 3px; cursor: pointer; From 3590ef379871dd993d77192834f66d85b3a82410 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 21:40:02 -0600 Subject: [PATCH 11/14] Add FALSE condition indicators and transpiler-side line tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This completes the active LC highlighting feature with two major improvements: 1. FALSE condition indicators: Gray hollow circles (○) display when conditions evaluate to FALSE, complementing the existing green checkmarks for TRUE conditions. Mixed states (TRUE and FALSE on same line) show green checkmark. 2. Transpiler-side line tracking: LC-to-line mappings are now generated during transpilation, providing immediate visual feedback after "Transpile" or "Save to FC" without requiring "Load from FC" roundtrip. Correctly adjusts for auto-added import statement offset. Technical changes: - Add lineOffset tracking in codegen to account for prepended import lines - Store currentSourceLine context during statement generation - Return lcToLineMapping in transpiler result - Update tab to use transpiler mapping immediately after transpile/save - Change polling interval from 100ms to 500ms to reduce MSP load (2Hz vs 10Hz) - Reorder checks: verify FC data availability before isDirty state - Clean up excessive debug logging for production readiness Testing: - Verified circles appear on correct lines after transpile - Tested TRUE/FALSE/MIXED states display properly - Confirmed decompiler mapping replaces transpiler mapping on load - Reviewed with inav-code-review agent - all critical issues resolved --- js/transpiler/transpiler/codegen.js | 67 ++++++++++----- js/transpiler/transpiler/index.js | 4 +- tabs/javascript_programming.html | 12 ++- tabs/javascript_programming.js | 122 ++++++++++++++++------------ 4 files changed, 130 insertions(+), 75 deletions(-) diff --git a/js/transpiler/transpiler/codegen.js b/js/transpiler/transpiler/codegen.js index f9df21f06..27859f974 100644 --- a/js/transpiler/transpiler/codegen.js +++ b/js/transpiler/transpiler/codegen.js @@ -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); @@ -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; } } @@ -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; } diff --git a/js/transpiler/transpiler/index.js b/js/transpiler/transpiler/index.js index cacdf4631..84ad741fc 100644 --- a/js/transpiler/transpiler/index.js +++ b/js/transpiler/transpiler/index.js @@ -115,8 +115,9 @@ class Transpiler { const optimized = this.optimize(analyzed.ast); // Step 4: Generate INAV CLI commands - // Pass the analyzer's variableHandler to codegen + // Pass the analyzer's variableHandler and lineOffset to codegen this.codegen.variableHandler = this.analyzer.variableHandler; + this.codegen.lineOffset = lineOffset; const commands = this.codegen.generate(optimized); // Combine all warnings @@ -153,6 +154,7 @@ class Transpiler { success: true, commands, logicConditionCount: this.codegen.lcIndex, + lcToLineMapping: this.codegen.lcToLineMapping, warnings: categorized, optimizations: this.optimizer.getStats(), gvarUsage: gvarSummary, diff --git a/tabs/javascript_programming.html b/tabs/javascript_programming.html index 66b98872a..7377dcce5 100644 --- a/tabs/javascript_programming.html +++ b/tabs/javascript_programming.html @@ -227,12 +227,22 @@
cursor: pointer; } +/* Inactive LC gutter marker - gray hollow circle for false conditions */ +.lc-active-false { + background: url('data:image/svg+xml;utf8,') no-repeat center; + background-size: 16px 16px; + width: 16px; + margin-left: 3px; + cursor: pointer; +} + /* Monaco gutter margin styling */ .monaco-editor .margin { background-color: #f5f5f5; } -.monaco-editor .margin-view-overlays .lc-active-true { +.monaco-editor .margin-view-overlays .lc-active-true, +.monaco-editor .margin-view-overlays .lc-active-false { cursor: pointer; } diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index b404040ae..b38457e19 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -346,6 +346,11 @@ if (inav.flight.homeDistance > 100) { GUI.log(`Transpiled successfully: ${result.logicConditionCount}/64 logic conditions`); + // Store LC-to-line mapping for transpiler-side highlighting + if (result.lcToLineMapping) { + self.lcToLineMapping = result.lcToLineMapping; + } + } else { // Show error this.showError(result.error); @@ -528,7 +533,6 @@ if (inav.flight.homeDistance > 100) { // Store LC-to-line mapping for active highlighting self.lcToLineMapping = result.lcToLineMapping || {}; - console.log('[JavaScript Programming] Stored LC-to-line mapping:', self.lcToLineMapping); // Show stats if (result.stats) { @@ -632,6 +636,11 @@ if (inav.flight.homeDistance > 100) { return; } + // Store LC-to-line mapping for transpiler-side highlighting + if (result.lcToLineMapping) { + self.lcToLineMapping = result.lcToLineMapping; + } + // Confirm save const confirmMsg = i18n.getMessage('confirmSaveLogicConditions') || `Save ${result.logicConditionCount} logic conditions to flight controller?`; @@ -770,14 +779,12 @@ if (inav.flight.homeDistance > 100) { self.updateActiveHighlighting(); }); - // Start 100ms polling interval + // Start 500ms polling interval (2Hz - sufficient for debugging without saturating MSP) interval.add('js_programming_lc_highlight', function() { if (self.statusChainer) { self.statusChainer.execute(); } - }, 100); - - console.log('[JavaScript Programming] Active highlighting polling started'); + }, 500); }, /** @@ -786,26 +793,8 @@ if (inav.flight.homeDistance > 100) { updateActiveHighlighting: function() { const self = this; - console.log('[JavaScript Programming] updateActiveHighlighting called'); - - // Don't highlight if code has been modified - if (self.isDirty) { - console.log('[JavaScript Programming] Skipping highlighting - code is dirty'); - self.clearActiveHighlighting(); - return; - } - - // Don't highlight if no mapping available - if (!self.lcToLineMapping || Object.keys(self.lcToLineMapping).length === 0) { - console.log('[JavaScript Programming] Skipping highlighting - no mapping'); - return; - } - - console.log('[JavaScript Programming] LC-to-line mapping:', self.lcToLineMapping); - - // Check if FC data is available + // Check if FC data is available first (short-circuit if disconnected) if (!FC.LOGIC_CONDITIONS_STATUS || !FC.LOGIC_CONDITIONS) { - console.log('[JavaScript Programming] Skipping highlighting - FC data not available'); return; } @@ -814,50 +803,81 @@ if (inav.flight.homeDistance > 100) { // Verify data is loaded (not null) if (!lcStatus || !lcConditions) { - console.log('[JavaScript Programming] Skipping highlighting - FC data is null'); return; } - // Find all LCs with true/non-zero status + // Don't highlight if code has been modified + if (self.isDirty) { + self.clearActiveHighlighting(); + return; + } + + // Don't highlight if no mapping available + if (!self.lcToLineMapping || Object.keys(self.lcToLineMapping).length === 0) { + return; + } + + // Find all enabled LCs and categorize by status const trueLCs = []; + const falseLCs = []; for (let lcIndex = 0; lcIndex < lcStatus.length; lcIndex++) { const status = lcStatus[lcIndex]; const condition = lcConditions[lcIndex]; - // Check if LC is enabled and has non-zero status (true) - if (condition && condition.getEnabled && condition.getEnabled() !== 0 && status !== 0) { - trueLCs.push(lcIndex); - console.log(`[JavaScript Programming] LC${lcIndex} is TRUE (status: ${status})`); + // Only process enabled LCs that are in our mapping (i.e., visible in the editor) + if (condition && condition.getEnabled && condition.getEnabled() !== 0 && self.lcToLineMapping[lcIndex] !== undefined) { + if (status !== 0) { + trueLCs.push(lcIndex); + } else { + falseLCs.push(lcIndex); + } } } - console.log('[JavaScript Programming] True LCs:', trueLCs); - - // Map LC indices to line numbers - const linesToHighlight = trueLCs - .map(lcIndex => self.lcToLineMapping[lcIndex]) - .filter(line => line !== undefined); + // Map LC indices to line numbers with their status + const lineStatus = {}; // { lineNum: 'true'|'false'|'mixed' } - console.log('[JavaScript Programming] Lines to highlight:', linesToHighlight); - - // Remove duplicates (multiple LCs on same line) - const uniqueLines = [...new Set(linesToHighlight)]; - - console.log('[JavaScript Programming] Unique lines:', uniqueLines); + for (const lcIndex of trueLCs) { + const line = self.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'; + } + } + } - // Create Monaco decorations - const decorations = uniqueLines.map(lineNum => ({ - range: new monaco.Range(lineNum, 1, lineNum, 1), - options: { - glyphMarginClassName: 'lc-active-true', - glyphMarginHoverMessage: { - value: 'Logic condition is TRUE' + for (const lcIndex of falseLCs) { + const line = self.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'; } } - })); + } - console.log('[JavaScript Programming] Creating', decorations.length, 'decorations'); + // Create Monaco decorations + const decorations = 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 (Monaco efficiently handles diff) if (self.editor && self.editor.deltaDecorations) { From 10bdcd2a99c02e8ffc65a05a576f9a98f9681087 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 22:00:33 -0600 Subject: [PATCH 12/14] Address qodo-merge bot feedback and refactor highlighting logic Fixes all 6 medium-priority issues identified by qodo-merge bot: 1. Clear stale decorations when loading code from FC 2. Clear mapping/decorations when loading default code 3. Add in-flight guard to prevent overlapping MSP polling requests 4. Remove duplicate intervals before adding new one 5. Improve type safety with Array.isArray() checks for MSP data Code organization improvements: - Extract highlighting logic to new module js/transpiler/lc_highlighting.js - Reduce tab file from 928 to 893 lines (-35 lines) - Create 5 testable, pure functions: categorizeLCsByStatus(), mapLCsToLines(), createMonacoDecorations(), applyDecorations(), clearDecorations() - Better separation of concerns (tab orchestrates, module implements) - Improved code maintainability and testability --- js/transpiler/lc_highlighting.js | 138 +++++++++++++++++++++++++++++++ tabs/javascript_programming.js | 130 +++++++++++------------------ 2 files changed, 187 insertions(+), 81 deletions(-) create mode 100644 js/transpiler/lc_highlighting.js diff --git a/js/transpiler/lc_highlighting.js b/js/transpiler/lc_highlighting.js new file mode 100644 index 000000000..806ed1edb --- /dev/null +++ b/js/transpiler/lc_highlighting.js @@ -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} lcStatus - Array of LC status values (0=FALSE, non-zero=TRUE) + * @param {Array} 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} 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} currentDecorations - Current decoration IDs + * @param {Array} newDecorations - New decorations to apply + * @returns {Array} 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} currentDecorations - Current decoration IDs to clear + * @returns {Array} Empty array (no decorations) + */ +export function clearDecorations(editor, currentDecorations) { + if (!editor || !editor.deltaDecorations || !currentDecorations) { + return []; + } + + return editor.deltaDecorations(currentDecorations, []); +} diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index b38457e19..623dc67af 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -15,6 +15,7 @@ import interval from './../js/intervals.js'; import { Transpiler } from './../js/transpiler/index.js'; import { Decompiler } from './../js/transpiler/transpiler/decompiler.js'; import * as MonacoLoader from './../js/transpiler/editor/monaco_loader.js'; +import * as LCHighlighting from './../js/transpiler/lc_highlighting.js'; import examples from './../js/transpiler/examples/index.js'; import settingsCache from './../js/settingsCache.js'; import * as monaco from 'monaco-editor'; @@ -58,6 +59,9 @@ TABS.javascript_programming = { try { self.initTranspiler(); + // Store monaco reference for use in highlighting + self.monaco = monaco; + // Initialize editor with INAV configuration self.editor = MonacoLoader.initializeMonacoEditor(monaco, 'monaco-editor'); @@ -500,6 +504,9 @@ if (inav.flight.homeDistance > 100) { setTimeout(() => { self.isDirty = false; }, 0); + // Clear stale mapping and decorations + self.lcToLineMapping = {}; + self.clearActiveHighlighting(); if (callback) callback(); return; } @@ -531,6 +538,9 @@ if (inav.flight.homeDistance > 100) { // Set the decompiled code self.editor.setValue(result.code); + // Clear old decorations before setting new mapping + self.clearActiveHighlighting(); + // Store LC-to-line mapping for active highlighting self.lcToLineMapping = result.lcToLineMapping || {}; @@ -770,6 +780,12 @@ if (inav.flight.homeDistance > 100) { setupActiveHighlighting: function() { const self = this; + // Prevent duplicate polling loops if initialize/setup runs multiple times + interval.remove('js_programming_lc_highlight'); + + // In-flight guard to prevent overlapping MSP requests + self._lcPollInFlight = false; + // Create MSP chainer for polling LC status self.statusChainer = new MSPChainerClass(); self.statusChainer.setChain([ @@ -777,13 +793,14 @@ if (inav.flight.homeDistance > 100) { ]); self.statusChainer.setExitPoint(function() { self.updateActiveHighlighting(); + self._lcPollInFlight = false; }); // Start 500ms polling interval (2Hz - sufficient for debugging without saturating MSP) interval.add('js_programming_lc_highlight', function() { - if (self.statusChainer) { - self.statusChainer.execute(); - } + if (!self.statusChainer || self._lcPollInFlight) return; + self._lcPollInFlight = true; + self.statusChainer.execute(); }, 500); }, @@ -801,8 +818,8 @@ if (inav.flight.homeDistance > 100) { const lcStatus = FC.LOGIC_CONDITIONS_STATUS.getAll(); const lcConditions = FC.LOGIC_CONDITIONS.get(); - // Verify data is loaded (not null) - if (!lcStatus || !lcConditions) { + // Verify data is loaded and has expected types + if (!Array.isArray(lcStatus) || !Array.isArray(lcConditions)) { return; } @@ -817,75 +834,29 @@ if (inav.flight.homeDistance > 100) { return; } - // Find all enabled LCs and categorize by status - 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 && self.lcToLineMapping[lcIndex] !== undefined) { - if (status !== 0) { - trueLCs.push(lcIndex); - } else { - falseLCs.push(lcIndex); - } - } - } - - // Map LC indices to line numbers with their status - const lineStatus = {}; // { lineNum: 'true'|'false'|'mixed' } - - for (const lcIndex of trueLCs) { - const line = self.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'; - } - } - } - - for (const lcIndex of falseLCs) { - const line = self.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'; - } - } - } - - // Create Monaco decorations - const decorations = 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 (Monaco efficiently handles diff) - if (self.editor && self.editor.deltaDecorations) { - self.activeDecorations = self.editor.deltaDecorations( - self.activeDecorations || [], - decorations - ); - } + // Categorize LCs by status (TRUE/FALSE) + const { trueLCs, falseLCs } = LCHighlighting.categorizeLCsByStatus( + lcStatus, + lcConditions, + self.lcToLineMapping + ); + + // Map LCs to editor line numbers with combined status + const lineStatus = LCHighlighting.mapLCsToLines( + trueLCs, + falseLCs, + self.lcToLineMapping + ); + + // Create Monaco decorations from line status + const decorations = LCHighlighting.createMonacoDecorations(lineStatus, self.monaco); + + // Apply decorations to editor + self.activeDecorations = LCHighlighting.applyDecorations( + self.editor, + self.activeDecorations, + decorations + ); }, /** @@ -893,13 +864,10 @@ if (inav.flight.homeDistance > 100) { */ clearActiveHighlighting: function() { const self = this; - - if (self.editor && self.editor.deltaDecorations && self.activeDecorations) { - self.activeDecorations = self.editor.deltaDecorations( - self.activeDecorations, - [] - ); - } + self.activeDecorations = LCHighlighting.clearDecorations( + self.editor, + self.activeDecorations + ); }, cleanup: function (callback) { From a078606e87c84df59549a075d32472c076e95080 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Mon, 26 Jan 2026 22:10:38 -0600 Subject: [PATCH 13/14] Fix malformed JSDoc comment syntax --- tabs/javascript_programming.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tabs/javascript_programming.js b/tabs/javascript_programming.js index 623dc67af..1e04543cc 100644 --- a/tabs/javascript_programming.js +++ b/tabs/javascript_programming.js @@ -198,8 +198,7 @@ if (inav.flight.homeDistance > 100) { }); }, - /* - ** + /** * Load a specific example into the editor * @param {string} exampleId - The ID of the example to load */ From ce1a1931c07da49b86e92848dea70d0fa8789ac4 Mon Sep 17 00:00:00 2001 From: Ray Morris Date: Tue, 27 Jan 2026 14:52:45 -0600 Subject: [PATCH 14/14] Fix JS Programming decompiler replacing spaces with variable names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Problem:** When decompiling logic conditions with activator hoisting and a stored variable map, all spaces between words were being replaced with custom variable names (e.g., "const min" became "constmin", comments like "INAV JavaScript Programming" became "INAVminJavaScriptminProgramming"). **Root Cause:** In decompiler.js applyCustomVariableNames() at line 293, the code was storing entire objects from hoistedActivatorVars instead of extracting the varName string: const entry = { varName: 'cond1', scopeLcIndex: -1 } When this object was used in a template literal to build a regex pattern: `\\b${entry}\\b` → `\\b[object Object]\\b` In regex, `[object Object]` is a CHARACTER CLASS that matches any single character from the set: {o,b,j,e,c,t,space,O}. This caused the regex pattern `\\b[object Object]\\b` to match spaces between word boundaries, replacing them with the custom variable name. **Fix:** Changed line 293-294 to extract the .varName property: - Before: lcIndexToGeneratedName.set(lcIndex, generatedName); - After: lcIndexToGeneratedName.set(lcIndex, entry.varName); This ensures the regex replacement uses the actual variable name string instead of "[object Object]" character class. **Files Changed:** - js/transpiler/transpiler/decompiler.js **Testing:** Verified with test cases showing the fix prevents space replacement in both code and comments. --- js/transpiler/transpiler/decompiler.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/transpiler/transpiler/decompiler.js b/js/transpiler/transpiler/decompiler.js index c93d85d01..94c85c4fb 100644 --- a/js/transpiler/transpiler/decompiler.js +++ b/js/transpiler/transpiler/decompiler.js @@ -290,8 +290,8 @@ class Decompiler { // Build mapping: LC index -> generated name (cond1, cond2, etc.) const lcIndexToGeneratedName = new Map(); - for (const [lcIndex, generatedName] of this.hoistingManager.hoistedActivatorVars.entries()) { - lcIndexToGeneratedName.set(lcIndex, generatedName); + for (const [lcIndex, entry] of this.hoistingManager.hoistedActivatorVars.entries()) { + lcIndexToGeneratedName.set(lcIndex, entry.varName); } // For each LC that has both a custom name and a generated name, rename