Файли
l0ng-ai e3dae02de7 feat(languages): Add Godot / GDScript support (#73)
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
2026-10-27 15:31:25 +08:00

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"));
}