lsp-cli 0.1.6

Command-line tool for talking to Language Server Protocol (LSP) servers from the terminal.
use super::{SymbolMatch, WorkspaceSymbolQueryResult};
use crate::cli::WorkspaceQueryArgs;
use crate::suggest::SuggestedLanguage;
use serde_json::{Value, json};
use std::collections::{BTreeSet, HashSet};
use std::path::{Path, PathBuf};

pub(crate) fn render_workspace_symbol_result(
    name: &str,
    query: &WorkspaceQueryArgs,
    files_with_matches: bool,
    full: bool,
    result: WorkspaceSymbolQueryResult,
) -> String {
    let matches = truncate_items(
        result.matches,
        query.limit,
        if query.json { "items" } else { "lines" },
    );

    if query.json {
        if full {
            render_workspace_symbol_json_full(
                name,
                &query.directory,
                &result.detected_filetypes,
                &result.server,
                &matches,
            )
        } else {
            render_workspace_symbol_json(
                name,
                &query.directory,
                &result.detected_filetypes,
                &result.server,
                &matches,
            )
        }
    } else if files_with_matches {
        render_symbol_match_paths_text(&matches)
    } else if full {
        render_symbol_matches_text_full(&matches)
    } else {
        render_symbol_matches_text(&matches)
    }
}

pub(crate) fn truncate_items<T>(mut items: Vec<T>, limit: usize, unit: &str) -> Vec<T> {
    if items.len() > limit {
        items.truncate(limit);
        eprintln!("output limit reached ({limit} {unit}); increase it with --limit");
    }

    items
}

pub(crate) fn render_symbol_matches_text(matches: &[SymbolMatch]) -> String {
    matches
        .iter()
        .map(render_symbol_match_text)
        .collect::<Vec<_>>()
        .join("\n")
}

fn render_symbol_match_text(matched: &SymbolMatch) -> String {
    format!(
        "{}:{}:{}:{}",
        matched.path.display(),
        matched.line,
        matched.col,
        matched.line_content
    )
}

pub(crate) fn render_symbol_match_paths_text(matches: &[SymbolMatch]) -> String {
    let mut seen = HashSet::new();
    let paths = matches
        .iter()
        .filter(|matched| seen.insert(matched.path.clone()))
        .map(|matched| matched.path.clone())
        .collect::<Vec<_>>();

    render_paths_text(&paths)
}

pub(crate) fn render_symbol_matches_text_full(matches: &[SymbolMatch]) -> String {
    matches
        .iter()
        .map(render_symbol_match_text_full)
        .collect::<Vec<_>>()
        .join("\n\n")
}

fn render_symbol_match_text_full(matched: &SymbolMatch) -> String {
    format!(
        "{}:{}:{}:\n{}",
        matched.path.display(),
        matched.line,
        matched.col,
        matched
            .full_content
            .clone()
            .unwrap_or_else(|| matched.line_content.clone())
    )
}

pub(crate) fn render_symbol_names_text(matches: &[SymbolMatch]) -> String {
    matches
        .iter()
        .map(|matched| matched.name.clone())
        .collect::<Vec<_>>()
        .join("\n")
}

pub(crate) fn render_paths_text(paths: &[PathBuf]) -> String {
    paths
        .iter()
        .map(|path| path.display().to_string())
        .collect::<Vec<_>>()
        .join("\n")
}

pub(crate) fn render_workspace_symbol_json(
    query: &str,
    directory: &Path,
    detected_filetypes: &BTreeSet<String>,
    server: &SuggestedLanguage,
    matches: &[SymbolMatch],
) -> String {
    json!({
        "query": query,
        "directory": directory,
        "detected": detected_filetypes,
        "server": render_server_json(server),
        "matches": render_symbol_matches_json(matches, false),
    })
    .to_string()
}

pub(crate) fn render_workspace_symbol_json_full(
    query: &str,
    directory: &Path,
    detected_filetypes: &BTreeSet<String>,
    server: &SuggestedLanguage,
    matches: &[SymbolMatch],
) -> String {
    json!({
        "query": query,
        "directory": directory,
        "detected": detected_filetypes,
        "server": render_server_json(server),
        "matches": render_symbol_matches_json(matches, true),
    })
    .to_string()
}

pub(crate) fn render_document_symbol_json(
    file: &Path,
    detected_filetypes: &BTreeSet<String>,
    server: &SuggestedLanguage,
    matches: &[SymbolMatch],
) -> String {
    json!({
        "file": file,
        "detected": detected_filetypes,
        "server": render_server_json(server),
        "matches": render_symbol_matches_json(matches, false),
    })
    .to_string()
}

pub(crate) fn render_list_symbols_json(
    path: &Path,
    is_file: bool,
    detected_filetypes: &BTreeSet<String>,
    server: &SuggestedLanguage,
    matches: &[SymbolMatch],
) -> String {
    if is_file {
        return render_document_symbol_json(path, detected_filetypes, server, matches);
    }

    json!({
        "directory": path,
        "detected": detected_filetypes,
        "server": render_server_json(server),
        "matches": render_symbol_matches_json(matches, false),
    })
    .to_string()
}

pub(crate) fn render_file_list_json(
    directory: &Path,
    detected_filetypes: &BTreeSet<String>,
    server: &SuggestedLanguage,
    files: &[PathBuf],
) -> String {
    json!({
        "directory": directory,
        "detected": detected_filetypes,
        "server": render_server_json(server),
        "files": files,
    })
    .to_string()
}

fn render_server_json(server: &SuggestedLanguage) -> Value {
    json!({
        "name": server.server,
        "languages": server.languages,
        "command": server.command,
        "workspace_root": server.workspace_root,
    })
}

fn render_symbol_matches_json(matches: &[SymbolMatch], include_full_content: bool) -> Vec<Value> {
    matches
        .iter()
        .map(|matched| {
            let mut value = json!({
                "name": matched.name,
                "kind": matched.kind,
                "path": matched.path,
                "line": matched.line,
                "col": matched.col,
                "line_content": matched.line_content,
            });
            if include_full_content {
                value["full_content"] = json!(
                    matched
                        .full_content
                        .clone()
                        .unwrap_or_else(|| matched.line_content.clone())
                );
            }
            value
        })
        .collect()
}