]> git.lizzy.rs Git - rust.git/blob - crates/ra_lsp_server/src/main_loop/handlers.rs
9478ebfb89cfdf56147829f8dcdc4ad693dceac4
[rust.git] / crates / ra_lsp_server / src / main_loop / handlers.rs
1 use gen_lsp_server::ErrorCode;
2 use lsp_types::{
3     CodeActionResponse, CodeLens, Command, Diagnostic, DiagnosticSeverity,
4     DocumentFormattingParams, DocumentHighlight, DocumentSymbol, Documentation, FoldingRange,
5     FoldingRangeKind, FoldingRangeParams, Hover, HoverContents, Location, MarkupContent,
6     MarkupKind, ParameterInformation, ParameterLabel, Position, PrepareRenameResponse, Range,
7     RenameParams, SignatureInformation, SymbolInformation, TextDocumentIdentifier, TextEdit,
8     WorkspaceEdit
9 };
10 use ra_ide_api::{
11     FileId, FilePosition, FileRange, FoldKind, Query, RangeInfo, RunnableKind, Severity, Cancelable,
12 };
13 use ra_syntax::{AstNode, TextUnit};
14 use rustc_hash::FxHashMap;
15 use serde_json::to_value;
16 use std::io::Write;
17
18 use crate::{
19     cargo_target_spec::{runnable_args, CargoTargetSpec},
20     conv::{to_location, to_location_link, Conv, ConvWith, MapConvWith, TryConvWith},
21     req::{self, Decoration},
22     server_world::ServerWorld,
23     LspError, Result,
24 };
25
26 pub fn handle_analyzer_status(world: ServerWorld, _: ()) -> Result<String> {
27     Ok(world.status())
28 }
29
30 pub fn handle_syntax_tree(world: ServerWorld, params: req::SyntaxTreeParams) -> Result<String> {
31     let id = params.text_document.try_conv_with(&world)?;
32     let res = world.analysis().syntax_tree(id);
33     Ok(res)
34 }
35
36 pub fn handle_extend_selection(
37     world: ServerWorld,
38     params: req::ExtendSelectionParams,
39 ) -> Result<req::ExtendSelectionResult> {
40     let file_id = params.text_document.try_conv_with(&world)?;
41     let line_index = world.analysis().file_line_index(file_id);
42     let selections = params
43         .selections
44         .into_iter()
45         .map_conv_with(&line_index)
46         .map(|range| FileRange { file_id, range })
47         .map(|frange| {
48             world
49                 .analysis()
50                 .extend_selection(frange)
51                 .map(|it| it.conv_with(&line_index))
52         })
53         .collect::<Cancelable<Vec<_>>>()?;
54     Ok(req::ExtendSelectionResult { selections })
55 }
56
57 pub fn handle_find_matching_brace(
58     world: ServerWorld,
59     params: req::FindMatchingBraceParams,
60 ) -> Result<Vec<Position>> {
61     let file_id = params.text_document.try_conv_with(&world)?;
62     let line_index = world.analysis().file_line_index(file_id);
63     let res = params
64         .offsets
65         .into_iter()
66         .map_conv_with(&line_index)
67         .map(|offset| {
68             world
69                 .analysis()
70                 .matching_brace(FilePosition { file_id, offset })
71                 .unwrap_or(offset)
72         })
73         .map_conv_with(&line_index)
74         .collect();
75     Ok(res)
76 }
77
78 pub fn handle_join_lines(
79     world: ServerWorld,
80     params: req::JoinLinesParams,
81 ) -> Result<req::SourceChange> {
82     let frange = (&params.text_document, params.range).try_conv_with(&world)?;
83     world.analysis().join_lines(frange).try_conv_with(&world)
84 }
85
86 pub fn handle_on_enter(
87     world: ServerWorld,
88     params: req::TextDocumentPositionParams,
89 ) -> Result<Option<req::SourceChange>> {
90     let position = params.try_conv_with(&world)?;
91     match world.analysis().on_enter(position) {
92         None => Ok(None),
93         Some(edit) => Ok(Some(edit.try_conv_with(&world)?)),
94     }
95 }
96
97 pub fn handle_on_type_formatting(
98     world: ServerWorld,
99     params: req::DocumentOnTypeFormattingParams,
100 ) -> Result<Option<Vec<TextEdit>>> {
101     let file_id = params.text_document.try_conv_with(&world)?;
102     let line_index = world.analysis().file_line_index(file_id);
103     let position = FilePosition {
104         file_id,
105         /// in `ra_ide_api`, the `on_type` invariant is that
106         /// `text.char_at(position) == typed_char`.
107         offset: params.position.conv_with(&line_index) - TextUnit::of_char('.'),
108     };
109
110     let edit = match params.ch.as_str() {
111         "=" => world.analysis().on_eq_typed(position),
112         "." => world.analysis().on_dot_typed(position),
113         _ => return Ok(None),
114     };
115     let mut edit = match edit {
116         Some(it) => it,
117         None => return Ok(None),
118     };
119
120     // This should be a single-file edit
121     let edit = edit.source_file_edits.pop().unwrap();
122
123     let change: Vec<TextEdit> = edit.edit.conv_with(&line_index);
124     return Ok(Some(change));
125 }
126
127 pub fn handle_document_symbol(
128     world: ServerWorld,
129     params: req::DocumentSymbolParams,
130 ) -> Result<Option<req::DocumentSymbolResponse>> {
131     let file_id = params.text_document.try_conv_with(&world)?;
132     let line_index = world.analysis().file_line_index(file_id);
133
134     let mut parents: Vec<(DocumentSymbol, Option<usize>)> = Vec::new();
135
136     for symbol in world.analysis().file_structure(file_id) {
137         let doc_symbol = DocumentSymbol {
138             name: symbol.label,
139             detail: symbol.detail,
140             kind: symbol.kind.conv(),
141             deprecated: None,
142             range: symbol.node_range.conv_with(&line_index),
143             selection_range: symbol.navigation_range.conv_with(&line_index),
144             children: None,
145         };
146         parents.push((doc_symbol, symbol.parent));
147     }
148     let mut res = Vec::new();
149     while let Some((node, parent)) = parents.pop() {
150         match parent {
151             None => res.push(node),
152             Some(i) => {
153                 let children = &mut parents[i].0.children;
154                 if children.is_none() {
155                     *children = Some(Vec::new());
156                 }
157                 children.as_mut().unwrap().push(node);
158             }
159         }
160     }
161
162     Ok(Some(req::DocumentSymbolResponse::Nested(res)))
163 }
164
165 pub fn handle_workspace_symbol(
166     world: ServerWorld,
167     params: req::WorkspaceSymbolParams,
168 ) -> Result<Option<Vec<SymbolInformation>>> {
169     let all_symbols = params.query.contains('#');
170     let libs = params.query.contains('*');
171     let query = {
172         let query: String = params
173             .query
174             .chars()
175             .filter(|&c| c != '#' && c != '*')
176             .collect();
177         let mut q = Query::new(query);
178         if !all_symbols {
179             q.only_types();
180         }
181         if libs {
182             q.libs();
183         }
184         q.limit(128);
185         q
186     };
187     let mut res = exec_query(&world, query)?;
188     if res.is_empty() && !all_symbols {
189         let mut query = Query::new(params.query);
190         query.limit(128);
191         res = exec_query(&world, query)?;
192     }
193
194     return Ok(Some(res));
195
196     fn exec_query(world: &ServerWorld, query: Query) -> Result<Vec<SymbolInformation>> {
197         let mut res = Vec::new();
198         for nav in world.analysis().symbol_search(query)? {
199             let info = SymbolInformation {
200                 name: nav.name().to_string(),
201                 kind: nav.kind().conv(),
202                 location: nav.try_conv_with(world)?,
203                 container_name: None,
204                 deprecated: None,
205             };
206             res.push(info);
207         }
208         Ok(res)
209     }
210 }
211
212 pub fn handle_goto_definition(
213     world: ServerWorld,
214     params: req::TextDocumentPositionParams,
215 ) -> Result<Option<req::GotoDefinitionResponse>> {
216     let position = params.try_conv_with(&world)?;
217     let line_index = world.analysis().file_line_index(position.file_id);
218     let nav_info = match world.analysis().goto_definition(position)? {
219         None => return Ok(None),
220         Some(it) => it,
221     };
222     let nav_range = nav_info.range;
223     let res = nav_info
224         .info
225         .into_iter()
226         .map(|nav| RangeInfo::new(nav_range, nav))
227         .map(|nav| to_location_link(&nav, &world, &line_index))
228         .collect::<Result<Vec<_>>>()?;
229     Ok(Some(req::GotoDefinitionResponse::Link(res)))
230 }
231
232 pub fn handle_parent_module(
233     world: ServerWorld,
234     params: req::TextDocumentPositionParams,
235 ) -> Result<Vec<Location>> {
236     let position = params.try_conv_with(&world)?;
237     world
238         .analysis()
239         .parent_module(position)?
240         .into_iter()
241         .map(|nav| nav.try_conv_with(&world))
242         .collect::<Result<Vec<_>>>()
243 }
244
245 pub fn handle_runnables(
246     world: ServerWorld,
247     params: req::RunnablesParams,
248 ) -> Result<Vec<req::Runnable>> {
249     let file_id = params.text_document.try_conv_with(&world)?;
250     let line_index = world.analysis().file_line_index(file_id);
251     let offset = params.position.map(|it| it.conv_with(&line_index));
252     let mut res = Vec::new();
253     for runnable in world.analysis().runnables(file_id)? {
254         if let Some(offset) = offset {
255             if !runnable.range.contains_inclusive(offset) {
256                 continue;
257             }
258         }
259
260         let args = runnable_args(&world, file_id, &runnable.kind)?;
261
262         let r = req::Runnable {
263             range: runnable.range.conv_with(&line_index),
264             label: match &runnable.kind {
265                 RunnableKind::Test { name } => format!("test {}", name),
266                 RunnableKind::TestMod { path } => format!("test-mod {}", path),
267                 RunnableKind::Bench { name } => format!("bench {}", name),
268                 RunnableKind::Bin => "run binary".to_string(),
269             },
270             bin: "cargo".to_string(),
271             args,
272             env: {
273                 let mut m = FxHashMap::default();
274                 m.insert("RUST_BACKTRACE".to_string(), "short".to_string());
275                 m
276             },
277         };
278         res.push(r);
279     }
280     let mut check_args = vec!["check".to_string()];
281     let label;
282     match CargoTargetSpec::for_file(&world, file_id)? {
283         Some(spec) => {
284             label = format!("cargo check -p {}", spec.package);
285             spec.push_to(&mut check_args);
286         }
287         None => {
288             label = "cargo check --all".to_string();
289             check_args.push("--all".to_string())
290         }
291     }
292     // Always add `cargo check`.
293     res.push(req::Runnable {
294         range: Default::default(),
295         label,
296         bin: "cargo".to_string(),
297         args: check_args,
298         env: FxHashMap::default(),
299     });
300     return Ok(res);
301 }
302
303 pub fn handle_decorations(
304     world: ServerWorld,
305     params: TextDocumentIdentifier,
306 ) -> Result<Vec<Decoration>> {
307     let file_id = params.try_conv_with(&world)?;
308     highlight(&world, file_id)
309 }
310
311 pub fn handle_completion(
312     world: ServerWorld,
313     params: req::CompletionParams,
314 ) -> Result<Option<req::CompletionResponse>> {
315     let position = {
316         let file_id = params.text_document.try_conv_with(&world)?;
317         let line_index = world.analysis().file_line_index(file_id);
318         let offset = params.position.conv_with(&line_index);
319         FilePosition { file_id, offset }
320     };
321     let completion_triggered_after_single_colon = {
322         let mut res = false;
323         if let Some(ctx) = params.context {
324             if ctx.trigger_character.unwrap_or_default() == ":" {
325                 let source_file = world.analysis().parse(position.file_id);
326                 let syntax = source_file.syntax();
327                 let text = syntax.text();
328                 if let Some(next_char) = text.char_at(position.offset) {
329                     let diff = TextUnit::of_char(next_char) + TextUnit::of_char(':');
330                     let prev_char = position.offset - diff;
331                     if text.char_at(prev_char) != Some(':') {
332                         res = true;
333                     }
334                 }
335             }
336         }
337         res
338     };
339     if completion_triggered_after_single_colon {
340         return Ok(None);
341     }
342
343     let items = match world.analysis().completions(position)? {
344         None => return Ok(None),
345         Some(items) => items,
346     };
347     let line_index = world.analysis().file_line_index(position.file_id);
348     let items = items
349         .into_iter()
350         .map(|item| item.conv_with(&line_index))
351         .collect();
352
353     Ok(Some(req::CompletionResponse::Array(items)))
354 }
355
356 pub fn handle_folding_range(
357     world: ServerWorld,
358     params: FoldingRangeParams,
359 ) -> Result<Option<Vec<FoldingRange>>> {
360     let file_id = params.text_document.try_conv_with(&world)?;
361     let line_index = world.analysis().file_line_index(file_id);
362
363     let res = Some(
364         world
365             .analysis()
366             .folding_ranges(file_id)
367             .into_iter()
368             .map(|fold| {
369                 let kind = match fold.kind {
370                     FoldKind::Comment => Some(FoldingRangeKind::Comment),
371                     FoldKind::Imports => Some(FoldingRangeKind::Imports),
372                     FoldKind::Mods => None,
373                     FoldKind::Block => None,
374                 };
375                 let range = fold.range.conv_with(&line_index);
376                 FoldingRange {
377                     start_line: range.start.line,
378                     start_character: Some(range.start.character),
379                     end_line: range.end.line,
380                     end_character: Some(range.start.character),
381                     kind,
382                 }
383             })
384             .collect(),
385     );
386
387     Ok(res)
388 }
389
390 pub fn handle_signature_help(
391     world: ServerWorld,
392     params: req::TextDocumentPositionParams,
393 ) -> Result<Option<req::SignatureHelp>> {
394     let position = params.try_conv_with(&world)?;
395     if let Some(call_info) = world.analysis().call_info(position)? {
396         let parameters: Vec<ParameterInformation> = call_info
397             .parameters
398             .into_iter()
399             .map(|param| ParameterInformation {
400                 label: ParameterLabel::Simple(param.clone()),
401                 documentation: None,
402             })
403             .collect();
404         let documentation = call_info.doc.map(|value| {
405             Documentation::MarkupContent(MarkupContent {
406                 kind: MarkupKind::Markdown,
407                 value,
408             })
409         });
410         let sig_info = SignatureInformation {
411             label: call_info.label,
412             documentation,
413             parameters: Some(parameters),
414         };
415         Ok(Some(req::SignatureHelp {
416             signatures: vec![sig_info],
417             active_signature: Some(0),
418             active_parameter: call_info.active_parameter.map(|it| it as u64),
419         }))
420     } else {
421         Ok(None)
422     }
423 }
424
425 pub fn handle_hover(
426     world: ServerWorld,
427     params: req::TextDocumentPositionParams,
428 ) -> Result<Option<Hover>> {
429     let position = params.try_conv_with(&world)?;
430     let info = match world.analysis().hover(position)? {
431         None => return Ok(None),
432         Some(info) => info,
433     };
434     let line_index = world.analysis.file_line_index(position.file_id);
435     let range = info.range.conv_with(&line_index);
436     let res = Hover {
437         contents: HoverContents::Markup(MarkupContent {
438             kind: MarkupKind::Markdown,
439             value: info.info,
440         }),
441         range: Some(range),
442     };
443     Ok(Some(res))
444 }
445
446 /// Test doc comment
447 pub fn handle_prepare_rename(
448     world: ServerWorld,
449     params: req::TextDocumentPositionParams,
450 ) -> Result<Option<PrepareRenameResponse>> {
451     let position = params.try_conv_with(&world)?;
452
453     // We support renaming references like handle_rename does.
454     // In the future we may want to reject the renaming of things like keywords here too.
455     let refs = world.analysis().find_all_refs(position)?;
456     let r = match refs.first() {
457         Some(r) => r,
458         None => return Ok(None),
459     };
460     let file_id = params.text_document.try_conv_with(&world)?;
461     let line_index = world.analysis().file_line_index(file_id);
462     let loc = to_location(r.0, r.1, &world, &line_index)?;
463
464     Ok(Some(PrepareRenameResponse::Range(loc.range)))
465 }
466
467 pub fn handle_rename(world: ServerWorld, params: RenameParams) -> Result<Option<WorkspaceEdit>> {
468     let file_id = params.text_document.try_conv_with(&world)?;
469     let line_index = world.analysis().file_line_index(file_id);
470     let offset = params.position.conv_with(&line_index);
471
472     if params.new_name.is_empty() {
473         return Err(LspError::new(
474             ErrorCode::InvalidParams as i32,
475             "New Name cannot be empty".into(),
476         )
477         .into());
478     }
479
480     let optional_change = world
481         .analysis()
482         .rename(FilePosition { file_id, offset }, &*params.new_name)?;
483     let change = match optional_change {
484         None => return Ok(None),
485         Some(it) => it,
486     };
487
488     let source_change_req = change.try_conv_with(&world)?;
489
490     Ok(Some(source_change_req.workspace_edit))
491 }
492
493 pub fn handle_references(
494     world: ServerWorld,
495     params: req::ReferenceParams,
496 ) -> Result<Option<Vec<Location>>> {
497     let file_id = params.text_document.try_conv_with(&world)?;
498     let line_index = world.analysis().file_line_index(file_id);
499     let offset = params.position.conv_with(&line_index);
500
501     let refs = world
502         .analysis()
503         .find_all_refs(FilePosition { file_id, offset })?;
504
505     Ok(Some(
506         refs.into_iter()
507             .filter_map(|r| to_location(r.0, r.1, &world, &line_index).ok())
508             .collect(),
509     ))
510 }
511
512 pub fn handle_formatting(
513     world: ServerWorld,
514     params: DocumentFormattingParams,
515 ) -> Result<Option<Vec<TextEdit>>> {
516     let file_id = params.text_document.try_conv_with(&world)?;
517     let file = world.analysis().file_text(file_id);
518
519     let file_line_index = world.analysis().file_line_index(file_id);
520     let end_position = TextUnit::of_str(&file).conv_with(&file_line_index);
521
522     use std::process;
523     let mut rustfmt = process::Command::new("rustfmt");
524     rustfmt
525         .stdin(process::Stdio::piped())
526         .stdout(process::Stdio::piped());
527
528     if let Ok(path) = params.text_document.uri.to_file_path() {
529         if let Some(parent) = path.parent() {
530             rustfmt.current_dir(parent);
531         }
532     }
533     let mut rustfmt = rustfmt.spawn()?;
534
535     rustfmt.stdin.as_mut().unwrap().write_all(file.as_bytes())?;
536
537     let output = rustfmt.wait_with_output()?;
538     let captured_stdout = String::from_utf8(output.stdout)?;
539     if !output.status.success() {
540         return Err(LspError::new(
541             -32900,
542             format!(
543                 r#"rustfmt exited with:
544             Status: {}
545             stdout: {}"#,
546                 output.status, captured_stdout,
547             ),
548         )
549         .into());
550     }
551
552     Ok(Some(vec![TextEdit {
553         range: Range::new(Position::new(0, 0), end_position),
554         new_text: captured_stdout,
555     }]))
556 }
557
558 pub fn handle_code_action(
559     world: ServerWorld,
560     params: req::CodeActionParams,
561 ) -> Result<Option<CodeActionResponse>> {
562     let file_id = params.text_document.try_conv_with(&world)?;
563     let line_index = world.analysis().file_line_index(file_id);
564     let range = params.range.conv_with(&line_index);
565
566     let assists = world
567         .analysis()
568         .assists(FileRange { file_id, range })?
569         .into_iter();
570     let fixes = world
571         .analysis()
572         .diagnostics(file_id)?
573         .into_iter()
574         .filter_map(|d| Some((d.range, d.fix?)))
575         .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some())
576         .map(|(_range, fix)| fix);
577
578     let mut res = Vec::new();
579     for source_edit in assists.chain(fixes) {
580         let title = source_edit.label.clone();
581         let edit = source_edit.try_conv_with(&world)?;
582         let cmd = Command {
583             title,
584             command: "rust-analyzer.applySourceChange".to_string(),
585             arguments: Some(vec![to_value(edit).unwrap()]),
586         };
587         res.push(cmd);
588     }
589
590     Ok(Some(CodeActionResponse::Commands(res)))
591 }
592
593 pub fn handle_code_lens(
594     world: ServerWorld,
595     params: req::CodeLensParams,
596 ) -> Result<Option<Vec<CodeLens>>> {
597     let file_id = params.text_document.try_conv_with(&world)?;
598     let line_index = world.analysis().file_line_index(file_id);
599
600     let mut lenses: Vec<CodeLens> = Default::default();
601
602     for runnable in world.analysis().runnables(file_id)? {
603         let title = match &runnable.kind {
604             RunnableKind::Test { name: _ } | RunnableKind::TestMod { path: _ } => Some("Run Test"),
605             RunnableKind::Bench { name: _ } => Some("Run Bench"),
606             _ => None,
607         };
608
609         if let Some(title) = title {
610             let args = runnable_args(&world, file_id, &runnable.kind)?;
611             let range = runnable.range.conv_with(&line_index);
612
613             // This represents the actual command that will be run.
614             let r: req::Runnable = req::Runnable {
615                 range,
616                 label: Default::default(),
617                 bin: "cargo".into(),
618                 args,
619                 env: Default::default(),
620             };
621
622             let lens = CodeLens {
623                 range,
624                 command: Some(Command {
625                     title: title.into(),
626                     command: "rust-analyzer.runSingle".into(),
627                     arguments: Some(vec![to_value(r).unwrap()]),
628                 }),
629                 data: None,
630             };
631
632             lenses.push(lens);
633         }
634     }
635
636     return Ok(Some(lenses));
637 }
638
639 pub fn handle_document_highlight(
640     world: ServerWorld,
641     params: req::TextDocumentPositionParams,
642 ) -> Result<Option<Vec<DocumentHighlight>>> {
643     let file_id = params.text_document.try_conv_with(&world)?;
644     let line_index = world.analysis().file_line_index(file_id);
645
646     let refs = world
647         .analysis()
648         .find_all_refs(params.try_conv_with(&world)?)?;
649
650     Ok(Some(
651         refs.into_iter()
652             .map(|r| DocumentHighlight {
653                 range: r.1.conv_with(&line_index),
654                 kind: None,
655             })
656             .collect(),
657     ))
658 }
659
660 pub fn publish_diagnostics(
661     world: &ServerWorld,
662     file_id: FileId,
663 ) -> Result<req::PublishDiagnosticsParams> {
664     let uri = world.file_id_to_uri(file_id)?;
665     let line_index = world.analysis().file_line_index(file_id);
666     let diagnostics = world
667         .analysis()
668         .diagnostics(file_id)?
669         .into_iter()
670         .map(|d| Diagnostic {
671             range: d.range.conv_with(&line_index),
672             severity: Some(to_diagnostic_severity(d.severity)),
673             code: None,
674             source: Some("rust-analyzer".to_string()),
675             message: d.message,
676             related_information: None,
677         })
678         .collect();
679     Ok(req::PublishDiagnosticsParams { uri, diagnostics })
680 }
681
682 pub fn publish_decorations(
683     world: &ServerWorld,
684     file_id: FileId,
685 ) -> Result<req::PublishDecorationsParams> {
686     let uri = world.file_id_to_uri(file_id)?;
687     Ok(req::PublishDecorationsParams {
688         uri,
689         decorations: highlight(&world, file_id)?,
690     })
691 }
692
693 fn highlight(world: &ServerWorld, file_id: FileId) -> Result<Vec<Decoration>> {
694     let line_index = world.analysis().file_line_index(file_id);
695     let res = world
696         .analysis()
697         .highlight(file_id)?
698         .into_iter()
699         .map(|h| Decoration {
700             range: h.range.conv_with(&line_index),
701             tag: h.tag,
702         })
703         .collect();
704     Ok(res)
705 }
706
707 fn to_diagnostic_severity(severity: Severity) -> DiagnosticSeverity {
708     use ra_ide_api::Severity::*;
709
710     match severity {
711         Error => DiagnosticSeverity::Error,
712         WeakWarning => DiagnosticSeverity::Hint,
713     }
714 }