outline-context.tsx 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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 { isValidTeXFile } from '@/main/is-valid-tex-file'
  14. import localStorage from '@/infrastructure/local-storage'
  15. import { useProjectContext } from '@/shared/context/project-context'
  16. import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  17. import { useFileTreeOpenContext } from './file-tree-open-context'
  18. import { useEditorAnalytics } from '@/shared/hooks/use-editor-analytics'
  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 { sendEvent } = useEditorAnalytics()
  53. const goToLineEmitter = useScopeEventEmitter('editor:gotoLine', true)
  54. useEventListener(
  55. 'file-view:file-opened',
  56. useCallback(() => {
  57. setBinaryFileOpened(true)
  58. }, [])
  59. )
  60. useEventListener(
  61. 'scroll:editor:update',
  62. useCallback(
  63. (evt: CustomEvent) => {
  64. if (ignoreNextScroll) {
  65. setIgnoreNextScroll(false)
  66. return
  67. }
  68. setCurrentlyHighlightedLine(evt.detail + 1)
  69. },
  70. [ignoreNextScroll]
  71. )
  72. )
  73. useEventListener(
  74. 'cursor:editor:update',
  75. useCallback(
  76. (evt: CustomEvent) => {
  77. if (ignoreNextCursorUpdate) {
  78. setIgnoreNextCursorUpdate(false)
  79. return
  80. }
  81. setCurrentlyHighlightedLine(evt.detail.row + 1)
  82. },
  83. [ignoreNextCursorUpdate]
  84. )
  85. )
  86. useEventListener(
  87. 'doc:after-opened',
  88. useCallback((evt: CustomEvent) => {
  89. if (evt.detail.isNewDoc) {
  90. setIgnoreNextCursorUpdate(true)
  91. }
  92. setBinaryFileOpened(false)
  93. setIgnoreNextScroll(true)
  94. }, [])
  95. )
  96. const jumpToLine = useCallback(
  97. (lineNumber: number, syncToPdf: boolean) => {
  98. setIgnoreNextScroll(true)
  99. goToLineEmitter({
  100. gotoLine: lineNumber,
  101. gotoColumn: 0,
  102. syncToPdf,
  103. })
  104. sendEvent('outline-jump-to-line')
  105. },
  106. [goToLineEmitter, sendEvent]
  107. )
  108. const highlightedLine = useMemo(
  109. () =>
  110. closestSectionLineNumber(flatOutline?.items, currentlyHighlightedLine),
  111. [flatOutline, currentlyHighlightedLine]
  112. )
  113. const { openDocName } = useEditorOpenDocContext()
  114. const isTexFile = useMemo(
  115. () => (openDocName ? isValidTeXFile(openDocName) : false),
  116. [openDocName]
  117. )
  118. const { selectedEntityCount } = useFileTreeOpenContext()
  119. const hasSingleEntityOpen = selectedEntityCount === 1
  120. const { projectId } = useProjectContext()
  121. const storageKey = `file_outline.expanded.${projectId}`
  122. const [outlineExpanded, setOutlineExpanded] = useState(
  123. () => localStorage.getItem(storageKey) !== false
  124. )
  125. const canShowOutline = hasSingleEntityOpen && isTexFile && !binaryFileOpened
  126. const expandOutline = useCallback(() => {
  127. if (canShowOutline) {
  128. localStorage.setItem(storageKey, true)
  129. sendEvent('outline-expand')
  130. setOutlineExpanded(true)
  131. }
  132. }, [canShowOutline, storageKey, sendEvent])
  133. const collapseOutline = useCallback(() => {
  134. if (canShowOutline) {
  135. localStorage.setItem(storageKey, false)
  136. sendEvent('outline-collapse')
  137. setOutlineExpanded(false)
  138. }
  139. }, [canShowOutline, storageKey, sendEvent])
  140. const toggleOutlineExpanded = useCallback(() => {
  141. if (outlineExpanded) {
  142. collapseOutline()
  143. } else {
  144. expandOutline()
  145. }
  146. }, [collapseOutline, expandOutline, outlineExpanded])
  147. const value = useMemo(
  148. () => ({
  149. flatOutline,
  150. setFlatOutline,
  151. highlightedLine,
  152. jumpToLine,
  153. canShowOutline,
  154. outlineExpanded,
  155. toggleOutlineExpanded,
  156. expandOutline,
  157. collapseOutline,
  158. }),
  159. [
  160. flatOutline,
  161. highlightedLine,
  162. jumpToLine,
  163. canShowOutline,
  164. outlineExpanded,
  165. toggleOutlineExpanded,
  166. expandOutline,
  167. collapseOutline,
  168. ]
  169. )
  170. return (
  171. <OutlineContext.Provider value={value}>{children}</OutlineContext.Provider>
  172. )
  173. }
  174. export const useOutlineContext = () => {
  175. const context = useContext(OutlineContext)
  176. if (!context) {
  177. throw new Error(
  178. 'useOutlineProvider is only available inside OutlineProvider'
  179. )
  180. }
  181. return context
  182. }
  183. const closestSectionLineNumber = (
  184. outline: { line: number }[] | undefined,
  185. lineNumber: number
  186. ): number => {
  187. if (!outline) {
  188. return -1
  189. }
  190. let highestLine = -1
  191. for (const section of outline) {
  192. if (section.line > lineNumber) {
  193. return highestLine
  194. }
  195. highestLine = section.line
  196. }
  197. return highestLine
  198. }