From 0c76afff7617bd9719e29e89d5774b82ffc1eac6 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Mon, 4 Aug 2025 09:28:52 +0100 Subject: [PATCH] Merge pull request #27099 from overleaf/mj-no-duplicate-themes [web] Avoid creating duplicate CM6 themes GitOrigin-RevId: f6132d6cdd94ef353e047ce229d89147acc89603 --- services/web/.eslintrc.js | 1 + .../js/features/history/extensions/theme.ts | 7 ++++--- .../pdf-preview/components/visual-preview.tsx | 18 ++++++++++-------- .../features/source-editor/extensions/theme.ts | 10 +++++++--- .../source-editor/utils/theme-cache.ts | 18 ++++++++++++++++++ 5 files changed, 40 insertions(+), 14 deletions(-) create mode 100644 services/web/frontend/js/features/source-editor/utils/theme-cache.ts diff --git a/services/web/.eslintrc.js b/services/web/.eslintrc.js index 7dd154c942..4b629698c1 100644 --- a/services/web/.eslintrc.js +++ b/services/web/.eslintrc.js @@ -321,6 +321,7 @@ module.exports = { 'react/no-did-update-set-state': 'error', 'react/no-unused-prop-types': 'error', 'react/prop-types': 'error', + '@overleaf/no-generated-editor-themes': 'error', // "react/react-in-jsx-scope": "error", // END: inline standard-react rules diff --git a/services/web/frontend/js/features/history/extensions/theme.ts b/services/web/frontend/js/features/history/extensions/theme.ts index 268b9d2dfd..8ddf312748 100644 --- a/services/web/frontend/js/features/history/extensions/theme.ts +++ b/services/web/frontend/js/features/history/extensions/theme.ts @@ -1,6 +1,7 @@ import { EditorView } from '@codemirror/view' import { Compartment, TransactionSpec } from '@codemirror/state' import { FontFamily, LineHeight, userStyles } from '@/shared/utils/styles' +import { ThemeCache } from '@/features/source-editor/utils/theme-cache' export type Options = { fontSize: number @@ -15,6 +16,8 @@ export const theme = (options: Options) => [ optionsThemeConf.of(createThemeFromOptions(options)), ] +const tooltipThemeCache = new ThemeCache() + const createThemeFromOptions = ({ fontSize = 12, fontFamily = 'monaco', @@ -33,9 +36,7 @@ const createThemeFromOptions = ({ .map(([key, value]) => `${key}: ${value}`) .join(';'), }), - // Set variables for tooltips, which are outside the editor - // TODO: set these on document.body, or a new container element for the tooltips, without using a style mod - EditorView.theme({ + tooltipThemeCache.get({ '.cm-tooltip': { '--font-size': styles.fontSize, '--source-font-family': styles.fontFamily, diff --git a/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx index 074d26501a..7eeb7dfdac 100644 --- a/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx +++ b/services/web/frontend/js/features/pdf-preview/components/visual-preview.tsx @@ -31,6 +31,15 @@ import { useEditorOpenDocContext } from '@/features/ide-react/context/editor-ope import { isValidTeXFile } from '@/main/is-valid-tex-file' import { mousedown } from '@/features/source-editor/extensions/visual/selection' +const visualPreviewTheme = EditorView.theme({ + '&.cm-editor': { + background: '#fff', + }, + '.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': { + visibility: 'hidden', + }, +}) + export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => { const [previewState, setPreviewState] = useState() @@ -71,14 +80,7 @@ export const VisualPreview: FC<{ view: EditorView }> = ({ view }) => { lineHeight: 'normal', activeOverallTheme: 'light', }), - EditorView.theme({ - '&.cm-editor': { - background: '#fff', - }, - '.ol-cm-preamble-wrapper, .ol-cm-end-document-widget': { - visibility: 'hidden', - }, - }), + visualPreviewTheme, visualTheme, visualHighlightStyle, tableGeneratorTheme, diff --git a/services/web/frontend/js/features/source-editor/extensions/theme.ts b/services/web/frontend/js/features/source-editor/extensions/theme.ts index 3b3ac4afdf..2e50b6abee 100644 --- a/services/web/frontend/js/features/source-editor/extensions/theme.ts +++ b/services/web/frontend/js/features/source-editor/extensions/theme.ts @@ -5,6 +5,7 @@ import { classHighlighter } from './class-highlighter' import classNames from 'classnames' import { FontFamily, LineHeight, userStyles } from '@/shared/utils/styles' import { ActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme' +import { ThemeCache } from '../utils/theme-cache' const optionsThemeConf = new Compartment() const selectedThemeConf = new Compartment() @@ -50,6 +51,8 @@ const svgUrl = (content: string) => `${content}` )}')` +const tooltipThemeCache = new ThemeCache() + const createThemeFromOptions = ({ fontSize = 12, fontFamily = 'monaco', @@ -74,9 +77,7 @@ const createThemeFromOptions = ({ .map(([key, value]) => `${key}: ${value}`) .join(';'), }), - // set variables for tooltips, which are outside the editor - // TODO: set these on document.body, or a new container element for the tooltips, without using a style mod - EditorView.theme({ + tooltipThemeCache.get({ '.cm-tooltip': { '--font-size': styles.fontSize, '--source-font-family': styles.fontFamily, @@ -280,8 +281,11 @@ const loadSelectedTheme = async (editorTheme: string) => { /* webpackChunkName: "cm6-theme" */ `../themes/cm6/${editorTheme}.json` ) + // We store these in a cache, so we'll reuse after the first load const extension = [ + // eslint-disable-next-line @overleaf/no-generated-editor-themes EditorView.theme(theme, { dark }), + // eslint-disable-next-line @overleaf/no-generated-editor-themes EditorView.theme(highlightStyle, { dark }), ] diff --git a/services/web/frontend/js/features/source-editor/utils/theme-cache.ts b/services/web/frontend/js/features/source-editor/utils/theme-cache.ts new file mode 100644 index 0000000000..2d6cbccad4 --- /dev/null +++ b/services/web/frontend/js/features/source-editor/utils/theme-cache.ts @@ -0,0 +1,18 @@ +import { Extension } from '@codemirror/state' +import { EditorView } from '@codemirror/view' + +export class ThemeCache { + private cache: Map = new Map() + + public get: typeof EditorView.theme = (styleMod, options) => { + const key = JSON.stringify({ styleMod, options }) + const existing = this.cache.get(key) + if (existing) { + return existing + } + // eslint-disable-next-line @overleaf/no-generated-editor-themes + const theme = EditorView.theme(styleMod, options) + this.cache.set(key, theme) + return theme + } +}