Skip to content

Commit

Permalink
Add a UnindexedProject notification and a corresponding setting.
Browse files Browse the repository at this point in the history
  • Loading branch information
davidbarsky committed Jan 16, 2024
1 parent 0333646 commit 73f8fad
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 9 deletions.
9 changes: 8 additions & 1 deletion crates/rust-analyzer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,9 @@ config_data! {
/// Whether to show `can't find Cargo.toml` error message.
notifications_cargoTomlNotFound: bool = "true",

/// Whether to send an UnindexedProject notification to the client.
notifications_unindexedProject: bool = "false",

/// How many worker threads in the main loop. The default `null` means to pick automatically.
numThreads: Option<usize> = "null",

Expand Down Expand Up @@ -737,6 +740,7 @@ pub enum FilesWatcher {
#[derive(Debug, Clone)]
pub struct NotificationsConfig {
pub cargo_toml_not_found: bool,
pub unindexed_project: bool,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -1212,7 +1216,10 @@ impl Config {
}

pub fn notifications(&self) -> NotificationsConfig {
NotificationsConfig { cargo_toml_not_found: self.data.notifications_cargoTomlNotFound }
NotificationsConfig {
cargo_toml_not_found: self.data.notifications_cargoTomlNotFound,
unindexed_project: self.data.notifications_unindexedProject,
}
}

pub fn cargo_autoreload(&self) -> bool {
Expand Down
21 changes: 16 additions & 5 deletions crates/rust-analyzer/src/global_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use crate::{
mem_docs::MemDocs,
op_queue::OpQueue,
reload,
task_pool::TaskPool,
task_pool::{TaskPool, TaskQueue},
};

// Enforces drop order
Expand All @@ -54,7 +54,7 @@ type ReqQueue = lsp_server::ReqQueue<(String, Instant), ReqHandler>;
/// Note that this struct has more than one impl in various modules!
#[doc(alias = "GlobalMess")]
pub(crate) struct GlobalState {
sender: Sender<lsp_server::Message>,
pub(crate) sender: Sender<lsp_server::Message>,
req_queue: ReqQueue,

pub(crate) task_pool: Handle<TaskPool<Task>, Receiver<Task>>,
Expand Down Expand Up @@ -84,7 +84,7 @@ pub(crate) struct GlobalState {
pub(crate) last_flycheck_error: Option<String>,

// VFS
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle + Send>, Receiver<vfs::loader::Message>>,
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, IntMap<FileId, LineEndings>)>>,
pub(crate) vfs_config_version: u32,
pub(crate) vfs_progress_config_version: u32,
Expand Down Expand Up @@ -126,6 +126,10 @@ pub(crate) struct GlobalState {
OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>,
pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, bool>,
pub(crate) prime_caches_queue: OpQueue,

/// a deferred task queue. this should only be used if the enqueued Task
/// can only run *after* [`GlobalState::process_changes`] has been called.
pub(crate) deferred_task_queue: TaskQueue,
}

/// An immutable snapshot of the world's state at a point in time.
Expand All @@ -150,7 +154,7 @@ impl GlobalState {
let (sender, receiver) = unbounded::<vfs::loader::Message>();
let handle: vfs_notify::NotifyHandle =
vfs::loader::Handle::spawn(Box::new(move |msg| sender.send(msg).unwrap()));
let handle = Box::new(handle) as Box<dyn vfs::loader::Handle>;
let handle = Box::new(handle) as Box<dyn vfs::loader::Handle + Send>;
Handle { handle, receiver }
};

Expand All @@ -165,6 +169,11 @@ impl GlobalState {
Handle { handle, receiver }
};

let task_queue = {
let (sender, receiver) = unbounded();
TaskQueue { sender, receiver }
};

let mut analysis_host = AnalysisHost::new(config.lru_parse_query_capacity());
if let Some(capacities) = config.lru_query_capacities() {
analysis_host.update_lru_capacities(capacities);
Expand Down Expand Up @@ -208,6 +217,8 @@ impl GlobalState {
fetch_proc_macros_queue: OpQueue::default(),

prime_caches_queue: OpQueue::default(),

deferred_task_queue: task_queue,
};
// Apply any required database inputs from the config.
this.update_configuration(config);
Expand Down Expand Up @@ -425,7 +436,7 @@ impl GlobalState {
self.req_queue.incoming.is_completed(&request.id)
}

fn send(&self, message: lsp_server::Message) {
pub(crate) fn send(&self, message: lsp_server::Message) {
self.sender.send(message).unwrap()
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/rust-analyzer/src/handlers/notification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,15 @@ pub(crate) fn handle_did_open_text_document(
if already_exists {
tracing::error!("duplicate DidOpenTextDocument: {}", path);
}

state.vfs.write().0.set_file_contents(path, Some(params.text_document.text.into_bytes()));
if state.config.notifications().unindexed_project {
tracing::debug!("queuing task");
let _ = state
.deferred_task_queue
.sender
.send(crate::main_loop::QueuedTask::CheckIfIndexed(params.text_document.uri));
}
}
Ok(())
}
Expand Down
13 changes: 13 additions & 0 deletions crates/rust-analyzer/src/lsp/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,3 +696,16 @@ pub struct CompletionImport {
pub struct ClientCommandOptions {
pub commands: Vec<String>,
}

pub enum UnindexedProject {}

impl Notification for UnindexedProject {
type Params = UnindexedProjectParams;
const METHOD: &'static str = "rust-analyzer/unindexedProject";
}

#[derive(Deserialize, Serialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct UnindexedProjectParams {
pub text_documents: Vec<TextDocumentIdentifier>,
}
50 changes: 49 additions & 1 deletion crates/rust-analyzer/src/main_loop.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
//! The main loop of `rust-analyzer` responsible for dispatching LSP
//! requests/replies and notifications back to the client.
use crate::lsp::ext;
use std::{
fmt,
time::{Duration, Instant},
Expand Down Expand Up @@ -56,10 +57,16 @@ pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> {
enum Event {
Lsp(lsp_server::Message),
Task(Task),
QueuedTask(QueuedTask),
Vfs(vfs::loader::Message),
Flycheck(flycheck::Message),
}

#[derive(Debug)]
pub(crate) enum QueuedTask {
CheckIfIndexed(lsp_types::Url),
}

#[derive(Debug)]
pub(crate) enum Task {
Response(lsp_server::Response),
Expand Down Expand Up @@ -104,6 +111,7 @@ impl fmt::Debug for Event {
match self {
Event::Lsp(it) => fmt::Debug::fmt(it, f),
Event::Task(it) => fmt::Debug::fmt(it, f),
Event::QueuedTask(it) => fmt::Debug::fmt(it, f),
Event::Vfs(it) => fmt::Debug::fmt(it, f),
Event::Flycheck(it) => fmt::Debug::fmt(it, f),
}
Expand Down Expand Up @@ -182,6 +190,9 @@ impl GlobalState {
recv(self.task_pool.receiver) -> task =>
Some(Event::Task(task.unwrap())),

recv(self.deferred_task_queue.receiver) -> task =>
Some(Event::QueuedTask(task.unwrap())),

recv(self.fmt_pool.receiver) -> task =>
Some(Event::Task(task.unwrap())),

Expand All @@ -199,7 +210,7 @@ impl GlobalState {
let _p = profile::span("GlobalState::handle_event");

let event_dbg_msg = format!("{event:?}");
tracing::debug!("{:?} handle_event({})", loop_start, event_dbg_msg);
tracing::debug!(?loop_start, ?event, "handle_event");
if tracing::enabled!(tracing::Level::INFO) {
let task_queue_len = self.task_pool.handle.len();
if task_queue_len > 0 {
Expand All @@ -214,6 +225,14 @@ impl GlobalState {
lsp_server::Message::Notification(not) => self.on_notification(not)?,
lsp_server::Message::Response(resp) => self.complete_request(resp),
},
Event::QueuedTask(task) => {
let _p = profile::span("GlobalState::handle_event/queued_task");
self.handle_queued_task(task);
// Coalesce multiple task events into one loop turn
while let Ok(task) = self.deferred_task_queue.receiver.try_recv() {
self.handle_queued_task(task);
}
}
Event::Task(task) => {
let _p = profile::span("GlobalState::handle_event/task");
let mut prime_caches_progress = Vec::new();
Expand Down Expand Up @@ -625,6 +644,35 @@ impl GlobalState {
}
}

fn handle_queued_task(&mut self, task: QueuedTask) {
match task {
QueuedTask::CheckIfIndexed(uri) => {
let snap = self.snapshot();
let sender = self.sender.clone();

self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |_| {
let _p = profile::span("GlobalState::check_if_indexed");
tracing::debug!(?uri, "handling uri");
let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId");
if let Ok(crates) = &snap.analysis.crates_for(id) {
if crates.is_empty() {
let not = lsp_server::Notification::new(
ext::UnindexedProject::METHOD.to_string(),
ext::UnindexedProjectParams {
text_documents: vec![lsp_types::TextDocumentIdentifier { uri }],
},
);
sender.send(not.into()).expect("unable to send notification");
tracing::debug!("sent notification");
} else {
tracing::debug!(?uri, "is indexed");
}
}
});
}
}
}

fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
match message {
flycheck::Message::AddDiagnostic { id, workspace_root, diagnostic } => {
Expand Down
11 changes: 11 additions & 0 deletions crates/rust-analyzer/src/task_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
use crossbeam_channel::Sender;
use stdx::thread::{Pool, ThreadIntent};

use crate::main_loop::QueuedTask;

pub(crate) struct TaskPool<T> {
sender: Sender<T>,
pool: Pool,
Expand Down Expand Up @@ -40,3 +42,12 @@ impl<T> TaskPool<T> {
self.pool.len()
}
}

/// `TaskQueue`, like its name suggests, queues tasks.
///
/// This should only be used used if a task must run after [`GlobalState::process_changes`]
/// has been called.
pub(crate) struct TaskQueue {
pub(crate) sender: crossbeam_channel::Sender<QueuedTask>,
pub(crate) receiver: crossbeam_channel::Receiver<QueuedTask>,
}
62 changes: 61 additions & 1 deletion crates/rust-analyzer/tests/slow-tests/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ use lsp_types::{
PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem,
TextDocumentPositionParams, WorkDoneProgressParams,
};
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams};
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject};
use serde_json::json;
use test_utils::skip_slow_tests;

Expand Down Expand Up @@ -588,6 +588,66 @@ fn main() {{}}
);
}

#[test]
fn test_opening_a_file_outside_of_indexed_workspace() {
if skip_slow_tests() {
return;
}

let tmp_dir = TestDir::new();
let path = tmp_dir.path();

let project = json!({
"roots": [path],
"crates": [ {
"root_module": path.join("src/crate_one/lib.rs"),
"deps": [],
"edition": "2015",
"cfg": [ "cfg_atom_1", "feature=\"cfg_1\""],
} ]
});

let code = format!(
r#"
//- /rust-project.json
{project}
//- /src/crate_one/lib.rs
mod bar;
fn main() {{}}
"#,
);

let server = Project::with_fixture(&code)
.tmp_dir(tmp_dir)
.with_config(serde_json::json!({
"notifications": {
"unindexedProject": true
},
}))
.server()
.wait_until_workspace_is_loaded();

let uri = server.doc_id(&format!("src/crate_two/lib.rs")).uri;
server.notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: uri.clone(),
language_id: "rust".to_string(),
version: 0,
text: "/// Docs\nfn foo() {}".to_string(),
},
});
let expected = json!({
"textDocuments": [
{
"uri": uri
}
]
});
server.expect_notification::<UnindexedProject>(expected);
}

#[test]
fn diagnostics_dont_block_typing() {
if skip_slow_tests() {
Expand Down
34 changes: 34 additions & 0 deletions crates/rust-analyzer/tests/slow-tests/support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,40 @@ impl Server {
self.send_notification(r)
}

pub(crate) fn expect_notification<N>(&self, expected: Value)
where
N: lsp_types::notification::Notification,
N::Params: Serialize,
{
while let Some(Message::Notification(actual)) =
recv_timeout(&self.client.receiver).unwrap_or_else(|_| panic!("timed out"))
{
if actual.method == N::METHOD {
let actual = actual
.clone()
.extract::<Value>(N::METHOD)
.expect("was not able to extract notification");

tracing::debug!(?actual, "got notification");
if let Some((expected_part, actual_part)) = find_mismatch(&expected, &actual) {
panic!(
"JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
to_string_pretty(&expected).unwrap(),
to_string_pretty(&actual).unwrap(),
to_string_pretty(expected_part).unwrap(),
to_string_pretty(actual_part).unwrap(),
);
} else {
tracing::debug!("sucessfully matched notification");
return;
}
} else {
continue;
}
}
panic!("never got expected notification");
}

#[track_caller]
pub(crate) fn request<R>(&self, params: R::Params, expected_resp: Value)
where
Expand Down
Loading

0 comments on commit 73f8fad

Please sign in to comment.