aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_lsp_server
diff options
context:
space:
mode:
authorEmil Lauridsen <[email protected]>2020-01-31 18:23:25 +0000
committerEmil Lauridsen <[email protected]>2020-02-03 10:34:24 +0000
commit790788d5f4013d8d92f110bc12a581d18cf4b6ae (patch)
tree311e11529c7546b7a09486d5c161039d8bd8f975 /crates/ra_lsp_server
parent52456c44901c8c38c8bcb742ebe305484af8f36f (diff)
Rework how we send diagnostics to client.
The previous way of sending from the thread pool suffered from stale diagnostics due to being canceled before we could clear the old ones. The key change is moving to sending diagnostics from the main loop thread, but doing all the hard work in the thread pool. This should provide the best of both worlds, with little to no of the downsides. This should hopefully fix a lot of issues, but we'll need testing in each individual issue to be sure.
Diffstat (limited to 'crates/ra_lsp_server')
-rw-r--r--crates/ra_lsp_server/src/diagnostics.rs85
-rw-r--r--crates/ra_lsp_server/src/lib.rs1
-rw-r--r--crates/ra_lsp_server/src/main_loop.rs76
-rw-r--r--crates/ra_lsp_server/src/main_loop/handlers.rs37
-rw-r--r--crates/ra_lsp_server/src/world.rs11
5 files changed, 140 insertions, 70 deletions
diff --git a/crates/ra_lsp_server/src/diagnostics.rs b/crates/ra_lsp_server/src/diagnostics.rs
new file mode 100644
index 000000000..ea08bce24
--- /dev/null
+++ b/crates/ra_lsp_server/src/diagnostics.rs
@@ -0,0 +1,85 @@
1//! Book keeping for keeping diagnostics easily in sync with the client.
2use lsp_types::{CodeActionOrCommand, Diagnostic, Range};
3use ra_ide::FileId;
4use std::{collections::HashMap, sync::Arc};
5
6pub type CheckFixes = Arc<HashMap<FileId, Vec<Fix>>>;
7
8#[derive(Debug, Default, Clone)]
9pub struct DiagnosticCollection {
10 pub native: HashMap<FileId, Vec<Diagnostic>>,
11 pub check: HashMap<FileId, Vec<Diagnostic>>,
12 pub check_fixes: CheckFixes,
13}
14
15#[derive(Debug, Clone)]
16pub struct Fix {
17 pub range: Range,
18 pub action: CodeActionOrCommand,
19}
20
21#[derive(Debug)]
22pub enum DiagnosticTask {
23 ClearCheck,
24 AddCheck(FileId, Diagnostic, Vec<CodeActionOrCommand>),
25 SetNative(FileId, Vec<Diagnostic>),
26}
27
28impl DiagnosticCollection {
29 pub fn clear_check(&mut self) -> Vec<FileId> {
30 Arc::make_mut(&mut self.check_fixes).clear();
31 self.check.drain().map(|(key, _value)| key).collect()
32 }
33
34 pub fn add_check_diagnostic(
35 &mut self,
36 file_id: FileId,
37 diagnostic: Diagnostic,
38 fixes: Vec<CodeActionOrCommand>,
39 ) {
40 let diagnostics = self.check.entry(file_id).or_default();
41 for existing_diagnostic in diagnostics.iter() {
42 if are_diagnostics_equal(&existing_diagnostic, &diagnostic) {
43 return;
44 }
45 }
46
47 let check_fixes = Arc::make_mut(&mut self.check_fixes);
48 check_fixes
49 .entry(file_id)
50 .or_default()
51 .extend(fixes.into_iter().map(|action| Fix { range: diagnostic.range, action }));
52 diagnostics.push(diagnostic);
53 }
54
55 pub fn set_native_diagnostics(&mut self, file_id: FileId, diagnostics: Vec<Diagnostic>) {
56 self.native.insert(file_id, diagnostics);
57 }
58
59 pub fn diagnostics_for(&self, file_id: FileId) -> impl Iterator<Item = &Diagnostic> {
60 let native = self.native.get(&file_id).into_iter().flatten();
61 let check = self.check.get(&file_id).into_iter().flatten();
62 native.chain(check)
63 }
64
65 pub fn handle_task(&mut self, task: DiagnosticTask) -> Vec<FileId> {
66 match task {
67 DiagnosticTask::ClearCheck => self.clear_check(),
68 DiagnosticTask::AddCheck(file_id, diagnostic, fixes) => {
69 self.add_check_diagnostic(file_id, diagnostic, fixes);
70 vec![file_id]
71 }
72 DiagnosticTask::SetNative(file_id, diagnostics) => {
73 self.set_native_diagnostics(file_id, diagnostics);
74 vec![file_id]
75 }
76 }
77 }
78}
79
80fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
81 left.source == right.source
82 && left.severity == right.severity
83 && left.range == right.range
84 && left.message == right.message
85}
diff --git a/crates/ra_lsp_server/src/lib.rs b/crates/ra_lsp_server/src/lib.rs
index 2ca149fd5..1208c1343 100644
--- a/crates/ra_lsp_server/src/lib.rs
+++ b/crates/ra_lsp_server/src/lib.rs
@@ -29,6 +29,7 @@ mod markdown;
29pub mod req; 29pub mod req;
30mod config; 30mod config;
31mod world; 31mod world;
32mod diagnostics;
32 33
33pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>; 34pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
34pub use crate::{ 35pub use crate::{
diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs
index 508fe08c0..12961ba37 100644
--- a/crates/ra_lsp_server/src/main_loop.rs
+++ b/crates/ra_lsp_server/src/main_loop.rs
@@ -17,16 +17,17 @@ use std::{
17use crossbeam_channel::{select, unbounded, RecvError, Sender}; 17use crossbeam_channel::{select, unbounded, RecvError, Sender};
18use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response}; 18use lsp_server::{Connection, ErrorCode, Message, Notification, Request, RequestId, Response};
19use lsp_types::{ClientCapabilities, NumberOrString}; 19use lsp_types::{ClientCapabilities, NumberOrString};
20use ra_cargo_watch::{CheckOptions, CheckTask}; 20use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckTask};
21use ra_ide::{Canceled, FeatureFlags, FileId, LibraryData, SourceRootId}; 21use ra_ide::{Canceled, FeatureFlags, FileId, LibraryData, SourceRootId};
22use ra_prof::profile; 22use ra_prof::profile;
23use ra_vfs::{VfsTask, Watch}; 23use ra_vfs::{VfsFile, VfsTask, Watch};
24use relative_path::RelativePathBuf; 24use relative_path::RelativePathBuf;
25use rustc_hash::FxHashSet; 25use rustc_hash::FxHashSet;
26use serde::{de::DeserializeOwned, Serialize}; 26use serde::{de::DeserializeOwned, Serialize};
27use threadpool::ThreadPool; 27use threadpool::ThreadPool;
28 28
29use crate::{ 29use crate::{
30 diagnostics::DiagnosticTask,
30 main_loop::{ 31 main_loop::{
31 pending_requests::{PendingRequest, PendingRequests}, 32 pending_requests::{PendingRequest, PendingRequests},
32 subscriptions::Subscriptions, 33 subscriptions::Subscriptions,
@@ -254,6 +255,7 @@ pub fn main_loop(
254enum Task { 255enum Task {
255 Respond(Response), 256 Respond(Response),
256 Notify(Notification), 257 Notify(Notification),
258 Diagnostic(DiagnosticTask),
257} 259}
258 260
259enum Event { 261enum Event {
@@ -359,7 +361,7 @@ fn loop_turn(
359 world_state.maybe_collect_garbage(); 361 world_state.maybe_collect_garbage();
360 loop_state.in_flight_libraries -= 1; 362 loop_state.in_flight_libraries -= 1;
361 } 363 }
362 Event::CheckWatcher(task) => on_check_task(pool, task, world_state, task_sender)?, 364 Event::CheckWatcher(task) => on_check_task(task, world_state, task_sender)?,
363 Event::Msg(msg) => match msg { 365 Event::Msg(msg) => match msg {
364 Message::Request(req) => on_request( 366 Message::Request(req) => on_request(
365 world_state, 367 world_state,
@@ -464,6 +466,7 @@ fn on_task(
464 Task::Notify(n) => { 466 Task::Notify(n) => {
465 msg_sender.send(n.into()).unwrap(); 467 msg_sender.send(n.into()).unwrap();
466 } 468 }
469 Task::Diagnostic(task) => on_diagnostic_task(task, msg_sender, state),
467 } 470 }
468} 471}
469 472
@@ -621,23 +624,26 @@ fn on_notification(
621} 624}
622 625
623fn on_check_task( 626fn on_check_task(
624 pool: &ThreadPool,
625 task: CheckTask, 627 task: CheckTask,
626 world_state: &mut WorldState, 628 world_state: &mut WorldState,
627 task_sender: &Sender<Task>, 629 task_sender: &Sender<Task>,
628) -> Result<()> { 630) -> Result<()> {
629 let urls = match task { 631 match task {
630 CheckTask::ClearDiagnostics => { 632 CheckTask::ClearDiagnostics => {
631 let state = Arc::get_mut(&mut world_state.check_watcher.state) 633 task_sender.send(Task::Diagnostic(DiagnosticTask::ClearCheck))?;
632 .expect("couldn't get check watcher state as mutable");
633 state.clear()
634 } 634 }
635 635
636 CheckTask::AddDiagnostic(url, diagnostic) => { 636 CheckTask::AddDiagnostic { url, diagnostic, fixes } => {
637 let state = Arc::get_mut(&mut world_state.check_watcher.state) 637 let path = url.to_file_path().map_err(|()| format!("invalid uri: {}", url))?;
638 .expect("couldn't get check watcher state as mutable"); 638 let file_id = world_state
639 state.add_diagnostic_with_fixes(url.clone(), diagnostic); 639 .vfs
640 vec![url] 640 .read()
641 .path2file(&path)
642 .map(|it| FileId(it.0))
643 .ok_or_else(|| format!("unknown file: {}", path.to_string_lossy()))?;
644
645 task_sender
646 .send(Task::Diagnostic(DiagnosticTask::AddCheck(file_id, diagnostic, fixes)))?;
641 } 647 }
642 648
643 CheckTask::Status(progress) => { 649 CheckTask::Status(progress) => {
@@ -647,31 +653,30 @@ fn on_check_task(
647 }; 653 };
648 let not = notification_new::<req::Progress>(params); 654 let not = notification_new::<req::Progress>(params);
649 task_sender.send(Task::Notify(not)).unwrap(); 655 task_sender.send(Task::Notify(not)).unwrap();
650 Vec::new()
651 } 656 }
652 }; 657 };
653 658
654 let subscriptions = urls 659 Ok(())
655 .into_iter() 660}
656 .map(|url| {
657 let path = url.to_file_path().map_err(|()| format!("invalid uri: {}", url))?;
658 Ok(world_state.vfs.read().path2file(&path).map(|it| FileId(it.0)))
659 })
660 .filter_map(|res| res.transpose())
661 .collect::<Result<Vec<_>>>()?;
662 661
663 // We manually send a diagnostic update when the watcher asks 662fn on_diagnostic_task(task: DiagnosticTask, msg_sender: &Sender<Message>, state: &mut WorldState) {
664 // us to, to avoid the issue of having to change the file to 663 let subscriptions = state.diagnostics.handle_task(task);
665 // receive updated diagnostics.
666 update_file_notifications_on_threadpool(
667 pool,
668 world_state.snapshot(),
669 false,
670 task_sender.clone(),
671 subscriptions,
672 );
673 664
674 Ok(()) 665 for file_id in subscriptions {
666 let path = state.vfs.read().file2path(VfsFile(file_id.0));
667 let uri = match url_from_path_with_drive_lowercasing(&path) {
668 Ok(uri) => uri,
669 Err(err) => {
670 log::error!("Couldn't convert path to url ({}): {:?}", err, path.to_string_lossy());
671 continue;
672 }
673 };
674
675 let diagnostics = state.diagnostics.diagnostics_for(file_id).cloned().collect();
676 let params = req::PublishDiagnosticsParams { uri, diagnostics, version: None };
677 let not = notification_new::<req::PublishDiagnostics>(params);
678 msg_sender.send(not.into()).unwrap();
679 }
675} 680}
676 681
677struct PoolDispatcher<'a> { 682struct PoolDispatcher<'a> {
@@ -819,9 +824,8 @@ fn update_file_notifications_on_threadpool(
819 log::error!("failed to compute diagnostics: {:?}", e); 824 log::error!("failed to compute diagnostics: {:?}", e);
820 } 825 }
821 } 826 }
822 Ok(params) => { 827 Ok(task) => {
823 let not = notification_new::<req::PublishDiagnostics>(params); 828 task_sender.send(Task::Diagnostic(task)).unwrap();
824 task_sender.send(Task::Notify(not)).unwrap();
825 } 829 }
826 } 830 }
827 } 831 }
diff --git a/crates/ra_lsp_server/src/main_loop/handlers.rs b/crates/ra_lsp_server/src/main_loop/handlers.rs
index 9aa1e7eea..839bca342 100644
--- a/crates/ra_lsp_server/src/main_loop/handlers.rs
+++ b/crates/ra_lsp_server/src/main_loop/handlers.rs
@@ -33,6 +33,7 @@ use crate::{
33 to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith, 33 to_call_hierarchy_item, to_location, Conv, ConvWith, FoldConvCtx, MapConvWith, TryConvWith,
34 TryConvWithToVec, 34 TryConvWithToVec,
35 }, 35 },
36 diagnostics::DiagnosticTask,
36 req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind}, 37 req::{self, Decoration, InlayHint, InlayHintsParams, InlayKind},
37 world::WorldSnapshot, 38 world::WorldSnapshot,
38 LspError, Result, 39 LspError, Result,
@@ -656,6 +657,7 @@ pub fn handle_code_action(
656 .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some()) 657 .filter(|(diag_range, _fix)| diag_range.intersection(&range).is_some())
657 .map(|(_range, fix)| fix); 658 .map(|(_range, fix)| fix);
658 659
660 // TODO: When done, we won't need this, only the one pulling from world.diagnostics
659 for source_edit in fixes_from_diagnostics { 661 for source_edit in fixes_from_diagnostics {
660 let title = source_edit.label.clone(); 662 let title = source_edit.label.clone();
661 let edit = source_edit.try_conv_with(&world)?; 663 let edit = source_edit.try_conv_with(&world)?;
@@ -676,28 +678,12 @@ pub fn handle_code_action(
676 res.push(action.into()); 678 res.push(action.into());
677 } 679 }
678 680
679 for fix in world.check_watcher.fixes_for(&params.text_document.uri).into_iter().flatten() { 681 for fix in world.check_fixes.get(&file_id).into_iter().flatten() {
680 let fix_range = fix.location.range.conv_with(&line_index); 682 let fix_range = fix.range.conv_with(&line_index);
681 if fix_range.intersection(&range).is_none() { 683 if fix_range.intersection(&range).is_none() {
682 continue; 684 continue;
683 } 685 }
684 686 res.push(fix.action.clone());
685 let edit = {
686 let edits = vec![TextEdit::new(fix.location.range, fix.replacement.clone())];
687 let mut edit_map = std::collections::HashMap::new();
688 edit_map.insert(fix.location.uri.clone(), edits);
689 WorkspaceEdit::new(edit_map)
690 };
691
692 let action = CodeAction {
693 title: fix.title.clone(),
694 kind: Some("quickfix".to_string()),
695 diagnostics: Some(fix.diagnostics.clone()),
696 edit: Some(edit),
697 command: None,
698 is_preferred: None,
699 };
700 res.push(action.into());
701 } 687 }
702 688
703 for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() { 689 for assist in world.analysis().assists(FileRange { file_id, range })?.into_iter() {
@@ -875,14 +861,10 @@ pub fn handle_document_highlight(
875 )) 861 ))
876} 862}
877 863
878pub fn publish_diagnostics( 864pub fn publish_diagnostics(world: &WorldSnapshot, file_id: FileId) -> Result<DiagnosticTask> {
879 world: &WorldSnapshot,
880 file_id: FileId,
881) -> Result<req::PublishDiagnosticsParams> {
882 let _p = profile("publish_diagnostics"); 865 let _p = profile("publish_diagnostics");
883 let uri = world.file_id_to_uri(file_id)?;
884 let line_index = world.analysis().file_line_index(file_id)?; 866 let line_index = world.analysis().file_line_index(file_id)?;
885 let mut diagnostics: Vec<Diagnostic> = world 867 let diagnostics: Vec<Diagnostic> = world
886 .analysis() 868 .analysis()
887 .diagnostics(file_id)? 869 .diagnostics(file_id)?
888 .into_iter() 870 .into_iter()
@@ -896,10 +878,7 @@ pub fn publish_diagnostics(
896 tags: None, 878 tags: None,
897 }) 879 })
898 .collect(); 880 .collect();
899 if let Some(check_diags) = world.check_watcher.diagnostics_for(&uri) { 881 Ok(DiagnosticTask::SetNative(file_id, diagnostics))
900 diagnostics.extend(check_diags.iter().cloned());
901 }
902 Ok(req::PublishDiagnosticsParams { uri, diagnostics, version: None })
903} 882}
904 883
905pub fn publish_decorations( 884pub fn publish_decorations(
diff --git a/crates/ra_lsp_server/src/world.rs b/crates/ra_lsp_server/src/world.rs
index 3059ef9ec..1ee02b47c 100644
--- a/crates/ra_lsp_server/src/world.rs
+++ b/crates/ra_lsp_server/src/world.rs
@@ -12,9 +12,7 @@ use crossbeam_channel::{unbounded, Receiver};
12use lsp_server::ErrorCode; 12use lsp_server::ErrorCode;
13use lsp_types::Url; 13use lsp_types::Url;
14use parking_lot::RwLock; 14use parking_lot::RwLock;
15use ra_cargo_watch::{ 15use ra_cargo_watch::{url_from_path_with_drive_lowercasing, CheckOptions, CheckWatcher};
16 url_from_path_with_drive_lowercasing, CheckOptions, CheckState, CheckWatcher,
17};
18use ra_ide::{ 16use ra_ide::{
19 Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData, 17 Analysis, AnalysisChange, AnalysisHost, CrateGraph, FeatureFlags, FileId, LibraryData,
20 SourceRootId, 18 SourceRootId,
@@ -25,6 +23,7 @@ use ra_vfs_glob::{Glob, RustPackageFilterBuilder};
25use relative_path::RelativePathBuf; 23use relative_path::RelativePathBuf;
26 24
27use crate::{ 25use crate::{
26 diagnostics::{CheckFixes, DiagnosticCollection},
28 main_loop::pending_requests::{CompletedRequest, LatestRequests}, 27 main_loop::pending_requests::{CompletedRequest, LatestRequests},
29 LspError, Result, 28 LspError, Result,
30}; 29};
@@ -55,6 +54,7 @@ pub struct WorldState {
55 pub task_receiver: Receiver<VfsTask>, 54 pub task_receiver: Receiver<VfsTask>,
56 pub latest_requests: Arc<RwLock<LatestRequests>>, 55 pub latest_requests: Arc<RwLock<LatestRequests>>,
57 pub check_watcher: CheckWatcher, 56 pub check_watcher: CheckWatcher,
57 pub diagnostics: DiagnosticCollection,
58} 58}
59 59
60/// An immutable snapshot of the world's state at a point in time. 60/// An immutable snapshot of the world's state at a point in time.
@@ -63,7 +63,7 @@ pub struct WorldSnapshot {
63 pub workspaces: Arc<Vec<ProjectWorkspace>>, 63 pub workspaces: Arc<Vec<ProjectWorkspace>>,
64 pub analysis: Analysis, 64 pub analysis: Analysis,
65 pub latest_requests: Arc<RwLock<LatestRequests>>, 65 pub latest_requests: Arc<RwLock<LatestRequests>>,
66 pub check_watcher: CheckState, 66 pub check_fixes: CheckFixes,
67 vfs: Arc<RwLock<Vfs>>, 67 vfs: Arc<RwLock<Vfs>>,
68} 68}
69 69
@@ -159,6 +159,7 @@ impl WorldState {
159 task_receiver, 159 task_receiver,
160 latest_requests: Default::default(), 160 latest_requests: Default::default(),
161 check_watcher, 161 check_watcher,
162 diagnostics: Default::default(),
162 } 163 }
163 } 164 }
164 165
@@ -220,7 +221,7 @@ impl WorldState {
220 analysis: self.analysis_host.analysis(), 221 analysis: self.analysis_host.analysis(),
221 vfs: Arc::clone(&self.vfs), 222 vfs: Arc::clone(&self.vfs),
222 latest_requests: Arc::clone(&self.latest_requests), 223 latest_requests: Arc::clone(&self.latest_requests),
223 check_watcher: (*self.check_watcher.state).clone(), 224 check_fixes: Arc::clone(&self.diagnostics.check_fixes),
224 } 225 }
225 } 226 }
226 227