| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- 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 { showClipboardPasteErrorToast } from '../components/clipboard-toasts'
- 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'
- import { isCursorOnEmptyLine } from '../utils/is-cursor-on-empty-line'
- import { selectAll } from '@codemirror/commands'
- import { useTrackedChangesActions } from '@/features/review-panel/hooks/use-tracked-changes-actions'
- 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 { changesInSelection, acceptChangesHandler, rejectChangesHandler } =
- useTrackedChangesActions()
- 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> | boolean
- ) =>
- async () => {
- sendContextMenuEvent('menu-click', {
- location: 'editor-context-menu',
- item,
- })
- const result = await command()
- if (result !== false) {
- view.focus()
- closeMenu()
- }
- },
- [view, closeMenu]
- )
- const getShortcut = useCallback(
- (id: string) => {
- const shortcut = shortcuts[id]?.[0]
- return shortcut ? formatShortcut(shortcut) : undefined
- },
- [shortcuts]
- )
- const inVisualMode = isVisual(view)
- const handleCut = wrapForContextMenu('cut', () => cutSelection(view))
- const handleCopy = wrapForContextMenu('copy', () => copySelection(view))
- const handlePaste = wrapForContextMenu('paste', async () => {
- const result = await (inVisualMode
- ? pasteWithFormatting(view)
- : pasteWithoutFormatting(view))
- if (result === false) {
- showClipboardPasteErrorToast(getShortcut('paste'))
- }
- return result
- })
- const handlePasteSpecial = wrapForContextMenu(
- inVisualMode ? 'paste-without-formatting' : 'paste-with-formatting',
- async () => {
- const result = await (inVisualMode
- ? pasteWithoutFormatting(view)
- : pasteWithFormatting(view))
- if (result === false) {
- showClipboardPasteErrorToast(getShortcut('paste'))
- }
- return result
- }
- )
- const handleSelectAll = wrapForContextMenu('select-all', () =>
- selectAll(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
- })
- const handleAcceptChanges = wrapForContextMenu('accept-changes', () => {
- acceptChangesHandler()
- return true
- })
- const handleRejectChanges = wrapForContextMenu('reject-changes', () => {
- rejectChangesHandler()
- 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])
- 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('select_all'),
- handler: handleSelectAll,
- disabled: false,
- show: true,
- shortcut: getShortcut('select-all'),
- },
- {
- 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: isCursorOnEmptyLine(state),
- show: permissions.comment,
- shortcut: getShortcut('insert-comment'),
- },
- {
- label: t('accept_selected_changes'),
- handler: handleAcceptChanges,
- disabled: false,
- show: permissions.write && changesInSelection.length > 0,
- shortcut: undefined,
- },
- {
- label: t('reject_selected_changes'),
- handler: handleRejectChanges,
- disabled: false,
- show: permissions.write && changesInSelection.length > 0,
- shortcut: undefined,
- },
- ].filter(item => item.show),
- }
- }
|