a3s 0.10.8

a3s — A3S coding agent CLI; `a3s code` launches the interactive TUI
Documentation
use super::*;
use a3s_code_core::workspace::WorkspaceFileSystem;
use a3s_code_core::{CodeDiagnostic, CodeIntelligenceStatus};

pub(super) async fn execute_ide_intelligence_query(
    provider: Arc<dyn WorkspaceCodeIntelligence>,
    file_system: Arc<dyn WorkspaceFileSystem>,
    prepared: PreparedIdeIntelligenceQuery,
    cancellation: CancellationToken,
) -> Result<IdeIntelligenceResult, String> {
    if cancellation.is_cancelled() {
        return Err("Code Intelligence query cancelled".to_owned());
    }
    let title = prepared.title;
    let saved_version = prepared.saved_version;
    let dirty_buffer = prepared.dirty_buffer;
    match prepared.task {
        IdeIntelligenceTask::Status => {
            let status = provider.status();
            Ok(status_result(title, status, dirty_buffer))
        }
        IdeIntelligenceTask::DocumentSymbols { path } => {
            let result = provider
                .document_symbols(&path, cancellation)
                .await
                .map_err(|error| error.to_string())?;
            let rows = document_symbol_rows(result.items, path.as_str());
            Ok(query_result(
                title,
                rows,
                result.truncated,
                saved_version,
                dirty_buffer,
                result.workspace_revision,
                result
                    .document
                    .as_ref()
                    .is_some_and(|document| document.stale),
            ))
        }
        IdeIntelligenceTask::WorkspaceSymbols { query } => {
            let result = provider
                .search_symbols(&query, WORKSPACE_SYMBOL_LIMIT, cancellation)
                .await
                .map_err(|error| error.to_string())?;
            let rows = result.items.into_iter().map(workspace_symbol_row).collect();
            Ok(query_result(
                title,
                rows,
                result.truncated,
                saved_version,
                dirty_buffer,
                result.workspace_revision,
                false,
            ))
        }
        IdeIntelligenceTask::Navigate {
            kind,
            path,
            row,
            expanded_col,
        } => {
            let saved = tokio::select! {
                _ = cancellation.cancelled() => {
                    return Err("Code Intelligence query cancelled".to_owned());
                }
                result = file_system.read_text(&path) => result.map_err(|error| {
                    format!("failed to read saved file {}: {error}", path.as_str())
                })?,
            };
            let position = editor_position_to_saved_utf16(&saved, row, expanded_col)?;
            let result = provider
                .navigate(kind, &path, position, cancellation)
                .await
                .map_err(|error| error.to_string())?;
            let rows = result.items.into_iter().map(navigation_row).collect();
            Ok(query_result(
                title,
                rows,
                result.truncated,
                saved_version,
                dirty_buffer,
                result.workspace_revision,
                result
                    .document
                    .as_ref()
                    .is_some_and(|document| document.stale),
            ))
        }
        IdeIntelligenceTask::Diagnostics { path } => {
            let result = provider
                .diagnostics(path.as_ref(), cancellation)
                .await
                .map_err(|error| error.to_string())?;
            let rows = result.items.into_iter().map(diagnostic_row).collect();
            Ok(query_result(
                title,
                rows,
                result.truncated,
                saved_version,
                dirty_buffer,
                result.workspace_revision,
                result
                    .document
                    .as_ref()
                    .is_some_and(|document| document.stale),
            ))
        }
    }
}

fn query_result(
    title: String,
    rows: Vec<IdeIntelligenceRow>,
    truncated: bool,
    saved_version: bool,
    dirty_buffer: bool,
    workspace_revision: u64,
    stale: bool,
) -> IdeIntelligenceResult {
    IdeIntelligenceResult {
        title,
        rows,
        truncated,
        saved_version,
        dirty_buffer,
        stale,
        workspace_revision: Some(workspace_revision),
    }
}

fn status_result(
    title: String,
    status: CodeIntelligenceStatus,
    dirty_buffer: bool,
) -> IdeIntelligenceResult {
    let mut rows = vec![IdeIntelligenceRow {
        text: format!(
            "Workspace: {} · {}",
            intelligence_state_label(status.state),
            capability_labels(status.capabilities)
        ),
        target: None,
    }];
    if let Some(message) = status.message.filter(|message| !message.trim().is_empty()) {
        rows.push(IdeIntelligenceRow {
            text: message,
            target: None,
        });
    }
    for language in status.languages {
        let mut text = format!(
            "{}: {} · {}",
            language.language,
            intelligence_state_label(language.state),
            capability_labels(language.capabilities)
        );
        if let Some(message) = language
            .message
            .filter(|message| !message.trim().is_empty())
        {
            text.push_str(" · ");
            text.push_str(&message);
        }
        rows.push(IdeIntelligenceRow { text, target: None });
    }
    IdeIntelligenceResult {
        title,
        rows,
        truncated: false,
        saved_version: false,
        dirty_buffer,
        stale: false,
        workspace_revision: None,
    }
}

fn document_symbol_rows(symbols: Vec<DocumentSymbol>, path: &str) -> Vec<IdeIntelligenceRow> {
    fn append(
        rows: &mut Vec<IdeIntelligenceRow>,
        symbols: Vec<DocumentSymbol>,
        path: &str,
        depth: usize,
    ) {
        for symbol in symbols {
            let detail = symbol
                .detail
                .as_deref()
                .filter(|detail| !detail.trim().is_empty())
                .map(|detail| format!(" · {detail}"))
                .unwrap_or_default();
            rows.push(IdeIntelligenceRow {
                text: format!(
                    "{}{} · {}{} · {}",
                    "  ".repeat(depth),
                    symbol.name,
                    symbol_kind_label(symbol.kind),
                    detail,
                    display_position(symbol.selection_range.start)
                ),
                target: Some(IdeIntelligenceTarget {
                    path: path.to_owned(),
                    position: symbol.selection_range.start,
                }),
            });
            append(rows, symbol.children, path, depth + 1);
        }
    }

    let mut rows = Vec::new();
    append(&mut rows, symbols, path, 0);
    rows
}

fn workspace_symbol_row(symbol: SymbolInformation) -> IdeIntelligenceRow {
    let container = symbol
        .container_name
        .as_deref()
        .filter(|container| !container.trim().is_empty())
        .map(|container| format!(" · {container}"))
        .unwrap_or_default();
    let target = target_from_location(&symbol.location);
    IdeIntelligenceRow {
        text: format!(
            "{} · {}{} · {}:{}",
            symbol.name,
            symbol_kind_label(symbol.kind),
            container,
            symbol.location.path.as_str(),
            display_position(symbol.location.range.start)
        ),
        target: Some(target),
    }
}

fn navigation_row(location: CodeLocation) -> IdeIntelligenceRow {
    let target = target_from_location(&location);
    IdeIntelligenceRow {
        text: format!(
            "{}:{}",
            location.path.as_str(),
            display_position(location.range.start)
        ),
        target: Some(target),
    }
}

fn diagnostic_row(diagnostic: CodeDiagnostic) -> IdeIntelligenceRow {
    let severity = diagnostic
        .severity
        .map(diagnostic_severity_label)
        .unwrap_or("diagnostic");
    let mut origin = diagnostic.source.unwrap_or_default();
    if let Some(code) = diagnostic.code {
        if !origin.is_empty() {
            origin.push('/');
        }
        origin.push_str(&code);
    }
    let origin = if origin.is_empty() {
        String::new()
    } else {
        format!(" [{origin}]")
    };
    let target = target_from_location(&diagnostic.location);
    IdeIntelligenceRow {
        text: format!(
            "{} · {}:{} · {}{}",
            severity,
            diagnostic.location.path.as_str(),
            display_position(diagnostic.location.range.start),
            diagnostic.message.replace(['\r', '\n'], " "),
            origin
        ),
        target: Some(target),
    }
}

fn target_from_location(location: &CodeLocation) -> IdeIntelligenceTarget {
    IdeIntelligenceTarget {
        path: location.path.as_str().to_owned(),
        position: location.range.start,
    }
}

fn display_position(position: CodePosition) -> String {
    format!(
        "{}:{}",
        position.line.saturating_add(1),
        position.character.saturating_add(1)
    )
}

pub(super) fn navigation_label(kind: NavigationKind) -> &'static str {
    match kind {
        NavigationKind::Definition => "Definition",
        NavigationKind::Declaration => "Declaration",
        NavigationKind::References => "References",
        NavigationKind::Implementations => "Implementations",
    }
}

fn intelligence_state_label(state: CodeIntelligenceState) -> &'static str {
    match state {
        CodeIntelligenceState::Starting => "starting",
        CodeIntelligenceState::Ready => "ready",
        CodeIntelligenceState::Degraded => "degraded",
        CodeIntelligenceState::Unavailable => "unavailable",
    }
}

fn capability_labels(capabilities: CodeIntelligenceCapabilities) -> String {
    let mut labels = Vec::new();
    if capabilities.document_symbols {
        labels.push("document symbols");
    }
    if capabilities.workspace_symbols {
        labels.push("workspace symbols");
    }
    if capabilities.definition {
        labels.push("definition");
    }
    if capabilities.declaration {
        labels.push("declaration");
    }
    if capabilities.references {
        labels.push("references");
    }
    if capabilities.implementations {
        labels.push("implementations");
    }
    if capabilities.diagnostics {
        labels.push("diagnostics");
    }
    if labels.is_empty() {
        "no capabilities".to_owned()
    } else {
        labels.join(", ")
    }
}

fn symbol_kind_label(kind: CodeSymbolKind) -> &'static str {
    match kind {
        CodeSymbolKind::File => "file",
        CodeSymbolKind::Module => "module",
        CodeSymbolKind::Namespace => "namespace",
        CodeSymbolKind::Package => "package",
        CodeSymbolKind::Class => "class",
        CodeSymbolKind::Method => "method",
        CodeSymbolKind::Property => "property",
        CodeSymbolKind::Field => "field",
        CodeSymbolKind::Constructor => "constructor",
        CodeSymbolKind::Enum => "enum",
        CodeSymbolKind::Interface => "interface",
        CodeSymbolKind::Function => "function",
        CodeSymbolKind::Variable => "variable",
        CodeSymbolKind::Constant => "constant",
        CodeSymbolKind::String => "string",
        CodeSymbolKind::Number => "number",
        CodeSymbolKind::Boolean => "boolean",
        CodeSymbolKind::Array => "array",
        CodeSymbolKind::Object => "object",
        CodeSymbolKind::Key => "key",
        CodeSymbolKind::Null => "null",
        CodeSymbolKind::EnumMember => "enum member",
        CodeSymbolKind::Struct => "struct",
        CodeSymbolKind::Event => "event",
        CodeSymbolKind::Operator => "operator",
        CodeSymbolKind::TypeParameter => "type parameter",
        CodeSymbolKind::Unknown => "symbol",
        _ => "symbol",
    }
}

fn diagnostic_severity_label(severity: CodeDiagnosticSeverity) -> &'static str {
    match severity {
        CodeDiagnosticSeverity::Error => "error",
        CodeDiagnosticSeverity::Warning => "warning",
        CodeDiagnosticSeverity::Information => "information",
        CodeDiagnosticSeverity::Hint => "hint",
    }
}