local-compile-context.tsx 26 KB

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