]> git.lizzy.rs Git - rust.git/commitdiff
Initial implementation of cargo check watching
authorEmil Lauridsen <mine809@gmail.com>
Wed, 25 Dec 2019 11:21:38 +0000 (12:21 +0100)
committerEmil Lauridsen <mine809@gmail.com>
Wed, 25 Dec 2019 16:37:40 +0000 (17:37 +0100)
Cargo.lock
crates/ra_lsp_server/Cargo.toml
crates/ra_lsp_server/src/caps.rs
crates/ra_lsp_server/src/cargo_check.rs [new file with mode: 0644]
crates/ra_lsp_server/src/lib.rs
crates/ra_lsp_server/src/main_loop.rs
crates/ra_lsp_server/src/main_loop/handlers.rs
crates/ra_lsp_server/src/world.rs

index 792e30494a97529b8c65c4ac6e57893a5dd881fb..77c10a10e3f5d22a888432ef6671a10bf747dd8e 100644 (file)
@@ -1051,6 +1051,7 @@ dependencies = [
 name = "ra_lsp_server"
 version = "0.1.0"
 dependencies = [
+ "cargo_metadata 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "crossbeam-channel 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)",
  "jod-thread 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
index 030e9033ce7360e225bb3e3061a749b82d13f20f..aa1acdc330651092b20d79a5e987f4cbc0ce548c 100644 (file)
@@ -27,6 +27,7 @@ ra_project_model = { path = "../ra_project_model" }
 ra_prof = { path = "../ra_prof" }
 ra_vfs_glob = { path = "../ra_vfs_glob" }
 env_logger = { version = "0.7.1", default-features = false, features = ["humantime"] }
+cargo_metadata = "0.9.1"
 
 [dev-dependencies]
 tempfile = "3"
index ceb4c4259d7b457bf2bfd549de6c5ee776a483f9..0f84e7a34ef6a4aecda7a6630737e8fe5121516e 100644 (file)
@@ -6,7 +6,7 @@
     ImplementationProviderCapability, RenameOptions, RenameProviderCapability,
     SelectionRangeProviderCapability, ServerCapabilities, SignatureHelpOptions,
     TextDocumentSyncCapability, TextDocumentSyncKind, TextDocumentSyncOptions,
-    TypeDefinitionProviderCapability, WorkDoneProgressOptions,
+    TypeDefinitionProviderCapability, WorkDoneProgressOptions, SaveOptions
 };
 
 pub fn server_capabilities() -> ServerCapabilities {
@@ -16,7 +16,7 @@ pub fn server_capabilities() -> ServerCapabilities {
             change: Some(TextDocumentSyncKind::Full),
             will_save: None,
             will_save_wait_until: None,
-            save: None,
+            save: Some(SaveOptions::default()),
         })),
         hover_provider: Some(true),
         completion_provider: Some(CompletionOptions {
diff --git a/crates/ra_lsp_server/src/cargo_check.rs b/crates/ra_lsp_server/src/cargo_check.rs
new file mode 100644 (file)
index 0000000..d5ff021
--- /dev/null
@@ -0,0 +1,533 @@
+use cargo_metadata::{
+    diagnostic::{
+        Applicability, Diagnostic as RustDiagnostic, DiagnosticLevel, DiagnosticSpan,
+        DiagnosticSpanMacroExpansion,
+    },
+    Message,
+};
+use crossbeam_channel::{select, unbounded, Receiver, RecvError, Sender, TryRecvError};
+use lsp_types::{
+    Diagnostic, DiagnosticRelatedInformation, DiagnosticSeverity, DiagnosticTag, Location,
+    NumberOrString, Position, Range, Url,
+};
+use parking_lot::RwLock;
+use std::{
+    collections::HashMap,
+    fmt::Write,
+    path::PathBuf,
+    process::{Command, Stdio},
+    sync::Arc,
+    thread::JoinHandle,
+    time::Instant,
+};
+
+#[derive(Debug)]
+pub struct CheckWatcher {
+    pub task_recv: Receiver<CheckTask>,
+    pub cmd_send: Sender<CheckCommand>,
+    pub shared: Arc<RwLock<CheckWatcherSharedState>>,
+    handle: JoinHandle<()>,
+}
+
+impl CheckWatcher {
+    pub fn new(workspace_root: PathBuf) -> CheckWatcher {
+        let shared = Arc::new(RwLock::new(CheckWatcherSharedState::new()));
+
+        let (task_send, task_recv) = unbounded::<CheckTask>();
+        let (cmd_send, cmd_recv) = unbounded::<CheckCommand>();
+        let shared_ = shared.clone();
+        let handle = std::thread::spawn(move || {
+            let mut check = CheckWatcherState::new(shared_, workspace_root);
+            check.run(&task_send, &cmd_recv);
+        });
+
+        CheckWatcher { task_recv, cmd_send, handle, shared }
+    }
+
+    pub fn update(&self) {
+        self.cmd_send.send(CheckCommand::Update).unwrap();
+    }
+}
+
+pub struct CheckWatcherState {
+    workspace_root: PathBuf,
+    running: bool,
+    watcher: WatchThread,
+    last_update_req: Option<Instant>,
+    shared: Arc<RwLock<CheckWatcherSharedState>>,
+}
+
+#[derive(Debug)]
+pub struct CheckWatcherSharedState {
+    diagnostic_collection: HashMap<Url, Vec<Diagnostic>>,
+    suggested_fix_collection: HashMap<Url, Vec<SuggestedFix>>,
+}
+
+impl CheckWatcherSharedState {
+    fn new() -> CheckWatcherSharedState {
+        CheckWatcherSharedState {
+            diagnostic_collection: HashMap::new(),
+            suggested_fix_collection: HashMap::new(),
+        }
+    }
+
+    pub fn clear(&mut self, task_send: &Sender<CheckTask>) {
+        let cleared_files: Vec<Url> = self.diagnostic_collection.keys().cloned().collect();
+
+        self.diagnostic_collection.clear();
+        self.suggested_fix_collection.clear();
+
+        for uri in cleared_files {
+            task_send.send(CheckTask::Update(uri.clone())).unwrap();
+        }
+    }
+
+    pub fn diagnostics_for(&self, uri: &Url) -> Option<&[Diagnostic]> {
+        self.diagnostic_collection.get(uri).map(|d| d.as_slice())
+    }
+
+    pub fn fixes_for(&self, uri: &Url) -> Option<&[SuggestedFix]> {
+        self.suggested_fix_collection.get(uri).map(|d| d.as_slice())
+    }
+
+    fn add_diagnostic(&mut self, file_uri: Url, diagnostic: Diagnostic) {
+        let diagnostics = self.diagnostic_collection.entry(file_uri).or_default();
+
+        // If we're building multiple targets it's possible we've already seen this diagnostic
+        let is_duplicate = diagnostics.iter().any(|d| are_diagnostics_equal(d, &diagnostic));
+        if is_duplicate {
+            return;
+        }
+
+        diagnostics.push(diagnostic);
+    }
+
+    fn add_suggested_fix_for_diagnostic(
+        &mut self,
+        mut suggested_fix: SuggestedFix,
+        diagnostic: &Diagnostic,
+    ) {
+        let file_uri = suggested_fix.location.uri.clone();
+        let file_suggestions = self.suggested_fix_collection.entry(file_uri).or_default();
+
+        let existing_suggestion: Option<&mut SuggestedFix> =
+            file_suggestions.iter_mut().find(|s| s == &&suggested_fix);
+        if let Some(existing_suggestion) = existing_suggestion {
+            // The existing suggestion also applies to this new diagnostic
+            existing_suggestion.diagnostics.push(diagnostic.clone());
+        } else {
+            // We haven't seen this suggestion before
+            suggested_fix.diagnostics.push(diagnostic.clone());
+            file_suggestions.push(suggested_fix);
+        }
+    }
+}
+
+#[derive(Debug)]
+pub enum CheckTask {
+    Update(Url),
+}
+
+pub enum CheckCommand {
+    Update,
+}
+
+impl CheckWatcherState {
+    pub fn new(
+        shared: Arc<RwLock<CheckWatcherSharedState>>,
+        workspace_root: PathBuf,
+    ) -> CheckWatcherState {
+        let watcher = WatchThread::new(&workspace_root);
+        CheckWatcherState { workspace_root, running: false, watcher, last_update_req: None, shared }
+    }
+
+    pub fn run(&mut self, task_send: &Sender<CheckTask>, cmd_recv: &Receiver<CheckCommand>) {
+        self.running = true;
+        while self.running {
+            select! {
+                recv(&cmd_recv) -> cmd => match cmd {
+                    Ok(cmd) => self.handle_command(cmd),
+                    Err(RecvError) => {
+                        // Command channel has closed, so shut down
+                        self.running = false;
+                    },
+                },
+                recv(self.watcher.message_recv) -> msg => match msg {
+                    Ok(msg) => self.handle_message(msg, task_send),
+                    Err(RecvError) => {},
+                }
+            };
+
+            if self.should_recheck() {
+                self.last_update_req.take();
+                self.shared.write().clear(task_send);
+
+                self.watcher.cancel();
+                self.watcher = WatchThread::new(&self.workspace_root);
+            }
+        }
+    }
+
+    fn should_recheck(&mut self) -> bool {
+        if let Some(_last_update_req) = &self.last_update_req {
+            // We currently only request an update on save, as we need up to
+            // date source on disk for cargo check to do it's magic, so we
+            // don't really need to debounce the requests at this point.
+            return true;
+        }
+        false
+    }
+
+    fn handle_command(&mut self, cmd: CheckCommand) {
+        match cmd {
+            CheckCommand::Update => self.last_update_req = Some(Instant::now()),
+        }
+    }
+
+    fn handle_message(&mut self, msg: cargo_metadata::Message, task_send: &Sender<CheckTask>) {
+        match msg {
+            Message::CompilerArtifact(_msg) => {
+                // TODO: Status display
+            }
+
+            Message::CompilerMessage(msg) => {
+                let map_result =
+                    match map_rust_diagnostic_to_lsp(&msg.message, &self.workspace_root) {
+                        Some(map_result) => map_result,
+                        None => return,
+                    };
+
+                let MappedRustDiagnostic { location, diagnostic, suggested_fixes } = map_result;
+                let file_uri = location.uri.clone();
+
+                if !suggested_fixes.is_empty() {
+                    for suggested_fix in suggested_fixes {
+                        self.shared
+                            .write()
+                            .add_suggested_fix_for_diagnostic(suggested_fix, &diagnostic);
+                    }
+                }
+                self.shared.write().add_diagnostic(file_uri, diagnostic);
+
+                task_send.send(CheckTask::Update(location.uri)).unwrap();
+            }
+
+            Message::BuildScriptExecuted(_msg) => {}
+            Message::Unknown => {}
+        }
+    }
+}
+
+/// WatchThread exists to wrap around the communication needed to be able to
+/// run `cargo check` without blocking. Currently the Rust standard library
+/// doesn't provide a way to read sub-process output without blocking, so we
+/// have to wrap sub-processes output handling in a thread and pass messages
+/// back over a channel.
+struct WatchThread {
+    message_recv: Receiver<cargo_metadata::Message>,
+    cancel_send: Sender<()>,
+}
+
+impl WatchThread {
+    fn new(workspace_root: &PathBuf) -> WatchThread {
+        let manifest_path = format!("{}/Cargo.toml", workspace_root.to_string_lossy());
+        let (message_send, message_recv) = unbounded();
+        let (cancel_send, cancel_recv) = unbounded();
+        std::thread::spawn(move || {
+            let mut command = Command::new("cargo")
+                .args(&["check", "--message-format=json", "--manifest-path", &manifest_path])
+                .stdout(Stdio::piped())
+                .stderr(Stdio::null())
+                .spawn()
+                .expect("couldn't launch cargo");
+
+            for message in cargo_metadata::parse_messages(command.stdout.take().unwrap()) {
+                match cancel_recv.try_recv() {
+                    Ok(()) | Err(TryRecvError::Disconnected) => {
+                        command.kill().expect("couldn't kill command");
+                    }
+                    Err(TryRecvError::Empty) => (),
+                }
+
+                message_send.send(message.unwrap()).unwrap();
+            }
+        });
+        WatchThread { message_recv, cancel_send }
+    }
+
+    fn cancel(&self) {
+        let _ = self.cancel_send.send(());
+    }
+}
+
+/// Converts a Rust level string to a LSP severity
+fn map_level_to_severity(val: DiagnosticLevel) -> Option<DiagnosticSeverity> {
+    match val {
+        DiagnosticLevel::Ice => Some(DiagnosticSeverity::Error),
+        DiagnosticLevel::Error => Some(DiagnosticSeverity::Error),
+        DiagnosticLevel::Warning => Some(DiagnosticSeverity::Warning),
+        DiagnosticLevel::Note => Some(DiagnosticSeverity::Information),
+        DiagnosticLevel::Help => Some(DiagnosticSeverity::Hint),
+        DiagnosticLevel::Unknown => None,
+    }
+}
+
+/// Check whether a file name is from macro invocation
+fn is_from_macro(file_name: &str) -> bool {
+    file_name.starts_with('<') && file_name.ends_with('>')
+}
+
+/// Converts a Rust macro span to a LSP location recursively
+fn map_macro_span_to_location(
+    span_macro: &DiagnosticSpanMacroExpansion,
+    workspace_root: &PathBuf,
+) -> Option<Location> {
+    if !is_from_macro(&span_macro.span.file_name) {
+        return Some(map_span_to_location(&span_macro.span, workspace_root));
+    }
+
+    if let Some(expansion) = &span_macro.span.expansion {
+        return map_macro_span_to_location(&expansion, workspace_root);
+    }
+
+    None
+}
+
+/// Converts a Rust span to a LSP location
+fn map_span_to_location(span: &DiagnosticSpan, workspace_root: &PathBuf) -> Location {
+    if is_from_macro(&span.file_name) && span.expansion.is_some() {
+        let expansion = span.expansion.as_ref().unwrap();
+        if let Some(macro_range) = map_macro_span_to_location(&expansion, workspace_root) {
+            return macro_range;
+        }
+    }
+
+    let mut file_name = workspace_root.clone();
+    file_name.push(&span.file_name);
+    let uri = Url::from_file_path(file_name).unwrap();
+
+    let range = Range::new(
+        Position::new(span.line_start as u64 - 1, span.column_start as u64 - 1),
+        Position::new(span.line_end as u64 - 1, span.column_end as u64 - 1),
+    );
+
+    Location { uri, range }
+}
+
+/// Converts a secondary Rust span to a LSP related information
+///
+/// If the span is unlabelled this will return `None`.
+fn map_secondary_span_to_related(
+    span: &DiagnosticSpan,
+    workspace_root: &PathBuf,
+) -> Option<DiagnosticRelatedInformation> {
+    if let Some(label) = &span.label {
+        let location = map_span_to_location(span, workspace_root);
+        Some(DiagnosticRelatedInformation { location, message: label.clone() })
+    } else {
+        // Nothing to label this with
+        None
+    }
+}
+
+/// Determines if diagnostic is related to unused code
+fn is_unused_or_unnecessary(rd: &RustDiagnostic) -> bool {
+    if let Some(code) = &rd.code {
+        match code.code.as_str() {
+            "dead_code" | "unknown_lints" | "unreachable_code" | "unused_attributes"
+            | "unused_imports" | "unused_macros" | "unused_variables" => true,
+            _ => false,
+        }
+    } else {
+        false
+    }
+}
+
+/// Determines if diagnostic is related to deprecated code
+fn is_deprecated(rd: &RustDiagnostic) -> bool {
+    if let Some(code) = &rd.code {
+        match code.code.as_str() {
+            "deprecated" => true,
+            _ => false,
+        }
+    } else {
+        false
+    }
+}
+
+#[derive(Debug)]
+pub struct SuggestedFix {
+    pub title: String,
+    pub location: Location,
+    pub replacement: String,
+    pub applicability: Applicability,
+    pub diagnostics: Vec<Diagnostic>,
+}
+
+impl std::cmp::PartialEq<SuggestedFix> for SuggestedFix {
+    fn eq(&self, other: &SuggestedFix) -> bool {
+        if self.title == other.title
+            && self.location == other.location
+            && self.replacement == other.replacement
+        {
+            // Applicability doesn't impl PartialEq...
+            match (&self.applicability, &other.applicability) {
+                (Applicability::MachineApplicable, Applicability::MachineApplicable) => true,
+                (Applicability::HasPlaceholders, Applicability::HasPlaceholders) => true,
+                (Applicability::MaybeIncorrect, Applicability::MaybeIncorrect) => true,
+                (Applicability::Unspecified, Applicability::Unspecified) => true,
+                _ => false,
+            }
+        } else {
+            false
+        }
+    }
+}
+
+enum MappedRustChildDiagnostic {
+    Related(DiagnosticRelatedInformation),
+    SuggestedFix(SuggestedFix),
+    MessageLine(String),
+}
+
+fn map_rust_child_diagnostic(
+    rd: &RustDiagnostic,
+    workspace_root: &PathBuf,
+) -> MappedRustChildDiagnostic {
+    let span: &DiagnosticSpan = match rd.spans.iter().find(|s| s.is_primary) {
+        Some(span) => span,
+        None => {
+            // `rustc` uses these spanless children as a way to print multi-line
+            // messages
+            return MappedRustChildDiagnostic::MessageLine(rd.message.clone());
+        }
+    };
+
+    // If we have a primary span use its location, otherwise use the parent
+    let location = map_span_to_location(&span, workspace_root);
+
+    if let Some(suggested_replacement) = &span.suggested_replacement {
+        // Include our replacement in the title unless it's empty
+        let title = if !suggested_replacement.is_empty() {
+            format!("{}: '{}'", rd.message, suggested_replacement)
+        } else {
+            rd.message.clone()
+        };
+
+        MappedRustChildDiagnostic::SuggestedFix(SuggestedFix {
+            title,
+            location,
+            replacement: suggested_replacement.clone(),
+            applicability: span.suggestion_applicability.clone().unwrap_or(Applicability::Unknown),
+            diagnostics: vec![],
+        })
+    } else {
+        MappedRustChildDiagnostic::Related(DiagnosticRelatedInformation {
+            location,
+            message: rd.message.clone(),
+        })
+    }
+}
+
+struct MappedRustDiagnostic {
+    location: Location,
+    diagnostic: Diagnostic,
+    suggested_fixes: Vec<SuggestedFix>,
+}
+
+/// Converts a Rust root diagnostic to LSP form
+///
+/// This flattens the Rust diagnostic by:
+///
+/// 1. Creating a LSP diagnostic with the root message and primary span.
+/// 2. Adding any labelled secondary spans to `relatedInformation`
+/// 3. Categorising child diagnostics as either `SuggestedFix`es,
+///    `relatedInformation` or additional message lines.
+///
+/// If the diagnostic has no primary span this will return `None`
+fn map_rust_diagnostic_to_lsp(
+    rd: &RustDiagnostic,
+    workspace_root: &PathBuf,
+) -> Option<MappedRustDiagnostic> {
+    let primary_span = rd.spans.iter().find(|s| s.is_primary)?;
+
+    let location = map_span_to_location(&primary_span, workspace_root);
+
+    let severity = map_level_to_severity(rd.level);
+    let mut primary_span_label = primary_span.label.as_ref();
+
+    let mut source = String::from("rustc");
+    let mut code = rd.code.as_ref().map(|c| c.code.clone());
+    if let Some(code_val) = &code {
+        // See if this is an RFC #2103 scoped lint (e.g. from Clippy)
+        let scoped_code: Vec<&str> = code_val.split("::").collect();
+        if scoped_code.len() == 2 {
+            source = String::from(scoped_code[0]);
+            code = Some(String::from(scoped_code[1]));
+        }
+    }
+
+    let mut related_information = vec![];
+    let mut tags = vec![];
+
+    for secondary_span in rd.spans.iter().filter(|s| !s.is_primary) {
+        let related = map_secondary_span_to_related(secondary_span, workspace_root);
+        if let Some(related) = related {
+            related_information.push(related);
+        }
+    }
+
+    let mut suggested_fixes = vec![];
+    let mut message = rd.message.clone();
+    for child in &rd.children {
+        let child = map_rust_child_diagnostic(&child, workspace_root);
+        match child {
+            MappedRustChildDiagnostic::Related(related) => related_information.push(related),
+            MappedRustChildDiagnostic::SuggestedFix(suggested_fix) => {
+                suggested_fixes.push(suggested_fix)
+            }
+            MappedRustChildDiagnostic::MessageLine(message_line) => {
+                write!(&mut message, "\n{}", message_line).unwrap();
+
+                // These secondary messages usually duplicate the content of the
+                // primary span label.
+                primary_span_label = None;
+            }
+        }
+    }
+
+    if let Some(primary_span_label) = primary_span_label {
+        write!(&mut message, "\n{}", primary_span_label).unwrap();
+    }
+
+    if is_unused_or_unnecessary(rd) {
+        tags.push(DiagnosticTag::Unnecessary);
+    }
+
+    if is_deprecated(rd) {
+        tags.push(DiagnosticTag::Deprecated);
+    }
+
+    let diagnostic = Diagnostic {
+        range: location.range,
+        severity,
+        code: code.map(NumberOrString::String),
+        source: Some(source),
+        message: rd.message.clone(),
+        related_information: if !related_information.is_empty() {
+            Some(related_information)
+        } else {
+            None
+        },
+        tags: if !tags.is_empty() { Some(tags) } else { None },
+    };
+
+    Some(MappedRustDiagnostic { location, diagnostic, suggested_fixes })
+}
+
+fn are_diagnostics_equal(left: &Diagnostic, right: &Diagnostic) -> bool {
+    left.source == right.source
+        && left.severity == right.severity
+        && left.range == right.range
+        && left.message == right.message
+}
index 2ca149fd56b82738216a6eaf665869a97740f785..2811231facd90e8173ca22ccfb4ff6f64440a6c2 100644 (file)
@@ -22,6 +22,7 @@ macro_rules! print {
 }
 
 mod caps;
+mod cargo_check;
 mod cargo_target_spec;
 mod conv;
 mod main_loop;
index dda318e43eb80bf96146b1dd8f05af83333ad062..943d38943f1cb6d9dd7518dc92c902a4b90b755f 100644 (file)
@@ -19,6 +19,7 @@
 use threadpool::ThreadPool;
 
 use crate::{
+    cargo_check::CheckTask,
     main_loop::{
         pending_requests::{PendingRequest, PendingRequests},
         subscriptions::Subscriptions,
@@ -176,7 +177,8 @@ pub fn main_loop(
                     Ok(task) => Event::Vfs(task),
                     Err(RecvError) => Err("vfs died")?,
                 },
-                recv(libdata_receiver) -> data => Event::Lib(data.unwrap())
+                recv(libdata_receiver) -> data => Event::Lib(data.unwrap()),
+                recv(world_state.check_watcher.task_recv) -> task => Event::CheckWatcher(task.unwrap())
             };
             if let Event::Msg(Message::Request(req)) = &event {
                 if connection.handle_shutdown(&req)? {
@@ -222,6 +224,7 @@ enum Event {
     Task(Task),
     Vfs(VfsTask),
     Lib(LibraryData),
+    CheckWatcher(CheckTask),
 }
 
 impl fmt::Debug for Event {
@@ -259,6 +262,7 @@ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
             Event::Task(it) => fmt::Debug::fmt(it, f),
             Event::Vfs(it) => fmt::Debug::fmt(it, f),
             Event::Lib(it) => fmt::Debug::fmt(it, f),
+            Event::CheckWatcher(it) => fmt::Debug::fmt(it, f),
         }
     }
 }
@@ -318,6 +322,20 @@ fn loop_turn(
             world_state.maybe_collect_garbage();
             loop_state.in_flight_libraries -= 1;
         }
+        Event::CheckWatcher(task) => match task {
+            CheckTask::Update(uri) => {
+                // We manually send a diagnostic update when the watcher asks
+                // us to, to avoid the issue of having to change the file to
+                // receive updated diagnostics.
+                let path = uri.to_file_path().map_err(|()| format!("invalid uri: {}", uri))?;
+                if let Some(file_id) = world_state.vfs.read().path2file(&path) {
+                    let params =
+                        handlers::publish_diagnostics(&world_state.snapshot(), FileId(file_id.0))?;
+                    let not = notification_new::<req::PublishDiagnostics>(params);
+                    task_sender.send(Task::Notify(not)).unwrap();
+                }
+            }
+        },
         Event::Msg(msg) => match msg {
             Message::Request(req) => on_request(
                 world_state,
@@ -517,6 +535,13 @@ fn on_notification(
         }
         Err(not) => not,
     };
+    let not = match notification_cast::<req::DidSaveTextDocument>(not) {
+        Ok(_params) => {
+            state.check_watcher.update();
+            return Ok(());
+        }
+        Err(not) => not,
+    };
     let not = match notification_cast::<req::DidCloseTextDocument>(not) {
         Ok(params) => {
             let uri = params.text_document.uri;
index 39eb3df3e55acf72dae02b9e4a4433f40d245d9b..331beab1345e679c00a50233e869adf05698dc23 100644 (file)
@@ -654,6 +654,29 @@ pub fn handle_code_action(
         res.push(action.into());
     }
 
+    for fix in world.check_watcher.read().fixes_for(&params.text_document.uri).into_iter().flatten()
+    {
+        let fix_range = fix.location.range.conv_with(&line_index);
+        if fix_range.intersection(&range).is_none() {
+            continue;
+        }
+
+        let edits = vec![TextEdit::new(fix.location.range, fix.replacement.clone())];
+        let mut edit_map = std::collections::HashMap::new();
+        edit_map.insert(fix.location.uri.clone(), edits);
+        let edit = WorkspaceEdit::new(edit_map);
+
+        let action = CodeAction {
+            title: fix.title.clone(),
+            kind: Some("quickfix".to_string()),
+            diagnostics: Some(fix.diagnostics.clone()),
+            edit: Some(edit),
+            command: None,
+            is_preferred: None,
+        };
+        res.push(action.into());
+    }
+
     for assist in assists {
         let title = assist.change.label.clone();
         let edit = assist.change.try_conv_with(&world)?;
@@ -820,7 +843,7 @@ pub fn publish_diagnostics(
     let _p = profile("publish_diagnostics");
     let uri = world.file_id_to_uri(file_id)?;
     let line_index = world.analysis().file_line_index(file_id)?;
-    let diagnostics = world
+    let mut diagnostics: Vec<Diagnostic> = world
         .analysis()
         .diagnostics(file_id)?
         .into_iter()
@@ -834,6 +857,9 @@ pub fn publish_diagnostics(
             tags: None,
         })
         .collect();
+    if let Some(check_diags) = world.check_watcher.read().diagnostics_for(&uri) {
+        diagnostics.extend(check_diags.iter().cloned());
+    }
     Ok(req::PublishDiagnosticsParams { uri, diagnostics, version: None })
 }
 
index 79431e7e6f62087e72635528e3c0190362d2eecd..8e9380ca07ecf7fb6ef9b514d467732b8deba6a3 100644 (file)
@@ -24,6 +24,7 @@
 
 use crate::{
     main_loop::pending_requests::{CompletedRequest, LatestRequests},
+    cargo_check::{CheckWatcher, CheckWatcherSharedState},
     LspError, Result,
 };
 use std::str::FromStr;
@@ -52,6 +53,7 @@ pub struct WorldState {
     pub vfs: Arc<RwLock<Vfs>>,
     pub task_receiver: Receiver<VfsTask>,
     pub latest_requests: Arc<RwLock<LatestRequests>>,
+    pub check_watcher: CheckWatcher,
 }
 
 /// An immutable snapshot of the world's state at a point in time.
@@ -61,6 +63,7 @@ pub struct WorldSnapshot {
     pub analysis: Analysis,
     pub vfs: Arc<RwLock<Vfs>>,
     pub latest_requests: Arc<RwLock<LatestRequests>>,
+    pub check_watcher: Arc<RwLock<CheckWatcherSharedState>>,
 }
 
 impl WorldState {
@@ -127,6 +130,9 @@ pub fn new(
         }
         change.set_crate_graph(crate_graph);
 
+        // FIXME: Figure out the multi-workspace situation
+        let check_watcher = CheckWatcher::new(folder_roots.first().cloned().unwrap());
+
         let mut analysis_host = AnalysisHost::new(lru_capacity, feature_flags);
         analysis_host.apply_change(change);
         WorldState {
@@ -138,6 +144,7 @@ pub fn new(
             vfs: Arc::new(RwLock::new(vfs)),
             task_receiver,
             latest_requests: Default::default(),
+            check_watcher,
         }
     }
 
@@ -199,6 +206,7 @@ pub fn snapshot(&self) -> WorldSnapshot {
             analysis: self.analysis_host.analysis(),
             vfs: Arc::clone(&self.vfs),
             latest_requests: Arc::clone(&self.latest_requests),
+            check_watcher: self.check_watcher.shared.clone(),
         }
     }