magi-code 0.64.0

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 latest_for(&self, uri: &DocumentUri) -> Option<&DiagnosticSnapshot> {
        self.by_uri.get(uri)
    }

    pub(crate) fn summary(&self, limit: usize) -> DiagnosticsSummary {
        let total_files = self.by_uri.len();
        let files = self
            .by_uri
            .iter()
            .take(limit)
            .map(|(uri, snapshot)| FileDiagnosticsSummary {
                uri: uri.clone(),
                error_count: snapshot
                    .diagnostics
                    .iter()
                    .filter(|diagnostic| diagnostic.severity == Some(DiagnosticSeverity::Error))
                    .count(),
                warning_count: snapshot
                    .diagnostics
                    .iter()
                    .filter(|diagnostic| diagnostic.severity == Some(DiagnosticSeverity::Warning))
                    .count(),
                total_count: snapshot.diagnostics.len(),
            })
            .collect();
        DiagnosticsSummary { files, total_files }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DiagnosticsSummary {
    pub(crate) files: Vec<FileDiagnosticsSummary>,
    pub(crate) total_files: usize,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct FileDiagnosticsSummary {
    pub(crate) uri: DocumentUri,
    pub(crate) error_count: usize,
    pub(crate) warning_count: usize,
    pub(crate) total_count: usize,
}

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

pub(crate) fn format_diagnostics_tool(path: Option<&Path>, summary: DiagnosticsSummary) -> String {
    if summary.files.is_empty() {
        return redact_sensitive_text(&match path {
            Some(path) => format!(
                "no cached diagnostics for {}; run after edit or use project checks",
                path.display()
            ),
            None => "no cached LSP diagnostics; run after edit or use project checks".to_string(),
        });
    }
    let mut lines = Vec::new();
    for file in &summary.files {
        let path = file
            .uri
            .to_file_path()
            .map(|path| path.display().to_string())
            .unwrap_or_else(|_| file.uri.as_str().to_string());
        lines.push(format!(
            "{}: {} diagnostics ({} errors, {} warnings)",
            path, file.total_count, file.error_count, file.warning_count
        ));
    }
    if summary.total_files > summary.files.len() {
        lines.push(format!(
            "... truncated {} files",
            summary.total_files - summary.files.len()
        ));
    }
    redact_sensitive_text(&lines.join("\n"))
}

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()),
    })
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::lsp::LspPosition;
    use serde_json::json;
    use std::time::Duration;

    fn diagnostic(message: &str) -> LspDiagnostic {
        LspDiagnostic {
            range: LspRange {
                start: LspPosition {
                    line: 0,
                    character: 0,
                },
                end: LspPosition {
                    line: 0,
                    character: 1,
                },
            },
            severity: Some(DiagnosticSeverity::Error),
            code: Some("E1".to_string()),
            source: Some("fake".to_string()),
            message: message.to_string(),
        }
    }

    #[test]
    fn publish_diagnostics_fixture_parses() {
        let parsed: LspDiagnostic = serde_json::from_value(json!({
            "range": {"start": {"line": 1, "character": 2}, "end": {"line": 3, "character": 4}},
            "severity": 1,
            "code": 123,
            "source": "rust-analyzer",
            "message": "expected type"
        }))
        .unwrap();

        assert_eq!(parsed.severity, Some(DiagnosticSeverity::Error));
        assert_eq!(parsed.code.as_deref(), Some("123"));
        assert_eq!(parsed.range.start.line, 1);
    }

    #[test]
    fn diagnostics_store_tracks_version_freshness() {
        let mut store = DiagnosticsStore::default();
        let uri = DocumentUri("file:///tmp/a.rs".to_string());
        let sync_started = Instant::now();
        store.update(
            uri.clone(),
            Some(DocumentVersion(1)),
            vec![diagnostic("old")],
            sync_started + Duration::from_millis(1),
        );

        assert!(
            store
                .fresh_for(&uri, DocumentVersion(2), sync_started)
                .is_none()
        );
        assert!(
            store
                .fresh_for(&uri, DocumentVersion(1), sync_started)
                .is_some()
        );
    }

    #[test]
    fn diagnostics_store_accepts_unversioned_after_sync() {
        let mut store = DiagnosticsStore::default();
        let uri = DocumentUri("file:///tmp/a.rs".to_string());
        let sync_started = Instant::now();
        store.update(
            uri.clone(),
            None,
            vec![diagnostic("fresh")],
            sync_started + Duration::from_millis(1),
        );

        assert!(
            store
                .fresh_for(&uri, DocumentVersion(9), sync_started)
                .is_some()
        );
    }

    #[test]
    fn diagnostics_store_rejects_unversioned_before_sync() {
        let mut store = DiagnosticsStore::default();
        let uri = DocumentUri("file:///tmp/a.rs".to_string());
        let sync_started = Instant::now();
        store.update(
            uri.clone(),
            None,
            vec![diagnostic("stale")],
            sync_started - Duration::from_millis(1),
        );

        assert!(
            store
                .fresh_for(&uri, DocumentVersion(9), sync_started)
                .is_none()
        );
    }

    #[test]
    fn injected_diagnostics_orders_errors_warnings_caps_and_redacts() {
        let mut warning = diagnostic("warning token sk-testsecret000000000000000000000000");
        warning.severity = Some(DiagnosticSeverity::Warning);
        let error = diagnostic("error before warning");

        let output = format_injected_diagnostics(
            "rust-analyzer",
            Path::new("src/lib.rs"),
            &[warning, error],
        );

        assert!(output.starts_with("DIAGNOSTICS (rust-analyzer, src/lib.rs):"));
        assert!(
            output.find("error before warning") < output.find("warning token").or(Some(usize::MAX))
        );
        assert!(output.contains("<redacted>"));
        assert!(!output.contains("sk-testsecret"));
    }

    #[test]
    fn injected_diagnostics_caps_at_utf8_boundary() {
        let long = diagnostic(&"é".repeat(3000));

        let output = format_injected_diagnostics("server", Path::new("a.rs"), &[long]);

        assert!(output.len() <= crate::lsp::MAX_INJECTED_DIAGNOSTICS_BYTES);
        assert!(output.is_char_boundary(output.len()));
    }
}