diff --git a/services/web/frontend/js/features/pdf-preview/hooks/use-compile-triggers.js b/services/web/frontend/js/features/pdf-preview/hooks/use-compile-triggers.js index f60ae98a2e..e7dd52fcaf 100644 --- a/services/web/frontend/js/features/pdf-preview/hooks/use-compile-triggers.js +++ b/services/web/frontend/js/features/pdf-preview/hooks/use-compile-triggers.js @@ -20,7 +20,11 @@ export const startCompileKeypress = event => { } } -export default function useCompileTriggers(startCompile, setChangedAt) { +export default function useCompileTriggers( + startCompile, + setChangedAt, + setSavedAt +) { const handleKeyDown = useCallback( event => { if (startCompileKeypress(event)) { @@ -54,5 +58,16 @@ export default function useCompileTriggers(startCompile, setChangedAt) { setOrTriggerChangedAt(Date.now()) }, [setOrTriggerChangedAt]) useEventListener('doc:changed', setChangedAtHandler) - useEventListener('doc:saved', setChangedAtHandler) // TODO: store this separately? + + // record when the server acknowledges saving changes + const setOrTriggerSavedAt = useDetachAction( + 'set-saved-at', + setSavedAt, + 'detacher', + 'detached' + ) + const setSavedAtHandler = useCallback(() => { + setOrTriggerSavedAt(Date.now()) + }, [setOrTriggerSavedAt]) + useEventListener('doc:saved', setSavedAtHandler) } diff --git a/services/web/frontend/js/features/pdf-preview/util/compiler.js b/services/web/frontend/js/features/pdf-preview/util/compiler.js index 0ae0753ec3..a5a2c757b7 100644 --- a/services/web/frontend/js/features/pdf-preview/util/compiler.js +++ b/services/web/frontend/js/features/pdf-preview/util/compiler.js @@ -20,6 +20,7 @@ export default class DocumentCompiler { projectId, rootDocId, setChangedAt, + setSavedAt, setCompiling, setData, setFirstRenderDone, @@ -32,6 +33,7 @@ export default class DocumentCompiler { this.projectId = projectId this.rootDocId = rootDocId this.setChangedAt = setChangedAt + this.setSavedAt = setSavedAt this.setCompiling = setCompiling this.setData = setData this.setFirstRenderDone = setFirstRenderDone @@ -83,6 +85,7 @@ export default class DocumentCompiler { try { // reset values this.setChangedAt(0) // TODO: wait for doc:saved? + this.setSavedAt(0) this.validationIssues = undefined window.dispatchEvent(new CustomEvent('flush-changes')) // TODO: wait for this? diff --git a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts index e9512055d9..c17493a237 100644 --- a/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts +++ b/services/web/frontend/js/features/source-editor/hooks/use-codemirror-scope.ts @@ -56,7 +56,8 @@ function useCodeMirrorScope(view: EditorView) { // set up scope listeners - const { logEntryAnnotations, uncompiled, compiling } = useCompileContext() + const { logEntryAnnotations, editedSinceCompileStarted, compiling } = + useCompileContext() const [loadingThreads] = useScopeValue('loadingThreads') @@ -394,7 +395,7 @@ function useCodeMirrorScope(view: EditorView) { // the project "changed at" date is reset at the start of the compile, i.e. "the project hasn't changed", // but we don't want to display the compile log diagnostics from the previous compile. const enableCompileLogLinter = - !syntaxValidation || (!uncompiled && !compiling) + !syntaxValidation || (!editedSinceCompileStarted && !compiling) // store enableCompileLogLinter in a ref for use in useEffect const enableCompileLogLinterRef = useRef(enableCompileLogLinter) diff --git a/services/web/frontend/js/shared/context/detach-compile-context.js b/services/web/frontend/js/shared/context/detach-compile-context.js index e3320a37d5..01e37576de 100644 --- a/services/web/frontend/js/shared/context/detach-compile-context.js +++ b/services/web/frontend/js/shared/context/detach-compile-context.js @@ -30,6 +30,7 @@ export function DetachCompileProvider({ children }) { compiling: _compiling, deliveryLatencies: _deliveryLatencies, draft: _draft, + editedSinceCompileStarted: _editedSinceCompileStarted, error: _error, fileList: _fileList, forceNewDomainVariant: _forceNewDomainVariant, @@ -69,6 +70,7 @@ export function DetachCompileProvider({ children }) { startCompile: _startCompile, stopCompile: _stopCompile, setChangedAt: _setChangedAt, + setSavedAt: _setSavedAt, clearCache: _clearCache, } = localCompileContext @@ -224,6 +226,12 @@ export function DetachCompileProvider({ children }) { 'detacher', 'detached' ) + const [editedSinceCompileStarted] = useDetachStateWatcher( + 'editedSinceCompileStarted', + _editedSinceCompileStarted, + 'detacher', + 'detached' + ) const [validationIssues] = useDetachStateWatcher( 'validationIssues', _validationIssues, @@ -345,6 +353,12 @@ export function DetachCompileProvider({ children }) { 'detached', 'detacher' ) + const setSavedAt = useDetachAction( + 'setSavedAt', + _setSavedAt, + 'detached', + 'detacher' + ) const clearCache = useDetachAction( 'clearCache', _clearCache, @@ -352,7 +366,7 @@ export function DetachCompileProvider({ children }) { 'detacher' ) - useCompileTriggers(startCompile, setChangedAt) + useCompileTriggers(startCompile, setChangedAt, setSavedAt) useEffect(() => { // Sync the split test variant across the editor and pdf-detach. const variants = getMeta('ol-splitTestVariants') || {} @@ -370,6 +384,7 @@ export function DetachCompileProvider({ children }) { compiling, deliveryLatencies, draft, + editedSinceCompileStarted, error, fileList, forceNewDomainVariant, @@ -423,6 +438,7 @@ export function DetachCompileProvider({ children }) { deliveryLatencies, draft, error, + editedSinceCompileStarted, fileList, forceNewDomainVariant, hasChanges, diff --git a/services/web/frontend/js/shared/context/local-compile-context.js b/services/web/frontend/js/shared/context/local-compile-context.js index c557459624..312ce14bba 100644 --- a/services/web/frontend/js/shared/context/local-compile-context.js +++ b/services/web/frontend/js/shared/context/local-compile-context.js @@ -123,9 +123,13 @@ export function LocalCompileProvider({ children }) { setPdfUrl(pdfFile?.pdfUrl) }, [pdfFile, setPdfDownloadUrl, setPdfUrl]) - // the project is considered to be "uncompiled" if a doc has changed since the last compile started + // the project is considered to be "uncompiled" if a doc has changed, or finished saving, since the last compile started. const [uncompiled, setUncompiled] = useScopeValue('pdf.uncompiled') + // whether a doc has been edited since the last compile started + const [editedSinceCompileStarted, setEditedSinceCompileStarted] = + useState(false) + // the id of the CLSI server which ran the compile const [clsiServerId, setClsiServerId] = useState() @@ -223,9 +227,12 @@ export function LocalCompileProvider({ children }) { // whether syntax validation is enabled globally const [syntaxValidation] = useScopeValue('settings.syntaxValidation') - // the timestamp that a doc was last changed or saved + // the timestamp that a doc was last changed const [changedAt, setChangedAt] = useState(0) + // the timestamp that a doc was last saved + const [savedAt, setSavedAt] = useState(0) + const { signal } = useAbortController() const cleanupCompileResult = useCallback(() => { @@ -246,6 +253,7 @@ export function LocalCompileProvider({ children }) { projectId, rootDocId, setChangedAt, + setSavedAt, setCompiling, setData, setFirstRenderDone, @@ -272,10 +280,13 @@ export function LocalCompileProvider({ children }) { compiler.setOption('stopOnFirstError', stopOnFirstError) }, [compiler, stopOnFirstError]) - // pass the "uncompiled" value up into the scope for use outside this context provider useEffect(() => { - setUncompiled(changedAt > 0) - }, [setUncompiled, changedAt]) + setUncompiled(changedAt > 0 || savedAt > 0) + }, [setUncompiled, changedAt, savedAt]) + + useEffect(() => { + setEditedSinceCompileStarted(changedAt > 0) + }, [setEditedSinceCompileStarted, changedAt]) // always compile the PDF once after opening the project, after the doc has loaded useEffect(() => { @@ -468,13 +479,13 @@ export function LocalCompileProvider({ children }) { // call the debounced autocompile function if the project is available for auto-compiling and it has changed useEffect(() => { if (canAutoCompile) { - if (changedAt > 0) { + if (changedAt > 0 || savedAt > 0) { compiler.debouncedAutoCompile() } } else { compiler.debouncedAutoCompile.cancel() } - }, [compiler, canAutoCompile, changedAt]) + }, [compiler, canAutoCompile, changedAt, savedAt]) // cancel debounced recompile on unmount useEffect(() => { @@ -533,6 +544,7 @@ export function LocalCompileProvider({ children }) { compiling, deliveryLatencies, draft, + editedSinceCompileStarted, error, fileList, forceNewDomainVariant, @@ -573,6 +585,7 @@ export function LocalCompileProvider({ children }) { validationIssues, firstRenderDone, setChangedAt, + setSavedAt, cleanupCompileResult, }), [ @@ -585,6 +598,7 @@ export function LocalCompileProvider({ children }) { compiling, deliveryLatencies, draft, + editedSinceCompileStarted, error, fileList, forceNewDomainVariant, @@ -620,6 +634,7 @@ export function LocalCompileProvider({ children }) { validationIssues, firstRenderDone, setChangedAt, + setSavedAt, cleanupCompileResult, setShowLogs, toggleLogs,