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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ladderSelectors } from '@root/renderer/hooks'
import { openPLCStoreBase, useOpenPLCStore } from '@root/renderer/store'
import { RungLadderState, zodLadderFlowSchema } from '@root/renderer/store/slices'
import { cn } from '@root/utils'
import { useEffect, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createPortal } from 'react-dom'
import { v4 as uuidv4 } from 'uuid'

Expand All @@ -50,6 +50,7 @@ export default function LadderEditor() {
snapshotActions: { addSnapshot },
libraries: { user: userLibraries },
workspace: { isDebuggerVisible },
workspaceActions: { setDebugViewportVarNames },
} = useOpenPLCStore()

const updateModelLadder = ladderSelectors.useUpdateModelLadder()
Expand Down Expand Up @@ -244,6 +245,76 @@ export default function LadderEditor() {
}
}, [scrollableRef.current, editor.meta.name])

// Collect variable names from ladder rung nodes (contacts, coils, blocks)
const collectVarNamesFromRungs = useCallback((visibleRungs: RungLadderState[]) => {
const names = new Set<string>()
for (const rung of visibleRungs) {
for (const node of rung.nodes) {
if (node.type === 'contact' || node.type === 'coil') {
const varName = (node.data as { variable?: { name?: string } }).variable?.name
if (varName) names.add(varName)
}
if (node.type === 'block') {
const blockData = node.data as { variant?: { type: string }; numericId?: string }
if (blockData.numericId) names.add(blockData.numericId)
}
Comment on lines +257 to +260
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.

}
}
return names
}, [])

// Push visible viewport variable names to store (debounced 1s) during debug
const debugTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
if (!isDebuggerVisible || !scrollableRef.current) {
return
}

const computeVisibleVars = () => {
const container = scrollableRef.current
if (!container) return

const scrollTop = container.scrollTop
const viewportHeight = container.clientHeight
const viewportBottom = scrollTop + viewportHeight

// Find visible rungs by checking rung element positions (each rung div has id={rung.id})
const visibleRungIds = new Set<string>()

for (const rung of rungs) {
const el = document.getElementById(rung.id)
if (!el) continue
const top = el.offsetTop
const bottom = top + el.offsetHeight
// Rung overlaps with viewport
if (bottom > scrollTop && top < viewportBottom) {
visibleRungIds.add(rung.id)
}
}

const visibleRungs = rungs.filter((r) => visibleRungIds.has(r.id))
const varNames = collectVarNamesFromRungs(visibleRungs.length > 0 ? visibleRungs : rungs)
setDebugViewportVarNames(varNames)
}

// Compute immediately
computeVisibleVars()

// Debounced scroll handler
const handleScroll = () => {
if (debugTimerRef.current) clearTimeout(debugTimerRef.current)
debugTimerRef.current = setTimeout(computeVisibleVars, 1000)
}

const container = scrollableRef.current
container.addEventListener('scroll', handleScroll)

return () => {
container.removeEventListener('scroll', handleScroll)
if (debugTimerRef.current) clearTimeout(debugTimerRef.current)
}
}, [isDebuggerVisible, rungs, collectVarNamesFromRungs, setDebugViewportVarNames])

function getLibraryDivergences() {
if (!flow) return []

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import './configs'
import { Editor as PrimitiveEditor } from '@monaco-editor/react'
import { Modal, ModalContent, ModalTitle } from '@process:renderer/components/_molecules/modal'
import { openPLCStoreBase, useOpenPLCStore } from '@process:renderer/store'
import { collectSTVariableNames } from '@root/renderer/utils/debug/collect-st-variables'
import { type BlockCommentState, stripLineComments } from '@root/renderer/utils/debug/strip-line-comments'
import { PLCVariable } from '@root/types/PLC'
import { baseTypeSchema, type PLCPou } from '@root/types/PLC/open-plc'
import { getExtensionFromLanguage, getFolderFromPouType } from '@root/utils/PLC/pou-file-extensions'
Expand Down Expand Up @@ -65,50 +67,6 @@ const bridge = window.bridge as unknown as {
onFileExternalChange: (handler: (event: IpcRendererEvent, data: { filePath: string }) => void) => () => void
}

// Replaces comment regions with spaces so column positions are preserved.
// Tracks block comment state across lines: (*..*), /*..*/, and // line comments.
type BlockCommentState = false | 'paren' | 'slash'
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 }
}

const MonacoEditor = (props: monacoEditorProps): ReturnType<typeof PrimitiveEditor> => {
const { language, path, name } = props
const editorRef = useRef<null | monaco.editor.IStandaloneCodeEditor>(null)
Expand Down Expand Up @@ -146,6 +104,7 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType<typeof PrimitiveEdit
projectActions: { updatePou, createVariable },
sharedWorkspaceActions: { handleFileAndWorkspaceSavedState },
snapshotActions: { addSnapshot },
workspaceActions: { setDebugViewportVarNames },
} = useOpenPLCStore()

// Create a unique Monaco path by combining project path with relative path
Expand Down Expand Up @@ -288,6 +247,46 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType<typeof PrimitiveEdit
editorRef.current?.updateOptions({ readOnly: isDebuggerVisible })
}, [isDebuggerVisible])

// Push visible variable names to store for ST/IL debugger polling (debounced 1s on scroll)
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])
Comment on lines +251 to +288
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.


// Resolve FB instance context for composite key building
const fbInstanceContext = useMemo(() => {
if (!pou || pou.type !== 'function-block') return null
Expand Down
57 changes: 57 additions & 0 deletions src/renderer/components/_molecules/graphical-editor/fbd/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false }
modalActions: { closeModal, openModal },
snapshotActions: { addSnapshot },
workspace: { isDebuggerVisible, debugVariableValues, debugForcedVariables },
workspaceActions: { setDebugViewportVarNames },
} = useOpenPLCStore()
const getCompositeKey = useDebugCompositeKey()

Expand Down Expand Up @@ -697,6 +698,60 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false }
openModal(modalToOpen, node)
}

// Compute visible variable names from FBD nodes within viewport bounds (debounced 1s)
const debugViewportTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)

const computeFbdVisibleVars = useCallback(() => {
if (!isDebuggerVisible || !reactFlowInstance || !reactFlowViewportRef.current) return

const vp = reactFlowInstance.getViewport()
const containerRect = reactFlowViewportRef.current.getBoundingClientRect()
const visibleBounds = {
minX: -vp.x / vp.zoom,
maxX: (-vp.x + containerRect.width) / vp.zoom,
minY: -vp.y / vp.zoom,
maxY: (-vp.y + containerRect.height) / vp.zoom,
}

const varNames = new Set<string>()
for (const node of rung.nodes) {
// Check if node overlaps with visible bounds
const nodeRight = node.position.x + (node.measured?.width ?? node.width ?? 150)
const nodeBottom = node.position.y + (node.measured?.height ?? node.height ?? 50)
if (nodeRight < visibleBounds.minX || node.position.x > visibleBounds.maxX) continue
if (nodeBottom < visibleBounds.minY || node.position.y > visibleBounds.maxY) continue

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)
Comment on lines +724 to +730
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.

}
}

setDebugViewportVarNames(varNames)
}, [isDebuggerVisible, reactFlowInstance, rung.nodes, setDebugViewportVarNames])

// Compute on debug start and on viewport move (debounced)
useEffect(() => {
if (!isDebuggerVisible) return
computeFbdVisibleVars()
}, [isDebuggerVisible, computeFbdVisibleVars])

const handleDebugViewportMove = useCallback(() => {
if (!isDebuggerVisible) return
if (debugViewportTimerRef.current) clearTimeout(debugViewportTimerRef.current)
debugViewportTimerRef.current = setTimeout(computeFbdVisibleVars, 1000)
}, [isDebuggerVisible, computeFbdVisibleVars])

useEffect(() => {
return () => {
if (debugViewportTimerRef.current) clearTimeout(debugViewportTimerRef.current)
}
}, [])

/**
* Handle the close of the modal
*/
Expand Down Expand Up @@ -769,6 +824,8 @@ export const FBDBody = ({ rung, nodeDivergences = [], isDebuggerActive = false }
onNodeDragStart: isDebuggerActive ? undefined : onNodeDragStart,
onNodeDragStop: isDebuggerActive ? undefined : onNodeDragStop,

onMoveEnd: handleDebugViewportMove,

preventScrolling: canZoom,
panOnDrag: canPan,

Expand Down
Loading
Loading