Skip to content
Open
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
@@ -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;
}
}
188 changes: 188 additions & 0 deletions src/renderer/components/editor/fbd/FBDEditor.tsx
Original file line number Diff line number Diff line change
@@ -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<FBDEditorProps> = ({ nodeId }) => {
const { nodes, wires, updateNode, addNode, addWire, removeNode, removeWire } = useEditorStore();
const { variables } = useProjectStore();
const [selectedNode, setSelectedNode] = useState<string | null>(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<string>('');
const editorRef = useRef<HTMLDivElement>(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 (
<div
ref={editorRef}
className="fbd-editor"
onClick={handleEditorClick}
>
{currentNode && (
<FBDNodeComponent
key={currentNode.id}
node={currentNode}
isSelected={selectedNode === currentNode.id}
onSelect={handleNodeSelect}
onMove={handleNodeMove}
onWireStart={handleWireStart}
onWireEnd={handleWireEnd}
onVariableInput={handleVariableInput}
getVariableClass={getVariableClass}
/>
)}

{wires.map(wire => (
<FBDWireComponent
key={wire.id}
wire={wire}
onCancel={handleWireCancel}
/>
))}

{draggingWire && (
<div
className="wire-drag-preview"
style={{
position: 'absolute',
left: 0,
top: 0,
width: '100%',
height: '100%',
pointerEvents: 'none',
}}
/>
)}

{autocompletePosition && (
<FBDAutocomplete
position={autocompletePosition}
variables={getAllVariables()}
onSelect={handleVariableSelect}
filterText={autocompleteVariable}
/>
)}
</div>
);
};

export default FBDEditor;
125 changes: 125 additions & 0 deletions src/renderer/components/editor/syntax/SyntaxHighlighter.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}