[clsi] address zod violations (#36199)

* [editor-core] gracefully handle no-op TextOperation

* [web] avoid sending sync to pdf requests without a ?file

* [validation-tools] log deeper nested violations

* [editor-core] fix round trip of EditNoOperation

* [web] use custom error for sync to pdf w/o file, fix tests

* [validation-tools] include request params in log msg for violation

* Revert "[editor-core] gracefully handle no-op TextOperation"

This reverts commit c1d37261d5db72b37dbf39428c54edcfb5423f41.

* [web] gracefully handle rejected sync to pdf from context menu

GitOrigin-RevId: 21de7dc96b23575b0d67b6eb0c68ed9c2f86292b
This commit is contained in:
Jakob Ackermann
2026-09-17 11:48:12 +00:00
committed by Copybot
parent 761d4efc1f
commit 731ed50f78
14 changed files with 254 additions and 16 deletions
@@ -583,7 +583,7 @@ const _CompileController = {
async proxySyncCode(req, res) {
const { file, line, column } = req.query
if (file == null) {
if (!file) {
throw new Error('missing file parameter')
}
// Check that we are dealing with a simple file path (this is not
@@ -2066,6 +2066,7 @@
"syncing": "",
"synctex_error_recompile_and_try_again": "",
"synctex_failed": "",
"synctex_switch_files_and_try_again": "",
"syntax_checks": "",
"syntax_validation": "",
"tab_connecting": "",
@@ -28,6 +28,12 @@ export const SynctexRequestErrorToast = () => {
return <span>{t('synctex_error_recompile_and_try_again')}</span>
}
export const SynctexNoFileToast = () => {
const { t } = useTranslation()
return <span>{t('synctex_switch_files_and_try_again')}</span>
}
const generators: GlobalToastGeneratorEntry[] = [
{
key: 'synctex:file-error',
@@ -49,6 +55,16 @@ const generators: GlobalToastGeneratorEntry[] = [
isDismissible: true,
}),
},
{
key: 'synctex:no-file-error',
generator: () => ({
content: <SynctexNoFileToast />,
type: 'warning',
autoHide: true,
delay: 4000,
isDismissible: true,
}),
},
]
export default generators
@@ -72,3 +88,13 @@ export const showSynctexRequestErrorToast = () => {
})
)
}
export const showSynctexNoFileToast = () => {
window.dispatchEvent(
new CustomEvent('ide:show-toast', {
detail: {
key: 'synctex:no-file-error',
},
})
)
}
@@ -18,11 +18,12 @@ import { isValidTeXFile } from '@/main/is-valid-tex-file'
import { PdfScrollPosition } from '@/shared/hooks/use-pdf-scroll-position'
import {
showFileErrorToast,
showSynctexNoFileToast,
showSynctexRequestErrorToast,
} from '@/features/pdf-preview/components/synctex-toasts'
export default function useSynctex(): {
syncToPdf: () => void
syncToPdf: () => boolean
syncToCode: ({ visualOffset }: { visualOffset?: number }) => void
syncToPdfInFlight: boolean
syncToCodeInFlight: boolean
@@ -150,18 +151,25 @@ export default function useSynctex(): {
const syncToPdf = useCallback(() => {
const file = getCurrentFilePath()
if (!file) {
showSynctexNoFileToast()
return false
}
if (cursorPositionRef.current) {
const { row, column } = cursorPositionRef.current
const params = new URLSearchParams({
file: file ?? '',
file,
line: String(row + 1),
column: String(column),
}).toString()
goToPdfLocation(params)
return true
}
return false
}, [getCurrentFilePath, goToPdfLocation])
useEventListener(
@@ -221,10 +221,14 @@ export const useContextMenuItems = () => {
method: 'editor-context-menu',
direction: 'code-location-in-pdf',
})
requestedPdfSyncRef.current = true
syncToPdf()
view.focus()
}, [syncToPdf, view, changeLayout, isEditorOnly])
if (syncToPdf()) {
requestedPdfSyncRef.current = true
view.focus()
} else {
closeMenu()
}
}, [syncToPdf, view, changeLayout, isEditorOnly, closeMenu])
return {
closeMenu,
+1
View File
@@ -2651,6 +2651,7 @@
"syncing": "Syncing",
"synctex_error_recompile_and_try_again": "That didn’t work. Recompile and try again.",
"synctex_failed": "Couldn’t find the corresponding source file",
"synctex_switch_files_and_try_again": "That didn’t work. Try switching files and try again.",
"syntax_checks": "Syntax checks",
"syntax_validation": "Code check",
"tab_connecting": "Connecting with the editor",
@@ -1,4 +1,5 @@
import PdfSynctexControls from '../../../../frontend/js/features/pdf-preview/components/pdf-synctex-controls'
import { GlobalToasts } from '../../../../frontend/js/features/ide-react/components/global-toasts'
import { cloneDeep } from 'lodash'
import { useDetachCompileContext as useCompileContext } from '../../../../frontend/js/shared/context/detach-compile-context'
import { useFileTreeData } from '../../../../frontend/js/shared/context/file-tree-data-context'
@@ -76,11 +77,11 @@ const WithSelectedEntities = ({
return null
}
function mockProviders() {
function mockProviders(currentDocumentId: string | null = '_root_doc_id') {
return {
EditorOpenDocProvider: makeEditorOpenDocProvider({
openDocName: 'main.tex',
currentDocumentId: null,
currentDocumentId: currentDocumentId as any,
currentDocument: {
doc_id: 'test-doc',
getSnapshot: () => 'some doc content',
@@ -162,6 +163,50 @@ describe('<PdfSynctexControls/>', function () {
)
})
it('shows an error toast and does not sync when there is no resolvable file path', function () {
cy.interceptCompile()
const scope = mockScope()
const providers = mockProviders(null)
cy.intercept(
{ pathname: '/project/*/sync/code' },
cy.spy().as('syncCodeRequest')
)
cy.mount(
<EditorProviders scope={scope} providers={providers}>
<WithPosition mockPosition={mockPosition} />
<WithSelectedEntities mockSelectedEntities={mockSelectedEntities} />
<PdfSynctexControls />
<GlobalToasts />
</EditorProviders>
)
cy.waitForCompile()
// mock editor cursor position update
cy.window().then(win => {
win.dispatchEvent(
new CustomEvent('cursor:editor:update', {
detail: { row: 100, column: 10 },
})
)
})
cy.findByRole('button', { name: 'Go to code location in PDF' }).click()
cy.findByText(
'That didn’t work. Try switching files and try again.'
).should('exist')
cy.findByRole('button', { name: 'Go to code location in PDF' }).should(
'not.be.disabled'
)
cy.get('@syncCodeRequest').should('not.have.been.called')
})
it('disables button when multiple entities are selected', function () {
cy.interceptCompile()
@@ -65,6 +65,34 @@ const MockFileTreeDataProvider: FC<React.PropsWithChildren> = ({
value={
{
selectedEntities: [{ type: 'doc', id: '_root_doc_id' } as any],
fileTreeData: {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [{ _id: docId, name: 'test.tex' }],
folders: [],
fileRefs: [],
},
} as any
}
>
{children}
</FileTreeDataContext.Provider>
)
const MockEmptyFileTreeDataProvider: FC<React.PropsWithChildren> = ({
children,
}) => (
<FileTreeDataContext.Provider
value={
{
selectedEntities: [{ type: 'doc', id: '_root_doc_id' } as any],
fileTreeData: {
_id: 'root-folder-id',
name: 'rootFolder',
docs: [],
folders: [],
fileRefs: [],
},
} as any
}
>
@@ -1008,6 +1036,46 @@ describe('editor context menu', { scrollBehavior: false }, function () {
})
})
it('should close the menu and show an error toast when there is no resolvable file path', function () {
const scope = mockScope()
cy.intercept(
'GET',
'/project/*/sync/code*',
cy.spy().as('syncToPdfRequest')
)
cy.mount(
<TestContainer>
<EditorProviders
scope={scope}
providers={{
DetachCompileProvider: MockDetachCompileProvider,
FileTreeDataProvider: MockEmptyFileTreeDataProvider,
}}
>
<GlobalToasts />
<CodeMirrorEditor />
</EditorProviders>
</TestContainer>
)
cy.get('.cm-line').eq(10).rightclick()
cy.findByRole('menu').within(() => {
cy.findByRole('menuitem', { name: /jump to location in pdf/i }).click()
})
cy.findByRole('menu').should('not.exist')
cy.get('.global-toasts').should(
'contain.text',
'That didn’t work. Try switching files and try again.'
)
cy.get('@syncToPdfRequest').should('not.have.been.called')
})
it('should hide button when visual preview is enabled', function () {
window.metaAttributesCache.set('ol-splitTestVariants', {
'editor-context-menu': 'enabled',