diff options
author | Aleksey Kladov <[email protected]> | 2018-09-16 10:54:24 +0100 |
---|---|---|
committer | Aleksey Kladov <[email protected]> | 2018-09-16 11:07:39 +0100 |
commit | b5021411a84822cb3f1e3aeffad9550dd15bdeb6 (patch) | |
tree | 9dca564f8e51b298dced01c4ce669c756dce3142 /crates/ra_lsp_server | |
parent | ba0bfeee12e19da40b5eabc8d0408639af10e96f (diff) |
rename all things
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r-- | crates/ra_lsp_server/Cargo.toml | 31 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/caps.rs | 49 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/conv.rs | 296 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/lib.rs | 37 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main.rs | 52 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/handlers.rs | 436 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/mod.rs | 419 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/main_loop/subscriptions.rs | 21 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/path_map.rs | 110 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/project_model.rs | 175 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/req.rs | 176 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/server_world.rs | 167 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/thread_watcher.rs | 70 | ||||
-rw-r--r-- | crates/ra_lsp_server/src/vfs.rs | 71 | ||||
-rw-r--r-- | crates/ra_lsp_server/tests/heavy_tests/main.rs | 99 | ||||
-rw-r--r-- | crates/ra_lsp_server/tests/heavy_tests/support.rs | 217 |
16 files changed, 2426 insertions, 0 deletions
diff --git a/crates/ra_lsp_server/Cargo.toml b/crates/ra_lsp_server/Cargo.toml new file mode 100644 index 000000000..b851f70e1 --- /dev/null +++ b/crates/ra_lsp_server/Cargo.toml | |||
@@ -0,0 +1,31 @@ | |||
1 | [package] | ||
2 | name = "ra_lsp_server" | ||
3 | version = "0.1.0" | ||
4 | authors = ["Aleksey Kladov <[email protected]>"] | ||
5 | |||
6 | [dependencies] | ||
7 | rayon = "1.0.2" | ||
8 | relative-path = "0.3.7" | ||
9 | failure = "0.1.2" | ||
10 | serde_json = "1.0.24" | ||
11 | serde = "1.0.71" | ||
12 | serde_derive = "1.0.71" | ||
13 | drop_bomb = "0.1.0" | ||
14 | crossbeam-channel = "0.2.4" | ||
15 | flexi_logger = "0.9.1" | ||
16 | log = "0.4.3" | ||
17 | url_serde = "0.2.0" | ||
18 | languageserver-types = "0.50.0" | ||
19 | walkdir = "2.2.0" | ||
20 | im = "12.0.0" | ||
21 | cargo_metadata = "0.6.0" | ||
22 | text_unit = { version = "0.1.2", features = ["serde"] } | ||
23 | smol_str = { version = "0.1.5", features = ["serde"] } | ||
24 | |||
25 | ra_syntax = { path = "../ra_syntax" } | ||
26 | ra_editor = { path = "../ra_editor" } | ||
27 | ra_analysis = { path = "../ra_analysis" } | ||
28 | gen_lsp_server = { path = "../gen_lsp_server" } | ||
29 | |||
30 | [dev-dependencies] | ||
31 | tempdir = "0.3.7" | ||
diff --git a/crates/ra_lsp_server/src/caps.rs b/crates/ra_lsp_server/src/caps.rs new file mode 100644 index 000000000..7456aea8a --- /dev/null +++ b/crates/ra_lsp_server/src/caps.rs | |||
@@ -0,0 +1,49 @@ | |||
1 | use languageserver_types::{ | ||
2 | ServerCapabilities, | ||
3 | TextDocumentSyncCapability, | ||
4 | TextDocumentSyncOptions, | ||
5 | TextDocumentSyncKind, | ||
6 | ExecuteCommandOptions, | ||
7 | CompletionOptions, | ||
8 | DocumentOnTypeFormattingOptions, | ||
9 | }; | ||
10 | |||
11 | pub fn server_capabilities() -> ServerCapabilities { | ||
12 | ServerCapabilities { | ||
13 | text_document_sync: Some(TextDocumentSyncCapability::Options( | ||
14 | TextDocumentSyncOptions { | ||
15 | open_close: Some(true), | ||
16 | change: Some(TextDocumentSyncKind::Full), | ||
17 | will_save: None, | ||
18 | will_save_wait_until: None, | ||
19 | save: None, | ||
20 | } | ||
21 | )), | ||
22 | hover_provider: None, | ||
23 | completion_provider: Some(CompletionOptions { | ||
24 | resolve_provider: None, | ||
25 | trigger_characters: None, | ||
26 | }), | ||
27 | signature_help_provider: None, | ||
28 | definition_provider: Some(true), | ||
29 | type_definition_provider: None, | ||
30 | implementation_provider: None, | ||
31 | references_provider: None, | ||
32 | document_highlight_provider: None, | ||
33 | document_symbol_provider: Some(true), | ||
34 | workspace_symbol_provider: Some(true), | ||
35 | code_action_provider: Some(true), | ||
36 | code_lens_provider: None, | ||
37 | document_formatting_provider: None, | ||
38 | document_range_formatting_provider: None, | ||
39 | document_on_type_formatting_provider: Some(DocumentOnTypeFormattingOptions { | ||
40 | first_trigger_character: "=".to_string(), | ||
41 | more_trigger_character: None, | ||
42 | }), | ||
43 | rename_provider: None, | ||
44 | color_provider: None, | ||
45 | execute_command_provider: Some(ExecuteCommandOptions { | ||
46 | commands: vec!["apply_code_action".to_string()], | ||
47 | }), | ||
48 | } | ||
49 | } | ||
diff --git a/crates/ra_lsp_server/src/conv.rs b/crates/ra_lsp_server/src/conv.rs new file mode 100644 index 000000000..759e5e914 --- /dev/null +++ b/crates/ra_lsp_server/src/conv.rs | |||
@@ -0,0 +1,296 @@ | |||
1 | use languageserver_types::{ | ||
2 | Range, SymbolKind, Position, TextEdit, Location, Url, | ||
3 | TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentItem, | ||
4 | TextDocumentPositionParams, TextDocumentEdit, | ||
5 | }; | ||
6 | use ra_editor::{LineIndex, LineCol, Edit, AtomEdit}; | ||
7 | use ra_syntax::{SyntaxKind, TextUnit, TextRange}; | ||
8 | use ra_analysis::{FileId, SourceChange, SourceFileEdit, FileSystemEdit}; | ||
9 | |||
10 | use { | ||
11 | Result, | ||
12 | server_world::ServerWorld, | ||
13 | req, | ||
14 | }; | ||
15 | |||
16 | pub trait Conv { | ||
17 | type Output; | ||
18 | fn conv(self) -> Self::Output; | ||
19 | } | ||
20 | |||
21 | pub trait ConvWith { | ||
22 | type Ctx; | ||
23 | type Output; | ||
24 | fn conv_with(self, ctx: &Self::Ctx) -> Self::Output; | ||
25 | } | ||
26 | |||
27 | pub trait TryConvWith { | ||
28 | type Ctx; | ||
29 | type Output; | ||
30 | fn try_conv_with(self, ctx: &Self::Ctx) -> Result<Self::Output>; | ||
31 | } | ||
32 | |||
33 | impl Conv for SyntaxKind { | ||
34 | type Output = SymbolKind; | ||
35 | |||
36 | fn conv(self) -> <Self as Conv>::Output { | ||
37 | match self { | ||
38 | SyntaxKind::FN_DEF => SymbolKind::Function, | ||
39 | SyntaxKind::STRUCT_DEF => SymbolKind::Struct, | ||
40 | SyntaxKind::ENUM_DEF => SymbolKind::Enum, | ||
41 | SyntaxKind::TRAIT_DEF => SymbolKind::Interface, | ||
42 | SyntaxKind::MODULE => SymbolKind::Module, | ||
43 | SyntaxKind::TYPE_DEF => SymbolKind::TypeParameter, | ||
44 | SyntaxKind::STATIC_DEF => SymbolKind::Constant, | ||
45 | SyntaxKind::CONST_DEF => SymbolKind::Constant, | ||
46 | SyntaxKind::IMPL_ITEM => SymbolKind::Object, | ||
47 | _ => SymbolKind::Variable, | ||
48 | } | ||
49 | } | ||
50 | } | ||
51 | |||
52 | impl ConvWith for Position { | ||
53 | type Ctx = LineIndex; | ||
54 | type Output = TextUnit; | ||
55 | |||
56 | fn conv_with(self, line_index: &LineIndex) -> TextUnit { | ||
57 | // TODO: UTF-16 | ||
58 | let line_col = LineCol { | ||
59 | line: self.line as u32, | ||
60 | col: (self.character as u32).into(), | ||
61 | }; | ||
62 | line_index.offset(line_col) | ||
63 | } | ||
64 | } | ||
65 | |||
66 | impl ConvWith for TextUnit { | ||
67 | type Ctx = LineIndex; | ||
68 | type Output = Position; | ||
69 | |||
70 | fn conv_with(self, line_index: &LineIndex) -> Position { | ||
71 | let line_col = line_index.line_col(self); | ||
72 | // TODO: UTF-16 | ||
73 | Position::new(line_col.line as u64, u32::from(line_col.col) as u64) | ||
74 | } | ||
75 | } | ||
76 | |||
77 | impl ConvWith for TextRange { | ||
78 | type Ctx = LineIndex; | ||
79 | type Output = Range; | ||
80 | |||
81 | fn conv_with(self, line_index: &LineIndex) -> Range { | ||
82 | Range::new( | ||
83 | self.start().conv_with(line_index), | ||
84 | self.end().conv_with(line_index), | ||
85 | ) | ||
86 | } | ||
87 | } | ||
88 | |||
89 | impl ConvWith for Range { | ||
90 | type Ctx = LineIndex; | ||
91 | type Output = TextRange; | ||
92 | |||
93 | fn conv_with(self, line_index: &LineIndex) -> TextRange { | ||
94 | TextRange::from_to( | ||
95 | self.start.conv_with(line_index), | ||
96 | self.end.conv_with(line_index), | ||
97 | ) | ||
98 | } | ||
99 | } | ||
100 | |||
101 | impl ConvWith for Edit { | ||
102 | type Ctx = LineIndex; | ||
103 | type Output = Vec<TextEdit>; | ||
104 | |||
105 | fn conv_with(self, line_index: &LineIndex) -> Vec<TextEdit> { | ||
106 | self.into_atoms() | ||
107 | .into_iter() | ||
108 | .map_conv_with(line_index) | ||
109 | .collect() | ||
110 | } | ||
111 | } | ||
112 | |||
113 | impl ConvWith for AtomEdit { | ||
114 | type Ctx = LineIndex; | ||
115 | type Output = TextEdit; | ||
116 | |||
117 | fn conv_with(self, line_index: &LineIndex) -> TextEdit { | ||
118 | TextEdit { | ||
119 | range: self.delete.conv_with(line_index), | ||
120 | new_text: self.insert, | ||
121 | } | ||
122 | } | ||
123 | } | ||
124 | |||
125 | impl<T: ConvWith> ConvWith for Option<T> { | ||
126 | type Ctx = <T as ConvWith>::Ctx; | ||
127 | type Output = Option<<T as ConvWith>::Output>; | ||
128 | fn conv_with(self, ctx: &Self::Ctx) -> Self::Output { | ||
129 | self.map(|x| ConvWith::conv_with(x, ctx)) | ||
130 | } | ||
131 | } | ||
132 | |||
133 | impl<'a> TryConvWith for &'a Url { | ||
134 | type Ctx = ServerWorld; | ||
135 | type Output = FileId; | ||
136 | fn try_conv_with(self, world: &ServerWorld) -> Result<FileId> { | ||
137 | world.uri_to_file_id(self) | ||
138 | } | ||
139 | } | ||
140 | |||
141 | impl TryConvWith for FileId { | ||
142 | type Ctx = ServerWorld; | ||
143 | type Output = Url; | ||
144 | fn try_conv_with(self, world: &ServerWorld) -> Result<Url> { | ||
145 | world.file_id_to_uri(self) | ||
146 | } | ||
147 | } | ||
148 | |||
149 | impl<'a> TryConvWith for &'a TextDocumentItem { | ||
150 | type Ctx = ServerWorld; | ||
151 | type Output = FileId; | ||
152 | fn try_conv_with(self, world: &ServerWorld) -> Result<FileId> { | ||
153 | self.uri.try_conv_with(world) | ||
154 | } | ||
155 | } | ||
156 | |||
157 | impl<'a> TryConvWith for &'a VersionedTextDocumentIdentifier { | ||
158 | type Ctx = ServerWorld; | ||
159 | type Output = FileId; | ||
160 | fn try_conv_with(self, world: &ServerWorld) -> Result<FileId> { | ||
161 | self.uri.try_conv_with(world) | ||
162 | } | ||
163 | } | ||
164 | |||
165 | impl<'a> TryConvWith for &'a TextDocumentIdentifier { | ||
166 | type Ctx = ServerWorld; | ||
167 | type Output = FileId; | ||
168 | fn try_conv_with(self, world: &ServerWorld) -> Result<FileId> { | ||
169 | world.uri_to_file_id(&self.uri) | ||
170 | } | ||
171 | } | ||
172 | |||
173 | impl<T: TryConvWith> TryConvWith for Vec<T> { | ||
174 | type Ctx = <T as TryConvWith>::Ctx; | ||
175 | type Output = Vec<<T as TryConvWith>::Output>; | ||
176 | fn try_conv_with(self, ctx: &Self::Ctx) -> Result<Self::Output> { | ||
177 | let mut res = Vec::with_capacity(self.len()); | ||
178 | for item in self { | ||
179 | res.push(item.try_conv_with(ctx)?); | ||
180 | } | ||
181 | Ok(res) | ||
182 | } | ||
183 | } | ||
184 | |||
185 | impl TryConvWith for SourceChange { | ||
186 | type Ctx = ServerWorld; | ||
187 | type Output = req::SourceChange; | ||
188 | fn try_conv_with(self, world: &ServerWorld) -> Result<req::SourceChange> { | ||
189 | let cursor_position = match self.cursor_position { | ||
190 | None => None, | ||
191 | Some(pos) => { | ||
192 | let line_index = world.analysis().file_line_index(pos.file_id); | ||
193 | Some(TextDocumentPositionParams { | ||
194 | text_document: TextDocumentIdentifier::new(pos.file_id.try_conv_with(world)?), | ||
195 | position: pos.offset.conv_with(&line_index), | ||
196 | }) | ||
197 | } | ||
198 | }; | ||
199 | let source_file_edits = self.source_file_edits.try_conv_with(world)?; | ||
200 | let file_system_edits = self.file_system_edits.try_conv_with(world)?; | ||
201 | Ok(req::SourceChange { | ||
202 | label: self.label, | ||
203 | source_file_edits, | ||
204 | file_system_edits, | ||
205 | cursor_position, | ||
206 | }) | ||
207 | } | ||
208 | } | ||
209 | |||
210 | impl TryConvWith for SourceFileEdit { | ||
211 | type Ctx = ServerWorld; | ||
212 | type Output = TextDocumentEdit; | ||
213 | fn try_conv_with(self, world: &ServerWorld) -> Result<TextDocumentEdit> { | ||
214 | let text_document = VersionedTextDocumentIdentifier { | ||
215 | uri: self.file_id.try_conv_with(world)?, | ||
216 | version: None, | ||
217 | }; | ||
218 | let line_index = world.analysis().file_line_index(self.file_id); | ||
219 | let edits = self.edits | ||
220 | .into_iter() | ||
221 | .map_conv_with(&line_index) | ||
222 | .collect(); | ||
223 | Ok(TextDocumentEdit { text_document, edits }) | ||
224 | } | ||
225 | } | ||
226 | |||
227 | impl TryConvWith for FileSystemEdit { | ||
228 | type Ctx = ServerWorld; | ||
229 | type Output = req::FileSystemEdit; | ||
230 | fn try_conv_with(self, world: &ServerWorld) -> Result<req::FileSystemEdit> { | ||
231 | let res = match self { | ||
232 | FileSystemEdit::CreateFile { anchor, path } => { | ||
233 | let uri = world.file_id_to_uri(anchor)?; | ||
234 | let path = &path.as_str()[3..]; // strip `../` b/c url is weird | ||
235 | let uri = uri.join(path)?; | ||
236 | req::FileSystemEdit::CreateFile { uri } | ||
237 | }, | ||
238 | FileSystemEdit::MoveFile { file, path } => { | ||
239 | let src = world.file_id_to_uri(file)?; | ||
240 | let path = &path.as_str()[3..]; // strip `../` b/c url is weird | ||
241 | let dst = src.join(path)?; | ||
242 | req::FileSystemEdit::MoveFile { src, dst } | ||
243 | }, | ||
244 | }; | ||
245 | Ok(res) | ||
246 | } | ||
247 | } | ||
248 | |||
249 | pub fn to_location( | ||
250 | file_id: FileId, | ||
251 | range: TextRange, | ||
252 | world: &ServerWorld, | ||
253 | line_index: &LineIndex, | ||
254 | ) -> Result<Location> { | ||
255 | let url = file_id.try_conv_with(world)?; | ||
256 | let loc = Location::new( | ||
257 | url, | ||
258 | range.conv_with(line_index), | ||
259 | ); | ||
260 | Ok(loc) | ||
261 | } | ||
262 | |||
263 | pub trait MapConvWith<'a>: Sized { | ||
264 | type Ctx; | ||
265 | type Output; | ||
266 | |||
267 | fn map_conv_with(self, ctx: &'a Self::Ctx) -> ConvWithIter<'a, Self, Self::Ctx> { | ||
268 | ConvWithIter { iter: self, ctx } | ||
269 | } | ||
270 | } | ||
271 | |||
272 | impl<'a, I> MapConvWith<'a> for I | ||
273 | where I: Iterator, | ||
274 | I::Item: ConvWith | ||
275 | { | ||
276 | type Ctx = <I::Item as ConvWith>::Ctx; | ||
277 | type Output = <I::Item as ConvWith>::Output; | ||
278 | } | ||
279 | |||
280 | pub struct ConvWithIter<'a, I, Ctx: 'a> { | ||
281 | iter: I, | ||
282 | ctx: &'a Ctx, | ||
283 | } | ||
284 | |||
285 | impl<'a, I, Ctx> Iterator for ConvWithIter<'a, I, Ctx> | ||
286 | where | ||
287 | I: Iterator, | ||
288 | I::Item: ConvWith<Ctx=Ctx>, | ||
289 | { | ||
290 | type Item = <I::Item as ConvWith>::Output; | ||
291 | |||
292 | fn next(&mut self) -> Option<Self::Item> { | ||
293 | self.iter.next().map(|item| item.conv_with(self.ctx)) | ||
294 | } | ||
295 | } | ||
296 | |||
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs new file mode 100644 index 000000000..d2f76972f --- /dev/null +++ b/crates/ra_lsp_server/src/lib.rs | |||
@@ -0,0 +1,37 @@ | |||
1 | #[macro_use] | ||
2 | extern crate failure; | ||
3 | #[macro_use] | ||
4 | extern crate serde_derive; | ||
5 | extern crate serde; | ||
6 | extern crate serde_json; | ||
7 | extern crate languageserver_types; | ||
8 | #[macro_use] | ||
9 | extern crate crossbeam_channel; | ||
10 | extern crate rayon; | ||
11 | #[macro_use] | ||
12 | extern crate log; | ||
13 | extern crate drop_bomb; | ||
14 | extern crate url_serde; | ||
15 | extern crate walkdir; | ||
16 | extern crate im; | ||
17 | extern crate relative_path; | ||
18 | extern crate cargo_metadata; | ||
19 | |||
20 | extern crate gen_lsp_server; | ||
21 | extern crate ra_editor; | ||
22 | extern crate ra_analysis; | ||
23 | extern crate ra_syntax; | ||
24 | |||
25 | mod caps; | ||
26 | pub mod req; | ||
27 | mod conv; | ||
28 | mod main_loop; | ||
29 | mod vfs; | ||
30 | mod path_map; | ||
31 | mod server_world; | ||
32 | mod project_model; | ||
33 | pub mod thread_watcher; | ||
34 | |||
35 | pub type Result<T> = ::std::result::Result<T, ::failure::Error>; | ||
36 | pub use caps::server_capabilities; | ||
37 | pub use main_loop::main_loop; | ||
diff --git a/crates/ra_lsp_server/src/main.rs b/crates/ra_lsp_server/src/main.rs new file mode 100644 index 000000000..c547764f6 --- /dev/null +++ b/crates/ra_lsp_server/src/main.rs | |||
@@ -0,0 +1,52 @@ | |||
1 | #[macro_use] | ||
2 | extern crate log; | ||
3 | #[macro_use] | ||
4 | extern crate failure; | ||
5 | extern crate flexi_logger; | ||
6 | extern crate gen_lsp_server; | ||
7 | extern crate ra_lsp_server; | ||
8 | |||
9 | use flexi_logger::{Logger, Duplicate}; | ||
10 | use gen_lsp_server::{run_server, stdio_transport}; | ||
11 | use ra_lsp_server::Result; | ||
12 | |||
13 | fn main() -> Result<()> { | ||
14 | ::std::env::set_var("RUST_BACKTRACE", "short"); | ||
15 | Logger::with_env_or_str("error") | ||
16 | .duplicate_to_stderr(Duplicate::All) | ||
17 | .log_to_file() | ||
18 | .directory("log") | ||
19 | .start()?; | ||
20 | info!("lifecycle: server started"); | ||
21 | match ::std::panic::catch_unwind(|| main_inner()) { | ||
22 | Ok(res) => { | ||
23 | info!("lifecycle: terminating process with {:?}", res); | ||
24 | res | ||
25 | } | ||
26 | Err(_) => { | ||
27 | error!("server panicked"); | ||
28 | bail!("server panicked") | ||
29 | } | ||
30 | } | ||
31 | } | ||
32 | |||
33 | fn main_inner() -> Result<()> { | ||
34 | let (receiver, sender, threads) = stdio_transport(); | ||
35 | let cwd = ::std::env::current_dir()?; | ||
36 | run_server( | ||
37 | ra_lsp_server::server_capabilities(), | ||
38 | |params, r, s| { | ||
39 | let root = params.root_uri | ||
40 | .and_then(|it| it.to_file_path().ok()) | ||
41 | .unwrap_or(cwd); | ||
42 | ra_lsp_server::main_loop(false, root, r, s) | ||
43 | }, | ||
44 | receiver, | ||
45 | sender, | ||
46 | )?; | ||
47 | info!("shutting down IO..."); | ||
48 | threads.join()?; | ||
49 | info!("... IO is down"); | ||
50 | Ok(()) | ||
51 | } | ||
52 | |||
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs new file mode 100644 index 000000000..568f5344c --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/handlers.rs | |||
@@ -0,0 +1,436 @@ | |||
1 | use std::collections::HashMap; | ||
2 | |||
3 | use languageserver_types::{ | ||
4 | Diagnostic, DiagnosticSeverity, DocumentSymbol, | ||
5 | Command, TextDocumentIdentifier, | ||
6 | SymbolInformation, Position, Location, TextEdit, | ||
7 | CompletionItem, InsertTextFormat, CompletionItemKind, | ||
8 | }; | ||
9 | use serde_json::to_value; | ||
10 | use ra_analysis::{Query, FileId, RunnableKind, JobToken}; | ||
11 | use ra_syntax::{ | ||
12 | text_utils::contains_offset_nonstrict, | ||
13 | }; | ||
14 | |||
15 | use ::{ | ||
16 | req::{self, Decoration}, Result, | ||
17 | conv::{Conv, ConvWith, TryConvWith, MapConvWith, to_location}, | ||
18 | server_world::ServerWorld, | ||
19 | project_model::TargetKind, | ||
20 | }; | ||
21 | |||
22 | pub fn handle_syntax_tree( | ||
23 | world: ServerWorld, | ||
24 | params: req::SyntaxTreeParams, | ||
25 | _token: JobToken, | ||
26 | ) -> Result<String> { | ||
27 | let id = params.text_document.try_conv_with(&world)?; | ||
28 | let res = world.analysis().syntax_tree(id); | ||
29 | Ok(res) | ||
30 | } | ||
31 | |||
32 | pub fn handle_extend_selection( | ||
33 | world: ServerWorld, | ||
34 | params: req::ExtendSelectionParams, | ||
35 | _token: JobToken, | ||
36 | ) -> Result<req::ExtendSelectionResult> { | ||
37 | let file_id = params.text_document.try_conv_with(&world)?; | ||
38 | let file = world.analysis().file_syntax(file_id); | ||
39 | let line_index = world.analysis().file_line_index(file_id); | ||
40 | let selections = params.selections.into_iter() | ||
41 | .map_conv_with(&line_index) | ||
42 | .map(|r| world.analysis().extend_selection(&file, r)) | ||
43 | .map_conv_with(&line_index) | ||
44 | .collect(); | ||
45 | Ok(req::ExtendSelectionResult { selections }) | ||
46 | } | ||
47 | |||
48 | pub fn handle_find_matching_brace( | ||
49 | world: ServerWorld, | ||
50 | params: req::FindMatchingBraceParams, | ||
51 | _token: JobToken, | ||
52 | ) -> Result<Vec<Position>> { | ||
53 | let file_id = params.text_document.try_conv_with(&world)?; | ||
54 | let file = world.analysis().file_syntax(file_id); | ||
55 | let line_index = world.analysis().file_line_index(file_id); | ||
56 | let res = params.offsets | ||
57 | .into_iter() | ||
58 | .map_conv_with(&line_index) | ||
59 | .map(|offset| { | ||
60 | world.analysis().matching_brace(&file, offset).unwrap_or(offset) | ||
61 | }) | ||
62 | .map_conv_with(&line_index) | ||
63 | .collect(); | ||
64 | Ok(res) | ||
65 | } | ||
66 | |||
67 | pub fn handle_join_lines( | ||
68 | world: ServerWorld, | ||
69 | params: req::JoinLinesParams, | ||
70 | _token: JobToken, | ||
71 | ) -> Result<req::SourceChange> { | ||
72 | let file_id = params.text_document.try_conv_with(&world)?; | ||
73 | let line_index = world.analysis().file_line_index(file_id); | ||
74 | let range = params.range.conv_with(&line_index); | ||
75 | world.analysis().join_lines(file_id, range) | ||
76 | .try_conv_with(&world) | ||
77 | } | ||
78 | |||
79 | pub fn handle_on_type_formatting( | ||
80 | world: ServerWorld, | ||
81 | params: req::DocumentOnTypeFormattingParams, | ||
82 | _token: JobToken, | ||
83 | ) -> Result<Option<Vec<TextEdit>>> { | ||
84 | if params.ch != "=" { | ||
85 | return Ok(None); | ||
86 | } | ||
87 | |||
88 | let file_id = params.text_document.try_conv_with(&world)?; | ||
89 | let line_index = world.analysis().file_line_index(file_id); | ||
90 | let offset = params.position.conv_with(&line_index); | ||
91 | let edits = match world.analysis().on_eq_typed(file_id, offset) { | ||
92 | None => return Ok(None), | ||
93 | Some(mut action) => action.source_file_edits.pop().unwrap().edits, | ||
94 | }; | ||
95 | let edits = edits.into_iter().map_conv_with(&line_index).collect(); | ||
96 | Ok(Some(edits)) | ||
97 | } | ||
98 | |||
99 | pub fn handle_document_symbol( | ||
100 | world: ServerWorld, | ||
101 | params: req::DocumentSymbolParams, | ||
102 | _token: JobToken, | ||
103 | ) -> Result<Option<req::DocumentSymbolResponse>> { | ||
104 | let file_id = params.text_document.try_conv_with(&world)?; | ||
105 | let line_index = world.analysis().file_line_index(file_id); | ||
106 | |||
107 | let mut parents: Vec<(DocumentSymbol, Option<usize>)> = Vec::new(); | ||
108 | |||
109 | for symbol in world.analysis().file_structure(file_id) { | ||
110 | let doc_symbol = DocumentSymbol { | ||
111 | name: symbol.label, | ||
112 | detail: Some("".to_string()), | ||
113 | kind: symbol.kind.conv(), | ||
114 | deprecated: None, | ||
115 | range: symbol.node_range.conv_with(&line_index), | ||
116 | selection_range: symbol.navigation_range.conv_with(&line_index), | ||
117 | children: None, | ||
118 | }; | ||
119 | parents.push((doc_symbol, symbol.parent)); | ||
120 | } | ||
121 | let mut res = Vec::new(); | ||
122 | while let Some((node, parent)) = parents.pop() { | ||
123 | match parent { | ||
124 | None => res.push(node), | ||
125 | Some(i) => { | ||
126 | let children = &mut parents[i].0.children; | ||
127 | if children.is_none() { | ||
128 | *children = Some(Vec::new()); | ||
129 | } | ||
130 | children.as_mut().unwrap().push(node); | ||
131 | } | ||
132 | } | ||
133 | } | ||
134 | |||
135 | Ok(Some(req::DocumentSymbolResponse::Nested(res))) | ||
136 | } | ||
137 | |||
138 | pub fn handle_workspace_symbol( | ||
139 | world: ServerWorld, | ||
140 | params: req::WorkspaceSymbolParams, | ||
141 | token: JobToken, | ||
142 | ) -> Result<Option<Vec<SymbolInformation>>> { | ||
143 | let all_symbols = params.query.contains("#"); | ||
144 | let libs = params.query.contains("*"); | ||
145 | let query = { | ||
146 | let query: String = params.query.chars() | ||
147 | .filter(|&c| c != '#' && c != '*') | ||
148 | .collect(); | ||
149 | let mut q = Query::new(query); | ||
150 | if !all_symbols { | ||
151 | q.only_types(); | ||
152 | } | ||
153 | if libs { | ||
154 | q.libs(); | ||
155 | } | ||
156 | q.limit(128); | ||
157 | q | ||
158 | }; | ||
159 | let mut res = exec_query(&world, query, &token)?; | ||
160 | if res.is_empty() && !all_symbols { | ||
161 | let mut query = Query::new(params.query); | ||
162 | query.limit(128); | ||
163 | res = exec_query(&world, query, &token)?; | ||
164 | } | ||
165 | |||
166 | return Ok(Some(res)); | ||
167 | |||
168 | fn exec_query(world: &ServerWorld, query: Query, token: &JobToken) -> Result<Vec<SymbolInformation>> { | ||
169 | let mut res = Vec::new(); | ||
170 | for (file_id, symbol) in world.analysis().symbol_search(query, token) { | ||
171 | let line_index = world.analysis().file_line_index(file_id); | ||
172 | let info = SymbolInformation { | ||
173 | name: symbol.name.to_string(), | ||
174 | kind: symbol.kind.conv(), | ||
175 | location: to_location( | ||
176 | file_id, symbol.node_range, | ||
177 | world, &line_index | ||
178 | )?, | ||
179 | container_name: None, | ||
180 | }; | ||
181 | res.push(info); | ||
182 | }; | ||
183 | Ok(res) | ||
184 | } | ||
185 | } | ||
186 | |||
187 | pub fn handle_goto_definition( | ||
188 | world: ServerWorld, | ||
189 | params: req::TextDocumentPositionParams, | ||
190 | token: JobToken, | ||
191 | ) -> Result<Option<req::GotoDefinitionResponse>> { | ||
192 | let file_id = params.text_document.try_conv_with(&world)?; | ||
193 | let line_index = world.analysis().file_line_index(file_id); | ||
194 | let offset = params.position.conv_with(&line_index); | ||
195 | let mut res = Vec::new(); | ||
196 | for (file_id, symbol) in world.analysis().approximately_resolve_symbol(file_id, offset, &token) { | ||
197 | let line_index = world.analysis().file_line_index(file_id); | ||
198 | let location = to_location( | ||
199 | file_id, symbol.node_range, | ||
200 | &world, &line_index, | ||
201 | )?; | ||
202 | res.push(location) | ||
203 | } | ||
204 | Ok(Some(req::GotoDefinitionResponse::Array(res))) | ||
205 | } | ||
206 | |||
207 | pub fn handle_parent_module( | ||
208 | world: ServerWorld, | ||
209 | params: TextDocumentIdentifier, | ||
210 | _token: JobToken, | ||
211 | ) -> Result<Vec<Location>> { | ||
212 | let file_id = params.try_conv_with(&world)?; | ||
213 | let mut res = Vec::new(); | ||
214 | for (file_id, symbol) in world.analysis().parent_module(file_id) { | ||
215 | let line_index = world.analysis().file_line_index(file_id); | ||
216 | let location = to_location( | ||
217 | file_id, symbol.node_range, | ||
218 | &world, &line_index | ||
219 | )?; | ||
220 | res.push(location); | ||
221 | } | ||
222 | Ok(res) | ||
223 | } | ||
224 | |||
225 | pub fn handle_runnables( | ||
226 | world: ServerWorld, | ||
227 | params: req::RunnablesParams, | ||
228 | _token: JobToken, | ||
229 | ) -> Result<Vec<req::Runnable>> { | ||
230 | let file_id = params.text_document.try_conv_with(&world)?; | ||
231 | let line_index = world.analysis().file_line_index(file_id); | ||
232 | let offset = params.position.map(|it| it.conv_with(&line_index)); | ||
233 | let mut res = Vec::new(); | ||
234 | for runnable in world.analysis().runnables(file_id) { | ||
235 | if let Some(offset) = offset { | ||
236 | if !contains_offset_nonstrict(runnable.range, offset) { | ||
237 | continue; | ||
238 | } | ||
239 | } | ||
240 | |||
241 | let args = runnable_args(&world, file_id, &runnable.kind); | ||
242 | |||
243 | let r = req::Runnable { | ||
244 | range: runnable.range.conv_with(&line_index), | ||
245 | label: match &runnable.kind { | ||
246 | RunnableKind::Test { name } => | ||
247 | format!("test {}", name), | ||
248 | RunnableKind::Bin => | ||
249 | "run binary".to_string(), | ||
250 | }, | ||
251 | bin: "cargo".to_string(), | ||
252 | args, | ||
253 | env: { | ||
254 | let mut m = HashMap::new(); | ||
255 | m.insert( | ||
256 | "RUST_BACKTRACE".to_string(), | ||
257 | "short".to_string(), | ||
258 | ); | ||
259 | m | ||
260 | } | ||
261 | }; | ||
262 | res.push(r); | ||
263 | } | ||
264 | return Ok(res); | ||
265 | |||
266 | fn runnable_args(world: &ServerWorld, file_id: FileId, kind: &RunnableKind) -> Vec<String> { | ||
267 | let spec = if let Some(&crate_id) = world.analysis().crate_for(file_id).first() { | ||
268 | let file_id = world.analysis().crate_root(crate_id); | ||
269 | let path = world.path_map.get_path(file_id); | ||
270 | world.workspaces.iter() | ||
271 | .filter_map(|ws| { | ||
272 | let tgt = ws.target_by_root(path)?; | ||
273 | Some((tgt.package(ws).name(ws).clone(), tgt.name(ws).clone(), tgt.kind(ws))) | ||
274 | }) | ||
275 | .next() | ||
276 | } else { | ||
277 | None | ||
278 | }; | ||
279 | let mut res = Vec::new(); | ||
280 | match kind { | ||
281 | RunnableKind::Test { name } => { | ||
282 | res.push("test".to_string()); | ||
283 | if let Some((pkg_name, tgt_name, tgt_kind)) = spec { | ||
284 | spec_args(pkg_name, tgt_name, tgt_kind, &mut res); | ||
285 | } | ||
286 | res.push("--".to_string()); | ||
287 | res.push(name.to_string()); | ||
288 | res.push("--nocapture".to_string()); | ||
289 | } | ||
290 | RunnableKind::Bin => { | ||
291 | res.push("run".to_string()); | ||
292 | if let Some((pkg_name, tgt_name, tgt_kind)) = spec { | ||
293 | spec_args(pkg_name, tgt_name, tgt_kind, &mut res); | ||
294 | } | ||
295 | } | ||
296 | } | ||
297 | res | ||
298 | } | ||
299 | |||
300 | fn spec_args(pkg_name: &str, tgt_name: &str, tgt_kind: TargetKind, buf: &mut Vec<String>) { | ||
301 | buf.push("--package".to_string()); | ||
302 | buf.push(pkg_name.to_string()); | ||
303 | match tgt_kind { | ||
304 | TargetKind::Bin => { | ||
305 | buf.push("--bin".to_string()); | ||
306 | buf.push(tgt_name.to_string()); | ||
307 | } | ||
308 | TargetKind::Test => { | ||
309 | buf.push("--test".to_string()); | ||
310 | buf.push(tgt_name.to_string()); | ||
311 | } | ||
312 | TargetKind::Bench => { | ||
313 | buf.push("--bench".to_string()); | ||
314 | buf.push(tgt_name.to_string()); | ||
315 | } | ||
316 | TargetKind::Example => { | ||
317 | buf.push("--example".to_string()); | ||
318 | buf.push(tgt_name.to_string()); | ||
319 | } | ||
320 | TargetKind::Lib => { | ||
321 | buf.push("--lib".to_string()); | ||
322 | } | ||
323 | TargetKind::Other => (), | ||
324 | } | ||
325 | } | ||
326 | } | ||
327 | |||
328 | pub fn handle_decorations( | ||
329 | world: ServerWorld, | ||
330 | params: TextDocumentIdentifier, | ||
331 | _token: JobToken, | ||
332 | ) -> Result<Vec<Decoration>> { | ||
333 | let file_id = params.try_conv_with(&world)?; | ||
334 | Ok(highlight(&world, file_id)) | ||
335 | } | ||
336 | |||
337 | pub fn handle_completion( | ||
338 | world: ServerWorld, | ||
339 | params: req::CompletionParams, | ||
340 | _token: JobToken, | ||
341 | ) -> Result<Option<req::CompletionResponse>> { | ||
342 | let file_id = params.text_document.try_conv_with(&world)?; | ||
343 | let line_index = world.analysis().file_line_index(file_id); | ||
344 | let offset = params.position.conv_with(&line_index); | ||
345 | let items = match world.analysis().completions(file_id, offset) { | ||
346 | None => return Ok(None), | ||
347 | Some(items) => items, | ||
348 | }; | ||
349 | let items = items.into_iter() | ||
350 | .map(|item| { | ||
351 | let mut res = CompletionItem { | ||
352 | label: item.label, | ||
353 | filter_text: item.lookup, | ||
354 | .. Default::default() | ||
355 | }; | ||
356 | if let Some(snip) = item.snippet { | ||
357 | res.insert_text = Some(snip); | ||
358 | res.insert_text_format = Some(InsertTextFormat::Snippet); | ||
359 | res.kind = Some(CompletionItemKind::Keyword); | ||
360 | }; | ||
361 | res | ||
362 | }) | ||
363 | .collect(); | ||
364 | |||
365 | Ok(Some(req::CompletionResponse::Array(items))) | ||
366 | } | ||
367 | |||
368 | pub fn handle_code_action( | ||
369 | world: ServerWorld, | ||
370 | params: req::CodeActionParams, | ||
371 | _token: JobToken, | ||
372 | ) -> Result<Option<Vec<Command>>> { | ||
373 | let file_id = params.text_document.try_conv_with(&world)?; | ||
374 | let line_index = world.analysis().file_line_index(file_id); | ||
375 | let range = params.range.conv_with(&line_index); | ||
376 | |||
377 | let assists = world.analysis().assists(file_id, range).into_iter(); | ||
378 | let fixes = world.analysis().diagnostics(file_id).into_iter() | ||
379 | .filter_map(|d| Some((d.range, d.fix?))) | ||
380 | .filter(|(range, _fix)| contains_offset_nonstrict(*range, range.start())) | ||
381 | .map(|(_range, fix)| fix); | ||
382 | |||
383 | let mut res = Vec::new(); | ||
384 | for source_edit in assists.chain(fixes) { | ||
385 | let title = source_edit.label.clone(); | ||
386 | let edit = source_edit.try_conv_with(&world)?; | ||
387 | let cmd = Command { | ||
388 | title, | ||
389 | command: "libsyntax-rust.applySourceChange".to_string(), | ||
390 | arguments: Some(vec![to_value(edit).unwrap()]), | ||
391 | }; | ||
392 | res.push(cmd); | ||
393 | } | ||
394 | |||
395 | Ok(Some(res)) | ||
396 | } | ||
397 | |||
398 | pub fn publish_diagnostics( | ||
399 | world: ServerWorld, | ||
400 | file_id: FileId, | ||
401 | ) -> Result<req::PublishDiagnosticsParams> { | ||
402 | let uri = world.file_id_to_uri(file_id)?; | ||
403 | let line_index = world.analysis().file_line_index(file_id); | ||
404 | let diagnostics = world.analysis().diagnostics(file_id) | ||
405 | .into_iter() | ||
406 | .map(|d| Diagnostic { | ||
407 | range: d.range.conv_with(&line_index), | ||
408 | severity: Some(DiagnosticSeverity::Error), | ||
409 | code: None, | ||
410 | source: Some("libsyntax2".to_string()), | ||
411 | message: d.message, | ||
412 | related_information: None, | ||
413 | }).collect(); | ||
414 | Ok(req::PublishDiagnosticsParams { uri, diagnostics }) | ||
415 | } | ||
416 | |||
417 | pub fn publish_decorations( | ||
418 | world: ServerWorld, | ||
419 | file_id: FileId, | ||
420 | ) -> Result<req::PublishDecorationsParams> { | ||
421 | let uri = world.file_id_to_uri(file_id)?; | ||
422 | Ok(req::PublishDecorationsParams { | ||
423 | uri, | ||
424 | decorations: highlight(&world, file_id), | ||
425 | }) | ||
426 | } | ||
427 | |||
428 | fn highlight(world: &ServerWorld, file_id: FileId) -> Vec<Decoration> { | ||
429 | let line_index = world.analysis().file_line_index(file_id); | ||
430 | world.analysis().highlight(file_id) | ||
431 | .into_iter() | ||
432 | .map(|h| Decoration { | ||
433 | range: h.range.conv_with(&line_index), | ||
434 | tag: h.tag, | ||
435 | }).collect() | ||
436 | } | ||
diff --git a/crates/ra_lsp_server/src/main_loop/mod.rs b/crates/ra_lsp_server/src/main_loop/mod.rs new file mode 100644 index 000000000..2b2279e97 --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/mod.rs | |||
@@ -0,0 +1,419 @@ | |||
1 | mod handlers; | ||
2 | mod subscriptions; | ||
3 | |||
4 | use std::{ | ||
5 | path::PathBuf, | ||
6 | collections::{HashMap}, | ||
7 | }; | ||
8 | |||
9 | use serde::{Serialize, de::DeserializeOwned}; | ||
10 | use crossbeam_channel::{unbounded, Sender, Receiver}; | ||
11 | use rayon::{self, ThreadPool}; | ||
12 | use languageserver_types::{NumberOrString}; | ||
13 | use ra_analysis::{FileId, JobHandle, JobToken, LibraryData}; | ||
14 | use gen_lsp_server::{ | ||
15 | RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode, | ||
16 | handle_shutdown, | ||
17 | }; | ||
18 | |||
19 | use { | ||
20 | req, | ||
21 | Result, | ||
22 | vfs::{self, FileEvent}, | ||
23 | server_world::{ServerWorldState, ServerWorld}, | ||
24 | main_loop::subscriptions::{Subscriptions}, | ||
25 | project_model::{CargoWorkspace, workspace_loader}, | ||
26 | thread_watcher::Worker, | ||
27 | }; | ||
28 | |||
29 | #[derive(Debug)] | ||
30 | enum Task { | ||
31 | Respond(RawResponse), | ||
32 | Notify(RawNotification), | ||
33 | } | ||
34 | |||
35 | pub fn main_loop( | ||
36 | internal_mode: bool, | ||
37 | root: PathBuf, | ||
38 | msg_receriver: &mut Receiver<RawMessage>, | ||
39 | msg_sender: &mut Sender<RawMessage>, | ||
40 | ) -> Result<()> { | ||
41 | let pool = rayon::ThreadPoolBuilder::new() | ||
42 | .num_threads(4) | ||
43 | .panic_handler(|_| error!("thread panicked :(")) | ||
44 | .build() | ||
45 | .unwrap(); | ||
46 | let (task_sender, task_receiver) = unbounded::<Task>(); | ||
47 | let (fs_worker, fs_watcher) = vfs::roots_loader(); | ||
48 | let (ws_worker, ws_watcher) = workspace_loader(); | ||
49 | |||
50 | info!("server initialized, serving requests"); | ||
51 | let mut state = ServerWorldState::new(); | ||
52 | |||
53 | let mut pending_requests = HashMap::new(); | ||
54 | let mut subs = Subscriptions::new(); | ||
55 | let main_res = main_loop_inner( | ||
56 | internal_mode, | ||
57 | root, | ||
58 | &pool, | ||
59 | msg_sender, | ||
60 | msg_receriver, | ||
61 | task_sender, | ||
62 | task_receiver.clone(), | ||
63 | fs_worker, | ||
64 | ws_worker, | ||
65 | &mut state, | ||
66 | &mut pending_requests, | ||
67 | &mut subs, | ||
68 | ); | ||
69 | |||
70 | info!("waiting for tasks to finish..."); | ||
71 | task_receiver.for_each(|task| on_task(task, msg_sender, &mut pending_requests)); | ||
72 | info!("...tasks have finished"); | ||
73 | info!("joining threadpool..."); | ||
74 | drop(pool); | ||
75 | info!("...threadpool has finished"); | ||
76 | |||
77 | let fs_res = fs_watcher.stop(); | ||
78 | let ws_res = ws_watcher.stop(); | ||
79 | |||
80 | main_res?; | ||
81 | fs_res?; | ||
82 | ws_res?; | ||
83 | |||
84 | Ok(()) | ||
85 | } | ||
86 | |||
87 | fn main_loop_inner( | ||
88 | internal_mode: bool, | ||
89 | ws_root: PathBuf, | ||
90 | pool: &ThreadPool, | ||
91 | msg_sender: &mut Sender<RawMessage>, | ||
92 | msg_receiver: &mut Receiver<RawMessage>, | ||
93 | task_sender: Sender<Task>, | ||
94 | task_receiver: Receiver<Task>, | ||
95 | fs_worker: Worker<PathBuf, (PathBuf, Vec<FileEvent>)>, | ||
96 | ws_worker: Worker<PathBuf, Result<CargoWorkspace>>, | ||
97 | state: &mut ServerWorldState, | ||
98 | pending_requests: &mut HashMap<u64, JobHandle>, | ||
99 | subs: &mut Subscriptions, | ||
100 | ) -> Result<()> { | ||
101 | let (libdata_sender, libdata_receiver) = unbounded(); | ||
102 | ws_worker.send(ws_root.clone()); | ||
103 | fs_worker.send(ws_root.clone()); | ||
104 | loop { | ||
105 | #[derive(Debug)] | ||
106 | enum Event { | ||
107 | Msg(RawMessage), | ||
108 | Task(Task), | ||
109 | Fs(PathBuf, Vec<FileEvent>), | ||
110 | Ws(Result<CargoWorkspace>), | ||
111 | Lib(LibraryData), | ||
112 | } | ||
113 | trace!("selecting"); | ||
114 | let event = select! { | ||
115 | recv(msg_receiver, msg) => match msg { | ||
116 | Some(msg) => Event::Msg(msg), | ||
117 | None => bail!("client exited without shutdown"), | ||
118 | }, | ||
119 | recv(task_receiver, task) => Event::Task(task.unwrap()), | ||
120 | recv(fs_worker.out, events) => match events { | ||
121 | None => bail!("roots watcher died"), | ||
122 | Some((pb, events)) => Event::Fs(pb, events), | ||
123 | } | ||
124 | recv(ws_worker.out, ws) => match ws { | ||
125 | None => bail!("workspace watcher died"), | ||
126 | Some(ws) => Event::Ws(ws), | ||
127 | } | ||
128 | recv(libdata_receiver, data) => Event::Lib(data.unwrap()) | ||
129 | }; | ||
130 | let mut state_changed = false; | ||
131 | match event { | ||
132 | Event::Task(task) => on_task(task, msg_sender, pending_requests), | ||
133 | Event::Fs(root, events) => { | ||
134 | info!("fs change, {}, {} events", root.display(), events.len()); | ||
135 | if root == ws_root { | ||
136 | state.apply_fs_changes(events); | ||
137 | } else { | ||
138 | let (files, resolver) = state.events_to_files(events); | ||
139 | let sender = libdata_sender.clone(); | ||
140 | pool.spawn(move || { | ||
141 | let start = ::std::time::Instant::now(); | ||
142 | info!("indexing {} ... ", root.display()); | ||
143 | let data = LibraryData::prepare(files, resolver); | ||
144 | info!("indexed {:?} {}", start.elapsed(), root.display()); | ||
145 | sender.send(data); | ||
146 | }); | ||
147 | } | ||
148 | state_changed = true; | ||
149 | } | ||
150 | Event::Ws(ws) => { | ||
151 | match ws { | ||
152 | Ok(ws) => { | ||
153 | let workspaces = vec![ws]; | ||
154 | feedback(internal_mode, "workspace loaded", msg_sender); | ||
155 | for ws in workspaces.iter() { | ||
156 | for pkg in ws.packages().filter(|pkg| !pkg.is_member(ws)) { | ||
157 | debug!("sending root, {}", pkg.root(ws).to_path_buf().display()); | ||
158 | fs_worker.send(pkg.root(ws).to_path_buf()); | ||
159 | } | ||
160 | } | ||
161 | state.set_workspaces(workspaces); | ||
162 | state_changed = true; | ||
163 | } | ||
164 | Err(e) => warn!("loading workspace failed: {}", e), | ||
165 | } | ||
166 | } | ||
167 | Event::Lib(lib) => { | ||
168 | feedback(internal_mode, "library loaded", msg_sender); | ||
169 | state.add_lib(lib); | ||
170 | } | ||
171 | Event::Msg(msg) => { | ||
172 | match msg { | ||
173 | RawMessage::Request(req) => { | ||
174 | let req = match handle_shutdown(req, msg_sender) { | ||
175 | Some(req) => req, | ||
176 | None => return Ok(()), | ||
177 | }; | ||
178 | match on_request(state, pending_requests, pool, &task_sender, req)? { | ||
179 | None => (), | ||
180 | Some(req) => { | ||
181 | error!("unknown request: {:?}", req); | ||
182 | let resp = RawResponse::err( | ||
183 | req.id, | ||
184 | ErrorCode::MethodNotFound as i32, | ||
185 | "unknown request".to_string(), | ||
186 | ); | ||
187 | msg_sender.send(RawMessage::Response(resp)) | ||
188 | } | ||
189 | } | ||
190 | } | ||
191 | RawMessage::Notification(not) => { | ||
192 | on_notification(msg_sender, state, pending_requests, subs, not)?; | ||
193 | state_changed = true; | ||
194 | } | ||
195 | RawMessage::Response(resp) => { | ||
196 | error!("unexpected response: {:?}", resp) | ||
197 | } | ||
198 | } | ||
199 | } | ||
200 | }; | ||
201 | |||
202 | if state_changed { | ||
203 | update_file_notifications_on_threadpool( | ||
204 | pool, | ||
205 | state.snapshot(), | ||
206 | task_sender.clone(), | ||
207 | subs.subscriptions(), | ||
208 | ) | ||
209 | } | ||
210 | } | ||
211 | } | ||
212 | |||
213 | fn on_task( | ||
214 | task: Task, | ||
215 | msg_sender: &mut Sender<RawMessage>, | ||
216 | pending_requests: &mut HashMap<u64, JobHandle>, | ||
217 | ) { | ||
218 | match task { | ||
219 | Task::Respond(response) => { | ||
220 | if let Some(handle) = pending_requests.remove(&response.id) { | ||
221 | assert!(handle.has_completed()); | ||
222 | } | ||
223 | msg_sender.send(RawMessage::Response(response)) | ||
224 | } | ||
225 | Task::Notify(n) => | ||
226 | msg_sender.send(RawMessage::Notification(n)), | ||
227 | } | ||
228 | } | ||
229 | |||
230 | fn on_request( | ||
231 | world: &mut ServerWorldState, | ||
232 | pending_requests: &mut HashMap<u64, JobHandle>, | ||
233 | pool: &ThreadPool, | ||
234 | sender: &Sender<Task>, | ||
235 | req: RawRequest, | ||
236 | ) -> Result<Option<RawRequest>> { | ||
237 | let mut pool_dispatcher = PoolDispatcher { | ||
238 | req: Some(req), | ||
239 | res: None, | ||
240 | pool, world, sender | ||
241 | }; | ||
242 | let req = pool_dispatcher | ||
243 | .on::<req::SyntaxTree>(handlers::handle_syntax_tree)? | ||
244 | .on::<req::ExtendSelection>(handlers::handle_extend_selection)? | ||
245 | .on::<req::FindMatchingBrace>(handlers::handle_find_matching_brace)? | ||
246 | .on::<req::JoinLines>(handlers::handle_join_lines)? | ||
247 | .on::<req::OnTypeFormatting>(handlers::handle_on_type_formatting)? | ||
248 | .on::<req::DocumentSymbolRequest>(handlers::handle_document_symbol)? | ||
249 | .on::<req::WorkspaceSymbol>(handlers::handle_workspace_symbol)? | ||
250 | .on::<req::GotoDefinition>(handlers::handle_goto_definition)? | ||
251 | .on::<req::ParentModule>(handlers::handle_parent_module)? | ||
252 | .on::<req::Runnables>(handlers::handle_runnables)? | ||
253 | .on::<req::DecorationsRequest>(handlers::handle_decorations)? | ||
254 | .on::<req::Completion>(handlers::handle_completion)? | ||
255 | .on::<req::CodeActionRequest>(handlers::handle_code_action)? | ||
256 | .finish(); | ||
257 | match req { | ||
258 | Ok((id, handle)) => { | ||
259 | let inserted = pending_requests.insert(id, handle).is_none(); | ||
260 | assert!(inserted, "duplicate request: {}", id); | ||
261 | Ok(None) | ||
262 | }, | ||
263 | Err(req) => Ok(Some(req)), | ||
264 | } | ||
265 | } | ||
266 | |||
267 | fn on_notification( | ||
268 | msg_sender: &mut Sender<RawMessage>, | ||
269 | state: &mut ServerWorldState, | ||
270 | pending_requests: &mut HashMap<u64, JobHandle>, | ||
271 | subs: &mut Subscriptions, | ||
272 | not: RawNotification, | ||
273 | ) -> Result<()> { | ||
274 | let not = match not.cast::<req::Cancel>() { | ||
275 | Ok(params) => { | ||
276 | let id = match params.id { | ||
277 | NumberOrString::Number(id) => id, | ||
278 | NumberOrString::String(id) => { | ||
279 | panic!("string id's not supported: {:?}", id); | ||
280 | } | ||
281 | }; | ||
282 | if let Some(handle) = pending_requests.remove(&id) { | ||
283 | handle.cancel(); | ||
284 | } | ||
285 | return Ok(()) | ||
286 | } | ||
287 | Err(not) => not, | ||
288 | }; | ||
289 | let not = match not.cast::<req::DidOpenTextDocument>() { | ||
290 | Ok(params) => { | ||
291 | let uri = params.text_document.uri; | ||
292 | let path = uri.to_file_path() | ||
293 | .map_err(|()| format_err!("invalid uri: {}", uri))?; | ||
294 | let file_id = state.add_mem_file(path, params.text_document.text); | ||
295 | subs.add_sub(file_id); | ||
296 | return Ok(()) | ||
297 | } | ||
298 | Err(not) => not, | ||
299 | }; | ||
300 | let not = match not.cast::<req::DidChangeTextDocument>() { | ||
301 | Ok(mut params) => { | ||
302 | let uri = params.text_document.uri; | ||
303 | let path = uri.to_file_path() | ||
304 | .map_err(|()| format_err!("invalid uri: {}", uri))?; | ||
305 | let text = params.content_changes.pop() | ||
306 | .ok_or_else(|| format_err!("empty changes"))? | ||
307 | .text; | ||
308 | state.change_mem_file(path.as_path(), text)?; | ||
309 | return Ok(()) | ||
310 | } | ||
311 | Err(not) => not, | ||
312 | }; | ||
313 | let not = match not.cast::<req::DidCloseTextDocument>() { | ||
314 | Ok(params) => { | ||
315 | let uri = params.text_document.uri; | ||
316 | let path = uri.to_file_path() | ||
317 | .map_err(|()| format_err!("invalid uri: {}", uri))?; | ||
318 | let file_id = state.remove_mem_file(path.as_path())?; | ||
319 | subs.remove_sub(file_id); | ||
320 | let params = req::PublishDiagnosticsParams { uri, diagnostics: Vec::new() }; | ||
321 | let not = RawNotification::new::<req::PublishDiagnostics>(¶ms); | ||
322 | msg_sender.send(RawMessage::Notification(not)); | ||
323 | return Ok(()) | ||
324 | } | ||
325 | Err(not) => not, | ||
326 | }; | ||
327 | error!("unhandled notification: {:?}", not); | ||
328 | Ok(()) | ||
329 | } | ||
330 | |||
331 | struct PoolDispatcher<'a> { | ||
332 | req: Option<RawRequest>, | ||
333 | res: Option<(u64, JobHandle)>, | ||
334 | pool: &'a ThreadPool, | ||
335 | world: &'a ServerWorldState, | ||
336 | sender: &'a Sender<Task>, | ||
337 | } | ||
338 | |||
339 | impl<'a> PoolDispatcher<'a> { | ||
340 | fn on<'b, R>( | ||
341 | &'b mut self, | ||
342 | f: fn(ServerWorld, R::Params, JobToken) -> Result<R::Result> | ||
343 | ) -> Result<&'b mut Self> | ||
344 | where R: req::Request, | ||
345 | R::Params: DeserializeOwned + Send + 'static, | ||
346 | R::Result: Serialize + 'static, | ||
347 | { | ||
348 | let req = match self.req.take() { | ||
349 | None => return Ok(self), | ||
350 | Some(req) => req, | ||
351 | }; | ||
352 | match req.cast::<R>() { | ||
353 | Ok((id, params)) => { | ||
354 | let (handle, token) = JobHandle::new(); | ||
355 | let world = self.world.snapshot(); | ||
356 | let sender = self.sender.clone(); | ||
357 | self.pool.spawn(move || { | ||
358 | let resp = match f(world, params, token) { | ||
359 | Ok(resp) => RawResponse::ok::<R>(id, &resp), | ||
360 | Err(e) => RawResponse::err(id, ErrorCode::InternalError as i32, e.to_string()), | ||
361 | }; | ||
362 | let task = Task::Respond(resp); | ||
363 | sender.send(task); | ||
364 | }); | ||
365 | self.res = Some((id, handle)); | ||
366 | } | ||
367 | Err(req) => { | ||
368 | self.req = Some(req) | ||
369 | } | ||
370 | } | ||
371 | Ok(self) | ||
372 | } | ||
373 | |||
374 | fn finish(&mut self) -> ::std::result::Result<(u64, JobHandle), RawRequest> { | ||
375 | match (self.res.take(), self.req.take()) { | ||
376 | (Some(res), None) => Ok(res), | ||
377 | (None, Some(req)) => Err(req), | ||
378 | _ => unreachable!(), | ||
379 | } | ||
380 | } | ||
381 | } | ||
382 | |||
383 | fn update_file_notifications_on_threadpool( | ||
384 | pool: &ThreadPool, | ||
385 | world: ServerWorld, | ||
386 | sender: Sender<Task>, | ||
387 | subscriptions: Vec<FileId>, | ||
388 | ) { | ||
389 | pool.spawn(move || { | ||
390 | for file_id in subscriptions { | ||
391 | match handlers::publish_diagnostics(world.clone(), file_id) { | ||
392 | Err(e) => { | ||
393 | error!("failed to compute diagnostics: {:?}", e) | ||
394 | } | ||
395 | Ok(params) => { | ||
396 | let not = RawNotification::new::<req::PublishDiagnostics>(¶ms); | ||
397 | sender.send(Task::Notify(not)); | ||
398 | } | ||
399 | } | ||
400 | match handlers::publish_decorations(world.clone(), file_id) { | ||
401 | Err(e) => { | ||
402 | error!("failed to compute decorations: {:?}", e) | ||
403 | } | ||
404 | Ok(params) => { | ||
405 | let not = RawNotification::new::<req::PublishDecorations>(¶ms); | ||
406 | sender.send(Task::Notify(not)) | ||
407 | } | ||
408 | } | ||
409 | } | ||
410 | }); | ||
411 | } | ||
412 | |||
413 | fn feedback(intrnal_mode: bool, msg: &str, sender: &Sender<RawMessage>) { | ||
414 | if !intrnal_mode { | ||
415 | return; | ||
416 | } | ||
417 | let not = RawNotification::new::<req::InternalFeedback>(&msg.to_string()); | ||
418 | sender.send(RawMessage::Notification(not)); | ||
419 | } | ||
diff --git a/crates/ra_lsp_server/src/main_loop/subscriptions.rs b/crates/ra_lsp_server/src/main_loop/subscriptions.rs new file mode 100644 index 000000000..27f92cc9a --- /dev/null +++ b/crates/ra_lsp_server/src/main_loop/subscriptions.rs | |||
@@ -0,0 +1,21 @@ | |||
1 | use std::collections::HashSet; | ||
2 | use ra_analysis::FileId; | ||
3 | |||
4 | pub struct Subscriptions { | ||
5 | subs: HashSet<FileId>, | ||
6 | } | ||
7 | |||
8 | impl Subscriptions { | ||
9 | pub fn new() -> Subscriptions { | ||
10 | Subscriptions { subs: HashSet::new() } | ||
11 | } | ||
12 | pub fn add_sub(&mut self, file_id: FileId) { | ||
13 | self.subs.insert(file_id); | ||
14 | } | ||
15 | pub fn remove_sub(&mut self, file_id: FileId) { | ||
16 | self.subs.remove(&file_id); | ||
17 | } | ||
18 | pub fn subscriptions(&self) -> Vec<FileId> { | ||
19 | self.subs.iter().cloned().collect() | ||
20 | } | ||
21 | } | ||
diff --git a/crates/ra_lsp_server/src/path_map.rs b/crates/ra_lsp_server/src/path_map.rs new file mode 100644 index 000000000..19c3b1d3b --- /dev/null +++ b/crates/ra_lsp_server/src/path_map.rs | |||
@@ -0,0 +1,110 @@ | |||
1 | use std::path::{PathBuf, Path, Component}; | ||
2 | use im; | ||
3 | use relative_path::RelativePath; | ||
4 | use ra_analysis::{FileId, FileResolver}; | ||
5 | |||
6 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
7 | pub enum Root { | ||
8 | Workspace, Lib | ||
9 | } | ||
10 | |||
11 | #[derive(Debug, Default, Clone)] | ||
12 | pub struct PathMap { | ||
13 | next_id: u32, | ||
14 | path2id: im::HashMap<PathBuf, FileId>, | ||
15 | id2path: im::HashMap<FileId, PathBuf>, | ||
16 | id2root: im::HashMap<FileId, Root>, | ||
17 | } | ||
18 | |||
19 | impl PathMap { | ||
20 | pub fn new() -> PathMap { | ||
21 | Default::default() | ||
22 | } | ||
23 | pub fn get_or_insert(&mut self, path: PathBuf, root: Root) -> FileId { | ||
24 | self.path2id.get(path.as_path()) | ||
25 | .map(|&id| id) | ||
26 | .unwrap_or_else(|| { | ||
27 | let id = self.new_file_id(); | ||
28 | self.insert(path, id, root); | ||
29 | id | ||
30 | }) | ||
31 | } | ||
32 | pub fn get_id(&self, path: &Path) -> Option<FileId> { | ||
33 | self.path2id.get(path).map(|&id| id) | ||
34 | } | ||
35 | pub fn get_path(&self, file_id: FileId) -> &Path { | ||
36 | self.id2path.get(&file_id) | ||
37 | .unwrap() | ||
38 | .as_path() | ||
39 | } | ||
40 | pub fn get_root(&self, file_id: FileId) -> Root { | ||
41 | self.id2root[&file_id] | ||
42 | } | ||
43 | fn insert(&mut self, path: PathBuf, file_id: FileId, root: Root) { | ||
44 | self.path2id.insert(path.clone(), file_id); | ||
45 | self.id2path.insert(file_id, path.clone()); | ||
46 | self.id2root.insert(file_id, root); | ||
47 | } | ||
48 | |||
49 | fn new_file_id(&mut self) -> FileId { | ||
50 | let id = FileId(self.next_id); | ||
51 | self.next_id += 1; | ||
52 | id | ||
53 | } | ||
54 | } | ||
55 | |||
56 | impl FileResolver for PathMap { | ||
57 | fn file_stem(&self, file_id: FileId) -> String { | ||
58 | self.get_path(file_id).file_stem().unwrap().to_str().unwrap().to_string() | ||
59 | } | ||
60 | |||
61 | fn resolve(&self, file_id: FileId, path: &RelativePath) -> Option<FileId> { | ||
62 | let path = path.to_path(&self.get_path(file_id)); | ||
63 | let path = normalize(&path); | ||
64 | self.get_id(&path) | ||
65 | } | ||
66 | } | ||
67 | |||
68 | fn normalize(path: &Path) -> PathBuf { | ||
69 | let mut components = path.components().peekable(); | ||
70 | let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { | ||
71 | components.next(); | ||
72 | PathBuf::from(c.as_os_str()) | ||
73 | } else { | ||
74 | PathBuf::new() | ||
75 | }; | ||
76 | |||
77 | for component in components { | ||
78 | match component { | ||
79 | Component::Prefix(..) => unreachable!(), | ||
80 | Component::RootDir => { | ||
81 | ret.push(component.as_os_str()); | ||
82 | } | ||
83 | Component::CurDir => {} | ||
84 | Component::ParentDir => { | ||
85 | ret.pop(); | ||
86 | } | ||
87 | Component::Normal(c) => { | ||
88 | ret.push(c); | ||
89 | } | ||
90 | } | ||
91 | } | ||
92 | ret | ||
93 | } | ||
94 | |||
95 | #[cfg(test)] | ||
96 | mod test { | ||
97 | use super::*; | ||
98 | |||
99 | #[test] | ||
100 | fn test_resolve() { | ||
101 | let mut m = PathMap::new(); | ||
102 | let id1 = m.get_or_insert(PathBuf::from("/foo"), Root::Workspace); | ||
103 | let id2 = m.get_or_insert(PathBuf::from("/foo/bar.rs"), Root::Workspace); | ||
104 | assert_eq!( | ||
105 | m.resolve(id1, &RelativePath::new("bar.rs")), | ||
106 | Some(id2), | ||
107 | ) | ||
108 | } | ||
109 | } | ||
110 | |||
diff --git a/crates/ra_lsp_server/src/project_model.rs b/crates/ra_lsp_server/src/project_model.rs new file mode 100644 index 000000000..5db34e3e5 --- /dev/null +++ b/crates/ra_lsp_server/src/project_model.rs | |||
@@ -0,0 +1,175 @@ | |||
1 | use std::{ | ||
2 | collections::{HashMap, HashSet}, | ||
3 | path::{Path, PathBuf}, | ||
4 | }; | ||
5 | use cargo_metadata::{metadata_run, CargoOpt}; | ||
6 | use ra_syntax::SmolStr; | ||
7 | |||
8 | use { | ||
9 | Result, | ||
10 | thread_watcher::{Worker, ThreadWatcher}, | ||
11 | }; | ||
12 | |||
13 | #[derive(Debug, Clone)] | ||
14 | pub struct CargoWorkspace { | ||
15 | packages: Vec<PackageData>, | ||
16 | targets: Vec<TargetData>, | ||
17 | } | ||
18 | |||
19 | #[derive(Clone, Copy, Debug, Serialize)] | ||
20 | pub struct Package(usize); | ||
21 | #[derive(Clone, Copy, Debug, Serialize)] | ||
22 | pub struct Target(usize); | ||
23 | |||
24 | #[derive(Debug, Clone)] | ||
25 | struct PackageData { | ||
26 | name: SmolStr, | ||
27 | manifest: PathBuf, | ||
28 | targets: Vec<Target>, | ||
29 | is_member: bool, | ||
30 | } | ||
31 | |||
32 | #[derive(Debug, Clone)] | ||
33 | struct TargetData { | ||
34 | pkg: Package, | ||
35 | name: SmolStr, | ||
36 | root: PathBuf, | ||
37 | kind: TargetKind, | ||
38 | } | ||
39 | |||
40 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||
41 | pub enum TargetKind { | ||
42 | Bin, Lib, Example, Test, Bench, Other, | ||
43 | } | ||
44 | |||
45 | impl Package { | ||
46 | pub fn name(self, ws: &CargoWorkspace) -> &str { | ||
47 | ws.pkg(self).name.as_str() | ||
48 | } | ||
49 | pub fn root(self, ws: &CargoWorkspace) -> &Path { | ||
50 | ws.pkg(self).manifest.parent().unwrap() | ||
51 | } | ||
52 | pub fn targets<'a>(self, ws: &'a CargoWorkspace) -> impl Iterator<Item=Target> + 'a { | ||
53 | ws.pkg(self).targets.iter().cloned() | ||
54 | } | ||
55 | pub fn is_member(self, ws: &CargoWorkspace) -> bool { | ||
56 | ws.pkg(self).is_member | ||
57 | } | ||
58 | } | ||
59 | |||
60 | impl Target { | ||
61 | pub fn package(self, ws: &CargoWorkspace) -> Package { | ||
62 | ws.tgt(self).pkg | ||
63 | } | ||
64 | pub fn name(self, ws: &CargoWorkspace) -> &str { | ||
65 | ws.tgt(self).name.as_str() | ||
66 | } | ||
67 | pub fn root(self, ws: &CargoWorkspace) -> &Path { | ||
68 | ws.tgt(self).root.as_path() | ||
69 | } | ||
70 | pub fn kind(self, ws: &CargoWorkspace) -> TargetKind { | ||
71 | ws.tgt(self).kind | ||
72 | } | ||
73 | } | ||
74 | |||
75 | impl CargoWorkspace { | ||
76 | pub fn from_cargo_metadata(path: &Path) -> Result<CargoWorkspace> { | ||
77 | let cargo_toml = find_cargo_toml(path)?; | ||
78 | let meta = metadata_run( | ||
79 | Some(cargo_toml.as_path()), | ||
80 | true, | ||
81 | Some(CargoOpt::AllFeatures) | ||
82 | ).map_err(|e| format_err!("cargo metadata failed: {}", e))?; | ||
83 | let mut pkg_by_id = HashMap::new(); | ||
84 | let mut packages = Vec::new(); | ||
85 | let mut targets = Vec::new(); | ||
86 | |||
87 | let ws_members: HashSet<String> = meta.workspace_members | ||
88 | .into_iter() | ||
89 | .map(|it| it.raw) | ||
90 | .collect(); | ||
91 | |||
92 | for meta_pkg in meta.packages { | ||
93 | let pkg = Package(packages.len()); | ||
94 | let is_member = ws_members.contains(&meta_pkg.id); | ||
95 | pkg_by_id.insert(meta_pkg.id.clone(), pkg); | ||
96 | let mut pkg_data = PackageData { | ||
97 | name: meta_pkg.name.into(), | ||
98 | manifest: PathBuf::from(meta_pkg.manifest_path), | ||
99 | targets: Vec::new(), | ||
100 | is_member, | ||
101 | }; | ||
102 | for meta_tgt in meta_pkg.targets { | ||
103 | let tgt = Target(targets.len()); | ||
104 | targets.push(TargetData { | ||
105 | pkg, | ||
106 | name: meta_tgt.name.into(), | ||
107 | root: PathBuf::from(meta_tgt.src_path), | ||
108 | kind: TargetKind::new(meta_tgt.kind.as_slice()), | ||
109 | }); | ||
110 | pkg_data.targets.push(tgt); | ||
111 | } | ||
112 | packages.push(pkg_data) | ||
113 | } | ||
114 | |||
115 | Ok(CargoWorkspace { packages, targets }) | ||
116 | } | ||
117 | pub fn packages<'a>(&'a self) -> impl Iterator<Item=Package> + 'a { | ||
118 | (0..self.packages.len()).map(Package) | ||
119 | } | ||
120 | pub fn target_by_root(&self, root: &Path) -> Option<Target> { | ||
121 | self.packages() | ||
122 | .filter_map(|pkg| pkg.targets(self).find(|it| it.root(self) == root)) | ||
123 | .next() | ||
124 | } | ||
125 | fn pkg(&self, pkg: Package) -> &PackageData { | ||
126 | &self.packages[pkg.0] | ||
127 | } | ||
128 | fn tgt(&self, tgt: Target) -> &TargetData { | ||
129 | &self.targets[tgt.0] | ||
130 | } | ||
131 | } | ||
132 | |||
133 | fn find_cargo_toml(path: &Path) -> Result<PathBuf> { | ||
134 | if path.ends_with("Cargo.toml") { | ||
135 | return Ok(path.to_path_buf()); | ||
136 | } | ||
137 | let mut curr = Some(path); | ||
138 | while let Some(path) = curr { | ||
139 | let candidate = path.join("Cargo.toml"); | ||
140 | if candidate.exists() { | ||
141 | return Ok(candidate); | ||
142 | } | ||
143 | curr = path.parent(); | ||
144 | } | ||
145 | bail!("can't find Cargo.toml at {}", path.display()) | ||
146 | } | ||
147 | |||
148 | impl TargetKind { | ||
149 | fn new(kinds: &[String]) -> TargetKind { | ||
150 | for kind in kinds { | ||
151 | return match kind.as_str() { | ||
152 | "bin" => TargetKind::Bin, | ||
153 | "test" => TargetKind::Test, | ||
154 | "bench" => TargetKind::Bench, | ||
155 | "example" => TargetKind::Example, | ||
156 | _ if kind.contains("lib") => TargetKind::Lib, | ||
157 | _ => continue, | ||
158 | } | ||
159 | } | ||
160 | TargetKind::Other | ||
161 | } | ||
162 | } | ||
163 | |||
164 | pub fn workspace_loader() -> (Worker<PathBuf, Result<CargoWorkspace>>, ThreadWatcher) { | ||
165 | Worker::<PathBuf, Result<CargoWorkspace>>::spawn( | ||
166 | "workspace loader", | ||
167 | 1, | ||
168 | |input_receiver, output_sender| { | ||
169 | input_receiver | ||
170 | .into_iter() | ||
171 | .map(|path| CargoWorkspace::from_cargo_metadata(path.as_path())) | ||
172 | .for_each(|it| output_sender.send(it)) | ||
173 | } | ||
174 | ) | ||
175 | } | ||
diff --git a/crates/ra_lsp_server/src/req.rs b/crates/ra_lsp_server/src/req.rs new file mode 100644 index 000000000..4af61dbbd --- /dev/null +++ b/crates/ra_lsp_server/src/req.rs | |||
@@ -0,0 +1,176 @@ | |||
1 | use std::collections::HashMap; | ||
2 | use languageserver_types::{TextDocumentIdentifier, Range, Url, Position, Location}; | ||
3 | use url_serde; | ||
4 | |||
5 | pub use languageserver_types::{ | ||
6 | request::*, notification::*, | ||
7 | InitializeResult, PublishDiagnosticsParams, | ||
8 | DocumentSymbolParams, DocumentSymbolResponse, | ||
9 | CodeActionParams, ApplyWorkspaceEditParams, | ||
10 | ExecuteCommandParams, | ||
11 | WorkspaceSymbolParams, | ||
12 | TextDocumentPositionParams, | ||
13 | TextEdit, | ||
14 | CompletionParams, CompletionResponse, | ||
15 | DocumentOnTypeFormattingParams, | ||
16 | TextDocumentEdit, | ||
17 | }; | ||
18 | |||
19 | pub enum SyntaxTree {} | ||
20 | |||
21 | impl Request for SyntaxTree { | ||
22 | type Params = SyntaxTreeParams; | ||
23 | type Result = String; | ||
24 | const METHOD: &'static str = "m/syntaxTree"; | ||
25 | } | ||
26 | |||
27 | #[derive(Deserialize, Debug)] | ||
28 | #[serde(rename_all = "camelCase")] | ||
29 | pub struct SyntaxTreeParams { | ||
30 | pub text_document: TextDocumentIdentifier | ||
31 | } | ||
32 | |||
33 | pub enum ExtendSelection {} | ||
34 | |||
35 | impl Request for ExtendSelection { | ||
36 | type Params = ExtendSelectionParams; | ||
37 | type Result = ExtendSelectionResult; | ||
38 | const METHOD: &'static str = "m/extendSelection"; | ||
39 | } | ||
40 | |||
41 | #[derive(Deserialize, Debug)] | ||
42 | #[serde(rename_all = "camelCase")] | ||
43 | pub struct ExtendSelectionParams { | ||
44 | pub text_document: TextDocumentIdentifier, | ||
45 | pub selections: Vec<Range>, | ||
46 | } | ||
47 | |||
48 | #[derive(Serialize, Debug)] | ||
49 | #[serde(rename_all = "camelCase")] | ||
50 | pub struct ExtendSelectionResult { | ||
51 | pub selections: Vec<Range>, | ||
52 | } | ||
53 | |||
54 | pub enum FindMatchingBrace {} | ||
55 | |||
56 | impl Request for FindMatchingBrace { | ||
57 | type Params = FindMatchingBraceParams; | ||
58 | type Result = Vec<Position>; | ||
59 | const METHOD: &'static str = "m/findMatchingBrace"; | ||
60 | } | ||
61 | |||
62 | #[derive(Deserialize, Debug)] | ||
63 | #[serde(rename_all = "camelCase")] | ||
64 | pub struct FindMatchingBraceParams { | ||
65 | pub text_document: TextDocumentIdentifier, | ||
66 | pub offsets: Vec<Position>, | ||
67 | } | ||
68 | |||
69 | pub enum DecorationsRequest {} | ||
70 | |||
71 | impl Request for DecorationsRequest { | ||
72 | type Params = TextDocumentIdentifier; | ||
73 | type Result = Vec<Decoration>; | ||
74 | const METHOD: &'static str = "m/decorationsRequest"; | ||
75 | } | ||
76 | |||
77 | pub enum PublishDecorations {} | ||
78 | |||
79 | impl Notification for PublishDecorations { | ||
80 | type Params = PublishDecorationsParams; | ||
81 | const METHOD: &'static str = "m/publishDecorations"; | ||
82 | } | ||
83 | |||
84 | #[derive(Serialize, Debug)] | ||
85 | #[serde(rename_all = "camelCase")] | ||
86 | pub struct PublishDecorationsParams { | ||
87 | #[serde(with = "url_serde")] | ||
88 | pub uri: Url, | ||
89 | pub decorations: Vec<Decoration>, | ||
90 | } | ||
91 | |||
92 | #[derive(Serialize, Debug)] | ||
93 | #[serde(rename_all = "camelCase")] | ||
94 | pub struct Decoration { | ||
95 | pub range: Range, | ||
96 | pub tag: &'static str | ||
97 | } | ||
98 | |||
99 | pub enum ParentModule {} | ||
100 | |||
101 | impl Request for ParentModule { | ||
102 | type Params = TextDocumentIdentifier; | ||
103 | type Result = Vec<Location>; | ||
104 | const METHOD: &'static str = "m/parentModule"; | ||
105 | } | ||
106 | |||
107 | pub enum JoinLines {} | ||
108 | |||
109 | impl Request for JoinLines { | ||
110 | type Params = JoinLinesParams; | ||
111 | type Result = SourceChange; | ||
112 | const METHOD: &'static str = "m/joinLines"; | ||
113 | } | ||
114 | |||
115 | #[derive(Deserialize, Debug)] | ||
116 | #[serde(rename_all = "camelCase")] | ||
117 | pub struct JoinLinesParams { | ||
118 | pub text_document: TextDocumentIdentifier, | ||
119 | pub range: Range, | ||
120 | } | ||
121 | |||
122 | pub enum Runnables {} | ||
123 | |||
124 | impl Request for Runnables { | ||
125 | type Params = RunnablesParams; | ||
126 | type Result = Vec<Runnable>; | ||
127 | const METHOD: &'static str = "m/runnables"; | ||
128 | } | ||
129 | |||
130 | #[derive(Serialize, Deserialize, Debug)] | ||
131 | #[serde(rename_all = "camelCase")] | ||
132 | pub struct RunnablesParams { | ||
133 | pub text_document: TextDocumentIdentifier, | ||
134 | pub position: Option<Position>, | ||
135 | } | ||
136 | |||
137 | #[derive(Serialize, Debug)] | ||
138 | #[serde(rename_all = "camelCase")] | ||
139 | pub struct Runnable { | ||
140 | pub range: Range, | ||
141 | pub label: String, | ||
142 | pub bin: String, | ||
143 | pub args: Vec<String>, | ||
144 | pub env: HashMap<String, String>, | ||
145 | } | ||
146 | |||
147 | #[derive(Serialize, Debug)] | ||
148 | #[serde(rename_all = "camelCase")] | ||
149 | pub struct SourceChange { | ||
150 | pub label: String, | ||
151 | pub source_file_edits: Vec<TextDocumentEdit>, | ||
152 | pub file_system_edits: Vec<FileSystemEdit>, | ||
153 | pub cursor_position: Option<TextDocumentPositionParams>, | ||
154 | } | ||
155 | |||
156 | #[derive(Serialize, Debug)] | ||
157 | #[serde(tag = "type", rename_all = "camelCase")] | ||
158 | pub enum FileSystemEdit { | ||
159 | CreateFile { | ||
160 | #[serde(with = "url_serde")] | ||
161 | uri: Url | ||
162 | }, | ||
163 | MoveFile { | ||
164 | #[serde(with = "url_serde")] | ||
165 | src: Url, | ||
166 | #[serde(with = "url_serde")] | ||
167 | dst: Url, | ||
168 | } | ||
169 | } | ||
170 | |||
171 | pub enum InternalFeedback {} | ||
172 | |||
173 | impl Notification for InternalFeedback { | ||
174 | const METHOD: &'static str = "internalFeedback"; | ||
175 | type Params = String; | ||
176 | } | ||
diff --git a/crates/ra_lsp_server/src/server_world.rs b/crates/ra_lsp_server/src/server_world.rs new file mode 100644 index 000000000..865f7c491 --- /dev/null +++ b/crates/ra_lsp_server/src/server_world.rs | |||
@@ -0,0 +1,167 @@ | |||
1 | use std::{ | ||
2 | fs, | ||
3 | path::{PathBuf, Path}, | ||
4 | collections::HashMap, | ||
5 | sync::Arc, | ||
6 | }; | ||
7 | |||
8 | use languageserver_types::Url; | ||
9 | use ra_analysis::{FileId, AnalysisHost, Analysis, CrateGraph, CrateId, LibraryData, FileResolver}; | ||
10 | |||
11 | use { | ||
12 | Result, | ||
13 | path_map::{PathMap, Root}, | ||
14 | vfs::{FileEvent, FileEventKind}, | ||
15 | project_model::CargoWorkspace, | ||
16 | }; | ||
17 | |||
18 | #[derive(Debug)] | ||
19 | pub struct ServerWorldState { | ||
20 | pub workspaces: Arc<Vec<CargoWorkspace>>, | ||
21 | pub analysis_host: AnalysisHost, | ||
22 | pub path_map: PathMap, | ||
23 | pub mem_map: HashMap<FileId, Option<String>>, | ||
24 | } | ||
25 | |||
26 | #[derive(Clone)] | ||
27 | pub struct ServerWorld { | ||
28 | pub workspaces: Arc<Vec<CargoWorkspace>>, | ||
29 | pub analysis: Analysis, | ||
30 | pub path_map: PathMap, | ||
31 | } | ||
32 | |||
33 | impl ServerWorldState { | ||
34 | pub fn new() -> ServerWorldState { | ||
35 | ServerWorldState { | ||
36 | workspaces: Arc::new(Vec::new()), | ||
37 | analysis_host: AnalysisHost::new(), | ||
38 | path_map: PathMap::new(), | ||
39 | mem_map: HashMap::new(), | ||
40 | } | ||
41 | } | ||
42 | pub fn apply_fs_changes(&mut self, events: Vec<FileEvent>) { | ||
43 | { | ||
44 | let pm = &mut self.path_map; | ||
45 | let mm = &mut self.mem_map; | ||
46 | let changes = events.into_iter() | ||
47 | .map(|event| { | ||
48 | let text = match event.kind { | ||
49 | FileEventKind::Add(text) => Some(text), | ||
50 | }; | ||
51 | (event.path, text) | ||
52 | }) | ||
53 | .map(|(path, text)| { | ||
54 | (pm.get_or_insert(path, Root::Workspace), text) | ||
55 | }) | ||
56 | .filter_map(|(id, text)| { | ||
57 | if mm.contains_key(&id) { | ||
58 | mm.insert(id, text); | ||
59 | None | ||
60 | } else { | ||
61 | Some((id, text)) | ||
62 | } | ||
63 | }); | ||
64 | self.analysis_host.change_files(changes); | ||
65 | } | ||
66 | self.analysis_host.set_file_resolver(Arc::new(self.path_map.clone())); | ||
67 | } | ||
68 | pub fn events_to_files(&mut self, events: Vec<FileEvent>) -> (Vec<(FileId, String)>, Arc<FileResolver>) { | ||
69 | let files = { | ||
70 | let pm = &mut self.path_map; | ||
71 | events.into_iter() | ||
72 | .map(|event| { | ||
73 | let text = match event.kind { | ||
74 | FileEventKind::Add(text) => text, | ||
75 | }; | ||
76 | (event.path, text) | ||
77 | }) | ||
78 | .map(|(path, text)| (pm.get_or_insert(path, Root::Lib), text)) | ||
79 | .collect() | ||
80 | }; | ||
81 | let resolver = Arc::new(self.path_map.clone()); | ||
82 | (files, resolver) | ||
83 | } | ||
84 | pub fn add_lib(&mut self, data: LibraryData) { | ||
85 | self.analysis_host.add_library(data); | ||
86 | } | ||
87 | |||
88 | pub fn add_mem_file(&mut self, path: PathBuf, text: String) -> FileId { | ||
89 | let file_id = self.path_map.get_or_insert(path, Root::Workspace); | ||
90 | self.analysis_host.set_file_resolver(Arc::new(self.path_map.clone())); | ||
91 | self.mem_map.insert(file_id, None); | ||
92 | if self.path_map.get_root(file_id) != Root::Lib { | ||
93 | self.analysis_host.change_file(file_id, Some(text)); | ||
94 | } | ||
95 | file_id | ||
96 | } | ||
97 | |||
98 | pub fn change_mem_file(&mut self, path: &Path, text: String) -> Result<()> { | ||
99 | let file_id = self.path_map.get_id(path).ok_or_else(|| { | ||
100 | format_err!("change to unknown file: {}", path.display()) | ||
101 | })?; | ||
102 | if self.path_map.get_root(file_id) != Root::Lib { | ||
103 | self.analysis_host.change_file(file_id, Some(text)); | ||
104 | } | ||
105 | Ok(()) | ||
106 | } | ||
107 | |||
108 | pub fn remove_mem_file(&mut self, path: &Path) -> Result<FileId> { | ||
109 | let file_id = self.path_map.get_id(path).ok_or_else(|| { | ||
110 | format_err!("change to unknown file: {}", path.display()) | ||
111 | })?; | ||
112 | match self.mem_map.remove(&file_id) { | ||
113 | Some(_) => (), | ||
114 | None => bail!("unmatched close notification"), | ||
115 | }; | ||
116 | // Do this via file watcher ideally. | ||
117 | let text = fs::read_to_string(path).ok(); | ||
118 | if self.path_map.get_root(file_id) != Root::Lib { | ||
119 | self.analysis_host.change_file(file_id, text); | ||
120 | } | ||
121 | Ok(file_id) | ||
122 | } | ||
123 | pub fn set_workspaces(&mut self, ws: Vec<CargoWorkspace>) { | ||
124 | let mut crate_roots = HashMap::new(); | ||
125 | ws.iter() | ||
126 | .flat_map(|ws| { | ||
127 | ws.packages() | ||
128 | .flat_map(move |pkg| pkg.targets(ws)) | ||
129 | .map(move |tgt| tgt.root(ws)) | ||
130 | }) | ||
131 | .for_each(|root| { | ||
132 | if let Some(file_id) = self.path_map.get_id(root) { | ||
133 | let crate_id = CrateId(crate_roots.len() as u32); | ||
134 | crate_roots.insert(crate_id, file_id); | ||
135 | } | ||
136 | }); | ||
137 | let crate_graph = CrateGraph { crate_roots }; | ||
138 | self.workspaces = Arc::new(ws); | ||
139 | self.analysis_host.set_crate_graph(crate_graph); | ||
140 | } | ||
141 | pub fn snapshot(&self) -> ServerWorld { | ||
142 | ServerWorld { | ||
143 | workspaces: Arc::clone(&self.workspaces), | ||
144 | analysis: self.analysis_host.analysis(), | ||
145 | path_map: self.path_map.clone() | ||
146 | } | ||
147 | } | ||
148 | } | ||
149 | |||
150 | impl ServerWorld { | ||
151 | pub fn analysis(&self) -> &Analysis { | ||
152 | &self.analysis | ||
153 | } | ||
154 | |||
155 | pub fn uri_to_file_id(&self, uri: &Url) -> Result<FileId> { | ||
156 | let path = uri.to_file_path() | ||
157 | .map_err(|()| format_err!("invalid uri: {}", uri))?; | ||
158 | self.path_map.get_id(&path).ok_or_else(|| format_err!("unknown file: {}", path.display())) | ||
159 | } | ||
160 | |||
161 | pub fn file_id_to_uri(&self, id: FileId) -> Result<Url> { | ||
162 | let path = self.path_map.get_path(id); | ||
163 | let url = Url::from_file_path(path) | ||
164 | .map_err(|()| format_err!("can't convert path to url: {}", path.display()))?; | ||
165 | Ok(url) | ||
166 | } | ||
167 | } | ||
diff --git a/crates/ra_lsp_server/src/thread_watcher.rs b/crates/ra_lsp_server/src/thread_watcher.rs new file mode 100644 index 000000000..86a3a91e0 --- /dev/null +++ b/crates/ra_lsp_server/src/thread_watcher.rs | |||
@@ -0,0 +1,70 @@ | |||
1 | use std::thread; | ||
2 | use crossbeam_channel::{bounded, unbounded, Sender, Receiver}; | ||
3 | use drop_bomb::DropBomb; | ||
4 | use Result; | ||
5 | |||
6 | pub struct Worker<I, O> { | ||
7 | pub inp: Sender<I>, | ||
8 | pub out: Receiver<O>, | ||
9 | } | ||
10 | |||
11 | impl<I, O> Worker<I, O> { | ||
12 | pub fn spawn<F>(name: &'static str, buf: usize, f: F) -> (Self, ThreadWatcher) | ||
13 | where | ||
14 | F: FnOnce(Receiver<I>, Sender<O>) + Send + 'static, | ||
15 | I: Send + 'static, | ||
16 | O: Send + 'static, | ||
17 | { | ||
18 | let ((inp, out), inp_r, out_s) = worker_chan(buf); | ||
19 | let worker = Worker { inp, out }; | ||
20 | let watcher = ThreadWatcher::spawn(name, move || f(inp_r, out_s)); | ||
21 | (worker, watcher) | ||
22 | } | ||
23 | |||
24 | pub fn stop(self) -> Receiver<O> { | ||
25 | self.out | ||
26 | } | ||
27 | |||
28 | pub fn send(&self, item: I) { | ||
29 | self.inp.send(item) | ||
30 | } | ||
31 | } | ||
32 | |||
33 | pub struct ThreadWatcher { | ||
34 | name: &'static str, | ||
35 | thread: thread::JoinHandle<()>, | ||
36 | bomb: DropBomb, | ||
37 | } | ||
38 | |||
39 | impl ThreadWatcher { | ||
40 | fn spawn(name: &'static str, f: impl FnOnce() + Send + 'static) -> ThreadWatcher { | ||
41 | let thread = thread::spawn(f); | ||
42 | ThreadWatcher { | ||
43 | name, | ||
44 | thread, | ||
45 | bomb: DropBomb::new(format!("ThreadWatcher {} was not stopped", name)), | ||
46 | } | ||
47 | } | ||
48 | |||
49 | pub fn stop(mut self) -> Result<()> { | ||
50 | info!("waiting for {} to finish ...", self.name); | ||
51 | let name = self.name; | ||
52 | self.bomb.defuse(); | ||
53 | let res = self.thread.join() | ||
54 | .map_err(|_| format_err!("ThreadWatcher {} died", name)); | ||
55 | match &res { | ||
56 | Ok(()) => info!("... {} terminated with ok", name), | ||
57 | Err(_) => error!("... {} terminated with err", name) | ||
58 | } | ||
59 | res | ||
60 | } | ||
61 | } | ||
62 | |||
63 | /// Sets up worker channels in a deadlock-avoind way. | ||
64 | /// If one sets both input and output buffers to a fixed size, | ||
65 | /// a worker might get stuck. | ||
66 | fn worker_chan<I, O>(buf: usize) -> ((Sender<I>, Receiver<O>), Receiver<I>, Sender<O>) { | ||
67 | let (input_sender, input_receiver) = bounded::<I>(buf); | ||
68 | let (output_sender, output_receiver) = unbounded::<O>(); | ||
69 | ((input_sender, output_receiver), input_receiver, output_sender) | ||
70 | } | ||
diff --git a/crates/ra_lsp_server/src/vfs.rs b/crates/ra_lsp_server/src/vfs.rs new file mode 100644 index 000000000..a1c1783f2 --- /dev/null +++ b/crates/ra_lsp_server/src/vfs.rs | |||
@@ -0,0 +1,71 @@ | |||
1 | use std::{ | ||
2 | path::{PathBuf, Path}, | ||
3 | fs, | ||
4 | }; | ||
5 | |||
6 | use walkdir::WalkDir; | ||
7 | |||
8 | use { | ||
9 | thread_watcher::{Worker, ThreadWatcher}, | ||
10 | }; | ||
11 | |||
12 | |||
13 | #[derive(Debug)] | ||
14 | pub struct FileEvent { | ||
15 | pub path: PathBuf, | ||
16 | pub kind: FileEventKind, | ||
17 | } | ||
18 | |||
19 | #[derive(Debug)] | ||
20 | pub enum FileEventKind { | ||
21 | Add(String), | ||
22 | } | ||
23 | |||
24 | pub fn roots_loader() -> (Worker<PathBuf, (PathBuf, Vec<FileEvent>)>, ThreadWatcher) { | ||
25 | Worker::<PathBuf, (PathBuf, Vec<FileEvent>)>::spawn( | ||
26 | "roots loader", | ||
27 | 128, |input_receiver, output_sender| { | ||
28 | input_receiver | ||
29 | .into_iter() | ||
30 | .map(|path| { | ||
31 | debug!("loading {} ...", path.as_path().display()); | ||
32 | let events = load_root(path.as_path()); | ||
33 | debug!("... loaded {}", path.as_path().display()); | ||
34 | (path, events) | ||
35 | }) | ||
36 | .for_each(|it| output_sender.send(it)) | ||
37 | } | ||
38 | ) | ||
39 | } | ||
40 | |||
41 | fn load_root(path: &Path) -> Vec<FileEvent> { | ||
42 | let mut res = Vec::new(); | ||
43 | for entry in WalkDir::new(path) { | ||
44 | let entry = match entry { | ||
45 | Ok(entry) => entry, | ||
46 | Err(e) => { | ||
47 | warn!("watcher error: {}", e); | ||
48 | continue; | ||
49 | } | ||
50 | }; | ||
51 | if !entry.file_type().is_file() { | ||
52 | continue; | ||
53 | } | ||
54 | let path = entry.path(); | ||
55 | if path.extension().and_then(|os| os.to_str()) != Some("rs") { | ||
56 | continue; | ||
57 | } | ||
58 | let text = match fs::read_to_string(path) { | ||
59 | Ok(text) => text, | ||
60 | Err(e) => { | ||
61 | warn!("watcher error: {}", e); | ||
62 | continue; | ||
63 | } | ||
64 | }; | ||
65 | res.push(FileEvent { | ||
66 | path: path.to_owned(), | ||
67 | kind: FileEventKind::Add(text), | ||
68 | }) | ||
69 | } | ||
70 | res | ||
71 | } | ||
diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs new file mode 100644 index 000000000..dced45f55 --- /dev/null +++ b/crates/ra_lsp_server/tests/heavy_tests/main.rs | |||
@@ -0,0 +1,99 @@ | |||
1 | #[macro_use] | ||
2 | extern crate crossbeam_channel; | ||
3 | extern crate tempdir; | ||
4 | extern crate languageserver_types; | ||
5 | extern crate serde; | ||
6 | extern crate serde_json; | ||
7 | extern crate gen_lsp_server; | ||
8 | extern crate flexi_logger; | ||
9 | extern crate ra_lsp_server; | ||
10 | |||
11 | mod support; | ||
12 | |||
13 | use ra_lsp_server::req::{Runnables, RunnablesParams}; | ||
14 | |||
15 | use support::project; | ||
16 | |||
17 | |||
18 | const LOG: &'static str = ""; | ||
19 | |||
20 | #[test] | ||
21 | fn test_runnables_no_project() { | ||
22 | let server = project(r" | ||
23 | //- lib.rs | ||
24 | #[test] | ||
25 | fn foo() { | ||
26 | } | ||
27 | "); | ||
28 | server.request::<Runnables>( | ||
29 | RunnablesParams { | ||
30 | text_document: server.doc_id("lib.rs"), | ||
31 | position: None, | ||
32 | }, | ||
33 | r#"[ | ||
34 | { | ||
35 | "args": [ "test", "--", "foo", "--nocapture" ], | ||
36 | "bin": "cargo", | ||
37 | "env": { "RUST_BACKTRACE": "short" }, | ||
38 | "label": "test foo", | ||
39 | "range": { | ||
40 | "end": { "character": 1, "line": 2 }, | ||
41 | "start": { "character": 0, "line": 0 } | ||
42 | } | ||
43 | } | ||
44 | ]"# | ||
45 | ); | ||
46 | } | ||
47 | |||
48 | #[test] | ||
49 | fn test_runnables_project() { | ||
50 | let server = project(r#" | ||
51 | //- Cargo.toml | ||
52 | [package] | ||
53 | name = "foo" | ||
54 | version = "0.0.0" | ||
55 | |||
56 | //- src/lib.rs | ||
57 | pub fn foo() {} | ||
58 | |||
59 | //- tests/spam.rs | ||
60 | #[test] | ||
61 | fn test_eggs() {} | ||
62 | "#); | ||
63 | server.wait_for_feedback("workspace loaded"); | ||
64 | server.request::<Runnables>( | ||
65 | RunnablesParams { | ||
66 | text_document: server.doc_id("tests/spam.rs"), | ||
67 | position: None, | ||
68 | }, | ||
69 | r#"[ | ||
70 | { | ||
71 | "args": [ "test", "--package", "foo", "--test", "spam", "--", "test_eggs", "--nocapture" ], | ||
72 | "bin": "cargo", | ||
73 | "env": { "RUST_BACKTRACE": "short" }, | ||
74 | "label": "test test_eggs", | ||
75 | "range": { | ||
76 | "end": { "character": 17, "line": 1 }, | ||
77 | "start": { "character": 0, "line": 0 } | ||
78 | } | ||
79 | } | ||
80 | ]"# | ||
81 | ); | ||
82 | } | ||
83 | |||
84 | // #[test] | ||
85 | // fn test_deps() { | ||
86 | // let server = project(r#" | ||
87 | // //- Cargo.toml | ||
88 | // [package] | ||
89 | // name = "foo" | ||
90 | // version = "0.0.0" | ||
91 | // [dependencies] | ||
92 | // regex = "=1.0.4" | ||
93 | |||
94 | // //- src/lib.rs | ||
95 | // extern crate regex; | ||
96 | // "#); | ||
97 | // server.wait_for_feedback("workspace loaded"); | ||
98 | // server.wait_for_feedback_n("library loaded", 9); | ||
99 | // } | ||
diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs new file mode 100644 index 000000000..8fe2aa816 --- /dev/null +++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs | |||
@@ -0,0 +1,217 @@ | |||
1 | use std::{ | ||
2 | fs, | ||
3 | cell::{Cell, RefCell}, | ||
4 | path::PathBuf, | ||
5 | time::Duration, | ||
6 | sync::Once, | ||
7 | }; | ||
8 | |||
9 | use tempdir::TempDir; | ||
10 | use crossbeam_channel::{after, Receiver}; | ||
11 | use flexi_logger::Logger; | ||
12 | use languageserver_types::{ | ||
13 | Url, | ||
14 | TextDocumentIdentifier, | ||
15 | request::{Request, Shutdown}, | ||
16 | notification::DidOpenTextDocument, | ||
17 | DidOpenTextDocumentParams, | ||
18 | TextDocumentItem, | ||
19 | }; | ||
20 | use serde::Serialize; | ||
21 | use serde_json::{Value, from_str, to_string_pretty}; | ||
22 | use gen_lsp_server::{RawMessage, RawRequest, RawNotification}; | ||
23 | |||
24 | use ra_lsp_server::{main_loop, req, thread_watcher::{ThreadWatcher, Worker}}; | ||
25 | |||
26 | pub fn project(fixture: &str) -> Server { | ||
27 | static INIT: Once = Once::new(); | ||
28 | INIT.call_once(|| Logger::with_env_or_str(::LOG).start().unwrap()); | ||
29 | |||
30 | let tmp_dir = TempDir::new("test-project") | ||
31 | .unwrap(); | ||
32 | let mut buf = String::new(); | ||
33 | let mut file_name = None; | ||
34 | let mut paths = vec![]; | ||
35 | macro_rules! flush { | ||
36 | () => { | ||
37 | if let Some(file_name) = file_name { | ||
38 | let path = tmp_dir.path().join(file_name); | ||
39 | fs::create_dir_all(path.parent().unwrap()).unwrap(); | ||
40 | fs::write(path.as_path(), buf.as_bytes()).unwrap(); | ||
41 | paths.push((path, buf.clone())); | ||
42 | } | ||
43 | } | ||
44 | }; | ||
45 | for line in fixture.lines() { | ||
46 | if line.starts_with("//-") { | ||
47 | flush!(); | ||
48 | buf.clear(); | ||
49 | file_name = Some(line["//-".len()..].trim()); | ||
50 | continue; | ||
51 | } | ||
52 | buf.push_str(line); | ||
53 | buf.push('\n'); | ||
54 | } | ||
55 | flush!(); | ||
56 | Server::new(tmp_dir, paths) | ||
57 | } | ||
58 | |||
59 | pub struct Server { | ||
60 | req_id: Cell<u64>, | ||
61 | messages: RefCell<Vec<RawMessage>>, | ||
62 | dir: TempDir, | ||
63 | worker: Option<Worker<RawMessage, RawMessage>>, | ||
64 | watcher: Option<ThreadWatcher>, | ||
65 | } | ||
66 | |||
67 | impl Server { | ||
68 | fn new(dir: TempDir, files: Vec<(PathBuf, String)>) -> Server { | ||
69 | let path = dir.path().to_path_buf(); | ||
70 | let (worker, watcher) = Worker::<RawMessage, RawMessage>::spawn( | ||
71 | "test server", | ||
72 | 128, | ||
73 | move |mut msg_receiver, mut msg_sender| { | ||
74 | main_loop(true, path, &mut msg_receiver, &mut msg_sender) | ||
75 | .unwrap() | ||
76 | } | ||
77 | ); | ||
78 | let res = Server { | ||
79 | req_id: Cell::new(1), | ||
80 | dir, | ||
81 | messages: Default::default(), | ||
82 | worker: Some(worker), | ||
83 | watcher: Some(watcher), | ||
84 | }; | ||
85 | |||
86 | for (path, text) in files { | ||
87 | res.send_notification(RawNotification::new::<DidOpenTextDocument>( | ||
88 | &DidOpenTextDocumentParams { | ||
89 | text_document: TextDocumentItem { | ||
90 | uri: Url::from_file_path(path).unwrap(), | ||
91 | language_id: "rust".to_string(), | ||
92 | version: 0, | ||
93 | text, | ||
94 | } | ||
95 | } | ||
96 | )) | ||
97 | } | ||
98 | res | ||
99 | } | ||
100 | |||
101 | pub fn doc_id(&self, rel_path: &str) -> TextDocumentIdentifier { | ||
102 | let path = self.dir.path().join(rel_path); | ||
103 | TextDocumentIdentifier { | ||
104 | uri: Url::from_file_path(path).unwrap(), | ||
105 | } | ||
106 | } | ||
107 | |||
108 | pub fn request<R>( | ||
109 | &self, | ||
110 | params: R::Params, | ||
111 | expected_resp: &str, | ||
112 | ) | ||
113 | where | ||
114 | R: Request, | ||
115 | R::Params: Serialize, | ||
116 | { | ||
117 | let id = self.req_id.get(); | ||
118 | self.req_id.set(id + 1); | ||
119 | let expected_resp: Value = from_str(expected_resp).unwrap(); | ||
120 | let actual = self.send_request::<R>(id, params); | ||
121 | assert_eq!( | ||
122 | expected_resp, actual, | ||
123 | "Expected:\n{}\n\ | ||
124 | Actual:\n{}\n", | ||
125 | to_string_pretty(&expected_resp).unwrap(), | ||
126 | to_string_pretty(&actual).unwrap(), | ||
127 | ); | ||
128 | } | ||
129 | |||
130 | fn send_request<R>(&self, id: u64, params: R::Params) -> Value | ||
131 | where | ||
132 | R: Request, | ||
133 | R::Params: Serialize, | ||
134 | { | ||
135 | let r = RawRequest::new::<R>(id, ¶ms); | ||
136 | self.send_request_(r) | ||
137 | } | ||
138 | fn send_request_(&self, r: RawRequest) -> Value | ||
139 | { | ||
140 | let id = r.id; | ||
141 | self.worker.as_ref() | ||
142 | .unwrap() | ||
143 | .send(RawMessage::Request(r)); | ||
144 | while let Some(msg) = self.recv() { | ||
145 | match msg { | ||
146 | RawMessage::Request(req) => panic!("unexpected request: {:?}", req), | ||
147 | RawMessage::Notification(_) => (), | ||
148 | RawMessage::Response(res) => { | ||
149 | assert_eq!(res.id, id); | ||
150 | if let Some(err) = res.error { | ||
151 | panic!("error response: {:#?}", err); | ||
152 | } | ||
153 | return res.result.unwrap(); | ||
154 | } | ||
155 | } | ||
156 | } | ||
157 | panic!("no response"); | ||
158 | } | ||
159 | pub fn wait_for_feedback(&self, feedback: &str) { | ||
160 | self.wait_for_feedback_n(feedback, 1) | ||
161 | } | ||
162 | pub fn wait_for_feedback_n(&self, feedback: &str, n: usize) { | ||
163 | let f = |msg: &RawMessage| match msg { | ||
164 | RawMessage::Notification(n) if n.method == "internalFeedback" => { | ||
165 | return n.clone().cast::<req::InternalFeedback>() | ||
166 | .unwrap() == feedback | ||
167 | } | ||
168 | _ => false, | ||
169 | }; | ||
170 | let mut total = 0; | ||
171 | for msg in self.messages.borrow().iter() { | ||
172 | if f(msg) { | ||
173 | total += 1 | ||
174 | } | ||
175 | } | ||
176 | while total < n { | ||
177 | let msg = self.recv().expect("no response"); | ||
178 | if f(&msg) { | ||
179 | total += 1; | ||
180 | } | ||
181 | } | ||
182 | } | ||
183 | fn recv(&self) -> Option<RawMessage> { | ||
184 | recv_timeout(&self.worker.as_ref().unwrap().out) | ||
185 | .map(|msg| { | ||
186 | self.messages.borrow_mut().push(msg.clone()); | ||
187 | msg | ||
188 | }) | ||
189 | } | ||
190 | fn send_notification(&self, not: RawNotification) { | ||
191 | self.worker.as_ref() | ||
192 | .unwrap() | ||
193 | .send(RawMessage::Notification(not)); | ||
194 | } | ||
195 | } | ||
196 | |||
197 | impl Drop for Server { | ||
198 | fn drop(&mut self) { | ||
199 | self.send_request::<Shutdown>(666, ()); | ||
200 | let receiver = self.worker.take().unwrap().stop(); | ||
201 | while let Some(msg) = recv_timeout(&receiver) { | ||
202 | drop(msg); | ||
203 | } | ||
204 | self.watcher.take() | ||
205 | .unwrap() | ||
206 | .stop() | ||
207 | .unwrap(); | ||
208 | } | ||
209 | } | ||
210 | |||
211 | fn recv_timeout(receiver: &Receiver<RawMessage>) -> Option<RawMessage> { | ||
212 | let timeout = Duration::from_secs(5); | ||
213 | select! { | ||
214 | recv(receiver, msg) => msg, | ||
215 | recv(after(timeout)) => panic!("timed out"), | ||
216 | } | ||
217 | } | ||