2023-12-15 04:19:42 -05:00
|
|
|
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<SetStateAction<FlatOutlineState>>
|
|
|
|
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<FlatOutlineState>(undefined)
|
|
|
|
const [currentlyHighlightedLine, setCurrentlyHighlightedLine] =
|
|
|
|
useState<number>(-1)
|
|
|
|
const [binaryFileOpened, setBinaryFileOpened] = useState<boolean>(false)
|
|
|
|
const [ignoreNextCursorUpdate, setIgnoreNextCursorUpdate] =
|
|
|
|
useState<boolean>(false)
|
|
|
|
const [ignoreNextScroll, setIgnoreNextScroll] = useState<boolean>(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 => {
|
2024-05-16 04:29:58 -04:00
|
|
|
if (evt.detail.isNewDoc) {
|
2023-12-15 04:19:42 -05:00
|
|
|
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<string | null>('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 (
|
|
|
|
<OutlineContext.Provider value={value}>{children}</OutlineContext.Provider>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|