magi-code 0.62.0

Repository-aware CLI coding agent for terminal work
Documentation
mod client;
mod diagnostics;
mod manager;
mod queries;
mod stdio;
mod sync;
pub(crate) use manager::{EditDiagnosticsRequest, LspManager};

pub(crate) use diagnostics::{DiagnosticsStore, LspDiagnostic};
pub(crate) use queries::ReferenceLocation;
pub(crate) use sync::DocumentVersions;

use anyhow::{Context, bail};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};

pub(crate) const MAX_INJECTED_DIAGNOSTICS: usize = 20;
pub(crate) const MAX_INJECTED_DIAGNOSTICS_BYTES: usize = 2 * 1024;
pub(crate) const MAX_TOOL_DIAGNOSTICS: usize = 200;
pub(crate) const MAX_REFERENCES: usize = 100;

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub(crate) struct DocumentUri(String);

impl DocumentUri {
    pub(crate) fn from_path(path: &Path) -> anyhow::Result<Self> {
        let absolute = if path.is_absolute() {
            path.to_path_buf()
        } else {
            std::env::current_dir()?.join(path)
        };
        let path = absolute
            .canonicalize()
            .with_context(|| format!("canonicalizing {}", absolute.display()))?;
        Ok(Self(format!("file://{}", percent_encode_path(&path))))
    }

    pub(crate) fn to_file_path(&self) -> anyhow::Result<PathBuf> {
        let Some(rest) = self.0.strip_prefix("file://") else {
            bail!("unsupported LSP URI scheme");
        };
        Ok(PathBuf::from(percent_decode(rest)?))
    }

    pub(crate) fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[repr(u8)]
pub(crate) enum DiagnosticSeverity {
    Error = 1,
    Warning = 2,
    Information = 3,
    Hint = 4,
}

impl<'de> Deserialize<'de> for DiagnosticSeverity {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        match u8::deserialize(deserializer)? {
            1 => Ok(Self::Error),
            2 => Ok(Self::Warning),
            3 => Ok(Self::Information),
            4 => Ok(Self::Hint),
            other => Err(serde::de::Error::custom(format!(
                "unknown diagnostic severity {other}"
            ))),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LspRange {
    pub(crate) start: LspPosition,
    pub(crate) end: LspPosition,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct LspPosition {
    pub(crate) line: u32,
    pub(crate) character: u32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub(crate) struct DocumentVersion(pub(crate) i32);

fn percent_encode_path(path: &Path) -> String {
    path.to_string_lossy()
        .bytes()
        .flat_map(|byte| match byte {
            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'-' | b'_' | b'.' | b'~' => {
                vec![byte as char]
            }
            _ => format!("%{byte:02X}").chars().collect(),
        })
        .collect()
}

fn percent_decode(value: &str) -> anyhow::Result<String> {
    let bytes = value.as_bytes();
    let mut decoded = Vec::with_capacity(bytes.len());
    let mut index = 0;
    while index < bytes.len() {
        if bytes[index] == b'%' {
            if index + 2 >= bytes.len() {
                bail!("invalid percent-encoded URI");
            }
            let hex = std::str::from_utf8(&bytes[index + 1..index + 3])?;
            decoded.push(u8::from_str_radix(hex, 16).context("invalid percent-encoded URI")?);
            index += 3;
        } else {
            decoded.push(bytes[index]);
            index += 1;
        }
    }
    Ok(String::from_utf8(decoded)?)
}

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

    #[test]
    fn document_uri_round_trips_path_with_spaces() {
        let temp = tempfile::TempDir::new().unwrap();
        let path = temp.path().join("file with spaces.rs");
        std::fs::write(&path, "fn main() {}\n").unwrap();

        let uri = DocumentUri::from_path(&path).unwrap();

        assert!(uri.as_str().starts_with("file://"));
        assert!(uri.as_str().contains("%20"));
        assert_eq!(uri.to_file_path().unwrap(), path.canonicalize().unwrap());
    }
}