Skip to content

Commit c3db016

Browse files
committed
feature: Add a UnindexedProject notification and a corresponding setting.
1 parent ca44e1e commit c3db016

File tree

13 files changed

+261
-7
lines changed

13 files changed

+261
-7
lines changed

crates/rust-analyzer/src/config.rs

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

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

@@ -745,6 +748,7 @@ pub enum FilesWatcher {
745748
#[derive(Debug, Clone)]
746749
pub struct NotificationsConfig {
747750
pub cargo_toml_not_found: bool,
751+
pub unindexed_project: bool,
748752
}
749753

750754
#[derive(Debug, Clone)]
@@ -1220,7 +1224,10 @@ impl Config {
12201224
}
12211225

12221226
pub fn notifications(&self) -> NotificationsConfig {
1223-
NotificationsConfig { cargo_toml_not_found: self.data.notifications_cargoTomlNotFound }
1227+
NotificationsConfig {
1228+
cargo_toml_not_found: self.data.notifications_cargoTomlNotFound,
1229+
unindexed_project: self.data.notifications_unindexedProject,
1230+
}
12241231
}
12251232

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

crates/rust-analyzer/src/global_state.rs

+19-1
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
@@ -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
@@ -703,3 +703,16 @@ pub struct CompletionImport {
703703
pub struct ClientCommandOptions {
704704
pub commands: Vec<String>,
705705
}
706+
707+
pub enum UnindexedProject {}
708+
709+
impl Notification for UnindexedProject {
710+
type Params = UnindexedProjectParams;
711+
const METHOD: &'static str = "rust-analyzer/unindexedProject";
712+
}
713+
714+
#[derive(Deserialize, Serialize, Debug)]
715+
#[serde(rename_all = "camelCase")]
716+
pub struct UnindexedProjectParams {
717+
pub text_documents: Vec<TextDocumentIdentifier>,
718+
}

crates/rust-analyzer/src/main_loop.rs

+52-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,6 +57,7 @@ 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
}
@@ -67,13 +69,20 @@ impl fmt::Display for Event {
6769
Event::Task(_) => write!(f, "Event::Task"),
6870
Event::Vfs(_) => write!(f, "Event::Vfs"),
6971
Event::Flycheck(_) => write!(f, "Event::Flycheck"),
72+
Event::QueuedTask(_) => write!(f, "Event::QueuedTask"),
7073
}
7174
}
7275
}
7376

77+
#[derive(Debug)]
78+
pub(crate) enum QueuedTask {
79+
CheckIfIndexed(lsp_types::Url),
80+
}
81+
7482
#[derive(Debug)]
7583
pub(crate) enum Task {
7684
Response(lsp_server::Response),
85+
ClientNotification(ext::UnindexedProjectParams),
7786
Retry(lsp_server::Request),
7887
Diagnostics(Vec<(FileId, Vec<lsp_types::Diagnostic>)>),
7988
PrimeCaches(PrimeCachesProgress),
@@ -115,6 +124,7 @@ impl fmt::Debug for Event {
115124
match self {
116125
Event::Lsp(it) => fmt::Debug::fmt(it, f),
117126
Event::Task(it) => fmt::Debug::fmt(it, f),
127+
Event::QueuedTask(it) => fmt::Debug::fmt(it, f),
118128
Event::Vfs(it) => fmt::Debug::fmt(it, f),
119129
Event::Flycheck(it) => fmt::Debug::fmt(it, f),
120130
}
@@ -193,6 +203,9 @@ impl GlobalState {
193203
recv(self.task_pool.receiver) -> task =>
194204
Some(Event::Task(task.unwrap())),
195205

206+
recv(self.deferred_task_queue.receiver) -> task =>
207+
Some(Event::QueuedTask(task.unwrap())),
208+
196209
recv(self.fmt_pool.receiver) -> task =>
197210
Some(Event::Task(task.unwrap())),
198211

@@ -211,7 +224,7 @@ impl GlobalState {
211224
.entered();
212225

213226
let event_dbg_msg = format!("{event:?}");
214-
tracing::debug!("{:?} handle_event({})", loop_start, event_dbg_msg);
227+
tracing::debug!(?loop_start, ?event, "handle_event");
215228
if tracing::enabled!(tracing::Level::INFO) {
216229
let task_queue_len = self.task_pool.handle.len();
217230
if task_queue_len > 0 {
@@ -226,6 +239,16 @@ impl GlobalState {
226239
lsp_server::Message::Notification(not) => self.on_notification(not)?,
227240
lsp_server::Message::Response(resp) => self.complete_request(resp),
228241
},
242+
Event::QueuedTask(task) => {
243+
let _p =
244+
tracing::span!(tracing::Level::INFO, "GlobalState::handle_event/queued_task")
245+
.entered();
246+
self.handle_queued_task(task);
247+
// Coalesce multiple task events into one loop turn
248+
while let Ok(task) = self.deferred_task_queue.receiver.try_recv() {
249+
self.handle_queued_task(task);
250+
}
251+
}
229252
Event::Task(task) => {
230253
let _p = tracing::span!(tracing::Level::INFO, "GlobalState::handle_event/task")
231254
.entered();
@@ -498,6 +521,9 @@ impl GlobalState {
498521
fn handle_task(&mut self, prime_caches_progress: &mut Vec<PrimeCachesProgress>, task: Task) {
499522
match task {
500523
Task::Response(response) => self.respond(response),
524+
Task::ClientNotification(params) => {
525+
self.send_notification::<lsp_ext::UnindexedProject>(params)
526+
}
501527
// Only retry requests that haven't been cancelled. Otherwise we do unnecessary work.
502528
Task::Retry(req) if !self.is_completed(&req) => self.on_request(req),
503529
Task::Retry(_) => (),
@@ -638,6 +664,31 @@ impl GlobalState {
638664
}
639665
}
640666

667+
fn handle_queued_task(&mut self, task: QueuedTask) {
668+
match task {
669+
QueuedTask::CheckIfIndexed(uri) => {
670+
let snap = self.snapshot();
671+
672+
self.task_pool.handle.spawn_with_sender(ThreadIntent::Worker, move |sender| {
673+
let _p = tracing::span!(tracing::Level::INFO, "GlobalState::check_if_indexed")
674+
.entered();
675+
tracing::debug!(?uri, "handling uri");
676+
let id = from_proto::file_id(&snap, &uri).expect("unable to get FileId");
677+
if let Ok(crates) = &snap.analysis.crates_for(id) {
678+
if crates.is_empty() {
679+
let params = ext::UnindexedProjectParams {
680+
text_documents: vec![lsp_types::TextDocumentIdentifier { uri }],
681+
};
682+
sender.send(Task::ClientNotification(params)).unwrap();
683+
} else {
684+
tracing::debug!(?uri, "is indexed");
685+
}
686+
}
687+
});
688+
}
689+
}
690+
}
691+
641692
fn handle_flycheck_msg(&mut self, message: flycheck::Message) {
642693
match message {
643694
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
@@ -30,7 +30,7 @@ use lsp_types::{
3030
PartialResultParams, Position, Range, RenameFilesParams, TextDocumentItem,
3131
TextDocumentPositionParams, WorkDoneProgressParams,
3232
};
33-
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams};
33+
use rust_analyzer::lsp::ext::{OnEnter, Runnables, RunnablesParams, UnindexedProject};
3434
use serde_json::json;
3535
use stdx::format_to_acc;
3636
use test_utils::skip_slow_tests;
@@ -587,6 +587,66 @@ fn main() {{}}
587587
);
588588
}
589589

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

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

+36-2
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use std::{
99
use crossbeam_channel::{after, select, Receiver};
1010
use lsp_server::{Connection, Message, Notification, Request};
1111
use lsp_types::{notification::Exit, request::Shutdown, TextDocumentIdentifier, Url};
12-
use rust_analyzer::{config::Config, lsp, main_loop, tracing};
12+
use rust_analyzer::{config::Config, lsp, main_loop};
1313
use serde::Serialize;
1414
use serde_json::{json, to_string_pretty, Value};
1515
use test_utils::FixtureWithProjectMeta;
@@ -91,7 +91,7 @@ impl Project<'_> {
9191

9292
static INIT: Once = Once::new();
9393
INIT.call_once(|| {
94-
let _ = tracing::Config {
94+
let _ = rust_analyzer::tracing::Config {
9595
writer: TestWriter::default(),
9696
// Deliberately enable all `error` logs if the user has not set RA_LOG, as there is usually
9797
// useful information in there for debugging.
@@ -214,6 +214,40 @@ impl Server {
214214
self.send_notification(r)
215215
}
216216

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

0 commit comments

Comments
 (0)