diff --git a/src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx b/src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx index 536cca6dd..5ebbed98f 100644 --- a/src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx +++ b/src/renderer/components/_features/[workspace]/editor/graphical/ladder/index.tsx @@ -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' @@ -50,6 +50,7 @@ export default function LadderEditor() { snapshotActions: { addSnapshot }, libraries: { user: userLibraries }, workspace: { isDebuggerVisible }, + workspaceActions: { setDebugViewportVarNames }, } = useOpenPLCStore() const updateModelLadder = ladderSelectors.useUpdateModelLadder() @@ -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() + 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) + } + } + } + return names + }, []) + + // Push visible viewport variable names to store (debounced 1s) during debug + const debugTimerRef = useRef | 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() + + 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 [] diff --git a/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx b/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx index a0c678c1d..eb80ec9e5 100644 --- a/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx +++ b/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx @@ -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' @@ -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 => { const { language, path, name } = props const editorRef = useRef(null) @@ -146,6 +104,7 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType { + 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 + const disposable = editor.onDidScrollChange(() => { + clearTimeout(timer) + timer = setTimeout(computeVisibleVars, 1000) + }) + + return () => { + disposable.dispose() + clearTimeout(timer) + } + }, [isDebuggerVisible, editorMounted, name, language, pou?.data.variables, setDebugViewportVarNames]) + // Resolve FB instance context for composite key building const fbInstanceContext = useMemo(() => { if (!pou || pou.type !== 'function-block') return null diff --git a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx index e29384bbd..35a25cd3d 100644 --- a/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx +++ b/src/renderer/components/_molecules/graphical-editor/fbd/index.tsx @@ -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() @@ -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 | 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() + 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) + } + } + + 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 */ @@ -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, diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index e4585790c..7ea6694c0 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -1172,7 +1172,7 @@ const WorkspaceScreen = () => { } }) - const { editor, ladderFlows } = useOpenPLCStore.getState() + const { editor } = useOpenPLCStore.getState() const currentPou = currentProject.data.pous.find((pou) => pou.data.name === editor.meta.name) // Helper to create composite key for current POU, handling FB instance context @@ -1190,322 +1190,63 @@ const WorkspaceScreen = () => { return `${currentPou.data.name}:${variableName}` } - if (currentPou && currentPou.data.body.language === 'ld') { - const currentLadderFlow = ladderFlows.find((flow) => flow.name === editor.meta.name) - if (currentLadderFlow) { - currentLadderFlow.rungs.forEach((rung) => { - rung.nodes.forEach((node) => { - if (node.type === 'contact' || node.type === 'coil') { - const nodeData = node.data as { - variable?: { name?: string; type?: { definition?: string; value?: string } } - } - const variableName = nodeData.variable?.name - - if ( - variableName && - nodeData.variable?.type?.definition === 'base-type' && - nodeData.variable?.type?.value?.toUpperCase() === 'BOOL' - ) { - const compositeKey = makeCompositeKeyForCurrentPou(variableName) - if (compositeKey) { - debugVariableKeys.add(compositeKey) - } - } - } - }) - }) - } - - // Get FB instance context for function block POUs - let fbInstanceCtx: { programName: string; fbVariableName: string } | null = null - if (currentPou.type === 'function-block') { - 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) { - fbInstanceCtx = { - programName: selectedInstance.programName, - fbVariableName: selectedInstance.fbVariableName, - } + // Resolve FB instance context once (shared by all language sections) + let fbInstanceCtx: { programName: string; fbVariableName: string } | null = null + if (currentPou?.type === 'function-block') { + 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) { + fbInstanceCtx = { + programName: selectedInstance.programName, + fbVariableName: selectedInstance.fbVariableName, } } } - - // 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' && fbInstanceCtx) { - // Poll all nested BOOL variables within the FB instance - Array.from(variableInfoMapRef.current.entries()).forEach(([_, varInfos]) => { - for (const varInfo of varInfos) { - if ( - varInfo.pouName === fbInstanceCtx.programName && - varInfo.variable.name.startsWith(`${fbInstanceCtx.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 ( - varInfo.pouName === currentPou.data.name && - varInfo.variable.name.startsWith(`${fbInstance.name}.`) && - varInfo.variable.type.definition === 'base-type' && - varInfo.variable.type.value.toLowerCase() === 'bool' - ) { - const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}` - debugVariableKeys.add(compositeKey) - } - } - }) - }) - } - - // For FB POUs, poll function outputs using instance context - // For program POUs, poll function outputs using the standard approach - if (currentPou.type === 'function-block' && fbInstanceCtx && currentLadderFlow) { - currentLadderFlow.rungs.forEach((rung) => { - 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 === fbInstanceCtx.programName && - varInfo.variable.name.startsWith(`${fbInstanceCtx.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 && currentLadderFlow) { - currentLadderFlow.rungs.forEach((rung) => { - 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) - } - } - }) - } - } - }) - }) - } - } } - const { fbdFlows } = useOpenPLCStore.getState() - if (currentPou && currentPou.data.body.language === 'fbd') { - const currentFbdFlow = fbdFlows.find((flow) => flow.name === editor.meta.name) - if (currentFbdFlow) { - currentFbdFlow.rung.nodes.forEach((node) => { - if (node.type === 'input-variable' || node.type === 'output-variable' || node.type === 'inout-variable') { - const nodeData = node.data as { - variable?: { name?: string } - } - const variableName = nodeData.variable?.name - - if (variableName) { - const variable = currentPou.data.variables.find( - (v) => v.name.toLowerCase() === variableName.toLowerCase(), - ) - if (variable && variable.type.value.toUpperCase() === 'BOOL') { - const compositeKey = makeCompositeKeyForCurrentPou(variableName) - if (compositeKey) { - debugVariableKeys.add(compositeKey) - } - } - } - } - }) - } + // --- Viewport-visible variables (computed by visual components, debounced 1s) --- + const { debugViewportVarNames } = useOpenPLCStore.getState().workspace - // Get FB instance context for function block POUs (FBD) - let fbdFbInstanceCtx: { programName: string; fbVariableName: string } | null = null - if (currentPou.type === 'function-block') { - 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) { - fbdFbInstanceCtx = { - programName: selectedInstance.programName, - fbVariableName: selectedInstance.fbVariableName, - } - } + 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}`) } } }) - } + }) } // Forced variables must also be polled so their current value appears in the debugger panel diff --git a/src/renderer/store/slices/workspace/slice.ts b/src/renderer/store/slices/workspace/slice.ts index 4c683dbae..e52199755 100644 --- a/src/renderer/store/slices/workspace/slice.ts +++ b/src/renderer/store/slices/workspace/slice.ts @@ -27,6 +27,7 @@ const createWorkspaceSlice: StateCreator debugExpandedNodes: new Map(), fbDebugInstances: new Map(), fbSelectedInstance: new Map(), + debugViewportVarNames: new Set(), isPlcLogsVisible: false, plcLogs: '', plcLogsLastId: null, @@ -156,6 +157,7 @@ const createWorkspaceSlice: StateCreator workspace.debugExpandedNodes = new Map() workspace.fbDebugInstances = new Map() workspace.fbSelectedInstance = new Map() + workspace.debugViewportVarNames = new Set() // Reset PLC logs state workspace.isPlcLogsVisible = false workspace.plcLogs = '' @@ -172,6 +174,9 @@ const createWorkspaceSlice: StateCreator setState( produce(({ workspace }: WorkspaceSlice) => { workspace.isDebuggerVisible = isVisible + if (!isVisible) { + workspace.debugViewportVarNames = new Set() + } }), ) }, @@ -258,6 +263,13 @@ const createWorkspaceSlice: StateCreator }), ) }, + setDebugViewportVarNames: (varNames: Set): void => { + setState( + produce(({ workspace }: WorkspaceSlice) => { + workspace.debugViewportVarNames = varNames + }), + ) + }, setPlcLogsVisible: (isVisible: boolean): void => { setState( produce(({ workspace }: WorkspaceSlice) => { diff --git a/src/renderer/store/slices/workspace/types.ts b/src/renderer/store/slices/workspace/types.ts index 270fc56c0..6b2dbee71 100644 --- a/src/renderer/store/slices/workspace/types.ts +++ b/src/renderer/store/slices/workspace/types.ts @@ -45,6 +45,7 @@ const workspaceStateSchema = z.object({ debugExpandedNodes: z.custom>((val) => val instanceof Map), fbDebugInstances: z.custom>((val) => val instanceof Map), fbSelectedInstance: z.custom>((val) => val instanceof Map), + debugViewportVarNames: z.custom>((val) => val instanceof Set), isPlcLogsVisible: z.boolean(), plcLogs: z.custom(), plcLogsLastId: z.number().nullable(), @@ -105,6 +106,7 @@ const workspaceActionsSchema = z.object({ setFbSelectedInstance: z.function().args(z.string(), z.string()).returns(z.void()), clearFbDebugContext: z.function().returns(z.void()), removeDebugVariable: z.function().args(z.string()).returns(z.void()), + setDebugViewportVarNames: z.function().args(z.custom>()).returns(z.void()), setPlcLogsVisible: z.function().args(z.boolean()).returns(z.void()), setPlcLogs: z.function().args(z.custom()).returns(z.void()), setPlcLogsLastId: z.function().args(z.number().nullable()).returns(z.void()), diff --git a/src/renderer/utils/debug/collect-st-variables.ts b/src/renderer/utils/debug/collect-st-variables.ts new file mode 100644 index 000000000..d95c81719 --- /dev/null +++ b/src/renderer/utils/debug/collect-st-variables.ts @@ -0,0 +1,37 @@ +import { type BlockCommentState, stripLineComments } from './strip-line-comments' + +/** + * Collect variable names that appear in ST/IL source text. + * Returns a Set of variable names found in the code, excluding occurrences inside comments. + */ +function collectSTVariableNames(sourceText: string, variableNames: string[]): Set { + if (variableNames.length === 0 || !sourceText) return new Set() + + // 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') } + }) + + const found = new Set() + const lines = sourceText.split('\n') + let blockCommentState: BlockCommentState = false + + for (const line of lines) { + const result = stripLineComments(line, blockCommentState) + blockCommentState = result.state + + for (const { name, pattern } of patterns) { + if (!found.has(name) && pattern.test(result.stripped)) { + found.add(name) + } + } + + // Early exit if all variables found + if (found.size === variableNames.length) break + } + + return found +} + +export { collectSTVariableNames } diff --git a/src/renderer/utils/debug/strip-line-comments.ts b/src/renderer/utils/debug/strip-line-comments.ts new file mode 100644 index 000000000..316daf776 --- /dev/null +++ b/src/renderer/utils/debug/strip-line-comments.ts @@ -0,0 +1,46 @@ +// 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 } +} + +export { type BlockCommentState, stripLineComments }