diff --git a/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx b/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx index a0c678c1d..c72b38746 100644 --- a/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx +++ b/src/renderer/components/_features/[workspace]/editor/monaco/index.tsx @@ -115,6 +115,7 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType(null) const focusDisposables = useRef<{ onFocus?: monaco.IDisposable; onBlur?: monaco.IDisposable }>({}) const [editorMounted, setEditorMounted] = useState(false) + const [modelVersion, setModelVersion] = useState(0) const { editor, @@ -283,6 +284,18 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType { + const editor = editorRef.current + if (!editor) return + const disposable = editor.onDidChangeModel(() => { + setModelVersion((v) => v + 1) + }) + return () => disposable.dispose() + }, [editorMounted]) + // Update readOnly when debugger visibility changes on an already-mounted editor useEffect(() => { editorRef.current?.updateOptions({ readOnly: isDebuggerVisible }) @@ -315,6 +328,11 @@ const MonacoEditor = (props: monacoEditorProps): ReturnType { diff --git a/src/renderer/screens/workspace-screen.tsx b/src/renderer/screens/workspace-screen.tsx index e4585790c..bfafdf624 100644 --- a/src/renderer/screens/workspace-screen.tsx +++ b/src/renderer/screens/workspace-screen.tsx @@ -1115,44 +1115,71 @@ const WorkspaceScreen = () => { // creates a watched key like main:IRRIGATION_MAIN_CONTROLLER0.TON0, and its children // (like ET, PT) should be polled when TON0 is expanded const shouldPollNestedVariable = (varName: string, pouName: string, currentGraphList: string[]): boolean => { - const parts = varName.split('.') - if (parts.length <= 1) return true // Not a nested variable - - // Check if this variable is in the graph list + // Fast-path: graphed variables must always be polled. const compositeKey = `${pouName}:${varName}` if (currentGraphList.includes(compositeKey)) { return true } - // Find the deepest watched ancestor - // For example, if varName is 'IRRIGATION_MAIN_CONTROLLER0.TON0.ET': - // - Check if 'main:IRRIGATION_MAIN_CONTROLLER0.TON0' is watched (deepest) - // - If not, check if 'main:IRRIGATION_MAIN_CONTROLLER0' is watched - let watchedAncestorIndex = -1 - for (let i = parts.length - 1; i >= 1; i--) { - const candidatePath = parts.slice(0, i).join('.') - const candidateKey = `${pouName}:${candidatePath}` + if (debugVariableKeys.has(compositeKey)) { + return true + } + + const hierarchyPaths: string[] = [] + const dotParts = varName.split('.') + let prefix = '' + for (const part of dotParts) { + const hasBracket = part.includes('[') + if (hasBracket) { + const base = part.split('[')[0] + if (base) { + const basePath = prefix ? `${prefix}.${base}` : base + if (hierarchyPaths[hierarchyPaths.length - 1] !== basePath) { + hierarchyPaths.push(basePath) + } + } + } + + const fullPath = prefix ? `${prefix}.${part}` : part + hierarchyPaths.push(fullPath) + prefix = fullPath + } + + // If there's no hierarchy, treat as not pollable. + if (hierarchyPaths.length === 0) { + return false + } + + // If this is not nested (single path, no bracket-derived parent), it must be explicitly watched/forced. + // (This matches previous behavior for simple variables.) + if (hierarchyPaths.length === 1) { + return debugVariableKeys.has(`${pouName}:${hierarchyPaths[0]}`) + } + + // Find the deepest watched ancestor in the hierarchy. + let watchedAncestorPos = -1 + for (let i = hierarchyPaths.length - 2; i >= 0; i--) { + const candidateKey = `${pouName}:${hierarchyPaths[i]}` if (debugVariableKeys.has(candidateKey)) { - watchedAncestorIndex = i + watchedAncestorPos = i break } } - // If no ancestor is watched, don't poll this variable - if (watchedAncestorIndex === -1) { + if (watchedAncestorPos === -1) { return false } - // Check if all nodes from the watched ancestor to this variable are expanded - // Start from the watched ancestor (which must be expanded to see its children) - for (let i = watchedAncestorIndex; i < parts.length; i++) { - const parentPath = parts.slice(0, i).join('.') - const parentKey = `${pouName}:${parentPath}` + // Ensure every ancestor from watched -> parent of target is expanded. + // The target node itself does not need to be expanded to show its value. + for (let i = watchedAncestorPos; i < hierarchyPaths.length - 1; i++) { + const parentKey = `${pouName}:${hierarchyPaths[i]}` const isParentExpanded = debugExpandedNodes.get(parentKey) ?? false if (!isParentExpanded) { return false } } + return true } @@ -1160,11 +1187,9 @@ const WorkspaceScreen = () => { // This now supports arbitrary nesting depth by finding the deepest watched ancestor Array.from(variableInfoMapRef.current.entries()).forEach(([_, varInfos]) => { for (const varInfo of varInfos) { - if (varInfo.variable.name.includes('.')) { + // Treat both dot-nesting (A.B) and array indexing (A[1]) as nested. + if (varInfo.variable.name.includes('.') || varInfo.variable.name.includes('[')) { const childKey = `${varInfo.pouName}:${varInfo.variable.name}` - - // Check if this nested variable should be polled based on expansion state - // shouldPollNestedVariable now handles finding the watched ancestor internally if (shouldPollNestedVariable(varInfo.variable.name, varInfo.pouName, graphListRef.current)) { debugVariableKeys.add(childKey) } @@ -1172,7 +1197,7 @@ const WorkspaceScreen = () => { } }) - const { editor, ladderFlows } = useOpenPLCStore.getState() + const { editor, ladderFlows, fbdFlows } = 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 @@ -1212,127 +1237,66 @@ const WorkspaceScreen = () => { } } } - }) - }) - } - - // 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, - } - } - } - } - - // 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) + // Variable nodes can display non-BOOL DebugValueBadges and also support force menus. + // Poll only the variable referenced by the node (not every variable in the POU). + if (node.type === 'variable') { + const nodeData = node.data as { + variable?: { name?: string } + } + const variableName = nodeData.variable?.name + if (variableName) { + const compositeKey = makeCompositeKeyForCurrentPou(variableName) + if (compositeKey) { + 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) => { + // Block nodes may show output DebugValueBadges (function blocks + functions). + // Poll only those output variables for blocks present on the diagram. if (node.type === 'block') { const blockData = node.data as { - variant?: { type: string } + variant?: { + type?: string + name?: string + variables?: Array<{ name: string; class: string; type: { definition: string; value: string } }> + } + variable?: { name?: 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}` + const variantType = blockData.variant?.type + const variantName = blockData.variant?.name + const variantVars = blockData.variant?.variables ?? [] + const outputVars = variantVars.filter((v) => v.class === 'output' || v.class === 'inOut') + + if (variantType === 'function-block') { + const instanceName = blockData.variable?.name + if (instanceName) { + for (const outVar of outputVars) { + const compositeKey = makeCompositeKeyForCurrentPou(`${instanceName}.${outVar.name}`) + if (compositeKey) { 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) - } - } - }) + } else if (variantType === 'function' && variantName && blockData.numericId) { + const numericId = blockData.numericId + for (const outVar of outputVars) { + const tmpName = `_TMP_${variantName.toUpperCase()}${numericId}_${outVar.name}` + const compositeKey = makeCompositeKeyForCurrentPou(tmpName) + if (compositeKey) { + 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) { @@ -1344,11 +1308,44 @@ const WorkspaceScreen = () => { 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) + const compositeKey = makeCompositeKeyForCurrentPou(variableName) + if (compositeKey) { + debugVariableKeys.add(compositeKey) + } + } + } + + if (node.type === 'block') { + const blockData = node.data as { + variant?: { + type?: string + name?: string + variables?: Array<{ name: string; class: string; type: { definition: string; value: string } }> + } + variable?: { name?: string } + numericId?: string + } + + const variantType = blockData.variant?.type + const variantName = blockData.variant?.name + const variantVars = blockData.variant?.variables ?? [] + const outputVars = variantVars.filter((v) => v.class === 'output' || v.class === 'inOut') + + if (variantType === 'function-block') { + const instanceName = blockData.variable?.name + if (instanceName) { + for (const outVar of outputVars) { + const compositeKey = makeCompositeKeyForCurrentPou(`${instanceName}.${outVar.name}`) + if (compositeKey) { + debugVariableKeys.add(compositeKey) + } + } + } + } else if (variantType === 'function' && variantName && blockData.numericId) { + const numericId = blockData.numericId + for (const outVar of outputVars) { + const tmpName = `_TMP_${variantName.toUpperCase()}${numericId}_${outVar.name}` + const compositeKey = makeCompositeKeyForCurrentPou(tmpName) if (compositeKey) { debugVariableKeys.add(compositeKey) } @@ -1357,154 +1354,63 @@ const WorkspaceScreen = () => { } }) } + } - // 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, + // --- ST/IL: poll variables that appear in the editor source text --- + if (currentPou && (currentPou.data.body.language === 'st' || currentPou.data.body.language === 'il')) { + const sourceText = typeof currentPou.data.body.value === 'string' ? currentPou.data.body.value : '' + if (sourceText) { + // Build candidate list: all POU variables + const candidates = currentPou.data.variables.map((v) => v.name).filter((n) => n && n.trim() !== '') + + // Check which variables appear in the source text (word-boundary match) + for (const varName of candidates) { + const pattern = new RegExp(`\\b${varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i') + if (pattern.test(sourceText)) { + 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]) => { - 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', + // For derived-type (FB instance) variables that appear in source, poll their children + const fbInstances = currentPou.data.variables.filter( + (v) => v.type.definition === 'derived' && v.name && v.name.trim() !== '', ) + for (const fbInstance of fbInstances) { + const fbPattern = new RegExp(`\\b${fbInstance.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i') + if (!fbPattern.test(sourceText)) continue - functionBlockInstances.forEach((fbInstance) => { - Array.from(variableInfoMapRef.current!.entries()).forEach(([_, varInfos]) => { + 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' && 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 (currentPou.type === 'function-block') { + // FB POU: resolve through instance context + 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 ( - varInfo.pouName === fbdFbInstanceCtx.programName && - varInfo.variable.name.startsWith(`${fbdFbInstanceCtx.fbVariableName}.`) && - varInfo.variable.name.includes(blockData.numericId!) + selectedInstance && + varInfo.pouName === selectedInstance.programName && + varInfo.variable.name.startsWith(`${selectedInstance.fbVariableName}.${fbInstance.name}.`) ) { - const compositeKey = `${varInfo.pouName}:${varInfo.variable.name}` - debugVariableKeys.add(compositeKey) + debugVariableKeys.add(`${varInfo.pouName}:${varInfo.variable.name}`) } } - }) - } - } - }) - } 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) { + } else { + // Program POU: match directly if ( - varInfo.pouName === selectedInstance.programName && - varInfo.variable.name.startsWith(instancePrefix) + 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}`) } - } - }) + }) + } } }