on master help sign in
Improve error typing.
commit81ccca0c6a18de86223b8142b5742e0603b9b230 authorBlaž Hrastnik <blaz@mxxn.io> dateWed 21 Oct 2020, 16:42 (6 years ago) parentef5e5f9
5 files changed+308−242
Cargo.lock+31−10 helix-lsp/Cargo.toml+1 helix-lsp/src/client.rs+242 helix-lsp/src/lib.rs+17−224 helix-lsp/src/transport.rs+17−8
Cargo.lock+31−10
@@ -100,9 +100,9 @@ dependencies = [
100100
101101[[package]]
102102name = "async-net"
103−version = "1.4.7"
103+version = "1.5.0"
104104source = "registry+https://github.com/rust-lang/crates.io-index"
105−checksum = "ee4c3668eb091d781e97f0026b5289b457c77d407a85749a9bb4c057456c428f"
105+checksum = "06de475c85affe184648202401d7622afb32f0f74e02192857d0201a16defbe5"
106106dependencies = [
107107 "async-io",
108108 "blocking",
@@ -350,9 +350,9 @@ checksum = "5fc94b64bb39543b4e432f1790b6bf18e3ee3b74653c5449f63310e9a74b123c"
350350
351351[[package]]
352352name = "futures-lite"
353−version = "1.11.1"
353+version = "1.11.2"
354354source = "registry+https://github.com/rust-lang/crates.io-index"
355−checksum = "381a7ad57b1bad34693f63f6f377e1abded7a9c85c9d3eb6771e11c60aaadab9"
355+checksum = "5e6c079abfac3ab269e2927ec048dabc89d009ebfdda6b8ee86624f30c689658"
356356dependencies = [
357357 "fastrand",
358358 "futures-core",
@@ -454,6 +454,7 @@ dependencies = [
454454 "serde_json",
455455 "shellexpand",
456456 "smol",
457+ "thiserror",
457458 "url",
458459]
459460
@@ -623,9 +624,9 @@ checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400"
623624
624625[[package]]
625626name = "mio"
626−version = "0.7.3"
627+version = "0.7.4"
627628source = "registry+https://github.com/rust-lang/crates.io-index"
628−checksum = "e53a6ea5f38c0a48ca42159868c6d8e1bd56c0451238856cc08d58563643bdc3"
629+checksum = "f8f1c83949125de4a582aa2da15ae6324d91cf6a58a70ea407643941ff98f558"
629630dependencies = [
630631 "libc",
631632 "log",
@@ -757,9 +758,9 @@ dependencies = [
757758
758759[[package]]
759760name = "pin-project-lite"
760−version = "0.1.10"
761+version = "0.1.11"
761762source = "registry+https://github.com/rust-lang/crates.io-index"
762−checksum = "e555d9e657502182ac97b539fb3dae8b79cda19e3e4f8ffb5e8de4f18df93c95"
763+checksum = "c917123afa01924fc84bb20c4c03f004d9c38e5127e3c039bbf7f4b9c76a2f6b"
763764
764765[[package]]
765766name = "pin-utils"
@@ -1022,6 +1023,26 @@ dependencies = [
10221023 "unicode-width",
10231024]
10241025
1026+[[package]]
1027+name = "thiserror"
1028+version = "1.0.21"
1029+source = "registry+https://github.com/rust-lang/crates.io-index"
1030+checksum = "318234ffa22e0920fe9a40d7b8369b5f649d490980cf7aadcf1eb91594869b42"
1031+dependencies = [
1032+ "thiserror-impl",
1033+]
1034+
1035+[[package]]
1036+name = "thiserror-impl"
1037+version = "1.0.21"
1038+source = "registry+https://github.com/rust-lang/crates.io-index"
1039+checksum = "cae2447b6282786c3493999f40a9be2a6ad20cb8bd268b0a0dbf5a065535c0ab"
1040+dependencies = [
1041+ "proc-macro2",
1042+ "quote",
1043+ "syn",
1044+]
1045+
10251046[[package]]
10261047name = "thread_local"
10271048version = "1.0.1"
@@ -1139,9 +1160,9 @@ checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
11391160
11401161[[package]]
11411162name = "wepoll-sys"
1142−version = "3.0.0"
1163+version = "3.0.1"
11431164source = "registry+https://github.com/rust-lang/crates.io-index"
1144−checksum = "142bc2cba3fe88be1a8fcb55c727fa4cd5b0cf2d7438722792e22f26f04bc1e0"
1165+checksum = "0fcb14dea929042224824779fbc82d9fab8d2e6d3cbc0ac404de8edf489e77ff"
11451166dependencies = [
11461167 "cc",
11471168]
helix-lsp/Cargo.toml+1
@@ -20,3 +20,4 @@ serde_json = "1.0"
2020serde = { version = "1.0", features = ["derive"] }
2121jsonrpc-core = "15.1"
2222futures-util = "0.3"
23+thiserror = "1.0.21"
helix-lsp/src/client.rsnew file+242
@@ -0,0 +1,242 @@
1+use crate::{
2+ transport::{Payload, Transport},
3+ Error, Notification,
4+};
5+
6+type Result<T> = core::result::Result<T, Error>;
7+
8+use helix_core::{State, Transaction};
9+
10+// use std::collections::HashMap;
11+
12+use jsonrpc_core as jsonrpc;
13+use lsp_types as lsp;
14+use serde_json::Value;
15+
16+use smol::{
17+ channel::{Receiver, Sender},
18+ io::{BufReader, BufWriter},
19+ // prelude::*,
20+ process::{Child, ChildStderr, Command, Stdio},
21+ Executor,
22+};
23+
24+pub struct Client {
25+ _process: Child,
26+ stderr: BufReader<ChildStderr>,
27+
28+ outgoing: Sender<Payload>,
29+ pub incoming: Receiver<Notification>,
30+
31+ pub request_counter: u64,
32+
33+ capabilities: Option<lsp::ServerCapabilities>,
34+ // TODO: handle PublishDiagnostics Version
35+ // diagnostics: HashMap<lsp::Url, Vec<lsp::Diagnostic>>,
36+}
37+
38+impl Client {
39+ pub fn start(ex: &Executor, cmd: &str, args: &[String]) -> Self {
40+ let mut process = Command::new(cmd)
41+ .args(args)
42+ .stdin(Stdio::piped())
43+ .stdout(Stdio::piped())
44+ .stderr(Stdio::piped())
45+ .spawn()
46+ .expect("Failed to start language server");
47+ // smol makes sure the process is reaped on drop, but using kill_on_drop(true) maybe?
48+
49+ // TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
50+ let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
51+ let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
52+ let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
53+
54+ let (incoming, outgoing) = Transport::start(ex, reader, writer);
55+
56+ Client {
57+ _process: process,
58+ stderr,
59+
60+ outgoing,
61+ incoming,
62+
63+ request_counter: 0,
64+
65+ capabilities: None,
66+ // diagnostics: HashMap::new(),
67+ }
68+ }
69+
70+ fn next_request_id(&mut self) -> jsonrpc::Id {
71+ let id = jsonrpc::Id::Num(self.request_counter);
72+ self.request_counter += 1;
73+ id
74+ }
75+
76+ fn to_params(value: Value) -> Result<jsonrpc::Params> {
77+ use jsonrpc::Params;
78+
79+ let params = match value {
80+ Value::Null => Params::None,
81+ Value::Bool(_) | Value::Number(_) | Value::String(_) => Params::Array(vec![value]),
82+ Value::Array(vec) => Params::Array(vec),
83+ Value::Object(map) => Params::Map(map),
84+ };
85+
86+ Ok(params)
87+ }
88+
89+ pub async fn request<R: lsp::request::Request>(
90+ &mut self,
91+ params: R::Params,
92+ ) -> Result<R::Result>
93+ where
94+ R::Params: serde::Serialize,
95+ R::Result: core::fmt::Debug, // TODO: temporary
96+ {
97+ let params = serde_json::to_value(params)?;
98+
99+ let request = jsonrpc::MethodCall {
100+ jsonrpc: Some(jsonrpc::Version::V2),
101+ id: self.next_request_id(),
102+ method: R::METHOD.to_string(),
103+ params: Self::to_params(params)?,
104+ };
105+
106+ let (tx, rx) = smol::channel::bounded::<Result<Value>>(1);
107+
108+ self.outgoing
109+ .send(Payload::Request {
110+ chan: tx,
111+ value: request,
112+ })
113+ .await
114+ .map_err(|e| Error::Other(e.into()))?;
115+
116+ let response = rx.recv().await.map_err(|e| Error::Other(e.into()))??;
117+
118+ let response = serde_json::from_value(response)?;
119+
120+ // TODO: we should pass request to a sender thread via a channel
121+ // so it can't be interleaved
122+
123+ // TODO: responses can be out of order, we need to register a single shot response channel
124+
125+ Ok(response)
126+ }
127+
128+ pub async fn notify<R: lsp::notification::Notification>(
129+ &mut self,
130+ params: R::Params,
131+ ) -> Result<()>
132+ where
133+ R::Params: serde::Serialize,
134+ {
135+ let params = serde_json::to_value(params)?;
136+
137+ let notification = jsonrpc::Notification {
138+ jsonrpc: Some(jsonrpc::Version::V2),
139+ method: R::METHOD.to_string(),
140+ params: Self::to_params(params)?,
141+ };
142+
143+ self.outgoing
144+ .send(Payload::Notification(notification))
145+ .await
146+ .map_err(|e| Error::Other(e.into()))?;
147+
148+ Ok(())
149+ }
150+
151+ // -------------------------------------------------------------------------------------------
152+ // General messages
153+ // -------------------------------------------------------------------------------------------
154+
155+ pub async fn initialize(&mut self) -> Result<()> {
156+ // TODO: delay any requests that are triggered prior to initialize
157+
158+ #[allow(deprecated)]
159+ let params = lsp::InitializeParams {
160+ process_id: Some(u64::from(std::process::id())),
161+ root_path: None,
162+ // root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
163+ root_uri: None, // set to project root in the future
164+ initialization_options: None,
165+ capabilities: lsp::ClientCapabilities::default(),
166+ trace: None,
167+ workspace_folders: None,
168+ client_info: None,
169+ };
170+
171+ let response = self.request::<lsp::request::Initialize>(params).await?;
172+ self.capabilities = Some(response.capabilities);
173+
174+ // next up, notify<initialized>
175+ self.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
176+ .await?;
177+
178+ Ok(())
179+ }
180+
181+ pub async fn shutdown(&mut self) -> Result<()> {
182+ self.request::<lsp::request::Shutdown>(()).await
183+ }
184+
185+ pub async fn exit(&mut self) -> Result<()> {
186+ self.notify::<lsp::notification::Exit>(()).await
187+ }
188+
189+ // -------------------------------------------------------------------------------------------
190+ // Text document
191+ // -------------------------------------------------------------------------------------------
192+
193+ pub async fn text_document_did_open(&mut self, state: &State) -> Result<()> {
194+ self.notify::<lsp::notification::DidOpenTextDocument>(lsp::DidOpenTextDocumentParams {
195+ text_document: lsp::TextDocumentItem {
196+ uri: lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
197+ language_id: "rust".to_string(), // TODO: hardcoded for now
198+ version: state.version,
199+ text: String::from(&state.doc),
200+ },
201+ })
202+ .await
203+ }
204+
205+ // TODO: trigger any time history.commit_revision happens
206+ pub async fn text_document_did_change(
207+ &mut self,
208+ state: &State,
209+ transaction: &Transaction,
210+ ) -> Result<()> {
211+ self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
212+ text_document: lsp::VersionedTextDocumentIdentifier::new(
213+ lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
214+ state.version,
215+ ),
216+ content_changes: vec![lsp::TextDocumentContentChangeEvent {
217+ // range = None -> whole document
218+ range: None, //Some(Range)
219+ range_length: None, // u64 apparently deprecated
220+ text: "".to_string(),
221+ }], // TODO: probably need old_state here too?
222+ })
223+ .await
224+ }
225+
226+ // TODO: impl into() TextDocumentIdentifier / VersionedTextDocumentIdentifier for State.
227+
228+ pub async fn text_document_did_close(&mut self, state: &State) -> Result<()> {
229+ self.notify::<lsp::notification::DidCloseTextDocument>(lsp::DidCloseTextDocumentParams {
230+ text_document: lsp::TextDocumentIdentifier::new(
231+ lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
232+ ),
233+ })
234+ .await
235+ }
236+
237+ // will_save / will_save_wait_until
238+
239+ pub async fn text_document_did_save(&mut self) -> anyhow::Result<()> {
240+ unimplemented!()
241+ }
242+}
helix-lsp/src/lib.rs+17−224
@@ -1,27 +1,26 @@
1+mod client;
12mod transport;
23
3−use transport::{Payload, Transport};
4−
5−use helix_core::{State, Transaction};
6−
7−// use std::collections::HashMap;
8−
94use jsonrpc_core as jsonrpc;
105use lsp_types as lsp;
11−use serde_json::Value;
12−
13−use serde::{Deserialize, Serialize};
146
15−pub use lsp::Position;
16−pub use lsp::Url;
7+pub use client::Client;
8+pub use lsp::{Position, Url};
179
18−use smol::{
19− channel::{Receiver, Sender},
20− io::{BufReader, BufWriter},
21− // prelude::*,
22− process::{Child, ChildStderr, Command, Stdio},
23− Executor,
24−};
10+use serde::{Deserialize, Serialize};
11+use thiserror::Error;
12+
13+#[derive(Error, Debug)]
14+pub enum Error {
15+ #[error("protocol error: {0}")]
16+ Rpc(#[from] jsonrpc::Error),
17+ #[error("failed to parse: {0}")]
18+ Parse(#[from] serde_json::Error),
19+ #[error("request timed out")]
20+ Timeout,
21+ #[error(transparent)]
22+ Other(#[from] anyhow::Error),
23+}
2524
2625pub mod util {
2726 use super::*;
@@ -68,209 +67,3 @@ impl Notification {
6867 }
6968 }
7069}
71−
72−pub struct Client {
73− _process: Child,
74− stderr: BufReader<ChildStderr>,
75−
76− outgoing: Sender<Payload>,
77− pub incoming: Receiver<Notification>,
78−
79− pub request_counter: u64,
80−
81− capabilities: Option<lsp::ServerCapabilities>,
82− // TODO: handle PublishDiagnostics Version
83− // diagnostics: HashMap<lsp::Url, Vec<lsp::Diagnostic>>,
84−}
85−
86−impl Client {
87− pub fn start(ex: &Executor, cmd: &str, args: &[String]) -> Self {
88− let mut process = Command::new(cmd)
89− .args(args)
90− .stdin(Stdio::piped())
91− .stdout(Stdio::piped())
92− .stderr(Stdio::piped())
93− .spawn()
94− .expect("Failed to start language server");
95− // smol makes sure the process is reaped on drop, but using kill_on_drop(true) maybe?
96−
97− // TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
98− let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
99− let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
100− let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
101−
102− let (incoming, outgoing) = Transport::start(ex, reader, writer);
103−
104− Client {
105− _process: process,
106− stderr,
107−
108− outgoing,
109− incoming,
110−
111− request_counter: 0,
112−
113− capabilities: None,
114− // diagnostics: HashMap::new(),
115− }
116− }
117−
118− fn next_request_id(&mut self) -> jsonrpc::Id {
119− let id = jsonrpc::Id::Num(self.request_counter);
120− self.request_counter += 1;
121− id
122− }
123−
124− fn to_params(value: Value) -> anyhow::Result<jsonrpc::Params> {
125− use jsonrpc::Params;
126−
127− let params = match value {
128− Value::Null => Params::None,
129− Value::Bool(_) | Value::Number(_) | Value::String(_) => Params::Array(vec![value]),
130− Value::Array(vec) => Params::Array(vec),
131− Value::Object(map) => Params::Map(map),
132− };
133−
134− Ok(params)
135− }
136−
137− pub async fn request<R: lsp::request::Request>(
138− &mut self,
139− params: R::Params,
140− ) -> anyhow::Result<R::Result>
141− where
142− R::Params: serde::Serialize,
143− R::Result: core::fmt::Debug, // TODO: temporary
144− {
145− let params = serde_json::to_value(params)?;
146−
147− let request = jsonrpc::MethodCall {
148− jsonrpc: Some(jsonrpc::Version::V2),
149− id: self.next_request_id(),
150− method: R::METHOD.to_string(),
151− params: Self::to_params(params)?,
152− };
153−
154− let (tx, rx) = smol::channel::bounded::<anyhow::Result<Value>>(1);
155−
156− self.outgoing
157− .send(Payload::Request {
158− chan: tx,
159− value: request,
160− })
161− .await?;
162−
163− let response = rx.recv().await??;
164−
165− let response = serde_json::from_value(response)?;
166−
167− // TODO: we should pass request to a sender thread via a channel
168− // so it can't be interleaved
169−
170− // TODO: responses can be out of order, we need to register a single shot response channel
171−
172− Ok(response)
173− }
174−
175− pub async fn notify<R: lsp::notification::Notification>(
176− &mut self,
177− params: R::Params,
178− ) -> anyhow::Result<()>
179− where
180− R::Params: serde::Serialize,
181− {
182− let params = serde_json::to_value(params)?;
183−
184− let notification = jsonrpc::Notification {
185− jsonrpc: Some(jsonrpc::Version::V2),
186− method: R::METHOD.to_string(),
187− params: Self::to_params(params)?,
188− };
189−
190− self.outgoing
191− .send(Payload::Notification(notification))
192− .await?;
193−
194− Ok(())
195− }
196−
197− // -------------------------------------------------------------------------------------------
198− // General messages
199− // -------------------------------------------------------------------------------------------
200−
201− pub async fn initialize(&mut self) -> anyhow::Result<()> {
202− // TODO: delay any requests that are triggered prior to initialize
203−
204− #[allow(deprecated)]
205− let params = lsp::InitializeParams {
206− process_id: Some(u64::from(std::process::id())),
207− root_path: None,
208− // root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
209− root_uri: None, // set to project root in the future
210− initialization_options: None,
211− capabilities: lsp::ClientCapabilities::default(),
212− trace: None,
213− workspace_folders: None,
214− client_info: None,
215− };
216−
217− let response = self.request::<lsp::request::Initialize>(params).await?;
218− self.capabilities = Some(response.capabilities);
219−
220− // next up, notify<initialized>
221− self.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
222− .await?;
223−
224− Ok(())
225− }
226−
227− pub async fn shutdown(&mut self) -> anyhow::Result<()> {
228− self.request::<lsp::request::Shutdown>(()).await
229− }
230−
231− pub async fn exit(&mut self) -> anyhow::Result<()> {
232− self.notify::<lsp::notification::Exit>(()).await
233− }
234−
235− // -------------------------------------------------------------------------------------------
236− // Text document
237− // -------------------------------------------------------------------------------------------
238−
239− pub async fn text_document_did_open(&mut self, state: &State) -> anyhow::Result<()> {
240− self.notify::<lsp::notification::DidOpenTextDocument>(lsp::DidOpenTextDocumentParams {
241− text_document: lsp::TextDocumentItem {
242− uri: lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
243− language_id: "rust".to_string(), // TODO: hardcoded for now
244− version: state.version,
245− text: String::from(&state.doc),
246− },
247− })
248− .await
249− }
250−
251− // TODO: trigger any time history.commit_revision happens
252− pub async fn text_document_did_change(
253− &mut self,
254− state: &State,
255− transaction: &Transaction,
256− ) -> anyhow::Result<()> {
257− self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
258− text_document: lsp::VersionedTextDocumentIdentifier::new(
259− lsp::Url::from_file_path(state.path().unwrap()).unwrap(),
260− state.version,
261− ),
262− content_changes: vec![], // TODO: probably need old_state here too?
263− })
264− .await
265− }
266−
267− pub async fn text_document_did_close(&mut self) -> anyhow::Result<()> {
268− unimplemented!()
269− }
270−
271− // will_save / will_save_wait_until
272−
273− pub async fn text_document_did_save(&mut self) -> anyhow::Result<()> {
274− unimplemented!()
275− }
276−}
helix-lsp/src/transport.rs+17−8
@@ -1,9 +1,10 @@
11use std::collections::HashMap;
22
3−use crate::{Message, Notification};
3+use crate::{Error, Message, Notification};
4+
5+type Result<T> = core::result::Result<T, Error>;
46
57use jsonrpc_core as jsonrpc;
6−use lsp_types as lsp;
78use serde_json::Value;
89
910use smol::prelude::*;
@@ -17,7 +18,7 @@ use smol::{
1718
1819pub(crate) enum Payload {
1920 Request {
20− chan: Sender<anyhow::Result<Value>>,
21+ chan: Sender<Result<Value>>,
2122 value: jsonrpc::MethodCall,
2223 },
2324 Notification(jsonrpc::Notification),
@@ -27,7 +28,7 @@ pub(crate) struct Transport {
2728 incoming: Sender<Notification>, // TODO Notification | Call
2829 outgoing: Receiver<Payload>,
2930
30− pending_requests: HashMap<jsonrpc::Id, Sender<anyhow::Result<Value>>>,
31+ pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
3132 headers: HashMap<String, String>,
3233
3334 writer: BufWriter<ChildStdin>,
@@ -60,7 +61,7 @@ impl Transport {
6061 async fn recv(
6162 reader: &mut (impl AsyncBufRead + Unpin),
6263 headers: &mut HashMap<String, String>,
63− ) -> Result<Message, std::io::Error> {
64+ ) -> core::result::Result<Message, std::io::Error> {
6465 // read headers
6566 loop {
6667 let mut header = String::new();
@@ -74,8 +75,10 @@ impl Transport {
7475
7576 let parts: Vec<&str> = header.split(": ").collect();
7677 if parts.len() != 2 {
77− // return Err(Error::new(ErrorKind::Other, "Failed to parse header"));
78− panic!()
78+ return Err(std::io::Error::new(
79+ std::io::ErrorKind::Other,
80+ "Failed to parse header",
81+ ));
7982 }
8083 headers.insert(parts[0].to_string(), parts[1].to_string());
8184 }
@@ -155,7 +158,13 @@ impl Transport {
155158 .expect("pending_request with id not found!");
156159 tx.send(Ok(result)).await?;
157160 }
158− jsonrpc::Output::Failure(_) => panic!("recv fail"),
161+ jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
162+ let tx = self
163+ .pending_requests
164+ .remove(&id)
165+ .expect("pending_request with id not found!");
166+ tx.send(Err(error.into())).await?;
167+ }
159168 msg => unimplemented!("{:?}", msg),
160169 }
161170 Ok(())