lsp-cli 0.1.0

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

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(|matched| {
            format!(
                "{}:{}:{}:{}",
                matched.path.display(),
                matched.line,
                matched.col,
                matched.line_content
            )
        })
        .collect::<Vec<_>>()
        .join("\n")
}

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(|matched| {
            format!(
                "{}:{}:{}:\n{}",
                matched.path.display(),
                matched.line,
                matched.col,
                matched
                    .full_content
                    .clone()
                    .unwrap_or_else(|| matched.line_content.clone())
            )
        })
        .collect::<Vec<_>>()
        .join("\n\n")
}

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