UNPKG

@ydb-platform/monaco-ghost

Version:

Inline completion adapter for Monaco Editor

635 lines (620 loc) 20.8 kB
// node_modules/uuid/dist/esm-browser/rng.js var getRandomValues; var rnds8 = new Uint8Array(16); function rng() { if (!getRandomValues) { getRandomValues = typeof crypto !== "undefined" && crypto.getRandomValues && crypto.getRandomValues.bind(crypto); if (!getRandomValues) { throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"); } } return getRandomValues(rnds8); } // node_modules/uuid/dist/esm-browser/stringify.js var byteToHex = []; for (let i = 0; i < 256; ++i) { byteToHex.push((i + 256).toString(16).slice(1)); } function unsafeStringify(arr, offset = 0) { return byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]; } // node_modules/uuid/dist/esm-browser/native.js var randomUUID = typeof crypto !== "undefined" && crypto.randomUUID && crypto.randomUUID.bind(crypto); var native_default = { randomUUID }; // node_modules/uuid/dist/esm-browser/v4.js function v4(options, buf, offset) { if (native_default.randomUUID && !buf && !options) { return native_default.randomUUID(); } options = options || {}; const rnds = options.random || (options.rng || rng)(); rnds[6] = rnds[6] & 15 | 64; rnds[8] = rnds[8] & 63 | 128; if (buf) { offset = offset || 0; for (let i = 0; i < 16; ++i) { buf[offset + i] = rnds[i]; } return buf; } return unsafeStringify(rnds); } var v4_default = v4; // src/codeCompletion/config.ts var DEFAULT_CONFIG = { debounceTime: 200, suggestionCache: { enabled: true }, sessionId: v4_default() }; function createServiceConfig(api, userConfig) { return { ...DEFAULT_CONFIG, ...userConfig, suggestionCache: { ...DEFAULT_CONFIG.suggestionCache, ...(userConfig == null ? void 0 : userConfig.suggestionCache) || {} }, api }; } // src/codeCompletion/cache.ts import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js"; var SuggestionCacheManager = class { constructor() { this.currentGroup = null; this.activeCompletion = null; } setCompletionGroup(group) { this.currentGroup = group; } getCompletionGroup() { return this.currentGroup; } getActiveCompletion() { return this.activeCompletion; } getCachedCompletion(model, position) { const completions = []; if (this.currentGroup) { for (const completion of this.currentGroup.items) { if (!completion.range) { continue; } if (position.lineNumber < completion.range.startLineNumber || position.column < completion.range.startColumn) { continue; } const startCompletionPosition = new monaco.Position( completion.range.startLineNumber, completion.range.startColumn ); const startOffset = model.getOffsetAt(startCompletionPosition); const endOffset = startOffset + completion.insertText.toString().length; const positionOffset = model.getOffsetAt(position); if (positionOffset > endOffset) { continue; } const completionReplaceText = completion.insertText.toString().slice(0, positionOffset - startOffset); const newRange = new monaco.Range( completion.range.startLineNumber, completion.range.startColumn, position.lineNumber, position.column ); const currentReplaceText = model.getValueInRange(newRange); if (completionReplaceText.toLowerCase() === currentReplaceText.toLowerCase()) { completions.push({ insertText: currentReplaceText + completion.insertText.toString().slice(positionOffset - startOffset), range: newRange, command: completion.command, pristine: completion.pristine }); } } } return completions; } emptyCache() { this.currentGroup = null; this.activeCompletion = null; } incrementShownCount(pristineText) { if (this.currentGroup) { for (const completion of this.currentGroup.items) { if (completion.pristine === pristineText) { this.currentGroup.shownCount++; this.activeCompletion = pristineText; break; } } } } markAsAccepted(pristineText) { var _a; if ((_a = this.currentGroup) == null ? void 0 : _a.items.some((item) => item.pristine === pristineText)) { this.currentGroup.wasAccepted = true; } } }; // src/codeCompletion/suggestionProvider.ts import * as monaco2 from "monaco-editor/esm/vs/editor/editor.api.js"; // src/codeCompletion/prompt.ts function getPromptFileContent(model, position, sessionId = v4_default()) { const linesContent = model.getLinesContent(); const currentLine = linesContent[position.lineNumber - 1]; if (!currentLine) { return void 0; } const prevTextInCurrentLine = currentLine.slice(0, position.column - 1); const postTextInCurrentLine = currentLine.slice(position.column - 1); const prevText = linesContent.slice(0, position.lineNumber - 1).concat([prevTextInCurrentLine]).join("\n"); const postText = [postTextInCurrentLine].concat(linesContent.slice(position.lineNumber)).join("\n"); const cursorPosition = { lineNumber: position.lineNumber, column: position.column }; const fragments = []; if (prevText) { fragments.push({ text: prevText, start: { lineNumber: 1, column: 1 }, end: cursorPosition }); } if (postText) { const lastLine = linesContent[linesContent.length - 1]; if (!lastLine) { return void 0; } fragments.push({ text: postText, start: cursorPosition, end: { lineNumber: linesContent.length, column: lastLine.length } }); } return fragments.length ? [ { fragments, cursorPosition, path: `${sessionId}` } ] : void 0; } // src/codeCompletion/suggestionProvider.ts var CodeSuggestionProvider = class { constructor(config, events) { this.timer = null; this.pendingPromise = null; this.pendingResolve = null; this.config = config; this.events = events; } async getSuggestions(model, position) { if (this.timer) { window.clearTimeout(this.timer); } if (!this.pendingPromise) { this.pendingPromise = new Promise((resolve) => { this.pendingResolve = resolve; }); } const currentPromise = this.pendingPromise; this.timer = window.setTimeout(async () => { var _a, _b, _c; try { let suggestions = []; let requestId = ""; const data = getPromptFileContent(model, position, this.config.sessionId); if (!data) { (_a = this.pendingResolve) == null ? void 0 : _a.call(this, { suggestions: [], requestId: "" }); return; } const codeAssistSuggestions = await this.config.api.getCodeAssistSuggestions(data); requestId = (codeAssistSuggestions == null ? void 0 : codeAssistSuggestions.requestId) || ""; const { word, startColumn: lastWordStartColumn } = model.getWordUntilPosition(position); suggestions = ((codeAssistSuggestions == null ? void 0 : codeAssistSuggestions.items) || []).map((text) => { const suggestionText = text; const label = word + suggestionText; return { label, sortText: "a", insertText: label, pristine: suggestionText, range: new monaco2.Range( position.lineNumber, lastWordStartColumn, position.lineNumber, position.column ), command: { id: "acceptCodeAssistCompletion", title: "", arguments: [ { requestId, suggestionText, prevWordLength: word.length } ] } }; }); (_b = this.pendingResolve) == null ? void 0 : _b.call(this, { suggestions, requestId }); } catch (err) { this.events.emit("completion:error", err instanceof Error ? err : new Error(String(err))); (_c = this.pendingResolve) == null ? void 0 : _c.call(this, { suggestions: [], requestId: "" }); } finally { this.pendingPromise = null; this.pendingResolve = null; this.timer = null; } }, this.config.debounceTime); return currentPromise; } }; // src/events.ts var GhostEventEmitter = class { constructor() { this.events = /* @__PURE__ */ new Map(); } on(event, callback) { var _a; if (!this.events.has(event)) { this.events.set(event, /* @__PURE__ */ new Set()); } (_a = this.events.get(event)) == null ? void 0 : _a.add(callback); } off(event, callback) { var _a; (_a = this.events.get(event)) == null ? void 0 : _a.delete(callback); } emit(event, data) { var _a; (_a = this.events.get(event)) == null ? void 0 : _a.forEach((callback) => { try { callback(data); } catch (error) { console.error(`Error in event listener for ${event}:`, error); } }); } }; // src/codeCompletion/index.ts var CodeCompletionService = class { constructor(api, userConfig) { this.events = new GhostEventEmitter(); this.config = createServiceConfig(api, userConfig); this.cacheManager = new SuggestionCacheManager(); this.suggestionProvider = new CodeSuggestionProvider(this.config, this.events); } async provideInlineCompletions(model, position, _context, _token) { if (this.config.suggestionCache.enabled) { const cachedCompletions = this.cacheManager.getCachedCompletion(model, position); if (cachedCompletions.length) { return { items: cachedCompletions }; } } const prevGroup = this.cacheManager.getCompletionGroup(); if (prevGroup && !prevGroup.wasAccepted) { this.dismissCompletion(prevGroup); } this.cacheManager.emptyCache(); const { suggestions, requestId } = await this.suggestionProvider.getSuggestions( model, position ); const completionGroup = { items: suggestions, shownCount: 0, requestId }; this.cacheManager.setCompletionGroup(completionGroup); return { items: suggestions }; } handleItemDidShow(_completions, item) { if (!this.config.suggestionCache.enabled) { return; } this.cacheManager.incrementShownCount(item.pristine); } handlePartialAccept(_completions, item, acceptedLetters) { var _a, _b; const { command } = item; const commandArguments = (_b = (_a = command == null ? void 0 : command.arguments) == null ? void 0 : _a[0]) != null ? _b : {}; const { suggestionText, requestId, prevWordLength = 0 } = commandArguments; if (requestId && suggestionText && typeof item.insertText === "string") { const acceptedText = item.insertText.slice(prevWordLength, acceptedLetters); if (acceptedText) { this.cacheManager.markAsAccepted(suggestionText); this.events.emit("completion:accept", { requestId, acceptedText }); } } } handleAccept({ requestId, suggestionText }) { this.cacheManager.emptyCache(); this.events.emit("completion:accept", { requestId, acceptedText: suggestionText }); } commandDiscard(reason = "OnCancel", editor2) { var _a, _b; const group = this.cacheManager.getCompletionGroup(); if ((group == null ? void 0 : group.requestId) && ((_a = group.items) == null ? void 0 : _a.length)) { const allSuggestions = group.items.map((item) => item.pristine); const activeSuggestion = this.cacheManager.getActiveCompletion() || ((_b = group.items[0]) == null ? void 0 : _b.pristine) || ""; this.events.emit("completion:decline", { requestId: group.requestId, suggestionText: activeSuggestion, reason, hitCount: group.shownCount, allSuggestions }); this.cacheManager.emptyCache(); } editor2.trigger(void 0, "editor.action.inlineSuggest.hide", void 0); } emptyCache() { this.cacheManager.emptyCache(); } hasActiveSuggestions() { return this.cacheManager.getCompletionGroup() !== null; } freeInlineCompletions() { } dismissCompletion(group) { var _a; if (!group.requestId || !((_a = group.items) == null ? void 0 : _a.length) || !group.shownCount || group.wasAccepted) { return; } const [firstItem] = group.items; if (!firstItem) { return; } const allSuggestions = group.items.map((item) => item.pristine); const activeSuggestion = this.cacheManager.getActiveCompletion() || firstItem.pristine || ""; this.events.emit("completion:ignore", { requestId: group.requestId, suggestionText: activeSuggestion, allSuggestions }); } }; // src/factory.ts import * as monaco3 from "monaco-editor"; function createCodeCompletionService(api, userConfig = {}) { return new CodeCompletionService(api, userConfig); } var MonacoGhostInstance = class { constructor(editor2) { this.editor = editor2; this.disposables = []; this.completionProvider = null; } register({ api, config, eventHandlers }) { this.completionProvider = createCodeCompletionService(api, config); const disposable = monaco3.languages.registerInlineCompletionsProvider( [config.language], this.completionProvider ); this.disposables.push(disposable); const commandDisposables = registerCompletionCommands( monaco3, this.completionProvider, this.editor ); this.disposables.push(...commandDisposables); if (eventHandlers) { const provider = this.completionProvider; if (eventHandlers.onCompletionAccept) { provider.events.on("completion:accept", eventHandlers.onCompletionAccept); } if (eventHandlers.onCompletionDecline) { provider.events.on("completion:decline", eventHandlers.onCompletionDecline); } if (eventHandlers.onCompletionIgnore) { provider.events.on("completion:ignore", eventHandlers.onCompletionIgnore); } if (eventHandlers.onCompletionError) { provider.events.on("completion:error", eventHandlers.onCompletionError); } } } dispose() { this.disposables.forEach((d) => d.dispose()); this.disposables = []; this.completionProvider = null; } }; function createMonacoGhostInstance(editor2) { const instance = new MonacoGhostInstance(editor2); return { register: (props) => instance.register(props), unregister: () => instance.dispose() }; } // src/registerCommands.ts function registerCompletionCommands(monacoInstance, completionService, editor2) { const disposables = []; const acceptCommand = monacoInstance.editor.registerCommand( "acceptCodeAssistCompletion", (_accessor, ...args) => { var _a; const data = (_a = args[0]) != null ? _a : {}; if (!data || typeof data !== "object") { return; } const { requestId, suggestionText } = data; if (requestId && suggestionText) { completionService.handleAccept({ requestId, suggestionText }); } } ); disposables.push(acceptCommand); const declineCommand = monacoInstance.editor.registerCommand( "declineCodeAssistCompletion", () => { completionService.commandDiscard("OnCancel", editor2); } ); disposables.push(declineCommand); const keyDownDisposable = editor2.onKeyDown((e) => { var _a, _b, _c, _d; if (e.keyCode === monacoInstance.KeyCode.Escape) { const suggestController = editor2.getContribution("editor.contrib.suggestController"); const hasRegularSuggestions = (_d = (_c = (_b = (_a = suggestController == null ? void 0 : suggestController.widget) == null ? void 0 : _a.value) == null ? void 0 : _b.selectFirst) == null ? void 0 : _c.call(_b)) != null ? _d : false; const hasInlineSuggestion = completionService.hasActiveSuggestions(); if (hasInlineSuggestion && !hasRegularSuggestions) { e.preventDefault(); editor2.trigger("keyboard", "declineCodeAssistCompletion", null); } } }); disposables.push(keyDownDisposable); return disposables; } // src/hooks/useMonacoGhost.ts import { useEffect, useRef, useCallback } from "react"; import * as monaco4 from "monaco-editor"; function useMonacoGhost({ api, config, eventHandlers }) { const disposables = useRef([]); const editorRef = useRef(null); const completionProviderRef = useRef(null); const dispose = useCallback(() => { disposables.current.forEach((d) => d.dispose()); disposables.current = []; }, []); const { onCompletionAccept, onCompletionDecline, onCompletionIgnore, onCompletionError } = eventHandlers || {}; const register = useCallback( (editor2) => { editorRef.current = editor2; completionProviderRef.current = createCodeCompletionService(api, config); const disposable = monaco4.languages.registerInlineCompletionsProvider( [config.language], completionProviderRef.current ); disposables.current.push(disposable); const commandDisposables = registerCompletionCommands( monaco4, completionProviderRef.current, editor2 ); disposables.current.push(...commandDisposables); const provider = completionProviderRef.current; provider.events.on("completion:accept", (event) => { onCompletionAccept == null ? void 0 : onCompletionAccept(event); }); provider.events.on("completion:decline", (event) => { onCompletionDecline == null ? void 0 : onCompletionDecline(event); }); provider.events.on("completion:ignore", (event) => { onCompletionIgnore == null ? void 0 : onCompletionIgnore(event); }); provider.events.on("completion:error", (error) => { onCompletionError == null ? void 0 : onCompletionError(error); }); }, [api, config, onCompletionAccept, onCompletionDecline, onCompletionIgnore, onCompletionError] ); useEffect(() => { return () => { dispose(); }; }, [dispose]); return { register, unregister: dispose }; } // src/components/MonacoEditor.tsx import React, { useEffect as useEffect2, useRef as useRef2 } from "react"; import * as monaco5 from "monaco-editor"; import "monaco-editor/min/vs/editor/editor.main.css"; var MonacoEditor = ({ initialValue = "", language = "sql", theme = "vs-dark", api, config, eventHandlers, editorOptions = {}, className, style }) => { const editorRef = useRef2(null); const editor2 = useRef2(null); const { register } = useMonacoGhost({ api, config: { ...config, language }, eventHandlers }); useEffect2(() => { if (!editorRef.current) return; const defaultOptions = { value: initialValue, language, theme, minimap: { enabled: false }, automaticLayout: true, fontSize: 14, lineNumbers: "on", scrollBeyondLastLine: false, roundedSelection: false, padding: { top: 10 }, suggestOnTriggerCharacters: true, quickSuggestions: true, parameterHints: { enabled: true }, codeLens: false, renderLineHighlight: "all", matchBrackets: "always", selectionHighlight: true, occurrencesHighlight: "singleFile", links: true, cursorBlinking: "smooth", cursorSmoothCaretAnimation: "on", smoothScrolling: true }; editor2.current = monaco5.editor.create(editorRef.current, { ...defaultOptions, ...editorOptions }); register(editor2.current); }, [language, initialValue]); useEffect2(() => { if (editor2.current) { editor2.current.updateOptions({ theme, ...editorOptions }); } }, [theme, editorOptions]); const defaultStyle = { width: "100%", height: "600px", border: "1px solid #ccc", overflow: "hidden" }; return /* @__PURE__ */ React.createElement("div", { ref: editorRef, className, style: { ...defaultStyle, ...style } }); }; export { MonacoEditor, MonacoGhostInstance, createCodeCompletionService, createMonacoGhostInstance, registerCompletionCommands, useMonacoGhost };