erlls 0.0.28

Erlang language server
use erlls_core::{config::Config, header::Header, message::DocumentUri, server::LanguageServer};
use orfail::OrFail;
use std::{
    future::Future,
    io::{BufRead, Write},
    path::PathBuf,
};

fn main() -> orfail::Result<()> {
    env_logger::init();

    let config = create_config();
    let mut server = LanguageServer::<FileSystem>::new(config, FileSystem);
    let stdin = std::io::stdin();
    let stdout = std::io::stdout();
    let mut stdin = stdin.lock();
    let mut stdout = stdout.lock();
    loop {
        let buf = read_message(&mut stdin).or_fail()?;
        log::debug!("Received JSON: {}", std::str::from_utf8(&buf).or_fail()?);

        let future = server.handle_incoming_message(buf);
        futures::executor::block_on(future);

        while let Some(msg) = server.take_outgoing_message() {
            write_message(&mut stdout, &msg).or_fail()?;
        }
    }
}

fn read_message<R: BufRead>(reader: &mut R) -> orfail::Result<Vec<u8>> {
    let header = Header::from_reader(reader).or_fail()?;
    let mut buf = vec![0; header.content_length];
    reader.read_exact(&mut buf).or_fail()?;
    Ok(buf)
}

fn write_message<W: Write>(writer: &mut W, msg: &[u8]) -> orfail::Result<()> {
    Header::new(msg.len()).to_writer(writer).or_fail()?;
    writer.write_all(msg).or_fail()?;
    writer.flush().or_fail()?;
    log::debug!("Sent JSON: {}", std::str::from_utf8(msg).or_fail()?);
    Ok(())
}

#[derive(Debug)]
struct FileSystem;

impl erlls_core::fs::FileSystem for FileSystem {
    fn exists(&mut self, uri: &DocumentUri) -> Box<dyn Unpin + Future<Output = bool>> {
        Box::new(std::future::ready(uri.path().exists()))
    }

    fn read_file(
        &mut self,
        uri: &DocumentUri,
    ) -> Box<dyn Unpin + Future<Output = orfail::Result<String>>> {
        let result = std::fs::read_to_string(uri.path())
            .or_fail_with(|e| format!("{e}: {}", uri.path().display()));
        Box::new(std::future::ready(result))
    }

    fn read_sub_dirs(
        &mut self,
        uri: &DocumentUri,
    ) -> Box<dyn Unpin + Future<Output = Vec<DocumentUri>>> {
        let mut dirs = Vec::new();
        for entry in std::fs::read_dir(uri.path()).ok().into_iter().flatten() {
            let Ok(entry) = entry.or_fail() else {
                continue;
            };
            if entry.file_type().map_or(true, |e| !e.is_dir()) {
                continue;
            }
            if let Some(dir) = entry.path().to_str().and_then(|p| uri.join(p).ok()) {
                dirs.push(dir);
            }
        }
        Box::new(std::future::ready(dirs))
    }
}

fn create_config() -> Config {
    let mut config = Config::default();
    if let Some(kernel_lib_dir) = std::process::Command::new("erl")
        .arg("-boot")
        .arg("start_clean")
        .arg("-noshell")
        .arg("-eval")
        .arg("io:format(code:lib_dir(kernel)).")
        .arg("-s")
        .arg("init")
        .arg("stop")
        .output()
        .ok()
        .and_then(|output| String::from_utf8(output.stdout).ok().map(PathBuf::from))
    {
        config
            .erl_libs
            .extend(kernel_lib_dir.parent().into_iter().map(|p| p.to_owned()));
        config.erl_libs.push(PathBuf::from("_checkouts/"));
        config.erl_libs.push(PathBuf::from("_build/default/lib"));
    }
    config
}