use-context-menu-items.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. import {
  2. useCodeMirrorStateContext,
  3. useCodeMirrorViewContext,
  4. } from '@/features/source-editor/components/codemirror-context'
  5. import { usePermissionsContext } from '@/features/ide-react/context/permissions-context'
  6. import { useEditorPropertiesContext } from '@/features/ide-react/context/editor-properties-context'
  7. import { useProjectContext } from '@/shared/context/project-context'
  8. import useSynctex from '@/features/pdf-preview/hooks/use-synctex'
  9. import { useDetachCompileContext } from '@/shared/context/detach-compile-context'
  10. import { useLayoutContext } from '@/shared/context/layout-context'
  11. import { useFeatureFlag } from '@/shared/context/split-test-context'
  12. import { useTranslation } from 'react-i18next'
  13. import { useCallback, useEffect, useRef } from 'react'
  14. import {
  15. formatShortcut,
  16. useCommandRegistry,
  17. } from '@/features/ide-react/context/command-registry-context'
  18. import { closeContextMenuEffect } from '../extensions/context-menu'
  19. import * as commands from '../extensions/toolbar/commands'
  20. import {
  21. cutSelection,
  22. copySelection,
  23. pasteWithoutFormatting,
  24. pasteWithFormatting,
  25. } from '../commands/clipboard'
  26. import { isVisual } from '../extensions/visual/visual'
  27. export const useContextMenuItems = () => {
  28. const view = useCodeMirrorViewContext()
  29. const state = useCodeMirrorStateContext()
  30. const permissions = usePermissionsContext()
  31. const { wantTrackChanges } = useEditorPropertiesContext()
  32. const { syncToPdf, syncToPdfInFlight, canSyncToPdf } = useSynctex()
  33. const { pdfUrl, pdfViewer } = useDetachCompileContext()
  34. const { detachRole } = useLayoutContext()
  35. const visualPreviewEnabled = useFeatureFlag('visual-preview')
  36. const { t } = useTranslation()
  37. const { shortcuts } = useCommandRegistry()
  38. const { features } = useProjectContext()
  39. const requestedPdfSyncRef = useRef(false)
  40. const closeMenu = useCallback(() => {
  41. view.dispatch({ effects: closeContextMenuEffect.of(null) })
  42. }, [view])
  43. // Handle closing the menu when it loses focus, e.g. click outside the editor
  44. const onToggle = (show: boolean) => {
  45. if (!show) {
  46. // Skip closing if a sync to PDF is in flight
  47. if (requestedPdfSyncRef.current) {
  48. return
  49. }
  50. closeMenu()
  51. }
  52. }
  53. // Wait for syncToPdf to finish before closing the menu
  54. useEffect(() => {
  55. if (requestedPdfSyncRef.current && !syncToPdfInFlight) {
  56. closeMenu()
  57. // Clear the synchronous flag when the close completes
  58. requestedPdfSyncRef.current = false
  59. }
  60. }, [syncToPdfInFlight, closeMenu])
  61. const hasSelection = !state.selection.main.empty
  62. const canEdit = permissions.write || permissions.trackedWrite
  63. const jumpToLocationInPdfEnabled =
  64. pdfUrl &&
  65. pdfViewer !== 'native' &&
  66. !detachRole &&
  67. !visualPreviewEnabled &&
  68. canSyncToPdf
  69. const wrapForContextMenu = useCallback(
  70. (command: () => Promise<boolean> | boolean) => async () => {
  71. const result = await command()
  72. if (result !== false) {
  73. view.focus()
  74. closeMenu()
  75. }
  76. },
  77. [view, closeMenu]
  78. )
  79. const inVisualMode = isVisual(view)
  80. const handleCut = wrapForContextMenu(() => cutSelection(view))
  81. const handleCopy = wrapForContextMenu(() => copySelection(view))
  82. const handlePaste = wrapForContextMenu(() =>
  83. inVisualMode ? pasteWithFormatting(view) : pasteWithoutFormatting(view)
  84. )
  85. const handlePasteSpecial = wrapForContextMenu(() =>
  86. inVisualMode ? pasteWithoutFormatting(view) : pasteWithFormatting(view)
  87. )
  88. const handleDelete = wrapForContextMenu(() => commands.deleteSelection(view))
  89. const handleToggleTrackChanges = wrapForContextMenu(() => {
  90. window.dispatchEvent(new Event('toggle-track-changes'))
  91. return true
  92. })
  93. const handleComment = wrapForContextMenu(() => {
  94. commands.addComment()
  95. return true
  96. })
  97. // Sync-to-PDF is special: it needs to wait for async completion before closing
  98. const handleSyncToPdf = useCallback(() => {
  99. requestedPdfSyncRef.current = true
  100. syncToPdf()
  101. view.focus()
  102. }, [syncToPdf, view])
  103. const getShortcut = useCallback(
  104. (id: string) => {
  105. const shortcut = shortcuts[id]?.[0]
  106. return shortcut ? formatShortcut(shortcut) : undefined
  107. },
  108. [shortcuts]
  109. )
  110. return {
  111. closeMenu,
  112. onToggle,
  113. menuItems: [
  114. {
  115. label: t('cut'),
  116. handler: handleCut,
  117. disabled: false,
  118. show: canEdit,
  119. shortcut: getShortcut('cut'),
  120. },
  121. {
  122. label: t('copy'),
  123. handler: handleCopy,
  124. disabled: false,
  125. show: true,
  126. shortcut: getShortcut('copy'),
  127. },
  128. {
  129. label: t('paste'),
  130. handler: handlePaste,
  131. disabled: false,
  132. show: canEdit,
  133. shortcut: getShortcut('paste'),
  134. },
  135. {
  136. label: inVisualMode
  137. ? t('paste_without_formatting')
  138. : t('paste_with_formatting'),
  139. handler: handlePasteSpecial,
  140. disabled: false,
  141. show: canEdit,
  142. shortcut: inVisualMode ? getShortcut('paste-special') : undefined,
  143. },
  144. {
  145. label: t('delete'),
  146. handler: handleDelete,
  147. disabled: !hasSelection,
  148. show: canEdit,
  149. shortcut: undefined,
  150. },
  151. {
  152. label: t('jump_to_location_in_pdf'),
  153. handler: handleSyncToPdf,
  154. disabled: syncToPdfInFlight,
  155. separatorAbove: true,
  156. show: jumpToLocationInPdfEnabled,
  157. shortcut: undefined,
  158. },
  159. {
  160. label: wantTrackChanges ? t('back_to_editing') : t('suggest_edits'),
  161. handler: handleToggleTrackChanges,
  162. // disable for now, future work opens upgrade modal
  163. disabled: !features.trackChanges,
  164. separatorAbove: true,
  165. show: canEdit,
  166. shortcut: getShortcut('toggle-track-changes'),
  167. },
  168. {
  169. label: t('comment'),
  170. handler: handleComment,
  171. disabled: !hasSelection,
  172. show: permissions.comment,
  173. shortcut: getShortcut('insert-comment'),
  174. },
  175. ].filter(item => item.show),
  176. }
  177. }