outline-context.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import {
  2. createContext,
  3. Dispatch,
  4. FC,
  5. SetStateAction,
  6. useCallback,
  7. useContext,
  8. useMemo,
  9. useState,
  10. } from 'react'
  11. import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter'
  12. import useEventListener from '@/shared/hooks/use-event-listener'
  13. import * as eventTracking from '@/infrastructure/event-tracking'
  14. import { isValidTeXFile } from '@/main/is-valid-tex-file'
  15. import localStorage from '@/infrastructure/local-storage'
  16. import { useProjectContext } from '@/shared/context/project-context'
  17. import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  18. import { useFileTreeOpenContext } from './file-tree-open-context'
  19. export type PartialFlatOutline = {
  20. level: number
  21. title: string
  22. line: number
  23. }[]
  24. export type FlatOutlineState =
  25. | {
  26. items: PartialFlatOutline
  27. partial: boolean
  28. }
  29. | undefined
  30. const OutlineContext = createContext<
  31. | {
  32. flatOutline: FlatOutlineState
  33. setFlatOutline: Dispatch<SetStateAction<FlatOutlineState>>
  34. highlightedLine: number
  35. jumpToLine: (lineNumber: number, syncToPdf: boolean) => void
  36. canShowOutline: boolean
  37. outlineExpanded: boolean
  38. toggleOutlineExpanded: () => void
  39. expandOutline: () => void
  40. collapseOutline: () => void
  41. }
  42. | undefined
  43. >(undefined)
  44. export const OutlineProvider: FC<React.PropsWithChildren> = ({ children }) => {
  45. const [flatOutline, setFlatOutline] = useState<FlatOutlineState>(undefined)
  46. const [currentlyHighlightedLine, setCurrentlyHighlightedLine] =
  47. useState<number>(-1)
  48. const [binaryFileOpened, setBinaryFileOpened] = useState<boolean>(false)
  49. const [ignoreNextCursorUpdate, setIgnoreNextCursorUpdate] =
  50. useState<boolean>(false)
  51. const [ignoreNextScroll, setIgnoreNextScroll] = useState<boolean>(false)
  52. const goToLineEmitter = useScopeEventEmitter('editor:gotoLine', true)
  53. useEventListener(
  54. 'file-view:file-opened',
  55. useCallback(() => {
  56. setBinaryFileOpened(true)
  57. }, [])
  58. )
  59. useEventListener(
  60. 'scroll:editor:update',
  61. useCallback(
  62. (evt: CustomEvent) => {
  63. if (ignoreNextScroll) {
  64. setIgnoreNextScroll(false)
  65. return
  66. }
  67. setCurrentlyHighlightedLine(evt.detail + 1)
  68. },
  69. [ignoreNextScroll]
  70. )
  71. )
  72. useEventListener(
  73. 'cursor:editor:update',
  74. useCallback(
  75. (evt: CustomEvent) => {
  76. if (ignoreNextCursorUpdate) {
  77. setIgnoreNextCursorUpdate(false)
  78. return
  79. }
  80. setCurrentlyHighlightedLine(evt.detail.row + 1)
  81. },
  82. [ignoreNextCursorUpdate]
  83. )
  84. )
  85. useEventListener(
  86. 'doc:after-opened',
  87. useCallback((evt: CustomEvent) => {
  88. if (evt.detail.isNewDoc) {
  89. setIgnoreNextCursorUpdate(true)
  90. }
  91. setBinaryFileOpened(false)
  92. setIgnoreNextScroll(true)
  93. }, [])
  94. )
  95. const jumpToLine = useCallback(
  96. (lineNumber: number, syncToPdf: boolean) => {
  97. setIgnoreNextScroll(true)
  98. goToLineEmitter({
  99. gotoLine: lineNumber,
  100. gotoColumn: 0,
  101. syncToPdf,
  102. })
  103. eventTracking.sendMB('outline-jump-to-line')
  104. },
  105. [goToLineEmitter]
  106. )
  107. const highlightedLine = useMemo(
  108. () =>
  109. closestSectionLineNumber(flatOutline?.items, currentlyHighlightedLine),
  110. [flatOutline, currentlyHighlightedLine]
  111. )
  112. const { openDocName } = useEditorOpenDocContext()
  113. const isTexFile = useMemo(
  114. () => (openDocName ? isValidTeXFile(openDocName) : false),
  115. [openDocName]
  116. )
  117. const { selectedEntityCount } = useFileTreeOpenContext()
  118. const hasSingleEntityOpen = selectedEntityCount === 1
  119. const { projectId } = useProjectContext()
  120. const storageKey = `file_outline.expanded.${projectId}`
  121. const [outlineExpanded, setOutlineExpanded] = useState(
  122. () => localStorage.getItem(storageKey) !== false
  123. )
  124. const canShowOutline = hasSingleEntityOpen && isTexFile && !binaryFileOpened
  125. const expandOutline = useCallback(() => {
  126. if (canShowOutline) {
  127. localStorage.setItem(storageKey, true)
  128. eventTracking.sendMB('outline-expand')
  129. setOutlineExpanded(true)
  130. }
  131. }, [canShowOutline, storageKey])
  132. const collapseOutline = useCallback(() => {
  133. if (canShowOutline) {
  134. localStorage.setItem(storageKey, false)
  135. eventTracking.sendMB('outline-collapse')
  136. setOutlineExpanded(false)
  137. }
  138. }, [canShowOutline, storageKey])
  139. const toggleOutlineExpanded = useCallback(() => {
  140. if (outlineExpanded) {
  141. collapseOutline()
  142. } else {
  143. expandOutline()
  144. }
  145. }, [collapseOutline, expandOutline, outlineExpanded])
  146. const value = useMemo(
  147. () => ({
  148. flatOutline,
  149. setFlatOutline,
  150. highlightedLine,
  151. jumpToLine,
  152. canShowOutline,
  153. outlineExpanded,
  154. toggleOutlineExpanded,
  155. expandOutline,
  156. collapseOutline,
  157. }),
  158. [
  159. flatOutline,
  160. highlightedLine,
  161. jumpToLine,
  162. canShowOutline,
  163. outlineExpanded,
  164. toggleOutlineExpanded,
  165. expandOutline,
  166. collapseOutline,
  167. ]
  168. )
  169. return (
  170. <OutlineContext.Provider value={value}>{children}</OutlineContext.Provider>
  171. )
  172. }
  173. export const useOutlineContext = () => {
  174. const context = useContext(OutlineContext)
  175. if (!context) {
  176. throw new Error(
  177. 'useOutlineProvider is only available inside OutlineProvider'
  178. )
  179. }
  180. return context
  181. }
  182. const closestSectionLineNumber = (
  183. outline: { line: number }[] | undefined,
  184. lineNumber: number
  185. ): number => {
  186. if (!outline) {
  187. return -1
  188. }
  189. let highestLine = -1
  190. for (const section of outline) {
  191. if (section.line > lineNumber) {
  192. return highestLine
  193. }
  194. highestLine = section.line
  195. }
  196. return highestLine
  197. }