diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index b77b58a5c1a..c0abe44dea5 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -1971,6 +1971,16 @@ pointer-events: auto; } +/* Why: Monaco leaves the peek preview container `inline-block` and only sizes the + editor child, so the container shrink-wraps the child in BOTH axes while the + inherited automaticLayout ResizeObserver measures the container — a feedback + loop that ratchets the preview to blank at fractional zoom/DPI (issue #7405). + Pinning both dimensions to the splitview slot severs the loop. */ +.monaco-editor .reference-zone-widget .preview.inline { + height: 100%; + width: 100%; +} + /* ── Git conflict decorations ───────────────────────────────────── */ .monaco-editor .orca-conflict-marker-line { diff --git a/src/renderer/src/lib/monaco-peek-preview-options.test.ts b/src/renderer/src/lib/monaco-peek-preview-options.test.ts new file mode 100644 index 00000000000..62e3952aa51 --- /dev/null +++ b/src/renderer/src/lib/monaco-peek-preview-options.test.ts @@ -0,0 +1,98 @@ +// @vitest-environment happy-dom + +import { describe, expect, it, vi } from 'vitest' +import type * as Monaco from 'monaco-editor' +import { installMonacoPeekReferencesPreviewOptions } from './monaco-peek-preview-options' + +type FakePreviewEditor = Pick + +type FakeReferenceWidgetInstance = { + _preview?: FakePreviewEditor + _fillBody?: (containerElement: HTMLElement) => void + _revealReference?: (...args: unknown[]) => Promise +} + +function createReferenceWidgetConstructor(hooks: { + fillBody?: (instance: FakeReferenceWidgetInstance) => void + revealReference?: (instance: FakeReferenceWidgetInstance) => Promise +}): { + prototype: FakeReferenceWidgetInstance & + Required> +} { + return { + prototype: { + _fillBody(this: FakeReferenceWidgetInstance): void { + hooks.fillBody?.(this) + }, + async _revealReference(this: FakeReferenceWidgetInstance): Promise { + return hooks.revealReference?.(this) + } + } + } +} + +function createPreviewEditor(): FakePreviewEditor { + return { updateOptions: vi.fn() } +} + +const peekPreviewOptions = { + smoothScrolling: false, + stickyScroll: { enabled: false }, + wordWrap: 'off' +} + +describe('installMonacoPeekReferencesPreviewOptions', () => { + it('updates the embedded preview after ReferenceWidget creates it', () => { + const preview = createPreviewEditor() + const referenceWidget = createReferenceWidgetConstructor({ + fillBody(instance) { + instance._preview = preview + } + }) + + installMonacoPeekReferencesPreviewOptions(referenceWidget) + referenceWidget.prototype._fillBody(document.createElement('div')) + + expect(preview.updateOptions).toHaveBeenCalledWith(peekPreviewOptions) + }) + + it('updates the embedded preview before revealing a reference', async () => { + const calls: string[] = [] + const preview: FakePreviewEditor = { + updateOptions: vi.fn(() => calls.push('updateOptions')) + } + const referenceWidget = createReferenceWidgetConstructor({ + revealReference: async () => { + calls.push('revealReference') + } + }) + referenceWidget.prototype._preview = preview + + installMonacoPeekReferencesPreviewOptions(referenceWidget) + await referenceWidget.prototype._revealReference('reference') + + expect(calls).toEqual(['updateOptions', 'revealReference', 'updateOptions']) + expect(preview.updateOptions).toHaveBeenCalledWith(peekPreviewOptions) + }) + + it('skips patching when the private Monaco hooks are missing', () => { + const referenceWidget: { prototype: FakeReferenceWidgetInstance } = { prototype: {} } + + installMonacoPeekReferencesPreviewOptions(referenceWidget) + + expect(referenceWidget.prototype._fillBody).toBeUndefined() + expect(referenceWidget.prototype._revealReference).toBeUndefined() + }) + + it('does not wrap ReferenceWidget more than once', async () => { + const preview = createPreviewEditor() + const referenceWidget = createReferenceWidgetConstructor({}) + referenceWidget.prototype._preview = preview + + installMonacoPeekReferencesPreviewOptions(referenceWidget) + installMonacoPeekReferencesPreviewOptions(referenceWidget) + await referenceWidget.prototype._revealReference('reference') + + expect(preview.updateOptions).toHaveBeenCalledTimes(2) + }) +}) diff --git a/src/renderer/src/lib/monaco-peek-preview-options.ts b/src/renderer/src/lib/monaco-peek-preview-options.ts new file mode 100644 index 00000000000..8927ed7721f --- /dev/null +++ b/src/renderer/src/lib/monaco-peek-preview-options.ts @@ -0,0 +1,67 @@ +import type * as Monaco from 'monaco-editor' +import { ReferenceWidget as MonacoReferenceWidget } from 'monaco-editor/esm/vs/editor/contrib/gotoSymbol/browser/peek/referencesWidget.js' + +type PeekPreviewEditor = Pick + +const PEEK_REFERENCES_PREVIEW_OPTIONS: Monaco.editor.IEditorOptions = { + smoothScrolling: false, + stickyScroll: { enabled: false }, + wordWrap: 'off' +} + +type ReferenceWidgetInstance = { + _preview?: PeekPreviewEditor + _fillBody?: (containerElement: HTMLElement) => void + _revealReference?: (...args: unknown[]) => Promise +} + +type ReferenceWidgetConstructor = { + prototype: ReferenceWidgetInstance & { + __orcaPeekPreviewOptionsInstalled?: true + } +} + +function applyPeekReferencesPreviewOptions(editor: PeekPreviewEditor | undefined): void { + // Why: Monaco embedded editors inherit Orca's full-editor options first. + // Peek previews are transient readers, so keep scroll/wrap widgets out of them. + editor?.updateOptions(PEEK_REFERENCES_PREVIEW_OPTIONS) +} + +export function installMonacoPeekReferencesPreviewOptions( + referenceWidget: ReferenceWidgetConstructor = MonacoReferenceWidget as unknown as ReferenceWidgetConstructor +): void { + const prototype = referenceWidget.prototype + if (prototype.__orcaPeekPreviewOptionsInstalled) { + return + } + + const originalFillBody = prototype._fillBody + const originalRevealReference = prototype._revealReference + // Why: these are private Monaco members with no stability guarantee; if an + // upgrade removes either, skip patching so Peek keeps Monaco's defaults. + if (typeof originalFillBody !== 'function' || typeof originalRevealReference !== 'function') { + return + } + + prototype._fillBody = function fillBodyWithPeekPreviewOptions( + this: ReferenceWidgetInstance, + containerElement: HTMLElement + ): void { + originalFillBody.call(this, containerElement) + applyPeekReferencesPreviewOptions(this._preview) + } + + prototype._revealReference = async function revealReferenceWithPeekPreviewOptions( + this: ReferenceWidgetInstance, + ...args: unknown[] + ): Promise { + applyPeekReferencesPreviewOptions(this._preview) + try { + return await originalRevealReference.apply(this, args) + } finally { + applyPeekReferencesPreviewOptions(this._preview) + } + } + + prototype.__orcaPeekPreviewOptionsInstalled = true +} diff --git a/src/renderer/src/lib/monaco-setup.ts b/src/renderer/src/lib/monaco-setup.ts index 67d05eaefc3..8a955e99d1c 100644 --- a/src/renderer/src/lib/monaco-setup.ts +++ b/src/renderer/src/lib/monaco-setup.ts @@ -13,6 +13,7 @@ import { registerSvelteLanguage } from './monaco-languages/register-svelte' import { registerVueLanguage } from './monaco-languages/register-vue' import { installMonacoDelayerCancellationGuard } from './monaco-delayer-cancellation-guard' import { installMonacoDiffEditorDisposalGuard } from './monaco-diff-editor-disposal' +import { installMonacoPeekReferencesPreviewOptions } from './monaco-peek-preview-options' import { installMonacoContextMenuPaste } from '@/components/editor/install-monaco-context-menu-paste' globalThis.MonacoEnvironment = { @@ -78,6 +79,7 @@ registerAstroLanguage(monaco) registerNimLanguage(monaco) installMonacoDelayerCancellationGuard() installMonacoDiffEditorDisposalGuard(monaco) +installMonacoPeekReferencesPreviewOptions() // Why: Monaco's built-in context-menu Paste reads navigator.clipboard, which is // blocked in Orca's sandboxed renderer. Route it through the trusted IPC bridge // so right-click Paste works like Cmd+V (which already works via native events).