outline-context.tsx 5.3 KB

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