Skip to content

feat: viewport-aware debugger polling for all editor languages#686

Closed
thiagoralves wants to merge 1 commit into
developmentfrom
fix/debug-inline-mount-timing
Closed

feat: viewport-aware debugger polling for all editor languages#686
thiagoralves wants to merge 1 commit into
developmentfrom
fix/debug-inline-mount-timing

Conversation

@thiagoralves
Copy link
Copy Markdown
Contributor

@thiagoralves thiagoralves commented Mar 12, 2026

Summary

  • Replaces ~280 lines of duplicated LD/FBD/catch-all polling code with a unified push-based architecture where each visual component computes its own visible variable set (debounced ~1s) and pushes it to the store
  • LD editor: Detects visible rungs via scroll position, extracts variable names from contacts, coils, and blocks
  • FBD editor: Filters nodes by viewport bounds using ReactFlow getViewport() + container dimensions
  • ST/IL editor: Scans visible Monaco editor lines for variable references using getVisibleRanges()
  • Fixes ST/IL inline debug badges broken by PR Fix debugger polling to fetch only visible variables #646's removal of the "poll all POU variables" section
  • Extracts stripLineComments and collectSTVariableNames as shared utilities under src/renderer/utils/debug/
  • Adds debugViewportVarNames store field with proper cleanup on debug hide and workspace clear
  • Hoists fbInstanceCtx computation to avoid duplication across language sections

Test plan

  • ST program debug: open ST program, start debugger, verify inline badges appear with live values. Scroll to new variables — they show ? briefly then values within ~1s
  • IL program debug: same test with IL language
  • ST function block debug: create FB with ST body, instantiate in program, select instance, verify inline badges
  • LD debug: open LD program, start debugger, scroll through rungs — contacts/coils show values for visible rungs only
  • FBD debug: open FBD program, pan across diagram — only visible node variables are polled
  • POU switching: switch between ST and LD POUs during debug, verify visible set updates correctly
  • Regression — watched variables: checkbox-watched variables must always be polled regardless of viewport
  • Regression — forced variables: forced variables must always be polled

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added viewport-aware variable tracking in the debugger—now only variables visible in the current editor view are monitored, improving performance when debugging large programs.
  • Performance

    • Optimized variable polling to reduce unnecessary updates during debugging sessions, resulting in a more responsive editor experience across all supported diagram and text editors.

Replace the ~280-line LD/FBD/catch-all polling sections in workspace-screen
with a unified push-based architecture. Each visual component (LD, FBD,
Monaco ST/IL) computes its own visible variable set debounced to ~1s and
pushes it to the store. The polling tick simply reads the pre-computed set.

- LD: detects visible rungs via scroll position, extracts variable names
- FBD: filters nodes by viewport bounds using ReactFlow getViewport()
- ST/IL: scans visible editor lines for variable references
- Extract stripLineComments and collectSTVariableNames as shared utilities
- Add debugViewportVarNames store field with cleanup on debug hide
- Hoist fbInstanceCtx computation to avoid duplication across languages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 12, 2026

Walkthrough

This PR introduces a viewport-driven debugging system where editors (Ladder, Monaco ST/IL, FBD) automatically collect and report visible variable names via a shared store. The workspace polling logic then uses these viewport-collected variable names to optimize which debugger variables to poll, replacing extensive conditional branches with a centralized, viewport-aware approach.

Changes

Cohort / File(s) Summary
Store & State Management
src/renderer/store/slices/workspace/slice.ts, src/renderer/store/slices/workspace/types.ts
Introduce new debugViewportVarNames: Set<string> state field and setDebugViewportVarNames action; reset on workspace clear and debugger hide.
Debug Utility Functions
src/renderer/utils/debug/strip-line-comments.ts, src/renderer/utils/debug/collect-st-variables.ts
Add comment-stripping and ST/IL variable collection utilities to scan source text for variable occurrences while ignoring comments.
Ladder Editor Integration
src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx
Implement debounced viewport scanning (1s) to extract variable names from visible rungs (contacts, coils, blocks) and push to store on scroll.
Monaco ST/IL Editor Integration
src/renderer/components/_features/[workspace]/editor/monaco/index.tsx
Add debounced collection of visible ST/IL variable names from editor viewport; filter with collectSTVariableNames and update store on scroll.
FBD Editor Integration
src/renderer/components/_molecules/graphical-editor/fbd/index.tsx
Compute and expose visible FBD variable names (from nodes within viewport bounds); wire debounced recomputation to onMoveEnd lifecycle and debugger start.
Workspace Polling Refactor
src/renderer/screens/workspace-screen.tsx
Replace extensive per-language polling branches with viewport-driven logic; centralize FB instance context resolution and use debugViewportVarNames to determine polling targets.

Sequence Diagram

sequenceDiagram
    actor User
    participant Editor as Ladder/Monaco/FBD Editor
    participant Viewport as Viewport Scanner<br/>(debounced)
    participant Store as Workspace Store
    participant Polling as Workspace Polling<br/>Logic

    User->>Editor: Scroll/Pan viewport
    Editor->>Viewport: Trigger debounced scan (1s)
    Viewport->>Viewport: Collect visible variable names
    Viewport->>Store: setDebugViewportVarNames(visibleVars)
    Store->>Store: Update debugViewportVarNames
    Store->>Polling: Notify state change
    Polling->>Polling: Compute polling targets from<br/>debugViewportVarNames
    Polling->>Polling: Poll selected variables from debugger
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • JoaoGSP

Poem

🐰 A viewport hops through code so fine,
Collecting vars that come in line—
No more branches, left and right,
Just visible names in scrolling sight!
Debounced whispers to the store,
Debuggers dance like never before! 🎭

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides a clear summary of changes and a comprehensive test plan, but lacks issue references and DOD checklist items required by the template. Add issue/Jira references, fill out the DOD checklist with completion status, and specify test coverage percentage to fully comply with the repository template.
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: viewport-aware debugger polling for all editor languages' clearly and concisely summarizes the main change: adding viewport-aware polling across all editor types (LD, FBD, ST/IL).

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/debug-inline-mount-timing
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Tip

You can disable poems in the walkthrough.

Disable the reviews.poem setting to disable the poems in the walkthrough.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/renderer/components/_features/`[workspace]/editor/graphical/ladder/index.tsx:
- Around line 257-260: The code only adds blockData.numericId for block nodes,
but the polling uses debugViewportVarNames to match
currentPou.data.variables[*].name, so you must also publish the block instance
name; update the block branch (where node.type === 'block' and blockData is
read) to also add the block instance identifier (e.g., blockData.variant?.type
or the instance/name field present on node.data) into the names set alongside
numericId so visible instances like TON0 are emitted and will match
currentPou.data.variables[*].name.

In `@src/renderer/components/_features/`[workspace]/editor/monaco/index.tsx:
- Around line 251-288: When there are no variables the effect currently returns
early and never clears the workspace-global debug viewport state; change the
early-return branch in the useEffect inside the Monaco editor (the block that
computes varNames from pou?.data.variables) to call setDebugViewportVarNames([])
before returning so the global store is reset; ensure this uses the same editor
effect (referenced symbols: pou?.data.variables, varNames, computeVisibleVars,
setDebugViewportVarNames) so switching POUs clears the previous viewport names.

In `@src/renderer/components/_molecules/graphical-editor/fbd/index.tsx`:
- Around line 724-730: The collector currently adds only blockData.numericId for
nodes of type 'block', which mismatches the visible variable names used
elsewhere (e.g., workspace-screen.tsx) and prevents enqueuing member names like
TON0.Q or PID0.OUT; change the 'block' branch in index.tsx to extract and add
the block's variable name (e.g., blockData.variable?.name or the field that
contains the human-facing block name) to varNames instead of—or in addition
to—numericId so the key-space matches the visible variable names used when
expanding FB members.

In `@src/renderer/screens/workspace-screen.tsx`:
- Around line 1213-1249: The viewport-poll expansion only handles derived-type
parents and misses function-output numericId markers and children of array/UDT
parents; update the logic that builds debugVariableKeys (around
debugViewportVarNames, makeCompositeKeyForCurrentPou, variableInfoMapRef,
currentPou, fbInstanceCtx) to also: 1) when a visible variable is a
function/block marker (numericId) call the same composite-key mapping used for
direct vars (via makeCompositeKeyForCurrentPou or the existing composite-key
convention) and add the resulting _TMP_* keys to debugVariableKeys; 2) treat
visible array/UDT parents similarly to derived by scanning
variableInfoMapRef.current entries for children whose varInfo.variable.name
startsWith the parent name (using the same pou-context checks with
fbInstanceCtx/currentPou.data.name) and add pouName:childName for each child so
array/UDT fields and struct members are included in polling.

In `@src/renderer/utils/debug/collect-st-variables.ts`:
- Around line 10-14: The regex in the variableNames.map block uses a negative
lookahead (/(?![\w.\[])/) that prevents matching parent symbols when they're
accessed via member or array notation (e.g., TON0.Q, cfg.limit, arr[1]); update
the RegExp construction in that block (the code that builds patterns from
variableNames/escaped) to remove the negative lookahead and instead rely on the
existing word-boundary (\\b) or a more permissive lookahead that still prevents
partial identifier matches but allows '.' and '[' after the identifier (for
example, use new RegExp(`\\b${escaped}`, 'i')). This will ensure
debugViewportVarNames can include parent symbols used only via member or array
access so downstream checks (e.g., debugViewportVarNames.has(v.name) in
workspace-screen.tsx) work correctly.

In `@src/renderer/utils/debug/strip-line-comments.ts`:
- Around line 5-43: stripLineComments fails to track string quotes so it treats
// inside single-quoted literals as comments; update the BlockCommentState union
to include 'single-quote' and modify stripLineComments (use the local state
variable s, the chars array and the scanning while loop) to detect entering a
single-quote when not already in a comment (on chars[i] === ''' ), set s =
'single-quote' and advance, and when s === 'single-quote' scan forward until the
closing quote (handle escaped single-quotes by treating doubled quotes '' as an
escaped quote and advancing past both) without blanking characters (unlike
block-comment states), then on closing quote set s = false and continue so //
inside strings won't be treated as line comments.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: da2353c0-6a7a-4e1f-a1e2-95b9e4956240

📥 Commits

Reviewing files that changed from the base of the PR and between 6e1daaf and 0a973e6.

📒 Files selected for processing (8)
  • src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx
  • src/renderer/components/_features/[workspace]/editor/monaco/index.tsx
  • src/renderer/components/_molecules/graphical-editor/fbd/index.tsx
  • src/renderer/screens/workspace-screen.tsx
  • src/renderer/store/slices/workspace/slice.ts
  • src/renderer/store/slices/workspace/types.ts
  • src/renderer/utils/debug/collect-st-variables.ts
  • src/renderer/utils/debug/strip-line-comments.ts

Comment on lines +257 to +260
if (node.type === 'block') {
const blockData = node.data as { variant?: { type: string }; numericId?: string }
if (blockData.numericId) names.add(blockData.numericId)
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Publish the block instance name here as well.

For block nodes this only emits numericId. The polling side expands visible FB members by matching debugViewportVarNames against currentPou.data.variables[*].name, so a visible instance like TON0 never matches and TON0.Q/ET/... stop updating unless they are watched separately.

Suggested fix
-        if (node.type === 'block') {
-          const blockData = node.data as { variant?: { type: string }; numericId?: string }
-          if (blockData.numericId) names.add(blockData.numericId)
+        if (node.type === 'block') {
+          const blockData = node.data as {
+            variant?: { type?: string }
+            numericId?: string
+            variable?: { name?: string }
+          }
+          if (blockData.variable?.name) names.add(blockData.variable.name)
+          if (blockData.variant?.type === 'function' && blockData.numericId) names.add(blockData.numericId)
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/renderer/components/_features/`[workspace]/editor/graphical/ladder/index.tsx
around lines 257 - 260, The code only adds blockData.numericId for block nodes,
but the polling uses debugViewportVarNames to match
currentPou.data.variables[*].name, so you must also publish the block instance
name; update the block branch (where node.type === 'block' and blockData is
read) to also add the block instance identifier (e.g., blockData.variant?.type
or the instance/name field present on node.data) into the names set alongside
numericId so visible instances like TON0 are emitted and will match
currentPou.data.variables[*].name.

Comment on lines +251 to +288
useEffect(() => {
if (!isDebuggerVisible || !editorRef.current || (language !== 'st' && language !== 'il')) return

const editor = editorRef.current
const model = editor.getModel()
if (!model) return

const varNames = pou?.data.variables.map((v) => v.name).filter((n) => n && n.trim() !== '') || []
if (varNames.length === 0) return

const computeVisibleVars = () => {
const ranges = editor.getVisibleRanges()
const visibleLines: string[] = []
for (const range of ranges) {
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
visibleLines.push(model.getLineContent(line))
}
}
const sourceText = visibleLines.join('\n')
const visibleVarNames = collectSTVariableNames(sourceText, varNames)
setDebugViewportVarNames(visibleVarNames)
}

// Compute immediately
computeVisibleVars()

// Debounce on scroll
let timer: ReturnType<typeof setTimeout>
const disposable = editor.onDidScrollChange(() => {
clearTimeout(timer)
timer = setTimeout(computeVisibleVars, 1000)
})

return () => {
disposable.dispose()
clearTimeout(timer)
}
}, [isDebuggerVisible, editorMounted, name, language, pou?.data.variables, setDebugViewportVarNames])
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the global viewport set when this editor has nothing to publish.

Line 258-259 returns early for POUs with no variables, and cleanup never clears the store. Since debugViewportVarNames is workspace-global, the previous tab's names keep driving workspace-screen.tsx polling after a POU switch.

Suggested fix
   useEffect(() => {
-    if (!isDebuggerVisible || !editorRef.current || (language !== 'st' && language !== 'il')) return
+    if (!isDebuggerVisible || !editorRef.current || (language !== 'st' && language !== 'il')) {
+      setDebugViewportVarNames(new Set<string>())
+      return
+    }
 
     const editor = editorRef.current
     const model = editor.getModel()
     if (!model) return
 
     const varNames = pou?.data.variables.map((v) => v.name).filter((n) => n && n.trim() !== '') || []
-    if (varNames.length === 0) return
+    if (varNames.length === 0) {
+      setDebugViewportVarNames(new Set<string>())
+      return
+    }
@@
     return () => {
       disposable.dispose()
       clearTimeout(timer)
+      setDebugViewportVarNames(new Set<string>())
     }
   }, [isDebuggerVisible, editorMounted, name, language, pou?.data.variables, setDebugViewportVarNames])
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!isDebuggerVisible || !editorRef.current || (language !== 'st' && language !== 'il')) return
const editor = editorRef.current
const model = editor.getModel()
if (!model) return
const varNames = pou?.data.variables.map((v) => v.name).filter((n) => n && n.trim() !== '') || []
if (varNames.length === 0) return
const computeVisibleVars = () => {
const ranges = editor.getVisibleRanges()
const visibleLines: string[] = []
for (const range of ranges) {
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
visibleLines.push(model.getLineContent(line))
}
}
const sourceText = visibleLines.join('\n')
const visibleVarNames = collectSTVariableNames(sourceText, varNames)
setDebugViewportVarNames(visibleVarNames)
}
// Compute immediately
computeVisibleVars()
// Debounce on scroll
let timer: ReturnType<typeof setTimeout>
const disposable = editor.onDidScrollChange(() => {
clearTimeout(timer)
timer = setTimeout(computeVisibleVars, 1000)
})
return () => {
disposable.dispose()
clearTimeout(timer)
}
}, [isDebuggerVisible, editorMounted, name, language, pou?.data.variables, setDebugViewportVarNames])
useEffect(() => {
if (!isDebuggerVisible || !editorRef.current || (language !== 'st' && language !== 'il')) {
setDebugViewportVarNames(new Set<string>())
return
}
const editor = editorRef.current
const model = editor.getModel()
if (!model) return
const varNames = pou?.data.variables.map((v) => v.name).filter((n) => n && n.trim() !== '') || []
if (varNames.length === 0) {
setDebugViewportVarNames(new Set<string>())
return
}
const computeVisibleVars = () => {
const ranges = editor.getVisibleRanges()
const visibleLines: string[] = []
for (const range of ranges) {
for (let line = range.startLineNumber; line <= range.endLineNumber; line++) {
visibleLines.push(model.getLineContent(line))
}
}
const sourceText = visibleLines.join('\n')
const visibleVarNames = collectSTVariableNames(sourceText, varNames)
setDebugViewportVarNames(visibleVarNames)
}
// Compute immediately
computeVisibleVars()
// Debounce on scroll
let timer: ReturnType<typeof setTimeout>
const disposable = editor.onDidScrollChange(() => {
clearTimeout(timer)
timer = setTimeout(computeVisibleVars, 1000)
})
return () => {
disposable.dispose()
clearTimeout(timer)
setDebugViewportVarNames(new Set<string>())
}
}, [isDebuggerVisible, editorMounted, name, language, pou?.data.variables, setDebugViewportVarNames])
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/_features/`[workspace]/editor/monaco/index.tsx around
lines 251 - 288, When there are no variables the effect currently returns early
and never clears the workspace-global debug viewport state; change the
early-return branch in the useEffect inside the Monaco editor (the block that
computes varNames from pou?.data.variables) to call setDebugViewportVarNames([])
before returning so the global store is reset; ensure this uses the same editor
effect (referenced symbols: pou?.data.variables, varNames, computeVisibleVars,
setDebugViewportVarNames) so switching POUs clears the previous viewport names.

Comment on lines +724 to +730
if (node.type === 'input-variable' || node.type === 'output-variable' || node.type === 'inout-variable') {
const varName = (node.data as { variable?: { name?: string } }).variable?.name
if (varName) varNames.add(varName)
}
if (node.type === 'block') {
const blockData = node.data as { variant?: { type: string }; numericId?: string }
if (blockData.numericId) varNames.add(blockData.numericId)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Collect the FBD block variable name, not just numericId.

This has the same key-space mismatch as the ladder collector: workspace-screen.tsx looks for visible variable names when expanding FB members, but this code only sends numericId, so blocks like TON0/PID0 never enqueue TON0.Q, PID0.OUT, etc.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/components/_molecules/graphical-editor/fbd/index.tsx` around
lines 724 - 730, The collector currently adds only blockData.numericId for nodes
of type 'block', which mismatches the visible variable names used elsewhere
(e.g., workspace-screen.tsx) and prevents enqueuing member names like TON0.Q or
PID0.OUT; change the 'block' branch in index.tsx to extract and add the block's
variable name (e.g., blockData.variable?.name or the field that contains the
human-facing block name) to varNames instead of—or in addition to—numericId so
the key-space matches the visible variable names used when expanding FB members.

Comment on lines +1213 to +1249
if (currentPou && debugViewportVarNames.size > 0) {
// Add direct POU variables
for (const varName of debugViewportVarNames) {
const compositeKey = makeCompositeKeyForCurrentPou(varName)
if (compositeKey) {
debugVariableKeys.add(compositeKey)
}
}

// For FB POUs, poll nested FB variables using instance context
// For program POUs, poll FB instance variables using the standard approach
if (currentPou.type === 'function-block' && fbdFbInstanceCtx) {
// Poll all nested BOOL variables within the FB instance
Array.from(variableInfoMapRef.current.entries()).forEach(([_, varInfos]) => {
// Add FB instance nested variables for derived-type variables in the visible set
// (e.g., if TON0 is visible, also poll TON0.ET, TON0.Q, TON0.IN, TON0.PT)
const visibleFbInstances = currentPou.data.variables.filter(
(v) => v.type.definition === 'derived' && debugViewportVarNames.has(v.name),
)
visibleFbInstances.forEach((fbInstance) => {
Array.from(variableInfoMapRef.current!.entries()).forEach(([_, varInfos]) => {
for (const varInfo of varInfos) {
if (
varInfo.pouName === fbdFbInstanceCtx.programName &&
varInfo.variable.name.startsWith(`${fbdFbInstanceCtx.fbVariableName}.`) &&
varInfo.variable.type.definition === 'base-type' &&
varInfo.variable.type.value.toLowerCase() === 'bool'
) {
const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}`
debugVariableKeys.add(compositeKey)
}
}
})
} else {
const functionBlockInstances = currentPou.data.variables.filter(
(variable) => variable.type.definition === 'derived',
)

functionBlockInstances.forEach((fbInstance) => {
Array.from(variableInfoMapRef.current!.entries()).forEach(([_, varInfos]) => {
for (const varInfo of varInfos) {
if (fbInstanceCtx) {
// FB POU context
if (
varInfo.pouName === currentPou.data.name &&
varInfo.variable.name.startsWith(`${fbInstance.name}.`) &&
varInfo.variable.type.definition === 'base-type' &&
varInfo.variable.type.value.toLowerCase() === 'bool'
varInfo.pouName === fbInstanceCtx.programName &&
varInfo.variable.name.startsWith(`${fbInstanceCtx.fbVariableName}.${fbInstance.name}.`)
) {
const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}`
debugVariableKeys.add(compositeKey)
debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`)
}
}
})
})
}

// For FB POUs, poll function outputs using instance context
// For program POUs, poll function outputs using the standard approach
if (currentPou.type === 'function-block' && fbdFbInstanceCtx && currentFbdFlow) {
currentFbdFlow.rung.nodes.forEach((node) => {
if (node.type === 'block') {
const blockData = node.data as {
variant?: { type: string }
numericId?: string
}

if (blockData.variant?.type === 'function' && blockData.numericId) {
Array.from(variableInfoMapRef.current!.entries()).forEach(([_, varInfos]) => {
for (const varInfo of varInfos) {
if (
varInfo.pouName === fbdFbInstanceCtx.programName &&
varInfo.variable.name.startsWith(`${fbdFbInstanceCtx.fbVariableName}.`) &&
varInfo.variable.name.includes(blockData.numericId!)
) {
const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}`
debugVariableKeys.add(compositeKey)
}
}
})
}
}
})
} else {
const instances = currentProject.data.configuration.resource.instances
const programInstance = instances.find((inst) => inst.program === currentPou.data.name)
if (programInstance && currentFbdFlow) {
currentFbdFlow.rung.nodes.forEach((node) => {
if (node.type === 'block') {
const blockData = node.data as {
variant?: { type: string }
numericId?: string
}

if (blockData.variant?.type === 'function' && blockData.numericId) {
Array.from(variableInfoMapRef.current!.entries()).forEach(([_, varInfos]) => {
for (const varInfo of varInfos) {
if (
varInfo.pouName === currentPou.data.name &&
varInfo.variable.name.includes(blockData.numericId!)
) {
const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}`
debugVariableKeys.add(compositeKey)
}
}
})
}
}
})
}
}
}

// Poll all variables of the active POU so non-BOOL values can be displayed on the diagram.
// This adds every variable registered in variableInfoMapRef that belongs to the current POU,
// enabling the DebugValueBadge components to show real-time values for INT, REAL, etc.
if (currentPou) {
if (currentPou.type === 'function-block') {
// For FB POUs, resolve the selected instance context and match variables by
// program name + instance path prefix (same pattern used for BOOL polling above).
const fbTypeKey = currentPou.data.name.toUpperCase()
const selectedKey = fbSelectedInstance.get(fbTypeKey)
if (selectedKey) {
const instances = fbDebugInstances.get(fbTypeKey) || []
const selectedInstance = instances.find((inst) => inst.key === selectedKey)
if (selectedInstance) {
const instancePrefix = `${selectedInstance.fbVariableName}.`
Array.from(variableInfoMapRef.current.values()).forEach((varInfos) => {
for (const varInfo of varInfos) {
if (
varInfo.pouName === selectedInstance.programName &&
varInfo.variable.name.startsWith(instancePrefix)
) {
debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`)
}
} else {
// Program POU context
if (
varInfo.pouName === currentPou.data.name &&
varInfo.variable.name.startsWith(`${fbInstance.name}.`)
) {
debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`)
}
})
}
}
} else {
const activePouName = currentPou.data.name
Array.from(variableInfoMapRef.current.values()).forEach((varInfos) => {
for (const varInfo of varInfos) {
if (varInfo.pouName === activePouName) {
debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`)
}
}
})
}
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Viewport polling still ignores non-direct markers.

This branch only expands derived parents. It never turns graphical numericId markers into the _TMP_* keys used by function outputs, and it skips array/UDT parents even though variableInfoMapRef already contains their children. Visible function blocks, struct fields, and array elements will therefore stay stale unless they are watched or tree-expanded elsewhere.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/screens/workspace-screen.tsx` around lines 1213 - 1249, The
viewport-poll expansion only handles derived-type parents and misses
function-output numericId markers and children of array/UDT parents; update the
logic that builds debugVariableKeys (around debugViewportVarNames,
makeCompositeKeyForCurrentPou, variableInfoMapRef, currentPou, fbInstanceCtx) to
also: 1) when a visible variable is a function/block marker (numericId) call the
same composite-key mapping used for direct vars (via
makeCompositeKeyForCurrentPou or the existing composite-key convention) and add
the resulting _TMP_* keys to debugVariableKeys; 2) treat visible array/UDT
parents similarly to derived by scanning variableInfoMapRef.current entries for
children whose varInfo.variable.name startsWith the parent name (using the same
pou-context checks with fbInstanceCtx/currentPou.data.name) and add
pouName:childName for each child so array/UDT fields and struct members are
included in polling.

Comment on lines +10 to +14
// Build regex patterns for each variable name (word-boundary match)
const patterns = variableNames.map((name) => {
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
return { name, pattern: new RegExp(`\\b${escaped}(?![\\w.\\[])`, 'i') }
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/renderer/utils/debug/collect-st-variables.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1554


🏁 Script executed:

cat -n src/renderer/screens/workspace-screen.tsx | head -100

Repository: Autonomy-Logic/openplc-editor

Length of output: 5066


🏁 Script executed:

rg -n "debugViewportVarNames" src/renderer/screens/workspace-screen.tsx -A 5 -B 5

Repository: Autonomy-Logic/openplc-editor

Length of output: 1322


🏁 Script executed:

rg -n "collectSTVariableNames" --type ts --type tsx

Repository: Autonomy-Logic/openplc-editor

Length of output: 100


🏁 Script executed:

rg -n "debugViewportVarNames" src/ -B 3 -A 3 | head -80

Repository: Autonomy-Logic/openplc-editor

Length of output: 5037


🏁 Script executed:

rg "collectSTVariableNames" src/ -A 5 -B 5

Repository: Autonomy-Logic/openplc-editor

Length of output: 4180


🏁 Script executed:

find src -name "*.ts" -o -name "*.tsx" | xargs grep -l "collectSTVariableNames"

Repository: Autonomy-Logic/openplc-editor

Length of output: 191


🏁 Script executed:

find src -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*.test.tsx" -o -name "*.spec.tsx" \) | xargs grep -l "collectSTVariableNames"

Repository: Autonomy-Logic/openplc-editor

Length of output: 57


🏁 Script executed:

cat -n src/renderer/components/_features/[workspace]/editor/monaco/index.tsx | sed -n '1,150p' | tail -80

Repository: Autonomy-Logic/openplc-editor

Length of output: 3468


🏁 Script executed:

cat -n src/renderer/components/_features/[workspace]/editor/monaco/index.tsx | grep -A 30 "const visibleVarNames = collectSTVariableNames"

Repository: Autonomy-Logic/openplc-editor

Length of output: 1428


🏁 Script executed:

cat -n src/renderer/components/_features/[workspace]/editor/monaco/index.tsx | grep -B 30 "const visibleVarNames = collectSTVariableNames"

Repository: Autonomy-Logic/openplc-editor

Length of output: 1480


🏁 Script executed:

cat -n src/renderer/components/_features/[workspace]/editor/monaco/index.tsx | sed -n '258p;259p;260p'

Repository: Autonomy-Logic/openplc-editor

Length of output: 234


🏁 Script executed:

cat -n src/renderer/utils/debug/collect-st-variables.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1554


The negative lookahead pattern prevents parent symbols from being detected when only accessed via member or array notation.

When source code contains TON0.Q, cfg.limit, or arr[1], the pattern \b${escaped}(?![\w.\[]) rejects parent matches because the negative lookahead forbids matches followed by . or [. As a result, parent symbols are never added to debugViewportVarNames. Since nested FB variable polling depends on parent presence (workspace-screen.tsx line 1225: debugViewportVarNames.has(v.name)), users cannot debug derived-type members unless the parent symbol appears standalone in the visible code. This breaks debugging when only member accesses are visible.

🧰 Tools
🪛 ast-grep (0.41.1)

[warning] 12-12: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\b${escaped}(?![\\w.\\[]), 'i')
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html

(regexp-from-variable)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/debug/collect-st-variables.ts` around lines 10 - 14, The
regex in the variableNames.map block uses a negative lookahead (/(?![\w.\[])/)
that prevents matching parent symbols when they're accessed via member or array
notation (e.g., TON0.Q, cfg.limit, arr[1]); update the RegExp construction in
that block (the code that builds patterns from variableNames/escaped) to remove
the negative lookahead and instead rely on the existing word-boundary (\\b) or a
more permissive lookahead that still prevents partial identifier matches but
allows '.' and '[' after the identifier (for example, use new
RegExp(`\\b${escaped}`, 'i')). This will ensure debugViewportVarNames can
include parent symbols used only via member or array access so downstream checks
(e.g., debugViewportVarNames.has(v.name) in workspace-screen.tsx) work
correctly.

Comment on lines +5 to +43
function stripLineComments(line: string, state: BlockCommentState): { stripped: string; state: BlockCommentState } {
const chars = [...line]
let i = 0
let s = state

while (i < chars.length) {
if (s) {
const endMarker = s === 'paren' ? ')' : '/'
if (chars[i] === '*' && chars[i + 1] === endMarker) {
chars[i] = ' '
chars[i + 1] = ' '
i += 2
s = false
} else {
chars[i] = ' '
i++
}
} else {
if (chars[i] === '/' && chars[i + 1] === '/') {
for (let j = i; j < chars.length; j++) chars[j] = ' '
break
}
if (chars[i] === '(' && chars[i + 1] === '*') {
chars[i] = ' '
chars[i + 1] = ' '
i += 2
s = 'paren'
} else if (chars[i] === '/' && chars[i + 1] === '*') {
chars[i] = ' '
chars[i + 1] = ' '
i += 2
s = 'slash'
} else {
i++
}
}
}

return { stripped: chars.join(''), state: s }
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "strip-line-comments.ts" -type f

Repository: Autonomy-Logic/openplc-editor

Length of output: 122


🏁 Script executed:

cat -n src/renderer/utils/debug/strip-line-comments.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1654


🏁 Script executed:

rg -r "stripLineComments" --type ts --type tsx -A 3 -B 1

Repository: Autonomy-Logic/openplc-editor

Length of output: 469


🏁 Script executed:

# Look for tests related to this function
fd ".*test.*" --type f | xargs grep -l "stripLineComments" 2>/dev/null || echo "No test files found"

Repository: Autonomy-Logic/openplc-editor

Length of output: 92


🏁 Script executed:

# Check if there's documentation or comments about the function's purpose
head -30 src/renderer/utils/debug/strip-line-comments.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1016


🏁 Script executed:

# Look at the broader debug utilities directory
ls -la src/renderer/utils/debug/

Repository: Autonomy-Logic/openplc-editor

Length of output: 328


🏁 Script executed:

rg "stripLineComments" --type ts --type tsx -A 3 -B 1

Repository: Autonomy-Logic/openplc-editor

Length of output: 100


🏁 Script executed:

# Check the collect-st-variables file which is in the same directory
cat -n src/renderer/utils/debug/collect-st-variables.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 1554


🏁 Script executed:

# Search for usage in the whole codebase to understand the context better
rg "stripLineComments\|BlockCommentState" --type ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Look for any tests or examples of how this is used
fd "test\|spec" --type f | head -20

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Check if there are any other related comment-stripping functions
rg "comment" src/renderer/utils/debug/ --type ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 598


🏁 Script executed:

# Check the language context - look for IEC 61131-3 or ST language references
rg -i "iec\|structured text\|st language" src/renderer/ --type ts | head -20

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Look at the tsconfig or package.json to understand the project
cat tsconfig.json | head -50

Repository: Autonomy-Logic/openplc-editor

Length of output: 1116


🏁 Script executed:

# Check for any documentation about string literal handling in ST language
find . -name "*.md" -o -name "*.txt" | xargs grep -l "string\|literal\|quote" 2>/dev/null | head -5

Repository: Autonomy-Logic/openplc-editor

Length of output: 282


🏁 Script executed:

# Check the project documentation for ST/IEC 61131-3 language info
cat ./CLAUDE.md | head -100

Repository: Autonomy-Logic/openplc-editor

Length of output: 3123


🏁 Script executed:

# Look for ST language specification or parsing logic
rg -i "st\|structured text" src/renderer/utils/debug/ --type ts -A 5 -B 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Check the collectSTVariableNames usage to understand what language/domain
rg "collectSTVariableNames" --type ts -B 5 -A 5

Repository: Autonomy-Logic/openplc-editor

Length of output: 4180


🏁 Script executed:

# Understand ST language string literal syntax better - check examples in codebase
rg "':=" --type ts | head -10

Repository: Autonomy-Logic/openplc-editor

Length of output: 410


🏁 Script executed:

# Check if there are any existing string/quote handling utilities
rg "quote\|string.*literal" src/renderer/utils/ --type ts -A 2 -B 2

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


🏁 Script executed:

# Look at the Monaco editor integration to see how stripLineComments is used in practice
sed -n '1,100p' src/renderer/components/_features/[workspace]/editor/monaco/index.tsx | head -50

Repository: Autonomy-Logic/openplc-editor

Length of output: 1969


🏁 Script executed:

# Search for any string literal pattern definitions in the codebase
rg "\'.*\'" src/renderer/ --type ts | grep -v node_modules | head -15

Repository: Autonomy-Logic/openplc-editor

Length of output: 145


🏁 Script executed:

# Search for ST language string or quoted literal handling
rg "string\|quoted" src/renderer/components/_features/workspace/editor/monaco/configs/languages/st/ --type ts -A 3 -B 1

Repository: Autonomy-Logic/openplc-editor

Length of output: 192


🏁 Script executed:

# Look at the ST language configuration
cat src/renderer/components/_features/\[workspace\]/editor/monaco/configs/languages/st/st.ts

Repository: Autonomy-Logic/openplc-editor

Length of output: 13834


🏁 Script executed:

# Search for IEC 61131-3 string literal syntax documentation in code
rg -i "string.*literal\|quote" docs/ --type md -A 2 -B 1

Repository: Autonomy-Logic/openplc-editor

Length of output: 55


Add quote-state tracking to prevent stripping comment markers inside string literals.

The stripLineComments() function tracks block-comment state (false | 'paren' | 'slash') but does not track quote state. In ST/IEC 61131-3, single-quoted strings can contain // sequences (e.g., msg := 'http://x'; ready := TRUE;). Without quote tracking, the function treats // inside the string as a line comment, blanking the rest of the line and hiding the ready identifier from both viewport variable collection and debug badge scanning.

Extend BlockCommentState to include 'single-quote' and implement quote detection in the scanner loop, mirroring the existing block-comment state transitions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/renderer/utils/debug/strip-line-comments.ts` around lines 5 - 43,
stripLineComments fails to track string quotes so it treats // inside
single-quoted literals as comments; update the BlockCommentState union to
include 'single-quote' and modify stripLineComments (use the local state
variable s, the chars array and the scanning while loop) to detect entering a
single-quote when not already in a comment (on chars[i] === ''' ), set s =
'single-quote' and advance, and when s === 'single-quote' scan forward until the
closing quote (handle escaped single-quotes by treating doubled quotes '' as an
escaped quote and advancing past both) without blanking characters (unlike
block-comment states), then on closing quote set s = false and continue so //
inside strings won't be treated as line comments.

@thiagoralves thiagoralves deleted the fix/debug-inline-mount-timing branch March 13, 2026 02:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant