Skip to content

Commit b6155f0

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

File tree

13 files changed

+256
-6
lines changed

13 files changed

+256
-6
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

+20-2
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>>,
@@ -126,6 +126,17 @@ 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.
131+
///
132+
/// This queue is used for doing database-dependent work inside of sync
133+
/// handlers, as accessing the database may block latency-sensitive
134+
/// interactions and should be moved away from the main thread.
135+
///
136+
/// For certain features, such as [`lsp_ext::UnindexedProjectParams`],
137+
/// this queue should run only *after* [`GlobalState::process_changes`] has
138+
/// been called.
139+
pub(crate) deferred_task_queue: TaskQueue,
129140
}
130141

131142
/// An immutable snapshot of the world's state at a point in time.
@@ -165,6 +176,11 @@ impl GlobalState {
165176
Handle { handle, receiver }
166177
};
167178

179+
let task_queue = {
180+
let (sender, receiver) = unbounded();
181+
TaskQueue { sender, receiver }
182+
};
183+
168184
let mut analysis_host = AnalysisHost::new(config.lru_parse_query_capacity());
169185
if let Some(capacities) = config.lru_query_capacities() {
170186
analysis_host.update_lru_capacities(capacities);
@@ -208,6 +224,8 @@ impl GlobalState {
208224
fetch_proc_macros_queue: OpQueue::default(),
209225

210226
prime_caches_queue: OpQueue::default(),
227+
228+
deferred_task_queue: task_queue,
211229
};
212230
// Apply any required database inputs from the config.
213231
this.update_configuration(config);

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

+48-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,13 +57,20 @@ 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),
73+
ClientNotification(ext::UnindexedProjectParams),
6674
Retry(lsp_server::Request),
6775
Diagnostics(Vec<(FileId, Vec<lsp_types::Diagnostic>)>),
6876
PrimeCaches(PrimeCachesProgress),
@@ -104,6 +112,7 @@ impl fmt::Debug for Event {
104112
match self {
105113
Event::Lsp(it) => fmt::Debug::fmt(it, f),
106114
Event::Task(it) => fmt::Debug::fmt(it, f),
115+
Event::QueuedTask(it) => fmt::Debug::fmt(it, f),
107116
Event::Vfs(it) => fmt::Debug::fmt(it, f),
108117
Event::Flycheck(it) => fmt::Debug::fmt(it, f),
109118
}
@@ -182,6 +191,9 @@ impl GlobalState {
182191
recv(self.task_pool.receiver) -> task =>
183192
Some(Event::Task(task.unwrap())),
184193

194+
recv(self.deferred_task_queue.receiver) -> task =>
195+
Some(Event::QueuedTask(task.unwrap())),
196+
185197
recv(self.fmt_pool.receiver) -> task =>
186198
Some(Event::Task(task.unwrap())),
187199

@@ -199,7 +211,7 @@ impl GlobalState {
199211
let _p = profile::span("GlobalState::handle_event");
200212

201213
let event_dbg_msg = format!("{event:?}");
202-
tracing::debug!("{:?} handle_event({})", loop_start, event_dbg_msg);
214+
tracing::debug!(?loop_start, ?event, "handle_event");
203215
if tracing::enabled!(tracing::Level::INFO) {
204216
let task_queue_len = self.task_pool.handle.len();
205217
if task_queue_len > 0 {
@@ -214,6 +226,14 @@ impl GlobalState {
214226
lsp_server::Message::Notification(not) => self.on_notification(not)?,
215227
lsp_server::Message::Response(resp) => self.complete_request(resp),
216228
},
229+
Event::QueuedTask(task) => {
230+
let _p = profile::span("GlobalState::handle_event/queued_task");
231+
self.handle_queued_task(task);
232+
// Coalesce multiple task events into one loop turn
233+
while let Ok(task) = self.deferred_task_queue.receiver.try_recv() {
234+
self.handle_queued_task(task);
235+
}
236+
}
217237
Event::Task(task) => {
218238
let _p = profile::span("GlobalState::handle_event/task");
219239
let mut prime_caches_progress = Vec::new();
@@ -483,6 +503,9 @@ impl GlobalState {
483503
fn handle_task(&mut self, prime_caches_progress: &mut Vec<PrimeCachesProgress>, task: Task) {
484504
match task {
485505
Task::Response(response) => self.respond(response),
506+
Task::ClientNotification(params) => {
507+
self.send_notification::<lsp_ext::UnindexedProject>(params)
508+
}
486509
// Only retry requests that haven't been cancelled. Otherwise we do unnecessary work.
487510
Task::Retry(req) if !self.is_completed(&req) => self.on_request(req),
488511
Task::Retry(_) => (),
@@ -625,6 +648,30 @@ impl GlobalState {
625648
}
626649
}
627650

651+
fn handle_queued_task(&mut self, task: QueuedTask) {
652+
match task {
653+
QueuedTask::CheckIfIndexed(uri) => {
654+
let snap = self.snapshot();
655+
656+
self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
657+
let _p = profile::span("GlobalState::check_if_indexed");
658+
tracing::debug!(?uri, "handling uri");
659+
let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId");
660+
if let Ok(crates) = &snap.analysis.crates_for(id) {
661+
if crates.is_empty() {
662+
let params = ext::UnindexedProjectParams {
663+
text_documents: vec![lsp_types::TextDocumentIdentifier { uri }],
664+
};
665+
sender.send(Task::ClientNotification(params)).unwrap();
666+
} else {
667+
tracing::debug!(?uri, "is indexed");
668+
}
669+
}
670+
});
671+
}
672+
}
673+
}
674+
628675
fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
629676
match message {
630677
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)