Skip to main content

rust_analyzer/handlers/
notification.rs

1//! This module is responsible for implementing handlers for Language Server
2//! Protocol. This module specifically handles notifications.
3
4use 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) = &params.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    // Just ignore this. It is OK to continue sending progress
53    // notifications for this token, as the client can't know when
54    // we accepted notification.
55    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(&params.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(&params.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        // The version passed in DidChangeTextDocument is the version after all edits are applied
112        // so we should apply it before the vfs is notified.
113        *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(&params.text_document.uri) {
136        if state.mem_docs.remove(&path).is_err() {
137            tracing::error!("orphan DidCloseTextDocument: {}", path);
138        }
139
140        // Clear diagnostics also for excluded files, just in case.
141        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(&params.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(&params.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        // Re-fetch workspaces if a workspace related file has changed
171        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            // FIXME: We should move this check into a QueuedTask and do semantic resolution of
179            // the files. There is only so much we can tell syntactically from the path.
180            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        // No specific flycheck was triggered, so let's trigger all of them.
204        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    // As stated in https://github.com/microsoft/language-server-protocol/issues/676,
217    // this notification's parameters should be ignored and the actual config queried separately.
218    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                        // Client config changes necessitates .update_config method to be called.
243                        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    // we want to trigger flycheck if a file outside of our workspaces has changed,
293    // as to reduce stale diagnostics when outside changes happen
294    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 if no workspaces contain this file.
299                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                        // FIXME: Because triomphe::Arc's auto UnwindSafe impl requires that the inner type
328                        // be UnwindSafe, and FlycheckHandle is not UnwindSafe, `word.flycheck` cannot
329                        // be captured directly. std::sync::Arc has an UnwindSafe impl that only requires
330                        // that the inner type be RefUnwindSafe, so if we were using that one we wouldn't
331                        // have this problem. Remove the line below when triomphe::Arc has an UnwindSafe impl
332                        // like std::sync::Arc's.
333                        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                        // we have a specific non-library target, attempt to only check that target, nothing
375                        // else will be affected
376                        let mut package_workspace_idx = None;
377                        if let Some((target, root, package)) = target {
378                            // trigger a package check if we have a non-library target as that can't affect
379                            // anything else in the workspace OR if we're not allowed to check the workspace as
380                            // the user opted into package checks then OR if this is not cargo.
381                            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                                    // flycheck handles are indexed by their ID (which is the workspace index),
405                                    // but not all workspaces have flycheck enabled (e.g., JSON projects without
406                                    // a flycheck template). Find the flycheck handle by its ID.
407                                    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                        // Trigger flychecks for all workspaces that depend on the saved file
428                        // Crates containing or depending on the saved file
429                        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                        // Find all workspaces that have at least one target containing the saved file
458                        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                        // Find and trigger corresponding flychecks
490                        '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                        // No specific flycheck was triggered, so let's trigger all of them.
501                        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    // No specific flycheck was triggered, so let's trigger all of them.
546    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}