file-tree-open-context.tsx 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import {
  2. createContext,
  3. FC,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. } from 'react'
  11. import { useProjectContext } from '@/shared/context/project-context'
  12. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  13. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  14. import { useSelectFileTreeEntity } from '@/features/ide-react/hooks/use-select-file-tree-entity'
  15. import useScopeValue from '@/shared/hooks/use-scope-value'
  16. import { BinaryFile } from '@/features/file-view/types/binary-file'
  17. import {
  18. FileTreeDocumentFindResult,
  19. FileTreeFileRefFindResult,
  20. FileTreeFindResult,
  21. } from '@/features/ide-react/types/file-tree'
  22. import { debugConsole } from '@/utils/debugging'
  23. import { convertFileRefToBinaryFile } from '@/features/ide-react/util/file-view'
  24. const FileTreeOpenContext = createContext<
  25. | {
  26. selectedEntityCount: number
  27. openEntity: FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  28. handleFileTreeInit: () => void
  29. handleFileTreeSelect: (selectedEntities: FileTreeFindResult[]) => void
  30. handleFileTreeDelete: (entity: FileTreeFindResult) => void
  31. }
  32. | undefined
  33. >(undefined)
  34. export const FileTreeOpenProvider: FC = ({ children }) => {
  35. const { rootDocId } = useProjectContext()
  36. const { eventEmitter, projectJoined } = useIdeReactContext()
  37. const {
  38. openDocId: openDocWithId,
  39. currentDocumentId: openDocId,
  40. openInitialDoc,
  41. } = useEditorManagerContext()
  42. const { selectEntity } = useSelectFileTreeEntity()
  43. const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
  44. const [openEntity, setOpenEntity] = useState<
  45. FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  46. >(null)
  47. const [selectedEntityCount, setSelectedEntityCount] = useState(0)
  48. const [fileTreeReady, setFileTreeReady] = useState(false)
  49. const handleFileTreeInit = useCallback(() => {
  50. setFileTreeReady(true)
  51. }, [])
  52. // Open a document in the editor when one is selected in the file tree
  53. const handleFileTreeSelect = useCallback(
  54. (selectedEntities: FileTreeFindResult[]) => {
  55. debugConsole.log('File tree selection changed', selectedEntities)
  56. setSelectedEntityCount(selectedEntities.length)
  57. if (selectedEntities.length !== 1) {
  58. setOpenEntity(null)
  59. return
  60. }
  61. const [selected] = selectedEntities
  62. if (selected.type === 'folder') {
  63. return
  64. }
  65. setOpenEntity(selected)
  66. if (selected.type === 'doc' && fileTreeReady) {
  67. openDocWithId(selected.entity._id)
  68. }
  69. // Keep openFile scope value in sync with the file tree
  70. const openFile =
  71. selected.type === 'fileRef'
  72. ? convertFileRefToBinaryFile(selected.entity)
  73. : null
  74. setOpenFile(openFile)
  75. if (openFile) {
  76. window.dispatchEvent(new CustomEvent('file-view:file-opened'))
  77. }
  78. },
  79. [fileTreeReady, setOpenFile, openDocWithId]
  80. )
  81. const handleFileTreeDelete = useCallback(
  82. (entity: FileTreeFindResult) => {
  83. eventEmitter.emit('entity:deleted', entity)
  84. // Select the root document if the current document was deleted
  85. if (entity.entity._id === openDocId) {
  86. openDocWithId(rootDocId!)
  87. }
  88. },
  89. [eventEmitter, openDocId, openDocWithId, rootDocId]
  90. )
  91. const openDocIdRef = useRef<typeof openDocId | null>(null)
  92. // Synchronize the file tree when openDoc or openDocId is called on the editor
  93. // manager context from elsewhere. If the file tree does change, it will
  94. // trigger the onSelect handler in this component, which will update the local
  95. // state.
  96. useEffect(() => {
  97. if (openDocId !== openDocIdRef.current) {
  98. debugConsole.log(`openDocId changed to ${openDocId}`)
  99. openDocIdRef.current = openDocId
  100. if (openDocId !== null) {
  101. selectEntity(openDocId)
  102. }
  103. }
  104. }, [openDocId, selectEntity])
  105. // Open a document once the file tree and project are ready
  106. const initialOpenDoneRef = useRef(false)
  107. useEffect(() => {
  108. if (
  109. rootDocId &&
  110. fileTreeReady &&
  111. projectJoined &&
  112. !initialOpenDoneRef.current
  113. ) {
  114. initialOpenDoneRef.current = true
  115. openInitialDoc(rootDocId)
  116. }
  117. }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
  118. const value = useMemo(() => {
  119. return {
  120. selectedEntityCount,
  121. openEntity,
  122. handleFileTreeInit,
  123. handleFileTreeSelect,
  124. handleFileTreeDelete,
  125. }
  126. }, [
  127. handleFileTreeDelete,
  128. handleFileTreeInit,
  129. handleFileTreeSelect,
  130. openEntity,
  131. selectedEntityCount,
  132. ])
  133. return (
  134. <FileTreeOpenContext.Provider value={value}>
  135. {children}
  136. </FileTreeOpenContext.Provider>
  137. )
  138. }
  139. export const useFileTreeOpenContext = () => {
  140. const context = useContext(FileTreeOpenContext)
  141. if (!context) {
  142. throw new Error(
  143. 'useFileTreeOpenContext is only available inside FileTreeOpenProvider'
  144. )
  145. }
  146. return context
  147. }