дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 11:15:40 +02:00
fix(viewers): yield image viewer to registered text editor wrappers (#1638)
A plugin's side-by-side custom editor for an image-extension subtype (such as *.excalidraw.svg) was intercepted by the image viewer. The image viewer now yields to a matching text editor wrapper, and an image tab already open switches to the text editor when such a wrapper registers later (after plugins load). Co-authored-by: dengliu <[email protected]>
Цей коміт міститься в:
1 джерело
e6b800971a
коміт
1f76e71621
4 files changed
+80
-1
No files matched your search
@@ -4,6 +4,7 @@ import { createMemoryBackend } from "./backend";
|
||||
import { initialTabsState } from "./model/tabs";
|
||||
import { useDocMeta } from "./model/document";
|
||||
import { resetDistractionFree, setDistractionFree } from "@/workbench/distractionFree";
|
||||
import { registerTextEditorWrapper } from "@/workbench/contributions";
|
||||
import { registerActions } from "@/platform/actions";
|
||||
import { useProject } from "@/platform/project";
|
||||
import { resolveMenuItems } from "@/ui";
|
||||
@@ -319,4 +320,22 @@ describe("EditorController", () => {
|
||||
dispose();
|
||||
useProject.setState({ project: null, settings: null });
|
||||
});
|
||||
|
||||
it("switches an open image tab to a text tab when a matching text editor wrapper registers", async () => {
|
||||
ctrl = new EditorController(createMemoryBackend({ "/p/arch.diagram.svg": "<svg/>" }));
|
||||
ctrl.start();
|
||||
await ctrl.openInput({ kind: "image", key: "image:/p/arch.diagram.svg", path: "/p/arch.diagram.svg" });
|
||||
expect(openFilePaths(useTabs.getState())).toEqual([]);
|
||||
const offWrapper = registerTextEditorWrapper({
|
||||
id: "plugin.acme.diagram.canvas",
|
||||
matches: (p) => p.endsWith(".diagram.svg"),
|
||||
component: () => null,
|
||||
});
|
||||
try {
|
||||
expect(openFilePaths(useTabs.getState())).toEqual(["/p/arch.diagram.svg"]);
|
||||
await vi.waitFor(() => expect(ctrl?.activeSlot?.model.path).toBe("/p/arch.diagram.svg"));
|
||||
} finally {
|
||||
offWrapper();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -23,13 +23,16 @@ import {
|
||||
menuGroupItems,
|
||||
notifications,
|
||||
resolveFileEditor,
|
||||
textEditorWrapperFor,
|
||||
useContributions,
|
||||
useStandaloneEditor,
|
||||
useTextEditorWrappers,
|
||||
type EditorCloseSession,
|
||||
type EditorInput,
|
||||
type OpenOptions,
|
||||
type FileEditorResolution,
|
||||
type OpenEditorTab,
|
||||
type TextEditorWrapperContribution,
|
||||
} from "@/workbench/contributions";
|
||||
import { useDistractionFree } from "@/workbench/distractionFree";
|
||||
import { ask } from "@/workbench/messages";
|
||||
@@ -344,6 +347,11 @@ export class EditorController implements SlotHost {
|
||||
);
|
||||
// Gutter, centred column and sticky lines follow Distraction Free Mode.
|
||||
this.disposers.push(useDistractionFree.subscribe(() => this.manager.applySettings()));
|
||||
this.disposers.push(
|
||||
useTextEditorWrappers.subscribe(({ wrappers }) => {
|
||||
this.upgradeWrappedImageTabs(wrappers);
|
||||
}),
|
||||
);
|
||||
if (prevRoot) this.restoreSession(prevRoot);
|
||||
if (this.manager.backend.onFsChanged)
|
||||
this.disposers.push(this.manager.backend.onFsChanged((e) => this.onFilesRemoved(e.removed)));
|
||||
@@ -454,6 +462,35 @@ export class EditorController implements SlotHost {
|
||||
|
||||
// --- session (open tabs, splits, carets per project) ---------------------------
|
||||
|
||||
private upgradeWrappedImageTabs(wrappers: ReadonlyMap<string, TextEditorWrapperContribution>) {
|
||||
const s = useTabs.getState();
|
||||
let changed = false;
|
||||
const panes: TabsState["panes"] = {};
|
||||
const toEnsure: string[] = [];
|
||||
for (const [paneId, pane] of Object.entries(s.panes)) {
|
||||
let paneChanged = false;
|
||||
const tabs = pane.tabs.map((tab) => {
|
||||
const path = tab.input.path;
|
||||
if (
|
||||
tab.input.kind === "image" &&
|
||||
path &&
|
||||
textEditorWrapperFor(wrappers, path) &&
|
||||
!pane.tabs.some((other) => other.id !== tab.id && other.input.key === fileKey(path))
|
||||
) {
|
||||
paneChanged = true;
|
||||
changed = true;
|
||||
if (pane.active === tab.id) toEnsure.push(tab.id);
|
||||
return { ...tab, input: fileInput(path) };
|
||||
}
|
||||
return tab;
|
||||
});
|
||||
panes[paneId] = paneChanged ? { ...pane, tabs } : pane;
|
||||
}
|
||||
if (!changed) return;
|
||||
useTabs.setState({ ...s, panes }, true);
|
||||
for (const tabId of toEnsure) void this.ensureSlot(tabId);
|
||||
}
|
||||
|
||||
private scheduleSessionSave() {
|
||||
if (!this.sessionRoot) return;
|
||||
clearTimeout(this.sessionTimer);
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
registerEditorKind,
|
||||
registerFileEditorProvider,
|
||||
registerTextEditorWrapper,
|
||||
textEditorWrapperFor,
|
||||
useTextEditorWrappers,
|
||||
type EditorInput,
|
||||
} from "@/workbench/contributions";
|
||||
import {
|
||||
@@ -85,7 +87,10 @@ export function activate(): () => void {
|
||||
registerFileEditorProvider({
|
||||
id: "viewers.image",
|
||||
order: 10,
|
||||
resolve: (path) => (isImagePath(path) ? imageInput(path) : null),
|
||||
resolve: (path) =>
|
||||
isImagePath(path) && !textEditorWrapperFor(useTextEditorWrappers.getState().wrappers, path)
|
||||
? imageInput(path)
|
||||
: null,
|
||||
}),
|
||||
registerFileEditorProvider({
|
||||
id: "viewers.hex",
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { registerTextEditorWrapper, resolveFileEditor } from "@/workbench/contributions";
|
||||
import { activate as activateViewers } from "./index";
|
||||
import { binaryInput, formatSize, imageInput, isImagePath, isMarkdownPath, isSvgPath, wantsHexView } from "./fileKinds";
|
||||
import {
|
||||
BYTES_PER_ROW,
|
||||
@@ -45,6 +47,22 @@ describe("file kinds", () => {
|
||||
expect(formatSize(1536)).toBe("1.5 KB");
|
||||
expect(formatSize(300 * 1024 * 1024)).toBe("300 MB");
|
||||
});
|
||||
|
||||
it("lets a text editor wrapper take precedence over the image viewer", async () => {
|
||||
const offViewers = activateViewers();
|
||||
const offWrapper = registerTextEditorWrapper({
|
||||
id: "plugin.acme.diagram.canvas",
|
||||
matches: (p) => p.endsWith(".diagram.svg"),
|
||||
component: () => null,
|
||||
});
|
||||
try {
|
||||
expect((await resolveFileEditor("/p/icon.svg"))?.input.kind).toBe("image");
|
||||
expect(await resolveFileEditor("/p/arch.diagram.svg")).toBeNull();
|
||||
} finally {
|
||||
offWrapper();
|
||||
offViewers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("hex model", () => {
|
||||
|
||||
Reference in new issue
Заблокувати користувача