From 73911b76dc74b13477aaf753bdb83ad2e380aa38 Mon Sep 17 00:00:00 2001 From: Jon Eugster Date: Sat, 27 Dec 2025 23:25:08 +0100 Subject: [PATCH 1/2] refactor/settings-in-jotai --- client/src/App.tsx | 125 ++++-------- client/src/Navigation.tsx | 12 +- client/src/Popups/LoadUrl.tsx | 2 +- client/src/Popups/LoadZulip.tsx | 2 +- client/src/Popups/Settings.tsx | 142 ------------- client/src/css/App.css | 5 + client/src/css/Modal.css | 10 +- client/src/settings/SettingsPopup.tsx | 191 ++++++++++++++++++ client/src/settings/settings-atoms.ts | 48 +++++ .../settings-types.ts} | 33 ++- client/src/store/window-atoms.ts | 9 + cypress/e2e/settings.cy.ts | 42 ++++ cypress/e2e/spec.cy.ts | 36 ---- doc/Usage.md | 2 +- package-lock.json | 75 +++++-- package.json | 2 + 16 files changed, 430 insertions(+), 306 deletions(-) delete mode 100644 client/src/Popups/Settings.tsx create mode 100644 client/src/settings/SettingsPopup.tsx create mode 100644 client/src/settings/settings-atoms.ts rename client/src/{config/settings.ts => settings/settings-types.ts} (69%) create mode 100644 client/src/store/window-atoms.ts create mode 100644 cypress/e2e/settings.cy.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 83babe88..91617e01 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -10,9 +10,8 @@ import * as path from 'path' // Local imports import LeanLogo from './assets/logo.svg' -import defaultSettings, { IPreferencesContext, lightThemes, preferenceParams } from './config/settings' +import { lightThemes } from './settings/settings-types' import { Menu } from './Navigation' -import { PreferencesContext } from './Popups/Settings' import { Entries } from './utils/Entries' import { save } from './utils/SaveToFile' import { fixedEncodeURIComponent, formatArgs, lookupUrl, parseArgs } from './utils/UrlParsing' @@ -21,6 +20,9 @@ import { useWindowDimensions } from './utils/WindowWidth' // CSS import './css/App.css' import './css/Editor.css' +import { mobileAtom, settingsAtom } from './settings/settings-atoms' +import { useAtom } from 'jotai/react' +import { screenWidthAtom } from './store/window-atoms' /** Returns true if the browser wants dark mode */ function isBrowserDefaultDark() { @@ -33,8 +35,9 @@ function App() { const [dragging, setDragging] = useState(false) const [editor, setEditor] = useState() const [leanMonaco, setLeanMonaco] = useState() - const [loaded, setLoaded] = useState(false) - const [preferences, setPreferences] = useState(defaultSettings) + const [settings] = useAtom(settingsAtom) + const [mobile] = useAtom(mobileAtom) + const [, setScreenWidth] = useAtom(screenWidthAtom) const { width } = useWindowDimensions() // Lean4monaco options @@ -83,67 +86,13 @@ function App() { setProject(project) }, []) - // Load preferences from store in the beginning + // save the screen width in jotai useEffect(() => { - // only load them once - if (loaded) { return } - console.debug('[Lean4web] Loading preferences') - - let saveInLocalStore = false; - let newPreferences: { [K in keyof IPreferencesContext]: IPreferencesContext[K] } = { ...preferences } - for (const [key, value] of (Object.entries(preferences) as Entries)) { - // prefer URL params over stored - const searchParams = new URLSearchParams(window.location.search); - let storedValue = ( - preferenceParams.includes(key) && // only for keys we explictly check for - searchParams.has(key) && searchParams.get(key)) - ?? window.localStorage.getItem(key) - if (storedValue) { - saveInLocalStore = window.localStorage.getItem(key) === storedValue - console.debug(`[Lean4web] Found value for ${key}: ${storedValue}`) - if (typeof value === 'string') { - if (key == 'theme') { - const theme = storedValue.toLowerCase().includes('dark') ? "Visual Studio Dark" : "Visual Studio Light" - newPreferences[key] = theme - } - else { - newPreferences[key] = storedValue - } - } else if (typeof value === 'boolean') { - newPreferences[key] = (storedValue === "true") - } else { - // other values aren't implemented yet. - console.error(`[Lean4web] Preferences (key: ${key}) contain a value of unsupported type: ${typeof value}`) - } - } else { - // no stored preferences, set a default value - if (key == 'theme') { - if (isBrowserDefaultDark()) { - console.debug("[Lean4web] Preferences: Set dark theme.") - newPreferences['theme'] = 'Visual Studio Dark' - } else { - console.debug("[Lean4web] Preferences: Set light theme.") - newPreferences['theme'] = 'Visual Studio Light' - } - } - } - } - newPreferences['saveInLocalStore'] = saveInLocalStore - setPreferences(newPreferences) - setLoaded(true) - }, []) - - // Use the window width to switch between mobile/desktop layout - useEffect(() => { - // Wait for preferences to be loaded - if (!loaded) { return } + const handleResize = () => setScreenWidth(window.innerWidth) + window.addEventListener('resize', handleResize) + return () => window.removeEventListener('resize', handleResize) + }, [setScreenWidth]) - const _mobile = width < 800 - const searchParams = new URLSearchParams(window.location.search); - if (!(searchParams.has("mobile") || preferences.saveInLocalStore) && _mobile !== preferences.mobile) { - setPreferences({ ...preferences, mobile: _mobile }) - } - }, [width, loaded]) // Update LeanMonaco options when preferences are loaded or change useEffect(() => { @@ -162,29 +111,28 @@ function App() { * for the desired setting, select "Copy Setting as JSON" from the "More Actions" * menu next to the selected setting, and paste the copied string here. */ - "workbench.colorTheme": preferences.theme, + "workbench.colorTheme": settings.theme, "editor.tabSize": 2, // "editor.rulers": [100], "editor.lightbulb.enabled": "on", - "editor.wordWrap": preferences.wordWrap ? "on" : "off", + "editor.wordWrap": settings.wordWrap ? "on" : "off", "editor.wrappingStrategy": "advanced", "editor.semanticHighlighting.enabled": true, - "editor.acceptSuggestionOnEnter": preferences.acceptSuggestionOnEnter ? "on" : "off", + "editor.acceptSuggestionOnEnter": settings.acceptSuggestionOnEnter ? "on" : "off", "lean4.input.eagerReplacementEnabled": true, - "lean4.infoview.showGoalNames": preferences.showGoalNames, + "lean4.infoview.showGoalNames": settings.showGoalNames, "lean4.infoview.emphasizeFirstGoal": true, "lean4.infoview.showExpectedType": false, "lean4.infoview.showTooltipOnHover": false, - "lean4.input.leader": preferences.abbreviationCharacter + "lean4.input.leader": settings.abbreviationCharacter } } setOptions(_options) - }, [editorRef, project, preferences]) + }, [editorRef, project, settings]) // Setting up the editor and infoview useEffect(() => { - // Wait for preferences to be loaded - if (!loaded) { return } + // if (project === undefined) return console.debug('[Lean4web] Restarting editor') var _leanMonaco = new LeanMonaco() var leanMonacoEditor = new LeanMonacoEditor() @@ -192,7 +140,7 @@ function App() { _leanMonaco.setInfoviewElement(infoviewRef.current!) ;(async () => { await _leanMonaco.start(options) - await leanMonacoEditor.start(editorRef.current!, path.join(project, `${project}.lean`), code) + await leanMonacoEditor.start(editorRef.current!, path.join(project, `${project}.lean`), code ?? '') setEditor(leanMonacoEditor.editor) setLeanMonaco(_leanMonaco) @@ -201,7 +149,7 @@ function App() { // Monaco does not support clipboard pasting as all browsers block it // due to security reasons. Therefore we use a codeMirror editor overlay // which features good mobile support (but no Lean support yet) - if (preferences.mobile) { + if (mobile) { leanMonacoEditor.editor?.addAction({ id: "myPaste", label: "Paste: open 'Plain Editor' for editing on mobile", @@ -274,7 +222,7 @@ function App() { leanMonacoEditor.dispose() _leanMonaco.dispose() } - }, [loaded, project, preferences, options, infoviewRef, editorRef]) + }, [project, settings, options, infoviewRef, editorRef]) // Load content from source URL. // Once the editor is loaded, this reads the content of any provided `url=` in the URL and @@ -330,7 +278,7 @@ function App() { code: null, codez: null } - } else if (preferences.compress) { + } else if (settings.compress) { // LZ padds the string with trailing `=`, which mess up the argument parsing // and aren't needed for LZ encoding, so we remove them. const compressed = LZString.compressToBase64(code).replace(/=*$/, '') @@ -402,7 +350,7 @@ function App() { } }, [handleKeyDown, handleKeyUp]) - return + return (