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
49 changes: 49 additions & 0 deletions src/editor/completion-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import * as vscode from 'vscode';
import { TextDocument, Position, CompletionItem, CompletionItemKind, Range } from 'vscode';

export class FunctionBlockParameterCompletionProvider implements vscode.CompletionItemProvider {

provideCompletionItems(
document: vscode.TextDocument,
position: vscode.Position,
token: vscode.CancellationToken,
context: vscode.CompletionContext
): vscode.ProviderResult<vscode.CompletionItem[] | vscode.CompletionList> {

const lineText = document.lineAt(position).text.substring(0, position.character);

// Check if we're in a function block instantiation context
// Looking for pattern like: fb_instance : FUNCTION_BLOCK_NAME (
const functionBlockPattern = /([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\(\s*$/;
const match = lineText.match(functionBlockPattern);

if (!match) {
return [];
}

const instanceName = match[1];
const functionBlockName = match[2];

// In a real implementation, we would look up the function block definition
// and extract parameter names. For now, we'll return a sample completion.
// TODO: Implement actual function block parameter lookup
const parameterNames = ['INPUT1', 'INPUT2', 'OUTPUT1'];

const completionItems = parameterNames.map(paramName => {
const item = new CompletionItem(paramName, CompletionItemKind.Field);
item.detail = `Parameter of ${functionBlockName}`;
return item;
});

return completionItems;
}
}

export function registerFunctionBlockParameterCompletionProvider(): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(
'st', // Assuming 'st' is the language identifier for Structured Text
new FunctionBlockParameterCompletionProvider(),
'(',
','
);
}
104 changes: 104 additions & 0 deletions src/editor/completion-widget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { CompletionItem, CompletionItemKind } from 'vscode-languageserver-types';

export interface FunctionBlockParameter {
name: string;
type: string;
documentation?: string;
defaultValue?: string;
}

export class CompletionWidget {
private container: HTMLElement;

constructor() {
this.container = document.createElement('div');
this.container.className = 'completion-widget';
this.container.style.display = 'none';
this.container.style.position = 'absolute';
this.container.style.zIndex = '1000';
this.container.style.backgroundColor = 'white';
this.container.style.border = '1px solid #ccc';
this.container.style.borderRadius = '4px';
this.container.style.boxShadow = '0 2px 8px rgba(0,0,0,0.1)';
this.container.style.maxHeight = '300px';
this.container.style.overflowY = 'auto';
}

show(items: CompletionItem[], position: { x: number; y: number }): void {
this.container.innerHTML = '';

items.forEach(item => {
const element = document.createElement('div');
element.className = 'completion-item';
element.style.padding = '8px 12px';
element.style.cursor = 'pointer';
element.style.borderBottom = '1px solid #eee';

element.addEventListener('mouseenter', () => {
element.style.backgroundColor = '#f0f0f0';
});

element.addEventListener('mouseleave', () => {
element.style.backgroundColor = 'white';
});

const label = document.createElement('div');
label.style.fontWeight = 'bold';
label.textContent = item.label;

const detail = document.createElement('div');
detail.style.fontSize = '0.9em';
detail.style.color = '#666';
detail.textContent = item.detail || '';

if (item.documentation) {
const doc = document.createElement('div');
doc.style.fontSize = '0.8em';
doc.style.color = '#888';
doc.style.marginTop = '4px';

if (typeof item.documentation === 'string') {
doc.textContent = item.documentation;
} else if (typeof item.documentation === 'object' && item.documentation.value) {
doc.textContent = item.documentation.value;
}

element.appendChild(label);
element.appendChild(detail);
element.appendChild(doc);
} else {
element.appendChild(label);
element.appendChild(detail);
}

this.container.appendChild(element);
});

this.container.style.left = `${position.x}px`;
this.container.style.top = `${position.y}px`;
this.container.style.display = 'block';
}

hide(): void {
this.container.style.display = 'none';
}

isVisible(): boolean {
return this.container.style.display === 'block';
}

getElement(): HTMLElement {
return this.container;
}

renderFunctionBlockParameters(parameters: FunctionBlockParameter[]): string {
return parameters.map(param => {
const defaultValue = param.defaultValue ? ` := ${param.defaultValue}` : '';
return `${param.name}: ${param.type}${defaultValue}`;
}).join('; ');
}
}

export function createCompletionWidget(): CompletionWidget {
return new CompletionWidget();
}
17 changes: 17 additions & 0 deletions src/editor/editor-keybindings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { KeyCode, KeyMod } from 'monaco-editor';
import { IEditorService } from './editor-service';

export class EditorKeybindings {
constructor(private editorService: IEditorService) {}

public registerKeybindings(): void {
// Existing keybindings...

// Add TAB key handling for function block parameter completion
this.editorService.addKeybinding({
key: KeyCode.Tab,
when: 'editorTextFocus && !editorReadonly && suggestWidgetVisible',
command: 'editor.action.triggerSuggest'
});
}
}
70 changes: 70 additions & 0 deletions src/parser/function-block-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { FunctionBlock, Parameter } from '../types/function-block';

export class FunctionBlockParser {
parse(source: string): FunctionBlock[] {
const functionBlocks: FunctionBlock[] = [];
const lines = source.split('\n');
let currentBlock: FunctionBlock | null = null;
let inInterface = false;

for (const line of lines) {
const trimmedLine = line.trim();

// Check for function block declaration
const fbMatch = trimmedLine.match(/^FUNCTION_BLOCK\s+(\w+)/);
if (fbMatch) {
currentBlock = {
name: fbMatch[1],
parameters: []
};
inInterface = false;
continue;
}

// Check for interface section
if (trimmedLine === 'VAR_INPUT' || trimmedLine === 'VAR_OUTPUT' || trimmedLine === 'VAR_IN_OUT') {
inInterface = true;
continue;
}

// End of interface section
if (trimmedLine === 'END_VAR' && currentBlock) {
inInterface = false;
continue;
}

// Parse parameters within interface
if (inInterface && currentBlock) {
const param = this.parseParameter(trimmedLine);
if (param) {
currentBlock.parameters.push(param);
}
}

// End of function block
if (trimmedLine === 'END_FUNCTION_BLOCK' && currentBlock) {
functionBlocks.push(currentBlock);
currentBlock = null;
}
}

return functionBlocks;
}

private parseParameter(line: string): Parameter | null {
// Match parameter declaration: name: type := default_value;
const paramMatch = line.match(/^([\w_]+)\s*:\s*([\w_]+)(?:\s*:=\s*(.+))?;$/);

if (!paramMatch) {
return null;
}

const [, name, type, defaultValue] = paramMatch;

return {
name,
type,
defaultValue: defaultValue ? defaultValue.trim() : undefined
};
}
}