magi-code 0.62.0

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    config::{LspServerConfig, LspSettings},
    lsp::DocumentVersion,
};
use std::{collections::BTreeMap, path::Path};

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct LanguageRoute {
    pub(crate) server_id: String,
    pub(crate) language_id: String,
    pub(crate) config: LspServerConfig,
}

#[derive(Debug, Default, Clone)]
pub(crate) struct DocumentVersions {
    by_path: BTreeMap<std::path::PathBuf, DocumentVersion>,
}

impl DocumentVersions {
    pub(crate) fn next_for(&mut self, path: &Path) -> DocumentVersion {
        let entry = self
            .by_path
            .entry(path.to_path_buf())
            .or_insert(DocumentVersion(0));
        entry.0 += 1;
        *entry
    }
}

pub(crate) fn default_servers() -> BTreeMap<String, LspServerConfig> {
    BTreeMap::from([
        (
            "rust-analyzer".to_string(),
            LspServerConfig {
                command: "rust-analyzer".to_string(),
                args: Vec::new(),
                enabled: true,
            },
        ),
        (
            "typescript-language-server".to_string(),
            LspServerConfig {
                command: "typescript-language-server".to_string(),
                args: vec!["--stdio".to_string()],
                enabled: true,
            },
        ),
        (
            "pyright-langserver".to_string(),
            LspServerConfig {
                command: "pyright-langserver".to_string(),
                args: vec!["--stdio".to_string()],
                enabled: true,
            },
        ),
        (
            "gopls".to_string(),
            LspServerConfig {
                command: "gopls".to_string(),
                args: Vec::new(),
                enabled: true,
            },
        ),
    ])
}

pub(crate) fn route_for_path(path: &Path, settings: &LspSettings) -> Option<LanguageRoute> {
    let language_id = language_id_for_path(path)?.to_string();
    let server_id = server_id_for_language(&language_id)?.to_string();
    let mut servers = default_servers();
    for (key, config) in &settings.servers {
        if servers.contains_key(key) {
            servers.insert(key.clone(), config.clone());
        }
    }
    let config = servers.remove(&server_id)?;
    config.enabled.then_some(LanguageRoute {
        server_id,
        language_id,
        config,
    })
}

pub(crate) fn language_id_for_path(path: &Path) -> Option<&'static str> {
    match path.extension().and_then(|extension| extension.to_str())? {
        "rs" => Some("rust"),
        "ts" | "tsx" => Some("typescript"),
        "js" | "jsx" => Some("javascript"),
        "py" => Some("python"),
        "go" => Some("go"),
        _ => None,
    }
}

fn server_id_for_language(language_id: &str) -> Option<&'static str> {
    match language_id {
        "rust" => Some("rust-analyzer"),
        "typescript" | "javascript" => Some("typescript-language-server"),
        "python" => Some("pyright-langserver"),
        "go" => Some("gopls"),
        _ => None,
    }
}

pub(crate) fn read_text_for_sync(path: &Path) -> anyhow::Result<String> {
    Ok(std::fs::read_to_string(path)?)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn routes_supported_extensions() {
        let settings = LspSettings::default();

        assert_eq!(
            route_for_path(Path::new("a.rs"), &settings)
                .unwrap()
                .server_id,
            "rust-analyzer"
        );
        assert_eq!(
            route_for_path(Path::new("a.ts"), &settings)
                .unwrap()
                .language_id,
            "typescript"
        );
        assert!(route_for_path(Path::new("a.txt"), &settings).is_none());
    }

    #[test]
    fn route_uses_configured_default_server_override() {
        let mut settings = LspSettings::default();
        settings.servers.insert(
            "rust-analyzer".to_string(),
            LspServerConfig {
                command: "custom-ra".to_string(),
                args: vec!["--x".to_string()],
                enabled: true,
            },
        );

        let route = route_for_path(Path::new("a.rs"), &settings).unwrap();

        assert_eq!(route.config.command, "custom-ra");
        assert_eq!(route.config.args, ["--x"]);
    }

    #[test]
    fn route_ignores_non_default_server_override_and_disabled_default() {
        let mut settings = LspSettings::default();
        settings.servers.insert(
            "custom-server".to_string(),
            LspServerConfig {
                command: "custom".to_string(),
                args: Vec::new(),
                enabled: true,
            },
        );
        assert_eq!(
            route_for_path(Path::new("a.rs"), &settings)
                .unwrap()
                .config
                .command,
            "rust-analyzer"
        );

        settings.servers.insert(
            "rust-analyzer".to_string(),
            LspServerConfig {
                command: "rust-analyzer".to_string(),
                args: Vec::new(),
                enabled: false,
            },
        );
        assert!(route_for_path(Path::new("a.rs"), &settings).is_none());
    }

    #[test]
    fn language_ids_cover_defaults() {
        assert_eq!(language_id_for_path(Path::new("x.tsx")), Some("typescript"));
        assert_eq!(language_id_for_path(Path::new("x.jsx")), Some("javascript"));
        assert_eq!(language_id_for_path(Path::new("x.py")), Some("python"));
        assert_eq!(language_id_for_path(Path::new("x.go")), Some("go"));
        assert_eq!(language_id_for_path(Path::new("x.md")), None);
    }
}