From 7c67612b8a894187fa3b64725531a5459f9211bf Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Fri, 10 Aug 2018 22:33:29 +0300 Subject: organizize --- crates/server/Cargo.toml | 18 +++ crates/server/src/caps.rs | 36 ++++++ crates/server/src/dispatch.rs | 174 +++++++++++++++++++++++++++++ crates/server/src/handlers.rs | 61 +++++++++++ crates/server/src/io.rs | 202 ++++++++++++++++++++++++++++++++++ crates/server/src/main.rs | 249 ++++++++++++++++++++++++++++++++++++++++++ crates/server/src/req.rs | 41 +++++++ 7 files changed, 781 insertions(+) create mode 100644 crates/server/Cargo.toml create mode 100644 crates/server/src/caps.rs create mode 100644 crates/server/src/dispatch.rs create mode 100644 crates/server/src/handlers.rs create mode 100644 crates/server/src/io.rs create mode 100644 crates/server/src/main.rs create mode 100644 crates/server/src/req.rs (limited to 'crates/server') diff --git a/crates/server/Cargo.toml b/crates/server/Cargo.toml new file mode 100644 index 000000000..e6d1b18c3 --- /dev/null +++ b/crates/server/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "m" +version = "0.1.0" +authors = ["Aleksey Kladov "] + +[dependencies] +failure = "0.1.2" +languageserver-types = "0.48.0" +serde_json = "1.0.24" +serde = "1.0.71" +serde_derive = "1.0.71" +drop_bomb = "0.1.0" +crossbeam-channel = "0.2.4" +threadpool = "1.7.1" +flexi_logger = "0.9.0" +log = "0.4.3" +libeditor = { path = "../libeditor" } +libanalysis = { path = "../libanalysis" } diff --git a/crates/server/src/caps.rs b/crates/server/src/caps.rs new file mode 100644 index 000000000..3d89c64a9 --- /dev/null +++ b/crates/server/src/caps.rs @@ -0,0 +1,36 @@ +use languageserver_types::{ + ServerCapabilities, + TextDocumentSyncCapability, + TextDocumentSyncOptions, + TextDocumentSyncKind, +}; + +pub const SERVER_CAPABILITIES: ServerCapabilities = ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Options( + TextDocumentSyncOptions { + open_close: Some(true), + change: Some(TextDocumentSyncKind::Full), + will_save: None, + will_save_wait_until: None, + save: None, + } + )), + hover_provider: None, + completion_provider: None, + signature_help_provider: None, + definition_provider: None, + type_definition_provider: None, + implementation_provider: None, + references_provider: None, + document_highlight_provider: None, + document_symbol_provider: None, + workspace_symbol_provider: None, + code_action_provider: None, + code_lens_provider: None, + document_formatting_provider: None, + document_range_formatting_provider: None, + document_on_type_formatting_provider: None, + rename_provider: None, + color_provider: None, + execute_command_provider: None, +}; diff --git a/crates/server/src/dispatch.rs b/crates/server/src/dispatch.rs new file mode 100644 index 000000000..2da0996e3 --- /dev/null +++ b/crates/server/src/dispatch.rs @@ -0,0 +1,174 @@ +use std::marker::PhantomData; + +use serde::{ + ser::Serialize, + de::DeserializeOwned, +}; +use serde_json; +use drop_bomb::DropBomb; + +use ::{ + Result, + req::{Request, Notification}, + io::{Io, RawMsg, RawResponse, RawRequest, RawNotification}, +}; + +pub struct Responder { + id: u64, + bomb: DropBomb, + ph: PhantomData, +} + +impl Responder + where + R::Params: DeserializeOwned, + R::Result: Serialize, +{ + pub fn response(self, io: &mut Io, resp: Result) -> Result<()> { + match resp { + Ok(res) => self.result(io, res)?, + Err(e) => { + self.error(io)?; + return Err(e); + } + } + Ok(()) + } + + pub fn result(mut self, io: &mut Io, result: R::Result) -> Result<()> { + self.bomb.defuse(); + io.send(RawMsg::Response(RawResponse { + id: Some(self.id), + result: serde_json::to_value(result)?, + error: serde_json::Value::Null, + })); + Ok(()) + } + + pub fn error(mut self, io: &mut Io) -> Result<()> { + self.bomb.defuse(); + error(io, self.id, ErrorCode::InternalError, "internal error") + } +} + + +fn parse_request_as(raw: RawRequest) -> Result<::std::result::Result<(R::Params, Responder), RawRequest>> + where + R: Request, + R::Params: DeserializeOwned, + R::Result: Serialize, +{ + if raw.method != R::METHOD { + return Ok(Err(raw)); + } + + let params: R::Params = serde_json::from_value(raw.params)?; + let responder = Responder { + id: raw.id, + bomb: DropBomb::new("dropped request"), + ph: PhantomData, + }; + Ok(Ok((params, responder))) +} + +pub fn handle_request(req: &mut Option, f: F) -> Result<()> + where + R: Request, + R::Params: DeserializeOwned, + R::Result: Serialize, + F: FnOnce(R::Params, Responder) -> Result<()> +{ + match req.take() { + None => Ok(()), + Some(r) => match parse_request_as::(r)? { + Ok((params, responder)) => f(params, responder), + Err(r) => { + *req = Some(r); + Ok(()) + }, + } + } +} + +pub fn expect_request(io: &mut Io, raw: RawRequest) -> Result)>> + where + R: Request, + R::Params: DeserializeOwned, + R::Result: Serialize, +{ + let ret = match parse_request_as::(raw)? { + Ok(x) => Some(x), + Err(raw) => { + unknown_method(io, raw)?; + None + } + }; + Ok(ret) +} + +fn parse_notification_as(raw: RawNotification) -> Result<::std::result::Result> + where + N: Notification, + N::Params: DeserializeOwned, +{ + if raw.method != N::METHOD { + return Ok(Err(raw)); + } + let params: N::Params = serde_json::from_value(raw.params)?; + Ok(Ok(params)) +} + +pub fn handle_notification(not: &mut Option, f: F) -> Result<()> + where + N: Notification, + N::Params: DeserializeOwned, + F: FnOnce(N::Params) -> Result<()> +{ + match not.take() { + None => Ok(()), + Some(n) => match parse_notification_as::(n)? { + Ok(params) => f(params), + Err(n) => { + *not = Some(n); + Ok(()) + }, + } + } +} + + +pub fn unknown_method(io: &mut Io, raw: RawRequest) -> Result<()> { + error(io, raw.id, ErrorCode::MethodNotFound, "unknown method") +} + +fn error(io: &mut Io, id: u64, code: ErrorCode, message: &'static str) -> Result<()> { + #[derive(Serialize)] + struct Error { + code: i32, + message: &'static str, + } + io.send(RawMsg::Response(RawResponse { + id: Some(id), + result: serde_json::Value::Null, + error: serde_json::to_value(Error { + code: code as i32, + message, + })?, + })); + Ok(()) +} + + +#[allow(unused)] +enum ErrorCode { + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + ServerErrorStart = -32099, + ServerErrorEnd = -32000, + ServerNotInitialized = -32002, + UnknownErrorCode = -32001, + RequestCancelled = -32800, +} diff --git a/crates/server/src/handlers.rs b/crates/server/src/handlers.rs new file mode 100644 index 000000000..5ee87a4dd --- /dev/null +++ b/crates/server/src/handlers.rs @@ -0,0 +1,61 @@ +use languageserver_types::{Range, Position}; +use libanalysis::World; +use libeditor::{self, LineIndex, LineCol, TextRange, TextUnit}; +use {req, Result, FilePath}; + +pub fn handle_syntax_tree( + world: World, + params: req::SyntaxTreeParams, +) -> Result { + let path = params.text_document.file_path()?; + let file = world.file_syntax(&path)?; + Ok(libeditor::syntax_tree(&file)) +} + +pub fn handle_extend_selection( + world: World, + params: req::ExtendSelectionParams, +) -> Result { + let path = params.text_document.file_path()?; + let file = world.file_syntax(&path)?; + let line_index = world.file_line_index(&path)?; + let selections = params.selections.into_iter() + .map(|r| { + let r = to_text_range(&line_index, r); + let r = libeditor::extend_selection(&file, r).unwrap_or(r); + to_vs_range(&line_index, r) + }) + .collect(); + Ok(req::ExtendSelectionResult { selections }) +} + + +fn to_text_range(line_index: &LineIndex, range: Range) -> TextRange { + TextRange::from_to( + to_text_unit(line_index, range.start), + to_text_unit(line_index, range.end), + ) +} + +fn to_text_unit(line_index: &LineIndex, position: Position) -> TextUnit { + // TODO: UTF-16 + let line_col = LineCol { + line: position.line as u32, + col: (position.character as u32).into(), + }; + line_index.offset(line_col) +} + + +fn to_vs_range(line_index: &LineIndex, range: TextRange) -> Range { + Range::new( + to_vs_position(line_index, range.start()), + to_vs_position(line_index, range.end()), + ) +} + +fn to_vs_position(line_index: &LineIndex, offset: TextUnit) -> Position { + let line_col = line_index.line_col(offset); + // TODO: UTF-16 + Position::new(line_col.line as u64, u32::from(line_col.col) as u64) +} diff --git a/crates/server/src/io.rs b/crates/server/src/io.rs new file mode 100644 index 000000000..5eafc6942 --- /dev/null +++ b/crates/server/src/io.rs @@ -0,0 +1,202 @@ +use std::{ + thread, + io::{ + stdout, stdin, + BufRead, Write, + }, +}; +use serde_json::{Value, from_str, to_string}; +use crossbeam_channel::{Receiver, Sender, bounded}; + +use Result; + + +#[derive(Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RawMsg { + Request(RawRequest), + Notification(RawNotification), + Response(RawResponse), +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RawRequest { + pub id: u64, + pub method: String, + pub params: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RawNotification { + pub method: String, + pub params: Value, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct RawResponse { + pub id: Option, + pub result: Value, + pub error: Value, +} + +struct MsgReceiver { + chan: Receiver, + thread: Option>>, +} + +impl MsgReceiver { + fn recv(&mut self) -> Result { + match self.chan.recv() { + Some(msg) => Ok(msg), + None => { + self.cleanup()?; + unreachable!() + } + } + } + + fn cleanup(&mut self) -> Result<()> { + self.thread + .take() + .ok_or_else(|| format_err!("MsgReceiver thread panicked"))? + .join() + .map_err(|_| format_err!("MsgReceiver thread panicked"))??; + bail!("client disconnected") + } + + fn stop(self) -> Result<()> { + // Can't really self.thread.join() here, b/c it might be + // blocking on read + Ok(()) + } +} + +struct MsgSender { + chan: Sender, + thread: thread::JoinHandle>, +} + +impl MsgSender { + fn send(&mut self, msg: RawMsg) { + self.chan.send(msg) + } + + fn stop(self) -> Result<()> { + drop(self.chan); + self.thread.join() + .map_err(|_| format_err!("MsgSender thread panicked"))??; + Ok(()) + } +} + +pub struct Io { + receiver: MsgReceiver, + sender: MsgSender, +} + +impl Io { + pub fn from_stdio() -> Io { + let sender = { + let (tx, rx) = bounded(16); + MsgSender { + chan: tx, + thread: thread::spawn(move || { + let stdout = stdout(); + let mut stdout = stdout.lock(); + for msg in rx { + #[derive(Serialize)] + struct JsonRpc { + jsonrpc: &'static str, + #[serde(flatten)] + msg: RawMsg, + } + let text = to_string(&JsonRpc { + jsonrpc: "2.0", + msg, + })?; + write_msg_text(&mut stdout, &text)?; + } + Ok(()) + }), + } + }; + let receiver = { + let (tx, rx) = bounded(16); + MsgReceiver { + chan: rx, + thread: Some(thread::spawn(move || { + let stdin = stdin(); + let mut stdin = stdin.lock(); + while let Some(text) = read_msg_text(&mut stdin)? { + let msg: RawMsg = from_str(&text)?; + tx.send(msg); + } + Ok(()) + })), + } + }; + Io { receiver, sender } + } + + pub fn send(&mut self, msg: RawMsg) { + self.sender.send(msg) + } + + pub fn recv(&mut self) -> Result { + self.receiver.recv() + } + + pub fn receiver(&mut self) -> &mut Receiver { + &mut self.receiver.chan + } + + pub fn cleanup_receiver(&mut self) -> Result<()> { + self.receiver.cleanup() + } + + pub fn stop(self) -> Result<()> { + self.receiver.stop()?; + self.sender.stop()?; + Ok(()) + } +} + + +fn read_msg_text(inp: &mut impl BufRead) -> Result> { + let mut size = None; + let mut buf = String::new(); + loop { + buf.clear(); + if inp.read_line(&mut buf)? == 0 { + return Ok(None); + } + if !buf.ends_with("\r\n") { + bail!("malformed header: {:?}", buf); + } + let buf = &buf[..buf.len() - 2]; + if buf.is_empty() { + break; + } + let mut parts = buf.splitn(2, ": "); + let header_name = parts.next().unwrap(); + let header_value = parts.next().ok_or_else(|| format_err!("malformed header: {:?}", buf))?; + if header_name == "Content-Length" { + size = Some(header_value.parse::()?); + } + } + let size = size.ok_or_else(|| format_err!("no Content-Length"))?; + let mut buf = buf.into_bytes(); + buf.resize(size, 0); + inp.read_exact(&mut buf)?; + let buf = String::from_utf8(buf)?; + debug!("< {}", buf); + Ok(Some(buf)) +} + +fn write_msg_text(out: &mut impl Write, msg: &str) -> Result<()> { + debug!("> {}", msg); + write!(out, "Content-Length: {}\r\n\r\n", msg.len())?; + out.write_all(msg.as_bytes())?; + out.flush()?; + Ok(()) +} diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs new file mode 100644 index 000000000..116abce1c --- /dev/null +++ b/crates/server/src/main.rs @@ -0,0 +1,249 @@ +#[macro_use] +extern crate failure; +#[macro_use] +extern crate serde_derive; +extern crate serde; +extern crate serde_json; +extern crate languageserver_types; +extern crate drop_bomb; +#[macro_use] +extern crate crossbeam_channel; +extern crate threadpool; +#[macro_use] +extern crate log; +extern crate flexi_logger; +extern crate libeditor; +extern crate libanalysis; + +mod io; +mod caps; +mod req; +mod dispatch; +mod handlers; + +use std::path::PathBuf; + +use threadpool::ThreadPool; +use crossbeam_channel::{bounded, Sender, Receiver}; +use flexi_logger::Logger; +use libanalysis::WorldState; +use languageserver_types::{TextDocumentItem, VersionedTextDocumentIdentifier, TextDocumentIdentifier}; + +use ::{ + io::{Io, RawMsg}, + handlers::{handle_syntax_tree, handle_extend_selection}, +}; + +pub type Result = ::std::result::Result; + +fn main() -> Result<()> { + Logger::with_env_or_str("m=trace, libanalysis=trace") + .log_to_file() + .directory("log") + .start()?; + info!("starting server"); + match ::std::panic::catch_unwind(|| main_inner()) { + Ok(res) => { + info!("shutting down: {:?}", res); + res + } + Err(_) => { + error!("server panicked"); + bail!("server panicked") + } + } +} + +fn main_inner() -> Result<()> { + let mut io = Io::from_stdio(); + let res = initialize(&mut io); + info!("shutting down IO..."); + let io_res = io.stop(); + info!("... IO is down"); + match (res, io_res) { + (Ok(()), Ok(())) => Ok(()), + (res, Ok(())) => res, + (Ok(()), io_res) => io_res, + (res, Err(io_err)) => { + error!("shutdown error: {:?}", io_err); + res + } + } +} + +fn initialize(io: &mut Io) -> Result<()> { + loop { + match io.recv()? { + RawMsg::Request(req) => { + if let Some((_params, resp)) = dispatch::expect_request::(io, req)? { + resp.result(io, req::InitializeResult { + capabilities: caps::SERVER_CAPABILITIES + })?; + match io.recv()? { + RawMsg::Notification(n) => { + if n.method != "initialized" { + bail!("expected initialized notification"); + } + } + _ => { + bail!("expected initialized notification"); + } + } + return initialized(io); + } + } + RawMsg::Notification(n) => { + bail!("expected initialize request, got {:?}", n) + } + RawMsg::Response(res) => { + bail!("expected initialize request, got {:?}", res) + } + } + } +} + +type Thunk = Box FnBox<&'a mut Io, Result<()>>>; + +fn initialized(io: &mut Io) -> Result<()> { + let mut world = WorldState::new(); + let mut pool = ThreadPool::new(4); + let (sender, receiver) = bounded::(16); + let res = main_loop(io, &mut world, &mut pool, sender, receiver.clone()); + info!("waiting for background jobs to finish..."); + receiver.for_each(drop); + pool.join(); + info!("...background jobs have finished"); + res +} + +fn main_loop( + io: &mut Io, + world: &mut WorldState, + pool: &mut ThreadPool, + sender: Sender, + receiver: Receiver, +) -> Result<()> { + info!("server initialized, serving requests"); + loop { + enum Event { + Msg(RawMsg), + Thunk(Thunk), + ReceiverDead, + } + + let event = select! { + recv(io.receiver(), msg) => match msg { + Some(msg) => Event::Msg(msg), + None => Event::ReceiverDead, + }, + recv(receiver, thunk) => Event::Thunk(thunk.unwrap()), + }; + + let msg = match event { + Event::ReceiverDead => { + io.cleanup_receiver()?; + unreachable!(); + } + Event::Thunk(thunk) => { + thunk.call_box(io)?; + continue; + } + Event::Msg(msg) => msg, + }; + + match msg { + RawMsg::Request(req) => { + let mut req = Some(req); + dispatch::handle_request::(&mut req, |params, resp| { + let world = world.snapshot(); + let sender = sender.clone(); + pool.execute(move || { + let res = handle_syntax_tree(world, params); + sender.send(Box::new(|io: &mut Io| resp.response(io, res))) + }); + Ok(()) + })?; + dispatch::handle_request::(&mut req, |params, resp| { + let world = world.snapshot(); + let sender = sender.clone(); + pool.execute(move || { + let res = handle_extend_selection(world, params); + sender.send(Box::new(|io: &mut Io| resp.response(io, res))) + }); + Ok(()) + })?; + dispatch::handle_request::(&mut req, |(), resp| { + resp.result(io, ())?; + Ok(()) + })?; + if let Some(req) = req { + error!("unknown method: {:?}", req); + dispatch::unknown_method(io, req)?; + } + } + RawMsg::Notification(not) => { + let mut not = Some(not); + dispatch::handle_notification::(&mut not, |params| { + let path = params.text_document.file_path()?; + world.change_overlay(path, Some(params.text_document.text)); + Ok(()) + })?; + dispatch::handle_notification::(&mut not, |mut params| { + let path = params.text_document.file_path()?; + let text = params.content_changes.pop() + .ok_or_else(|| format_err!("empty changes"))? + .text; + world.change_overlay(path, Some(text)); + Ok(()) + })?; + dispatch::handle_notification::(&mut not, |params| { + let path = params.text_document.file_path()?; + world.change_overlay(path, None); + Ok(()) + })?; + + if let Some(not) = not { + error!("unhandled notification: {:?}", not) + } + } + msg => { + eprintln!("msg = {:?}", msg); + } + } + } +} + +trait FnBox: Send { + fn call_box(self: Box, a: A) -> R; +} + +impl R + Send> FnBox for F { + fn call_box(self: Box, a: A) -> R { + (*self)(a) + } +} + +trait FilePath { + fn file_path(&self) -> Result; +} + +impl FilePath for TextDocumentItem { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} + +impl FilePath for VersionedTextDocumentIdentifier { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} + +impl FilePath for TextDocumentIdentifier { + fn file_path(&self) -> Result { + self.uri.to_file_path() + .map_err(|()| format_err!("invalid uri: {}", self.uri)) + } +} diff --git a/crates/server/src/req.rs b/crates/server/src/req.rs new file mode 100644 index 000000000..4e588159b --- /dev/null +++ b/crates/server/src/req.rs @@ -0,0 +1,41 @@ +use languageserver_types::{TextDocumentIdentifier, Range}; + +pub use languageserver_types::{ + request::*, notification::*, + InitializeResult, +}; + +pub enum SyntaxTree {} + +impl Request for SyntaxTree { + type Params = SyntaxTreeParams; + type Result = String; + const METHOD: &'static str = "m/syntaxTree"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct SyntaxTreeParams { + pub text_document: TextDocumentIdentifier +} + +pub enum ExtendSelection {} + +impl Request for ExtendSelection { + type Params = ExtendSelectionParams; + type Result = ExtendSelectionResult; + const METHOD: &'static str = "m/extendSelection"; +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExtendSelectionParams { + pub text_document: TextDocumentIdentifier, + pub selections: Vec, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct ExtendSelectionResult { + pub selections: Vec, +} -- cgit v1.2.3