дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
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.
Цей коміт міститься в:
1 джерело
1ae8194613
коміт
2e12f51cac
13 files changed
+119
-27
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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). */
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
/** The language id of SQL files and consoles. */
|
||||
export const SQL_LANGUAGE = "sql";
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Заблокувати користувача