local-compile-context.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. import {
  2. FC,
  3. createContext,
  4. useCallback,
  5. useContext,
  6. useEffect,
  7. useMemo,
  8. useRef,
  9. useState,
  10. Dispatch,
  11. SetStateAction,
  12. } from 'react'
  13. import usePersistedState from '../hooks/use-persisted-state'
  14. import useAbortController from '../hooks/use-abort-controller'
  15. import DocumentCompiler from '../../features/pdf-preview/util/compiler'
  16. import {
  17. send,
  18. sendMB,
  19. sendMBOnce,
  20. sendMBSampled,
  21. } from '../../infrastructure/event-tracking'
  22. import {
  23. buildLogEntryAnnotations,
  24. buildRuleCounts,
  25. buildRuleDeltas,
  26. handleLogFiles,
  27. handleOutputFiles,
  28. } from '@/features/pdf-preview/util/output-files'
  29. import { useProjectContext } from './project-context'
  30. import { useEditorContext } from './editor-context'
  31. import { buildFileList } from '../../features/pdf-preview/util/file-list'
  32. import { useLayoutContext } from './layout-context'
  33. import { useUserContext } from './user-context'
  34. import { useFileTreeData } from '@/shared/context/file-tree-data-context'
  35. import { useDetachContext } from '@/shared/context/detach-context'
  36. import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
  37. import { useRootDoc } from '@/shared/hooks/use-root-doc'
  38. import { useUserSettingsContext } from '@/shared/context/user-settings-context'
  39. import { useFeatureFlag } from '@/shared/context/split-test-context'
  40. import { useEditorManagerContext } from '@/features/ide-react/context/editor-manager-context'
  41. import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-open-doc-context'
  42. import { getJSON } from '@/infrastructure/fetch-json'
  43. import { CompileResponseData } from '../../../../types/compile'
  44. import {
  45. PdfScrollPosition,
  46. usePdfScrollPosition,
  47. } from '@/shared/hooks/use-pdf-scroll-position'
  48. import {
  49. DeliveryLatencies,
  50. HighlightData,
  51. LogEntry,
  52. PdfFileDataList,
  53. } from '@/features/pdf-preview/util/types'
  54. import { captureException } from '@/infrastructure/error-reporter'
  55. import OError from '@overleaf/o-error'
  56. import getMeta from '@/utils/meta'
  57. import type { Annotation } from '../../../../types/annotation'
  58. import { useProjectSettingsContext } from '@/features/editor-left-menu/context/project-settings-context'
  59. import {
  60. ActiveOverallTheme,
  61. useActiveOverallTheme,
  62. } from '../hooks/use-active-overall-theme'
  63. type PdfFile = Record<string, any>
  64. export type CompileContext = {
  65. autoCompile: boolean
  66. clearingCache: boolean
  67. clsiServerId?: string
  68. codeCheckFailed: boolean
  69. compiling: boolean
  70. deliveryLatencies: Record<string, any>
  71. draft: boolean
  72. error?: string
  73. fileList?: PdfFileDataList
  74. hasChanges: boolean
  75. hasShortCompileTimeout: boolean
  76. highlights?: HighlightData[]
  77. isProjectOwner: boolean
  78. logEntries?: {
  79. all: LogEntry[]
  80. errors: LogEntry[]
  81. warnings: LogEntry[]
  82. typesetting: LogEntry[]
  83. }
  84. logEntryAnnotations?: Record<string, Annotation[]>
  85. outputFilesArchive?: string
  86. pdfDownloadUrl?: string
  87. pdfFile?: PdfFile | null
  88. pdfUrl?: string
  89. pdfViewer?: string
  90. position?: PdfScrollPosition
  91. rawLog?: string
  92. setAutoCompile: (value: boolean) => void
  93. setDraft: (value: any) => void
  94. setError: (value: any) => void
  95. setHasLintingError: (value: boolean) => void // only for storybook
  96. setHighlights: (value: any) => void
  97. setPosition: Dispatch<SetStateAction<PdfScrollPosition>>
  98. setShowCompileTimeWarning: (value: any) => void
  99. setShowLogs: (value: boolean) => void
  100. toggleLogs: () => void
  101. setStopOnFirstError: (value: boolean) => void
  102. setStopOnValidationError: (value: boolean) => void
  103. showCompileTimeWarning: boolean
  104. showLogs: boolean
  105. stopOnFirstError: boolean
  106. stopOnValidationError: boolean
  107. stoppedOnFirstError: boolean
  108. uncompiled: boolean
  109. validationIssues?: Record<string, any>
  110. firstRenderDone: (metrics: {
  111. latencyFetch: number
  112. latencyRender: number | undefined
  113. pdfCachingMetrics: { viewerId: string }
  114. }) => void
  115. cleanupCompileResult?: () => void
  116. animateCompileDropdownArrow: boolean
  117. editedSinceCompileStarted: boolean
  118. lastCompileOptions: any
  119. setAnimateCompileDropdownArrow: (value: boolean) => void
  120. recompileFromScratch: () => void
  121. setCompiling: (value: boolean) => void
  122. startCompile: (options?: any) => Promise<void>
  123. stopCompile: () => void
  124. setChangedAt: (value: any) => void
  125. clearCache: () => void
  126. syncToEntry: (value: any, keepCurrentView?: boolean) => void
  127. recordAction: (action: string) => void
  128. darkModePdf: boolean | undefined
  129. setDarkModePdf: (value: boolean) => void
  130. activeOverallTheme: ActiveOverallTheme
  131. }
  132. export const LocalCompileContext = createContext<CompileContext | undefined>(
  133. undefined
  134. )
  135. export const LocalCompileProvider: FC<React.PropsWithChildren> = ({
  136. children,
  137. }) => {
  138. const { hasPremiumCompile, isProjectOwner } = useEditorContext()
  139. const { openDocWithId, openDocs } = useEditorManagerContext()
  140. const { currentDocument } = useEditorOpenDocContext()
  141. const { role } = useDetachContext()
  142. const { projectId, joinedOnce, project } = useProjectContext()
  143. const { rootDocId, imageName, compiler: compilerName } = project || {}
  144. const { pdfPreviewOpen } = useLayoutContext()
  145. const { features, alphaProgram } = useUserContext()
  146. const { fileTreeData } = useFileTreeData()
  147. const { findEntityByPath } = useFileTreePathContext()
  148. const getRootDocInfo = useRootDoc()
  149. // whether a compile is in progress
  150. const [compiling, setCompiling] = useState(false)
  151. // whether to show the compile time warning
  152. const [showCompileTimeWarning, setShowCompileTimeWarning] = useState(false)
  153. const [hasShortCompileTimeout, setHasShortCompileTimeout] = useState(false)
  154. // the log entries parsed from the compile output log
  155. const [logEntries, setLogEntries] = useState<CompileContext['logEntries']>()
  156. // annotations for display in the editor, built from the log entries
  157. const [logEntryAnnotations, setLogEntryAnnotations] = useState<
  158. undefined | Record<string, Annotation[]>
  159. >()
  160. // the PDF viewer and whether syntax validation is enabled globally
  161. const { userSettings } = useUserSettingsContext()
  162. const { pdfViewer, syntaxValidation } = userSettings
  163. // The active setting for dark mode PDF
  164. const { darkModePdf, setDarkModePdf } = useProjectSettingsContext()
  165. const activeOverallTheme = useActiveOverallTheme()
  166. // low level details for metrics
  167. const [pdfFile, setPdfFile] = useState<PdfFile | null | undefined>()
  168. // the project is considered to be "uncompiled" if a doc has changed, or finished saving, since the last compile started.
  169. const [uncompiled, setUncompiled] = useState(false)
  170. // whether a doc has been edited since the last compile started
  171. const [editedSinceCompileStarted, setEditedSinceCompileStarted] =
  172. useState(false)
  173. // the id of the CLSI server which ran the compile
  174. const [clsiServerId, setClsiServerId] = useState<string>()
  175. // data received in response to a compile request
  176. const [data, setData] = useState<CompileResponseData>()
  177. // the rootDocId used in the most recent compile request, which may not be the
  178. // same as the project rootDocId. This is used to calculate correct paths when
  179. // parsing the compile logs
  180. const lastCompileRootDocId = data ? (data.rootDocId ?? rootDocId) : null
  181. // callback to be invoked for PdfJsMetrics
  182. const [firstRenderDone, setFirstRenderDone] = useState(() => () => {})
  183. // latencies of compile/pdf download/rendering
  184. const [deliveryLatencies, setDeliveryLatencies] = useState<DeliveryLatencies>(
  185. {}
  186. )
  187. // whether the project has been compiled yet
  188. const [compiledOnce, setCompiledOnce] = useState(false)
  189. // fetch initial compile response from cache
  190. const [initialCompileFromCache, setInitialCompileFromCache] = useState(
  191. getMeta('ol-canUseClsiCache') &&
  192. // Avoid fetching the initial compile from cache in PDF detach tab
  193. role !== 'detached'
  194. )
  195. // fetch of initial compile from cache is pending
  196. const [pendingInitialCompileFromCache, setPendingInitialCompileFromCache] =
  197. useState(false)
  198. // Raw data from clsi-cache, will need post-processing and check settings
  199. const [dataFromCache, setDataFromCache] = useState<CompileResponseData>()
  200. // whether the cache is being cleared
  201. const [clearingCache, setClearingCache] = useState(false)
  202. // whether the logs should be visible
  203. const [showLogs, setShowLogs] = useState(false)
  204. // whether the compile dropdown arrow should be animated
  205. const [animateCompileDropdownArrow, setAnimateCompileDropdownArrow] =
  206. useState(false)
  207. const toggleLogs = useCallback(() => {
  208. setShowLogs(prev => {
  209. if (!prev) {
  210. sendMBOnce('ide-open-logs-once')
  211. }
  212. return !prev
  213. })
  214. }, [setShowLogs])
  215. // an error that occurred
  216. const [error, setError] = useState<string>()
  217. // the list of files that can be downloaded
  218. const [fileList, setFileList] = useState<PdfFileDataList>()
  219. // the raw contents of the log file
  220. const [rawLog, setRawLog] = useState<string>()
  221. // validation issues from CLSI
  222. const [validationIssues, setValidationIssues] = useState()
  223. // areas to highlight on the PDF, from synctex
  224. const [highlights, setHighlights] = useState()
  225. const [position, setPosition] = usePdfScrollPosition(lastCompileRootDocId)
  226. // whether autocompile is switched on
  227. const [autoCompile, setAutoCompile] = usePersistedState(
  228. `autocompile_enabled:${projectId}`,
  229. false,
  230. { listen: true }
  231. )
  232. // whether the compile should run in draft mode
  233. const [draft, setDraft] = usePersistedState(`draft:${projectId}`, false, {
  234. listen: true,
  235. })
  236. // whether compiling should stop on first error
  237. const [stopOnFirstError, setStopOnFirstError] = usePersistedState(
  238. `stop_on_first_error:${projectId}`,
  239. false,
  240. { listen: true }
  241. )
  242. // whether the last compiles stopped on first error
  243. const [stoppedOnFirstError, setStoppedOnFirstError] = useState(false)
  244. // whether compiling should be prevented if there are linting errors
  245. const [stopOnValidationError, setStopOnValidationError] = usePersistedState(
  246. `stop_on_validation_error:${projectId}`,
  247. true,
  248. { listen: true }
  249. )
  250. // whether the editor linter found errors
  251. const [hasLintingError, setHasLintingError] = useState(false)
  252. // the timestamp that a doc was last changed
  253. const [changedAt, setChangedAt] = useState(0)
  254. const { signal } = useAbortController()
  255. const cleanupCompileResult = useCallback(() => {
  256. setPdfFile(undefined)
  257. setLogEntries(undefined)
  258. setLogEntryAnnotations({})
  259. }, [setPdfFile, setLogEntries, setLogEntryAnnotations])
  260. const compilingRef = useRef(false)
  261. useEffect(() => {
  262. compilingRef.current = compiling
  263. }, [compiling])
  264. const _buildLogEntryAnnotations = useCallback(
  265. (entries: LogEntry[]) =>
  266. buildLogEntryAnnotations(entries, fileTreeData, lastCompileRootDocId),
  267. [fileTreeData, lastCompileRootDocId]
  268. )
  269. const buildLogEntryAnnotationsRef = useRef(_buildLogEntryAnnotations)
  270. useEffect(() => {
  271. buildLogEntryAnnotationsRef.current = _buildLogEntryAnnotations
  272. }, [_buildLogEntryAnnotations])
  273. // the document compiler
  274. const [compiler] = useState(() => {
  275. return new DocumentCompiler({
  276. projectId,
  277. setChangedAt,
  278. setCompiling,
  279. setData,
  280. setFirstRenderDone,
  281. setDeliveryLatencies,
  282. setError,
  283. cleanupCompileResult,
  284. compilingRef,
  285. signal,
  286. openDocs,
  287. getRootDocInfo,
  288. })
  289. })
  290. // keep the root doc lookup in sync with the compiler
  291. useEffect(() => {
  292. compiler.getRootDocInfo = getRootDocInfo
  293. }, [compiler, getRootDocInfo])
  294. // keep draft setting in sync with the compiler
  295. useEffect(() => {
  296. compiler.setOption('draft', draft)
  297. }, [compiler, draft])
  298. // keep stop on first error setting in sync with the compiler
  299. useEffect(() => {
  300. compiler.setOption('stopOnFirstError', stopOnFirstError)
  301. }, [compiler, stopOnFirstError])
  302. useEffect(() => {
  303. setUncompiled(changedAt > 0)
  304. }, [setUncompiled, changedAt])
  305. useEffect(() => {
  306. setEditedSinceCompileStarted(changedAt > 0)
  307. }, [setEditedSinceCompileStarted, changedAt])
  308. // try to fetch the last compile result after opening the project, potentially before joining the project.
  309. useEffect(() => {
  310. if (initialCompileFromCache && !pendingInitialCompileFromCache) {
  311. setPendingInitialCompileFromCache(true)
  312. getJSON(`/project/${projectId}/output/cached/output.overleaf.json`, {
  313. signal: AbortSignal.timeout(5_000),
  314. })
  315. .then((data: any) => {
  316. // Hand data over to next effect, it will wait for project/doc loading.
  317. setDataFromCache(data)
  318. })
  319. .catch(() => {
  320. // Let the isAutoCompileOnLoad effect take over
  321. setInitialCompileFromCache(false)
  322. setPendingInitialCompileFromCache(false)
  323. })
  324. }
  325. }, [projectId, initialCompileFromCache, pendingInitialCompileFromCache])
  326. // Maybe adopt the compile from cache
  327. useEffect(() => {
  328. if (!dataFromCache) return // no compile from cache available
  329. if (!joinedOnce) return // wait for joinProject, it populates the file-tree.
  330. if (!currentDocument) return // wait for current doc to load, it affects the rootDoc override
  331. if (compiledOnce) return // regular compile triggered
  332. // Gracefully access file-tree and getRootDocOverride
  333. let settingsUpToDate = false
  334. try {
  335. dataFromCache.rootDocId = findEntityByPath(
  336. dataFromCache.options?.rootResourcePath || ''
  337. )?.entity?._id
  338. settingsUpToDate =
  339. getRootDocInfo().rootDocId === dataFromCache.rootDocId &&
  340. dataFromCache.options.draft === draft &&
  341. // Allow stopOnFirstError to be enabled in the compile from cache and disabled locally.
  342. // Compiles that passed with stopOnFirstError=true will also pass with stopOnFirstError=false. The inverse does not hold, and we need to recompile.
  343. !!dataFromCache.options.stopOnFirstError >= stopOnFirstError
  344. } catch (err) {
  345. captureException(
  346. OError.tag(err as unknown as Error, 'validate compile options', {
  347. options: dataFromCache.options,
  348. })
  349. )
  350. }
  351. if (settingsUpToDate) {
  352. sendMB('compile-from-cache', { projectId })
  353. setData(dataFromCache)
  354. setCompiledOnce(true)
  355. }
  356. setDataFromCache(undefined)
  357. setInitialCompileFromCache(false)
  358. setPendingInitialCompileFromCache(false)
  359. }, [
  360. projectId,
  361. dataFromCache,
  362. joinedOnce,
  363. currentDocument,
  364. compiledOnce,
  365. findEntityByPath,
  366. getRootDocInfo,
  367. compilerName,
  368. imageName,
  369. stopOnFirstError,
  370. draft,
  371. ])
  372. // always compile the PDF once after opening the project, after the doc has loaded
  373. useEffect(() => {
  374. if (
  375. !compiledOnce &&
  376. currentDocument &&
  377. !initialCompileFromCache &&
  378. !pendingInitialCompileFromCache
  379. ) {
  380. setCompiledOnce(true)
  381. compiler.compile({ isAutoCompileOnLoad: true })
  382. }
  383. }, [
  384. compiledOnce,
  385. currentDocument,
  386. initialCompileFromCache,
  387. pendingInitialCompileFromCache,
  388. compiler,
  389. ])
  390. useEffect(() => {
  391. setHasShortCompileTimeout(
  392. features?.compileTimeout !== undefined && features.compileTimeout <= 60
  393. )
  394. }, [features])
  395. useEffect(() => {
  396. if (hasShortCompileTimeout && compiling && isProjectOwner) {
  397. const timeout = window.setTimeout(() => {
  398. setShowCompileTimeWarning(true)
  399. }, 30000)
  400. return () => {
  401. window.clearTimeout(timeout)
  402. }
  403. }
  404. }, [compiling, isProjectOwner, hasShortCompileTimeout])
  405. const hasCompileLogsEvents = useFeatureFlag('compile-log-events')
  406. // compare log entry counts with the previous compile, and record actions between compiles
  407. // these are refs rather than state so they don't trigger the effect to run
  408. const previousRuleCountsRef = useRef<{
  409. ruleCounts: Record<string, number>
  410. rootDocId: string | null | undefined
  411. } | null>(null)
  412. const recordedActionsRef = useRef<Record<string, boolean>>({})
  413. const recordAction = useCallback((action: string) => {
  414. recordedActionsRef.current[action] = true
  415. }, [])
  416. // handle the data returned from a compile request
  417. // note: this should _only_ run when `data` changes,
  418. // the other dependencies must all be static
  419. useEffect(() => {
  420. if (!joinedOnce) return // wait for joinProject, it populates the premium flags.
  421. const abortController = new AbortController()
  422. const recordedActions = recordedActionsRef.current
  423. recordedActionsRef.current = {}
  424. if (data) {
  425. if (data.clsiServerId) {
  426. setClsiServerId(data.clsiServerId) // set in scope, for PdfSynctexController
  427. }
  428. if (data.outputFiles) {
  429. const outputFiles = new Map()
  430. for (const outputFile of data.outputFiles) {
  431. // Use a shadow-copy, we will update it in place and append to .url.
  432. outputFiles.set(outputFile.path, { ...outputFile })
  433. }
  434. // set the PDF context
  435. if (data.status === 'success') {
  436. setPdfFile(handleOutputFiles(outputFiles, projectId, data))
  437. }
  438. setFileList(buildFileList(outputFiles, data))
  439. // handle log files
  440. // asynchronous (TODO: cancel on new compile?)
  441. setLogEntryAnnotations(undefined)
  442. setLogEntries(undefined)
  443. setRawLog(undefined)
  444. handleLogFiles(outputFiles, data, abortController.signal).then(
  445. (result: Record<string, any>) => {
  446. setRawLog(result.log)
  447. setLogEntries(result.logEntries)
  448. setLogEntryAnnotations(
  449. buildLogEntryAnnotationsRef.current(result.logEntries.all)
  450. )
  451. // sample compile stats for real users
  452. if (!alphaProgram) {
  453. if (['success', 'stopped-on-first-error'].includes(data.status)) {
  454. sendMBSampled(
  455. 'compile-result',
  456. {
  457. errors: result.logEntries.errors.length,
  458. warnings: result.logEntries.warnings.length,
  459. typesetting: result.logEntries.typesetting.length,
  460. newPdfPreview: true, // TODO: is this useful?
  461. stopOnFirstError: data.options.stopOnFirstError,
  462. },
  463. 0.01
  464. )
  465. }
  466. if (hasCompileLogsEvents) {
  467. const ruleCounts = buildRuleCounts(
  468. result.logEntries.all
  469. ) as Record<string, number>
  470. const rootDocId = data.rootDocId
  471. const previousRuleCounts = previousRuleCountsRef.current
  472. previousRuleCountsRef.current = { ruleCounts, rootDocId }
  473. const ruleDeltas =
  474. previousRuleCounts &&
  475. previousRuleCounts.rootDocId === rootDocId
  476. ? buildRuleDeltas(ruleCounts, previousRuleCounts.ruleCounts)
  477. : {}
  478. sendMB('compile-log-entries', {
  479. status: data.status,
  480. stopOnFirstError: data.options.stopOnFirstError,
  481. isAutoCompileOnLoad: !!data.options.isAutoCompileOnLoad,
  482. isAutoCompileOnChange: !!data.options.isAutoCompileOnChange,
  483. rootDocId,
  484. ...recordedActions,
  485. ...ruleCounts,
  486. ...ruleDeltas,
  487. })
  488. }
  489. }
  490. }
  491. )
  492. }
  493. switch (data.status) {
  494. case 'success':
  495. setError(undefined)
  496. setShowLogs(false)
  497. break
  498. case 'stopped-on-first-error':
  499. setError(undefined)
  500. setShowLogs(true)
  501. break
  502. case 'clsi-maintenance':
  503. case 'compile-in-progress':
  504. case 'exited':
  505. case 'failure':
  506. case 'project-too-large':
  507. case 'rate-limited':
  508. case 'terminated':
  509. case 'too-recently-compiled':
  510. setError(data.status)
  511. break
  512. case 'timedout':
  513. setError('timedout')
  514. if (!hasPremiumCompile && isProjectOwner) {
  515. send(
  516. 'subscription-funnel',
  517. 'editor-click-feature',
  518. 'compile-timeout'
  519. )
  520. }
  521. break
  522. case 'autocompile-backoff':
  523. if (!data.options.isAutoCompileOnLoad) {
  524. setError('autocompile-disabled')
  525. setAutoCompile(false)
  526. }
  527. break
  528. case 'unavailable':
  529. setError('clsi-unavailable')
  530. break
  531. case 'validation-problems':
  532. setError('validation-problems')
  533. setValidationIssues(data.validationProblems)
  534. break
  535. default:
  536. setError('error')
  537. break
  538. }
  539. setStoppedOnFirstError(data.status === 'stopped-on-first-error')
  540. }
  541. return () => {
  542. abortController.abort()
  543. }
  544. }, [
  545. joinedOnce,
  546. data,
  547. alphaProgram,
  548. features,
  549. hasCompileLogsEvents,
  550. hasPremiumCompile,
  551. isProjectOwner,
  552. projectId,
  553. setAutoCompile,
  554. setClsiServerId,
  555. setLogEntries,
  556. setLogEntryAnnotations,
  557. setPdfFile,
  558. ])
  559. // switch to logs if there's an error
  560. useEffect(() => {
  561. if (error) {
  562. setShowLogs(true)
  563. }
  564. }, [error])
  565. // whether there has been an autocompile linting error, if syntax validation is switched on
  566. const autoCompileLintingError = Boolean(
  567. autoCompile && syntaxValidation && hasLintingError
  568. )
  569. const codeCheckFailed = stopOnValidationError && autoCompileLintingError
  570. // the project is available for auto-compiling
  571. // (autocompile is enabled, the PDF preview is open, and the code check (if enabled) hasn't failed)
  572. const canAutoCompile = Boolean(
  573. autoCompile && pdfPreviewOpen && !codeCheckFailed
  574. )
  575. // show that the project has pending changes
  576. const hasChanges = Boolean(canAutoCompile && uncompiled && compiledOnce)
  577. // call the debounced autocompile function if the project is available for auto-compiling and it has changed
  578. useEffect(() => {
  579. if (canAutoCompile) {
  580. if (changedAt > 0) {
  581. compiler.debouncedAutoCompile()
  582. }
  583. } else {
  584. compiler.debouncedAutoCompile.cancel()
  585. }
  586. }, [compiler, canAutoCompile, changedAt])
  587. // cancel debounced recompile on unmount
  588. useEffect(() => {
  589. return () => {
  590. compiler.debouncedAutoCompile.cancel()
  591. }
  592. }, [compiler])
  593. // start a compile manually
  594. const startCompile = useCallback(
  595. (options: any) => {
  596. setCompiledOnce(true)
  597. return compiler.compile(options)
  598. },
  599. [compiler, setCompiledOnce]
  600. )
  601. // stop a compile manually
  602. const stopCompile = useCallback(() => {
  603. return compiler.stopCompile()
  604. }, [compiler])
  605. // clear the compile cache
  606. const clearCache = useCallback(() => {
  607. setClearingCache(true)
  608. return compiler
  609. .clearCache()
  610. .then(() => {
  611. setFileList(undefined)
  612. setPdfFile(undefined)
  613. })
  614. .finally(() => {
  615. setClearingCache(false)
  616. })
  617. }, [compiler])
  618. const syncToEntry = useCallback(
  619. (entry: any, keepCurrentView = false) => {
  620. const result = findEntityByPath(entry.file)
  621. if (result && result.type === 'doc') {
  622. openDocWithId(result.entity._id, {
  623. gotoLine: entry.line ?? undefined,
  624. gotoColumn: entry.column ?? undefined,
  625. keepCurrentView,
  626. })
  627. }
  628. },
  629. [findEntityByPath, openDocWithId]
  630. )
  631. // clear the cache then run a compile, triggered by a menu item
  632. const recompileFromScratch = useCallback(() => {
  633. clearCache().then(() => {
  634. compiler.compile()
  635. })
  636. }, [clearCache, compiler])
  637. // After a compile, the compiler sets `data.options` to the options that were
  638. // used for that compile.
  639. const lastCompileOptions = useMemo(() => data?.options || {}, [data])
  640. const value = useMemo(
  641. () => ({
  642. animateCompileDropdownArrow,
  643. autoCompile,
  644. clearCache,
  645. clearingCache,
  646. clsiServerId,
  647. codeCheckFailed,
  648. compiling,
  649. deliveryLatencies,
  650. draft,
  651. editedSinceCompileStarted,
  652. error,
  653. fileList,
  654. hasChanges,
  655. hasShortCompileTimeout,
  656. highlights,
  657. isProjectOwner,
  658. lastCompileOptions,
  659. logEntryAnnotations,
  660. logEntries,
  661. pdfDownloadUrl: pdfFile?.pdfDownloadUrl,
  662. pdfFile,
  663. pdfUrl: pdfFile?.pdfUrl,
  664. pdfViewer,
  665. position,
  666. rawLog,
  667. recompileFromScratch,
  668. setAnimateCompileDropdownArrow,
  669. setAutoCompile,
  670. setCompiling,
  671. setDraft,
  672. setError,
  673. setHasLintingError, // only for stories
  674. setHighlights,
  675. setPosition,
  676. showCompileTimeWarning,
  677. setShowCompileTimeWarning,
  678. setShowLogs,
  679. toggleLogs,
  680. setStopOnFirstError,
  681. setStopOnValidationError,
  682. showLogs,
  683. startCompile,
  684. stopCompile,
  685. stopOnFirstError,
  686. stopOnValidationError,
  687. stoppedOnFirstError,
  688. uncompiled,
  689. validationIssues,
  690. firstRenderDone,
  691. setChangedAt,
  692. cleanupCompileResult,
  693. syncToEntry,
  694. recordAction,
  695. darkModePdf,
  696. setDarkModePdf,
  697. activeOverallTheme,
  698. }),
  699. [
  700. animateCompileDropdownArrow,
  701. autoCompile,
  702. clearCache,
  703. clearingCache,
  704. clsiServerId,
  705. codeCheckFailed,
  706. compiling,
  707. deliveryLatencies,
  708. draft,
  709. editedSinceCompileStarted,
  710. error,
  711. fileList,
  712. hasChanges,
  713. hasShortCompileTimeout,
  714. highlights,
  715. isProjectOwner,
  716. lastCompileOptions,
  717. logEntries,
  718. logEntryAnnotations,
  719. position,
  720. pdfFile,
  721. pdfViewer,
  722. rawLog,
  723. recompileFromScratch,
  724. setAnimateCompileDropdownArrow,
  725. setAutoCompile,
  726. setDraft,
  727. setError,
  728. setHasLintingError, // only for stories
  729. setHighlights,
  730. setPosition,
  731. setShowCompileTimeWarning,
  732. setStopOnFirstError,
  733. setStopOnValidationError,
  734. showCompileTimeWarning,
  735. showLogs,
  736. startCompile,
  737. stopCompile,
  738. stopOnFirstError,
  739. stopOnValidationError,
  740. stoppedOnFirstError,
  741. uncompiled,
  742. validationIssues,
  743. firstRenderDone,
  744. setChangedAt,
  745. cleanupCompileResult,
  746. setShowLogs,
  747. toggleLogs,
  748. syncToEntry,
  749. recordAction,
  750. darkModePdf,
  751. setDarkModePdf,
  752. activeOverallTheme,
  753. ]
  754. )
  755. return (
  756. <LocalCompileContext.Provider value={value}>
  757. {children}
  758. </LocalCompileContext.Provider>
  759. )
  760. }
  761. export function useLocalCompileContext() {
  762. const context = useContext(LocalCompileContext)
  763. if (!context) {
  764. throw new Error(
  765. 'useLocalCompileContext is only available inside LocalCompileProvider'
  766. )
  767. }
  768. return context
  769. }