From 8687f6b1e31bafc124ae9fe86f94068841164567 Mon Sep 17 00:00:00 2001 From: ppxb <317842449@qq.com> Date: Wed, 4 Nov 2026 12:54:53 +0800 Subject: [PATCH] perf(settings): open the Settings window shell before its startup chain (#1175) The Settings window ran its whole startup chain (importing every settings page, then the project and settings IPC, one after another) before opening the dialog, so it showed an empty title bar for a while on every open. The dialog now opens at once with a loading state while the pages and the settings load in parallel, and re-bases the open dialog when the settings arrive; the search field is focused once the pages are in. Co-authored-by: ppxb <17081240+ppxb@users.noreply.github.com> --- .../__tests__/settingsWindowOpen.test.ts | 3 ++ .../settings/dialog/SettingsDialog.module.css | 9 ++++++ .../settings/dialog/SettingsDialog.tsx | 13 ++++++++- src/features/settings/index.ts | 29 +++++++++++-------- src/features/settings/model/staging.ts | 9 ++++++ .../settings/window/SettingsWindow.tsx | 7 +++-- 6 files changed, 55 insertions(+), 15 deletions(-) diff --git a/src/features/settings/__tests__/settingsWindowOpen.test.ts b/src/features/settings/__tests__/settingsWindowOpen.test.ts index f1ed12a2e..64b5ced29 100644 --- a/src/features/settings/__tests__/settingsWindowOpen.test.ts +++ b/src/features/settings/__tests__/settingsWindowOpen.test.ts @@ -38,7 +38,10 @@ describe("the native Settings window", () => { it("opens even when a page fails to register, and logs why", async () => { vi.spyOn(console, "error").mockImplementation(() => {}); const dispose = activateSettingsWindow("main"); + expect(useSettingsDialog.getState().open).toBe(true); + expect(useSettingsDialog.getState().pagesLoading).toBe(true); await vi.waitFor(() => expect(useSettingsDialog.getState().open).toBe(true)); + await vi.waitFor(() => expect(useSettingsDialog.getState().pagesLoading).toBe(false)); expect(logFrontend).toHaveBeenCalledWith( "error", expect.stringContaining("cannot register settings pages: Error: page failed"), diff --git a/src/features/settings/dialog/SettingsDialog.module.css b/src/features/settings/dialog/SettingsDialog.module.css index d7bad0428..f457b4a8d 100644 --- a/src/features/settings/dialog/SettingsDialog.module.css +++ b/src/features/settings/dialog/SettingsDialog.module.css @@ -63,6 +63,15 @@ flex-direction: column; min-height: 0; } +.loading { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + min-height: 0; + color: var(--fg-subtle); + font-size: var(--fs-small); +} .pageHeader { flex: none; display: flex; diff --git a/src/features/settings/dialog/SettingsDialog.tsx b/src/features/settings/dialog/SettingsDialog.tsx index 9d6bbf74d..0ae1ab284 100644 --- a/src/features/settings/dialog/SettingsDialog.tsx +++ b/src/features/settings/dialog/SettingsDialog.tsx @@ -17,6 +17,7 @@ import { Icon, IconButton, SearchField, + Spinner, Tree, type DialogButton, type TreeHandle, @@ -220,7 +221,7 @@ function SettingsDialogInner() { /** Page tree + current page; reads the shared settings staging store. */ export function SettingsLayout() { const pages = useSettingsPages((s) => s.pages); - const { pageId, query, staged, stagedSecrets, stagedExternal, saving } = useSettingsDialog(); + const { pageId, query, staged, stagedSecrets, stagedExternal, saving, pagesLoading } = useSettingsDialog(); const treeRef = useRef(null); const tree = useMemo(() => buildPageTree(pages.values()), [pages]); @@ -265,6 +266,16 @@ export function SettingsLayout() { } }; + if (pagesLoading) { + return ( +
+
+ Loading… +
+
+ ); + } + return (
diff --git a/src/features/settings/index.ts b/src/features/settings/index.ts index b78ba3780..a42918f70 100644 --- a/src/features/settings/index.ts +++ b/src/features/settings/index.ts @@ -24,6 +24,7 @@ import { LazySettingsDialog, loadBuiltinPages, unloadBuiltinPages } from "./dial import { closeSettings, openSettings, + rebaseSettings, selectPage, setNewProjects, setQuery, @@ -208,20 +209,24 @@ export function activateSettingsWindow(parent: string): () => void { }), ]; void (async () => { - let launch: SettingsLaunch | null = null; + useSettingsDialog.setState({ pagesLoading: true }); + openSettings(); try { - // A page that fails to register is reported; the window still opens with the others. - await loadBuiltinPages().catch((e) => logError("cannot register settings pages", e)); - await restoreRemote(); - const project = await commands.projectCurrent().catch(() => null); - if (project) useProject.setState({ project, settings: project.settings }); - else await loadSettings().catch((e) => console.warn("cannot load settings", e)); - // `listenLaunch` may have taken the launch first and applied it already. - launch = await takeLaunch(); + const launch = await takeLaunch(); + relaunch(launch); + await Promise.all([ + loadBuiltinPages().catch((e) => logError("cannot register settings pages", e)), + (async () => { + await restoreRemote(); + const project = await commands.projectCurrent().catch(() => null); + if (project) useProject.setState({ project, settings: project.settings }); + else await loadSettings().catch((e) => console.warn("cannot load settings", e)); + rebaseSettings(); + })(), + ]); } finally { - // Without this the window would stay an empty title bar. - openSettings(launch?.page, { newProjects: launch?.newProjects ?? useSettingsDialog.getState().newProjects }); + useSettingsDialog.setState({ pagesLoading: false }); } - })(); + })().catch((e) => logError("settings window startup failed", e)); return () => disposers.reverse().forEach((d) => d()); } diff --git a/src/features/settings/model/staging.ts b/src/features/settings/model/staging.ts index 6bc437ad3..b78d67bcb 100644 --- a/src/features/settings/model/staging.ts +++ b/src/features/settings/model/staging.ts @@ -46,6 +46,7 @@ export interface SettingsDialogState { scopes: Record; saving: boolean; error: string | null; + pagesLoading: boolean; } export const DEFAULT_PAGE = "appearance"; @@ -66,6 +67,7 @@ export const useSettingsDialog = create(() => ({ scopes: {}, saving: false, error: null, + pagesLoading: false, })); const stageKey = (scope: Scope, key: string) => `${scope}|${key}`; @@ -109,6 +111,12 @@ async function loadGlobalBase() { } } +export function rebaseSettings() { + const base = validateSettings(useProject.getState().settings); + useSettingsDialog.setState((s) => (s.open ? { base, globalBase: base } : {})); + void loadGlobalBase(); +} + /** IDEA's File › New Projects Setup › Settings for New Projects. */ export const settingsTitle = (newProjects: boolean) => (newProjects ? "Settings for New Projects" : "Settings"); @@ -129,6 +137,7 @@ export function closeSettings() { externalApplied: {}, error: null, saving: false, + pagesLoading: false, }); } diff --git a/src/features/settings/window/SettingsWindow.tsx b/src/features/settings/window/SettingsWindow.tsx index 231a60640..445dc59b4 100644 --- a/src/features/settings/window/SettingsWindow.tsx +++ b/src/features/settings/window/SettingsWindow.tsx @@ -14,12 +14,15 @@ export function SettingsWindow() { const os = usePlatform((s) => s.os); const ownTitleBar = usePlatform((s) => s.overlayTitleBar || s.framelessTitleBar); const open = useSettingsDialog((s) => s.open); + const pagesLoading = useSettingsDialog((s) => s.pagesLoading); const title = useSettingsTitle(); const ref = useRef(null); useEffect(() => { - if (open) ref.current?.querySelector("[data-autofocus]")?.focus({ preventScroll: true }); - }, [open]); + if (open && !pagesLoading) { + ref.current?.querySelector("[data-autofocus]")?.focus({ preventScroll: true }); + } + }, [open, pagesLoading]); return (