local-compile-context.tsx 27 KB

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