From e8ef4b824ad85cc3ac38487d2a65a6127d9b40c7 Mon Sep 17 00:00:00 2001 From: Syngnat <92659908+Syngnat@users.noreply.github.com> Date: Tue, 7 Jul 2026 15:10:45 +0800 Subject: [PATCH] fix(query-editor): prevent AI inline completion from trapping cursor --- frontend/src/components/MonacoEditor.tsx | 50 +++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/MonacoEditor.tsx b/frontend/src/components/MonacoEditor.tsx index 51a7b6c2..ce17b244 100644 --- a/frontend/src/components/MonacoEditor.tsx +++ b/frontend/src/components/MonacoEditor.tsx @@ -1,5 +1,5 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import Editor, { loader, type BeforeMount, type EditorProps } from '@monaco-editor/react'; +import Editor, { loader, type BeforeMount, type EditorProps, type OnMount } from '@monaco-editor/react'; import { useStore } from '../store'; import { sanitizeDataTableFontSize } from '../utils/dataGridDisplay'; import { DEFAULT_MONO_FONT_FAMILY } from '../utils/fontFamilies'; @@ -10,6 +10,7 @@ export type GonaviMonacoTypography = 'code' | 'data'; const DEFAULT_FONT_SIZE = 14; const MIN_FONT_SIZE = 12; const MAX_FONT_SIZE = 20; +const QUERY_EDITOR_AI_INLINE_CONTEXT_KEY = 'gonaviAiInlineSuggestionVisible'; let monacoConfiguredPromise: Promise | null = null; let transparentThemesRegistered = false; @@ -18,6 +19,46 @@ const isTestRuntime = (): boolean => { return env.MODE === 'test' || env.VITEST === true || env.VITEST === 'true'; }; +const sameEditorPosition = (left: any, right: any): boolean => ( + Number(left?.lineNumber) === Number(right?.lineNumber) + && Number(left?.column) === Number(right?.column) +); + +const patchQueryEditorAiInlineRightArrowFallback = (editor: any, monaco: any) => { + const originalAddCommand = editor?.addCommand?.bind?.(editor); + if (!originalAddCommand || !monaco?.KeyCode?.RightArrow) { + return; + } + if (editor.__gonaviAiInlineRightArrowFallbackPatched) { + return; + } + Object.defineProperty(editor, '__gonaviAiInlineRightArrowFallbackPatched', { + value: true, + configurable: true, + }); + + editor.addCommand = (keybinding: any, handler: any, context: any) => { + if ( + keybinding === monaco.KeyCode.RightArrow + && context === QUERY_EDITOR_AI_INLINE_CONTEXT_KEY + && typeof handler === 'function' + ) { + return originalAddCommand(keybinding, (...args: any[]) => { + const beforePosition = editor.getPosition?.(); + const beforeValue = String(editor.getValue?.() ?? ''); + const result = handler(...args); + const afterPosition = editor.getPosition?.(); + const afterValue = String(editor.getValue?.() ?? ''); + if (beforeValue === afterValue && sameEditorPosition(beforePosition, afterPosition)) { + editor.trigger?.('gonavi-ai-inline-fallback', 'cursorRight', null); + } + return result; + }, context); + } + return originalAddCommand(keybinding, handler, context); + }; +}; + export const registerGonaviMonacoThemes: BeforeMount = (monaco) => { if (transparentThemesRegistered) { return; @@ -75,6 +116,7 @@ const MonacoEditor: React.FC = ({ beforeMount, gonaviTypography = 'code', loading, + onMount, options, ...props }) => { @@ -111,6 +153,11 @@ const MonacoEditor: React.FC = ({ beforeMount?.(monaco); }, [beforeMount]); + const handleMount: OnMount = useCallback((editor, monaco) => { + patchQueryEditorAiInlineRightArrowFallback(editor, monaco); + onMount?.(editor, monaco); + }, [onMount]); + const resolvedOptions = useMemo(() => { if (uiVersion !== 'v2') { return { @@ -168,6 +215,7 @@ const MonacoEditor: React.FC = ({ options={resolvedOptions} loading={loading} beforeMount={handleBeforeMount} + onMount={handleMount} /> ); };