diff --git a/services/web/app/src/Features/Project/ProjectController.js b/services/web/app/src/Features/Project/ProjectController.js index 13923a46da..0fcdb35945 100644 --- a/services/web/app/src/Features/Project/ProjectController.js +++ b/services/web/app/src/Features/Project/ProjectController.js @@ -324,6 +324,7 @@ const _ProjectController = { const splitTests = [ !anonymous && 'bib-file-tpr-prompt', 'compile-log-events', + 'math-preview', 'null-test-share-modal', 'paywall-cta', 'pdf-caching-cached-url-lookup', @@ -673,6 +674,7 @@ const _ProjectController = { fontFamily: user.ace.fontFamily || 'lucida', lineHeight: user.ace.lineHeight || 'normal', overallTheme: user.ace.overallTheme, + mathPreview: user.ace.mathPreview, }, privilegeLevel, anonymous, diff --git a/services/web/app/src/Features/User/UserController.js b/services/web/app/src/Features/User/UserController.js index 227cef4e74..a0776a1813 100644 --- a/services/web/app/src/Features/User/UserController.js +++ b/services/web/app/src/Features/User/UserController.js @@ -372,6 +372,9 @@ async function updateUserSettings(req, res, next) { if (req.body.lineHeight != null) { user.ace.lineHeight = req.body.lineHeight } + if (req.body.mathPreview != null) { + user.ace.mathPreview = req.body.mathPreview + } await user.save() const newEmail = req.body.email?.trim().toLowerCase() diff --git a/services/web/app/src/models/User.js b/services/web/app/src/models/User.js index 12f9e6d675..69e9b900bd 100644 --- a/services/web/app/src/models/User.js +++ b/services/web/app/src/models/User.js @@ -86,6 +86,7 @@ const UserSchema = new Schema( syntaxValidation: { type: Boolean }, fontFamily: { type: String }, lineHeight: { type: String }, + mathPreview: { type: Boolean, default: true }, }, features: { collaborators: { diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json index 72c6c09301..cb55a49dd3 100644 --- a/services/web/frontend/extracted-translations.json +++ b/services/web/frontend/extracted-translations.json @@ -396,6 +396,7 @@ "enter_any_size_including_units_or_valid_latex_command": "", "enter_image_url": "", "enter_the_confirmation_code": "", + "equation_preview": "", "error": "", "error_opening_document": "", "error_opening_document_detail": "", diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx index 42c930cb40..90eb4ee0c1 100644 --- a/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx +++ b/services/web/frontend/js/features/editor-left-menu/components/settings-menu.tsx @@ -16,10 +16,13 @@ import SettingsOverallTheme from './settings/settings-overall-theme' import SettingsPdfViewer from './settings/settings-pdf-viewer' import SettingsSpellCheckLanguage from './settings/settings-spell-check-language' import SettingsSyntaxValidation from './settings/settings-syntax-validation' +import SettingsMathPreview from './settings/settings-math-preview' +import { useFeatureFlag } from '@/shared/context/split-test-context' export default function SettingsMenu() { const { t } = useTranslation() const anonymous = getMeta('ol-anonymous') + const enableMathPreview = useFeatureFlag('math-preview') if (anonymous) { return null @@ -37,6 +40,7 @@ export default function SettingsMenu() { + {enableMathPreview && } diff --git a/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx new file mode 100644 index 0000000000..12554ff758 --- /dev/null +++ b/services/web/frontend/js/features/editor-left-menu/components/settings/settings-math-preview.tsx @@ -0,0 +1,27 @@ +import { useTranslation } from 'react-i18next' +import { useProjectSettingsContext } from '../../context/project-settings-context' +import SettingsMenuSelect from './settings-menu-select' + +export default function SettingsMathPreview() { + const { t } = useTranslation() + const { mathPreview, setMathPreview } = useProjectSettingsContext() + + return ( + + ) +} diff --git a/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx b/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx index 422b9b7d1f..20aa62b2d6 100644 --- a/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx +++ b/services/web/frontend/js/features/editor-left-menu/context/project-settings-context.tsx @@ -26,6 +26,7 @@ type ProjectSettingsSetterContextValue = { setFontFamily: (fontFamily: UserSettings['fontFamily']) => void setLineHeight: (lineHeight: UserSettings['lineHeight']) => void setPdfViewer: (pdfViewer: UserSettings['pdfViewer']) => void + setMathPreview: (mathPreview: UserSettings['mathPreview']) => void } type ProjectSettingsContextValue = Partial & @@ -69,6 +70,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => { setLineHeight, pdfViewer, setPdfViewer, + mathPreview, + setMathPreview, } = useUserWideSettings() useProjectWideSettingsSocketListener() @@ -103,6 +106,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => { setLineHeight, pdfViewer, setPdfViewer, + mathPreview, + setMathPreview, }), [ compiler, @@ -133,6 +138,8 @@ export const ProjectSettingsProvider: FC = ({ children }) => { setLineHeight, pdfViewer, setPdfViewer, + mathPreview, + setMathPreview, ] ) diff --git a/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx b/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx index 058c018b88..70202c9446 100644 --- a/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx +++ b/services/web/frontend/js/features/editor-left-menu/hooks/use-user-wide-settings.tsx @@ -19,6 +19,7 @@ export default function useUserWideSettings() { fontFamily, lineHeight, pdfViewer, + mathPreview, } = userSettings const setOverallTheme = useSetOverallTheme() @@ -85,6 +86,13 @@ export default function useUserWideSettings() { [saveUserSettings] ) + const setMathPreview = useCallback( + (mathPreview: UserSettings['mathPreview']) => { + saveUserSettings('mathPreview', mathPreview) + }, + [saveUserSettings] + ) + return { autoComplete, setAutoComplete, @@ -106,5 +114,7 @@ export default function useUserWideSettings() { setLineHeight, pdfViewer, setPdfViewer, + mathPreview, + setMathPreview, } } diff --git a/services/web/frontend/js/features/source-editor/extensions/index.ts b/services/web/frontend/js/features/source-editor/extensions/index.ts index 68ddcf9d73..120734af0a 100644 --- a/services/web/frontend/js/features/source-editor/extensions/index.ts +++ b/services/web/frontend/js/features/source-editor/extensions/index.ts @@ -48,6 +48,7 @@ import { toolbarPanel } from './toolbar/toolbar-panel' import { geometryChangeEvent } from './geometry-change-event' import { docName } from './doc-name' import { fileTreeItemDrop } from './file-tree-item-drop' +import { mathPreview } from './math-preview' const moduleExtensions: Array<() => Extension> = importOverleafModules( 'sourceEditorExtensions' @@ -125,6 +126,7 @@ export const createExtensions = (options: Record): Extension[] => [ emptyLineFiller(), trackChanges(options.currentDoc, options.changeManager), visual(options.visual), + mathPreview(options.settings.mathPreview), toolbarPanel(), verticalOverflow(), highlightActiveLine(options.visual.visual), diff --git a/services/web/frontend/js/features/source-editor/extensions/math-preview.ts b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts new file mode 100644 index 0000000000..cce06f0fe1 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/extensions/math-preview.ts @@ -0,0 +1,159 @@ +import { + repositionTooltips, + showTooltip, + Tooltip, + ViewPlugin, +} from '@codemirror/view' +import { + Compartment, + EditorState, + Extension, + StateField, + TransactionSpec, +} from '@codemirror/state' +import { loadMathJax } from '../../mathjax/load-mathjax' +import { descendantsOfNodeWithType } from '../utils/tree-query' +import { + mathAncestorNode, + parseMathContainer, +} from '../utils/tree-operations/math' +import { documentCommands } from '../languages/latex/document-commands' +import { debugConsole } from '@/utils/debugging' +import { isSplitTestEnabled } from '@/utils/splitTestUtils' + +const REPOSITION_EVENT = 'editor:repositionMathTooltips' + +export const mathPreview = (enabled: boolean): Extension => { + if (!isSplitTestEnabled('math-preview')) { + return [] + } + + return mathPreviewConf.of(enabled ? mathPreviewStateField : []) +} + +const mathPreviewConf = new Compartment() + +export const setMathPreview = (enabled: boolean): TransactionSpec => ({ + effects: mathPreviewConf.reconfigure(enabled ? mathPreviewStateField : []), +}) + +const mathPreviewStateField = StateField.define({ + create: buildTooltips, + + update(tooltips, tr) { + if (tr.docChanged || tr.selection) { + tooltips = buildTooltips(tr.state) + } + + return tooltips + }, + + provide: field => [ + showTooltip.computeN([field], state => state.field(field)), + + ViewPlugin.define(view => { + const listener = () => repositionTooltips(view) + window.addEventListener(REPOSITION_EVENT, listener) + return { + destroy() { + window.removeEventListener(REPOSITION_EVENT, listener) + }, + } + }), + ], +}) + +const renderMath = async ( + content: string, + displayMode: boolean, + element: HTMLElement, + definitions: string +) => { + const MathJax = await loadMathJax() + + MathJax.texReset([0]) // equation numbering is disabled, but this is still needed + + try { + await MathJax.tex2svgPromise(definitions) + } catch { + // ignore errors thrown during parsing command definitions + } + + const math = await MathJax.tex2svgPromise(content, { + ...MathJax.getMetricsFor(element), + display: displayMode, + }) + element.textContent = '' + element.append(math) +} + +function buildTooltips(state: EditorState): readonly Tooltip[] { + const tooltips: Tooltip[] = [] + + for (const range of state.selection.ranges) { + if (range.empty) { + const pos = range.from + const content = buildTooltipContent(state, pos) + if (content) { + const tooltip: Tooltip = { + pos, + above: true, + arrow: false, + create() { + const dom = document.createElement('div') + dom.append(content) + dom.className = 'ol-cm-math-tooltip' + + return { dom, overlap: true, offset: { x: 0, y: 8 } } + }, + } + + tooltips.push(tooltip) + } + } + } + + return tooltips +} + +const buildTooltipContent = ( + state: EditorState, + pos: number +): HTMLDivElement | null => { + // if anywhere inside Math, render the whole Math content + const ancestorNode = mathAncestorNode(state, pos) + if (!ancestorNode) return null + + const [node] = descendantsOfNodeWithType(ancestorNode, 'Math', 'Math') + if (!node) return null + + const math = parseMathContainer(state, node, ancestorNode) + if (!math || !math.content.length) return null + + const element = document.createElement('div') + element.style.opacity = '0' + element.style.transition = 'opacity .01s ease-in' + element.textContent = math.content + + let definitions = '' + const commandState = state.field(documentCommands, false) + + if (commandState?.items) { + for (const command of commandState.items) { + if (command.type === 'definition' && command.raw) { + definitions += `${command.raw}\n` + } + } + } + + renderMath(math.content, math.displayMode, element, definitions) + .then(() => { + element.style.opacity = '1' + window.dispatchEvent(new Event(REPOSITION_EVENT)) + }) + .catch(error => { + debugConsole.error(error) + }) + + return element +} diff --git a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts index 51f08865f1..7e33049a52 100644 --- a/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts +++ b/services/web/frontend/js/features/source-editor/extensions/visual/atomic-decorations.ts @@ -82,6 +82,10 @@ import { createSpaceCommand, hasSpaceSubstitution, } from '@/features/source-editor/extensions/visual/visual-widgets/space' +import { + mathAncestorNode, + parseMathContainer, +} from '../../utils/tree-operations/math' type Options = { previewByPath: (path: string) => PreviewPath | null @@ -760,13 +764,7 @@ export const atomicDecorations = (options: Options) => { return false // no markup in input content } else if (nodeRef.type.is('Math')) { // math equations - let passToMathJax = true - - const ancestorNode = - ancestorNodeOfType(state, nodeRef.from, '$MathContainer') || - ancestorNodeOfType(state, nodeRef.from, 'EquationEnvironment') || - // NOTE: EquationArrayEnvironment can be nested inside EquationEnvironment - ancestorNodeOfType(state, nodeRef.from, 'EquationArrayEnvironment') + const ancestorNode = mathAncestorNode(state, nodeRef.from) if ( ancestorNode && @@ -774,57 +772,19 @@ export const atomicDecorations = (options: Options) => { ? shouldDecorateFromLineEdges(state, ancestorNode) : shouldDecorate(state, ancestorNode)) ) { - // the content of the Math element, without braces - const innerContent = state.doc - .sliceString(nodeRef.from, nodeRef.to) - .trim() + const math = parseMathContainer(state, nodeRef, ancestorNode) - // only replace when there's content inside the braces - if (innerContent.length) { - let content = innerContent - let displayMode = false - - if (ancestorNode.type.is('$Environment')) { - const environmentName = getEnvironmentName(ancestorNode, state) - if (environmentName) { - // use the outer content of environments that MathJax supports - // https://docs.mathjax.org/en/latest/input/tex/macros/index.html#environments - if (environmentName === 'tikzcd') { - passToMathJax = false - } - if ( - environmentName !== 'math' && - environmentName !== 'displaymath' - ) { - content = state.doc - .sliceString(ancestorNode.from, ancestorNode.to) - .trim() - } - - if (environmentName !== 'math') { - displayMode = true - } - } - } else { - if ( - ancestorNode.type.is('BracketMath') || - Boolean(ancestorNode.getChild('DisplayMath')) - ) { - displayMode = true - } - } - if (passToMathJax) { - decorations.push( - Decoration.replace({ - widget: new MathWidget( - content, - displayMode, - commandDefinitions - ), - block: displayMode, - }).range(ancestorNode.from, ancestorNode.to) - ) - } + if (math && math.passToMathJax) { + decorations.push( + Decoration.replace({ + widget: new MathWidget( + math.content, + math.displayMode, + commandDefinitions + ), + block: math.displayMode, + }).range(ancestorNode.from, ancestorNode.to) + ) } } 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 c83fe1c617..5b66a19c81 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 @@ -61,6 +61,7 @@ import { debugConsole } from '@/utils/debugging' import { useMetadataContext } from '@/features/ide-react/context/metadata-context' import { useUserContext } from '@/shared/context/user-context' import { useReferencesContext } from '@/features/ide-react/context/references-context' +import { setMathPreview } from '@/features/source-editor/extensions/math-preview' function useCodeMirrorScope(view: EditorView) { const { fileTreeData } = useFileTreeData() @@ -96,6 +97,7 @@ function useCodeMirrorScope(view: EditorView) { autoPairDelimiters, mode, syntaxValidation, + mathPreview, } = userSettings const [cursorHighlights] = useScopeValue>( @@ -153,6 +155,7 @@ function useCodeMirrorScope(view: EditorView) { autoPairDelimiters, mode, syntaxValidation, + mathPreview, }) const currentDocRef = useRef({ @@ -385,6 +388,11 @@ function useCodeMirrorScope(view: EditorView) { view.dispatch(setSyntaxValidation(syntaxValidation)) }, [view, syntaxValidation]) + useEffect(() => { + settingsRef.current.mathPreview = mathPreview + view.dispatch(setMathPreview(mathPreview)) + }, [view, mathPreview]) + const emitSyncToPdf = useScopeEventEmitter('cursor:editor:syncToPdf') const handleGoToLine = useCallback( diff --git a/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts b/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts index f54edebbcd..c37bee1cf0 100644 --- a/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts +++ b/services/web/frontend/js/features/source-editor/languages/latex/completions/doc-commands.ts @@ -46,6 +46,7 @@ const countCommandUsage = (context: CompletionContext) => { >() const commandListProjection = context.state.field(documentCommands) + if (!commandListProjection.items) { return result } diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts index f31516df94..5eb8ca2857 100644 --- a/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/commands.ts @@ -10,6 +10,8 @@ export class Command extends ProjectionItem { readonly title: string = '' readonly optionalArgCount: number = 0 readonly requiredArgCount: number = 0 + readonly type: 'usage' | 'definition' = 'usage' + readonly raw: string | undefined = undefined } /** @@ -62,16 +64,16 @@ export const enterNode = ( argCountNumber-- } - const thisCommand: Readonly = { + items.push({ line: state.doc.lineAt(node.from).number, title: commandNameText, from: node.from, to: node.to, optionalArgCount: commandDefinitionHasOptionalArgument ? 1 : 0, requiredArgCount: argCountNumber, - } - - items.push(thisCommand) + type: 'definition', + raw: state.sliceDoc(node.from, node.to), + }) } else if ( node.type.is('UnknownCommand') || node.type.is('KnownCommand') || @@ -112,7 +114,7 @@ export const enterNode = ( commandNode.getChildren('$Argument') const text = state.doc.sliceString(ctrlSeq.from, ctrlSeq.to) - const thisCommand = { + items.push({ line: state.doc.lineAt(commandNode.from).number, title: text, from: commandNode.from, @@ -120,7 +122,8 @@ export const enterNode = ( optionalArgCount: optionalArguments.length, requiredArgCount: commandArgumentsIncludingOptional.length - optionalArguments.length, - } - items.push(thisCommand) + type: 'usage', + raw: undefined, + }) } } diff --git a/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts b/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts new file mode 100644 index 0000000000..5fa7e4a4ce --- /dev/null +++ b/services/web/frontend/js/features/source-editor/utils/tree-operations/math.ts @@ -0,0 +1,56 @@ +import { getEnvironmentName } from './environments' +import { EditorState } from '@codemirror/state' +import { SyntaxNode, SyntaxNodeRef } from '@lezer/common' +import { ancestorNodeOfType } from './ancestors' + +export const mathAncestorNode = (state: EditorState, pos: number) => + ancestorNodeOfType(state, pos, '$MathContainer') || + ancestorNodeOfType(state, pos, 'EquationEnvironment') || + // NOTE: EquationArrayEnvironment can be nested inside EquationEnvironment + ancestorNodeOfType(state, pos, 'EquationArrayEnvironment') + +export const parseMathContainer = ( + state: EditorState, + nodeRef: SyntaxNodeRef, + ancestorNode: SyntaxNode +) => { + // the content of the Math element, without braces + const innerContent = state.doc.sliceString(nodeRef.from, nodeRef.to).trim() + + if (!innerContent.length) { + return null + } + + let content = innerContent + let displayMode = false + let passToMathJax = true + + if (ancestorNode.type.is('$Environment')) { + const environmentName = getEnvironmentName(ancestorNode, state) + if (environmentName) { + // use the outer content of environments that MathJax supports + // https://docs.mathjax.org/en/latest/input/tex/macros/index.html#environments + if (environmentName === 'tikzcd') { + passToMathJax = false + } + if (environmentName !== 'math' && environmentName !== 'displaymath') { + content = state.doc + .sliceString(ancestorNode.from, ancestorNode.to) + .trim() + } + + if (environmentName !== 'math') { + displayMode = true + } + } + } else { + if ( + ancestorNode.type.is('BracketMath') || + Boolean(ancestorNode.getChild('DisplayMath')) + ) { + displayMode = true + } + } + + return { content, displayMode, passToMathJax } +} diff --git a/services/web/frontend/js/shared/context/user-settings-context.tsx b/services/web/frontend/js/shared/context/user-settings-context.tsx index 34f49156df..755fc06bbe 100644 --- a/services/web/frontend/js/shared/context/user-settings-context.tsx +++ b/services/web/frontend/js/shared/context/user-settings-context.tsx @@ -23,6 +23,7 @@ const defaultSettings: UserSettings = { fontSize: 12, fontFamily: 'monaco', lineHeight: 'normal', + mathPreview: true, } type UserSettingsContextValue = { diff --git a/services/web/frontend/stylesheets/app/editor.less b/services/web/frontend/stylesheets/app/editor.less index d4b449e0c4..33fb81486d 100644 --- a/services/web/frontend/stylesheets/app/editor.less +++ b/services/web/frontend/stylesheets/app/editor.less @@ -11,6 +11,7 @@ @import './editor/search.less'; @import './editor/publish-template.less'; @import './editor/online-users.less'; +@import './editor/math-preview.less'; @import './editor/hotkeys.less'; @import './editor/review-panel.less'; @import './editor/publish-modal.less'; diff --git a/services/web/frontend/stylesheets/app/editor/math-preview.less b/services/web/frontend/stylesheets/app/editor/math-preview.less new file mode 100644 index 0000000000..e4bbbda2b8 --- /dev/null +++ b/services/web/frontend/stylesheets/app/editor/math-preview.less @@ -0,0 +1,10 @@ +.ol-cm-math-tooltip { + box-shadow: 0px 2px 4px 0px #1e253029; + border: 1px solid #e7e9ee !important; + border-radius: 4px; + background-color: white !important; + max-height: 400px; + max-width: 800px; + overflow: auto; + padding: 8px; +} diff --git a/services/web/locales/en.json b/services/web/locales/en.json index 33f482e8d0..33ff0fe34b 100644 --- a/services/web/locales/en.json +++ b/services/web/locales/en.json @@ -565,6 +565,7 @@ "enter_your_email_address": "Enter your email address", "enter_your_email_address_below_and_we_will_send_you_a_link_to_reset_your_password": "Enter your email address below, and we will send you a link to reset your password", "enter_your_new_password": "Enter your new password", + "equation_preview": "Equation preview", "error": "Error", "error_opening_document": "Error opening document", "error_opening_document_detail": "Sorry, something went wrong opening this document. Please try again.", diff --git a/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx b/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx new file mode 100644 index 0000000000..d7f3b957cf --- /dev/null +++ b/services/web/test/frontend/features/editor-left-menu/components/settings/settings-math-preview.test.tsx @@ -0,0 +1,23 @@ +import { screen, within } from '@testing-library/dom' +import { expect } from 'chai' +import fetchMock from 'fetch-mock' +import SettingsMathPreview from '@/features/editor-left-menu/components/settings/settings-math-preview' +import { renderWithEditorContext } from '../../../../helpers/render-with-context' + +describe('', function () { + afterEach(function () { + fetchMock.reset() + }) + + it('shows correct menu', async function () { + renderWithEditorContext() + + const select = screen.getByLabelText('Equation preview') + + const optionOn = within(select).getByText('On') + expect(optionOn.getAttribute('value')).to.equal('true') + + const optionOff = within(select).getByText('Off') + expect(optionOff.getAttribute('value')).to.equal('false') + }) +}) diff --git a/services/web/test/frontend/helpers/editor-providers.jsx b/services/web/test/frontend/helpers/editor-providers.jsx index adfca9c2fc..0b86a6746e 100644 --- a/services/web/test/frontend/helpers/editor-providers.jsx +++ b/services/web/test/frontend/helpers/editor-providers.jsx @@ -54,6 +54,7 @@ const defaultUserSettings = { autoPairDelimiters: true, trackChanges: true, syntaxValidation: false, + mathPreview: true, } export function EditorProviders({ diff --git a/services/web/types/user-settings.ts b/services/web/types/user-settings.ts index 95c71bda24..6dbb05851e 100644 --- a/services/web/types/user-settings.ts +++ b/services/web/types/user-settings.ts @@ -16,4 +16,5 @@ export type UserSettings = { fontSize: number fontFamily: FontFamily lineHeight: LineHeight + mathPreview: boolean }