1use std::{
5 ops::{Deref, Not as _},
6 panic::UnwindSafe,
7};
8
9use itertools::Itertools;
10use lsp_types::{
11 CancelParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
12 DidChangeWatchedFilesParams, DidChangeWorkspaceFoldersParams, DidCloseTextDocumentParams,
13 DidOpenTextDocumentParams, DidSaveTextDocumentParams, WorkDoneProgressCancelParams,
14};
15use paths::Utf8PathBuf;
16use triomphe::Arc;
17use vfs::{AbsPathBuf, ChangeKind, VfsPath};
18
19use crate::{
20 config::{Config, ConfigChange},
21 flycheck::{InvocationStrategy, PackageSpecifier, Target},
22 global_state::{FetchWorkspaceRequest, GlobalState},
23 lsp::{from_proto, utils::apply_document_changes},
24 lsp_ext::{self, RunFlycheckParams},
25 mem_docs::DocumentData,
26 reload,
27 target_spec::TargetSpec,
28 try_default,
29};
30
31pub(crate) fn handle_cancel(state: &mut GlobalState, params: CancelParams) -> anyhow::Result<()> {
32 let id: lsp_server::RequestId = match params.id {
33 lsp_types::NumberOrString::Number(id) => id.into(),
34 lsp_types::NumberOrString::String(id) => id.into(),
35 };
36 state.cancel(id);
37 Ok(())
38}
39
40pub(crate) fn handle_work_done_progress_cancel(
41 state: &mut GlobalState,
42 params: WorkDoneProgressCancelParams,
43) -> anyhow::Result<()> {
44 if let lsp_types::NumberOrString::String(s) = ¶ms.token
45 && let Some(id) = s.strip_prefix("rust-analyzer/flycheck/")
46 && let Ok(id) = id.parse::<u32>()
47 && let Some(flycheck) = state.flycheck.get(id as usize)
48 {
49 flycheck.cancel();
50 }
51
52 Ok(())
56}
57
58pub(crate) fn handle_did_open_text_document(
59 state: &mut GlobalState,
60 params: DidOpenTextDocumentParams,
61) -> anyhow::Result<()> {
62 let _p = tracing::info_span!("handle_did_open_text_document").entered();
63
64 if let Ok(path) = from_proto::vfs_path(¶ms.text_document.uri) {
65 let already_exists = state
66 .mem_docs
67 .insert(
68 path.clone(),
69 DocumentData::new(
70 params.text_document.version,
71 params.text_document.text.clone().into_bytes(),
72 ),
73 )
74 .is_err();
75 if already_exists {
76 tracing::error!("duplicate DidOpenTextDocument: {}", path);
77 }
78
79 if let Some(abs_path) = path.as_path()
80 && state.config.excluded().any(|excluded| abs_path.starts_with(&excluded))
81 {
82 tracing::trace!("opened excluded file {abs_path}");
83 state.vfs.write().0.insert_excluded_file(path);
84 return Ok(());
85 }
86
87 let contents = params.text_document.text.into_bytes();
88 state.vfs.write().0.set_file_contents(path, Some(contents));
89 if state.config.discover_workspace_config().is_some() {
90 tracing::debug!("queuing task");
91 let _ = state
92 .deferred_task_queue
93 .sender
94 .send(crate::main_loop::DeferredTask::CheckIfIndexed(params.text_document.uri));
95 }
96 }
97 Ok(())
98}
99
100pub(crate) fn handle_did_change_text_document(
101 state: &mut GlobalState,
102 params: DidChangeTextDocumentParams,
103) -> anyhow::Result<()> {
104 let _p = tracing::info_span!("handle_did_change_text_document").entered();
105
106 if let Ok(path) = from_proto::vfs_path(¶ms.text_document.uri) {
107 let Some(DocumentData { version, data }) = state.mem_docs.get_mut(&path) else {
108 tracing::error!(?path, "unexpected DidChangeTextDocument");
109 return Ok(());
110 };
111 *version = params.text_document.version;
114
115 let new_contents = apply_document_changes(
116 state.config.negotiated_encoding(),
117 std::str::from_utf8(data).unwrap(),
118 params.content_changes,
119 )
120 .into_bytes();
121 if *data != new_contents {
122 data.clone_from(&new_contents);
123 state.vfs.write().0.set_file_contents(path, Some(new_contents));
124 }
125 }
126 Ok(())
127}
128
129pub(crate) fn handle_did_close_text_document(
130 state: &mut GlobalState,
131 params: DidCloseTextDocumentParams,
132) -> anyhow::Result<()> {
133 let _p = tracing::info_span!("handle_did_close_text_document").entered();
134
135 if let Ok(path) = from_proto::vfs_path(¶ms.text_document.uri) {
136 if state.mem_docs.remove(&path).is_err() {
137 tracing::error!("orphan DidCloseTextDocument: {}", path);
138 }
139
140 if let Some((file_id, _)) = state.vfs.read().0.file_id(&path) {
142 state.diagnostics.clear_native_for(file_id);
143 }
144
145 state.semantic_tokens_cache.lock().remove(¶ms.text_document.uri);
146
147 if let Some(path) = path.as_path() {
148 state.loader.handle.invalidate(path.to_path_buf());
149 }
150 }
151 Ok(())
152}
153
154pub(crate) fn handle_did_save_text_document(
155 state: &mut GlobalState,
156 params: DidSaveTextDocumentParams,
157) -> anyhow::Result<()> {
158 if let Ok(vfs_path) = from_proto::vfs_path(¶ms.text_document.uri) {
159 let snap = state.snapshot();
160 let file_id = try_default!(snap.vfs_path_to_file_id(&vfs_path)?);
161 let sr = snap.analysis.source_root_id(file_id)?;
162
163 if state.config.script_rebuild_on_save(Some(sr)) && state.build_deps_changed {
164 state.build_deps_changed = false;
165 state
166 .fetch_build_data_queue
167 .request_op("build_deps_changed - save notification".to_owned(), ());
168 }
169
170 if let Some(path) = vfs_path.as_path() {
172 let additional_files = &state
173 .config
174 .discover_workspace_config()
175 .map(|cfg| cfg.files_to_watch.iter().map(String::as_str).collect::<Vec<&str>>())
176 .unwrap_or_default();
177
178 if reload::should_refresh_for_change(path, ChangeKind::Modify, additional_files) {
181 state.fetch_workspaces_queue.request_op(
182 format!("workspace vfs file change saved {path}"),
183 FetchWorkspaceRequest {
184 path: Some(path.to_owned()),
185 force_crate_graph_reload: false,
186 },
187 );
188 } else if state.detached_files.contains(path) {
189 state.fetch_workspaces_queue.request_op(
190 format!("detached file saved {path}"),
191 FetchWorkspaceRequest {
192 path: Some(path.to_owned()),
193 force_crate_graph_reload: false,
194 },
195 );
196 }
197 }
198
199 if !state.config.check_on_save(Some(sr)) || run_flycheck(state, vfs_path) {
200 return Ok(());
201 }
202 } else if state.config.check_on_save(None) && state.config.flycheck_workspace(None) {
203 for flycheck in state.flycheck.iter() {
205 flycheck.restart_workspace(None);
206 }
207 }
208
209 Ok(())
210}
211
212pub(crate) fn handle_did_change_configuration(
213 state: &mut GlobalState,
214 _params: DidChangeConfigurationParams,
215) -> anyhow::Result<()> {
216 state.send_request::<lsp_types::request::WorkspaceConfiguration>(
219 lsp_types::ConfigurationParams {
220 items: vec![lsp_types::ConfigurationItem {
221 scope_uri: None,
222 section: Some("rust-analyzer".to_owned()),
223 }],
224 },
225 |this, resp| {
226 tracing::debug!("config update response: '{:?}", resp);
227 let lsp_server::Response { error, result, .. } = resp;
228
229 match (error, result) {
230 (Some(err), _) => {
231 tracing::error!("failed to fetch the server settings: {:?}", err)
232 }
233 (None, Some(mut configs)) => {
234 if let Some(json) = configs.get_mut(0) {
235 let config = Config::clone(&*this.config);
236 let mut change = ConfigChange::default();
237 change.change_client_config(json.take());
238
239 let (config, e, _) = config.apply_change(change);
240 this.config_errors = e.is_empty().not().then_some(e);
241
242 this.update_configuration(config);
244 }
245 }
246 (None, None) => {
247 tracing::error!("received empty server settings response from the client")
248 }
249 }
250 },
251 );
252
253 Ok(())
254}
255
256pub(crate) fn handle_did_change_workspace_folders(
257 state: &mut GlobalState,
258 params: DidChangeWorkspaceFoldersParams,
259) -> anyhow::Result<()> {
260 let config = Arc::make_mut(&mut state.config);
261
262 for workspace in params.event.removed {
263 let Ok(path) = workspace.uri.to_file_path() else { continue };
264 let Ok(path) = Utf8PathBuf::from_path_buf(path) else { continue };
265 let Ok(path) = AbsPathBuf::try_from(path) else { continue };
266 config.remove_workspace(&path);
267 }
268
269 let added = params
270 .event
271 .added
272 .into_iter()
273 .filter_map(|it| it.uri.to_file_path().ok())
274 .filter_map(|it| Utf8PathBuf::from_path_buf(it).ok())
275 .filter_map(|it| AbsPathBuf::try_from(it).ok());
276 config.add_workspaces(added);
277
278 if !config.has_linked_projects() && config.detached_files().is_empty() {
279 config.rediscover_workspaces();
280
281 let req = FetchWorkspaceRequest { path: None, force_crate_graph_reload: false };
282 state.fetch_workspaces_queue.request_op("client workspaces changed".to_owned(), req);
283 }
284
285 Ok(())
286}
287
288pub(crate) fn handle_did_change_watched_files(
289 state: &mut GlobalState,
290 params: DidChangeWatchedFilesParams,
291) -> anyhow::Result<()> {
292 let mut trigger_flycheck = false;
295 for change in params.changes.iter().unique_by(|&it| &it.uri) {
296 if let Ok(path) = from_proto::abs_path(&change.uri) {
297 if !trigger_flycheck {
298 trigger_flycheck =
300 state.config.workspace_roots().iter().all(|root| !path.starts_with(root));
301 }
302 state.loader.handle.invalidate(path);
303 }
304 }
305
306 if trigger_flycheck && state.config.check_on_save(None) {
307 for flycheck in state.flycheck.iter() {
308 flycheck.restart_workspace(None);
309 }
310 }
311 Ok(())
312}
313
314fn run_flycheck(state: &mut GlobalState, vfs_path: VfsPath) -> bool {
315 let _p = tracing::info_span!("run_flycheck").entered();
316
317 let file_id = state.vfs.read().0.file_id(&vfs_path);
318 if let Some((file_id, vfs::FileExcluded::No)) = file_id {
319 let world = state.snapshot();
320 let invocation_strategy = state.config.flycheck(None).invocation_strategy();
321 let may_flycheck_workspace = state.config.flycheck_workspace(None);
322
323 let task: Box<dyn FnOnce() -> ide::Cancellable<()> + Send + UnwindSafe> =
324 match invocation_strategy {
325 InvocationStrategy::Once => {
326 Box::new(move || {
327 let world = world;
334 stdx::always!(
335 world.flycheck.len() == 1,
336 "should have exactly one flycheck handle when invocation strategy is once"
337 );
338 let saved_file = vfs_path.as_path().map(ToOwned::to_owned);
339 world.flycheck[0].restart_workspace(saved_file);
340 Ok(())
341 })
342 }
343 InvocationStrategy::PerWorkspace => {
344 Box::new(move || {
345 let saved_file = vfs_path.as_path().map(ToOwned::to_owned);
346 let target = TargetSpec::for_file(&world, file_id)?.map(|it| {
347 let tgt_kind = it.target_kind();
348 let (tgt_name, root, package) = match it {
349 TargetSpec::Cargo(c) => (
350 Some(c.target),
351 c.workspace_root,
352 PackageSpecifier::Cargo { package_id: c.package_id },
353 ),
354 TargetSpec::ProjectJson(p) => (
355 None,
356 p.project_root,
357 PackageSpecifier::BuildInfo { label: p.label.clone() },
358 ),
359 };
360
361 let tgt = tgt_name.and_then(|tgt_name| {
362 Some(match tgt_kind {
363 project_model::TargetKind::Bin => Target::Bin(tgt_name),
364 project_model::TargetKind::Example => Target::Example(tgt_name),
365 project_model::TargetKind::Test => Target::Test(tgt_name),
366 project_model::TargetKind::Bench => Target::Benchmark(tgt_name),
367 _ => return None,
368 })
369 });
370
371 (tgt, root, package)
372 });
373 tracing::debug!(?target, "flycheck target");
374 let mut package_workspace_idx = None;
377 if let Some((target, root, package)) = target {
378 let package_check_allowed = target.is_some()
382 || !may_flycheck_workspace
383 || matches!(package, PackageSpecifier::BuildInfo { .. });
384 if package_check_allowed {
385 package_workspace_idx =
386 world.workspaces.iter().position(|ws| match &ws.kind {
387 project_model::ProjectWorkspaceKind::Cargo {
388 cargo,
389 ..
390 }
391 | project_model::ProjectWorkspaceKind::DetachedFile {
392 cargo: Some((cargo, _, _)),
393 ..
394 } => *cargo.workspace_root() == root,
395 project_model::ProjectWorkspaceKind::Json(p) => {
396 *p.project_root() == root
397 }
398 project_model::ProjectWorkspaceKind::DetachedFile {
399 cargo: None,
400 ..
401 } => false,
402 });
403 if let Some(idx) = package_workspace_idx {
404 if let Some(flycheck) =
408 world.flycheck.iter().find(|fc| fc.id() == idx)
409 {
410 let workspace_deps =
411 world.all_workspace_dependencies_for_package(&package);
412 flycheck.restart_for_package(
413 package,
414 target,
415 workspace_deps,
416 saved_file.clone(),
417 );
418 }
419 }
420 }
421 }
422
423 if !may_flycheck_workspace {
424 return Ok(());
425 }
426
427 let crate_ids: Vec<_> = world
430 .analysis
431 .crates_for(file_id)?
432 .into_iter()
433 .flat_map(|id| world.analysis.transitive_rev_deps(id))
434 .flatten()
435 .unique()
436 .collect();
437 tracing::debug!(?crate_ids, "flycheck crate ids");
438 let crate_root_paths: Vec<_> = crate_ids
439 .iter()
440 .filter_map(|&crate_id| {
441 world
442 .analysis
443 .crate_root(crate_id)
444 .map(|file_id| {
445 world
446 .file_id_to_file_path(file_id)
447 .as_path()
448 .map(ToOwned::to_owned)
449 })
450 .transpose()
451 })
452 .collect::<ide::Cancellable<_>>()?;
453 let crate_root_paths: Vec<_> =
454 crate_root_paths.iter().map(Deref::deref).collect();
455 tracing::debug!(?crate_root_paths, "flycheck crate roots");
456
457 let workspace_ids =
459 world.workspaces.iter().enumerate().filter(|&(idx, ws)| {
460 let ws_contains_file = match &ws.kind {
461 project_model::ProjectWorkspaceKind::Cargo {
462 cargo, ..
463 }
464 | project_model::ProjectWorkspaceKind::DetachedFile {
465 cargo: Some((cargo, _, _)),
466 ..
467 } => cargo.packages().any(|pkg| {
468 cargo[pkg].targets.iter().any(|&it| {
469 crate_root_paths.contains(&cargo[it].root.as_path())
470 })
471 }),
472 project_model::ProjectWorkspaceKind::Json(project) => {
473 project.crates().any(|(_, krate)| {
474 crate_root_paths.contains(&krate.root_module.as_path())
475 })
476 }
477 project_model::ProjectWorkspaceKind::DetachedFile {
478 ..
479 } => false,
480 };
481 let is_pkg_ws = match package_workspace_idx {
482 Some(pkg_idx) => pkg_idx == idx,
483 None => false,
484 };
485 ws_contains_file && !is_pkg_ws
486 });
487
488 let mut workspace_check_triggered = false;
489 'flychecks: for flycheck in world.flycheck.iter() {
491 for (id, _) in workspace_ids.clone() {
492 if id == flycheck.id() {
493 workspace_check_triggered = true;
494 flycheck.restart_workspace(saved_file.clone());
495 continue 'flychecks;
496 }
497 }
498 }
499
500 if !workspace_check_triggered && package_workspace_idx.is_none() {
502 for flycheck in world.flycheck.iter() {
503 flycheck.restart_workspace(saved_file.clone());
504 }
505 }
506 Ok(())
507 })
508 }
509 };
510
511 state.task_pool.handle.spawn_with_sender(stdx::thread::ThreadIntent::Worker, move |_| {
512 if let Err(e) = std::panic::catch_unwind(task) {
513 tracing::error!("flycheck task panicked: {e:?}")
514 }
515 });
516 true
517 } else {
518 false
519 }
520}
521
522pub(crate) fn handle_cancel_flycheck(state: &mut GlobalState, _: ()) -> anyhow::Result<()> {
523 let _p = tracing::info_span!("handle_cancel_flycheck").entered();
524 state.flycheck.iter().for_each(|flycheck| flycheck.cancel());
525 Ok(())
526}
527
528pub(crate) fn handle_clear_flycheck(state: &mut GlobalState, _: ()) -> anyhow::Result<()> {
529 let _p = tracing::info_span!("handle_clear_flycheck").entered();
530 state.diagnostics.clear_check_all();
531 Ok(())
532}
533
534pub(crate) fn handle_run_flycheck(
535 state: &mut GlobalState,
536 params: RunFlycheckParams,
537) -> anyhow::Result<()> {
538 let _p = tracing::info_span!("handle_run_flycheck").entered();
539 if let Some(text_document) = params.text_document
540 && let Ok(vfs_path) = from_proto::vfs_path(&text_document.uri)
541 && run_flycheck(state, vfs_path)
542 {
543 return Ok(());
544 }
545 if state.config.flycheck_workspace(None) {
547 for flycheck in state.flycheck.iter() {
548 flycheck.restart_workspace(None);
549 }
550 }
551 Ok(())
552}
553
554pub(crate) fn handle_abort_run_test(state: &mut GlobalState, _: ()) -> anyhow::Result<()> {
555 if state.test_run_session.take().is_some() {
556 state.send_notification::<lsp_ext::EndRunTest>(());
557 }
558 Ok(())
559}