local-compile-context.tsx 26 KB

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