@ydb-platform/monaco-ghost
Version:
Inline completion adapter for Monaco Editor
635 lines (620 loc) • 20.8 kB
JavaScript
// 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
};