diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 00000000..d628dbc7 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,320 @@ +# Pyodide Autocomplete Implementation Plan + +## Overview + +This document outlines the implementation plan for adding Python autocomplete functionality to the Pyodide runtime agent in Anode. The goal is to provide context-aware Python code completions that work seamlessly within the collaborative notebook environment. + +## Objectives + +- **Primary**: Implement basic Python autocomplete using IPython's built-in completer +- **Secondary**: Establish patterns for runtime-specific IDE features +- **Tertiary**: Maintain multi-user collaboration without interference + +## Current State Analysis + +### ✅ Available Infrastructure +- CodeMirror 6 with `@codemirror/autocomplete` dependency installed +- `completionKeymap` and basic completion infrastructure in editor +- Pyodide worker with IPython shell setup (`shell = InteractiveShell.instance()`) +- Message passing communication between agent and worker via `sendWorkerMessage()` +- Python runtime with full IPython environment and rich display capabilities + +### ❌ Missing Components +- Completion message type in worker communication protocol +- Python-side completion logic using IPython's completion system +- CodeMirror completion source connected to runtime agent +- Integration between editor cursor position and Python execution context + +## Architecture Decisions + +### Communication Pattern: Direct Worker Messages (Not LiveStore Events) + +**Rationale:** +- **Multi-user safety**: Completion requests are user-specific and ephemeral +- **Performance**: High-frequency completion requests would pollute the event log +- **Privacy**: User typing shouldn't be broadcast to other collaborators +- **Existing pattern**: Reuses established `sendWorkerMessage()` infrastructure + +### Completion Engine: IPython's Built-in Completer + +**Rationale:** +- **Already available**: No additional dependencies required +- **Context-aware**: Understands current execution state and imported modules +- **Rich completions**: Supports module attributes, function signatures, variable names +- **Proven system**: Battle-tested completion logic from IPython ecosystem + +## Implementation Phases + +### Phase 1: Python Runtime Completion Support + +**File**: `packages/pyodide-runtime/src/runt_runtime_shell.py` + +Add completion function that leverages IPython's built-in completer: + +```python +def get_completions(code: str, cursor_pos: int) -> dict: + """Get code completions using IPython's built-in completer""" + try: + # Parse cursor position to find current line and position within line + lines = code.split('\n') + current_pos = 0 + line_number = 0 + + for i, line in enumerate(lines): + if current_pos + len(line) >= cursor_pos: + line_number = i + cursor_in_line = cursor_pos - current_pos + break + current_pos += len(line) + 1 # +1 for newline + else: + line_number = len(lines) - 1 + cursor_in_line = len(lines[-1]) if lines else 0 + + current_line = lines[line_number] if line_number < len(lines) else "" + + # Use IPython's completer + completions = shell.Completer.completions(current_line, cursor_in_line) + + return { + 'matches': [c.text for c in completions], + 'cursor_start': current_pos + (completions[0].start if completions else cursor_in_line), + 'cursor_end': current_pos + (completions[0].end if completions else cursor_in_line), + } + except Exception as e: + return {'matches': [], 'cursor_start': cursor_pos, 'cursor_end': cursor_pos} +``` + +### Phase 2: Worker Message Protocol Extension + +**File**: `packages/pyodide-runtime/src/pyodide-worker.ts` + +Add `get_completions` message handler: + +```typescript +case "get_completions": { + try { + const result = await pyodide!.runPythonAsync(` + get_completions(${JSON.stringify(data.code)}, ${data.cursor_pos}) + `); + const parsed = JSON.parse(result); + self.postMessage({ id, type: "response", data: parsed }); + } catch (error) { + self.postMessage({ + id, + type: "response", + data: { matches: [], cursor_start: data.cursor_pos, cursor_end: data.cursor_pos } + }); + } + break; +} +``` + +### Phase 3: Agent API Extension + +**File**: `packages/pyodide-runtime/src/index.ts` + +Add completion method to `PyodideRuntimeAgent`: + +```typescript +async getCompletions(code: string, cursorPos: number): Promise<{ + matches: string[]; + cursor_start: number; + cursor_end: number; +}> { + if (!this.worker) { + return { matches: [], cursor_start: cursorPos, cursor_end: cursorPos }; + } + + try { + return await this.sendWorkerMessage("get_completions", { + code, + cursor_pos: cursorPos, + }) as any; + } catch (error) { + console.warn("Python completion failed:", error); + return { matches: [], cursor_start: cursorPos, cursor_end: cursorPos }; + } +} +``` + +### Phase 4: Frontend Completion Source + +**File**: `src/components/notebook/codemirror/pythonCompletion.ts` + +Create CodeMirror completion source: + +```typescript +import { CompletionContext, CompletionResult } from "@codemirror/autocomplete"; +import type { PyodideRuntimeAgent } from "@runtimed/pyodide-runtime"; + +export function createPythonCompletionSource( + getRuntimeAgent: () => PyodideRuntimeAgent | null +) { + return async (context: CompletionContext): Promise => { + const agent = getRuntimeAgent(); + if (!agent || !agent.isRunning()) { + return null; + } + + const code = context.state.doc.toString(); + const pos = context.pos; + + try { + const result = await agent.getCompletions(code, pos); + + if (result.matches.length === 0) { + return null; + } + + return { + from: result.cursor_start, + to: result.cursor_end, + options: result.matches.map(match => ({ + label: match, + type: "variable", // TODO: Enhance with proper type detection + })), + }; + } catch (error) { + console.debug("Python completion failed:", error); + return null; + } + }; +} +``` + +### Phase 5: Editor Integration + +**Files**: +- `src/components/notebook/codemirror/baseExtensions.ts` +- `src/components/notebook/codemirror/CodeMirrorEditor.tsx` + +Extend editor with completion support: + +```typescript +// baseExtensions.ts +import { autocompletion, CompletionSource } from "@codemirror/autocomplete"; + +export function createPythonExtensions(completionSource?: CompletionSource) { + const extensions = [...baseExtensions]; + + if (completionSource) { + extensions.push( + autocompletion({ + override: [completionSource], + closeOnBlur: true, + maxRenderedOptions: 10, + }) + ); + } + + return extensions; +} +``` + +### Phase 6: Cell Editor Integration + +**File**: `src/components/notebook/cell/ExecutableCell.tsx` + +Wire completion source to runtime agent: + +```typescript +const pythonCompletionSource = useMemo(() => { + if (cell.cellType === 'python' && runtimeAgent) { + return createPythonCompletionSource(() => runtimeAgent); + } + return undefined; +}, [cell.cellType, runtimeAgent]); +``` + +## Technical Considerations + +### Performance Optimizations +- **Debouncing**: Implement request debouncing to avoid overwhelming the worker +- **Caching**: Cache recent completions for repeated partial matches +- **Timeouts**: Implement reasonable timeouts for completion requests +- **Throttling**: Limit concurrent completion requests + +### Error Handling +- **Graceful degradation**: Fall back gracefully when runtime unavailable +- **Clear messaging**: Provide user feedback for completion failures +- **Logging**: Add appropriate debug logging for troubleshooting + +### User Experience +- **Loading states**: Show visual feedback during completion requests +- **Runtime awareness**: Only enable completion when Python runtime is active +- **Performance**: Ensure completion doesn't impact typing responsiveness + +## Testing Strategy + +### Unit Tests +- Python completion function with various code samples +- Message passing between agent and worker +- CodeMirror completion source integration +- Error handling scenarios + +### Integration Tests +- Full completion workflow from editor to Python runtime +- Runtime lifecycle integration (startup/shutdown) +- Multi-user collaboration scenarios + +### Manual Test Cases +1. **Basic completions**: `pri|` → `print`, `len|` → `length` +2. **Module attributes**: `numpy.|` → `numpy.array`, `numpy.zeros` +3. **Variable completions**: User-defined variables in different scopes +4. **Import completions**: `from numpy import |` +5. **Method completions**: `"hello".upp|` → `"hello".upper` +6. **Runtime states**: Completion behavior during code execution +7. **Error scenarios**: Completion when runtime crashed/unavailable + +## Success Metrics + +### Primary Success Criteria +- [ ] Python cells show relevant completions for built-in functions +- [ ] Imported module attributes are completed correctly +- [ ] User-defined variables appear in completions +- [ ] Completion works without interfering with multi-user editing +- [ ] Performance remains smooth during typing + +### Secondary Success Criteria +- [ ] Completion works across different Python constructs (classes, functions, etc.) +- [ ] Error handling provides clear feedback +- [ ] Completion gracefully handles runtime restart scenarios + +## Future Enhancements + +### Phase 2 Features (Future) +- **Type annotations**: Enhanced completion with type information +- **Signature help**: Function parameter hints during typing +- **Documentation**: Hover documentation for completed items +- **Jedi integration**: More sophisticated completion engine +- **Cross-cell awareness**: Completions aware of variables from other cells + +### Performance Improvements +- **Incremental parsing**: Only reparse changed portions of code +- **Background pre-loading**: Pre-compute completions for common patterns +- **Server-side caching**: Cache completion results on runtime side + +## Dependencies + +### Required Dependencies +- No new dependencies required (leverages existing IPython and CodeMirror) + +### Optional Future Dependencies +- `jedi` (for enhanced Python completion) +- Additional CodeMirror completion extensions + +## Risk Mitigation + +### Potential Risks +1. **Performance impact**: Completion requests might slow down typing +2. **Worker crashes**: Completion requests might destabilize Pyodide worker +3. **Multi-user conflicts**: Completion state might interfere with collaboration + +### Mitigation Strategies +1. **Debouncing and timeouts**: Prevent excessive worker load +2. **Error isolation**: Ensure completion failures don't crash execution +3. **Separate communication channel**: Keep completion requests isolated from execution + +## Conclusion + +This implementation provides a solid foundation for Python autocomplete in Anode while maintaining the collaborative nature of the platform. The approach leverages existing infrastructure and follows established patterns, making it both maintainable and extensible for future enhancements. \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..940b842c --- /dev/null +++ b/TODO.md @@ -0,0 +1,183 @@ +# Pyodide Autocomplete Implementation TODO + +## Phase 1: Python Runtime Completion Support ✅ + +### Core Completion Function +- [x] Add `get_completions()` function to `runt_runtime_shell.py` +- [x] Handle cursor position parsing (line number and position within line) +- [x] Use IPython's `shell.Completer.completions()` +- [x] Return completion matches with cursor positions +- [x] Add error handling for completion failures + +## Phase 2: Worker Message Protocol Extension ✅ + +### Worker Message Handling +- [x] Add `get_completions` case to message handler in `pyodide-worker.ts` +- [x] Call Python `get_completions()` function from worker +- [x] Handle worker-side errors gracefully +- [x] Return completion results via worker message response + +### Testing Worker Protocol +- [ ] Test completion message handling in worker +- [ ] Verify error scenarios (invalid code, cursor position) +- [ ] Test with various Python code samples + +## Phase 3: Agent API Extension ✅ + +### PyodideRuntimeAgent Methods +- [x] Add `getCompletions()` method to `PyodideRuntimeAgent` class +- [x] Use existing `sendWorkerMessage()` infrastructure +- [x] Handle cases where worker is not initialized +- [x] Add proper TypeScript types for completion results +- [x] Add error handling and fallback responses + +### Agent Testing +- [ ] Test agent completion method with running worker +- [ ] Test error handling when worker is unavailable +- [ ] Verify completion results format + +## Phase 4: Frontend Completion Source ✅ + +### CodeMirror Integration +- [x] Create `src/components/notebook/codemirror/pythonCompletion.ts` +- [x] Implement `createPythonCompletionSource()` function +- [x] Handle async completion requests +- [x] Map completion results to CodeMirror format +- [x] Add error handling and null checks + +### Completion Source Testing +- [ ] Test completion source with mock runtime agent +- [ ] Verify CodeMirror completion result format +- [ ] Test error scenarios (agent unavailable, no completions) + +## Phase 5: Editor Extensions ✅ + +### Base Extensions Update +- [x] Extend `baseExtensions.ts` with completion support +- [x] Create `createPythonExtensions()` function +- [x] Configure autocompletion options (blur, max options, etc.) +- [x] Ensure compatibility with existing extensions + +### CodeMirror Editor Props +- [x] Add completion source prop to `CodeMirrorEditor` +- [x] Update editor extensions based on completion source +- [x] Maintain backward compatibility + +## Phase 6: Cell Editor Integration ✅ + +### ExecutableCell Integration +- [x] Connect Python completion source to runtime agent +- [x] Use `useMemo` for performance optimization +- [x] Only enable completion for Python cells +- [x] Handle runtime agent availability + +### Editor Component Updates +- [x] Pass completion source to CodeMirrorEditor +- [x] Update editor props and extensions +- [x] Test integration with existing cell functionality + +## Testing & Verification + +### Unit Tests +- [ ] Test Python `get_completions()` function with various inputs +- [ ] Test worker message protocol for completions +- [ ] Test agent `getCompletions()` method +- [ ] Test CodeMirror completion source +- [ ] Test error handling scenarios + +### Integration Tests +- [ ] Test full completion flow (editor → agent → worker → Python) +- [ ] Test completion during different runtime states +- [ ] Test multi-user scenarios (completion doesn't interfere) +- [ ] Test performance with large code samples + +### Manual Testing +- [ ] Basic Python completions (`print`, `len`, etc.) +- [ ] Module attribute completions (`numpy.array`, `pandas.DataFrame`) +- [ ] Variable completions (user-defined variables) +- [ ] Import completions (`from numpy import ...`) +- [ ] Method completions (`"string".upper()`) +- [ ] Completion behavior during code execution +- [ ] Error scenarios (runtime crashed, network issues) +- [ ] Performance testing (typing responsiveness) + +## Performance & UX + +### Optimization +- [ ] Implement request debouncing (avoid excessive requests) +- [ ] Add completion request timeout +- [ ] Cache recent completion results +- [ ] Limit concurrent completion requests + +### User Experience +- [ ] Add loading indicators for completion requests +- [ ] Ensure completion only works with active Python runtime +- [ ] Test completion behavior across runtime lifecycle +- [ ] Verify no interference with collaborative editing + +## Documentation & Polish + +### Code Documentation +- [ ] Add JSDoc comments to completion functions +- [ ] Document completion message protocol +- [ ] Add inline comments for complex logic +- [ ] Update type definitions + +### User Documentation +- [ ] Update relevant documentation with completion feature +- [ ] Add completion to feature list +- [ ] Document any keyboard shortcuts or usage patterns + +## Future Enhancements (Optional) + +### Advanced Features +- [ ] Enhanced completion types (function, variable, module, etc.) +- [ ] Completion ranking and filtering +- [ ] Signature help integration +- [ ] Documentation on hover +- [ ] Cross-cell variable awareness + +### Performance Improvements +- [ ] Background completion pre-loading +- [ ] Incremental parsing optimization +- [ ] Server-side completion caching + +## Success Criteria Verification + +### Primary Success Criteria +- [ ] Python cells show relevant completions for built-in functions +- [ ] Imported module attributes are completed correctly +- [ ] User-defined variables appear in completions +- [ ] Completion works without interfering with multi-user editing +- [ ] Performance remains smooth during typing + +### Secondary Success Criteria +- [ ] Completion works across different Python constructs +- [ ] Error handling provides clear feedback +- [ ] Completion gracefully handles runtime restart scenarios + +## Deployment Checklist + +### Pre-deployment +- [ ] All tests passing +- [ ] Performance benchmarks acceptable +- [ ] Error handling tested +- [ ] Multi-user scenarios verified +- [ ] Code review completed + +### Post-deployment +- [ ] Monitor completion request performance +- [ ] Track completion usage metrics +- [ ] Monitor for completion-related errors +- [ ] Gather user feedback + +--- + +## Notes + +- Use existing `sendWorkerMessage()` pattern for consistency +- Leverage IPython's built-in completer (no new dependencies) +- Keep completion requests separate from LiveStore events +- Focus on core functionality first, enhancements later +- Test thoroughly with various Python code patterns +- Ensure graceful degradation when runtime unavailable \ No newline at end of file diff --git a/packages/pyodide-runtime/src/index.ts b/packages/pyodide-runtime/src/index.ts index 41260a20..abb1f95f 100644 --- a/packages/pyodide-runtime/src/index.ts +++ b/packages/pyodide-runtime/src/index.ts @@ -679,6 +679,32 @@ export class PyodideRuntimeAgent extends LocalRuntimeAgent { return this.sendWorkerMessage(type, data); } + /** + * Get code completions from the Python runtime + */ + async getCompletions( + code: string, + cursorPos: number + ): Promise<{ + matches: string[]; + cursor_start: number; + cursor_end: number; + }> { + if (!this.worker) { + return { matches: [], cursor_start: cursorPos, cursor_end: cursorPos }; + } + + try { + return (await this.sendWorkerMessage("get_completions", { + code, + cursor_pos: cursorPos, + })) as any; + } catch (error) { + console.warn("Python completion failed:", error); + return { matches: [], cursor_start: cursorPos, cursor_end: cursorPos }; + } + } + /** * Cleanup worker resources */ diff --git a/packages/pyodide-runtime/src/pyodide-worker.ts b/packages/pyodide-runtime/src/pyodide-worker.ts index 9b3d029d..4d4ab6a9 100644 --- a/packages/pyodide-runtime/src/pyodide-worker.ts +++ b/packages/pyodide-runtime/src/pyodide-worker.ts @@ -155,6 +155,28 @@ await run_registered_tool("${data.toolName}", kwargs_string) break; } + case "get_completions": { + try { + const result = await pyodide!.runPythonAsync(` + get_completions(${JSON.stringify(data.code)}, ${data.cursor_pos}) + `); + const parsed = JSON.parse(result); + self.postMessage({ id, type: "response", data: parsed }); + } catch (error) { + console.debug("Python completion failed:", error); + self.postMessage({ + id, + type: "response", + data: { + matches: [], + cursor_start: data.cursor_pos, + cursor_end: data.cursor_pos, + }, + }); + } + break; + } + default: throw new Error(`Unknown message type: ${type}`); } @@ -523,6 +545,7 @@ globals()['shell'] = runt_runtime.shell globals()['get_registered_tools'] = runt_runtime.get_registered_tools globals()['run_registered_tool'] = runt_runtime.run_registered_tool globals()['tool'] = runt_runtime.tool +globals()['get_completions'] = runt_runtime.get_completions globals()['js_display_callback'] = runt_runtime.js_display_callback globals()['js_execution_callback'] = runt_runtime.js_execution_callback globals()['js_clear_callback'] = runt_runtime.js_clear_callback diff --git a/packages/pyodide-runtime/src/runt_runtime.py b/packages/pyodide-runtime/src/runt_runtime.py index 8dc97a50..0db413c9 100644 --- a/packages/pyodide-runtime/src/runt_runtime.py +++ b/packages/pyodide-runtime/src/runt_runtime.py @@ -15,7 +15,7 @@ """ # Import all components from the individual modules -from runt_runtime_shell import shell, initialize_ipython_environment +from runt_runtime_shell import shell, initialize_ipython_environment, get_completions from runt_runtime_registry import ( get_registered_tools, run_registered_tool, @@ -34,6 +34,7 @@ # Core shell and initialization "shell", "initialize_ipython_environment", + "get_completions", # Function registry and tools "get_registered_tools", "run_registered_tool", diff --git a/packages/pyodide-runtime/src/runt_runtime_shell.py b/packages/pyodide-runtime/src/runt_runtime_shell.py index 974fec92..e4bec67d 100644 --- a/packages/pyodide-runtime/src/runt_runtime_shell.py +++ b/packages/pyodide-runtime/src/runt_runtime_shell.py @@ -8,6 +8,7 @@ """ import os +import json from IPython.core.interactiveshell import InteractiveShell @@ -94,3 +95,437 @@ def initialize_ipython_environment(): setup_interrupt_patches() print("Pseudo-IPython environment ready with rich display support") + + +def get_completions(code: str, cursor_pos: int) -> str: + """Get code completions using IPython's built-in completer""" + try: + # Ensure shell has latest execution context + import __main__ + import sys + + # Sync both user namespace and global namespace with main execution context + main_dict = __main__.__dict__ + shell.user_ns.update(main_dict) + shell.user_global_ns.update(main_dict) + + # Also sync with current globals() if available + try: + import builtins + + current_globals = getattr(builtins, "_current_globals", {}) + if current_globals: + shell.user_ns.update(current_globals) + shell.user_global_ns.update(current_globals) + except: + pass + + # Force completer to refresh its namespace cache + if hasattr(shell.Completer, "namespace"): + shell.Completer.namespace = shell.user_ns + if hasattr(shell.Completer, "global_namespace"): + shell.Completer.global_namespace = shell.user_global_ns + + # Parse cursor position to find current line and position within line + lines = code.split("\n") + current_pos = 0 + line_number = 0 + cursor_in_line = cursor_pos + + # Find which line the cursor is on + for i, line in enumerate(lines): + line_end = current_pos + len(line) + if cursor_pos <= line_end: + line_number = i + cursor_in_line = cursor_pos - current_pos + break + current_pos = line_end + 1 # +1 for newline character + else: + # Cursor is at the very end + line_number = len(lines) - 1 + cursor_in_line = len(lines[-1]) if lines else 0 + + current_line = lines[line_number] if line_number < len(lines) else "" + + # For debugging + print(f"[COMPLETION DEBUG] Code: {repr(code[:50])}...") + print(f"[COMPLETION DEBUG] Cursor pos: {cursor_pos}") + print(f"[COMPLETION DEBUG] Current line: {repr(current_line)}") + print(f"[COMPLETION DEBUG] Cursor in line: {cursor_in_line}") + + # Get completions using IPython's completer + try: + completions = list( + shell.Completer.completions(current_line, cursor_in_line) + ) + print(f"[COMPLETION DEBUG] Found {len(completions)} completions") + for i, comp in enumerate(completions[:5]): # Show first 5 + print( + f"[COMPLETION DEBUG] {i}: {comp.text} ({comp.start}-{comp.end})" + ) + except Exception as comp_error: + print(f"[COMPLETION DEBUG] Completer error: {comp_error}") + completions = [] + + # Calculate absolute cursor positions + if completions: + first_comp = completions[0] + abs_start = current_pos + first_comp.start + abs_end = current_pos + first_comp.end + else: + abs_start = cursor_pos + abs_end = cursor_pos + + # If no completions found, try Jedi as fallback for module attributes + if not completions and current_line.strip() and "." in current_line: + try: + import jedi + + print(f"[COMPLETION DEBUG] Trying Jedi for: {current_line}") + + # Create Jedi script with current execution context + script = jedi.Script( + code=current_line, + line=1, + column=cursor_in_line, + path="", + ) + + # Set namespace to include executed modules + if hasattr(script, "_inference_state"): + # Try to add current namespace to Jedi + try: + script._inference_state.builtins_module._name_to_value.update( + shell.user_ns + ) + except: + pass + + jedi_completions = script.completions() + if jedi_completions: + completions_list = [] + for comp in jedi_completions: + # Create fake completion objects with text attribute + class FakeCompletion: + def __init__(self, text, start, end): + self.text = text + self.start = start + self.end = end + + # Calculate positions relative to current line + comp_start = ( + comp.start_pos[1] if comp.start_pos else cursor_in_line + ) + comp_end = cursor_in_line + + fake_comp = FakeCompletion(comp.name, comp_start, comp_end) + completions_list.append(fake_comp) + + completions = completions_list + abs_start = current_pos + ( + completions[0].start if completions else cursor_in_line + ) + abs_end = current_pos + ( + completions[0].end if completions else cursor_in_line + ) + + print( + f"[COMPLETION DEBUG] Jedi found {len(completions)} completions: {[c.text for c in completions[:5]]}" + ) + + except ImportError: + print("[COMPLETION DEBUG] Jedi not available") + except Exception as jedi_error: + print(f"[COMPLETION DEBUG] Jedi error: {jedi_error}") + + # If still no completions and we have module.attribute pattern, try manual completion + if not completions and current_line.strip() and "." in current_line: + # Extract module name and partial attribute + try: + line_before_cursor = current_line[:cursor_in_line] + if "." in line_before_cursor: + parts = line_before_cursor.split(".") + if len(parts) >= 2: + module_name = parts[-2].split()[-1] # Get last word before dot + partial_attr = parts[-1] if len(parts) > 1 else "" + + # Handle case where cursor is right after dot (empty partial) + if line_before_cursor.endswith("."): + partial_attr = "" + + print( + f"[COMPLETION DEBUG] Manual completion for module: {module_name}, partial: {partial_attr}" + ) + + # Try to get the module from current namespace + module_obj = None + if module_name in shell.user_ns: + module_obj = shell.user_ns[module_name] + elif module_name in __main__.__dict__: + module_obj = __main__.__dict__[module_name] + + if module_obj and hasattr(module_obj, "__dict__"): + # Get all attributes from the module + attrs = [ + attr + for attr in dir(module_obj) + if not attr.startswith("_") + and attr.startswith(partial_attr) + ] + + # For empty partial, limit to reasonable number of completions + if partial_attr == "" and len(attrs) > 20: + attrs = attrs[:20] # Show first 20 attributes + + if attrs: + print( + f"[COMPLETION DEBUG] Found manual completions: {attrs[:5]}" + ) + + # Create fake completion objects + class FakeCompletion: + def __init__(self, text): + self.text = text + self.start = ( + len(partial_attr) if partial_attr else 0 + ) + self.end = ( + len(partial_attr) if partial_attr else 0 + ) + + completions = [FakeCompletion(attr) for attr in attrs] + + # Calculate absolute positions + attr_start_pos = cursor_pos - len(partial_attr) + abs_start = attr_start_pos + abs_end = cursor_pos + + # Fallback: common module completions for well-known modules + elif not completions: + common_modules = { + "math": [ + "pi", + "e", + "sqrt", + "sin", + "cos", + "tan", + "log", + "exp", + "floor", + "ceil", + ], + "os": [ + "path", + "listdir", + "getcwd", + "chdir", + "mkdir", + "remove", + "rename", + ], + "sys": [ + "argv", + "exit", + "path", + "version", + "platform", + "stdout", + "stdin", + ], + "random": [ + "random", + "randint", + "choice", + "shuffle", + "uniform", + "seed", + ], + "time": [ + "time", + "sleep", + "strftime", + "localtime", + "gmtime", + ], + "json": ["loads", "dumps", "load", "dump"], + "datetime": [ + "datetime", + "date", + "time", + "timedelta", + "now", + "today", + ], + "numpy": [ + "array", + "zeros", + "ones", + "arange", + "linspace", + "reshape", + "sum", + "mean", + ], + "pandas": [ + "DataFrame", + "Series", + "read_csv", + "read_json", + "concat", + "merge", + ], + } + + if module_name in common_modules: + attrs = [ + attr + for attr in common_modules[module_name] + if attr.startswith(partial_attr) + ] + + print( + f"[COMPLETION DEBUG] Common module lookup for '{module_name}' with partial '{partial_attr}': found {len(attrs)} matches" + ) + + if attrs: + print( + f"[COMPLETION DEBUG] Using common module completions for {module_name}: {attrs}" + ) + + class FakeCompletion: + def __init__(self, text): + self.text = text + self.start = len(partial_attr) + self.end = len(partial_attr) + + completions = [ + FakeCompletion(attr) for attr in attrs + ] + attr_start_pos = cursor_pos - len(partial_attr) + abs_start = attr_start_pos + abs_end = cursor_pos + + except Exception as manual_error: + print(f"[COMPLETION DEBUG] Manual completion error: {manual_error}") + + # If still no completions found, try basic Python builtins as fallback + if not completions and current_line.strip(): + # Get the word being completed + word_part = ( + current_line[:cursor_in_line].split()[-1] + if current_line[:cursor_in_line].split() + else "" + ) + + # Basic Python builtins that might match + python_builtins = [ + "abs", + "all", + "any", + "ascii", + "bin", + "bool", + "bytearray", + "bytes", + "callable", + "chr", + "classmethod", + "compile", + "complex", + "delattr", + "dict", + "dir", + "divmod", + "enumerate", + "eval", + "exec", + "filter", + "float", + "format", + "frozenset", + "getattr", + "globals", + "hasattr", + "hash", + "help", + "hex", + "id", + "input", + "int", + "isinstance", + "issubclass", + "iter", + "len", + "list", + "locals", + "map", + "max", + "memoryview", + "min", + "next", + "object", + "oct", + "open", + "ord", + "pow", + "print", + "property", + "range", + "repr", + "reversed", + "round", + "set", + "setattr", + "slice", + "sorted", + "staticmethod", + "str", + "sum", + "super", + "tuple", + "type", + "vars", + "zip", + ] + + # Filter builtins that start with the current word part + if word_part: + matches = [ + builtin + for builtin in python_builtins + if builtin.startswith(word_part) + ] + if matches: + print(f"[COMPLETION DEBUG] Using fallback builtins: {matches}") + word_start = cursor_pos - len(word_part) + result = { + "matches": matches, + "cursor_start": word_start, + "cursor_end": cursor_pos, + } + else: + result = { + "matches": [], + "cursor_start": cursor_pos, + "cursor_end": cursor_pos, + } + else: + result = { + "matches": [], + "cursor_start": cursor_pos, + "cursor_end": cursor_pos, + } + else: + result = { + "matches": [c.text for c in completions], + "cursor_start": abs_start, + "cursor_end": abs_end, + } + + print(f"[COMPLETION DEBUG] Final result: {result}") + return json.dumps(result) + except Exception as e: + print(f"[COMPLETION DEBUG] Exception: {e}") + result = {"matches": [], "cursor_start": cursor_pos, "cursor_end": cursor_pos} + return json.dumps(result) diff --git a/src/components/notebook/cell/ExecutableCell.tsx b/src/components/notebook/cell/ExecutableCell.tsx index 8a4433e4..707f527e 100644 --- a/src/components/notebook/cell/ExecutableCell.tsx +++ b/src/components/notebook/cell/ExecutableCell.tsx @@ -18,7 +18,7 @@ import { useStore } from "@livestore/react"; import { focusedCellSignal$, hasManuallyFocused$ } from "../signals/focus.js"; import { events, tables, queries, CellTypeNoRaw } from "@runtimed/schema"; import { ChevronDown, ChevronUp } from "lucide-react"; -import React, { useCallback, useEffect, useRef } from "react"; +import React, { useCallback, useEffect, useRef, useMemo } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { CellContainer } from "./shared/CellContainer.js"; import { CellControls } from "./shared/CellControls.js"; @@ -42,6 +42,8 @@ import { MaybeCellOutputs } from "@/components/outputs/MaybeCellOutputs.js"; import { useToolApprovals } from "@/hooks/useToolApprovals.js"; import { AiToolApprovalOutput } from "../../outputs/shared-with-iframe/AiToolApprovalOutput.js"; import { cn } from "@/lib/utils.js"; +import { createPythonCompletionSource } from "@/components/notebook/codemirror/pythonCompletion.js"; +import type { PyodideRuntimeAgent } from "@runtimed/pyodide-runtime"; // Cell-specific styling configuration const getCellStyling = (cellType: "code" | "sql" | "ai") => { @@ -100,6 +102,38 @@ export const ExecutableCell: React.FC = ({ const { getUsersOnCell, getUserColor, getUserInfo } = useUserRegistry(); const activeRuntime = useActiveRuntime(); const detectedRuntimeType = useDetectedRuntimeType(); + + // Create Python completion source if we have a code cell and active Python runtime + const pythonCompletionSource = useMemo(() => { + console.log("🔧 ExecutableCell: Checking completion source conditions", { + cellType: cell.cellType, + runtimeType: activeRuntime?.runtimeType, + hasActiveRuntime: !!activeRuntime, + }); + + if (cell.cellType === "code" && activeRuntime?.runtimeType === "python") { + console.log("✅ ExecutableCell: Creating Python completion source"); + return createPythonCompletionSource(() => { + // Access the Pyodide runtime agent from the global launcher + if (typeof window !== "undefined" && window.__RUNT_LAUNCHER__) { + const launcher = window.__RUNT_LAUNCHER__ as any; + const agent = launcher.getCurrentPyodideAgent(); + console.log("🔍 ExecutableCell: Getting runtime agent", { + hasLauncher: !!launcher, + hasAgent: !!agent, + agentRunning: agent?.isRunning(), + }); + return agent; + } + console.log("❌ ExecutableCell: No runtime launcher available"); + return null; + }); + } + console.log( + "❌ ExecutableCell: Not creating completion source - conditions not met" + ); + return undefined; + }, [cell.cellType, activeRuntime?.runtimeType]); const { ensureRuntime, status: autoLaunchStatus } = useAutoLaunchRuntime({ runtimeType: detectedRuntimeType, }); @@ -530,7 +564,21 @@ export const ExecutableCell: React.FC = ({ enableLineWrapping={shouldEnableLineWrapping(cell.cellType)} autoFocus={autoFocus} keyMap={keyMap} + completionSource={pythonCompletionSource} /> + {/* Debug completion source status */} + {process.env.NODE_ENV === "development" && ( +
+ 🐍 Completion:{" "} + {pythonCompletionSource ? "✅ Active" : "❌ Inactive"} +
+ )} )} diff --git a/src/components/notebook/cell/shared/Editor.tsx b/src/components/notebook/cell/shared/Editor.tsx index 4f6a0022..be5f024a 100644 --- a/src/components/notebook/cell/shared/Editor.tsx +++ b/src/components/notebook/cell/shared/Editor.tsx @@ -1,5 +1,6 @@ import { cn } from "@/lib/utils"; import { KeyBinding } from "@codemirror/view"; +import { CompletionSource } from "@codemirror/autocomplete"; import { SupportedLanguage } from "@/types/misc.js"; import { Maximize2, Minimize2 } from "lucide-react"; import { @@ -36,6 +37,7 @@ interface EditorProps { enableLineWrapping?: boolean; autoFocus: boolean; keyMap: KeyBinding[]; + completionSource?: CompletionSource; } export const Editor = forwardRef( @@ -50,6 +52,7 @@ export const Editor = forwardRef( enableLineWrapping, autoFocus, keyMap, + completionSource, }, ref ) => { @@ -124,6 +127,7 @@ export const Editor = forwardRef( keyMap={keyMap} onBlur={onBlur} enableLineWrapping={enableLineWrapping} + completionSource={completionSource} />