mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-08-13 18:14:24 +08:00
Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
@@ -1073,7 +1073,7 @@ describe('DataGrid DDL interactions', () => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('selects one row when its row number cell is clicked', async () => {
|
||||
it('toggles one row when its row number cell is clicked', async () => {
|
||||
storeState.appearance.uiVersion = 'v2';
|
||||
const rows = [
|
||||
{ [GONAVI_ROW_KEY]: 'row-1', id: 1 },
|
||||
@@ -1107,6 +1107,14 @@ describe('DataGrid DDL interactions', () => {
|
||||
expect(stopPropagation).toHaveBeenCalledTimes(1);
|
||||
expect(testRenderState.latestTableProps.rowHoverable).toBe(false);
|
||||
expect(testRenderState.latestTableProps.rowSelection.selectedRowKeys).toEqual(['row-2']);
|
||||
|
||||
await act(async () => {
|
||||
rowNumberColumn.onCell(rows[1], 1).onClick({ stopPropagation });
|
||||
});
|
||||
await waitForEffects();
|
||||
|
||||
expect(stopPropagation).toHaveBeenCalledTimes(2);
|
||||
expect(testRenderState.latestTableProps.rowSelection.selectedRowKeys).toEqual([]);
|
||||
renderer!.unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -3381,7 +3381,9 @@ const DataGrid: React.FC<DataGridProps> = ({
|
||||
const handleRowNumberClick = useCallback((record: Item) => {
|
||||
const key = record?.[GONAVI_ROW_KEY];
|
||||
if (key === undefined || key === null) return;
|
||||
setSelectedRowKeys([key]);
|
||||
setSelectedRowKeys((previousKeys) => (
|
||||
previousKeys.length === 1 && previousKeys[0] === key ? [] : [key]
|
||||
));
|
||||
}, []);
|
||||
|
||||
const handleRowNumberDoubleClick = useCallback((index: number) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Editor, { loader, type BeforeMount, type EditorProps, type OnMount } from '@monaco-editor/react';
|
||||
import { message } from 'antd';
|
||||
import { t } from '../i18n';
|
||||
import { useStore } from '../store';
|
||||
import { sanitizeDataTableFontSize } from '../utils/dataGridDisplay';
|
||||
import { DEFAULT_MONO_FONT_FAMILY } from '../utils/fontFamilies';
|
||||
@@ -7,7 +9,11 @@ import {
|
||||
resolveSqlEditorFontSize,
|
||||
resolveSqlEditorSuggestionLayout,
|
||||
} from '../utils/sqlEditorTypography';
|
||||
import { installWailsMonacoClipboardPasteHandler } from '../utils/monacoClipboard';
|
||||
import {
|
||||
installWailsMonacoClipboardPasteHandler,
|
||||
MONACO_CLIPBOARD_HANDLER_REVISION,
|
||||
type MonacoClipboardReadFailure,
|
||||
} from '../utils/monacoClipboard';
|
||||
|
||||
export type { BeforeMount, OnMount } from '@monaco-editor/react';
|
||||
export type GonaviMonacoTypography = 'code' | 'data' | 'sql';
|
||||
@@ -810,6 +816,8 @@ const MonacoEditor: React.FC<MonacoEditorProps> = ({
|
||||
const monoFontFamily = useStore((state) => state.appearance.customMonoFontFamily);
|
||||
const globalFontSize = useStore((state) => state.fontSize);
|
||||
const clipboardPasteCleanupRef = useRef<(() => void) | null>(null);
|
||||
const clipboardEditorRef = useRef<Parameters<OnMount>[0] | null>(null);
|
||||
const clipboardMonacoRef = useRef<Parameters<OnMount>[1] | null>(null);
|
||||
// Monaco theme is process-global; never fall back to "light" or other editors get polluted.
|
||||
const resolvedTheme = theme
|
||||
?? (appTheme === 'dark' ? 'transparent-dark' : 'transparent-light');
|
||||
@@ -835,26 +843,57 @@ const MonacoEditor: React.FC<MonacoEditorProps> = ({
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => () => {
|
||||
clipboardPasteCleanupRef.current?.();
|
||||
clipboardPasteCleanupRef.current = null;
|
||||
}, []);
|
||||
|
||||
const handleBeforeMount: BeforeMount = useCallback((monaco) => {
|
||||
registerGonaviMonacoThemes(monaco);
|
||||
beforeMount?.(monaco);
|
||||
}, [beforeMount]);
|
||||
|
||||
const handleMount: OnMount = useCallback((editor, monaco) => {
|
||||
const handleClipboardReadFailure = useCallback(({ source, error }: MonacoClipboardReadFailure) => {
|
||||
console.warn('Failed to read clipboard text for Monaco paste', error);
|
||||
void message.warning({
|
||||
key: 'gonavi-query-editor-clipboard-read-failed',
|
||||
content: t(source === 'browser'
|
||||
? 'query_editor.message.clipboard_permission_required'
|
||||
: 'query_editor.message.clipboard_read_failed'),
|
||||
duration: 5,
|
||||
});
|
||||
}, []);
|
||||
|
||||
const replaceClipboardPasteHandler = useCallback((editor: Parameters<OnMount>[0], monaco: Parameters<OnMount>[1]) => {
|
||||
clipboardPasteCleanupRef.current?.();
|
||||
clipboardPasteCleanupRef.current = gonaviTypography === 'sql'
|
||||
? installWailsMonacoClipboardPasteHandler(monaco, editor)
|
||||
? installWailsMonacoClipboardPasteHandler(
|
||||
monaco,
|
||||
editor,
|
||||
undefined,
|
||||
undefined,
|
||||
handleClipboardReadFailure,
|
||||
)
|
||||
: null;
|
||||
}, [gonaviTypography, handleClipboardReadFailure]);
|
||||
|
||||
useEffect(() => {
|
||||
const editor = clipboardEditorRef.current;
|
||||
const monaco = clipboardMonacoRef.current;
|
||||
if (editor && monaco) {
|
||||
replaceClipboardPasteHandler(editor, monaco);
|
||||
}
|
||||
|
||||
return () => {
|
||||
clipboardPasteCleanupRef.current?.();
|
||||
clipboardPasteCleanupRef.current = null;
|
||||
};
|
||||
}, [MONACO_CLIPBOARD_HANDLER_REVISION, replaceClipboardPasteHandler]);
|
||||
|
||||
const handleMount: OnMount = useCallback((editor, monaco) => {
|
||||
clipboardEditorRef.current = editor;
|
||||
clipboardMonacoRef.current = monaco;
|
||||
replaceClipboardPasteHandler(editor, monaco);
|
||||
installOceanBaseOracleNavigationFallback(editor);
|
||||
installPrintableInputFallback(editor, monaco);
|
||||
installWebKitImeScrollStabilizer(editor);
|
||||
onMount?.(editor, monaco);
|
||||
}, [gonaviTypography, onMount]);
|
||||
}, [onMount, replaceClipboardPasteHandler]);
|
||||
|
||||
const resolvedOptions = useMemo(() => {
|
||||
if (uiVersion !== 'v2') {
|
||||
|
||||
@@ -45,19 +45,37 @@ const createInternals = (
|
||||
},
|
||||
});
|
||||
|
||||
const createEditor = (overrides: Record<string, unknown> = {}) => ({
|
||||
getOption: vi.fn(() => true),
|
||||
getRawOptions: vi.fn(() => ({ readOnly: false })),
|
||||
hasModel: vi.fn(() => true),
|
||||
hasTextFocus: vi.fn(() => false),
|
||||
onDidDispose: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
trigger: vi.fn(),
|
||||
...overrides,
|
||||
});
|
||||
const createEditorDomNode = () => {
|
||||
const listeners = new Map<string, (event?: unknown) => void>();
|
||||
return {
|
||||
addEventListener: vi.fn((type: string, listener: (event?: unknown) => void) => {
|
||||
listeners.set(type, listener);
|
||||
}),
|
||||
removeEventListener: vi.fn((type: string, listener: (event?: unknown) => void) => {
|
||||
if (listeners.get(type) === listener) listeners.delete(type);
|
||||
}),
|
||||
dispatch: (type: string, event?: unknown) => listeners.get(type)?.(event),
|
||||
};
|
||||
};
|
||||
|
||||
const createEditor = (overrides: Record<string, unknown> = {}) => {
|
||||
const domNode = createEditorDomNode();
|
||||
return {
|
||||
getDomNode: vi.fn(() => domNode),
|
||||
getOption: vi.fn(() => true),
|
||||
getRawOptions: vi.fn(() => ({ readOnly: false })),
|
||||
hasModel: vi.fn(() => true),
|
||||
hasTextFocus: vi.fn(() => false),
|
||||
hasWidgetFocus: vi.fn(() => false),
|
||||
onDidDispose: vi.fn(() => ({ dispose: vi.fn() })),
|
||||
trigger: vi.fn(),
|
||||
dispatchDomEvent: domNode.dispatch,
|
||||
...overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const wailsScope = (readText = vi.fn().mockResolvedValue('native text')) => ({
|
||||
window: {
|
||||
WailsInvoke: vi.fn(),
|
||||
runtime: { ClipboardGetText: readText },
|
||||
},
|
||||
});
|
||||
@@ -90,7 +108,53 @@ describe('Monaco clipboard fallback', () => {
|
||||
.resolves.toBe('SELECT * FROM users;');
|
||||
});
|
||||
|
||||
it('only handles paste while a registered SQL editor has text focus', async () => {
|
||||
it('leaves Ctrl+V to Monaco when no context-menu paste was requested', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const readText = vi.fn().mockResolvedValue('custom clipboard text');
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
const nativePaste = vi.fn(() => true);
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
wailsScope(readText),
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
await runPasteAction([...pasteAction.implementations, nativePaste]);
|
||||
|
||||
expect(nativePaste).toHaveBeenCalledTimes(1);
|
||||
expect(readText).not.toHaveBeenCalled();
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
it('clears a stale context-menu owner before Ctrl+V reaches Monaco', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const editorDomNode = createEditorDomNode();
|
||||
const readText = vi.fn().mockResolvedValue('custom clipboard text');
|
||||
const editor = createEditor({
|
||||
getDomNode: vi.fn(() => editorDomNode),
|
||||
hasTextFocus: vi.fn(() => true),
|
||||
});
|
||||
const nativePaste = vi.fn(() => true);
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
wailsScope(readText),
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
editorDomNode.dispatch('contextmenu');
|
||||
editorDomNode.dispatch('keydown');
|
||||
await runPasteAction([...pasteAction.implementations, nativePaste]);
|
||||
|
||||
expect(nativePaste).toHaveBeenCalledTimes(1);
|
||||
expect(readText).not.toHaveBeenCalled();
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
it('only handles context-menu paste for the registered SQL editor', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const sqlEditor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
@@ -105,6 +169,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
);
|
||||
|
||||
expect(pasteAction.addImplementation).toHaveBeenCalledTimes(1);
|
||||
sqlEditor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(scope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(sqlEditor.trigger).toHaveBeenCalledWith('keyboard', 'paste', {
|
||||
@@ -118,6 +183,277 @@ describe('Monaco clipboard fallback', () => {
|
||||
releaseSql();
|
||||
});
|
||||
|
||||
it('handles context-menu paste while the SQL editor retains widget focus', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const editor = createEditor({ hasWidgetFocus: vi.fn(() => true) });
|
||||
const scope = wailsScope();
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(scope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'native text' }),
|
||||
);
|
||||
release();
|
||||
});
|
||||
|
||||
it('routes context-menu paste to its editor after the menu takes focus', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const editorDomNode = createEditorDomNode();
|
||||
const editor = createEditor({ getDomNode: vi.fn(() => editorDomNode) });
|
||||
const scope = wailsScope();
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
editorDomNode.dispatch('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(scope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'native text' }),
|
||||
);
|
||||
|
||||
release();
|
||||
expect(editorDomNode.removeEventListener).toHaveBeenCalledWith('contextmenu', expect.any(Function));
|
||||
});
|
||||
|
||||
it('keeps context-menu ownership while clicking Monaco menu items', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const editor = createEditor();
|
||||
const scope = wailsScope();
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
editor.dispatchDomEvent('pointerdown', {
|
||||
target: {
|
||||
closest: vi.fn(() => null),
|
||||
},
|
||||
composedPath: vi.fn(() => [{
|
||||
closest: vi.fn((selector: string) => (
|
||||
selector === '.monaco-menu-container' ? {} : null
|
||||
)),
|
||||
}]),
|
||||
});
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(scope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'native text' }),
|
||||
);
|
||||
release();
|
||||
});
|
||||
|
||||
it('routes context-menu paste only to the most recently requested editor', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const firstEditorDomNode = createEditorDomNode();
|
||||
const secondEditorDomNode = createEditorDomNode();
|
||||
const firstEditor = createEditor({ getDomNode: vi.fn(() => firstEditorDomNode) });
|
||||
const secondEditor = createEditor({ getDomNode: vi.fn(() => secondEditorDomNode) });
|
||||
const firstScope = wailsScope(vi.fn().mockResolvedValue('first editor text'));
|
||||
const secondScope = wailsScope(vi.fn().mockResolvedValue('second editor text'));
|
||||
const monaco = { editor: { EditorOption: { emptySelectionClipboard: 45 } } };
|
||||
|
||||
const releaseFirst = installWailsMonacoClipboardPasteHandler(monaco, firstEditor, firstScope, internals);
|
||||
const releaseSecond = installWailsMonacoClipboardPasteHandler(monaco, secondEditor, secondScope, internals);
|
||||
|
||||
firstEditorDomNode.dispatch('contextmenu');
|
||||
secondEditorDomNode.dispatch('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
releaseFirst();
|
||||
releaseSecond();
|
||||
expect(firstScope.window.runtime.ClipboardGetText).not.toHaveBeenCalled();
|
||||
expect(firstEditor.trigger).not.toHaveBeenCalled();
|
||||
expect(secondScope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(secondEditor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'second editor text' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves Ctrl+V in another SQL editor to Monaco after a context menu is cancelled', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const firstEditor = createEditor();
|
||||
const secondEditor = createEditor();
|
||||
const firstReadText = vi.fn().mockResolvedValue('stale editor text');
|
||||
const secondReadText = vi.fn().mockResolvedValue('second editor text');
|
||||
const nativePaste = vi.fn(() => true);
|
||||
const monaco = { editor: { EditorOption: { emptySelectionClipboard: 45 } } };
|
||||
|
||||
const releaseFirst = installWailsMonacoClipboardPasteHandler(
|
||||
monaco,
|
||||
firstEditor,
|
||||
wailsScope(firstReadText),
|
||||
internals,
|
||||
);
|
||||
const releaseSecond = installWailsMonacoClipboardPasteHandler(
|
||||
monaco,
|
||||
secondEditor,
|
||||
wailsScope(secondReadText),
|
||||
internals,
|
||||
);
|
||||
|
||||
firstEditor.dispatchDomEvent('contextmenu');
|
||||
secondEditor.dispatchDomEvent('keydown');
|
||||
await runPasteAction([...pasteAction.implementations, nativePaste]);
|
||||
|
||||
expect(nativePaste).toHaveBeenCalledTimes(1);
|
||||
expect(firstReadText).not.toHaveBeenCalled();
|
||||
expect(secondReadText).not.toHaveBeenCalled();
|
||||
expect(firstEditor.trigger).not.toHaveBeenCalled();
|
||||
expect(secondEditor.trigger).not.toHaveBeenCalled();
|
||||
releaseFirst();
|
||||
releaseSecond();
|
||||
});
|
||||
|
||||
it('leaves Ctrl+V outside the SQL editor to Monaco after a context menu is cancelled', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const documentNode = createEditorDomNode();
|
||||
const editorDomNode = {
|
||||
...createEditorDomNode(),
|
||||
ownerDocument: documentNode,
|
||||
};
|
||||
const editor = createEditor({
|
||||
getDomNode: vi.fn(() => editorDomNode),
|
||||
});
|
||||
const scope = wailsScope();
|
||||
const defaultPaste = vi.fn(() => true);
|
||||
pasteAction.addImplementation(10000, 'monaco-default-paste', defaultPaste);
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
internals,
|
||||
);
|
||||
|
||||
editorDomNode.dispatch('contextmenu');
|
||||
documentNode.dispatch('keydown');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(defaultPaste).toHaveBeenCalledTimes(1);
|
||||
expect(scope.window.runtime.ClipboardGetText).not.toHaveBeenCalled();
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
it('does not finish an async context-menu paste after another SQL editor is activated', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
let resolveRead: ((text: string) => void) | undefined;
|
||||
const firstEditor = createEditor();
|
||||
const secondEditor = createEditor();
|
||||
const firstReadText = vi.fn(() => new Promise<string>((resolve) => {
|
||||
resolveRead = resolve;
|
||||
}));
|
||||
const monaco = { editor: { EditorOption: { emptySelectionClipboard: 45 } } };
|
||||
|
||||
const releaseFirst = installWailsMonacoClipboardPasteHandler(
|
||||
monaco,
|
||||
firstEditor,
|
||||
wailsScope(firstReadText),
|
||||
internals,
|
||||
);
|
||||
const releaseSecond = installWailsMonacoClipboardPasteHandler(
|
||||
monaco,
|
||||
secondEditor,
|
||||
wailsScope(),
|
||||
internals,
|
||||
);
|
||||
|
||||
firstEditor.dispatchDomEvent('contextmenu');
|
||||
const pastePromise = runPasteAction(pasteAction.implementations);
|
||||
secondEditor.dispatchDomEvent('pointerdown');
|
||||
resolveRead?.('stale editor text');
|
||||
await pastePromise;
|
||||
|
||||
expect(firstReadText).toHaveBeenCalledTimes(1);
|
||||
expect(firstEditor.trigger).not.toHaveBeenCalled();
|
||||
expect(secondEditor.trigger).not.toHaveBeenCalled();
|
||||
releaseFirst();
|
||||
releaseSecond();
|
||||
});
|
||||
|
||||
it('invokes Monaco trigger with the editor as its receiver', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
const editor = createEditor({ hasWidgetFocus: vi.fn(() => true) });
|
||||
editor.trigger = vi.fn(function (this: unknown) {
|
||||
if (this !== editor) {
|
||||
throw new Error('trigger called without editor receiver');
|
||||
}
|
||||
});
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
wailsScope(),
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(editor.trigger).toHaveBeenCalledTimes(1);
|
||||
release();
|
||||
});
|
||||
|
||||
it('does not paste after keyboard input interrupts an async context-menu read', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
let resolveRead: ((text: string) => void) | undefined;
|
||||
const editor = createEditor();
|
||||
const scope = wailsScope(vi.fn(() => new Promise<string>((resolve) => {
|
||||
resolveRead = resolve;
|
||||
})));
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
const pastePromise = runPasteAction(pasteAction.implementations);
|
||||
editor.dispatchDomEvent('keydown');
|
||||
resolveRead?.('stale text');
|
||||
await pastePromise;
|
||||
|
||||
expect(scope.window.runtime.ClipboardGetText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
it('leaves the global paste action to Monaco when only a non-SQL editor is focused', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
@@ -140,23 +476,23 @@ describe('Monaco clipboard fallback', () => {
|
||||
release();
|
||||
});
|
||||
|
||||
it('routes paste to the SQL editor that currently has focus and cleans each editor independently', async () => {
|
||||
it('routes context-menu paste to its requesting editor and cleans each editor independently', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const internals = createInternals(pasteAction);
|
||||
let focusedEditor = 'first';
|
||||
const firstEditor = createEditor({ hasTextFocus: vi.fn(() => focusedEditor === 'first') });
|
||||
const secondEditor = createEditor({ hasTextFocus: vi.fn(() => focusedEditor === 'second') });
|
||||
const firstEditor = createEditor();
|
||||
const secondEditor = createEditor();
|
||||
const scope = wailsScope(vi.fn().mockResolvedValue('native text'));
|
||||
const monaco = { editor: { EditorOption: { emptySelectionClipboard: 45 } } };
|
||||
|
||||
const releaseFirst = installWailsMonacoClipboardPasteHandler(monaco, firstEditor, scope, internals);
|
||||
const releaseSecond = installWailsMonacoClipboardPasteHandler(monaco, secondEditor, scope, internals);
|
||||
|
||||
firstEditor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(firstEditor.trigger).toHaveBeenCalledTimes(1);
|
||||
expect(secondEditor.trigger).not.toHaveBeenCalled();
|
||||
|
||||
focusedEditor = 'second';
|
||||
secondEditor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(firstEditor.trigger).toHaveBeenCalledTimes(1);
|
||||
expect(secondEditor.trigger).toHaveBeenCalledTimes(1);
|
||||
@@ -208,6 +544,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
const pastePromise = runPasteAction(pasteAction.implementations);
|
||||
release();
|
||||
resolveRead?.('late text');
|
||||
@@ -245,6 +582,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(editor.trigger).toHaveBeenLastCalledWith('keyboard', 'paste', {
|
||||
text: 'first value\nsecond value',
|
||||
@@ -253,6 +591,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
mode: null,
|
||||
});
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(editor.trigger).toHaveBeenLastCalledWith('keyboard', 'paste', {
|
||||
text: 'whole line\n',
|
||||
@@ -261,6 +600,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
mode: null,
|
||||
});
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(editor.trigger).toHaveBeenLastCalledWith('keyboard', 'paste', {
|
||||
text: 'foreign text',
|
||||
@@ -290,6 +630,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(wailsReadText).toHaveBeenCalledTimes(1);
|
||||
expect(browserReadText).toHaveBeenCalledTimes(1);
|
||||
@@ -297,19 +638,123 @@ describe('Monaco clipboard fallback', () => {
|
||||
release();
|
||||
});
|
||||
|
||||
it('does not register outside the Wails runtime', () => {
|
||||
it('registers for the generated Wails runtime shape without a legacy WailsInvoke global', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const readText = vi.fn().mockResolvedValue('bridge text');
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
createEditor(),
|
||||
editor,
|
||||
{
|
||||
navigator: { clipboard: { readText: vi.fn().mockResolvedValue('browser text') } },
|
||||
window: { runtime: { ClipboardGetText: vi.fn().mockResolvedValue('bridge text') } },
|
||||
window: { runtime: { ClipboardGetText: readText } },
|
||||
},
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
expect(pasteAction.addImplementation).not.toHaveBeenCalled();
|
||||
expect(pasteAction.addImplementation).toHaveBeenCalledTimes(1);
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(readText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'bridge text' }),
|
||||
);
|
||||
release();
|
||||
});
|
||||
|
||||
it('keeps the Monaco paste action in a plain browser runtime', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const browserReadText = vi.fn().mockResolvedValue('browser text');
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
{
|
||||
navigator: { clipboard: { readText: browserReadText } },
|
||||
window: {},
|
||||
},
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
expect(pasteAction.addImplementation).toHaveBeenCalledTimes(1);
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
expect(browserReadText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'browser text' }),
|
||||
);
|
||||
release();
|
||||
});
|
||||
|
||||
it('falls through when no clipboard reader is available at invocation time', () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
{ window: {} },
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
|
||||
expect(pasteAction.addImplementation).toHaveBeenCalledTimes(1);
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
expect(pasteAction.implementations[0]()).toBe(false);
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
it('resolves a Wails clipboard reader injected after editor mount', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
const scope: any = { window: { runtime: {} } };
|
||||
const readText = vi.fn().mockResolvedValue('late bridge text');
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
scope,
|
||||
createInternals(pasteAction),
|
||||
);
|
||||
scope.window.runtime.ClipboardGetText = readText;
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(readText).toHaveBeenCalledTimes(1);
|
||||
expect(editor.trigger).toHaveBeenCalledWith(
|
||||
'keyboard',
|
||||
'paste',
|
||||
expect.objectContaining({ text: 'late bridge text' }),
|
||||
);
|
||||
release();
|
||||
});
|
||||
|
||||
it('reports browser clipboard permission failures instead of swallowing them', async () => {
|
||||
const pasteAction = createPasteAction();
|
||||
const error = new DOMException('Read permission denied', 'NotAllowedError');
|
||||
const onReadFailure = vi.fn();
|
||||
const editor = createEditor({ hasTextFocus: vi.fn(() => true) });
|
||||
|
||||
const release = installWailsMonacoClipboardPasteHandler(
|
||||
{ editor: { EditorOption: { emptySelectionClipboard: 45 } } },
|
||||
editor,
|
||||
{
|
||||
navigator: { clipboard: { readText: vi.fn().mockRejectedValue(error) } },
|
||||
window: {},
|
||||
},
|
||||
createInternals(pasteAction),
|
||||
onReadFailure,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
await runPasteAction(pasteAction.implementations);
|
||||
|
||||
expect(onReadFailure).toHaveBeenCalledWith({ source: 'browser', error });
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
release();
|
||||
});
|
||||
|
||||
@@ -329,6 +774,7 @@ describe('Monaco clipboard fallback', () => {
|
||||
internals,
|
||||
);
|
||||
|
||||
editor.dispatchDomEvent('contextmenu');
|
||||
expect(pasteAction.implementations[0]()).toBe(false);
|
||||
expect(scope.window.runtime.ClipboardGetText).not.toHaveBeenCalled();
|
||||
expect(editor.trigger).not.toHaveBeenCalled();
|
||||
|
||||
@@ -9,7 +9,6 @@ interface WailsClipboardRuntimeLike {
|
||||
}
|
||||
|
||||
interface WailsWindowLike {
|
||||
WailsInvoke?: unknown;
|
||||
runtime?: WailsClipboardRuntimeLike;
|
||||
}
|
||||
|
||||
@@ -20,15 +19,47 @@ export interface MonacoClipboardScope {
|
||||
window?: WailsWindowLike;
|
||||
}
|
||||
|
||||
export interface MonacoClipboardReadFailure {
|
||||
source: 'wails' | 'browser';
|
||||
error: unknown;
|
||||
}
|
||||
|
||||
// A fresh token lets mounted editors replace stale Monaco actions after a Vite/Wails HMR update.
|
||||
export const MONACO_CLIPBOARD_HANDLER_REVISION = Symbol('gonavi-monaco-clipboard-handler');
|
||||
|
||||
interface DisposableLike {
|
||||
dispose: () => void;
|
||||
}
|
||||
|
||||
interface MonacoClipboardDomEventLike {
|
||||
target?: {
|
||||
closest?: (selector: string) => unknown;
|
||||
} | null;
|
||||
composedPath?: () => unknown[];
|
||||
}
|
||||
|
||||
interface MonacoClipboardEventTargetLike {
|
||||
addEventListener?: (
|
||||
type: string,
|
||||
listener: (event?: unknown) => void,
|
||||
useCapture?: boolean,
|
||||
) => void;
|
||||
removeEventListener?: (
|
||||
type: string,
|
||||
listener: (event?: unknown) => void,
|
||||
useCapture?: boolean,
|
||||
) => void;
|
||||
}
|
||||
|
||||
interface MonacoClipboardDomNodeLike extends MonacoClipboardEventTargetLike {
|
||||
ownerDocument?: MonacoClipboardEventTargetLike;
|
||||
}
|
||||
|
||||
interface MonacoClipboardEditorLike {
|
||||
getDomNode?: () => MonacoClipboardDomNodeLike | null;
|
||||
getOption?: (option: any) => unknown;
|
||||
getRawOptions?: () => { readOnly?: boolean };
|
||||
hasModel?: () => boolean;
|
||||
hasTextFocus?: () => boolean;
|
||||
onDidDispose?: (listener: () => void) => DisposableLike;
|
||||
trigger?: (source: string, handlerId: string, payload: unknown) => void;
|
||||
}
|
||||
@@ -67,15 +98,65 @@ export interface MonacoClipboardInternals {
|
||||
}
|
||||
|
||||
const MONACO_PASTE_IMPLEMENTATION_PRIORITY = 10001;
|
||||
const CONTEXT_MENU_OWNERSHIP_MS = 15_000;
|
||||
const CONTEXT_MENU_OWNER_KEY = Symbol.for('gonavi.monacoClipboard.contextMenuOwner');
|
||||
const INTERACTION_REVISION_KEY = Symbol.for('gonavi.monacoClipboard.interactionRevision');
|
||||
const noop = () => {};
|
||||
|
||||
let monacoClipboardInternalsPromise: Promise<MonacoClipboardInternals | null> | null = null;
|
||||
interface MonacoClipboardContextMenuOwner {
|
||||
editor: MonacoClipboardEditorLike;
|
||||
requestedAt: number;
|
||||
interactionRevision: number;
|
||||
}
|
||||
|
||||
const isWailsClipboardRuntime = (scope: MonacoClipboardScope): boolean => (
|
||||
typeof scope.window?.WailsInvoke === 'function'
|
||||
&& typeof scope.window.runtime?.ClipboardGetText === 'function'
|
||||
interface MonacoClipboardGlobalScope {
|
||||
[key: symbol]: unknown;
|
||||
}
|
||||
|
||||
const getContextMenuOwnerScope = (): MonacoClipboardGlobalScope => (
|
||||
globalThis as unknown as MonacoClipboardGlobalScope
|
||||
);
|
||||
|
||||
const getContextMenuOwner = (): MonacoClipboardContextMenuOwner | undefined => {
|
||||
const ownerScope = getContextMenuOwnerScope();
|
||||
const owner = ownerScope[CONTEXT_MENU_OWNER_KEY] as MonacoClipboardContextMenuOwner | undefined;
|
||||
if (owner && Date.now() - owner.requestedAt > CONTEXT_MENU_OWNERSHIP_MS) {
|
||||
delete ownerScope[CONTEXT_MENU_OWNER_KEY];
|
||||
return undefined;
|
||||
}
|
||||
return owner;
|
||||
};
|
||||
|
||||
const getInteractionRevision = (): number => {
|
||||
const revision = getContextMenuOwnerScope()[INTERACTION_REVISION_KEY];
|
||||
return typeof revision === 'number' ? revision : 0;
|
||||
};
|
||||
|
||||
const bumpInteractionRevision = (): number => {
|
||||
const ownerScope = getContextMenuOwnerScope();
|
||||
const revision = getInteractionRevision() + 1;
|
||||
ownerScope[INTERACTION_REVISION_KEY] = revision;
|
||||
return revision;
|
||||
};
|
||||
|
||||
const setContextMenuOwner = (editor: MonacoClipboardEditorLike): void => {
|
||||
getContextMenuOwnerScope()[CONTEXT_MENU_OWNER_KEY] = {
|
||||
editor,
|
||||
requestedAt: Date.now(),
|
||||
interactionRevision: bumpInteractionRevision(),
|
||||
};
|
||||
};
|
||||
|
||||
const clearContextMenuOwner = (editor?: MonacoClipboardEditorLike): void => {
|
||||
const ownerScope = getContextMenuOwnerScope();
|
||||
const owner = ownerScope[CONTEXT_MENU_OWNER_KEY] as MonacoClipboardContextMenuOwner | undefined;
|
||||
if (!editor || owner?.editor === editor) {
|
||||
delete ownerScope[CONTEXT_MENU_OWNER_KEY];
|
||||
}
|
||||
};
|
||||
|
||||
let monacoClipboardInternalsPromise: Promise<MonacoClipboardInternals | null> | null = null;
|
||||
|
||||
const getBrowserClipboardReader = (scope: MonacoClipboardScope): ClipboardReadText | undefined => {
|
||||
try {
|
||||
const clipboard = scope.navigator?.clipboard;
|
||||
@@ -85,6 +166,26 @@ const getBrowserClipboardReader = (scope: MonacoClipboardScope): ClipboardReadTe
|
||||
}
|
||||
};
|
||||
|
||||
const getClipboardReaders = (scope: MonacoClipboardScope): {
|
||||
primaryReadText?: ClipboardReadText;
|
||||
fallbackReadText?: ClipboardReadText;
|
||||
source: 'wails' | 'browser';
|
||||
} => {
|
||||
const wailsReadText = scope.window?.runtime?.ClipboardGetText;
|
||||
const browserReadText = getBrowserClipboardReader(scope);
|
||||
if (typeof wailsReadText === 'function') {
|
||||
return {
|
||||
primaryReadText: wailsReadText.bind(scope.window?.runtime),
|
||||
fallbackReadText: browserReadText,
|
||||
source: 'wails',
|
||||
};
|
||||
}
|
||||
return {
|
||||
primaryReadText: browserReadText,
|
||||
source: 'browser',
|
||||
};
|
||||
};
|
||||
|
||||
const loadMonacoClipboardInternals = (): Promise<MonacoClipboardInternals | null> => {
|
||||
if (!monacoClipboardInternalsPromise) {
|
||||
monacoClipboardInternalsPromise = Promise.all([
|
||||
@@ -136,48 +237,78 @@ const installPasteImplementation = (
|
||||
editor: MonacoClipboardEditorLike,
|
||||
scope: MonacoClipboardScope,
|
||||
internals: MonacoClipboardInternals,
|
||||
onReadFailure?: (failure: MonacoClipboardReadFailure) => void,
|
||||
): (() => void) => {
|
||||
const pasteAction = internals.pasteAction;
|
||||
const wailsReadText = scope.window?.runtime?.ClipboardGetText;
|
||||
if (!pasteAction?.addImplementation || typeof wailsReadText !== 'function') {
|
||||
if (!pasteAction?.addImplementation) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
const browserReadText = getBrowserClipboardReader(scope);
|
||||
let released = false;
|
||||
const editorDomNode = editor.getDomNode?.();
|
||||
const handleContextMenu = () => {
|
||||
setContextMenuOwner(editor);
|
||||
};
|
||||
const handleEditorInteraction = (event?: unknown) => {
|
||||
const domEvent = event as MonacoClipboardDomEventLike | undefined;
|
||||
const eventPath = domEvent?.composedPath?.() ?? [];
|
||||
const isMonacoMenuInteraction = [domEvent?.target, ...eventPath].some((node) => {
|
||||
const target = node as MonacoClipboardDomEventLike['target'];
|
||||
return Boolean(target?.closest?.('.monaco-menu-container'));
|
||||
});
|
||||
if (isMonacoMenuInteraction) {
|
||||
return;
|
||||
}
|
||||
bumpInteractionRevision();
|
||||
clearContextMenuOwner();
|
||||
};
|
||||
const interactionEventTarget = editorDomNode?.ownerDocument ?? editorDomNode;
|
||||
editorDomNode?.addEventListener?.('contextmenu', handleContextMenu);
|
||||
interactionEventTarget?.addEventListener?.('keydown', handleEditorInteraction, true);
|
||||
interactionEventTarget?.addEventListener?.('pointerdown', handleEditorInteraction, true);
|
||||
const implementationDisposable = pasteAction.addImplementation(
|
||||
MONACO_PASTE_IMPLEMENTATION_PRIORITY,
|
||||
'gonavi-wails-sql-editor',
|
||||
() => {
|
||||
const trigger = editor.trigger;
|
||||
const contextMenuOwner = getContextMenuOwner();
|
||||
const ownsContextMenu = contextMenuOwner?.editor === editor;
|
||||
if (
|
||||
editor.hasModel?.() === false
|
||||
|| editor.hasTextFocus?.() !== true
|
||||
!ownsContextMenu
|
||||
|| editor.hasModel?.() === false
|
||||
|| editor.getRawOptions?.().readOnly === true
|
||||
|| typeof trigger !== 'function'
|
||||
|| typeof editor.trigger !== 'function'
|
||||
) {
|
||||
// Let Monaco's default implementation handle another focused editor.
|
||||
// Keyboard paste and other editors must keep Monaco's native implementation.
|
||||
return false;
|
||||
}
|
||||
|
||||
clearContextMenuOwner(editor);
|
||||
const { primaryReadText, fallbackReadText, source } = getClipboardReaders(scope);
|
||||
if (!primaryReadText) {
|
||||
return false;
|
||||
}
|
||||
const requestRevision = contextMenuOwner.interactionRevision;
|
||||
|
||||
return (async () => {
|
||||
let text: string;
|
||||
try {
|
||||
text = await readClipboardTextWithFallback(wailsReadText.bind(scope.window?.runtime), browserReadText);
|
||||
} catch {
|
||||
text = await readClipboardTextWithFallback(primaryReadText, fallbackReadText);
|
||||
} catch (error) {
|
||||
onReadFailure?.({ source, error });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
released
|
||||
|| requestRevision !== getInteractionRevision()
|
||||
|| !text
|
||||
|| editor.hasModel?.() === false
|
||||
|| editor.hasTextFocus?.() !== true
|
||||
|| editor.getRawOptions?.().readOnly === true
|
||||
|| typeof editor.trigger !== 'function'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
trigger('keyboard', 'paste', createPastePayload(monaco, editor, text, internals.metadataManager));
|
||||
editor.trigger('keyboard', 'paste', createPastePayload(monaco, editor, text, internals.metadataManager));
|
||||
})();
|
||||
},
|
||||
);
|
||||
@@ -186,7 +317,11 @@ const installPasteImplementation = (
|
||||
const release = () => {
|
||||
if (released) return;
|
||||
released = true;
|
||||
clearContextMenuOwner(editor);
|
||||
implementationDisposable.dispose();
|
||||
editorDomNode?.removeEventListener?.('contextmenu', handleContextMenu);
|
||||
interactionEventTarget?.removeEventListener?.('keydown', handleEditorInteraction, true);
|
||||
interactionEventTarget?.removeEventListener?.('pointerdown', handleEditorInteraction, true);
|
||||
editorDisposeDisposable?.dispose();
|
||||
};
|
||||
editorDisposeDisposable = editor.onDidDispose?.(release);
|
||||
@@ -213,20 +348,17 @@ export const installWailsMonacoClipboardPasteHandler = (
|
||||
editor: MonacoClipboardEditorLike,
|
||||
scope: MonacoClipboardScope = globalThis as unknown as MonacoClipboardScope,
|
||||
internals?: MonacoClipboardInternals,
|
||||
onReadFailure?: (failure: MonacoClipboardReadFailure) => void,
|
||||
): (() => void) => {
|
||||
if (!isWailsClipboardRuntime(scope)) {
|
||||
return noop;
|
||||
}
|
||||
|
||||
if (internals) {
|
||||
return installPasteImplementation(monaco, editor, scope, internals);
|
||||
return installPasteImplementation(monaco, editor, scope, internals, onReadFailure);
|
||||
}
|
||||
|
||||
let released = false;
|
||||
let installedCleanup = noop;
|
||||
void loadMonacoClipboardInternals().then((loadedInternals) => {
|
||||
if (!released && loadedInternals) {
|
||||
installedCleanup = installPasteImplementation(monaco, editor, scope, loadedInternals);
|
||||
installedCleanup = installPasteImplementation(monaco, editor, scope, loadedInternals, onReadFailure);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "Abfrage konnte nicht abgebrochen werden: {{error}}",
|
||||
"query_editor.message.cancel_no_running": "Keine laufende Abfrage zum Abbrechen.",
|
||||
"query_editor.message.cancel_success": "Abfrage abgebrochen.",
|
||||
"query_editor.message.clipboard_permission_required": "Der Browser kann die Zwischenablage nicht lesen. Erlauben Sie dieser Website den Zugriff auf die Zwischenablage und versuchen Sie es erneut.",
|
||||
"query_editor.message.clipboard_read_failed": "Die Zwischenablage kann nicht gelesen werden. Versuchen Sie es erneut.",
|
||||
"query_editor.message.connection_not_found": "Verbindung nicht gefunden.",
|
||||
"query_editor.message.connection_readonly_blocked": "Für diese Verbindung ist der Produktionsschutz aktiv; es sind nur Abfragen erlaubt.",
|
||||
"query_editor.message.current_line_no_copyable_content": "Die aktuelle Zeile enthält keinen kopierbaren Inhalt.",
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "Failed to cancel query: {{error}}",
|
||||
"query_editor.message.cancel_no_running": "No running query to cancel.",
|
||||
"query_editor.message.cancel_success": "Query canceled.",
|
||||
"query_editor.message.clipboard_permission_required": "The browser cannot read the clipboard. Allow clipboard access for this site and try again.",
|
||||
"query_editor.message.clipboard_read_failed": "Unable to read the clipboard. Try again.",
|
||||
"query_editor.message.connection_not_found": "Connection not found.",
|
||||
"query_editor.message.connection_readonly_blocked": "This connection has production guard enabled and only allows query operations.",
|
||||
"query_editor.message.current_line_no_copyable_content": "No copyable content on the current line.",
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "クエリのキャンセルに失敗しました: {{error}}",
|
||||
"query_editor.message.cancel_no_running": "キャンセルできる実行中のクエリはありません。",
|
||||
"query_editor.message.cancel_success": "クエリをキャンセルしました。",
|
||||
"query_editor.message.clipboard_permission_required": "ブラウザーがクリップボードを読み取れません。このサイトのクリップボード権限を許可してから再試行してください。",
|
||||
"query_editor.message.clipboard_read_failed": "クリップボードを読み取れません。再試行してください。",
|
||||
"query_editor.message.connection_not_found": "接続が見つかりません。",
|
||||
"query_editor.message.connection_readonly_blocked": "この接続では本番保護が有効なため、問い合わせ操作のみ実行できます。",
|
||||
"query_editor.message.current_line_no_copyable_content": "現在の行にコピーできる内容がありません。",
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "Не удалось отменить запрос: {{error}}",
|
||||
"query_editor.message.cancel_no_running": "Нет выполняющегося запроса для отмены.",
|
||||
"query_editor.message.cancel_success": "Запрос отменен.",
|
||||
"query_editor.message.clipboard_permission_required": "Браузер не может прочитать буфер обмена. Разрешите этому сайту доступ к буферу обмена и повторите попытку.",
|
||||
"query_editor.message.clipboard_read_failed": "Не удалось прочитать буфер обмена. Повторите попытку.",
|
||||
"query_editor.message.connection_not_found": "Подключение не найдено.",
|
||||
"query_editor.message.connection_readonly_blocked": "Для этого подключения включена защита production, разрешены только операции запроса.",
|
||||
"query_editor.message.current_line_no_copyable_content": "В текущей строке нет содержимого для копирования.",
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "取消查询失败:{{error}}",
|
||||
"query_editor.message.cancel_no_running": "没有正在运行的查询可取消。",
|
||||
"query_editor.message.cancel_success": "查询已中止。",
|
||||
"query_editor.message.clipboard_permission_required": "浏览器无法读取剪贴板,请允许此站点的剪贴板权限后重试。",
|
||||
"query_editor.message.clipboard_read_failed": "无法读取剪贴板,请重试。",
|
||||
"query_editor.message.connection_not_found": "未找到连接。",
|
||||
"query_editor.message.connection_readonly_blocked": "当前连接已启用生产保护,仅允许执行查询操作。",
|
||||
"query_editor.message.current_line_no_copyable_content": "当前行没有可复制内容。",
|
||||
|
||||
@@ -6854,6 +6854,8 @@
|
||||
"query_editor.message.cancel_failed": "取消查詢失敗:{{error}}",
|
||||
"query_editor.message.cancel_no_running": "沒有正在執行的查詢可取消。",
|
||||
"query_editor.message.cancel_success": "查詢已終止。",
|
||||
"query_editor.message.clipboard_permission_required": "瀏覽器無法讀取剪貼簿,請允許此網站的剪貼簿權限後重試。",
|
||||
"query_editor.message.clipboard_read_failed": "無法讀取剪貼簿,請重試。",
|
||||
"query_editor.message.connection_not_found": "找不到連線。",
|
||||
"query_editor.message.connection_readonly_blocked": "目前連線已啟用正式保護,僅允許執行查詢操作。",
|
||||
"query_editor.message.current_line_no_copyable_content": "目前行沒有可複製內容。",
|
||||
|
||||
Reference in New Issue
Block a user