diff --git a/src/editor/completion-provider.ts b/src/editor/completion-provider.ts new file mode 100644 index 000000000..23edf8035 --- /dev/null +++ b/src/editor/completion-provider.ts @@ -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 { + + 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(), + '(', + ',' + ); +} \ No newline at end of file diff --git a/src/editor/completion-widget.ts b/src/editor/completion-widget.ts new file mode 100644 index 000000000..b13cb1253 --- /dev/null +++ b/src/editor/completion-widget.ts @@ -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(); +} \ No newline at end of file diff --git a/src/editor/editor-keybindings.ts b/src/editor/editor-keybindings.ts new file mode 100644 index 000000000..f8005d325 --- /dev/null +++ b/src/editor/editor-keybindings.ts @@ -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' + }); + } +} \ No newline at end of file diff --git a/src/parser/function-block-parser.ts b/src/parser/function-block-parser.ts new file mode 100644 index 000000000..6b867b6da --- /dev/null +++ b/src/parser/function-block-parser.ts @@ -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 + }; + } +} \ No newline at end of file