import { createContext, Dispatch, FC, SetStateAction, useCallback, useContext, useMemo, useState, } from 'react' import useScopeEventEmitter from '@/shared/hooks/use-scope-event-emitter' import useEventListener from '@/shared/hooks/use-event-listener' import * as eventTracking from '@/infrastructure/event-tracking' import useScopeValue from '@/shared/hooks/use-scope-value' import isValidTeXFile from '@/main/is-valid-tex-file' import localStorage from '@/infrastructure/local-storage' import { useProjectContext } from '@/shared/context/project-context' export type PartialFlatOutline = { level: number title: string line: number }[] export type FlatOutlineState = | { items: PartialFlatOutline partial: boolean } | undefined const OutlineContext = createContext< | { flatOutline: FlatOutlineState setFlatOutline: Dispatch> highlightedLine: number jumpToLine: (lineNumber: number, syncToPdf: boolean) => void canShowOutline: boolean outlineExpanded: boolean toggleOutlineExpanded: () => void } | undefined >(undefined) export const OutlineProvider: FC = ({ children }) => { const [flatOutline, setFlatOutline] = useState(undefined) const [currentlyHighlightedLine, setCurrentlyHighlightedLine] = useState(-1) const [binaryFileOpened, setBinaryFileOpened] = useState(false) const [ignoreNextCursorUpdate, setIgnoreNextCursorUpdate] = useState(false) const [ignoreNextScroll, setIgnoreNextScroll] = useState(false) const goToLineEmitter = useScopeEventEmitter('editor:gotoLine', true) useEventListener( 'file-view:file-opened', useCallback(_ => { setBinaryFileOpened(true) }, []) ) useEventListener( 'scroll:editor:update', useCallback( evt => { if (ignoreNextScroll) { setIgnoreNextScroll(false) return } setCurrentlyHighlightedLine(evt.detail + 1) }, [ignoreNextScroll] ) ) useEventListener( 'cursor:editor:update', useCallback( evt => { if (ignoreNextCursorUpdate) { setIgnoreNextCursorUpdate(false) return } setCurrentlyHighlightedLine(evt.detail.row + 1) }, [ignoreNextCursorUpdate] ) ) useEventListener( 'doc:after-opened', useCallback(evt => { if (evt.detail.isNewDoc) { setIgnoreNextCursorUpdate(true) } setBinaryFileOpened(false) setIgnoreNextScroll(true) }, []) ) const jumpToLine = useCallback( (lineNumber: number, syncToPdf: boolean) => { setIgnoreNextScroll(true) goToLineEmitter(lineNumber, 0, syncToPdf) eventTracking.sendMB('outline-jump-to-line') }, [goToLineEmitter] ) const highlightedLine = useMemo( () => closestSectionLineNumber(flatOutline?.items, currentlyHighlightedLine), [flatOutline, currentlyHighlightedLine] ) const [docName] = useScopeValue('editor.open_doc_name') const isTexFile = useMemo( () => (docName ? isValidTeXFile(docName) : false), [docName] ) const { _id: projectId } = useProjectContext() const storageKey = `file_outline.expanded.${projectId}` const [outlineExpanded, setOutlineExpanded] = useState( () => localStorage.getItem(storageKey) !== false ) const canShowOutline = isTexFile && !binaryFileOpened const toggleOutlineExpanded = useCallback(() => { if (canShowOutline) { localStorage.setItem(storageKey, !outlineExpanded) eventTracking.sendMB( outlineExpanded ? 'outline-collapse' : 'outline-expand' ) setOutlineExpanded(!outlineExpanded) } }, [canShowOutline, outlineExpanded, storageKey]) const value = useMemo( () => ({ flatOutline, setFlatOutline, highlightedLine, jumpToLine, canShowOutline, outlineExpanded, toggleOutlineExpanded, }), [ flatOutline, highlightedLine, jumpToLine, canShowOutline, outlineExpanded, toggleOutlineExpanded, ] ) return ( {children} ) } export const useOutlineContext = () => { const context = useContext(OutlineContext) if (!context) { throw new Error( 'useOutlineProvider is only available inside OutlineProvider' ) } return context } const closestSectionLineNumber = ( outline: { line: number }[] | undefined, lineNumber: number ): number => { if (!outline) { return -1 } let highestLine = -1 for (const section of outline) { if (section.line > lineNumber) { return highestLine } highestLine = section.line } return highestLine }