Skip to content

Commit 73f8fad

Browse files
committed
Add a UnindexedProject notification and a corresponding setting.
1 parent 0333646 commit 73f8fad

File tree

13 files changed

+251
-9
lines changed

13 files changed

+251
-9
lines changed

crates/rust-analyzer/src/config.rs

+8-1
Original file line numberDiff line numberDiff line change
@@ -477,6 +477,9 @@ config_data! {
477477
/// Whether to show `can't find Cargo.toml` error message.
478478
notifications_cargoTomlNotFound: bool = "true",
479479

480+
/// Whether to send an UnindexedProject notification to the client.
481+
notifications_unindexedProject: bool = "false",
482+
480483
/// How many worker threads in the main loop. The default `null` means to pick automatically.
481484
numThreads: Option<usize> = "null",
482485

@@ -737,6 +740,7 @@ pub enum FilesWatcher {
737740
#[derive(Debug, Clone)]
738741
pub struct NotificationsConfig {
739742
pub cargo_toml_not_found: bool,
743+
pub unindexed_project: bool,
740744
}
741745

742746
#[derive(Debug, Clone)]
@@ -1212,7 +1216,10 @@ impl Config {
12121216
}
12131217

12141218
pub fn notifications(&self) -> NotificationsConfig {
1215-
NotificationsConfig { cargo_toml_not_found: self.data.notifications_cargoTomlNotFound }
1219+
NotificationsConfig {
1220+
cargo_toml_not_found: self.data.notifications_cargoTomlNotFound,
1221+
unindexed_project: self.data.notifications_unindexedProject,
1222+
}
12161223
}
12171224

12181225
pub fn cargo_autoreload(&self) -> bool {

crates/rust-analyzer/src/global_state.rs

+16-5
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::{
3333
mem_docs::MemDocs,
3434
op_queue::OpQueue,
3535
reload,
36-
task_pool::TaskPool,
36+
task_pool::{TaskPool, TaskQueue},
3737
};
3838

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

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

8686
// VFS
87-
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle>, Receiver<vfs::loader::Message>>,
87+
pub(crate) loader: Handle<Box<dyn vfs::loader::Handle + Send>, Receiver<vfs::loader::Message>>,
8888
pub(crate) vfs: Arc<RwLock<(vfs::Vfs, IntMap<FileId, LineEndings>)>>,
8989
pub(crate) vfs_config_version: u32,
9090
pub(crate) vfs_progress_config_version: u32,
@@ -126,6 +126,10 @@ pub(crate) struct GlobalState {
126126
OpQueue<(), (Arc<Vec<ProjectWorkspace>>, Vec<anyhow::Result<WorkspaceBuildScripts>>)>,
127127
pub(crate) fetch_proc_macros_queue: OpQueue<Vec<ProcMacroPaths>, bool>,
128128
pub(crate) prime_caches_queue: OpQueue,
129+
130+
/// a deferred task queue. this should only be used if the enqueued Task
131+
/// can only run *after* [`GlobalState::process_changes`] has been called.
132+
pub(crate) deferred_task_queue: TaskQueue,
129133
}
130134

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

@@ -165,6 +169,11 @@ impl GlobalState {
165169
Handle { handle, receiver }
166170
};
167171

172+
let task_queue = {
173+
let (sender, receiver) = unbounded();
174+
TaskQueue { sender, receiver }
175+
};
176+
168177
let mut analysis_host = AnalysisHost::new(config.lru_parse_query_capacity());
169178
if let Some(capacities) = config.lru_query_capacities() {
170179
analysis_host.update_lru_capacities(capacities);
@@ -208,6 +217,8 @@ impl GlobalState {
208217
fetch_proc_macros_queue: OpQueue::default(),
209218

210219
prime_caches_queue: OpQueue::default(),
220+
221+
deferred_task_queue: task_queue,
211222
};
212223
// Apply any required database inputs from the config.
213224
this.update_configuration(config);
@@ -425,7 +436,7 @@ impl GlobalState {
425436
self.req_queue.incoming.is_completed(&request.id)
426437
}
427438

428-
fn send(&self, message: lsp_server::Message) {
439+
pub(crate) fn send(&self, message: lsp_server::Message) {
429440
self.sender.send(message).unwrap()
430441
}
431442
}

crates/rust-analyzer/src/handlers/notification.rs

+8
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,15 @@ pub(crate) fn handle_did_open_text_document(
7070
if already_exists {
7171
tracing::error!("duplicate DidOpenTextDocument: {}", path);
7272
}
73+
7374
state.vfs.write().0.set_file_contents(path, Some(params.text_document.text.into_bytes()));
75+
if state.config.notifications().unindexed_project {
76+
tracing::debug!("queuing task");
77+
let _ = state
78+
.deferred_task_queue
79+
.sender
80+
.send(crate::main_loop::QueuedTask::CheckIfIndexed(params.text_document.uri));
81+
}
7482
}
7583
Ok(())
7684
}

crates/rust-analyzer/src/lsp/ext.rs

+13
Original file line numberDiff line numberDiff line change
@@ -696,3 +696,16 @@ pub struct CompletionImport {
696696
pub struct ClientCommandOptions {
697697
pub commands: Vec<String>,
698698
}
699+
700+
pub enum UnindexedProject {}
701+
702+
impl Notification for UnindexedProject {
703+
type Params = UnindexedProjectParams;
704+
const METHOD: &'static str = "rust-analyzer/unindexedProject";
705+
}
706+
707+
#[derive(Deserialize, Serialize, Debug)]
708+
#[serde(rename_all = "camelCase")]
709+
pub struct UnindexedProjectParams {
710+
pub text_documents: Vec<TextDocumentIdentifier>,
711+
}

crates/rust-analyzer/src/main_loop.rs

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! The main loop of `rust-analyzer` responsible for dispatching LSP
22
//! requests/replies and notifications back to the client.
3+
use crate::lsp::ext;
34
use std::{
45
fmt,
56
time::{Duration, Instant},
@@ -56,10 +57,16 @@ pub fn main_loop(config: Config, connection: Connection) -> anyhow::Result<()> {
5657
enum Event {
5758
Lsp(lsp_server::Message),
5859
Task(Task),
60+
QueuedTask(QueuedTask),
5961
Vfs(vfs::loader::Message),
6062
Flycheck(flycheck::Message),
6163
}
6264

65+
#[derive(Debug)]
66+
pub(crate) enum QueuedTask {
67+
CheckIfIndexed(lsp_types::Url),
68+
}
69+
6370
#[derive(Debug)]
6471
pub(crate) enum Task {
6572
Response(lsp_server::Response),
@@ -104,6 +111,7 @@ impl fmt::Debug for Event {
104111
match self {
105112
Event::Lsp(it) => fmt::Debug::fmt(it, f),
106113
Event::Task(it) => fmt::Debug::fmt(it, f),
114+
Event::QueuedTask(it) => fmt::Debug::fmt(it, f),
107115
Event::Vfs(it) => fmt::Debug::fmt(it, f),
108116
Event::Flycheck(it) => fmt::Debug::fmt(it, f),
109117
}
@@ -182,6 +190,9 @@ impl GlobalState {
182190
recv(self.task_pool.receiver) -> task =>
183191
Some(Event::Task(task.unwrap())),
184192

193+
recv(self.deferred_task_queue.receiver) -> task =>
194+
Some(Event::QueuedTask(task.unwrap())),
195+
185196
recv(self.fmt_pool.receiver) -> task =>
186197
Some(Event::Task(task.unwrap())),
187198

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

201212
let event_dbg_msg = format!("{event:?}");
202-
tracing::debug!("{:?} handle_event({})", loop_start, event_dbg_msg);
213+
tracing::debug!(?loop_start, ?event, "handle_event");
203214
if tracing::enabled!(tracing::Level::INFO) {
204215
let task_queue_len = self.task_pool.handle.len();
205216
if task_queue_len > 0 {
@@ -214,6 +225,14 @@ impl GlobalState {
214225
lsp_server::Message::Notification(not) => self.on_notification(not)?,
215226
lsp_server::Message::Response(resp) => self.complete_request(resp),
216227
},
228+
Event::QueuedTask(task) => {
229+
let _p = profile::span("GlobalState::handle_event/queued_task");
230+
self.handle_queued_task(task);
231+
// Coalesce multiple task events into one loop turn
232+
while let Ok(task) = self.deferred_task_queue.receiver.try_recv() {
233+
self.handle_queued_task(task);
234+
}
235+
}
217236
Event::Task(task) => {
218237
let _p = profile::span("GlobalState::handle_event/task");
219238
let mut prime_caches_progress = Vec::new();
@@ -625,6 +644,35 @@ impl GlobalState {
625644
}
626645
}
627646

647+
fn handle_queued_task(&mut self, task: QueuedTask) {
648+
match task {
649+
QueuedTask::CheckIfIndexed(uri) => {
650+
let snap = self.snapshot();
651+
let sender = self.sender.clone();
652+
653+
self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |_| {
654+
let _p = profile::span("GlobalState::check_if_indexed");
655+
tracing::debug!(?uri, "handling uri");
656+
let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId");
657+
if let Ok(crates) = &snap.analysis.crates_for(id) {
658+
if crates.is_empty() {
659+
let not = lsp_server::Notification::new(
660+
ext::UnindexedProject::METHOD.to_string(),
661+
ext::UnindexedProjectParams {
662+
text_documents: vec![lsp_types::TextDocumentIdentifier { uri }],
663+
},
664+
);
665+
sender.send(not.into()).expect("unable to send notification");
666+
tracing::debug!("sent notification");
667+
} else {
668+
tracing::debug!(?uri, "is indexed");
669+
}
670+
}
671+
});
672+
}
673+
}
674+
}
675+
628676
fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
629677
match message {
630678
flycheck::Message::AddDiagnostic { id, workspace_root, diagnostic } => {

crates/rust-analyzer/src/task_pool.rs

+11
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
use crossbeam_channel::Sender;
55
use stdx::thread::{Pool, ThreadIntent};
66

7+
use crate::main_loop::QueuedTask;
8+
79
pub(crate) struct TaskPool<T> {
810
sender: Sender<T>,
911
pool: Pool,
@@ -40,3 +42,12 @@ impl<T> TaskPool<T> {
4042
self.pool.len()
4143
}
4244
}
45+
46+
/// `TaskQueue`, like its name suggests, queues tasks.
47+
///
48+
/// This should only be used used if a task must run after [`GlobalState::process_changes`]
49+
/// has been called.
50+
pub(crate) struct TaskQueue {
51+
pub(crate) sender: crossbeam_channel::Sender<QueuedTask>,
52+
pub(crate) receiver: crossbeam_channel::Receiver<QueuedTask>,
53+
}

crates/rust-analyzer/tests/slow-tests/main.rs

+61-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ use lsp_types::{
2929
PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem,
3030
TextDocumentPositionParams, WorkDoneProgressParams,
3131
};
32-
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams};
32+
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject};
3333
use serde_json::json;
3434
use test_utils::skip_slow_tests;
3535

@@ -588,6 +588,66 @@ fn main() {{}}
588588
);
589589
}
590590

591+
#[test]
592+
fn test_opening_a_file_outside_of_indexed_workspace() {
593+
if skip_slow_tests() {
594+
return;
595+
}
596+
597+
let tmp_dir = TestDir::new();
598+
let path = tmp_dir.path();
599+
600+
let project = json!({
601+
"roots": [path],
602+
"crates": [ {
603+
"root_module": path.join("src/crate_one/lib.rs"),
604+
"deps": [],
605+
"edition": "2015",
606+
"cfg": [ "cfg_atom_1", "feature=\"cfg_1\""],
607+
} ]
608+
});
609+
610+
let code = format!(
611+
r#"
612+
//- /rust-project.json
613+
{project}
614+
615+
//- /src/crate_one/lib.rs
616+
mod bar;
617+
618+
fn main() {{}}
619+
"#,
620+
);
621+
622+
let server = Project::with_fixture(&code)
623+
.tmp_dir(tmp_dir)
624+
.with_config(serde_json::json!({
625+
"notifications": {
626+
"unindexedProject": true
627+
},
628+
}))
629+
.server()
630+
.wait_until_workspace_is_loaded();
631+
632+
let uri = server.doc_id(&format!("src/crate_two/lib.rs")).uri;
633+
server.notification::<DidOpenTextDocument>(DidOpenTextDocumentParams {
634+
text_document: TextDocumentItem {
635+
uri: uri.clone(),
636+
language_id: "rust".to_string(),
637+
version: 0,
638+
text: "/// Docs\nfn foo() {}".to_string(),
639+
},
640+
});
641+
let expected = json!({
642+
"textDocuments": [
643+
{
644+
"uri": uri
645+
}
646+
]
647+
});
648+
server.expect_notification::<UnindexedProject>(expected);
649+
}
650+
591651
#[test]
592652
fn diagnostics_dont_block_typing() {
593653
if skip_slow_tests() {

crates/rust-analyzer/tests/slow-tests/support.rs

+34
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,40 @@ impl Server {
212212
self.send_notification(r)
213213
}
214214

215+
pub(crate) fn expect_notification<N>(&self, expected: Value)
216+
where
217+
N: lsp_types::notification::Notification,
218+
N::Params: Serialize,
219+
{
220+
while let Some(Message::Notification(actual)) =
221+
recv_timeout(&self.client.receiver).unwrap_or_else(|_| panic!("timed out"))
222+
{
223+
if actual.method == N::METHOD {
224+
let actual = actual
225+
.clone()
226+
.extract::<Value>(N::METHOD)
227+
.expect("was not able to extract notification");
228+
229+
tracing::debug!(?actual, "got notification");
230+
if let Some((expected_part, actual_part)) = find_mismatch(&expected, &actual) {
231+
panic!(
232+
"JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
233+
to_string_pretty(&expected).unwrap(),
234+
to_string_pretty(&actual).unwrap(),
235+
to_string_pretty(expected_part).unwrap(),
236+
to_string_pretty(actual_part).unwrap(),
237+
);
238+
} else {
239+
tracing::debug!("sucessfully matched notification");
240+
return;
241+
}
242+
} else {
243+
continue;
244+
}
245+
}
246+
panic!("never got expected notification");
247+
}
248+
215249
#[track_caller]
216250
pub(crate) fn request<R>(&self, params: R::Params, expected_resp: Value)
217251
where

0 commit comments

Comments
 (0)