дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
feat(csharp): navigate to decompiled assembly definitions (#1689)
Цей коміт міститься в:
1 джерело
0f0458b966
коміт
c3e8f20f53
10 files changed
+333
-13
No files matched your search
@@ -797,6 +797,9 @@ impl LspManager {
|
||||
if server.name == "gopls" {
|
||||
params["capabilities"]["experimental"]["interactiveResolve"] = json!({ "inputTypes": ["string"] });
|
||||
}
|
||||
if server.name == "csharp-ls" {
|
||||
params["capabilities"]["experimental"]["csharp"]["metadataUris"] = json!(true);
|
||||
}
|
||||
if !init_options.is_null() {
|
||||
params["initializationOptions"] = init_options;
|
||||
}
|
||||
@@ -1092,7 +1095,15 @@ impl LspManager {
|
||||
cancel: Option<&CancellationToken>,
|
||||
timeout: Duration,
|
||||
) -> IdeResult<R> {
|
||||
let fut = t.run.peer.request::<P, R>(method, params, cancel.cloned());
|
||||
let fut = async {
|
||||
if t.server.name == "csharp-ls" {
|
||||
let mut value = t.run.peer.request::<P, Value>(method, params, cancel.cloned()).await?;
|
||||
virtual_doc::normalize_csharp_uris(&mut value);
|
||||
serde_json::from_value(value).map_err(|e| RpcError::Protocol(format!("{method}: {e}")))
|
||||
} else {
|
||||
t.run.peer.request::<P, R>(method, params, cancel.cloned()).await
|
||||
}
|
||||
};
|
||||
match tokio::time::timeout(timeout, fut).await {
|
||||
Ok(r) => r.map_err(|e| rpc_err(&t.server.name, method, e)),
|
||||
Err(_) => Err(rpc_err(&t.server.name, method, RpcError::Timeout)),
|
||||
@@ -2630,7 +2641,14 @@ impl LspManager {
|
||||
};
|
||||
if let Some((entry, text)) = found {
|
||||
let title = entry.rsplit('/').next().unwrap_or(&entry).to_owned();
|
||||
return Ok(self.store_virtual(uri, text, Some(title), &[], self.origin_server(uri).map(|s| s.id)));
|
||||
return Ok(self.store_virtual(
|
||||
uri,
|
||||
text,
|
||||
Some(title),
|
||||
None,
|
||||
&[],
|
||||
self.origin_server(uri).map(|s| s.id),
|
||||
));
|
||||
}
|
||||
}
|
||||
let class_file = scheme == "jdt" || (scheme == "file" && uri.ends_with(".class"));
|
||||
@@ -2641,6 +2659,7 @@ impl LspManager {
|
||||
let mut error = None;
|
||||
for t in &targets {
|
||||
let languages = t.server.languages();
|
||||
let mut description = None;
|
||||
let serves_scheme = t.caps().content_schemes().contains(&scheme);
|
||||
let result: IdeResult<Option<String>> = if serves_scheme && !class_file {
|
||||
self.call::<_, Option<Value>>(
|
||||
@@ -2655,6 +2674,21 @@ impl LspManager {
|
||||
} else if class_file && languages.iter().any(|l| l.as_str() == "java") {
|
||||
self.call::<_, Option<String>>(t, "java/classFileContents", json!({ "uri": uri }), None, LONG_TIMEOUT)
|
||||
.await
|
||||
} else if scheme == "csharp" && t.server.name == "csharp-ls" {
|
||||
self.call::<_, Option<virtual_doc::CSharpMetadata>>(
|
||||
t,
|
||||
"csharp/metadata",
|
||||
json!({ "textDocument": { "uri": uri } }),
|
||||
None,
|
||||
LONG_TIMEOUT,
|
||||
)
|
||||
.await
|
||||
.map(|metadata| {
|
||||
metadata.map(|m| {
|
||||
description = m.assembly_name.filter(|name| !name.is_empty());
|
||||
m.source
|
||||
})
|
||||
})
|
||||
} else if jar_class && t.caps().commands().iter().any(|c| c == "decompile") {
|
||||
// The official Kotlin LSP: `decompile` answers `{ code, language }`.
|
||||
self.call::<_, Option<Value>>(
|
||||
@@ -2685,7 +2719,7 @@ impl LspManager {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
return Ok(self.store_virtual(uri, text, None, &languages, Some(t.server.id)));
|
||||
return Ok(self.store_virtual(uri, text, None, description, &languages, Some(t.server.id)));
|
||||
}
|
||||
if jar_class && error.is_none() {
|
||||
return Err(IdeError::not_found(format!(
|
||||
@@ -2702,10 +2736,15 @@ impl LspManager {
|
||||
uri: &str,
|
||||
text: String,
|
||||
title: Option<String>,
|
||||
description: Option<String>,
|
||||
languages: &[LanguageId],
|
||||
server: Option<ide_core::ServerId>,
|
||||
) -> VirtualDocument {
|
||||
let decompiled = virtual_doc::is_decompiled(&text);
|
||||
let decompiled = if virtual_doc::scheme(uri) == "csharp" {
|
||||
virtual_doc::is_csharp_decompiled(uri)
|
||||
} else {
|
||||
virtual_doc::is_decompiled(&text)
|
||||
};
|
||||
let title = title.unwrap_or_else(|| virtual_doc::title(uri, decompiled));
|
||||
let detected = self.languages.detect(Path::new(&title), None);
|
||||
let language_id = if detected == LanguageId::plain_text() {
|
||||
@@ -2718,7 +2757,7 @@ impl LspManager {
|
||||
text,
|
||||
language_id,
|
||||
title,
|
||||
description: virtual_doc::description(uri),
|
||||
description: description.or_else(|| virtual_doc::description(uri)),
|
||||
decompiled,
|
||||
};
|
||||
if let Some(server) = server {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
//! and shown together.
|
||||
|
||||
use crate::server::{Running, Server};
|
||||
use crate::{LspManager, convert};
|
||||
use crate::{LspManager, convert, virtual_doc};
|
||||
use ide_core::{DocId, IdeEvent};
|
||||
use ide_jsonrpc::RpcError;
|
||||
use lsp_types as lsp;
|
||||
@@ -43,6 +43,10 @@ impl LspManager {
|
||||
let targets: Vec<PullTarget> = {
|
||||
let docs = self.docs.lock();
|
||||
let Some(d) = docs.get(&doc) else { return };
|
||||
// Decompiled C# is a source view, not compilable project code (e.g. Unity's older language version).
|
||||
if virtual_doc::is_csharp_decompiled(d.uri.as_str()) {
|
||||
return;
|
||||
}
|
||||
d.servers
|
||||
.iter()
|
||||
.filter(|a| only.is_none_or(|s| s.id == a.server.id))
|
||||
|
||||
@@ -2,7 +2,68 @@
|
||||
//! URI (library classes from `jdt://`, disk classes from `file://`, or
|
||||
//! `workspace/textDocumentContent`).
|
||||
|
||||
use percent_encoding::percent_decode_str;
|
||||
use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode};
|
||||
|
||||
const URI_ESCAPES: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
.add(b'<')
|
||||
.add(b'>')
|
||||
.add(b'[')
|
||||
.add(b']')
|
||||
.add(b'\\')
|
||||
.add(b'^')
|
||||
.add(b'`')
|
||||
.add(b'{')
|
||||
.add(b'|')
|
||||
.add(b'}');
|
||||
|
||||
/// csharp-ls uses .NET Uri.ToString(), which can leave spaces and Unicode unescaped.
|
||||
/// Normalize only document URI fields, before lsp-types' strict URI deserialization.
|
||||
pub(crate) fn normalize_csharp_uris(value: &mut serde_json::Value) {
|
||||
match value {
|
||||
serde_json::Value::Array(values) => values.iter_mut().for_each(normalize_csharp_uris),
|
||||
serde_json::Value::Object(fields) => {
|
||||
for (key, value) in fields {
|
||||
// Opaque payloads must survive resolve/execute round trips unchanged.
|
||||
if matches!(key.as_str(), "data" | "arguments") {
|
||||
continue;
|
||||
}
|
||||
if matches!(key.as_str(), "uri" | "targetUri")
|
||||
&& let Some(uri) = value.as_str().filter(|s| s.starts_with("csharp:"))
|
||||
{
|
||||
*value = utf8_percent_encode(uri, URI_ESCAPES).to_string().into();
|
||||
} else {
|
||||
normalize_csharp_uris(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub(crate) struct CSharpMetadata {
|
||||
pub source: String,
|
||||
pub assembly_name: Option<String>,
|
||||
}
|
||||
|
||||
/// csharp-ls serves both decompiled and source-generated documents through metadata.
|
||||
pub(crate) fn is_csharp_decompiled(uri: &str) -> bool {
|
||||
if scheme(uri) != "csharp" {
|
||||
return false;
|
||||
}
|
||||
let segs = segments(uri);
|
||||
// The kind follows the project file, not a directory that happens to have this name.
|
||||
if let Some(kind) = segs.windows(2).find_map(|s| {
|
||||
(s[0].ends_with(".csproj") && matches!(s[1].as_str(), "decompiled" | "generated")).then_some(s[1].as_str())
|
||||
}) {
|
||||
return kind == "decompiled";
|
||||
}
|
||||
// Older csharp-ls versions used csharp:/metadata/... for decompiled symbols.
|
||||
segs.first().is_some_and(|s| s == "metadata")
|
||||
}
|
||||
|
||||
/// Recognize decompiler headers, without treating disassembler stubs as Java source.
|
||||
pub fn is_decompiled(text: &str) -> bool {
|
||||
@@ -107,6 +168,48 @@ mod tests {
|
||||
|
||||
const JDT: &str = "jdt://contents/spring-boot-4.1.1.jar/org.springframework.boot/SpringApplication.class?=spring-demo/%5C/Users%5C/me%5C/.m2%5C/repository%5C/spring-boot-4.1.1.jar%3Corg.springframework.boot%28SpringApplication.class";
|
||||
|
||||
#[test]
|
||||
fn csharp_metadata_titles_and_kinds() {
|
||||
for (uri, expected) in [
|
||||
("csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs", true),
|
||||
("csharp:/home/me/%E9%A1%B9%E7%9B%AE/App.csproj/decompiled/System.String.cs", true),
|
||||
("csharp:/metadata/projects/App/assemblies/System.Runtime/symbols/System.String.cs", true),
|
||||
("csharp:/work/App.csproj/generated/Generator/Player.g.cs", false),
|
||||
("csharp:/work/decompiled/App.csproj/generated/Generator/Player.g.cs", false),
|
||||
("csharp:/metadata/App.csproj/generated/Player.g.cs", false),
|
||||
("csharp:/work/App.csproj/generated/decompiled/Player.g.cs", false),
|
||||
("file:///work/App.csproj/decompiled/Player.cs", false),
|
||||
] {
|
||||
assert_eq!(is_csharp_decompiled(uri), expected, "{uri}");
|
||||
}
|
||||
assert_eq!(
|
||||
title("csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs", true),
|
||||
"UnityEngine.MonoBehaviour.cs"
|
||||
);
|
||||
assert_eq!(title("csharp:/home/me/App.csproj/decompiled/Example.%E7%B1%BB.cs", true), "Example.类.cs");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn csharp_uri_encoding_preserves_escapes_and_other_response_fields() {
|
||||
let uri = "csharp:/D:/My Game/项目 [1]/App%23%25.csproj/decompiled/Example.C%601.cs";
|
||||
let encoded = "csharp:/D:/My%20Game/%E9%A1%B9%E7%9B%AE%20%5B1%5D/App%23%25.csproj/decompiled/Example.C%601.cs";
|
||||
let mut response = serde_json::json!([
|
||||
{ "uri": uri, "source": uri },
|
||||
{ "location": { "targetUri": uri } },
|
||||
{ "uri": encoded },
|
||||
{ "uri": "file:///D:/project/App.cs" },
|
||||
{ "uri": "jdt://contents/a.jar/p/C.class?=p/%5C/a.jar" },
|
||||
{ "data": { "uri": uri }, "command": { "arguments": [{ "targetUri": uri }] } }
|
||||
]);
|
||||
let original = response.clone();
|
||||
normalize_csharp_uris(&mut response);
|
||||
assert_eq!(response[0]["uri"], encoded);
|
||||
assert_eq!(response[0]["source"], uri);
|
||||
assert_eq!(response[1]["location"]["targetUri"], encoded);
|
||||
assert_eq!(response.as_array().unwrap()[2..], original.as_array().unwrap()[2..]);
|
||||
assert!(encoded.parse::<lsp_types::Uri>().is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jdt_titles() {
|
||||
assert_eq!(title(JDT, true), "SpringApplication.class");
|
||||
|
||||
@@ -2253,6 +2253,106 @@ async fn noop_handler_is_usable_for_raw_peers() {
|
||||
|
||||
// ------------------------------------------------------------------ virtual documents
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn csharp_metadata_navigation_and_generated_documents() {
|
||||
let mut caps = incremental_caps();
|
||||
caps["diagnosticProvider"] = json!({ "interFileDependencies": false, "workspaceDiagnostics": false });
|
||||
let h = harness_for("csharp", "cs", &[("other-csharp", incremental_caps()), ("csharp-ls", caps)], &[]);
|
||||
let (doc, path) = h.open("Player.cs", "class Player : MonoBehaviour {}\n");
|
||||
h.wait_status("other-csharp", ServerStatus::Ready).await;
|
||||
h.wait_status("csharp-ls", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("csharp-ls");
|
||||
let other = h.launcher.state("other-csharp");
|
||||
eventually(|| !s.requested("textDocument/diagnostic").is_empty()).await;
|
||||
assert_eq!(s.requested("initialize")[0]["capabilities"]["experimental"]["csharp"]["metadataUris"], true);
|
||||
assert!(other.requested("initialize")[0]["capabilities"]["experimental"]["csharp"].is_null());
|
||||
|
||||
let uri = "csharp:/D:/My%20Game/Game.csproj/decompiled/UnityEngine.MonoBehaviour.cs";
|
||||
let text = "namespace UnityEngine;\npublic class MonoBehaviour {\n public void CancelInvoke() {}\n}\n";
|
||||
s.respond("csharp/metadata", json!({ "assemblyName": "UnityEngine.CoreModule", "source": text }));
|
||||
// Both Location and LocationLink preserve the server's type/member selection.
|
||||
for (response, range) in [
|
||||
(
|
||||
json!({ "uri": uri.replace("%20", " "), "range": {"start":{"line":1,"character":13},"end":{"line":1,"character":26}} }),
|
||||
rng(1, 13, 1, 26),
|
||||
),
|
||||
(
|
||||
json!([{ "targetUri": uri, "targetRange": {"start":{"line":0,"character":0},"end":{"line":3,"character":1}}, "targetSelectionRange": {"start":{"line":2,"character":16},"end":{"line":2,"character":28}} }]),
|
||||
rng(2, 16, 2, 28),
|
||||
),
|
||||
] {
|
||||
s.respond("textDocument/definition", response);
|
||||
let defs = h.mgr.goto(doc, Pos::new(0, 16), GotoKind::Definition).await.unwrap();
|
||||
assert_eq!(defs, vec![ide_core::Location { path: uri.into(), range, uri: Some(uri.into()) }]);
|
||||
let vd = h.mgr.virtual_document(uri).await.unwrap();
|
||||
assert_eq!(vd.text, text);
|
||||
assert_eq!(vd.title, "UnityEngine.MonoBehaviour.cs");
|
||||
assert_eq!(vd.language_id.as_str(), "csharp");
|
||||
assert_eq!(vd.description.as_deref(), Some("UnityEngine.CoreModule"));
|
||||
assert!(vd.decompiled);
|
||||
}
|
||||
assert_eq!(s.requested("csharp/metadata"), vec![json!({"textDocument":{"uri":uri}})]);
|
||||
assert!(other.requested("csharp/metadata").is_empty());
|
||||
|
||||
let vdoc = DocId::next();
|
||||
h.mgr.did_open_virtual(vdoc, uri, &h.lang, 0, text);
|
||||
eventually(|| s.docs.lock().contains_key(uri)).await;
|
||||
s.respond("textDocument/hover", json!({"contents":"class MonoBehaviour"}));
|
||||
h.mgr.hover(vdoc, Pos::new(1, 13), Default::default()).await.unwrap();
|
||||
assert_eq!(s.requested("textDocument/hover").last().unwrap()["textDocument"]["uri"], uri);
|
||||
assert!(other.requested("textDocument/hover").is_empty());
|
||||
h.mgr.did_close(vdoc);
|
||||
eventually(|| !s.docs.lock().contains_key(uri)).await;
|
||||
assert!(s.notified("textDocument/didChange").is_empty());
|
||||
|
||||
// Generated sources use the same request but are not decompiled, even under a directory named decompiled.
|
||||
let generated = "csharp:/work/decompiled/Game.csproj/generated/Generator/Player.g.cs";
|
||||
s.respond("csharp/metadata", json!({"source":"partial class Player {}"}));
|
||||
let vd = h.mgr.virtual_document(generated).await.unwrap();
|
||||
assert_eq!(vd.title, "Player.g.cs");
|
||||
assert_eq!(vd.language_id.as_str(), "csharp");
|
||||
assert!(vd.description.is_none());
|
||||
assert!(!vd.decompiled);
|
||||
// Generated and ordinary source still get diagnostics; decompiled assemblies do not.
|
||||
let generated_doc = DocId::next();
|
||||
h.mgr.did_open_virtual(generated_doc, generated, &vd.language_id, 0, &vd.text);
|
||||
eventually(|| s.requested("textDocument/diagnostic").iter().any(|p| p["textDocument"]["uri"] == generated)).await;
|
||||
assert!(s.requested("textDocument/diagnostic").iter().all(|p| p["textDocument"]["uri"] != uri));
|
||||
h.mgr.did_close(generated_doc);
|
||||
|
||||
// Ordinary definitions still resolve to a disk path.
|
||||
s.respond(
|
||||
"textDocument/definition",
|
||||
json!({"uri":uri_of(&path),"range":{"start":{"line":0,"character":6},"end":{"line":0,"character":12}}}),
|
||||
);
|
||||
let defs = h.mgr.goto(doc, Pos::new(0, 6), GotoKind::Definition).await.unwrap();
|
||||
assert_eq!(defs[0].path, path);
|
||||
assert!(defs[0].uri.is_none());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn csharp_metadata_failures_can_be_retried() {
|
||||
let h = harness_for("csharp", "cs", &[("csharp-ls", incremental_caps())], &[]);
|
||||
h.open("App.cs", "class App {}\n");
|
||||
h.wait_status("csharp-ls", ServerStatus::Ready).await;
|
||||
let s = h.launcher.state("csharp-ls");
|
||||
let uri = "csharp:/work/App.csproj/decompiled/System.String.cs";
|
||||
for response in [Value::Null, json!({"source":""})] {
|
||||
s.respond("csharp/metadata", response);
|
||||
let err = h.mgr.virtual_document(uri).await.unwrap_err();
|
||||
assert_eq!(err.kind, ide_core::error::ErrorKind::NotFound);
|
||||
assert!(err.message.contains("System.String.cs"));
|
||||
}
|
||||
s.errors.lock().insert("csharp/metadata".into(), (-32603, "decompilation failed".into()));
|
||||
assert!(h.mgr.virtual_document(uri).await.unwrap_err().message.contains("decompilation failed"));
|
||||
s.errors.lock().remove("csharp/metadata");
|
||||
s.respond("csharp/metadata", json!({"source":"public class String {}", "assemblyName":""}));
|
||||
let vd = h.mgr.virtual_document(uri).await.unwrap();
|
||||
assert!(vd.decompiled);
|
||||
assert!(vd.description.is_none());
|
||||
assert_eq!(s.requested("csharp/metadata").len(), 4);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn virtual_locations_and_text_document_content() {
|
||||
let mut caps = incremental_caps();
|
||||
|
||||
@@ -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
Заблокувати користувача