дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-08 19:02:17 +02:00
Adds GDScript, Godot resource and shader file types with tree-sitter highlighting, the GDScript language server over TCP (a headless Godot editor started on demand), "Godot Project" / current-scene run configurations, and debugging through Godot's debug adapter. Breakpoints set through symlinked paths (/tmp, /var on macOS) were reported verified but never hit; the adapter now sends resolved paths and maps stack frames back to the paths the IDE opened. The duplicate exit event Godot sends after disconnect is ignored. Fixes #68 Claude-Session: https://claude.ai/code/session_01VUYsdXA2nkF5v6zsC93dpn
395 рядки
14 KiB
Rust
395 рядки
14 KiB
Rust
use ide_run::{RunConfig, detect};
|
|
use std::fs;
|
|
use std::path::Path;
|
|
use tempfile::TempDir;
|
|
|
|
fn project(files: &[(&str, &str)]) -> TempDir {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
for (path, content) in files {
|
|
let p = dir.path().join(path);
|
|
fs::create_dir_all(p.parent().unwrap()).unwrap();
|
|
fs::write(p, content).unwrap();
|
|
}
|
|
dir
|
|
}
|
|
|
|
fn find<'a>(configs: &'a [RunConfig], id: &str) -> &'a RunConfig {
|
|
configs
|
|
.iter()
|
|
.find(|c| c.id == id)
|
|
.unwrap_or_else(|| panic!("missing {id}; have {:?}", configs.iter().map(|c| &c.id).collect::<Vec<_>>()))
|
|
}
|
|
|
|
fn ids(configs: &[RunConfig]) -> Vec<&str> {
|
|
configs.iter().map(|c| c.id.as_str()).collect()
|
|
}
|
|
|
|
fn cmdline(c: &RunConfig) -> String {
|
|
std::iter::once(c.command.as_str()).chain(c.args.iter().map(String::as_str)).collect::<Vec<_>>().join(" ")
|
|
}
|
|
|
|
#[test]
|
|
fn empty_project_detects_nothing() {
|
|
let dir = tempfile::tempdir().unwrap();
|
|
assert!(detect(dir.path()).is_empty());
|
|
assert!(detect(Path::new("/definitely/not/here")).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn npm_scripts_with_lockfile_package_manager() {
|
|
let dir = project(&[
|
|
("package.json", r#"{"scripts":{"dev":"vite","test":"vitest","build":"vite build","test:e2e":"playwright"}}"#),
|
|
("pnpm-lock.yaml", ""),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["npm:build", "npm:dev", "npm:test", "npm:test:e2e"]);
|
|
let dev = find(&c, "npm:dev");
|
|
assert_eq!(cmdline(dev), "pnpm run dev");
|
|
assert!(dev.detected && !dev.is_test);
|
|
assert_eq!(dev.kind, "npm");
|
|
assert!(find(&c, "npm:test").is_test);
|
|
assert!(find(&c, "npm:test:e2e").is_test);
|
|
}
|
|
|
|
#[test]
|
|
fn package_manager_selection() {
|
|
for (lock, pm) in [("yarn.lock", "yarn"), ("bun.lockb", "bun"), ("bun.lock", "bun"), ("package-lock.json", "npm")] {
|
|
let dir = project(&[("package.json", r#"{"scripts":{"start":"node ."}}"#), (lock, "")]);
|
|
assert_eq!(find(&detect(dir.path()), "npm:start").command, pm, "{lock}");
|
|
}
|
|
let dir = project(&[("package.json", r#"{"packageManager":"[email protected]","scripts":{"start":"x"}}"#)]);
|
|
assert_eq!(find(&detect(dir.path()), "npm:start").command, "yarn");
|
|
}
|
|
|
|
#[test]
|
|
fn npm_workspace_packages() {
|
|
let dir = project(&[
|
|
("package.json", r#"{"workspaces":["packages/*"],"scripts":{"lint":"eslint"}}"#),
|
|
("packages/web/package.json", r#"{"name":"@acme/web","scripts":{"dev":"vite"}}"#),
|
|
("packages/api/package.json", r#"{"scripts":{"test":"jest"}}"#),
|
|
("packages/notapkg/readme.md", ""),
|
|
]);
|
|
let c = detect(dir.path());
|
|
let web = find(&c, "npm:@acme/web:dev");
|
|
assert_eq!(web.cwd.as_deref(), Some(Path::new("packages/web")));
|
|
assert_eq!(cmdline(web), "npm run dev");
|
|
let api = find(&c, "npm:packages/api:test");
|
|
assert!(api.is_test);
|
|
|
|
let dir = project(&[
|
|
("package.json", "{}"),
|
|
("pnpm-lock.yaml", ""),
|
|
("pnpm-workspace.yaml", "packages:\n - 'apps/*'\n - \"libs/core\"\n"),
|
|
("apps/site/package.json", r#"{"name":"site","scripts":{"build":"astro build"}}"#),
|
|
("libs/core/package.json", r#"{"name":"core","scripts":{"test":"vitest"}}"#),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(cmdline(find(&c, "npm:site:build")), "pnpm run build");
|
|
assert!(find(&c, "npm:core:test").is_test);
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_single_package() {
|
|
let dir =
|
|
project(&[("Cargo.toml", "[package]\nname = \"app\"\nversion = \"0.1.0\"\n"), ("src/main.rs", "fn main() {}")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["cargo:run:app", "cargo:test", "cargo:build"]);
|
|
let run = find(&c, "cargo:run:app");
|
|
assert_eq!(cmdline(run), "cargo run");
|
|
assert_eq!(run.debug_adapter.as_deref(), Some("codelldb"));
|
|
assert!(find(&c, "cargo:test").is_test);
|
|
assert_eq!(cmdline(find(&c, "cargo:test")), "cargo test");
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_multiple_bins() {
|
|
let dir = project(&[
|
|
(
|
|
"Cargo.toml",
|
|
"[package]\nname = \"tool\"\nversion = \"0.1.0\"\n\n[[bin]]\nname = \"special\"\npath = \"tools/special.rs\"\n",
|
|
),
|
|
("src/main.rs", ""),
|
|
("src/bin/extra.rs", ""),
|
|
("src/bin/multi/main.rs", ""),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(cmdline(find(&c, "cargo:run:tool")), "cargo run --bin tool");
|
|
assert_eq!(cmdline(find(&c, "cargo:run:special")), "cargo run --bin special");
|
|
assert_eq!(cmdline(find(&c, "cargo:run:extra")), "cargo run --bin extra");
|
|
assert_eq!(cmdline(find(&c, "cargo:run:multi")), "cargo run --bin multi");
|
|
}
|
|
|
|
#[test]
|
|
fn cargo_workspace() {
|
|
let dir = project(&[
|
|
("Cargo.toml", "[workspace]\nmembers = [\"crates/*\", \"cli\"]\nexclude = [\"crates/skip\"]\n"),
|
|
("crates/core/Cargo.toml", "[package]\nname = \"core\"\n"),
|
|
("crates/core/src/lib.rs", ""),
|
|
("crates/skip/Cargo.toml", "[package]\nname = \"skip\"\n"),
|
|
("crates/skip/src/main.rs", ""),
|
|
("cli/Cargo.toml", "[package]\nname = \"openidea-cli\"\n"),
|
|
("cli/src/main.rs", ""),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["cargo:run:openidea-cli", "cargo:test", "cargo:build"]);
|
|
assert_eq!(cmdline(&c[0]), "cargo run -p openidea-cli --bin openidea-cli");
|
|
assert_eq!(cmdline(find(&c, "cargo:test")), "cargo test --workspace");
|
|
}
|
|
|
|
#[test]
|
|
fn gradle_projects() {
|
|
let dir = project(&[
|
|
("build.gradle.kts", "plugins {\n application\n id(\"org.springframework.boot\") version \"3.2.0\"\n}\n"),
|
|
("gradlew", "#!/bin/sh"),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["gradle:build", "gradle:test", "gradle:run", "gradle:bootRun"]);
|
|
if cfg!(unix) {
|
|
assert_eq!(cmdline(find(&c, "gradle:bootRun")), "./gradlew bootRun");
|
|
}
|
|
assert!(find(&c, "gradle:test").is_test);
|
|
|
|
let dir = project(&[("build.gradle", "plugins { id 'java' }\n")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["gradle:build", "gradle:test"]);
|
|
assert_eq!(c[0].command, "gradle");
|
|
|
|
let dir = project(&[("build.gradle", "apply plugin: 'application'\n")]);
|
|
assert!(detect(dir.path()).iter().any(|c| c.id == "gradle:run"));
|
|
|
|
// Multi-module: Spring Boot applied in a subproject.
|
|
let dir = project(&[
|
|
("settings.gradle", "include 'app'"),
|
|
("build.gradle", ""),
|
|
("app/build.gradle", "plugins { id 'org.springframework.boot' version '3.2.0' }"),
|
|
]);
|
|
assert!(detect(dir.path()).iter().any(|c| c.id == "gradle:bootRun"));
|
|
}
|
|
|
|
#[test]
|
|
fn maven_projects() {
|
|
let pom = "<project><build><plugins><plugin><groupId>org.springframework.boot</groupId>\
|
|
<artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>";
|
|
let dir = project(&[("pom.xml", pom), ("mvnw", "#!/bin/sh")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["maven:package", "maven:test", "maven:spring-boot:run"]);
|
|
if cfg!(unix) {
|
|
assert_eq!(cmdline(find(&c, "maven:test")), "./mvnw test");
|
|
}
|
|
assert!(find(&c, "maven:test").is_test);
|
|
|
|
let dir = project(&[("pom.xml", "<project/>")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["maven:package", "maven:test"]);
|
|
assert_eq!(c[0].command, "mvn");
|
|
}
|
|
|
|
#[test]
|
|
fn go_projects() {
|
|
let dir = project(&[
|
|
("go.mod", "module example.com/x\n"),
|
|
("main.go", "// comment\npackage main\n\nfunc main() {}\n"),
|
|
("main_test.go", "package main\n"),
|
|
("cmd/server/main.go", "package main\n"),
|
|
("cmd/lib/lib.go", "package lib\n"),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["go:run", "go:run:server", "go:test"]);
|
|
assert_eq!(cmdline(find(&c, "go:run:server")), "go run ./cmd/server");
|
|
assert_eq!(cmdline(find(&c, "go:test")), "go test ./...");
|
|
assert!(find(&c, "go:test").is_test);
|
|
assert_eq!(find(&c, "go:run").debug_adapter.as_deref(), Some("delve"));
|
|
|
|
let dir = project(&[("go.mod", "module x\n"), ("lib.go", "package x\n")]);
|
|
assert_eq!(ids(&detect(dir.path())), ["go:test"]);
|
|
}
|
|
|
|
#[test]
|
|
fn python_projects() {
|
|
let dir = project(&[
|
|
("pyproject.toml", "[tool.pytest.ini_options]\naddopts = \"-q\"\n"),
|
|
("uv.lock", ""),
|
|
("src/mypkg/__init__.py", ""),
|
|
("src/mypkg/__main__.py", ""),
|
|
("manage.py", ""),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["django:runserver", "python:module:mypkg", "pytest"]);
|
|
let pytest = find(&c, "pytest");
|
|
assert_eq!(cmdline(pytest), "uv run pytest");
|
|
assert!(pytest.is_test);
|
|
assert_eq!(pytest.debug_adapter.as_deref(), Some("debugpy"));
|
|
assert_eq!(cmdline(find(&c, "python:module:mypkg")), "uv run python -m mypkg");
|
|
assert_eq!(cmdline(find(&c, "django:runserver")), "uv run python manage.py runserver");
|
|
|
|
let dir = project(&[("pytest.ini", "[pytest]\n"), ("poetry.lock", "")]);
|
|
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), "poetry run pytest");
|
|
|
|
let dir = project(&[("setup.cfg", "[tool:pytest]\ntestpaths = tests\n")]);
|
|
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), "pytest");
|
|
|
|
let dir = project(&[("requirements.txt", "requests\n"), ("app/__main__.py", "")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["python:module:app"]);
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn python_virtualenv() {
|
|
let dir = project(&[("tox.ini", ""), (".venv/bin/python", ""), (".venv/bin/pytest", ""), ("manage.py", "")]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(cmdline(find(&c, "pytest")), ".venv/bin/pytest");
|
|
assert_eq!(cmdline(find(&c, "django:runserver")), ".venv/bin/python manage.py runserver");
|
|
}
|
|
|
|
#[cfg(unix)]
|
|
#[test]
|
|
fn bare_virtualenv_marks_a_python_project() {
|
|
// No packaging files: the venv alone says which interpreter / pytest to use.
|
|
let dir = project(&[(".venv/bin/python", ""), (".venv/bin/pytest", ""), ("worker/test_w.py", "")]);
|
|
assert_eq!(cmdline(find(&detect(dir.path()), "pytest")), ".venv/bin/pytest");
|
|
let dir = project(&[(".venv/bin/python", "")]);
|
|
assert!(detect(dir.path()).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn makefile_targets() {
|
|
let makefile = "\
|
|
CC := gcc
|
|
VERSION = 1.0:beta
|
|
.PHONY: all test clean
|
|
export PATH := $(PATH)
|
|
|
|
all: build test
|
|
build: main.o
|
|
\tgcc -o app main.o
|
|
%.o: %.c
|
|
\t$(CC) -c $<
|
|
test::
|
|
\t./run-tests
|
|
clean lint: ; rm -rf out
|
|
$(OUT)/x: y
|
|
out/file: z
|
|
define TEMPLATE
|
|
inner: x
|
|
endef
|
|
# comment: no
|
|
";
|
|
let dir = project(&[("Makefile", makefile)]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["make:all", "make:build", "make:test", "make:clean", "make:lint"]);
|
|
assert_eq!(cmdline(find(&c, "make:build")), "make build");
|
|
assert!(find(&c, "make:test").is_test);
|
|
}
|
|
|
|
#[test]
|
|
fn deno_tasks() {
|
|
let dir = project(&[(
|
|
"deno.jsonc",
|
|
"{\n // tasks\n \"tasks\": {\n \"dev\": \"deno run -A main.ts\", /* inline */\n \"test\": \"deno test\",\n },\n \"url\": \"https://x.dev//y\"\n}\n",
|
|
)]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["deno:dev", "deno:test"]);
|
|
assert_eq!(cmdline(&c[0]), "deno task dev");
|
|
assert!(c[1].is_test);
|
|
}
|
|
|
|
#[test]
|
|
fn godot_projects_run_their_main_scene() {
|
|
let dir = project(&[("project.godot", "config_version=5\n"), ("demo/project.godot", "config_version=5\n")]);
|
|
let c = detect(dir.path());
|
|
let root = find(&c, "godot:project");
|
|
assert_eq!((root.name.as_str(), root.kind.as_str()), ("Godot Project", "godot"));
|
|
assert_eq!(cmdline(root), "godot --path .");
|
|
assert_eq!(root.cwd, None);
|
|
assert_eq!(root.debug_adapter.as_deref(), Some("godot"));
|
|
let demo = find(&c, "godot:project:demo");
|
|
assert_eq!(demo.name, "Godot Project (demo)");
|
|
assert_eq!(demo.cwd, Some(std::path::PathBuf::from("demo").into()));
|
|
assert!(detect(project(&[("main.gd", "extends Node\n")]).path()).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn just_recipes() {
|
|
let justfile = "\
|
|
set shell := [\"bash\", \"-c\"]
|
|
alias b := build
|
|
version := \"1.0\"
|
|
export RUST_LOG := \"info\"
|
|
|
|
# Build it
|
|
build target='debug':
|
|
cargo build
|
|
@test *args: build
|
|
cargo test {{args}}
|
|
_hidden:
|
|
echo
|
|
[private]
|
|
secret:
|
|
echo
|
|
serve port=\"8080\": (build \"release\")
|
|
./serve
|
|
";
|
|
let dir = project(&[("justfile", justfile)]);
|
|
let c = detect(dir.path());
|
|
assert_eq!(ids(&c), ["just:build", "just:test", "just:serve"]);
|
|
assert_eq!(cmdline(&c[1]), "just test");
|
|
assert!(c[1].is_test);
|
|
}
|
|
|
|
#[test]
|
|
fn ordering_is_stable_across_ecosystems() {
|
|
let dir = project(&[
|
|
("Makefile", "all:\n"),
|
|
("package.json", r#"{"scripts":{"dev":"x"}}"#),
|
|
("Cargo.toml", "[package]\nname = \"a\"\n"),
|
|
("src/main.rs", ""),
|
|
("go.mod", "module a\n"),
|
|
]);
|
|
let first = ids(&detect(dir.path())).into_iter().map(str::to_owned).collect::<Vec<_>>();
|
|
assert_eq!(first, ["npm:dev", "cargo:run:a", "cargo:test", "cargo:build", "go:test", "make:all"]);
|
|
for _ in 0..3 {
|
|
assert_eq!(detect(dir.path()).iter().map(|c| c.id.clone()).collect::<Vec<_>>(), first);
|
|
}
|
|
assert!(detect(dir.path()).iter().all(|c| c.detected));
|
|
}
|
|
|
|
#[test]
|
|
fn nested_packages_in_polyglot_repo() {
|
|
let dir = project(&[
|
|
("Makefile", "all:\n\techo\n"),
|
|
("web/package.json", r#"{"scripts":{"dev":"vite","test":"vitest"}}"#),
|
|
("web/pnpm-lock.yaml", ""),
|
|
("api/go.mod", "module x\n"),
|
|
("api/main.go", "package main\nfunc main() {}\n"),
|
|
("node_modules/x/package.json", r#"{"scripts":{"no":"x"}}"#),
|
|
]);
|
|
let c = detect(dir.path());
|
|
let dev = find(&c, "npm:web:dev");
|
|
assert_eq!(cmdline(dev), "pnpm run dev");
|
|
assert_eq!(dev.cwd.as_deref(), Some(Path::new("web")));
|
|
assert_eq!(dev.name, "web: pnpm run dev");
|
|
assert!(find(&c, "npm:web:test").is_test);
|
|
let run = find(&c, "go:run:api");
|
|
assert_eq!(run.cwd.as_deref(), Some(Path::new("api")));
|
|
assert_eq!(run.name, "api: go run .");
|
|
let test = find(&c, "go:test:api");
|
|
assert!(test.is_test);
|
|
assert_eq!(cmdline(test), "go test ./...");
|
|
assert!(!ids(&c).iter().any(|id| id.contains("no")));
|
|
}
|
|
|
|
#[test]
|
|
fn root_package_disables_nested_scan() {
|
|
let dir = project(&[
|
|
("package.json", r#"{"scripts":{"build":"tsc"}}"#),
|
|
("tools/package.json", r#"{"scripts":{"gen":"x"}}"#),
|
|
("go.mod", "module x\n"),
|
|
("svc/go.mod", "module y\n"),
|
|
]);
|
|
let c = detect(dir.path());
|
|
assert!(!ids(&c).contains(&"npm:tools:gen"));
|
|
assert!(ids(&c).contains(&"go:test"));
|
|
assert!(!ids(&c).contains(&"go:test:svc"));
|
|
}
|