nginx-ui/app/src/components/CodeEditor/CodeCompletion.ts
2025-04-16 07:50:23 +00:00

224 lines
6.2 KiB
TypeScript

import type { Editor } from 'ace-builds'
import type { Point } from 'ace-builds-internal/document'
import openai from '@/api/openai'
import { debounce } from 'lodash'
import { v4 as uuidv4 } from 'uuid'
// eslint-disable-next-line ts/no-explicit-any
function debug(...args: any[]) {
if (import.meta.env.DEV) {
// eslint-disable-next-line no-console
console.debug(`[CodeEditor]`, ...args)
}
}
function useCodeCompletion() {
const editorRef = ref<Editor>()
const currentGhostText = ref<string>('')
const ws = openai.code_completion()
function getAISuggestions(code: string, context: string, position: Point, callback: (suggestion: string) => void, language: string = 'nginx', suffix: string = '', requestId: string) {
if (!ws || ws.readyState !== WebSocket.OPEN) {
debug('WebSocket is not open')
return
}
if (!code.trim()) {
debug('Code is empty')
return
}
const message = {
context,
code,
suffix,
language,
position,
request_id: requestId,
}
debug('Sending message', message)
ws.send(JSON.stringify(message))
ws.onmessage = event => {
const data = JSON.parse(event.data)
debug(`Received message`, data, requestId)
if (data.request_id === requestId) {
callback(data.code)
}
}
}
function applyGhostText() {
if (!editorRef.value) {
debug('Editor instance not available yet')
return
}
try {
const currentText = editorRef.value.getValue()
const cursorPosition = editorRef.value.getCursorPosition()
// Get all text before the current cursor position as the code part for the request
const allLines = currentText.split('\n')
const currentLine = allLines[cursorPosition.row]
const textUpToCursor = allLines.slice(0, cursorPosition.row).join('\n')
+ (cursorPosition.row > 0 ? '\n' : '')
+ currentLine.substring(0, cursorPosition.column)
// Get text after cursor position as suffix
const textAfterCursor = currentLine.substring(cursorPosition.column)
+ (cursorPosition.row < allLines.length - 1 ? '\n' : '')
+ allLines.slice(cursorPosition.row + 1).join('\n')
// Generate new request ID
const requestId = uuidv4()
// Clear existing ghost text before making the request
clearGhostText()
// Get AI suggestions
getAISuggestions(
textUpToCursor,
currentText,
cursorPosition,
suggestion => {
debug(`AI suggestions applied: ${suggestion}`)
// If there's a suggestion, set ghost text
if (suggestion && typeof editorRef.value!.setGhostText === 'function') {
clearGhostText()
// Get current cursor position (may have changed during async process)
const newPosition = editorRef.value!.getCursorPosition()
editorRef.value!.setGhostText(suggestion, {
column: newPosition.column,
row: newPosition.row,
})
debug(`Ghost text set: ${suggestion}`)
currentGhostText.value = suggestion
}
else if (suggestion) {
debug('setGhostText method not available on editor instance')
}
},
editorRef.value.session.getMode()?.path?.split('/').pop() || 'text',
textAfterCursor, // Pass text after cursor as suffix
requestId, // Pass request ID
)
}
catch (error) {
debug(`Error in applyGhostText: ${error}`)
}
}
// Accept the ghost text suggestion with Tab key
function setupTabHandler(editor: Editor) {
if (!editor) {
debug('Editor not available in setupTabHandler')
return
}
debug('Setting up Tab key handler')
// Remove existing command to avoid conflicts
const existingCommand = editor.commands.byName.acceptGhostText
if (existingCommand) {
editor.commands.removeCommand(existingCommand)
}
// Register new Tab key handler command with highest priority
editor.commands.addCommand({
name: 'acceptGhostText',
bindKey: { win: 'Tab', mac: 'Tab' },
exec: (editor: Editor) => {
// Use our saved ghost text, not dependent on editor.ghostText
if (currentGhostText.value) {
debug(`Accepting ghost text: ${currentGhostText.value}`)
const position = editor.getCursorPosition()
const text = currentGhostText.value
// Insert text through session API
editor.session.insert(position, text)
clearGhostText()
debug('Ghost text inserted successfully')
return true // Prevent event propagation
}
debug('No ghost text to accept, allowing default tab behavior')
return false // Allow default Tab behavior
},
readOnly: false,
})
debug('Tab key handler set up successfully')
}
// Clear ghost text and reset state
function clearGhostText() {
if (!editorRef.value)
return
if (typeof editorRef.value.removeGhostText === 'function') {
editorRef.value.removeGhostText()
}
currentGhostText.value = ''
}
const debouncedApplyGhostText = debounce(applyGhostText, 1000, { leading: false, trailing: true })
debug('Editor initialized')
async function init(editor: Editor) {
const { enabled } = await openai.get_code_completion_enabled_status()
if (!enabled) {
debug('Code completion is not enabled')
return
}
editorRef.value = editor
// Set up Tab key handler
setupTabHandler(editor)
setTimeout(() => {
editor.on('change', (e: { action: string }) => {
debug(`Editor change event: ${e.action}`)
// If change is caused by user input, interrupt current completion
clearGhostText()
if (e.action === 'insert' || e.action === 'remove') {
// Clear current ghost text
debouncedApplyGhostText()
}
})
// Listen for cursor changes, using debounce
editor.selection.on('changeCursor', () => {
debug('Cursor changed')
clearGhostText()
debouncedApplyGhostText()
})
}, 2000)
}
function cleanUp() {
if (ws) {
ws.close()
}
debug('CodeCompletion unmounted')
}
return {
init,
cleanUp,
}
}
export default useCodeCompletion