blues-lsp 0.1.3

LSP language server for the Bluespec SystemVerilog language
Documentation
use std::{
    collections::HashMap,
    error::Error,
    path::{Path, PathBuf},
};

use lsp_server::ErrorCode;
use lsp_types::Url;
use tracing::{debug, error, info, warn};

use crate::{
    lsp::handlers::{
        completions::completion,
        definitions::{
            document_highlight, document_highlight_capabilities, goto_definition, references,
            references_capabilities,
        },
        ranges::{folding_range, folding_range_capabilities},
        rename::{prepare_rename, rename, rename_capabilities},
        symbols::{
            document_symbol, document_symbol_capabilities, workspace_symbol,
            workspace_symbol_capabilities,
        },
    },
    project::{FileHandle, PkgHandle, Project},
    util::data::either::Either,
};

use super::{
    error::LspErrorString,
    handlers::{
        completions::completion_capabilities,
        definitions::definition_capabilites,
        diagnostics::{diagnostic_capabilities, document_diagnostic, workspace_diagnostic},
        document_sync::{
            did_change_text_document, did_close_text_document, did_open_text_document,
            text_document_sync_capability,
        },
        semantic_tokens::{semantic_tokens_capabilities, semantic_tokens_full},
    },
};

use super::error::LspResult;

pub struct Server {
    pub(super) proj: Project,
    pub(super) open_documents: HashMap<Url, FileHandle>,
}

impl Server {
    pub fn initialize(
        connection: &lsp_server::Connection,
    ) -> std::result::Result<Self, Either<lsp_server::ProtocolError, serde_json::Error>> {
        let server_capabilities = serde_json::to_value(&lsp_types::ServerCapabilities {
            text_document_sync: Some(text_document_sync_capability()),
            diagnostic_provider: Some(diagnostic_capabilities()),
            semantic_tokens_provider: Some(semantic_tokens_capabilities()),
            definition_provider: Some(definition_capabilites()),
            references_provider: Some(references_capabilities()),
            document_highlight_provider: Some(document_highlight_capabilities()),
            completion_provider: Some(completion_capabilities()),
            document_symbol_provider: Some(document_symbol_capabilities()),
            workspace_symbol_provider: Some(workspace_symbol_capabilities()),
            folding_range_provider: Some(folding_range_capabilities()),
            rename_provider: Some(rename_capabilities()),
            ..Default::default()
        })
        .expect("malformed ServerCapabilities");

        info!("Waiting for 'initialize' call from client");
        let (id, params) = connection.initialize_start().map_err(Either::Left)?;

        info!("Got 'initialize' from client");
        debug!("initialize client params: {params}");

        let initialize_data = serde_json::json!({
            "capabilities": server_capabilities,
        });

        debug!("sending server params: {initialize_data}");

        connection
            .initialize_finish(id, initialize_data)
            .map_err(Either::Left)?;

        info!("Got 'initialized' from client");

        let initialize_params: lsp_types::InitializeParams =
            serde_json::from_value(params).map_err(Either::Right)?;

        // TODO: proper multiple workspace support
        let root = initialize_params
            .workspace_folders
            .and_then(|w| w.first().and_then(|w| uri_to_pathbuf(&w.uri)));

        let mut proj = Project::new(root);
        proj.initialize();

        Ok(Self {
            proj,
            open_documents: HashMap::new(),
        })
    }

    pub fn run(
        &mut self,
        connection: &lsp_server::Connection,
    ) -> std::result::Result<(), Box<dyn Error>> {
        for msg in &connection.receiver {
            match msg {
                lsp_server::Message::Request(request)
                    if connection.handle_shutdown(&request)? =>
                {
                    break;
                }
                lsp_server::Message::Request(req) => self.handle_request(connection, req)?,
                lsp_server::Message::Notification(msg) => self.handle_notification(msg)?,
                msg => error!("unknown msg: {msg:?}"),
            }
        }

        Ok(())
    }

    fn handle_request(
        &mut self,
        connection: &lsp_server::Connection,
        req: lsp_server::Request,
    ) -> std::result::Result<(), Box<dyn Error>> {
        use lsp_types::request::*;
        let res = match req.method.as_str() {
            DocumentDiagnosticRequest::METHOD => {
                self.request_handler::<DocumentDiagnosticRequest>(document_diagnostic, req)
            }
            WorkspaceDiagnosticRequest::METHOD => {
                self.request_handler::<WorkspaceDiagnosticRequest>(workspace_diagnostic, req)
            }
            SemanticTokensFullRequest::METHOD => {
                self.request_handler::<SemanticTokensFullRequest>(semantic_tokens_full, req)
            }
            GotoDefinition::METHOD => self.request_handler::<GotoDefinition>(goto_definition, req),
            References::METHOD => self.request_handler::<References>(references, req),
            DocumentHighlightRequest::METHOD => {
                self.request_handler::<DocumentHighlightRequest>(document_highlight, req)
            }
            Completion::METHOD => self.request_handler::<Completion>(completion, req),
            DocumentSymbolRequest::METHOD => {
                self.request_handler::<DocumentSymbolRequest>(document_symbol, req)
            }
            WorkspaceSymbolRequest::METHOD => {
                self.request_handler::<WorkspaceSymbolRequest>(workspace_symbol, req)
            }
            FoldingRangeRequest::METHOD => {
                self.request_handler::<FoldingRangeRequest>(folding_range, req)
            }
            PrepareRenameRequest::METHOD => {
                self.request_handler::<PrepareRenameRequest>(prepare_rename, req)
            }
            Rename::METHOD => self.request_handler::<Rename>(rename, req),
            _ => {
                error!("unknown request: {req:?}");
                lsp_server::Response {
                    id: req.id,
                    result: None,
                    error: Some(lsp_server::ResponseError {
                        code: lsp_server::ErrorCode::MethodNotFound as i32,
                        message: "unknown LSP request method".into(),
                        data: None,
                    }),
                }
            }
        };

        connection.sender.send(lsp_server::Message::Response(res))?;
        Ok(())
    }

    fn handle_notification(
        &mut self,
        msg: lsp_server::Notification,
    ) -> std::result::Result<(), Box<dyn Error>> {
        use lsp_types::notification::*;
        match msg.method.as_str() {
            DidOpenTextDocument::METHOD => {
                self.notification_handler::<DidOpenTextDocument>(did_open_text_document, msg);
            }
            DidChangeTextDocument::METHOD => {
                self.notification_handler::<DidChangeTextDocument>(did_change_text_document, msg);
            }
            DidCloseTextDocument::METHOD => {
                self.notification_handler::<DidCloseTextDocument>(did_close_text_document, msg);
            }
            _ => {
                warn!("unknown notification: {msg:?}");
            }
        };

        Ok(())
    }

    fn request_handler<R>(
        &mut self,
        f: impl Fn(&mut Server, R::Params) -> LspResult<R::Result>,
        req: lsp_server::Request,
    ) -> lsp_server::Response
    where
        R: lsp_types::request::Request,
    {
        let (id, params) = req
            .extract::<R::Params>(R::METHOD)
            .expect("request_handler called with incorrect method ID");

        let res = ((f)(self, params)).and_then(|r| serde_json::to_value(r).map_err(Into::into));

        match res {
            Ok(result) => lsp_server::Response {
                id,
                result: Some(result),
                error: None,
            },
            Err(error) => {
                error!("Error handling request {}: {error}", R::METHOD);
                lsp_server::Response {
                    id,
                    result: None,
                    error: Some(lsp_server::ResponseError {
                        code: error.code() as i32,
                        message: error.to_string(),
                        data: None,
                    }),
                }
            }
        }
    }

    fn notification_handler<N>(
        &mut self,
        f: impl Fn(&mut Server, N::Params) -> std::result::Result<(), Box<dyn std::error::Error>>,
        msg: lsp_server::Notification,
    ) where
        N: lsp_types::notification::Notification,
    {
        let params = msg
            .extract::<N::Params>(N::METHOD)
            .expect("notification_handler called with incorrect method ID");

        let res = (f)(self, params);

        match res {
            Ok(_) => (),
            Err(error) => error!("Error handling notification {}: {error}", N::METHOD),
        }
    }

    pub fn uri_to_file(&self, uri: &Url) -> LspResult<FileHandle> {
        if let Some(file) = self.open_documents.get(uri) {
            return Ok(*file);
        }

        if let Some(file) = uri_to_pathbuf(uri) {
            let file = self.proj.lookup_file(&file);
            return Ok(file);
        }

        Err(LspErrorString(format!("Unknown URI '{}'", *uri), ErrorCode::InvalidParams).into())
    }

    pub fn file_to_uri(&self, file: FileHandle) -> LspResult<Url> {
        // Make sure we return identical URI's for open documents
        if let Some((uri, _)) = self.open_documents.iter().find(|(_, v)| **v == file) {
            return Ok(uri.clone());
        }

        if let Some(path) = &self.proj.get_file(file).path {
            return path_to_uri(path);
        }

        error!("Non local file not in open_documents");
        Err(LspErrorString(
            "Non local file not in open_documents".to_string(),
            ErrorCode::InternalError,
        )
        .into())
    }

    pub fn uri_to_pkg(&mut self, uri: &Url) -> LspResult<PkgHandle> {
        let file = self.uri_to_file(uri)?;
        Ok(self.proj.pkg_for_file(file))
    }
}

/// Convert LSP uri's to local file paths, if possible
pub fn uri_to_pathbuf(uri: &Url) -> Option<PathBuf> {
    if uri.scheme() != "file" {
        return None;
    }

    uri.to_file_path().ok()
}

pub fn path_to_uri(path: &Path) -> LspResult<Url> {
    Url::from_file_path(path).map_err(|_| {
        LspErrorString(
            format!("Failed to constuct URI from path '{}'", path.display()),
            ErrorCode::InternalError,
        )
        .into()
    })
}