use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LspConfig {
pub server_binary: String,
pub args: Vec<String>,
pub language_id: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum VerifierStage {
SyntaxCheck,
Build,
Test,
Lint,
}
impl std::fmt::Display for VerifierStage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerifierStage::SyntaxCheck => write!(f, "syntax_check"),
VerifierStage::Build => write!(f, "build"),
VerifierStage::Test => write!(f, "test"),
VerifierStage::Lint => write!(f, "lint"),
}
}
}
#[derive(Debug, Clone)]
pub struct VerifierCapability {
pub stage: VerifierStage,
pub command: Option<String>,
pub available: bool,
pub fallback_command: Option<String>,
pub fallback_available: bool,
}
impl VerifierCapability {
pub fn any_available(&self) -> bool {
self.available || self.fallback_available
}
pub fn effective_command(&self) -> Option<&str> {
if self.available {
self.command.as_deref()
} else if self.fallback_available {
self.fallback_command.as_deref()
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct LspCapability {
pub primary: LspConfig,
pub primary_available: bool,
pub fallback: Option<LspConfig>,
pub fallback_available: bool,
}
impl LspCapability {
pub fn effective_config(&self) -> Option<&LspConfig> {
if self.primary_available {
Some(&self.primary)
} else if self.fallback_available {
self.fallback.as_ref()
} else {
None
}
}
}
#[derive(Debug, Clone)]
pub struct VerifierProfile {
pub plugin_name: String,
pub capabilities: Vec<VerifierCapability>,
pub lsp: LspCapability,
}
impl VerifierProfile {
pub fn get(&self, stage: VerifierStage) -> Option<&VerifierCapability> {
self.capabilities.iter().find(|c| c.stage == stage)
}
pub fn available_stages(&self) -> Vec<VerifierStage> {
self.capabilities
.iter()
.filter(|c| c.any_available())
.map(|c| c.stage)
.collect()
}
pub fn fully_degraded(&self) -> bool {
self.capabilities.iter().all(|c| !c.any_available())
}
}
pub fn host_binary_available(binary: &str) -> bool {
std::process::Command::new(binary)
.arg("--version")
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
}
#[derive(Debug, Clone, Default)]
pub struct InitOptions {
pub name: String,
pub package_manager: Option<String>,
pub flags: Vec<String>,
pub is_empty_dir: bool,
}
#[derive(Debug, Clone)]
pub enum ProjectAction {
ExecCommand {
command: String,
description: String,
},
NoAction,
}
pub trait LanguagePlugin: Send + Sync {
fn name(&self) -> &str;
fn extensions(&self) -> &[&str];
fn key_files(&self) -> &[&str];
fn detect(&self, path: &Path) -> bool {
for key_file in self.key_files() {
if path.join(key_file).exists() {
return true;
}
}
if let Ok(entries) = std::fs::read_dir(path) {
for entry in entries.flatten() {
if let Some(ext) = entry.path().extension() {
let ext_str = ext.to_string_lossy();
if self.extensions().iter().any(|e| *e == ext_str) {
return true;
}
}
}
}
false
}
fn get_lsp_config(&self) -> LspConfig;
fn get_init_action(&self, opts: &InitOptions) -> ProjectAction;
fn check_tooling_action(&self, path: &Path) -> ProjectAction;
fn init_command(&self, opts: &InitOptions) -> String;
fn test_command(&self) -> String;
fn run_command(&self) -> String;
fn run_command_for_dir(&self, _path: &Path) -> String {
self.run_command()
}
fn syntax_check_command(&self) -> Option<String> {
None
}
fn build_command(&self) -> Option<String> {
None
}
fn lint_command(&self) -> Option<String> {
None
}
fn file_ownership_patterns(&self) -> &[&str] {
self.extensions()
}
fn owns_file(&self, path: &str) -> bool {
let path_lower = path.to_lowercase();
self.file_ownership_patterns().iter().any(|pattern| {
let pattern = pattern.trim_start_matches('*');
path_lower.ends_with(pattern)
})
}
fn host_tool_available(&self) -> bool {
true
}
fn required_binaries(&self) -> Vec<(&str, &str, &str)> {
Vec::new()
}
fn lsp_fallback(&self) -> Option<LspConfig> {
None
}
fn verifier_profile(&self) -> VerifierProfile {
let tool_available = self.host_tool_available();
let mut capabilities = Vec::new();
if let Some(cmd) = self.syntax_check_command() {
capabilities.push(VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some(cmd),
available: tool_available,
fallback_command: None,
fallback_available: false,
});
}
if let Some(cmd) = self.build_command() {
capabilities.push(VerifierCapability {
stage: VerifierStage::Build,
command: Some(cmd),
available: tool_available,
fallback_command: None,
fallback_available: false,
});
}
capabilities.push(VerifierCapability {
stage: VerifierStage::Test,
command: Some(self.test_command()),
available: tool_available,
fallback_command: None,
fallback_available: false,
});
if let Some(cmd) = self.lint_command() {
capabilities.push(VerifierCapability {
stage: VerifierStage::Lint,
command: Some(cmd),
available: tool_available,
fallback_command: None,
fallback_available: false,
});
}
let primary_config = self.get_lsp_config();
let primary_available = host_binary_available(&primary_config.server_binary);
let fallback = self.lsp_fallback();
let fallback_available = fallback
.as_ref()
.map(|f| host_binary_available(&f.server_binary))
.unwrap_or(false);
VerifierProfile {
plugin_name: self.name().to_string(),
capabilities,
lsp: LspCapability {
primary: primary_config,
primary_available,
fallback,
fallback_available,
},
}
}
}
pub struct RustPlugin;
impl LanguagePlugin for RustPlugin {
fn name(&self) -> &str {
"rust"
}
fn extensions(&self) -> &[&str] {
&["rs"]
}
fn key_files(&self) -> &[&str] {
&["Cargo.toml", "Cargo.lock"]
}
fn required_binaries(&self) -> Vec<(&str, &str, &str)> {
vec![
("cargo", "build/init", "Install Rust via https://rustup.rs"),
("rustc", "compiler", "Install Rust via https://rustup.rs"),
(
"rust-analyzer",
"language server",
"rustup component add rust-analyzer",
),
]
}
fn get_lsp_config(&self) -> LspConfig {
LspConfig {
server_binary: "rust-analyzer".to_string(),
args: vec![],
language_id: "rust".to_string(),
}
}
fn get_init_action(&self, opts: &InitOptions) -> ProjectAction {
let command = if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"cargo init .".to_string()
} else {
format!("cargo new {}", opts.name)
};
ProjectAction::ExecCommand {
command,
description: "Initialize Rust project with Cargo".to_string(),
}
}
fn check_tooling_action(&self, path: &Path) -> ProjectAction {
if !path.join("Cargo.lock").exists() && path.join("Cargo.toml").exists() {
ProjectAction::ExecCommand {
command: "cargo fetch".to_string(),
description: "Fetch Rust dependencies".to_string(),
}
} else {
ProjectAction::NoAction
}
}
fn init_command(&self, opts: &InitOptions) -> String {
if opts.name == "." || opts.name == "./" {
"cargo init .".to_string()
} else {
format!("cargo new {}", opts.name)
}
}
fn test_command(&self) -> String {
"cargo test".to_string()
}
fn run_command(&self) -> String {
"cargo run".to_string()
}
fn syntax_check_command(&self) -> Option<String> {
Some("cargo check".to_string())
}
fn build_command(&self) -> Option<String> {
Some("cargo build".to_string())
}
fn lint_command(&self) -> Option<String> {
Some("cargo clippy -- -D warnings".to_string())
}
fn file_ownership_patterns(&self) -> &[&str] {
&["rs", "Cargo.toml"]
}
fn host_tool_available(&self) -> bool {
host_binary_available("cargo")
}
fn verifier_profile(&self) -> VerifierProfile {
let cargo = host_binary_available("cargo");
let clippy = cargo;
let capabilities = vec![
VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some("cargo check".to_string()),
available: cargo,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Build,
command: Some("cargo build".to_string()),
available: cargo,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Test,
command: Some("cargo test".to_string()),
available: cargo,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Lint,
command: Some("cargo clippy -- -D warnings".to_string()),
available: clippy,
fallback_command: None,
fallback_available: false,
},
];
let primary = self.get_lsp_config();
let primary_available = host_binary_available(&primary.server_binary);
VerifierProfile {
plugin_name: self.name().to_string(),
capabilities,
lsp: LspCapability {
primary,
primary_available,
fallback: None,
fallback_available: false,
},
}
}
}
pub struct PythonPlugin;
impl LanguagePlugin for PythonPlugin {
fn name(&self) -> &str {
"python"
}
fn extensions(&self) -> &[&str] {
&["py"]
}
fn key_files(&self) -> &[&str] {
&["pyproject.toml", "setup.py", "requirements.txt", "uv.lock"]
}
fn required_binaries(&self) -> Vec<(&str, &str, &str)> {
vec![
(
"uv",
"package manager",
"curl -LsSf https://astral.sh/uv/install.sh | sh",
),
(
"python3",
"interpreter",
"uv python install (or install from https://python.org)",
),
(
"uvx",
"tool runner/LSP",
"Installed with uv — curl -LsSf https://astral.sh/uv/install.sh | sh",
),
]
}
fn get_lsp_config(&self) -> LspConfig {
LspConfig {
server_binary: "uvx".to_string(),
args: vec!["ty".to_string(), "server".to_string()],
language_id: "python".to_string(),
}
}
fn get_init_action(&self, opts: &InitOptions) -> ProjectAction {
let command = match opts.package_manager.as_deref() {
Some("poetry") => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"poetry init --no-interaction".to_string()
} else {
format!("poetry new {}", opts.name)
}
}
Some("pdm") => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"pdm init --non-interactive".to_string()
} else {
format!(
"mkdir -p {} && cd {} && pdm init --non-interactive",
opts.name, opts.name
)
}
}
_ => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"uv init --lib".to_string()
} else {
format!("uv init --lib {}", opts.name)
}
}
};
let description = match opts.package_manager.as_deref() {
Some("poetry") => "Initialize Python project with Poetry",
Some("pdm") => "Initialize Python project with PDM",
_ => "Initialize Python project with uv",
};
ProjectAction::ExecCommand {
command,
description: description.to_string(),
}
}
fn check_tooling_action(&self, path: &Path) -> ProjectAction {
let has_pyproject = path.join("pyproject.toml").exists();
let has_venv = path.join(".venv").exists();
let has_uv_lock = path.join("uv.lock").exists();
if has_pyproject && (!has_venv || !has_uv_lock) {
ProjectAction::ExecCommand {
command: "uv sync".to_string(),
description: "Sync Python dependencies with uv".to_string(),
}
} else {
ProjectAction::NoAction
}
}
fn init_command(&self, opts: &InitOptions) -> String {
if opts.package_manager.as_deref() == Some("poetry") {
if opts.name == "." || opts.name == "./" {
"poetry init".to_string()
} else {
format!("poetry new {}", opts.name)
}
} else {
format!("uv init --lib {}", opts.name)
}
}
fn test_command(&self) -> String {
"uv run pytest".to_string()
}
fn run_command(&self) -> String {
"uv run python -m main".to_string()
}
fn run_command_for_dir(&self, path: &Path) -> String {
if let Ok(entries) = std::fs::read_dir(path.join("src")) {
for entry in entries.flatten() {
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let name = entry.file_name().to_string_lossy().to_string();
if !name.starts_with('.') && !name.starts_with('_') {
return format!("uv run python -m {}", name);
}
}
}
}
if let Ok(content) = std::fs::read_to_string(path.join("pyproject.toml")) {
if content.contains("[project.scripts]") {
let mut in_scripts = false;
for raw_line in content.lines() {
let line = raw_line.trim();
if line == "[project.scripts]" {
in_scripts = true;
continue;
}
if in_scripts {
if line.starts_with('[') {
break;
}
if let Some((name, _)) = line.split_once('=') {
let script = name.trim().trim_matches('"');
if !script.is_empty() {
return format!("uv run {}", script);
}
}
}
}
}
}
"uv run python -m main".to_string()
}
fn syntax_check_command(&self) -> Option<String> {
Some("uvx ty check .".to_string())
}
fn lint_command(&self) -> Option<String> {
Some("uv run ruff check .".to_string())
}
fn file_ownership_patterns(&self) -> &[&str] {
&["py", "pyproject.toml", "setup.py", "requirements.txt"]
}
fn host_tool_available(&self) -> bool {
host_binary_available("uv")
}
fn lsp_fallback(&self) -> Option<LspConfig> {
Some(LspConfig {
server_binary: "pyright-langserver".to_string(),
args: vec!["--stdio".to_string()],
language_id: "python".to_string(),
})
}
fn verifier_profile(&self) -> VerifierProfile {
let uv = host_binary_available("uv");
let pyright = host_binary_available("pyright");
let capabilities = vec![
VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some("uvx ty check .".to_string()),
available: uv,
fallback_command: Some("pyright .".to_string()),
fallback_available: pyright,
},
VerifierCapability {
stage: VerifierStage::Build,
command: None,
available: true,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Test,
command: Some("uv run pytest".to_string()),
available: uv,
fallback_command: Some("python -m pytest".to_string()),
fallback_available: host_binary_available("python3")
|| host_binary_available("python"),
},
VerifierCapability {
stage: VerifierStage::Lint,
command: Some("uv run ruff check .".to_string()),
available: uv,
fallback_command: Some("ruff check .".to_string()),
fallback_available: host_binary_available("ruff"),
},
];
let primary = self.get_lsp_config();
let primary_available = host_binary_available("uvx");
let fallback = self.lsp_fallback();
let fallback_available = fallback
.as_ref()
.map(|f| host_binary_available(&f.server_binary))
.unwrap_or(false);
VerifierProfile {
plugin_name: self.name().to_string(),
capabilities,
lsp: LspCapability {
primary,
primary_available,
fallback,
fallback_available,
},
}
}
}
pub struct JsPlugin;
impl LanguagePlugin for JsPlugin {
fn name(&self) -> &str {
"javascript"
}
fn extensions(&self) -> &[&str] {
&["js", "ts", "jsx", "tsx"]
}
fn key_files(&self) -> &[&str] {
&["package.json", "tsconfig.json"]
}
fn required_binaries(&self) -> Vec<(&str, &str, &str)> {
vec![
(
"node",
"runtime",
"Install Node.js from https://nodejs.org or via nvm",
),
(
"npm",
"package manager",
"Included with Node.js — install from https://nodejs.org",
),
(
"typescript-language-server",
"language server",
"npm install -g typescript-language-server typescript",
),
]
}
fn get_lsp_config(&self) -> LspConfig {
LspConfig {
server_binary: "typescript-language-server".to_string(),
args: vec!["--stdio".to_string()],
language_id: "typescript".to_string(),
}
}
fn get_init_action(&self, opts: &InitOptions) -> ProjectAction {
let command = match opts.package_manager.as_deref() {
Some("pnpm") => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"pnpm init".to_string()
} else {
format!("mkdir -p {} && cd {} && pnpm init", opts.name, opts.name)
}
}
Some("yarn") => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"yarn init -y".to_string()
} else {
format!("mkdir -p {} && cd {} && yarn init -y", opts.name, opts.name)
}
}
_ => {
if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
"npm init -y".to_string()
} else {
format!("mkdir -p {} && cd {} && npm init -y", opts.name, opts.name)
}
}
};
let description = match opts.package_manager.as_deref() {
Some("pnpm") => "Initialize JavaScript project with pnpm",
Some("yarn") => "Initialize JavaScript project with Yarn",
_ => "Initialize JavaScript project with npm",
};
ProjectAction::ExecCommand {
command,
description: description.to_string(),
}
}
fn check_tooling_action(&self, path: &Path) -> ProjectAction {
let has_package_json = path.join("package.json").exists();
let has_node_modules = path.join("node_modules").exists();
if has_package_json && !has_node_modules {
ProjectAction::ExecCommand {
command: "npm install".to_string(),
description: "Install Node.js dependencies".to_string(),
}
} else {
ProjectAction::NoAction
}
}
fn init_command(&self, opts: &InitOptions) -> String {
format!("npm init -y && mv package.json {}/", opts.name)
}
fn test_command(&self) -> String {
"npm test".to_string()
}
fn run_command(&self) -> String {
"npm start".to_string()
}
fn syntax_check_command(&self) -> Option<String> {
Some("npx tsc --noEmit".to_string())
}
fn build_command(&self) -> Option<String> {
Some("npm run build".to_string())
}
fn lint_command(&self) -> Option<String> {
Some("npx eslint .".to_string())
}
fn file_ownership_patterns(&self) -> &[&str] {
&["js", "ts", "jsx", "tsx", "package.json", "tsconfig.json"]
}
fn host_tool_available(&self) -> bool {
host_binary_available("node")
}
fn verifier_profile(&self) -> VerifierProfile {
let node = host_binary_available("node");
let npx = host_binary_available("npx");
let capabilities = vec![
VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some("npx tsc --noEmit".to_string()),
available: npx,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Build,
command: Some("npm run build".to_string()),
available: node,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Test,
command: Some("npm test".to_string()),
available: node,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Lint,
command: Some("npx eslint .".to_string()),
available: npx,
fallback_command: None,
fallback_available: false,
},
];
let primary = self.get_lsp_config();
let primary_available = host_binary_available(&primary.server_binary);
VerifierProfile {
plugin_name: self.name().to_string(),
capabilities,
lsp: LspCapability {
primary,
primary_available,
fallback: None,
fallback_available: false,
},
}
}
}
pub struct PluginRegistry {
plugins: Vec<Box<dyn LanguagePlugin>>,
}
impl PluginRegistry {
pub fn new() -> Self {
Self {
plugins: vec![
Box::new(RustPlugin),
Box::new(PythonPlugin),
Box::new(JsPlugin),
],
}
}
pub fn detect(&self, path: &Path) -> Option<&dyn LanguagePlugin> {
self.plugins
.iter()
.find(|p| p.detect(path))
.map(|p| p.as_ref())
}
pub fn detect_all(&self, path: &Path) -> Vec<&dyn LanguagePlugin> {
self.plugins
.iter()
.filter(|p| p.detect(path))
.map(|p| p.as_ref())
.collect()
}
pub fn get(&self, name: &str) -> Option<&dyn LanguagePlugin> {
self.plugins
.iter()
.find(|p| p.name() == name)
.map(|p| p.as_ref())
}
pub fn all(&self) -> &[Box<dyn LanguagePlugin>] {
&self.plugins
}
}
impl Default for PluginRegistry {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_plugin_owns_file() {
let rust = RustPlugin;
assert!(rust.owns_file("src/main.rs"));
assert!(rust.owns_file("crates/core/src/lib.rs"));
assert!(!rust.owns_file("main.py"));
assert!(!rust.owns_file("index.js"));
let python = PythonPlugin;
assert!(python.owns_file("main.py"));
assert!(python.owns_file("tests/test_main.py"));
assert!(!python.owns_file("src/main.rs"));
let js = JsPlugin;
assert!(js.owns_file("index.js"));
assert!(js.owns_file("src/app.ts"));
assert!(!js.owns_file("main.py"));
assert!(!js.owns_file("src/main.rs"));
}
#[test]
fn test_verifier_capability_effective_command() {
let cap = VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some("cargo check".to_string()),
available: true,
fallback_command: Some("rustc --edition 2021".to_string()),
fallback_available: true,
};
assert_eq!(cap.effective_command(), Some("cargo check"));
assert!(cap.any_available());
let cap2 = VerifierCapability {
stage: VerifierStage::Lint,
command: Some("uv run ruff check .".to_string()),
available: false,
fallback_command: Some("ruff check .".to_string()),
fallback_available: true,
};
assert_eq!(cap2.effective_command(), Some("ruff check ."));
assert!(cap2.any_available());
let cap3 = VerifierCapability {
stage: VerifierStage::Build,
command: Some("cargo build".to_string()),
available: false,
fallback_command: None,
fallback_available: false,
};
assert_eq!(cap3.effective_command(), None);
assert!(!cap3.any_available());
}
#[test]
fn test_verifier_profile_get_and_available_stages() {
let profile = VerifierProfile {
plugin_name: "test".to_string(),
capabilities: vec![
VerifierCapability {
stage: VerifierStage::SyntaxCheck,
command: Some("check".to_string()),
available: true,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Build,
command: Some("build".to_string()),
available: false,
fallback_command: None,
fallback_available: false,
},
VerifierCapability {
stage: VerifierStage::Test,
command: Some("test".to_string()),
available: true,
fallback_command: None,
fallback_available: false,
},
],
lsp: LspCapability {
primary: LspConfig {
server_binary: "test-ls".to_string(),
args: vec![],
language_id: "test".to_string(),
},
primary_available: false,
fallback: None,
fallback_available: false,
},
};
assert!(profile.get(VerifierStage::SyntaxCheck).is_some());
assert!(profile.get(VerifierStage::Lint).is_none());
let available = profile.available_stages();
assert_eq!(available.len(), 2);
assert!(available.contains(&VerifierStage::SyntaxCheck));
assert!(available.contains(&VerifierStage::Test));
assert!(!available.contains(&VerifierStage::Build));
assert!(!profile.fully_degraded());
}
#[test]
fn test_verifier_profile_fully_degraded() {
let profile = VerifierProfile {
plugin_name: "empty".to_string(),
capabilities: vec![VerifierCapability {
stage: VerifierStage::Build,
command: Some("build".to_string()),
available: false,
fallback_command: None,
fallback_available: false,
}],
lsp: LspCapability {
primary: LspConfig {
server_binary: "none".to_string(),
args: vec![],
language_id: "none".to_string(),
},
primary_available: false,
fallback: None,
fallback_available: false,
},
};
assert!(profile.fully_degraded());
assert!(profile.available_stages().is_empty());
}
#[test]
fn test_lsp_capability_effective_config() {
let lsp = LspCapability {
primary: LspConfig {
server_binary: "rust-analyzer".to_string(),
args: vec![],
language_id: "rust".to_string(),
},
primary_available: true,
fallback: None,
fallback_available: false,
};
assert_eq!(
lsp.effective_config().unwrap().server_binary,
"rust-analyzer"
);
let lsp2 = LspCapability {
primary: LspConfig {
server_binary: "uvx".to_string(),
args: vec![],
language_id: "python".to_string(),
},
primary_available: false,
fallback: Some(LspConfig {
server_binary: "pyright-langserver".to_string(),
args: vec!["--stdio".to_string()],
language_id: "python".to_string(),
}),
fallback_available: true,
};
assert_eq!(
lsp2.effective_config().unwrap().server_binary,
"pyright-langserver"
);
let lsp3 = LspCapability {
primary: LspConfig {
server_binary: "nope".to_string(),
args: vec![],
language_id: "none".to_string(),
},
primary_available: false,
fallback: None,
fallback_available: false,
};
assert!(lsp3.effective_config().is_none());
}
#[test]
fn test_rust_plugin_verifier_profile_shape() {
let rust = RustPlugin;
let profile = rust.verifier_profile();
assert_eq!(profile.plugin_name, "rust");
assert_eq!(profile.capabilities.len(), 4);
let stages: Vec<_> = profile.capabilities.iter().map(|c| c.stage).collect();
assert!(stages.contains(&VerifierStage::SyntaxCheck));
assert!(stages.contains(&VerifierStage::Build));
assert!(stages.contains(&VerifierStage::Test));
assert!(stages.contains(&VerifierStage::Lint));
}
#[test]
fn test_python_plugin_verifier_profile_shape() {
let py = PythonPlugin;
let profile = py.verifier_profile();
assert_eq!(profile.plugin_name, "python");
assert_eq!(profile.capabilities.len(), 4);
let stages: Vec<_> = profile.capabilities.iter().map(|c| c.stage).collect();
assert!(stages.contains(&VerifierStage::SyntaxCheck));
assert!(stages.contains(&VerifierStage::Build));
assert!(stages.contains(&VerifierStage::Test));
assert!(stages.contains(&VerifierStage::Lint));
assert!(profile.lsp.fallback.is_some());
}
#[test]
fn test_js_plugin_verifier_profile_shape() {
let js = JsPlugin;
let profile = js.verifier_profile();
assert_eq!(profile.plugin_name, "javascript");
assert_eq!(profile.capabilities.len(), 4);
}
#[test]
fn test_verifier_stage_display() {
assert_eq!(format!("{}", VerifierStage::SyntaxCheck), "syntax_check");
assert_eq!(format!("{}", VerifierStage::Build), "build");
assert_eq!(format!("{}", VerifierStage::Test), "test");
assert_eq!(format!("{}", VerifierStage::Lint), "lint");
}
#[test]
fn test_python_run_command_for_dir_src_layout() {
let dir =
std::env::temp_dir().join(format!("perspt_test_pyrun_src_{}", uuid::Uuid::new_v4()));
std::fs::create_dir_all(dir.join("src/myapp")).unwrap();
std::fs::write(dir.join("src/myapp/__init__.py"), "").unwrap();
let plugin = PythonPlugin;
let cmd = plugin.run_command_for_dir(&dir);
assert_eq!(cmd, "uv run python -m myapp");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_python_run_command_for_dir_scripts() {
let dir = std::env::temp_dir().join(format!(
"perspt_test_pyrun_scripts_{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(
dir.join("pyproject.toml"),
"[project]\nname = \"myapp\"\n\n[project.scripts]\nmyapp = \"myapp:main\"\n",
)
.unwrap();
let plugin = PythonPlugin;
let cmd = plugin.run_command_for_dir(&dir);
assert_eq!(cmd, "uv run myapp");
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn test_python_run_command_for_dir_default() {
let dir = std::env::temp_dir().join(format!(
"perspt_test_pyrun_default_{}",
uuid::Uuid::new_v4()
));
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("pyproject.toml"), "[project]\nname = \"myapp\"\n").unwrap();
let plugin = PythonPlugin;
let cmd = plugin.run_command_for_dir(&dir);
assert_eq!(cmd, "uv run python -m main");
let _ = std::fs::remove_dir_all(&dir);
}
}