use std::collections::HashMap;
use std::error::Error;
use increparse::{CancelToken, Engine};
use lsp_server::Connection;
use lsp_types::{
CodeActionParams, CompletionParams, CompletionResponse, DidChangeTextDocumentParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, HoverParams, Location, OneOf,
TextDocumentSyncCapability, TextDocumentSyncKind,
};
use crate::diagnostics::{self, DiagnosticsOptions, FailedNode};
use crate::document::Document;
use crate::encoding::PositionEncoding;
#[allow(clippy::mutable_key_type)]
pub type Documents<C> = HashMap<lsp_types::Uri, Document<C>>;
pub trait Language<C: Clone + PartialEq + Send + 'static>: Send + Sync + 'static {
const SUPPORTS_SYMBOLS: bool = false;
fn supports_symbols(&self) -> bool {
Self::SUPPORTS_SYMBOLS
}
fn engine(&self) -> &Engine<C>;
fn root_ctx(&self) -> C;
fn encoding(&self) -> PositionEncoding {
PositionEncoding::Utf16
}
fn diagnostic(
&self,
doc: &Document<C>,
node: FailedNode<'_, C>,
) -> Option<lsp_types::Diagnostic>;
fn extra_diagnostics(&self, doc: &Document<C>) -> Vec<lsp_types::Diagnostic> {
let _ = doc;
Vec::new()
}
fn symbols(&self, doc: &Document<C>) -> Vec<lsp_types::DocumentSymbol> {
let _ = doc;
Vec::new()
}
fn supports_hover(&self) -> bool {
false
}
fn supports_definition(&self) -> bool {
false
}
fn supports_completion(&self) -> bool {
false
}
fn supports_code_actions(&self) -> bool {
false
}
fn code_action(
&self,
doc: &Document<C>,
range: lsp_types::Range,
) -> Vec<lsp_types::CodeAction> {
let _ = (doc, range);
Vec::new()
}
fn hover(&self, doc: &Document<C>, offset: usize) -> Option<lsp_types::Hover> {
let _ = (doc, offset);
None
}
fn definition(&self, doc: &Document<C>, offset: usize) -> Option<Vec<Location>> {
let _ = (doc, offset);
None
}
fn completion(&self, doc: &Document<C>, offset: usize) -> Option<CompletionResponse> {
let _ = (doc, offset);
None
}
}
pub(crate) fn capabilities<C, L>(language: &L) -> lsp_types::ServerCapabilities
where
C: Clone + PartialEq + Send + 'static,
L: Language<C>,
{
lsp_types::ServerCapabilities {
position_encoding: Some(language.encoding().capability()),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
document_symbol_provider: Some(OneOf::Left(language.supports_symbols())),
hover_provider: Some(lsp_types::HoverProviderCapability::Simple(
language.supports_hover(),
)),
definition_provider: Some(OneOf::Left(language.supports_definition())),
completion_provider: language
.supports_completion()
.then(|| lsp_types::CompletionOptions {
..Default::default()
}),
code_action_provider: Some(lsp_types::CodeActionProviderCapability::Simple(
language.supports_code_actions(),
)),
..Default::default()
}
}
fn publish<C, L>(
connection: &Connection,
language: &L,
doc: &Document<C>,
) -> Result<(), Box<dyn Error + Send + Sync>>
where
C: Clone + PartialEq + Send + 'static,
L: Language<C>,
{
let mut diags = diagnostics::diagnostics(doc, DiagnosticsOptions::default(), |node| {
language.diagnostic(doc, node)
});
diags.extend(language.extra_diagnostics(doc));
let params = lsp_types::PublishDiagnosticsParams {
uri: doc.uri().clone(),
diagnostics: diags,
version: Some(doc.version()),
};
connection.sender.send(lsp_server::Message::Notification(
lsp_server::Notification::new("textDocument/publishDiagnostics".into(), params),
))?;
Ok(())
}
#[allow(clippy::mutable_key_type)]
pub fn serve_on<C, L>(
connection: Connection,
language: L,
documents: Documents<C>,
) -> Result<(), Box<dyn Error + Send + Sync>>
where
C: Clone + PartialEq + Send + 'static,
L: Language<C>,
{
#[allow(clippy::mutable_key_type)]
let mut documents = documents;
let _initialization_params =
connection.initialize(serde_json::to_value(capabilities(&language))?)?;
run_loop(connection, language, &mut documents)
}
#[allow(clippy::mutable_key_type)]
fn run_loop<C, L>(
connection: Connection,
language: L,
documents: &mut Documents<C>,
) -> Result<(), Box<dyn Error + Send + Sync>>
where
C: Clone + PartialEq + Send + 'static,
L: Language<C>,
{
for msg in &connection.receiver {
match msg {
lsp_server::Message::Request(req) => {
if connection.handle_shutdown(&req)? {
break;
}
match req.method.as_str() {
"textDocument/documentSymbol" if language.supports_symbols() => {
let params: lsp_types::DocumentSymbolParams =
serde_json::from_value(req.params)?;
let symbols = documents
.get(¶ms.text_document.uri)
.map(|doc| language.symbols(doc))
.unwrap_or_default();
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_ok(req.id, symbols),
))?;
}
"textDocument/hover" if language.supports_hover() => {
let params: HoverParams = serde_json::from_value(req.params)?;
let tdp = ¶ms.text_document_position_params;
let hover = documents
.get(&tdp.text_document.uri)
.and_then(|doc| language.hover(doc, doc.offset(tdp.position)));
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_ok(req.id, hover),
))?;
}
"textDocument/definition" if language.supports_definition() => {
let params: lsp_types::GotoDefinitionParams =
serde_json::from_value(req.params)?;
let tdp = ¶ms.text_document_position_params;
let locations = documents
.get(&tdp.text_document.uri)
.and_then(|doc| language.definition(doc, doc.offset(tdp.position)));
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_ok(req.id, locations),
))?;
}
"textDocument/completion" if language.supports_completion() => {
let params: CompletionParams = serde_json::from_value(req.params)?;
let tdp = ¶ms.text_document_position;
let completions = documents
.get(&tdp.text_document.uri)
.and_then(|doc| language.completion(doc, doc.offset(tdp.position)));
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_ok(req.id, completions),
))?;
}
"textDocument/codeAction" if language.supports_code_actions() => {
let params: CodeActionParams = serde_json::from_value(req.params)?;
let actions = documents
.get(¶ms.text_document.uri)
.map(|doc| language.code_action(doc, params.range))
.unwrap_or_default();
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_ok(
req.id,
actions
.into_iter()
.map(lsp_types::CodeActionOrCommand::CodeAction)
.collect::<Vec<_>>(),
),
))?;
}
_ => {
connection.sender.send(lsp_server::Message::Response(
lsp_server::Response::new_err(
req.id,
lsp_server::ErrorCode::MethodNotFound as i32,
"method not supported".into(),
),
))?;
}
}
}
lsp_server::Message::Notification(notification) => {
let lsp_server::Notification { method, params, .. } = notification;
match method.as_str() {
"textDocument/didOpen" => {
let params: DidOpenTextDocumentParams = serde_json::from_value(params)?;
let item = params.text_document;
let mut doc = Document::open(
item.uri.clone(),
item.version,
item.language_id.clone(),
item.text,
language.encoding(),
language.root_ctx(),
);
doc.apply_changes(
language.engine(),
item.version,
&[],
&increparse::SerialExecutor,
&CancelToken::new(),
);
publish(&connection, &language, &doc)?;
documents.insert(item.uri, doc);
}
"textDocument/didChange" => {
let params: DidChangeTextDocumentParams = serde_json::from_value(params)?;
let uri = params.text_document.uri.clone();
if let Some(doc) = documents.get_mut(&uri) {
doc.apply_changes(
language.engine(),
params.text_document.version,
¶ms.content_changes,
&increparse::SerialExecutor,
&CancelToken::new(),
);
publish(&connection, &language, doc)?;
}
}
"textDocument/didClose" => {
let params: DidCloseTextDocumentParams = serde_json::from_value(params)?;
let uri = params.text_document.uri;
documents.remove(&uri);
connection.sender.send(lsp_server::Message::Notification(
lsp_server::Notification::new(
"textDocument/publishDiagnostics".into(),
lsp_types::PublishDiagnosticsParams {
uri,
diagnostics: Vec::new(),
version: None,
},
),
))?;
}
_ => {}
}
}
lsp_server::Message::Response(_) => {}
}
}
Ok(())
}
pub fn serve<C, L>(language: L) -> Result<(), Box<dyn Error + Send + Sync>>
where
C: Clone + PartialEq + Send + 'static,
L: Language<C>,
{
let (connection, io_threads) = Connection::stdio();
serve_on(connection, language, Documents::new())?;
io_threads.join()?;
Ok(())
}