From 6e258f01608bb897db8c5f95e1069e73ebf16e35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 31 May 2026 09:34:06 +0700 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20resolve=20#631=20=E2=80=94=20autocom?= =?UTF-8?q?plete=20for=20variables=20inside=20structs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #631 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- .../components/editor/fbd/FBDEditor.tsx | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 src/renderer/components/editor/fbd/FBDEditor.tsx diff --git a/src/renderer/components/editor/fbd/FBDEditor.tsx b/src/renderer/components/editor/fbd/FBDEditor.tsx new file mode 100644 index 000000000..62432ab9a --- /dev/null +++ b/src/renderer/components/editor/fbd/FBDEditor.tsx @@ -0,0 +1,188 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useEditorStore } from '../../../../store/editorStore'; +import { useProjectStore } from '../../../../store/projectStore'; +import { Variable } from '../../../../types/project'; +import { FBDNode, FBDWire, FBDElement } from '../../../../types/fbd'; +import { generateNodeId } from '../../../../utils/idGenerator'; +import { getVariableType } from '../../../../utils/variableUtils'; +import FBDNodeComponent from './FBDNodeComponent'; +import FBDWireComponent from './FBDWireComponent'; +import FBDAutocomplete from './FBDAutocomplete'; + +interface FBDEditorProps { + nodeId: string; +} + +const FBDEditor: React.FC = ({ nodeId }) => { + const { nodes, wires, updateNode, addNode, addWire, removeNode, removeWire } = useEditorStore(); + const { variables } = useProjectStore(); + const [selectedNode, setSelectedNode] = useState(null); + const [draggingWire, setDraggingWire] = useState<{ from: string; fromPort: string } | null>(null); + const [autocompletePosition, setAutocompletePosition] = useState<{ x: number; y: number } | null>(null); + const [autocompleteVariable, setAutocompleteVariable] = useState(''); + const editorRef = useRef(null); + + const currentNode = nodes.find(n => n.id === nodeId); + + // Get all variables including struct members for autocomplete + const getAllVariables = (): Variable[] => { + const allVars: Variable[] = []; + + const addStructMembers = (variable: Variable, prefix: string = '') => { + if (variable.type === 'STRUCT' && variable.structType) { + variable.structType.fields.forEach(field => { + const fullName = prefix ? `${prefix}.${field.name}` : field.name; + allVars.push({ + ...field, + name: fullName, + id: `${variable.id}.${field.name}` + }); + + // Recursively add nested struct members + if (field.type === 'STRUCT' && field.structType) { + addStructMembers({ + ...field, + name: fullName, + id: `${variable.id}.${field.name}` + }, fullName); + } + }); + } + }; + + variables.forEach(variable => { + allVars.push(variable); + addStructMembers(variable, variable.name); + }); + + return allVars; + }; + + const handleNodeSelect = (nodeId: string) => { + setSelectedNode(nodeId); + }; + + const handleNodeMove = (nodeId: string, x: number, y: number) => { + updateNode(nodeId, { x, y }); + }; + + const handleWireStart = (from: string, fromPort: string) => { + setDraggingWire({ from, fromPort }); + }; + + const handleWireEnd = (to: string, toPort: string) => { + if (draggingWire) { + const newWire: FBDWire = { + id: generateNodeId(), + from: draggingWire.from, + fromPort: draggingWire.fromPort, + to, + toPort, + }; + addWire(newWire); + setDraggingWire(null); + } + }; + + const handleWireCancel = () => { + setDraggingWire(null); + }; + + const handleAddNode = (type: string, x: number, y: number) => { + const newNode: FBDNode = { + id: generateNodeId(), + type, + x, + y, + inputs: [], + outputs: [], + }; + addNode(newNode); + }; + + const handleEditorClick = (e: React.MouseEvent) => { + if (e.target === editorRef.current) { + setSelectedNode(null); + setAutocompletePosition(null); + } + }; + + const handleVariableInput = (variable: string, x: number, y: number) => { + setAutocompleteVariable(variable); + setAutocompletePosition({ x, y }); + }; + + const handleVariableSelect = (variable: string) => { + // Handle variable selection + setAutocompletePosition(null); + setAutocompleteVariable(''); + }; + + // Get CSS class for variable highlighting + const getVariableClass = (variableName: string): string => { + const variable = variables.find(v => v.name === variableName.split('.')[0]); + if (!variable) return 'variable'; + + // Check if it's a struct member + if (variableName.includes('.')) { + return 'struct-member'; + } + + return 'variable'; + }; + + return ( +
+ {currentNode && ( + + )} + + {wires.map(wire => ( + + ))} + + {draggingWire && ( +
+ )} + + {autocompletePosition && ( + + )} +
+ ); +}; + +export default FBDEditor; \ No newline at end of file From 2e47783064f4bf369e432ca1bcbbbe50d4d975b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 31 May 2026 09:34:07 +0700 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20resolve=20#631=20=E2=80=94=20autocom?= =?UTF-8?q?plete=20for=20variables=20inside=20structs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #631 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- .../autocomplete/AutocompleteProvider.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 src/renderer/components/editor/autocomplete/AutocompleteProvider.ts diff --git a/src/renderer/components/editor/autocomplete/AutocompleteProvider.ts b/src/renderer/components/editor/autocomplete/AutocompleteProvider.ts new file mode 100644 index 000000000..a76130f45 --- /dev/null +++ b/src/renderer/components/editor/autocomplete/AutocompleteProvider.ts @@ -0,0 +1,66 @@ +import { CompletionItem, CompletionItemKind, Position, TextDocument } from 'vscode'; +import { VariableDeclaration } from '../../parser/VariableDeclaration'; +import { DataType } from '../../parser/DataType'; +import { StructType } from '../../parser/StructType'; + +export class AutocompleteProvider { + private variables: VariableDeclaration[] = []; + private dataTypes: DataType[] = []; + + public setVariables(variables: VariableDeclaration[]): void { + this.variables = variables; + } + + public setDataTypes(dataTypes: DataType[]): void { + this.dataTypes = dataTypes; + } + + public provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] { + const lineText = document.getText( + document.lineAt(position.line).range + ); + + const textBeforeCursor = lineText.substring(0, position.character); + + // Check if we're accessing a struct member (variable.member) + const structMemberMatch = textBeforeCursor.match(/([a-zA-Z_][a-zA-Z0-9_]*)\.([a-zA-Z_][a-zA-Z0-9_]*)?$/); + if (structMemberMatch) { + const variableName = structMemberMatch[1]; + const partialMember = structMemberMatch[2] || ''; + + // Find the variable + const variable = this.variables.find(v => v.name === variableName); + if (variable) { + // Find the struct type + const structType = this.findStructType(variable.type); + if (structType) { + // Return struct members + return structType.members + .filter(member => member.name.startsWith(partialMember)) + .map(member => { + const item = new CompletionItem(member.name, CompletionItemKind.Field); + item.detail = `${member.type} ${member.name}`; + return item; + }); + } + } + return []; + } + + // Default behavior - return top-level variables + return this.variables + .map(variable => { + const item = new CompletionItem(variable.name, CompletionItemKind.Variable); + item.detail = `${variable.type} ${variable.name}`; + return item; + }); + } + + private findStructType(typeName: string): StructType | undefined { + const dataType = this.dataTypes.find(dt => dt.name === typeName); + if (dataType && dataType instanceof StructType) { + return dataType as StructType; + } + return undefined; + } +} \ No newline at end of file From 2668e66d2e9a1c22a5698993d75f063dfa44e21b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=82n=20=C4=90o=C3=A0n?= <33853760+andoan16@users.noreply.github.com> Date: Sun, 31 May 2026 09:34:08 +0700 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20resolve=20#631=20=E2=80=94=20autocom?= =?UTF-8?q?plete=20for=20variables=20inside=20structs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #631 Signed-off-by: Ân Đoàn <33853760+andoan16@users.noreply.github.com> --- .../editor/syntax/SyntaxHighlighter.ts | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/renderer/components/editor/syntax/SyntaxHighlighter.ts diff --git a/src/renderer/components/editor/syntax/SyntaxHighlighter.ts b/src/renderer/components/editor/syntax/SyntaxHighlighter.ts new file mode 100644 index 000000000..fd51b8c0d --- /dev/null +++ b/src/renderer/components/editor/syntax/SyntaxHighlighter.ts @@ -0,0 +1,125 @@ +import { TextDocument } from 'vscode'; + +export class SyntaxHighlighter { + private static readonly KEYWORDS = [ + 'PROGRAM', 'END_PROGRAM', 'FUNCTION', 'END_FUNCTION', 'FUNCTION_BLOCK', 'END_FUNCTION_BLOCK', + 'VAR', 'VAR_INPUT', 'VAR_OUTPUT', 'VAR_IN_OUT', 'END_VAR', 'STRUCT', 'END_STRUCT', + 'IF', 'THEN', 'ELSIF', 'ELSE', 'END_IF', 'CASE', 'OF', 'END_CASE', + 'FOR', 'TO', 'BY', 'DO', 'END_FOR', 'WHILE', 'END_WHILE', 'REPEAT', 'UNTIL', 'END_REPEAT', + 'EXIT', 'RETURN', 'CONTINUE' + ]; + + private static readonly TYPES = [ + 'BOOL', 'SINT', 'INT', 'DINT', 'LINT', 'USINT', 'UINT', 'UDINT', 'ULINT', + 'REAL', 'LREAL', 'TIME', 'DATE', 'DT', 'TOD', 'STRING', 'BYTE', 'WORD', 'DWORD', 'LWORD' + ]; + + public static tokenize(document: TextDocument): any[] { + const tokens: any[] = []; + const text = document.getText(); + + // Tokenize keywords + this.KEYWORDS.forEach(keyword => { + const regex = new RegExp(`\\b${keyword}\\b`, 'g'); + let match; + while ((match = regex.exec(text)) !== null) { + tokens.push({ + line: document.positionAt(match.index).line, + startCharacter: document.positionAt(match.index).character, + length: keyword.length, + tokenType: 0 // keyword + }); + } + }); + + // Tokenize types + this.TYPES.forEach(type => { + const regex = new RegExp(`\\b${type}\\b`, 'g'); + let match; + while ((match = regex.exec(text)) !== null) { + tokens.push({ + line: document.positionAt(match.index).line, + startCharacter: document.positionAt(match.index).character, + length: type.length, + tokenType: 1 // type + }); + } + }); + + // Tokenize struct member variables (word.word pattern) + const structMemberRegex = /\\b([a-zA-Z_][a-zA-Z0-9_]*)\\.([a-zA-Z_][a-zA-Z0-9_]*)\\b/g; + let structMatch; + while ((structMatch = structMemberRegex.exec(text)) !== null) { + // Highlight the member part (after the dot) as a variable + const memberStart = structMatch.index + structMatch[1].length + 1; // +1 for the dot + tokens.push({ + line: document.positionAt(memberStart).line, + startCharacter: document.positionAt(memberStart).character, + length: structMatch[2].length, + tokenType: 2 // variable + }); + } + + // Tokenize regular variables (but not struct members) + const variableRegex = /\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b/g; + let varMatch; + while ((varMatch = variableRegex.exec(text)) !== null) { + // Skip if this is part of a struct member (already handled above) + const fullMatch = varMatch[0]; + const matchEnd = varMatch.index + fullMatch.length; + + // Check if this variable is followed by a dot (part of struct access) + const nextChar = text.charAt(matchEnd); + if (nextChar === '.') continue; + + // Check if this variable is preceded by a dot (struct member) + const prevChar = varMatch.index > 0 ? text.charAt(varMatch.index - 1) : ''; + if (prevChar === '.') continue; + + tokens.push({ + line: document.positionAt(varMatch.index).line, + startCharacter: document.positionAt(varMatch.index).character, + length: fullMatch.length, + tokenType: 2 // variable + }); + } + + // Tokenize numbers + const numberRegex = /\\b[0-9]+(?:\\.[0-9]+)?\\b/g; + let numMatch; + while ((numMatch = numberRegex.exec(text)) !== null) { + tokens.push({ + line: document.positionAt(numMatch.index).line, + startCharacter: document.positionAt(numMatch.index).character, + length: numMatch[0].length, + tokenType: 3 // number + }); + } + + // Tokenize strings + const stringRegex = /".*?"/g; + let strMatch; + while ((strMatch = stringRegex.exec(text)) !== null) { + tokens.push({ + line: document.positionAt(strMatch.index).line, + startCharacter: document.positionAt(strMatch.index).character, + length: strMatch[0].length, + tokenType: 4 // string + }); + } + + // Tokenize comments + const commentRegex = /\\/\\/.*$/gm; + let commentMatch; + while ((commentMatch = commentRegex.exec(text)) !== null) { + tokens.push({ + line: document.positionAt(commentMatch.index).line, + startCharacter: document.positionAt(commentMatch.index).character, + length: commentMatch[0].length, + tokenType: 5 // comment + }); + } + + return tokens; + } +} \ No newline at end of file