local-compile-context.tsx 26 KB

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