дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 19:25:54 +02:00
feat(csharp): navigate to decompiled assembly definitions (#1689)
Цей коміт міститься в:
1 джерело
0f0458b966
коміт
c3e8f20f53
10 files changed
+333
-13
No files matched your search
@@ -126,6 +126,7 @@ const EXT_LANG: Record<string, string> = {
|
||||
kt: "kotlin",
|
||||
kts: "kotlin",
|
||||
java: "java",
|
||||
cs: "csharp",
|
||||
py: "python",
|
||||
gd: "gdscript",
|
||||
go: "go",
|
||||
|
||||
@@ -43,7 +43,11 @@ export function VirtualBanner({
|
||||
<div className={`${styles.banner} ${styles.libraryBanner}`} role="status">
|
||||
<Icon name={decompiled ? "info" : "lock"} size={14} className={styles.bannerIcon} />
|
||||
<span className={styles.virtualBannerText}>
|
||||
{decompiled ? tx("Decompiled .class file: sources are not available.") : tx("Library source (read-only).")}
|
||||
{decompiled
|
||||
? path.startsWith("csharp:")
|
||||
? tx("Decompiled assembly (read-only).")
|
||||
: tx("Decompiled .class file: sources are not available.")
|
||||
: tx("Library source (read-only).")}
|
||||
{reason && ` ${tx(reason)}`}
|
||||
</span>
|
||||
{description && (
|
||||
|
||||
@@ -2,9 +2,19 @@ import { describe, expect, it } from "vitest";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import { registerEditorBanner } from "@/workbench/contributions";
|
||||
import { useDocMeta, type DocMeta } from "../model/document";
|
||||
import { documentBanner, EditorBanners } from "./Banner";
|
||||
import { documentBanner, EditorBanners, VirtualBanner } from "./Banner";
|
||||
|
||||
describe("editor banners", () => {
|
||||
it.each([
|
||||
["jdt://contents/lib.jar/p/Lib.class", true, "Decompiled .class file: sources are not available."],
|
||||
["csharp:/p/App.csproj/decompiled/Lib.cs", true, "Decompiled assembly (read-only)."],
|
||||
["csharp:/p/App.csproj/generated/Lib.g.cs", false, "Library source (read-only)."],
|
||||
])("describes %s", (path, decompiled, text) => {
|
||||
render(<VirtualBanner path={path} decompiled={decompiled} description="Example.Library" />);
|
||||
expect(screen.getByRole("status").textContent).toContain(text);
|
||||
expect(screen.getByTitle("Example.Library")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("shows the document notice through the banner extension point, before contributed ones", () => {
|
||||
const off = [
|
||||
registerEditorBanner(documentBanner),
|
||||
|
||||
@@ -4,12 +4,23 @@ import type { VirtualDocument } from "@/platform/ipc";
|
||||
const JDT = "jdt://contents/lib-1.0.jar/org.example/Lib.class?=demo/%5C/m2%5C/lib-1.0.jar%3Corg.example%28Lib.class";
|
||||
const TEXT =
|
||||
"// Source code is unavailable, and was generated by the Fernflower decompiler.\npackage org.example;\n\npublic class Lib {\n void run() {}\n}\n";
|
||||
const CSHARP = "csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs";
|
||||
const CSHARP_TEXT = "namespace UnityEngine;\npublic class MonoBehaviour {\n public void CancelInvoke() {}\n}\n";
|
||||
|
||||
vi.mock("@/platform/virtualDocs", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("@/platform/virtualDocs")>();
|
||||
return {
|
||||
...actual,
|
||||
virtualDocument: vi.fn(async (uri: string): Promise<VirtualDocument> => {
|
||||
if (uri === CSHARP)
|
||||
return {
|
||||
uri,
|
||||
text: CSHARP_TEXT,
|
||||
languageId: "csharp",
|
||||
title: "UnityEngine.MonoBehaviour.cs",
|
||||
description: "UnityEngine.CoreModule",
|
||||
decompiled: true,
|
||||
};
|
||||
if (uri !== JDT && uri !== "file:///p/target/classes/Lib.class") throw new Error(`no contents for ${uri}`);
|
||||
return {
|
||||
uri,
|
||||
@@ -128,10 +139,56 @@ describe("virtual editor input", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("does not open a tab when the server has no contents", async () => {
|
||||
ctrl = new EditorController(createMemoryBackend());
|
||||
it("navigates C# types and members in one read-only assembly tab", async () => {
|
||||
const backend = createMemoryBackend({ "/p/Player.cs": "class Player {}\n", [CSHARP]: CSHARP_TEXT });
|
||||
const save = vi.spyOn(backend, "save");
|
||||
ctrl = new EditorController(backend);
|
||||
ctrl.start();
|
||||
await ctrl.openFile("jdt://contents/missing.jar/p/X.class", { focus: false });
|
||||
expect(Object.values(useTabs.getState().panes).flatMap((p) => p.tabs)).toHaveLength(0);
|
||||
await ctrl.openFile("/p/Player.cs", { focus: false });
|
||||
await ctrl.openFile(CSHARP, { selection: { line: 1, col: 13, endLine: 1, endCol: 26 }, focus: false });
|
||||
const tab = activeTab();
|
||||
const slot = ctrl.slotFor(tab.id)!;
|
||||
expect(tab.input).toMatchObject({
|
||||
kind: "virtual",
|
||||
path: CSHARP,
|
||||
data: { title: "UnityEngine.MonoBehaviour.cs", description: "UnityEngine.CoreModule", decompiled: true },
|
||||
});
|
||||
expect(slot.model.ctx()).toMatchObject({ language: "csharp", readOnly: true, virtual: true });
|
||||
const selected = () => {
|
||||
const state = slot.currentState();
|
||||
return state.sliceDoc(state.selection.main.from, state.selection.main.to);
|
||||
};
|
||||
expect(selected()).toBe("MonoBehaviour");
|
||||
await ctrl.openFile(CSHARP, { selection: { line: 2, col: 16, endLine: 2, endCol: 28 }, focus: false });
|
||||
expect(activeTab().id).toBe(tab.id);
|
||||
expect(selected()).toBe("CancelInvoke");
|
||||
expect(Object.values(useTabs.getState().panes).flatMap((p) => p.tabs)).toHaveLength(2);
|
||||
const doc = [...backend.docs.values()].find((d) => d.path === CSHARP)!;
|
||||
await expect(backend.edit(doc.id, doc.version, [{ from: 0, to: 0, insert: "x" }])).rejects.toMatchObject({
|
||||
kind: "permissionDenied",
|
||||
});
|
||||
await slot.model.save("explicit");
|
||||
expect(save).not.toHaveBeenCalled();
|
||||
expect((await backend.snapshot(doc.id)).text).toBe(CSHARP_TEXT);
|
||||
await ctrl.back();
|
||||
expect(activeTab().input.path).toBe(CSHARP);
|
||||
expect(slot.currentState().selection.main.head).toBe(slot.currentState().doc.line(2).from + 13);
|
||||
await ctrl.back();
|
||||
expect(activeTab().input.path).toBe("/p/Player.cs");
|
||||
await ctrl.forward();
|
||||
expect(activeTab().input.path).toBe(CSHARP);
|
||||
await ctrl.forward();
|
||||
expect(slot.currentState().selection.main.head).toBe(slot.currentState().doc.line(3).from + 16);
|
||||
expect(JSON.stringify(serializeSession(useTabs.getState(), {}))).not.toContain("csharp:");
|
||||
});
|
||||
|
||||
it.each(["jdt://contents/missing.jar/p/X.class", "csharp:/p/App.csproj/decompiled/Missing.cs"])(
|
||||
"does not open a tab when the server has no contents for %s",
|
||||
async (uri) => {
|
||||
ctrl = new EditorController(createMemoryBackend());
|
||||
ctrl.start();
|
||||
await ctrl.openFile(uri, { focus: false });
|
||||
expect(Object.values(useTabs.getState().panes).flatMap((p) => p.tabs)).toHaveLength(0);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"Cannot copy reference": "Cannot copy reference",
|
||||
"Decompiled assembly (read-only).": "Decompiled assembly (read-only).",
|
||||
"Connect through:": "Connect through:",
|
||||
"Connect through": "Connect through",
|
||||
"GitHub Commit Checks": "GitHub Commit Checks",
|
||||
|
||||
@@ -4715,6 +4715,7 @@
|
||||
"Vim rc file": "Vim rc 文件",
|
||||
"Breakpoints are not supported in output classes without source code.": "输出目录中无源码的类不支持断点。",
|
||||
"Decompiled .class file: sources are not available.": "已反编译的 .class 文件:没有可用的源代码。",
|
||||
"Decompiled assembly (read-only).": "已反编译的程序集(只读)。",
|
||||
"No column of the file matches the table": "文件中没有与该表匹配的列",
|
||||
"Not a URL of PostgreSQL, MySQL, SQLite or a compatible database": "不是 PostgreSQL、MySQL、SQLite 或兼容数据库的 URL",
|
||||
"The Database tree shows only the checked names and patterns (* and ?); the connected database always shows.": "数据库树只显示选中的名称和模式(* 和 ?);已连接的数据库始终显示。",
|
||||
|
||||
Reference in new issue
Заблокувати користувача