lsp-cli 0.1.6

Command-line tool for talking to Language Server Protocol (LSP) servers from the terminal.
use std::path::{Path, PathBuf};
use std::process::Command;
use std::sync::OnceLock;
use std::time::Duration;

use crate::fixture::E2eFixture;
use crate::harness::E2eContext;
use crate::manifest::CommandStrategy;
use crate::process;
use crate::repository_root;

const SERVER_NAME: &str = "e2e-fake-lsp";
const UNSUPPORTED_SERVER_NAME: &str = "e2e-fake-lsp-unsupported";
const BINARY_NAME: &str = "lsp-cli-e2e-fake-lsp";
const BUILD_DEADLINE: Duration = Duration::from_secs(120);

pub(crate) struct LocalFixture {
    fixture: E2eFixture,
}

impl LocalFixture {
    pub(crate) fn new() -> Result<Self, String> {
        Self::with_server(SERVER_NAME)
    }

    pub(crate) fn new_unsupported() -> Result<Self, String> {
        Self::with_server(UNSUPPORTED_SERVER_NAME)
    }

    fn with_server(server_name: &'static str) -> Result<Self, String> {
        let root = repository_root();
        let fixtures = root.join("tests/e2e/fixtures");
        let fixture = E2eFixture::new_with_data_dir(fixtures.join("data"))?;
        fixture.context().copy_project(&fixtures.join("project"))?;
        fixture
            .context()
            .stage_program(server_name, fake_server_binary()?)?;
        Ok(Self { fixture })
    }

    pub(crate) fn context(&self) -> &E2eContext {
        self.fixture.context()
    }

    pub(crate) fn commands_for(&self, strategy: CommandStrategy) -> impl Iterator<Item = &str> {
        self.fixture.commands_for(strategy)
    }

    pub(crate) fn server_name(&self) -> &'static str {
        SERVER_NAME
    }

    pub(crate) fn unsupported_server_name(&self) -> &'static str {
        UNSUPPORTED_SERVER_NAME
    }
}

fn fake_server_binary() -> Result<&'static Path, String> {
    static BINARY: OnceLock<Result<PathBuf, String>> = OnceLock::new();
    match BINARY.get_or_init(build_fake_server) {
        Ok(path) => Ok(path),
        Err(error) => Err(error.clone()),
    }
}

fn build_fake_server() -> Result<PathBuf, String> {
    let root = repository_root();
    let manifest = root.join("tests/e2e/fixtures/fake-lsp/Cargo.toml");
    let target = root.join("target/e2e-fixtures");
    let cargo = option_env!("CARGO").unwrap_or("cargo");
    let mut command = Command::new(cargo);
    command.args([
        "build",
        "--quiet",
        "--locked",
        "--manifest-path",
        manifest
            .to_str()
            .ok_or_else(|| format!("fixture manifest path is not UTF-8: {}", manifest.display()))?,
        "--target-dir",
        target
            .to_str()
            .ok_or_else(|| format!("fixture target path is not UTF-8: {}", target.display()))?,
    ]);
    let output = process::run(&mut command, BUILD_DEADLINE)
        .map_err(|failure| failure.diagnostic("fake LSP build has no daemon runtime"))?;
    if !output.status().success() {
        return Err(output.diagnostic(
            "failed to build the fake LSP fixture",
            "fake LSP build has no daemon runtime",
        ));
    }
    let binary = target.join("debug").join(BINARY_NAME);
    if binary.is_file() {
        Ok(binary)
    } else {
        Err(format!(
            "fake LSP build did not create {}",
            binary.display()
        ))
    }
}