perf: load dialogs, the SQL console and diff views on first use

- Database dialogs, Assign Shortcut and the Logcat tool window load when shown.
- The SQL console extensions load with the first SQL editor
  (`lazyEditorExtension` reconfigures the editor once they arrive).
- The changes tree loads the diff viewer when a file is selected, and
  "Open Repository Log" loads the revision browser when run.
Цей коміт міститься в:
l0ng-ai committed 2026-11-06 08:18:43 +08:00
1 джерело 1ae8194613
коміт 2e12f51cac
13 files changed
+119 -27

No files matched your search

+41
Переглянути файл
@@ -65,6 +65,47 @@ export interface EditorContribution {
embedded?: boolean;
}
/**
* Editor extensions whose code loads with the first editor that wants them: `wants`
* decides synchronously, `load` resolves the real `extensions`. Until it has, the editor
* runs without them and reconfigures once loaded.
*/
export function lazyEditorExtension(
wants: (ctx: EditorDocContext) => boolean,
load: () => Promise<(ctx: EditorDocContext) => Extension>,
): (ctx: EditorDocContext) => Extension {
let loaded: ((ctx: EditorDocContext) => Extension) | null = null;
let pending: Promise<(ctx: EditorDocContext) => Extension> | null = null;
const get = () =>
(pending ??= load().then(
(f) => (loaded = f),
(e: unknown) => {
pending = null;
throw e;
},
));
return (ctx) => {
if (!wants(ctx)) return [];
if (loaded) return loaded(ctx);
const slot = new Compartment();
return [
slot.of([]),
ViewPlugin.define((view) => {
let live = true;
get().then(
(f) => live && view.dispatch({ effects: slot.reconfigure(f(ctx)) }),
(e: unknown) => console.error("could not load editor extensions", e),
);
return {
destroy: () => {
live = false;
},
};
}),
];
};
}
export interface TextEditorHandle {
view: EditorView;
ctx: EditorDocContext;
+26
Переглянути файл
@@ -0,0 +1,26 @@
import { describe, expect, it, vi } from "vitest";
import { EditorState, Facet } from "@codemirror/state";
import { EditorView } from "@codemirror/view";
import { lazyEditorExtension, type EditorDocContext } from "./api";
const marker = Facet.define<string>();
const ctx = (language: string) => ({ path: `/p/a.${language}`, language }) as EditorDocContext;
const flush = () => new Promise((resolve) => setTimeout(resolve));
describe("lazyEditorExtension", () => {
it("reconfigures wanted editors once loaded and applies directly afterwards", async () => {
const load = vi.fn(async () => (c: EditorDocContext) => marker.of(c.language));
const extensions = lazyEditorExtension((c) => c.language === "sql", load);
const first = new EditorView({ state: EditorState.create({ extensions: extensions(ctx("sql")) }) });
expect(first.state.facet(marker)).toEqual([]);
await flush();
expect(first.state.facet(marker)).toEqual(["sql"]);
const second = EditorState.create({ extensions: extensions(ctx("sql")) });
expect(second.facet(marker)).toEqual(["sql"]);
expect(extensions(ctx("ts"))).toEqual([]);
expect(load).toHaveBeenCalledTimes(1);
first.destroy();
});
});
+2 -1
Переглянути файл
@@ -63,7 +63,8 @@ import { tx } from "@/platform/i18n";
/** `templates` feature's language-data key for finer template contexts. */
const TEMPLATE_CONTEXT_KEY = "openideaTemplateContexts";
export const SQL_LANGUAGE = "sql";
import { SQL_LANGUAGE } from "./sqlLanguage";
export { SQL_LANGUAGE };
const ROW_LIMITS = ["100", "500", "1000", "5000", "50000"];
/** Statement outcomes of the last run, kept on the text (IDEA's gutter check / error and timing). */
+19 -8
Переглянути файл
@@ -4,8 +4,8 @@
* results grids, and a table data editor. Connections, introspection and
* statements run in Rust (`ide-db`).
*/
import { lazyComponent } from "@/ui";
import { activeEditor, registerEditorContribution } from "@/editor/api";
import { lazyComponent, lazyWhen } from "@/ui";
import { activeEditor, lazyEditorExtension, registerEditorContribution } from "@/editor/api";
import { registerActions } from "@/platform/actions";
import { useProject } from "@/platform/project";
import {
@@ -16,8 +16,7 @@ import {
setFileDecorations,
toolWindowService,
} from "@/workbench/contributions";
import { SQL_LANGUAGE, consoleExtension, executeInView, explainInView } from "./console";
import { DatabaseDialogs } from "./Dialogs";
import { SQL_LANGUAGE } from "./sqlLanguage";
import { TABLE_KIND, tableTitle } from "./tableInput";
import { nodeTarget, type DbNode } from "./nodes";
import { registerDatabaseTreeActions } from "./treeActions";
@@ -34,6 +33,8 @@ import {
watchIdleConnections,
} from "./store";
const useDialogOpen = () => useDatabase((s) => !!s.dialog);
export function activate(): () => void {
const disposers = [
registerToolWindow({
@@ -59,8 +60,18 @@ export function activate(): () => void {
title: tableTitle,
icon: () => "database",
}),
registerOverlay({ id: "database.dialogs", component: DatabaseDialogs }),
registerEditorContribution({ id: "database.console", order: 57, extensions: consoleExtension }),
registerOverlay({
id: "database.dialogs",
component: lazyWhen(useDialogOpen, () => import("./Dialogs").then((m) => m.DatabaseDialogs)),
}),
registerEditorContribution({
id: "database.console",
order: 57,
extensions: lazyEditorExtension(
(ctx) => ctx.language === SQL_LANGUAGE && !ctx.virtual && !ctx.readOnly,
() => import("./console").then((m) => m.consoleExtension),
),
}),
// A reopened project starts with a fresh backend: forget the old one's state.
useProject.subscribe((s, prev) => {
if (s.project?.root === prev.project?.root) return;
@@ -149,7 +160,7 @@ export function activate(): () => void {
openSourceDialog(null);
return;
}
await executeInView(e.view, e.ctx.path);
await (await import("./console")).executeInView(e.view, e.ctx.path);
},
},
{
@@ -163,7 +174,7 @@ export function activate(): () => void {
const e = activeEditor();
if (!e || e.ctx.language !== SQL_LANGUAGE) return;
if (!useDatabase.getState().loaded) await loadSources();
await explainInView(e.view, e.ctx.path);
await (await import("./console")).explainInView(e.view, e.ctx.path);
},
},
{
+2
Переглянути файл
@@ -0,0 +1,2 @@
/** The language id of SQL files and consoles. */
export const SQL_LANGUAGE = "sql";
+2 -2
Переглянути файл
@@ -3,7 +3,7 @@
* level and text filters of IDEA's Android plugin.
*/
import { registerToolWindow } from "@/workbench/contributions";
import { LogcatView } from "./LogcatView";
import { lazyComponent } from "@/ui";
import { resetLogcat } from "./store";
export function activate(): () => void {
@@ -15,7 +15,7 @@ export function activate(): () => void {
// Listed under the stripe's "…" until first opened: most projects are not Android ones.
stripe: "onUse",
order: 26,
component: LogcatView,
component: lazyComponent(() => import("./LogcatView").then((m) => m.LogcatView)),
// A device can be watched without a project open.
requiresProject: false,
});
+10 -3
Переглянути файл
@@ -14,7 +14,7 @@
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
import { registerActions, type Action } from "@/platform/actions";
import { call, commands, isTauri } from "@/platform/ipc";
import { isPopupOpen, lazyComponent } from "@/ui";
import { isPopupOpen, lazyComponent, lazyWhen } from "@/ui";
import { loadSettings, useProject } from "@/platform/project";
import { logError } from "@/platform/errorLog";
import { restoreRemote } from "@/platform/remote";
@@ -35,7 +35,7 @@ import { zoomActions } from "./appearanceRuntime";
import { activateFileTypes } from "./fileTypesRuntime";
import { trackRuntimeSettings } from "./runtime";
import { installActionProxies, installSettingsHost } from "./window/bridge";
import { AssignShortcutOverlay, showAssignShortcut } from "./keymap/AssignShortcut";
import { showAssignShortcut, useAssignShortcut } from "./keymap/assignShortcutStore";
import { settingsSearchEntries } from "./searchIndex";
import { activatePythonStatus } from "./languages/PythonStatus";
import { activatePhpStatus } from "./languages/PhpStatus";
@@ -105,6 +105,8 @@ async function showSettings(page?: string, newProjects = false) {
openSettings(page, { newProjects });
}
const useAssignShortcutOpen = () => useAssignShortcut((s) => s.actionId !== null);
export function activate(): () => void {
const disposers = [
trackRuntimeSettings(),
@@ -116,7 +118,12 @@ export function activate(): () => void {
registerActions(zoomActions()),
registerActions(settingsActions((page, newProjects) => void showSettings(page, newProjects))),
activateFileTypes(),
registerOverlay({ id: "settings.assignShortcut", component: AssignShortcutOverlay }),
registerOverlay({
id: "settings.assignShortcut",
component: lazyWhen(useAssignShortcutOpen, () =>
import("./keymap/AssignShortcut").then((m) => m.AssignShortcutOverlay),
),
}),
registerActions([
{
// Search Everywhere's ⌥↵ on an action (arg: the action id).
+2 -6
Переглянути файл
@@ -4,7 +4,6 @@
* active keymap (overrides of a bundled keymap, or the user keymap itself).
*/
import { useMemo } from "react";
import { create } from "zustand";
import { useActions } from "@/platform/actions";
import { call, commands, isTauri } from "@/platform/ipc";
import type { Keymap } from "@/platform/keymap";
@@ -21,11 +20,8 @@ import {
} from "./model";
import { RecordShortcutDialog } from "./RecordShortcutDialog";
export const useAssignShortcut = create<{ actionId: string | null }>(() => ({ actionId: null }));
export function showAssignShortcut(actionId: string) {
useAssignShortcut.setState({ actionId });
}
import { useAssignShortcut } from "./assignShortcutStore";
export { showAssignShortcut, useAssignShortcut } from "./assignShortcutStore";
interface KeymapSettings {
keymap: string;
+8
Переглянути файл
@@ -0,0 +1,8 @@
import { create } from "zustand";
/** The action whose shortcut the Assign Shortcut dialog is editing. */
export const useAssignShortcut = create<{ actionId: string | null }>(() => ({ actionId: null }));
export function showAssignShortcut(actionId: string) {
useAssignShortcut.setState({ actionId });
}
+3 -2
Переглянути файл
@@ -1,10 +1,11 @@
import { useState, type ReactNode } from "react";
import { Icon, Tree, fileIconName } from "@/ui";
import { DiffEditor } from "./DiffEditor";
import { Icon, Tree, fileIconName, lazyComponent } from "@/ui";
import { diffInput, openDiff, type DiffData } from "./diffInput";
import { dirKeys, type FileTreeNode } from "./fileTree";
import styles from "./common.module.css";
const DiffEditor = lazyComponent(() => import("./DiffEditor").then((m) => m.DiffEditor));
/** Shared changes tree and on-demand diff surface for revisions and disk folders. */
export function ComparisonTree<F>({
tree,
+2 -2
Переглянути файл
@@ -84,7 +84,7 @@ describe("disk directory comparison", () => {
it("previews on selection and opens the existing diff on double click, including missing sides", async () => {
setup();
fireEvent.mouseDown(await screen.findByText("中文.txt"), { button: 0 });
expect(screen.getByTestId("diff")).toHaveTextContent("/p/left/nested/中文.txt");
expect(await screen.findByTestId("diff")).toHaveTextContent("/p/left/nested/中文.txt");
fireEvent.doubleClick(screen.getByText("中文.txt"));
expect(mocks.open).toHaveBeenCalledWith(expect.objectContaining({ path: "/p/right/nested/中文.txt" }));
expect(directoryDiff("/l", "/r", rows[2]!)?.right.type).toBe("text");
@@ -122,7 +122,7 @@ describe("disk directory comparison", () => {
? ["/p/right", "/p/left", "nested/中文.txt"]
: ["/p/left", "/p/right", "nested/中文.txt"],
);
expect(screen.getByTestId("diff")).toBeInTheDocument();
expect(await screen.findByTestId("diff")).toBeInTheDocument();
},
);
+1 -1
Переглянути файл
@@ -147,7 +147,7 @@ describe("directory comparison", () => {
expect(screen.getByText("−1")).toBeInTheDocument();
expect(screen.queryByTestId("diff")).toBeNull();
fireEvent.mouseDown(screen.getByText("app.ts"), { button: 0 });
expect(screen.getByTestId("diff")).toHaveTextContent('"rev":"aaa"');
expect(await screen.findByTestId("diff")).toHaveTextContent('"rev":"aaa"');
expect(screen.getByTestId("diff")).toHaveTextContent("/workspace/client/src/app.ts");
});
+1 -2
Переглянути файл
@@ -60,7 +60,6 @@ import {
unshelve,
} from "./shelf";
import { currentRepo, focusRepo, useVcs } from "./store";
import { openExternalLog } from "./RevisionBrowser";
const VCS = "vcsAvailable";
const IN_GIT_LOG = "(vcsAvailable || projectOpen) && (activeToolWindow == git || vcsLogFocus)";
@@ -250,7 +249,7 @@ export function registerHistoryActions(): () => void {
id: "vcs.log.openExternal",
title: "Open External Git Log…",
category: "Git Log",
run: openExternalLog,
run: async () => (await import("./RevisionBrowser")).openExternalLog(),
},
{
id: "vcs.log.goToRef",