lux-lsp 0.40.4

LSP server for Lux
use std::borrow::Cow;
use std::io::Write;
use std::sync::Mutex;

use eyre::{Context, Result};
use lux_lib::progress::client::ProgressMessage;
use lux_lib::workspace::Workspace;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tower_lsp_server::ls_types::notification;
use tower_lsp_server::ls_types::{
    InitializeParams, InitializeResult, InitializedParams, MessageType, ProgressParams,
    ProgressParamsValue, ProgressToken, WorkDoneProgress, WorkDoneProgressBegin,
    WorkDoneProgressEnd, WorkDoneProgressReport,
};
use tower_lsp_server::{jsonrpc, Client, LanguageServer, LspService, Server};

use lux_lib::progress;

use crate::tempfile::TempFile;

mod tempfile;

#[derive(Debug)]
struct Backend {
    client: Client,
    workspace: Mutex<Option<Workspace>>,
}

impl Backend {
    async fn start_socket_listener(&self, workspace: &Workspace) -> Result<()> {
        let listener = TcpListener::bind("127.0.0.1:0").await?;
        let port = listener.local_addr()?.port();

        let port_path = progress::lsp_port_path(workspace);

        let mut port_file = TempFile::create(&port_path).wrap_err("failed to create port file")?;
        port_file
            .write_all(port.to_string().as_bytes())
            .wrap_err("failed to write port to file")?;

        tracing::info!("progress listener on 127.0.0.1:{port}");

        let client = self.client.clone();

        tokio::spawn(async move {
            let _port_file = port_file;

            loop {
                match listener.accept().await {
                    Ok((stream, addr)) => {
                        tracing::debug!("progress connection from {addr}");
                        let client = client.clone();
                        tokio::spawn(handle_connection(stream, client));
                    }
                    Err(e) => {
                        tracing::error!("accept error: {e}");
                        break;
                    }
                }
            }
        });

        Ok(())
    }
}

async fn handle_connection(stream: TcpStream, client: Client) {
    let reader = BufReader::new(stream);
    let mut lines = reader.lines();
    while let Ok(Some(line)) = lines.next_line().await {
        let msg = match serde_json::from_str::<ProgressMessage>(&line) {
            Ok(m) => m,
            Err(e) => {
                tracing::warn!("invalid progress message: {e}");
                continue;
            }
        };
        relay_progress(&client, msg).await;
    }
}

async fn relay_progress(client: &Client, msg: ProgressMessage) {
    match msg {
        ProgressMessage::Begin { id, title } => {
            client
                .send_notification::<notification::Progress>(ProgressParams {
                    token: ProgressToken::Number(id),
                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::Begin(
                        WorkDoneProgressBegin {
                            title,
                            ..Default::default()
                        },
                    )),
                })
                .await;
        }
        ProgressMessage::Report { id, message } => {
            client
                .send_notification::<notification::Progress>(ProgressParams {
                    token: ProgressToken::Number(id),
                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::Report(
                        WorkDoneProgressReport {
                            message: Some(message),
                            ..Default::default()
                        },
                    )),
                })
                .await;
        }
        ProgressMessage::End { id } => {
            client
                .send_notification::<notification::Progress>(ProgressParams {
                    token: ProgressToken::Number(id),
                    value: ProgressParamsValue::WorkDone(WorkDoneProgress::End(
                        WorkDoneProgressEnd { message: None },
                    )),
                })
                .await;
        }
    }
}

impl LanguageServer for Backend {
    async fn initialize(
        &self,
        params: InitializeParams,
    ) -> Result<InitializeResult, jsonrpc::Error> {
        let workspace = params
            .workspace_folders
            .and_then(|f| f.into_iter().next())
            .map(|folder| {
                folder.uri.to_file_path().map(Cow::into_owned).ok_or(
                    jsonrpc::Error::invalid_params("invalid workspace folder URI"),
                )
            })
            .transpose()?
            .map_or_else(Workspace::current, Workspace::from_exact)
            .map_err(|_| jsonrpc::Error::internal_error())?;

        match workspace {
            Some(ref ws) => {
                if let Ok(mut lock) = self.workspace.lock() {
                    *lock = Some(ws.clone());
                }
            }
            None => {
                self.client
                    .log_message(
                        MessageType::WARNING,
                        "no workspace found; progress and diagnostics disabled",
                    )
                    .await;
            }
        }

        Ok(InitializeResult::default())
    }

    async fn initialized(&self, _: InitializedParams) {
        self.client
            .log_message(MessageType::INFO, "lx-lsp initialized")
            .await;

        let workspace = self
            .workspace
            .lock()
            .unwrap_or_else(|err| unreachable!("we don't panic while holding the mutex.\n{}", err))
            .clone();

        match workspace {
            Some(ref ws) => {
                if let Err(e) = self.start_socket_listener(ws).await {
                    self.client
                        .log_message(
                            MessageType::ERROR,
                            format!("failed to start progress listener: {e}"),
                        )
                        .await;
                }
            }
            None => {
                self.client
                    .log_message(
                        MessageType::WARNING,
                        "no workspace found; progress and diagnostics disabled",
                    )
                    .await;
            }
        }
    }

    async fn shutdown(&self) -> Result<(), jsonrpc::Error> {
        Ok(())
    }
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let (service, socket) = LspService::new(|client| Backend {
        client,
        workspace: Mutex::new(None),
    });

    Server::new(tokio::io::stdin(), tokio::io::stdout(), socket)
        .serve(service)
        .await;
}