@ydb-platform/monaco-ghost
Version:
Inline completion adapter for Monaco Editor
538 lines (423 loc) • 13.2 kB
Markdown
# @ydb-platform/monaco-ghost
<div align="center">
[](https://github.com/ydb-platform/monaco-ghost/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/@ydb-platform/monaco-ghost)
[](LICENSE)
[](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.