UNPKG

@ydb-platform/monaco-ghost

Version:

Inline completion adapter for Monaco Editor

538 lines (423 loc) 13.2 kB
# @ydb-platform/monaco-ghost <div align="center"> [![CI](https://github.com/ydb-platform/monaco-ghost/actions/workflows/ci.yml/badge.svg)](https://github.com/ydb-platform/monaco-ghost/actions/workflows/ci.yml) [![npm version](https://badge.fury.io/js/%40ydb-platform%2Fmonaco-ghost.svg)](https://www.npmjs.com/package/@ydb-platform/monaco-ghost) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 🚀 A lightweight adapter for integrating completion services with Monaco Editor's inline completion system. [Installation](#-installation) [Quick Start](#-quick-start) [Documentation](#-documentation) [Contributing](#-contributing) </div> --- ## ✨ Features at a Glance - 👻 **Ghost Text Display** - Inline suggestions with keyboard navigation - **High Performance** - Debouncing, caching, and optimized for large files - 🎯 **Type Safety** - Comprehensive TypeScript support - 🎨 **Theme Support** - Dark and light themes with customization options - 📊 **Event System** - Rich analytics and error tracking - 🧩 **React Integration** - Pre-built components and hooks ## 📦 Installation ```bash npm install @ydb-platform/monaco-ghost monaco-editor ``` ## 🚀 Quick Start The library provides multiple ways to integrate with React applications. Here are the main approaches: <details> <summary>React Integration with Hook - Recommended for most use cases</summary> ```typescript import React, { useCallback } from 'react'; import MonacoEditor from 'react-monaco-editor'; import * as monaco from 'monaco-editor'; import { useMonacoGhost } from '@ydb-platform/monaco-ghost'; function MyCustomEditor() { // Java-specific API implementation const javaApi = { getCodeAssistSuggestions: async () => ({ Suggests: [{ Text: 'System.out.println("Hello, World!");' }], RequestId: 'demo-request', }), }; // Java-specific configuration const javaConfig = { debounceTime: 200, suggestionCache: { enabled: true, }, }; const eventHandlers = { onCompletionAccept: text => console.log('Accepted:', text), onCompletionDecline: (text, reason, otherSuggestions) => console.log('Declined:', text, reason, otherSuggestions), onCompletionIgnore: (text, otherSuggestions) => console.log('Ignored:', text, otherSuggestions), onCompletionError: error => console.error('Error:', error), }; const { register } = useMonacoGhost({ api: javaApi, eventHandlers, config: javaConfig, }); const editorDidMount = useCallback( (editor: monaco.editor.IStandaloneCodeEditor) => { register(editor); }, [register] ); const options = { selectOnLineNumbers: true, minimap: { enabled: false }, automaticLayout: true, fontSize: 14, lineNumbers: 'on', scrollBeyondLastLine: false, roundedSelection: false, padding: { top: 10 }, }; return ( <MonacoEditor width="800" height="600" language="java" theme="vs-dark" // or "vs-light" value="// Your Java code here" options={options} editorDidMount={editorDidMount} /> ); } ``` </details> <details> <summary>Alternative: Direct Instance Creation - For advanced use cases requiring fine-grained control</summary> For more control over the ghost instance lifecycle, you can use `createMonacoGhostInstance` instead of the hook: ```typescript import React from 'react'; import MonacoEditor from 'react-monaco-editor'; import * as monaco from 'monaco-editor'; import { createMonacoGhostInstance } from '@ydb-platform/monaco-ghost'; function MyCustomEditor() { const [monacoGhostInstance, setMonacoGhostInstance] = React.useState<ReturnType<typeof createMonacoGhostInstance>>(); // Configuration and helpers const monacoGhostConfig = { api: { getCodeAssistSuggestions: async () => ({ Suggests: [{ Text: 'System.out.println("Hello, World!");' }], RequestId: 'demo-request', }), }, eventHandlers: { onCompletionAccept: text => console.log('Accepted:', text), onCompletionDecline: (text, reason, otherSuggestions) => console.log('Declined:', text, reason, otherSuggestions), onCompletionIgnore: (text, otherSuggestions) => console.log('Ignored:', text, otherSuggestions), onCompletionError: error => console.error('Error:', error), }, config: { language: 'java', debounceTime: 200, suggestionCache: { enabled: true, }, }, }; // Initialize ghost instance when enabled React.useEffect(() => { if (monacoGhostInstance && isCodeAssistEnabled) { monacoGhostInstance.register(monacoGhostConfig); } return () => { monacoGhostInstance?.unregister(); }; }, [isCodeAssistEnabled, monacoGhostConfig, monacoGhostInstance]); const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor) => { setMonacoGhostInstance(createMonacoGhostInstance(editor)); }; return ( <MonacoEditor width="800" height="600" language="java" theme="vs-dark" value="// Your Java code here" options={{ selectOnLineNumbers: true, minimap: { enabled: false }, automaticLayout: true, }} editorDidMount={editorDidMount} /> ); } ``` This approach gives you more control over when the ghost instance is created and destroyed, and allows for more complex initialization logic if needed. </details> <details> <summary>Using the pre-built editor component - Simplest way to get started</summary> ```typescript // Using the pre-built editor component import { MonacoEditor } from '@ydb-platform/monaco-ghost'; function MyApp() { // SQL-specific API implementation const sqlApi = { getCodeAssistSuggestions: async () => ({ Suggests: [{ Text: 'SELECT * FROM users;' }], RequestId: 'demo-request', }), }; // SQL-specific configuration const sqlConfig = { debounceTime: 200, suggestionCache: { enabled: true, }, }; return ( <MonacoEditor initialValue="-- Your SQL code here" language="sql" theme="vs-dark" // or "vs-light" api={sqlApi} config={sqlConfig} onCompletionAccept={text => console.log('Accepted:', text)} onCompletionDecline={(text, reason, otherSuggestions) => console.log('Declined:', text, reason, otherSuggestions) } onCompletionIgnore={(text, otherSuggestions) => console.log('Ignored:', text, otherSuggestions) } onCompletionError={error => console.error('Error:', error)} editorOptions={{ minimap: { enabled: false }, fontSize: 14, }} /> ); } ``` </details> ### Vanilla JavaScript <details> <summary>View Vanilla JavaScript implementation</summary> ```typescript import * as monaco from 'monaco-editor'; import { createCodeCompletionService, registerCompletionCommands, } from '@ydb-platform/monaco-ghost'; // Create language-specific API implementation const sqlApi = { getCodeAssistSuggestions: async data => { // Call your completion service // Return suggestions in the expected format return { Suggests: [{ Text: 'SELECT * FROM users;' }], RequestId: 'request-id', }; }, }; // Configure the adapter with language-specific settings const sqlConfig = { debounceTime: 200, suggestionCache: { enabled: true, }, }; // Create provider for SQL const sqlCompletionProvider = createCodeCompletionService(sqlApi, sqlConfig); // Subscribe to completion events with type safety sqlCompletionProvider.events.on('completion:accept', data => { console.log('Completion accepted:', data.acceptedText); }); sqlCompletionProvider.events.on('completion:decline', data => { console.log( 'Completion declined:', data.suggestionText, 'reason:', data.reason, 'other suggestions:', data.otherSuggestions ); }); sqlCompletionProvider.events.on('completion:ignore', data => { console.log( 'Completion ignored:', data.suggestionText, 'other suggestions:', data.otherSuggestions ); }); sqlCompletionProvider.events.on('completion:error', error => { console.error('Completion error:', error); }); // Register with Monaco for SQL monaco.languages.registerInlineCompletionsProvider(['sql'], sqlCompletionProvider); // Register commands (assuming you have an editor instance) registerCompletionCommands(monaco, sqlCompletionProvider, editor); ``` </details> ## 📚 Documentation ### 🎮 Keyboard Shortcuts | Key | Action | | -------- | ---------------------------- | | `Tab` | Accept current suggestion | | `Escape` | Decline current suggestion | | `Alt+]` | Cycle to next suggestion | | `Alt+[` | Cycle to previous suggestion | ### ⚙️ Configuration ```typescript interface CodeCompletionConfig { // Required when using hooks language?: string; // The language this configuration applies to (e.g., 'sql', 'java') // Performance settings debounceTime?: number; // Time in ms to debounce API calls (default: 200) // Cache settings suggestionCache?: { enabled?: boolean; // Whether to enable suggestion caching (default: true) }; } ``` ### 🔌 API Interface <details> <summary>View API Interface details</summary> ```typescript interface ICodeCompletionAPI { getCodeAssistSuggestions(data: PromptFile[]): Promise<Suggestions>; } export interface PromptPosition { lineNumber: number; column: number; } export interface PromptFragment { text: string; start: PromptPosition; end: PromptPosition; } export interface PromptFile { path: string; fragments: PromptFragment[]; cursorPosition: PromptPosition; } export interface Suggestions { items: string[]; requestId?: string; } ``` </details> ### 📊 Events <details> <summary>View Events documentation</summary> The completion service emits four types of events with rich data: #### 1. Acceptance Events ```typescript interface CompletionAcceptEvent { requestId: string; acceptedText: string; } completionProvider.events.on('completion:accept', (data: CompletionAcceptEvent) => { console.log('Accepted:', data.acceptedText); }); ``` #### 2. Decline Events ```typescript interface CompletionDeclineEvent { requestId: string; suggestionText: string; reason: string; hitCount: number; otherSuggestions: string[]; } completionProvider.events.on('completion:decline', (data: CompletionDeclineEvent) => { console.log('Declined:', data.suggestionText, 'reason:', data.reason); console.log('Other suggestions:', data.otherSuggestions); console.log('Times shown:', data.hitCount); }); ``` #### 3. Ignore Events ```typescript interface CompletionIgnoreEvent { requestId: string; suggestionText: string; otherSuggestions: string[]; } completionProvider.events.on('completion:ignore', (data: CompletionIgnoreEvent) => { console.log('Ignored:', data.suggestionText); console.log('Other suggestions:', data.otherSuggestions); }); ``` #### 4. Error Events ```typescript completionProvider.events.on('completion:error', (error: Error) => { console.error('Completion error:', error); }); ``` </details> ## 🛠️ Development ### Setup ```bash # Install dependencies npm install # Start Storybook for development npm run storybook # Run tests npm run test # Run tests with coverage npm run test:coverage ``` ### Build System The package uses a hybrid build system: - **TypeScript (tsc)** for type checking and declaration files - **esbuild** for fast, optimized builds Output Formats: - **CommonJS**: `dist/cjs/index.js` - **ES Modules**: `dist/esm/index.js` - **TypeScript Declarations**: `dist/types/index.d.ts` <details> <summary>View Build Commands</summary> ```bash # Type checking only npm run type-check # Build type declarations npm run build:types # Build CommonJS version npm run build:cjs # Build ES Modules version npm run build:esm # Full build (all formats) npm run build ``` </details> ## 👥 Contributing 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request <details> <summary>View Development Guidelines</summary> ### Development Guidelines #### 1. Code Context - Handle text limits appropriately - Maintain cursor position accuracy - Consider edge cases - Support partial text acceptance #### 2. Error Handling - Wrap API calls in try-catch blocks - Fail gracefully on errors - Log issues without breaking editor - Emit error events for monitoring #### 3. Performance - Use debouncing for API calls - Implement efficient caching - Track suggestion hit counts - Clean up resources properly #### 4. Testing - Add tests for new features - Maintain backward compatibility - Test edge cases - Verify event handling </details> ## 📄 License Apache-2.0 - see [LICENSE](LICENSE) file for details.