magi-code 0.96.2

Repository-aware CLI coding agent for terminal work
Documentation
use crate::{
    lsp::{
        DiagnosticSeverity, DocumentUri, DocumentVersion, LspRange, MAX_INJECTED_DIAGNOSTICS,
        MAX_INJECTED_DIAGNOSTICS_BYTES,
    },
    output::redact_sensitive_text,
};
use serde::Deserialize;
use std::{collections::BTreeMap, path::Path, time::Instant};

#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct LspDiagnostic {
    pub(crate) range: LspRange,
    pub(crate) severity: Option<DiagnosticSeverity>,
    #[serde(default, deserialize_with = "deserialize_diagnostic_code")]
    pub(crate) code: Option<String>,
    pub(crate) source: Option<String>,
    pub(crate) message: String,
}

#[derive(Debug, Clone)]
pub(crate) struct DiagnosticSnapshot {
    pub(crate) version: Option<DocumentVersion>,
    pub(crate) received_at: Instant,
    pub(crate) diagnostics: Vec<LspDiagnostic>,
}

#[derive(Debug, Default, Clone)]
pub(crate) struct DiagnosticsStore {
    by_uri: BTreeMap<DocumentUri, DiagnosticSnapshot>,
}

impl DiagnosticsStore {
    pub(crate) fn update(
        &mut self,
        uri: DocumentUri,
        version: Option<DocumentVersion>,
        diagnostics: Vec<LspDiagnostic>,
        received_at: Instant,
    ) {
        self.by_uri.insert(
            uri,
            DiagnosticSnapshot {
                version,
                received_at,
                diagnostics,
            },
        );
    }

    pub(crate) fn fresh_for(
        &self,
        uri: &DocumentUri,
        min_version: DocumentVersion,
        sync_started_at: Instant,
    ) -> Option<&DiagnosticSnapshot> {
        let snapshot = self.by_uri.get(uri)?;
        match snapshot.version {
            Some(version) if version >= min_version => Some(snapshot),
            Some(_) => None,
            None if snapshot.received_at >= sync_started_at => Some(snapshot),
            None => None,
        }
    }
}

pub(crate) fn format_injected_diagnostics(
    server: &str,
    path: &Path,
    diagnostics: &[LspDiagnostic],
) -> String {
    let mut selected = diagnostics
        .iter()
        .filter(|diagnostic| {
            matches!(
                diagnostic.severity,
                Some(DiagnosticSeverity::Error | DiagnosticSeverity::Warning)
            )
        })
        .collect::<Vec<_>>();
    if selected.is_empty() {
        return String::new();
    }
    selected.sort_by_key(|diagnostic| diagnostic.severity.unwrap_or(DiagnosticSeverity::Hint));
    let shown = selected.len().min(MAX_INJECTED_DIAGNOSTICS);
    let mut output = format!("DIAGNOSTICS ({}, {}):", server, path.display());
    let mut omitted = selected.len() - shown;
    for diagnostic in selected.iter().take(shown) {
        let severity = match diagnostic.severity {
            Some(DiagnosticSeverity::Error) => "error",
            Some(DiagnosticSeverity::Warning) => "warning",
            _ => continue,
        };
        let source = diagnostic
            .source
            .as_deref()
            .map(|source| format!(" [{source}]"))
            .unwrap_or_default();
        let code = diagnostic
            .code
            .as_deref()
            .map(|code| format!(" {code}"))
            .unwrap_or_default();
        let line = format!(
            "\n- {severity}{}{} at {}:{}: {}",
            source,
            code,
            diagnostic.range.start.line + 1,
            diagnostic.range.start.character + 1,
            diagnostic.message.replace(['\r', '\n'], " ")
        );
        if output.len() + line.len() > MAX_INJECTED_DIAGNOSTICS_BYTES {
            omitted += 1;
            break;
        }
        output.push_str(&line);
    }
    if omitted > 0 {
        let line = format!("\n... truncated {omitted} diagnostics");
        push_utf8_capped(&mut output, &line, MAX_INJECTED_DIAGNOSTICS_BYTES);
    }
    redact_sensitive_text(&output)
}

fn push_utf8_capped(output: &mut String, line: &str, max_bytes: usize) {
    let remaining = max_bytes.saturating_sub(output.len());
    if remaining == 0 {
        return;
    }
    if line.len() <= remaining {
        output.push_str(line);
        return;
    }
    let end = line
        .char_indices()
        .map(|(index, _)| index)
        .take_while(|index| *index <= remaining)
        .last()
        .unwrap_or(0);
    output.push_str(&line[..end]);
}

fn deserialize_diagnostic_code<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
    D: serde::Deserializer<'de>,
{
    let Some(value) = Option::<serde_json::Value>::deserialize(deserializer)? else {
        return Ok(None);
    };
    Ok(match value {
        serde_json::Value::String(value) => Some(value),
        serde_json::Value::Number(value) => Some(value.to_string()),
        other => Some(other.to_string()),
    })
}