aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_lsp_server
diff options
context:
space:
mode:
authorAleksey Kladov <[email protected]>2018-09-16 10:54:24 +0100
committerAleksey Kladov <[email protected]>2018-09-16 11:07:39 +0100
commitb5021411a84822cb3f1e3aeffad9550dd15bdeb6 (patch)
tree9dca564f8e51b298dced01c4ce669c756dce3142 /crates/ra_lsp_server
parentba0bfeee12e19da40b5eabc8d0408639af10e96f (diff)
rename all things
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r--crates/ra_lsp_server/Cargo.toml31
-rw-r--r--crates/ra_lsp_server/src/caps.rs49
-rw-r--r--crates/ra_lsp_server/src/conv.rs296
-rw-r--r--crates/ra_lsp_server/src/lib.rs37
-rw-r--r--crates/ra_lsp_server/src/main.rs52
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs436
-rw-r--r--crates/ra_lsp_server/src/main_loop/mod.rs419
-rw-r--r--crates/ra_lsp_server/src/main_loop/subscriptions.rs21
-rw-r--r--crates/ra_lsp_server/src/path_map.rs110
-rw-r--r--crates/ra_lsp_server/src/project_model.rs175
-rw-r--r--crates/ra_lsp_server/src/req.rs176
-rw-r--r--crates/ra_lsp_server/src/server_world.rs167
-rw-r--r--crates/ra_lsp_server/src/thread_watcher.rs70
-rw-r--r--crates/ra_lsp_server/src/vfs.rs71
-rw-r--r--crates/ra_lsp_server/tests/heavy_tests/main.rs99
-rw-r--r--crates/ra_lsp_server/tests/heavy_tests/support.rs217
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]
2name = "ra_lsp_server"
3version = "0.1.0"
4authors = ["Aleksey Kladov <[email protected]>"]
5
6[dependencies]
7rayon = "1.0.2"
8relative-path = "0.3.7"
9failure = "0.1.2"
10serde_json = "1.0.24"
11serde = "1.0.71"
12serde_derive = "1.0.71"
13drop_bomb = "0.1.0"
14crossbeam-channel = "0.2.4"
15flexi_logger = "0.9.1"
16log = "0.4.3"
17url_serde = "0.2.0"
18languageserver-types = "0.50.0"
19walkdir = "2.2.0"
20im = "12.0.0"
21cargo_metadata = "0.6.0"
22text_unit = { version = "0.1.2", features = ["serde"] }
23smol_str = { version = "0.1.5", features = ["serde"] }
24
25ra_syntax = { path = "../ra_syntax" }
26ra_editor = { path = "../ra_editor" }
27ra_analysis = { path = "../ra_analysis" }
28gen_lsp_server = { path = "../gen_lsp_server" }
29
30[dev-dependencies]
31tempdir = "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 @@
1use languageserver_types::{
2 ServerCapabilities,
3 TextDocumentSyncCapability,
4 TextDocumentSyncOptions,
5 TextDocumentSyncKind,
6 ExecuteCommandOptions,
7 CompletionOptions,
8 DocumentOnTypeFormattingOptions,
9};
10
11pub 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 @@
1use languageserver_types::{
2 Range, SymbolKind, Position, TextEdit, Location, Url,
3 TextDocumentIdentifier, VersionedTextDocumentIdentifier, TextDocumentItem,
4 TextDocumentPositionParams, TextDocumentEdit,
5};
6use ra_editor::{LineIndex, LineCol, Edit, AtomEdit};
7use ra_syntax::{SyntaxKind, TextUnit, TextRange};
8use ra_analysis::{FileId, SourceChange, SourceFileEdit, FileSystemEdit};
9
10use {
11 Result,
12 server_world::ServerWorld,
13 req,
14};
15
16pub trait Conv {
17 type Output;
18 fn conv(self) -> Self::Output;
19}
20
21pub trait ConvWith {
22 type Ctx;
23 type Output;
24 fn conv_with(self, ctx: &Self::Ctx) -> Self::Output;
25}
26
27pub trait TryConvWith {
28 type Ctx;
29 type Output;
30 fn try_conv_with(self, ctx: &Self::Ctx) -> Result<Self::Output>;
31}
32
33impl 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
52impl 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
66impl 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
77impl 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
89impl 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
101impl 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
113impl 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
125impl<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
133impl<'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
141impl 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
149impl<'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
157impl<'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
165impl<'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
173impl<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
185impl 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
210impl 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
227impl 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
249pub 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
263pub 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
272impl<'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
280pub struct ConvWithIter<'a, I, Ctx: 'a> {
281 iter: I,
282 ctx: &'a Ctx,
283}
284
285impl<'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]
2extern crate failure;
3#[macro_use]
4extern crate serde_derive;
5extern crate serde;
6extern crate serde_json;
7extern crate languageserver_types;
8#[macro_use]
9extern crate crossbeam_channel;
10extern crate rayon;
11#[macro_use]
12extern crate log;
13extern crate drop_bomb;
14extern crate url_serde;
15extern crate walkdir;
16extern crate im;
17extern crate relative_path;
18extern crate cargo_metadata;
19
20extern crate gen_lsp_server;
21extern crate ra_editor;
22extern crate ra_analysis;
23extern crate ra_syntax;
24
25mod caps;
26pub mod req;
27mod conv;
28mod main_loop;
29mod vfs;
30mod path_map;
31mod server_world;
32mod project_model;
33pub mod thread_watcher;
34
35pub type Result<T> = ::std::result::Result<T, ::failure::Error>;
36pub use caps::server_capabilities;
37pub 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]
2extern crate log;
3#[macro_use]
4extern crate failure;
5extern crate flexi_logger;
6extern crate gen_lsp_server;
7extern crate ra_lsp_server;
8
9use flexi_logger::{Logger, Duplicate};
10use gen_lsp_server::{run_server, stdio_transport};
11use ra_lsp_server::Result;
12
13fn 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
33fn 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 @@
1use std::collections::HashMap;
2
3use languageserver_types::{
4 Diagnostic, DiagnosticSeverity, DocumentSymbol,
5 Command, TextDocumentIdentifier,
6 SymbolInformation, Position, Location, TextEdit,
7 CompletionItem, InsertTextFormat, CompletionItemKind,
8};
9use serde_json::to_value;
10use ra_analysis::{Query, FileId, RunnableKind, JobToken};
11use ra_syntax::{
12 text_utils::contains_offset_nonstrict,
13};
14
15use ::{
16 req::{self, Decoration}, Result,
17 conv::{Conv, ConvWith, TryConvWith, MapConvWith, to_location},
18 server_world::ServerWorld,
19 project_model::TargetKind,
20};
21
22pub 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
32pub 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
48pub 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
67pub 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
79pub 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
99pub 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
138pub 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
187pub 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
207pub 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
225pub 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
328pub 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
337pub 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
368pub 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
398pub 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
417pub 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
428fn 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 @@
1mod handlers;
2mod subscriptions;
3
4use std::{
5 path::PathBuf,
6 collections::{HashMap},
7};
8
9use serde::{Serialize, de::DeserializeOwned};
10use crossbeam_channel::{unbounded, Sender, Receiver};
11use rayon::{self, ThreadPool};
12use languageserver_types::{NumberOrString};
13use ra_analysis::{FileId, JobHandle, JobToken, LibraryData};
14use gen_lsp_server::{
15 RawRequest, RawNotification, RawMessage, RawResponse, ErrorCode,
16 handle_shutdown,
17};
18
19use {
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)]
30enum Task {
31 Respond(RawResponse),
32 Notify(RawNotification),
33}
34
35pub 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
87fn 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
213fn 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
230fn 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
267fn 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>(&params);
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
331struct 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
339impl<'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
383fn 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>(&params);
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>(&params);
406 sender.send(Task::Notify(not))
407 }
408 }
409 }
410 });
411}
412
413fn 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 @@
1use std::collections::HashSet;
2use ra_analysis::FileId;
3
4pub struct Subscriptions {
5 subs: HashSet<FileId>,
6}
7
8impl 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 @@
1use std::path::{PathBuf, Path, Component};
2use im;
3use relative_path::RelativePath;
4use ra_analysis::{FileId, FileResolver};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum Root {
8 Workspace, Lib
9}
10
11#[derive(Debug, Default, Clone)]
12pub 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
19impl 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
56impl 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
68fn 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)]
96mod 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 @@
1use std::{
2 collections::{HashMap, HashSet},
3 path::{Path, PathBuf},
4};
5use cargo_metadata::{metadata_run, CargoOpt};
6use ra_syntax::SmolStr;
7
8use {
9 Result,
10 thread_watcher::{Worker, ThreadWatcher},
11};
12
13#[derive(Debug, Clone)]
14pub struct CargoWorkspace {
15 packages: Vec<PackageData>,
16 targets: Vec<TargetData>,
17}
18
19#[derive(Clone, Copy, Debug, Serialize)]
20pub struct Package(usize);
21#[derive(Clone, Copy, Debug, Serialize)]
22pub struct Target(usize);
23
24#[derive(Debug, Clone)]
25struct PackageData {
26 name: SmolStr,
27 manifest: PathBuf,
28 targets: Vec<Target>,
29 is_member: bool,
30}
31
32#[derive(Debug, Clone)]
33struct TargetData {
34 pkg: Package,
35 name: SmolStr,
36 root: PathBuf,
37 kind: TargetKind,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum TargetKind {
42 Bin, Lib, Example, Test, Bench, Other,
43}
44
45impl 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
60impl 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
75impl 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
133fn 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
148impl 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
164pub 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 @@
1use std::collections::HashMap;
2use languageserver_types::{TextDocumentIdentifier, Range, Url, Position, Location};
3use url_serde;
4
5pub 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
19pub enum SyntaxTree {}
20
21impl 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")]
29pub struct SyntaxTreeParams {
30 pub text_document: TextDocumentIdentifier
31}
32
33pub enum ExtendSelection {}
34
35impl 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")]
43pub struct ExtendSelectionParams {
44 pub text_document: TextDocumentIdentifier,
45 pub selections: Vec<Range>,
46}
47
48#[derive(Serialize, Debug)]
49#[serde(rename_all = "camelCase")]
50pub struct ExtendSelectionResult {
51 pub selections: Vec<Range>,
52}
53
54pub enum FindMatchingBrace {}
55
56impl 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")]
64pub struct FindMatchingBraceParams {
65 pub text_document: TextDocumentIdentifier,
66 pub offsets: Vec<Position>,
67}
68
69pub enum DecorationsRequest {}
70
71impl Request for DecorationsRequest {
72 type Params = TextDocumentIdentifier;
73 type Result = Vec<Decoration>;
74 const METHOD: &'static str = "m/decorationsRequest";
75}
76
77pub enum PublishDecorations {}
78
79impl 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")]
86pub 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")]
94pub struct Decoration {
95 pub range: Range,
96 pub tag: &'static str
97}
98
99pub enum ParentModule {}
100
101impl Request for ParentModule {
102 type Params = TextDocumentIdentifier;
103 type Result = Vec<Location>;
104 const METHOD: &'static str = "m/parentModule";
105}
106
107pub enum JoinLines {}
108
109impl 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")]
117pub struct JoinLinesParams {
118 pub text_document: TextDocumentIdentifier,
119 pub range: Range,
120}
121
122pub enum Runnables {}
123
124impl 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")]
132pub struct RunnablesParams {
133 pub text_document: TextDocumentIdentifier,
134 pub position: Option<Position>,
135}
136
137#[derive(Serialize, Debug)]
138#[serde(rename_all = "camelCase")]
139pub 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")]
149pub 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")]
158pub 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
171pub enum InternalFeedback {}
172
173impl 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 @@
1use std::{
2 fs,
3 path::{PathBuf, Path},
4 collections::HashMap,
5 sync::Arc,
6};
7
8use languageserver_types::Url;
9use ra_analysis::{FileId, AnalysisHost, Analysis, CrateGraph, CrateId, LibraryData, FileResolver};
10
11use {
12 Result,
13 path_map::{PathMap, Root},
14 vfs::{FileEvent, FileEventKind},
15 project_model::CargoWorkspace,
16};
17
18#[derive(Debug)]
19pub 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)]
27pub struct ServerWorld {
28 pub workspaces: Arc<Vec<CargoWorkspace>>,
29 pub analysis: Analysis,
30 pub path_map: PathMap,
31}
32
33impl 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
150impl 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 @@
1use std::thread;
2use crossbeam_channel::{bounded, unbounded, Sender, Receiver};
3use drop_bomb::DropBomb;
4use Result;
5
6pub struct Worker<I, O> {
7 pub inp: Sender<I>,
8 pub out: Receiver<O>,
9}
10
11impl<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
33pub struct ThreadWatcher {
34 name: &'static str,
35 thread: thread::JoinHandle<()>,
36 bomb: DropBomb,
37}
38
39impl 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.
66fn 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 @@
1use std::{
2 path::{PathBuf, Path},
3 fs,
4};
5
6use walkdir::WalkDir;
7
8use {
9 thread_watcher::{Worker, ThreadWatcher},
10};
11
12
13#[derive(Debug)]
14pub struct FileEvent {
15 pub path: PathBuf,
16 pub kind: FileEventKind,
17}
18
19#[derive(Debug)]
20pub enum FileEventKind {
21 Add(String),
22}
23
24pub 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
41fn 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]
2extern crate crossbeam_channel;
3extern crate tempdir;
4extern crate languageserver_types;
5extern crate serde;
6extern crate serde_json;
7extern crate gen_lsp_server;
8extern crate flexi_logger;
9extern crate ra_lsp_server;
10
11mod support;
12
13use ra_lsp_server::req::{Runnables, RunnablesParams};
14
15use support::project;
16
17
18const LOG: &'static str = "";
19
20#[test]
21fn test_runnables_no_project() {
22 let server = project(r"
23//- lib.rs
24#[test]
25fn 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]
49fn test_runnables_project() {
50 let server = project(r#"
51//- Cargo.toml
52[package]
53name = "foo"
54version = "0.0.0"
55
56//- src/lib.rs
57pub fn foo() {}
58
59//- tests/spam.rs
60#[test]
61fn 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 @@
1use std::{
2 fs,
3 cell::{Cell, RefCell},
4 path::PathBuf,
5 time::Duration,
6 sync::Once,
7};
8
9use tempdir::TempDir;
10use crossbeam_channel::{after, Receiver};
11use flexi_logger::Logger;
12use languageserver_types::{
13 Url,
14 TextDocumentIdentifier,
15 request::{Request, Shutdown},
16 notification::DidOpenTextDocument,
17 DidOpenTextDocumentParams,
18 TextDocumentItem,
19};
20use serde::Serialize;
21use serde_json::{Value, from_str, to_string_pretty};
22use gen_lsp_server::{RawMessage, RawRequest, RawNotification};
23
24use ra_lsp_server::{main_loop, req, thread_watcher::{ThreadWatcher, Worker}};
25
26pub 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
59pub 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
67impl 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, &params);
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
197impl 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
211fn 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}