mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2025-07-29 05:00:06 +02:00

* Refactor project context to not use scope store * Fix Cypress tests for project context changes * Fix frontend React Testing Library tests for project context changes * Remove redundant code * Fix some project types in tests * Remove unused import and fix a type * Throw an error if updating the project in the project context before joining the project * Fix some review panel tests * Remove unused imports GitOrigin-RevId: 2f0c928b651f387aa980c29aef7d1ba0649790a7
309 lines
8.8 KiB
TypeScript
309 lines
8.8 KiB
TypeScript
import { useCallback, useEffect, useState, useRef } from 'react'
|
|
import { useProjectContext } from '../../../shared/context/project-context'
|
|
import { getJSON } from '../../../infrastructure/fetch-json'
|
|
import { useDetachCompileContext as useCompileContext } from '../../../shared/context/detach-compile-context'
|
|
import useIsMounted from '../../../shared/hooks/use-is-mounted'
|
|
import useAbortController from '../../../shared/hooks/use-abort-controller'
|
|
import useDetachState from '../../../shared/hooks/use-detach-state'
|
|
import useDetachAction from '../../../shared/hooks/use-detach-action'
|
|
import localStorage from '../../../infrastructure/local-storage'
|
|
import { useFileTreeData } from '../../../shared/context/file-tree-data-context'
|
|
import useScopeEventListener from '../../../shared/hooks/use-scope-event-listener'
|
|
import * as eventTracking from '../../../infrastructure/event-tracking'
|
|
import { debugConsole } from '@/utils/debugging'
|
|
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
|
|
import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
|
|
import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
|
|
import useEventListener from '@/shared/hooks/use-event-listener'
|
|
import { CursorPosition } from '@/features/ide-react/types/cursor-position'
|
|
import { isValidTeXFile } from '@/main/is-valid-tex-file'
|
|
import { PdfScrollPosition } from '@/shared/hooks/use-pdf-scroll-position'
|
|
import { showFileErrorToast } from '@/features/pdf-preview/components/synctex-toasts'
|
|
import { sendMB } from '@/infrastructure/event-tracking'
|
|
|
|
export default function useSynctex(): {
|
|
syncToPdf: () => void
|
|
syncToCode: ({ visualOffset }: { visualOffset?: number }) => void
|
|
syncToPdfInFlight: boolean
|
|
syncToCodeInFlight: boolean
|
|
canSyncToPdf: boolean
|
|
} {
|
|
const { projectId, project } = useProjectContext()
|
|
const rootDocId = project?.rootDocId
|
|
|
|
const { clsiServerId, pdfFile, position, setShowLogs, setHighlights } =
|
|
useCompileContext()
|
|
|
|
const { selectedEntities } = useFileTreeData()
|
|
const { findEntityByPath, dirname, pathInFolder } = useFileTreePathContext()
|
|
const { openDocName } = useEditorOpenDocContext()
|
|
const { getCurrentDocumentId, openDocWithId } = useEditorManagerContext()
|
|
|
|
const [cursorPosition, setCursorPosition] = useState<CursorPosition | null>(
|
|
() => {
|
|
const position = localStorage.getItem(
|
|
`doc.position.${getCurrentDocumentId()}`
|
|
)
|
|
return position ? position.cursorPosition : null
|
|
}
|
|
)
|
|
|
|
const isMounted = useIsMounted()
|
|
|
|
const { signal } = useAbortController()
|
|
|
|
useEventListener(
|
|
'cursor:editor:update',
|
|
useCallback((event: CustomEvent) => setCursorPosition(event.detail), [])
|
|
)
|
|
|
|
const [syncToPdfInFlight, setSyncToPdfInFlight] = useState(false)
|
|
const [syncToCodeInFlight, setSyncToCodeInFlight] = useDetachState(
|
|
'sync-to-code-inflight',
|
|
false,
|
|
'detacher',
|
|
'detached'
|
|
)
|
|
|
|
const getCurrentFilePath = useCallback(() => {
|
|
const docId = getCurrentDocumentId()
|
|
|
|
if (!docId || !rootDocId) {
|
|
return null
|
|
}
|
|
|
|
let path = pathInFolder(docId)
|
|
|
|
if (!path) {
|
|
return null
|
|
}
|
|
|
|
// If the root file is folder/main.tex, then synctex sees the path as folder/./main.tex
|
|
const rootDocDirname = dirname(rootDocId)
|
|
|
|
if (rootDocDirname) {
|
|
path = path.replace(RegExp(`^${rootDocDirname}`), `${rootDocDirname}/.`)
|
|
}
|
|
|
|
return path
|
|
}, [dirname, getCurrentDocumentId, pathInFolder, rootDocId])
|
|
|
|
const goToCodeLine = useCallback(
|
|
(file?: string, line?: number) => {
|
|
if (file) {
|
|
const doc = findEntityByPath(file)?.entity
|
|
if (doc) {
|
|
openDocWithId(doc._id, {
|
|
gotoLine: line,
|
|
})
|
|
return
|
|
}
|
|
}
|
|
showFileErrorToast()
|
|
},
|
|
[findEntityByPath, openDocWithId]
|
|
)
|
|
|
|
const goToPdfLocation = useCallback(
|
|
(params: string) => {
|
|
setSyncToPdfInFlight(true)
|
|
|
|
if (clsiServerId) {
|
|
params += `&clsiserverid=${clsiServerId}`
|
|
}
|
|
if (pdfFile?.editorId) params += `&editorId=${pdfFile.editorId}`
|
|
if (pdfFile?.build) params += `&buildId=${pdfFile.build}`
|
|
|
|
getJSON(`/project/${projectId}/sync/code?${params}`, { signal })
|
|
.then(data => {
|
|
setShowLogs(false)
|
|
setHighlights(data.pdf)
|
|
if (data.downloadedFromCache) {
|
|
sendMB('synctex-downloaded-from-cache', {
|
|
projectId,
|
|
method: 'code',
|
|
})
|
|
}
|
|
})
|
|
.catch(debugConsole.error)
|
|
.finally(() => {
|
|
if (isMounted.current) {
|
|
setSyncToPdfInFlight(false)
|
|
}
|
|
})
|
|
},
|
|
[
|
|
pdfFile,
|
|
clsiServerId,
|
|
isMounted,
|
|
projectId,
|
|
setShowLogs,
|
|
setHighlights,
|
|
setSyncToPdfInFlight,
|
|
signal,
|
|
]
|
|
)
|
|
|
|
const cursorPositionRef = useRef(cursorPosition)
|
|
|
|
useEffect(() => {
|
|
cursorPositionRef.current = cursorPosition
|
|
}, [cursorPosition])
|
|
|
|
const syncToPdf = useCallback(() => {
|
|
const file = getCurrentFilePath()
|
|
|
|
if (cursorPositionRef.current) {
|
|
const { row, column } = cursorPositionRef.current
|
|
|
|
const params = new URLSearchParams({
|
|
file: file ?? '',
|
|
line: String(row + 1),
|
|
column: String(column),
|
|
}).toString()
|
|
|
|
eventTracking.sendMB('jump-to-location', {
|
|
direction: 'code-location-in-pdf',
|
|
method: 'arrow',
|
|
})
|
|
|
|
goToPdfLocation(params)
|
|
}
|
|
}, [getCurrentFilePath, goToPdfLocation])
|
|
|
|
useScopeEventListener(
|
|
'cursor:editor:syncToPdf',
|
|
useCallback(() => {
|
|
syncToPdf()
|
|
}, [syncToPdf])
|
|
)
|
|
|
|
const positionRef = useRef(position)
|
|
useEffect(() => {
|
|
positionRef.current = position
|
|
}, [position])
|
|
|
|
const _syncToCode = useCallback(
|
|
({
|
|
position = positionRef.current,
|
|
visualOffset = 0,
|
|
}: {
|
|
position?: PdfScrollPosition
|
|
visualOffset?: number
|
|
}) => {
|
|
if (!position) {
|
|
return
|
|
}
|
|
|
|
setSyncToCodeInFlight(true)
|
|
// FIXME: this actually works better if it's halfway across the
|
|
// page (or the visible part of the page). Synctex doesn't
|
|
// always find the right place in the file when the point is at
|
|
// the edge of the page, it sometimes returns the start of the
|
|
// next paragraph instead.
|
|
const h = position.offset.left
|
|
|
|
// Compute the vertical position to pass to synctex, which
|
|
// works with coordinates increasing from the top of the page
|
|
// down. This matches the browser's DOM coordinate of the
|
|
// click point, but the pdf position is measured from the
|
|
// bottom of the page so we need to invert it.
|
|
let v = 0
|
|
if (position.pageSize?.height) {
|
|
v += position.pageSize.height - position.offset.top // measure from pdf point (inverted)
|
|
} else {
|
|
v += position.offset.top // measure from html click position
|
|
}
|
|
v += visualOffset
|
|
|
|
const params = new URLSearchParams({
|
|
page: position.page + 1,
|
|
h: h.toFixed(2),
|
|
v: v.toFixed(2),
|
|
})
|
|
|
|
if (clsiServerId) {
|
|
params.set('clsiserverid', clsiServerId)
|
|
}
|
|
if (pdfFile?.editorId) params.set('editorId', pdfFile.editorId)
|
|
if (pdfFile?.build) params.set('buildId', pdfFile.build)
|
|
|
|
getJSON(`/project/${projectId}/sync/pdf?${params}`, { signal })
|
|
.then(data => {
|
|
const [{ file, line }] = data.code
|
|
goToCodeLine(file, line)
|
|
if (data.downloadedFromCache) {
|
|
sendMB('synctex-downloaded-from-cache', {
|
|
projectId,
|
|
method: 'pdf',
|
|
})
|
|
}
|
|
})
|
|
.catch(debugConsole.error)
|
|
.finally(() => {
|
|
if (isMounted.current) {
|
|
setSyncToCodeInFlight(false)
|
|
}
|
|
})
|
|
},
|
|
[
|
|
pdfFile,
|
|
clsiServerId,
|
|
projectId,
|
|
signal,
|
|
isMounted,
|
|
setSyncToCodeInFlight,
|
|
goToCodeLine,
|
|
]
|
|
)
|
|
|
|
const syncToCode = useDetachAction(
|
|
'sync-to-code',
|
|
_syncToCode,
|
|
'detached',
|
|
'detacher'
|
|
)
|
|
|
|
useEventListener(
|
|
'synctex:sync-to-position',
|
|
useCallback(
|
|
(event: CustomEvent) => syncToCode({ position: event.detail }),
|
|
[syncToCode]
|
|
)
|
|
)
|
|
|
|
const [hasSingleSelectedDoc, setHasSingleSelectedDoc] = useDetachState(
|
|
'has-single-selected-doc',
|
|
false,
|
|
'detacher',
|
|
'detached'
|
|
)
|
|
|
|
useEffect(() => {
|
|
if (selectedEntities.length !== 1) {
|
|
setHasSingleSelectedDoc(false)
|
|
return
|
|
}
|
|
|
|
if (selectedEntities[0].type !== 'doc') {
|
|
setHasSingleSelectedDoc(false)
|
|
return
|
|
}
|
|
|
|
setHasSingleSelectedDoc(true)
|
|
}, [selectedEntities, setHasSingleSelectedDoc])
|
|
|
|
const canSyncToPdf: boolean =
|
|
hasSingleSelectedDoc &&
|
|
cursorPosition &&
|
|
openDocName &&
|
|
isValidTeXFile(openDocName)
|
|
|
|
return {
|
|
syncToCode,
|
|
syncToPdf,
|
|
syncToPdfInFlight,
|
|
syncToCodeInFlight,
|
|
canSyncToPdf,
|
|
}
|
|
}
|