editor-and-sidebar.tsx 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import React, { useCallback, useEffect, useRef, useState } from 'react'
  2. import TwoColumnMainContent from '@/features/ide-react/components/layout/two-column-main-content'
  3. import EditorSidebar from '@/features/ide-react/components/editor-sidebar'
  4. import History from '@/features/ide-react/components/history'
  5. import { HistoryProvider } from '@/features/history/context/history-context'
  6. import { useProjectContext } from '@/shared/context/project-context'
  7. import { useLayoutContext } from '@/shared/context/layout-context'
  8. import {
  9. FileTreeDeleteHandler,
  10. FileTreeDocumentFindResult,
  11. FileTreeFileRefFindResult,
  12. FileTreeFindResult,
  13. } from '@/features/ide-react/types/file-tree'
  14. import FileView from '@/features/file-view/components/file-view'
  15. import { FileRef } from '../../../../../types/file-ref'
  16. import { EditorPane } from '@/features/ide-react/components/editor/editor-pane'
  17. import EditorAndPdf from '@/features/ide-react/components/editor-and-pdf'
  18. import MultipleSelectionPane from '@/features/ide-react/components/editor/multiple-selection-pane'
  19. import NoSelectionPane from '@/features/ide-react/components/editor/no-selection-pane'
  20. import { useIdeReactContext } from '@/features/ide-react/context/ide-react-context'
  21. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  22. import NoOpenDocPane from '@/features/ide-react/components/editor/no-open-doc-pane'
  23. import { debugConsole } from '@/utils/debugging'
  24. import { HistorySidebar } from '@/features/ide-react/components/history-sidebar'
  25. import { BinaryFile } from '@/features/file-view/types/binary-file'
  26. import useScopeValue from '@/shared/hooks/use-scope-value'
  27. type EditorAndSidebarProps = {
  28. shouldPersistLayout: boolean
  29. leftColumnDefaultSize: number
  30. setLeftColumnDefaultSize: React.Dispatch<React.SetStateAction<number>>
  31. }
  32. function convertFileRefToBinaryFile(fileRef: FileRef): BinaryFile {
  33. return {
  34. _id: fileRef._id,
  35. name: fileRef.name,
  36. id: fileRef._id,
  37. type: 'file',
  38. selected: true,
  39. linkedFileData: fileRef.linkedFileData,
  40. created: fileRef.created ? new Date(fileRef.created) : new Date(),
  41. }
  42. }
  43. // `FileViewHeader`, which is TypeScript, expects a BinaryFile, which has a
  44. // `created` property of type `Date`, while `TPRFileViewInfo`, written in JS,
  45. // into which `FileViewHeader` passes its BinaryFile, expects a file object with
  46. // `created` property of type `string`, which is a mismatch. `TPRFileViewInfo`
  47. // is the only one making runtime complaints and it seems that other uses of
  48. // `FileViewHeader` pass in a string for `created`, so that's what this function
  49. // does too.
  50. function fileViewFile(fileRef: FileRef) {
  51. return {
  52. ...convertFileRefToBinaryFile(fileRef),
  53. created: fileRef.created,
  54. }
  55. }
  56. export function EditorAndSidebar({
  57. shouldPersistLayout,
  58. leftColumnDefaultSize,
  59. setLeftColumnDefaultSize,
  60. }: EditorAndSidebarProps) {
  61. const [leftColumnIsOpen, setLeftColumnIsOpen] = useState(true)
  62. const { rootDocId } = useProjectContext()
  63. const { eventEmitter } = useIdeReactContext()
  64. const {
  65. openDocId: openDocWithId,
  66. currentDocumentId: openDocId,
  67. openInitialDoc,
  68. } = useEditorManagerContext()
  69. const { view } = useLayoutContext()
  70. const { projectJoined } = useIdeReactContext()
  71. const [, setOpenFile] = useScopeValue<BinaryFile | null>('openFile')
  72. const historyIsOpen = view === 'history'
  73. const [openEntity, setOpenEntity] = useState<
  74. FileTreeDocumentFindResult | FileTreeFileRefFindResult | null
  75. >(null)
  76. const [selectedEntityCount, setSelectedEntityCount] = useState(0)
  77. const [fileTreeReady, setFileTreeReady] = useState(false)
  78. const handleFileTreeInit = useCallback(() => {
  79. setFileTreeReady(true)
  80. }, [])
  81. const handleFileTreeSelect = useCallback(
  82. (selectedEntities: FileTreeFindResult[]) => {
  83. debugConsole.log('File tree selection changed', selectedEntities)
  84. setSelectedEntityCount(selectedEntities.length)
  85. if (selectedEntities.length !== 1) {
  86. setOpenEntity(null)
  87. return
  88. }
  89. const [selected] = selectedEntities
  90. if (selected.type === 'folder') {
  91. return
  92. }
  93. setOpenEntity(selected)
  94. if (selected.type === 'doc' && fileTreeReady) {
  95. openDocWithId(selected.entity._id)
  96. }
  97. // Keep openFile scope value in sync with the file tree
  98. const openFile =
  99. selected.type === 'fileRef'
  100. ? convertFileRefToBinaryFile(selected.entity)
  101. : null
  102. setOpenFile(openFile)
  103. if (openFile) {
  104. window.dispatchEvent(new CustomEvent('file-view:file-opened'))
  105. }
  106. },
  107. [fileTreeReady, setOpenFile, openDocWithId]
  108. )
  109. const handleFileTreeDelete: FileTreeDeleteHandler = useCallback(
  110. entity => {
  111. eventEmitter.emit('entity:deleted', entity)
  112. // Select the root document if the current document was deleted
  113. if (entity.entity._id === openDocId) {
  114. openDocWithId(rootDocId)
  115. }
  116. },
  117. [eventEmitter, openDocId, openDocWithId, rootDocId]
  118. )
  119. // Synchronize the file tree when openDoc or openDocId is called on the editor
  120. // manager context from elsewhere. If the file tree does change, it will
  121. // trigger the onSelect handler in this component, which will update the local
  122. // state.
  123. useEffect(() => {
  124. debugConsole.log(`openDocId changed to ${openDocId}`)
  125. if (openDocId === null) {
  126. return
  127. }
  128. window.dispatchEvent(
  129. new CustomEvent('editor.openDoc', { detail: openDocId })
  130. )
  131. }, [openDocId])
  132. // Open a document once the file tree and project are ready
  133. const initialOpenDoneRef = useRef(false)
  134. useEffect(() => {
  135. if (fileTreeReady && projectJoined && !initialOpenDoneRef.current) {
  136. initialOpenDoneRef.current = true
  137. openInitialDoc(rootDocId)
  138. }
  139. }, [fileTreeReady, openInitialDoc, projectJoined, rootDocId])
  140. // Keep the editor file tree around so that it is available and up to date
  141. // when restoring a file
  142. const leftColumnContent = (
  143. <>
  144. <EditorSidebar
  145. shouldShow={!historyIsOpen}
  146. shouldPersistLayout={shouldPersistLayout}
  147. onFileTreeInit={handleFileTreeInit}
  148. onFileTreeSelect={handleFileTreeSelect}
  149. onFileTreeDelete={handleFileTreeDelete}
  150. />
  151. {historyIsOpen ? <HistorySidebar /> : null}
  152. </>
  153. )
  154. let rightColumnContent
  155. if (historyIsOpen) {
  156. rightColumnContent = (
  157. <HistoryProvider>
  158. <History />
  159. </HistoryProvider>
  160. )
  161. } else {
  162. let editorContent = null
  163. // Always have the editor mounted when not in history view, and hide and
  164. // show it as necessary
  165. const editorPane = (
  166. <EditorPane
  167. shouldPersistLayout={shouldPersistLayout}
  168. show={openEntity?.type === 'doc' && selectedEntityCount === 1}
  169. />
  170. )
  171. if (openDocId === undefined) {
  172. rightColumnContent = <NoOpenDocPane />
  173. } else if (selectedEntityCount === 0) {
  174. rightColumnContent = (
  175. <>
  176. {editorPane}
  177. <NoSelectionPane />
  178. </>
  179. )
  180. } else if (selectedEntityCount > 1) {
  181. editorContent = (
  182. <>
  183. {editorPane}
  184. <MultipleSelectionPane selectedEntityCount={selectedEntityCount} />
  185. </>
  186. )
  187. } else if (openEntity) {
  188. editorContent =
  189. openEntity.type === 'doc' ? (
  190. editorPane
  191. ) : (
  192. <>
  193. {editorPane}
  194. <FileView file={fileViewFile(openEntity.entity)} />
  195. </>
  196. )
  197. }
  198. if (editorContent) {
  199. rightColumnContent = (
  200. <EditorAndPdf
  201. editorContent={editorContent}
  202. shouldPersistLayout={shouldPersistLayout}
  203. />
  204. )
  205. }
  206. }
  207. return (
  208. <TwoColumnMainContent
  209. leftColumnId="editor-left-column"
  210. leftColumnContent={leftColumnContent}
  211. leftColumnDefaultSize={leftColumnDefaultSize}
  212. setLeftColumnDefaultSize={setLeftColumnDefaultSize}
  213. rightColumnContent={rightColumnContent}
  214. leftColumnIsOpen={leftColumnIsOpen}
  215. setLeftColumnIsOpen={setLeftColumnIsOpen}
  216. shouldPersistLayout={shouldPersistLayout}
  217. />
  218. )
  219. }