import { useCodeMirrorStateContext, useCodeMirrorViewContext, } from '@/features/source-editor/components/codemirror-context' import { usePermissionsContext } from '@/features/ide-react/context/permissions-context' import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context' import { useProjectContext } from '@/shared/context/project-context' import useSynctex from '@/features/pdf-preview/hooks/use-synctex' import { useDetachCompileContext } from '@/shared/context/detach-compile-context' import { useLayoutContext } from '@/shared/context/layout-context' import { useFeatureFlag } from '@/shared/context/split-test-context' import { useTranslation } from 'react-i18next' import { useCallback, useEffect, useRef } from 'react' import { formatShortcut, useCommandRegistry, } from '@/features/ide-react/context/command-registry-context' import { closeContextMenuEffect } from '../extensions/context-menu' import * as commands from '../extensions/toolbar/commands' import { cutSelection, copySelection, pasteWithoutFormatting, pasteWithFormatting, } from '../commands/clipboard' import { isVisual } from '../extensions/visual/visual' import { useEditorContext } from '@/shared/context/editor-context' import { useTrackingChangesMode } from '@/shared/hooks/use-tracking-changes-mode' import { sendContextMenuEvent, ContextMenuItemSegmentation, } from '../utils/context-menu-analytics' export const useContextMenuItems = () => { const view = useCodeMirrorViewContext() const state = useCodeMirrorStateContext() const permissions = usePermissionsContext() const { wantTrackChanges } = useEditorPropertiesContext() const { syncToPdf, syncToPdfInFlight, canSyncToPdf } = useSynctex() const { pdfUrl, pdfViewer } = useDetachCompileContext() const { detachRole, changeLayout, pdfLayout, view: ideView, } = useLayoutContext() const visualPreviewEnabled = useFeatureFlag('visual-preview') const { t } = useTranslation() const { shortcuts } = useCommandRegistry() const { features } = useProjectContext() const requestedPdfSyncRef = useRef(false) const { setUpgradeTrackChangesModal } = useEditorContext() const trackingChangesMode = useTrackingChangesMode() const isReview = trackingChangesMode === 'review' const closeMenu = useCallback(() => { view.dispatch({ effects: closeContextMenuEffect.of(null) }) view.focus() }, [view]) // Handle closing the menu when it loses focus, e.g. click outside the editor const onToggle = (show: boolean) => { if (!show) { // Skip closing if a sync to PDF is in flight if (requestedPdfSyncRef.current) { return } closeMenu() } } // Wait for syncToPdf to finish before closing the menu useEffect(() => { if (requestedPdfSyncRef.current && !syncToPdfInFlight) { closeMenu() // Clear the synchronous flag when the close completes requestedPdfSyncRef.current = false } }, [syncToPdfInFlight, closeMenu]) const hasSelection = !state.selection.main.empty const canEdit = permissions.write || permissions.trackedWrite // Determine layout states for PDF sync functionality const isPdfDetached = detachRole === 'detacher' const isEditorOnly = pdfLayout === 'flat' && ideView === 'editor' && !isPdfDetached const jumpToLocationInPdfEnabled = pdfUrl && pdfViewer !== 'native' && !visualPreviewEnabled && canSyncToPdf const wrapForContextMenu = useCallback( ( item: ContextMenuItemSegmentation, command: () => Promise | boolean ) => async () => { sendContextMenuEvent('menu-click', { location: 'editor-context-menu', item, }) const result = await command() if (result !== false) { view.focus() closeMenu() } }, [view, closeMenu] ) const inVisualMode = isVisual(view) const handleCut = wrapForContextMenu('cut', () => cutSelection(view)) const handleCopy = wrapForContextMenu('copy', () => copySelection(view)) const handlePaste = wrapForContextMenu('paste', () => inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view) ) const handlePasteSpecial = wrapForContextMenu( inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting', () => inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view) ) const handleDelete = wrapForContextMenu('delete', () => commands.deleteSelection(view) ) const handleToggleTrackChanges = wrapForContextMenu( wantTrackChanges ? 'back-to-editing' : 'suggest-edits', () => { // Matching the logic in review toggle to ensure consistency for server pro if (!features.trackChanges && !isReview) { setUpgradeTrackChangesModal({ show: true, location: 'editor-context-menu', }) return true } window.dispatchEvent(new Event('toggle-track-changes')) return true } ) const handleComment = wrapForContextMenu('comment', () => { commands.addComment('editor-context-menu') return true }) // Sync-to-PDF is special: it needs to wait for async completion before closing const handleSyncToPdf = useCallback(() => { // Switch to split view only when in editor-only mode with non-detached PDF if (isEditorOnly) { changeLayout('sideBySide') } sendContextMenuEvent('menu-click', { location: 'editor-context-menu', item: 'jump-to-location-in-pdf', }) sendContextMenuEvent('jump-to-location', { method: 'editor-context-menu', direction: 'code-location-in-pdf', }) requestedPdfSyncRef.current = true syncToPdf() view.focus() }, [syncToPdf, view, changeLayout, isEditorOnly]) const getShortcut = useCallback( (id: string) => { const shortcut = shortcuts[id]?.[0] return shortcut ? formatShortcut(shortcut) : undefined }, [shortcuts] ) return { closeMenu, onToggle, menuItems: [ { label: t('cut'), handler: handleCut, disabled: false, show: canEdit, shortcut: getShortcut('cut'), }, { label: t('copy'), handler: handleCopy, disabled: false, show: true, shortcut: getShortcut('copy'), }, { label: t('paste'), handler: handlePaste, disabled: false, show: canEdit, shortcut: getShortcut('paste'), }, { label: inVisualMode ? t('paste_without_formatting') : t('paste_with_formatting'), handler: handlePasteSpecial, disabled: false, show: canEdit, shortcut: inVisualMode ? getShortcut('paste-special') : undefined, }, { label: t('delete'), handler: handleDelete, disabled: !hasSelection, show: canEdit, shortcut: undefined, }, { label: t('jump_to_location_in_pdf'), handler: handleSyncToPdf, disabled: syncToPdfInFlight, separatorAbove: true, show: jumpToLocationInPdfEnabled, shortcut: undefined, }, { label: wantTrackChanges ? t('back_to_editing') : t('suggest_edits'), handler: handleToggleTrackChanges, disabled: false, separatorAbove: true, show: canEdit && features.trackChangesVisible, shortcut: getShortcut('toggle-track-changes'), }, { label: t('comment'), handler: handleComment, disabled: !hasSelection, show: permissions.comment, shortcut: getShortcut('insert-comment'), }, ].filter(item => item.show), } }