""" Base completion engine for DSL autocompletion. Provides an abstract base class that specific DSL implementations can extend to provide context-aware autocompletion. """ from abc import ABC, abstractmethod from typing import Any from . import utils from .base_provider import BaseMetadataProvider from .types import Position, Suggestion, CompletionResult class BaseCompletionEngine(ABC): """ Abstract base class for DSL completion engines. Subclasses must implement: - detect_scope(): Find the current scope from previous lines - detect_context(): Determine what kind of completion is expected - get_suggestions(): Generate suggestions for the detected context The main entry point is get_completions(), which orchestrates the flow. """ def __init__(self, provider: BaseMetadataProvider): """ Initialize the completion engine. Args: provider: Metadata provider for context-aware suggestions """ self.provider = provider self._id = type(self).__name__ def get_completions(self, text: str, cursor: Position) -> CompletionResult: """ Get autocompletion suggestions for the given cursor position. This is the main entry point. It: 1. Checks if cursor is in a comment (no suggestions) 2. Detects the current scope (e.g., which column) 3. Detects the completion context (what kind of token is expected) 4. Generates and filters suggestions Args: text: The full DSL document text cursor: Cursor position Returns: CompletionResult with suggestions and replacement range """ # Get the current line up to cursor line = utils.get_line_at(text, cursor.line) line_to_cursor = utils.get_line_up_to_cursor(text, cursor) # Check if in comment - no suggestions if utils.is_in_comment(line, cursor.ch): return self._empty_result(cursor) # Find word boundaries for replacement range word_range = utils.find_word_boundaries(line, cursor.ch) prefix = line[word_range.start: cursor.ch] # Detect scope from previous lines scope = self.detect_scope(text, cursor.line) # Detect completion context context = self.detect_context(text, cursor, scope) # Get suggestions for this context suggestions = self.get_suggestions(context, scope, prefix) # Filter suggestions by prefix if prefix: suggestions = self._filter_suggestions(suggestions, prefix) # Build result with correct positions from_pos = Position(line=cursor.line, ch=word_range.start) to_pos = Position(line=cursor.line, ch=word_range.end) return CompletionResult( from_pos=from_pos, to_pos=to_pos, suggestions=suggestions, ) @abstractmethod def detect_scope(self, text: str, current_line: int) -> Any: """ Detect the current scope by scanning previous lines. The scope determines which data context we're in (e.g., which column for column values suggestions). Args: text: The full document text current_line: Current line number (0-based) Returns: Scope object (type depends on the specific DSL) """ pass @abstractmethod def detect_context(self, text: str, cursor: Position, scope: Any) -> Any: """ Detect the completion context at the cursor position. Analyzes the current line to determine what kind of token is expected (e.g., keyword, preset name, operator). Args: text: The full document text cursor: Cursor position scope: The detected scope Returns: Context identifier (type depends on the specific DSL) """ pass @abstractmethod def get_suggestions(self, context: Any, scope: Any, prefix: str) -> list[Suggestion]: """ Generate suggestions for the given context. Args: context: The detected completion context scope: The detected scope prefix: The current word prefix (for filtering) Returns: List of suggestions """ pass def _filter_suggestions( self, suggestions: list[Suggestion], prefix: str ) -> list[Suggestion]: """ Filter suggestions by prefix (case-insensitive). Args: suggestions: List of suggestions prefix: Prefix to filter by Returns: Filtered list of suggestions """ prefix_lower = prefix.lower() return [s for s in suggestions if s.label.lower().startswith(prefix_lower)] def _empty_result(self, cursor: Position) -> CompletionResult: """ Return an empty completion result. Args: cursor: Cursor position Returns: CompletionResult with no suggestions """ return CompletionResult( from_pos=cursor, to_pos=cursor, suggestions=[], ) def get_id(self): return self._id