editor-and-sidebar.tsx 8.1 KB

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