use std::error::Error;
use crossbeam_channel::select;
use lsp_server::{Connection, Message};
use lsp_types::{
ClientCapabilities, FoldingRangeProviderCapability, InitializeParams, OneOf,
PositionEncodingKind, SelectionRangeProviderCapability, SemanticTokensFullOptions,
SemanticTokensOptions, ServerCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind,
};
use crate::text::PositionEncoding;
use super::analysis_thread::{AnalysisRequest, spawn_analysis_thread};
use super::read_jobs::ReadJob;
use super::semantic_tokens::legend;
use super::state::{GlobalState, Outbound};
use super::task_pool::{TaskPool, read_pool_size};
pub(crate) type DynError = Box<dyn Error + Sync + Send>;
pub fn run() -> Result<(), DynError> {
let (connection, io_threads) = Connection::stdio();
serve(&connection)?;
io_threads.join()?;
Ok(())
}
pub fn serve(connection: &Connection) -> Result<(), DynError> {
let (id, params) = connection.initialize_start()?;
let params: InitializeParams = serde_json::from_value(params)?;
let encoding = negotiate_position_encoding(¶ms.capabilities);
let result = serde_json::json!({ "capabilities": server_capabilities(encoding) });
connection.initialize_finish(id, result)?;
main_loop(connection, encoding)
}
fn negotiate_position_encoding(capabilities: &ClientCapabilities) -> PositionEncoding {
let offered = capabilities
.general
.as_ref()
.and_then(|general| general.position_encodings.as_deref())
.unwrap_or_default();
if offered.contains(&PositionEncodingKind::UTF8) {
PositionEncoding::Utf8
} else {
PositionEncoding::Utf16
}
}
fn server_capabilities(encoding: PositionEncoding) -> ServerCapabilities {
ServerCapabilities {
position_encoding: Some(match encoding {
PositionEncoding::Utf8 => PositionEncodingKind::UTF8,
PositionEncoding::Utf16 => PositionEncodingKind::UTF16,
}),
text_document_sync: Some(TextDocumentSyncCapability::Kind(
TextDocumentSyncKind::INCREMENTAL,
)),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
document_symbol_provider: Some(OneOf::Left(true)),
folding_range_provider: Some(FoldingRangeProviderCapability::Simple(true)),
selection_range_provider: Some(SelectionRangeProviderCapability::Simple(true)),
semantic_tokens_provider: Some(
SemanticTokensOptions {
work_done_progress_options: Default::default(),
legend: legend(),
range: None,
full: Some(SemanticTokensFullOptions::Bool(true)),
}
.into(),
),
..Default::default()
}
}
fn main_loop(connection: &Connection, encoding: PositionEncoding) -> Result<(), DynError> {
let (out_tx, out_rx) = crossbeam_channel::unbounded::<Outbound>();
let (analysis_tx, analysis_rx) = crossbeam_channel::unbounded::<AnalysisRequest>();
let (read_tx, read_rx) = crossbeam_channel::unbounded::<ReadJob>();
let read_pool = TaskPool::new("fatou-lsp-read", read_pool_size());
let analysis_handle =
spawn_analysis_thread(analysis_rx, read_rx, out_tx, read_pool.spawner(), encoding);
let mut state = GlobalState::new(connection.sender.clone(), analysis_tx, read_tx, encoding);
loop {
select! {
recv(connection.receiver) -> msg => {
let Ok(msg) = msg else { break };
match msg {
Message::Request(req) => {
if connection.handle_shutdown(&req)? {
break;
}
state.on_request(req);
}
Message::Notification(note) => state.on_notification(note),
Message::Response(_) => {}
}
}
recv(out_rx) -> outbound => {
let Ok(outbound) = outbound else { break };
state.on_outbound(outbound);
}
}
}
drop(state);
let _ = analysis_handle.join();
Ok(())
}
#[cfg(test)]
mod tests {
use lsp_types::GeneralClientCapabilities;
use super::*;
fn caps_offering(encodings: Option<Vec<PositionEncodingKind>>) -> ClientCapabilities {
ClientCapabilities {
general: Some(GeneralClientCapabilities {
position_encodings: encodings,
..Default::default()
}),
..Default::default()
}
}
#[test]
fn negotiation_defaults_to_utf16() {
let none = ClientCapabilities::default();
assert_eq!(negotiate_position_encoding(&none), PositionEncoding::Utf16);
assert_eq!(
negotiate_position_encoding(&caps_offering(None)),
PositionEncoding::Utf16
);
assert_eq!(
negotiate_position_encoding(&caps_offering(Some(vec![
PositionEncodingKind::UTF16,
PositionEncodingKind::UTF32,
]))),
PositionEncoding::Utf16
);
}
#[test]
fn negotiation_prefers_offered_utf8() {
assert_eq!(
negotiate_position_encoding(&caps_offering(Some(vec![
PositionEncodingKind::UTF16,
PositionEncodingKind::UTF8,
]))),
PositionEncoding::Utf8
);
}
}