outline-context.tsx 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  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 useScopeValue from '@/shared/hooks/use-scope-value'
  15. import { isValidTeXFile } from '@/main/is-valid-tex-file'
  16. import localStorage from '@/infrastructure/local-storage'
  17. import { useProjectContext } from '@/shared/context/project-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. }
  39. | undefined
  40. >(undefined)
  41. export const OutlineProvider: FC = ({ children }) => {
  42. const [flatOutline, setFlatOutline] = useState<FlatOutlineState>(undefined)
  43. const [currentlyHighlightedLine, setCurrentlyHighlightedLine] =
  44. useState<number>(-1)
  45. const [binaryFileOpened, setBinaryFileOpened] = useState<boolean>(false)
  46. const [ignoreNextCursorUpdate, setIgnoreNextCursorUpdate] =
  47. useState<boolean>(false)
  48. const [ignoreNextScroll, setIgnoreNextScroll] = useState<boolean>(false)
  49. const goToLineEmitter = useScopeEventEmitter('editor:gotoLine', true)
  50. useEventListener(
  51. 'file-view:file-opened',
  52. useCallback(_ => {
  53. setBinaryFileOpened(true)
  54. }, [])
  55. )
  56. useEventListener(
  57. 'scroll:editor:update',
  58. useCallback(
  59. evt => {
  60. if (ignoreNextScroll) {
  61. setIgnoreNextScroll(false)
  62. return
  63. }
  64. setCurrentlyHighlightedLine(evt.detail + 1)
  65. },
  66. [ignoreNextScroll]
  67. )
  68. )
  69. useEventListener(
  70. 'cursor:editor:update',
  71. useCallback(
  72. evt => {
  73. if (ignoreNextCursorUpdate) {
  74. setIgnoreNextCursorUpdate(false)
  75. return
  76. }
  77. setCurrentlyHighlightedLine(evt.detail.row + 1)
  78. },
  79. [ignoreNextCursorUpdate]
  80. )
  81. )
  82. useEventListener(
  83. 'doc:after-opened',
  84. useCallback(evt => {
  85. if (evt.detail.isNewDoc) {
  86. setIgnoreNextCursorUpdate(true)
  87. }
  88. setBinaryFileOpened(false)
  89. setIgnoreNextScroll(true)
  90. }, [])
  91. )
  92. const jumpToLine = useCallback(
  93. (lineNumber: number, syncToPdf: boolean) => {
  94. setIgnoreNextScroll(true)
  95. goToLineEmitter({
  96. gotoLine: lineNumber,
  97. gotoColumn: 0,
  98. syncToPdf,
  99. })
  100. eventTracking.sendMB('outline-jump-to-line')
  101. },
  102. [goToLineEmitter]
  103. )
  104. const highlightedLine = useMemo(
  105. () =>
  106. closestSectionLineNumber(flatOutline?.items, currentlyHighlightedLine),
  107. [flatOutline, currentlyHighlightedLine]
  108. )
  109. const [docName] = useScopeValue<string | null>('editor.open_doc_name')
  110. const isTexFile = useMemo(
  111. () => (docName ? isValidTeXFile(docName) : false),
  112. [docName]
  113. )
  114. const { _id: projectId } = useProjectContext()
  115. const storageKey = `file_outline.expanded.${projectId}`
  116. const [outlineExpanded, setOutlineExpanded] = useState(
  117. () => localStorage.getItem(storageKey) !== false
  118. )
  119. const canShowOutline = isTexFile && !binaryFileOpened
  120. const toggleOutlineExpanded = useCallback(() => {
  121. if (canShowOutline) {
  122. localStorage.setItem(storageKey, !outlineExpanded)
  123. eventTracking.sendMB(
  124. outlineExpanded ? 'outline-collapse' : 'outline-expand'
  125. )
  126. setOutlineExpanded(!outlineExpanded)
  127. }
  128. }, [canShowOutline, outlineExpanded, storageKey])
  129. const value = useMemo(
  130. () => ({
  131. flatOutline,
  132. setFlatOutline,
  133. highlightedLine,
  134. jumpToLine,
  135. canShowOutline,
  136. outlineExpanded,
  137. toggleOutlineExpanded,
  138. }),
  139. [
  140. flatOutline,
  141. highlightedLine,
  142. jumpToLine,
  143. canShowOutline,
  144. outlineExpanded,
  145. toggleOutlineExpanded,
  146. ]
  147. )
  148. return (
  149. <OutlineContext.Provider value={value}>{children}</OutlineContext.Provider>
  150. )
  151. }
  152. export const useOutlineContext = () => {
  153. const context = useContext(OutlineContext)
  154. if (!context) {
  155. throw new Error(
  156. 'useOutlineProvider is only available inside OutlineProvider'
  157. )
  158. }
  159. return context
  160. }
  161. const closestSectionLineNumber = (
  162. outline: { line: number }[] | undefined,
  163. lineNumber: number
  164. ): number => {
  165. if (!outline) {
  166. return -1
  167. }
  168. let highestLine = -1
  169. for (const section of outline) {
  170. if (section.line > lineNumber) {
  171. return highestLine
  172. }
  173. highestLine = section.line
  174. }
  175. return highestLine
  176. }