lsp: Add support for clangd's `inactiveRegions` extension (#26146)

Naim A. , Peter Tripp , and Kirill Bulatov created

Closes #13089 

Here we use `experimental` to advertise our support for
`inactiveRegions`. Note that clangd does not currently have a stable
release that reads the `experimental` object (PR
https://github.com/llvm/llvm-project/pull/116531), this can be tested
with one of clangd's recent "unstable snapshots" in their
[releases](https://github.com/clangd/clangd/releases).

Release Notes:

- Added support for clangd's `inactiveRegions` extension.

![Screen Recording 2025-03-05 at 22 39
58](https://github.com/user-attachments/assets/ceade8bd-4d8e-43c3-9520-ad44efa50d2f)

---------

Co-authored-by: Peter Tripp <peter@zed.dev>
Co-authored-by: Kirill Bulatov <kirill@zed.dev>

Change summary

crates/editor/src/clangd_ext.rs                   |  4 
crates/editor/src/rust_analyzer_ext.rs            |  6 
crates/languages/src/c.rs                         |  3 
crates/lsp/src/lsp.rs                             | 28 -----
crates/project/src/lsp_store.rs                   | 56 +---------
crates/project/src/lsp_store/clangd_ext.rs        | 75 +++++++++++++++
crates/project/src/lsp_store/lsp_ext_command.rs   |  0 
crates/project/src/lsp_store/rust_analyzer_ext.rs | 83 +++++++++++++++++
crates/project/src/project.rs                     |  1 
9 files changed, 174 insertions(+), 82 deletions(-)

Detailed changes

crates/editor/src/clangd_ext.rs 🔗

@@ -7,7 +7,7 @@ use crate::lsp_ext::find_specific_language_server_in_selection;
 
 use crate::{element::register_action, Editor, SwitchSourceHeader};
 
-const CLANGD_SERVER_NAME: &str = "clangd";
+use project::lsp_store::clangd_ext::CLANGD_SERVER_NAME;
 
 fn is_c_language(language: &Language) -> bool {
     return language.name() == "C++".into() || language.name() == "C".into();
@@ -46,7 +46,7 @@ pub fn switch_source_header(
         project.request_lsp(
             buffer,
             project::LanguageServerToQuery::Other(server_to_query),
-            project::lsp_ext_command::SwitchSourceHeader,
+            project::lsp_store::lsp_ext_command::SwitchSourceHeader,
             cx,
         )
     });

crates/editor/src/rust_analyzer_ext.rs 🔗

@@ -4,7 +4,7 @@ use anyhow::Context as _;
 use gpui::{App, AppContext as _, Context, Entity, Window};
 use language::{Capability, Language};
 use multi_buffer::MultiBuffer;
-use project::lsp_ext_command::ExpandMacro;
+use project::lsp_store::{lsp_ext_command::ExpandMacro, rust_analyzer_ext::RUST_ANALYZER_NAME};
 use text::ToPointUtf16;
 
 use crate::{
@@ -12,8 +12,6 @@ use crate::{
     ExpandMacroRecursively, OpenDocs,
 };
 
-const RUST_ANALYZER_NAME: &str = "rust-analyzer";
-
 fn is_rust_language(language: &Language) -> bool {
     language.name() == "Rust".into()
 }
@@ -131,7 +129,7 @@ pub fn open_docs(editor: &mut Editor, _: &OpenDocs, window: &mut Window, cx: &mu
         project.request_lsp(
             buffer,
             project::LanguageServerToQuery::Other(server_to_query),
-            project::lsp_ext_command::OpenDocs { position },
+            project::lsp_store::lsp_ext_command::OpenDocs { position },
             cx,
         )
     });

crates/languages/src/c.rs 🔗

@@ -278,6 +278,9 @@ impl super::LspAdapter for CLspAdapter {
             "textDocument": {
                 "completion" : {
                     "editsNearCursor": true
+                },
+                "inactiveRegionsCapabilities": {
+                    "inactiveRegions": true,
                 }
             }
         });

crates/lsp/src/lsp.rs 🔗

@@ -299,34 +299,6 @@ pub struct AdapterServerCapabilities {
     pub code_action_kinds: Option<Vec<CodeActionKind>>,
 }
 
-/// Experimental: Informs the end user about the state of the server
-///
-/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
-#[derive(Debug)]
-pub enum ServerStatus {}
-
-/// Other(String) variant to handle unknown values due to this still being experimental
-#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub enum ServerHealthStatus {
-    Ok,
-    Warning,
-    Error,
-    Other(String),
-}
-
-#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
-#[serde(rename_all = "camelCase")]
-pub struct ServerStatusParams {
-    pub health: ServerHealthStatus,
-    pub message: Option<String>,
-}
-
-impl lsp_types::notification::Notification for ServerStatus {
-    type Params = ServerStatusParams;
-    const METHOD: &'static str = "experimental/serverStatus";
-}
-
 impl LanguageServer {
     /// Starts a language server process.
     #[allow(clippy::too_many_arguments)]

crates/project/src/lsp_store.rs 🔗

@@ -1,9 +1,12 @@
+pub mod clangd_ext;
+pub mod lsp_ext_command;
+pub mod rust_analyzer_ext;
+
 use crate::{
     buffer_store::{BufferStore, BufferStoreEvent},
     deserialize_code_actions,
     environment::ProjectEnvironment,
     lsp_command::{self, *},
-    lsp_ext_command,
     prettier_store::{self, PrettierStore, PrettierStoreEvent},
     project_settings::{LspSettings, ProjectSettings},
     project_tree::{AdapterQuery, LanguageServerTree, LaunchDisposition, ProjectTree},
@@ -48,8 +51,8 @@ use lsp::{
     FileOperationPatternKind, FileOperationRegistrationOptions, FileRename, FileSystemWatcher,
     InsertTextFormat, LanguageServer, LanguageServerBinary, LanguageServerBinaryOptions,
     LanguageServerId, LanguageServerName, LspRequestFuture, MessageActionItem, MessageType, OneOf,
-    RenameFilesParams, ServerHealthStatus, ServerStatus, SymbolKind, TextEdit, WillRenameFiles,
-    WorkDoneProgressCancelParams, WorkspaceFolder,
+    RenameFilesParams, SymbolKind, TextEdit, WillRenameFiles, WorkDoneProgressCancelParams,
+    WorkspaceFolder,
 };
 use node_runtime::read_package_installed_version;
 use parking_lot::Mutex;
@@ -841,50 +844,6 @@ impl LocalLspStore {
                 }
             })
             .detach();
-
-        language_server
-            .on_notification::<ServerStatus, _>({
-                let this = this.clone();
-                let name = name.to_string();
-                move |params, mut cx| {
-                    let this = this.clone();
-                    let name = name.to_string();
-                    if let Some(ref message) = params.message {
-                        let message = message.trim();
-                        if !message.is_empty() {
-                            let formatted_message = format!(
-                                "Language server {name} (id {server_id}) status update: {message}"
-                            );
-                            match params.health {
-                                ServerHealthStatus::Ok => log::info!("{}", formatted_message),
-                                ServerHealthStatus::Warning => log::warn!("{}", formatted_message),
-                                ServerHealthStatus::Error => {
-                                    log::error!("{}", formatted_message);
-                                    let (tx, _rx) = smol::channel::bounded(1);
-                                    let request = LanguageServerPromptRequest {
-                                        level: PromptLevel::Critical,
-                                        message: params.message.unwrap_or_default(),
-                                        actions: Vec::new(),
-                                        response_channel: tx,
-                                        lsp_name: name.clone(),
-                                    };
-                                    let _ = this
-                                        .update(&mut cx, |_, cx| {
-                                            cx.emit(LspStoreEvent::LanguageServerPrompt(request));
-                                        })
-                                        .ok();
-                                }
-                                ServerHealthStatus::Other(status) => {
-                                    log::info!(
-                                        "Unknown server health: {status}\n{formatted_message}"
-                                    )
-                                }
-                            }
-                        }
-                    }
-                }
-            })
-            .detach();
         language_server
             .on_notification::<lsp::notification::ShowMessage, _>({
                 let this = this.clone();
@@ -970,6 +929,9 @@ impl LocalLspStore {
                 }
             })
             .detach();
+
+        rust_analyzer_ext::register_notifications(this.clone(), language_server);
+        clangd_ext::register_notifications(this, language_server, adapter);
     }
 
     fn shutdown_language_servers(

crates/project/src/lsp_store/clangd_ext.rs 🔗

@@ -0,0 +1,75 @@
+use std::sync::Arc;
+
+use ::serde::{Deserialize, Serialize};
+use gpui::WeakEntity;
+use language::CachedLspAdapter;
+use lsp::LanguageServer;
+use util::ResultExt as _;
+
+use crate::LspStore;
+
+pub const CLANGD_SERVER_NAME: &str = "clangd";
+
+#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct InactiveRegionsParams {
+    pub text_document: lsp::OptionalVersionedTextDocumentIdentifier,
+    pub regions: Vec<lsp::Range>,
+}
+
+/// InactiveRegions is a clangd extension that marks regions of inactive code.
+pub struct InactiveRegions;
+
+impl lsp::notification::Notification for InactiveRegions {
+    type Params = InactiveRegionsParams;
+    const METHOD: &'static str = "textDocument/inactiveRegions";
+}
+
+pub fn register_notifications(
+    lsp_store: WeakEntity<LspStore>,
+    language_server: &LanguageServer,
+    adapter: Arc<CachedLspAdapter>,
+) {
+    if language_server.name().0 != CLANGD_SERVER_NAME {
+        return;
+    }
+    let server_id = language_server.server_id();
+
+    language_server
+        .on_notification::<InactiveRegions, _>({
+            let adapter = adapter.clone();
+            let this = lsp_store;
+
+            move |params: InactiveRegionsParams, mut cx| {
+                let adapter = adapter.clone();
+                this.update(&mut cx, |this, cx| {
+                    let diagnostics = params
+                        .regions
+                        .into_iter()
+                        .map(|range| lsp::Diagnostic {
+                            range,
+                            severity: Some(lsp::DiagnosticSeverity::INFORMATION),
+                            source: Some(CLANGD_SERVER_NAME.to_string()),
+                            message: "inactive region".to_string(),
+                            tags: Some(vec![lsp::DiagnosticTag::UNNECESSARY]),
+                            ..Default::default()
+                        })
+                        .collect();
+                    let mapped_diagnostics = lsp::PublishDiagnosticsParams {
+                        uri: params.text_document.uri,
+                        version: params.text_document.version,
+                        diagnostics,
+                    };
+                    this.update_diagnostics(
+                        server_id,
+                        mapped_diagnostics,
+                        &adapter.disk_based_diagnostic_sources,
+                        cx,
+                    )
+                    .log_err();
+                })
+                .ok();
+            }
+        })
+        .detach();
+}

crates/project/src/lsp_store/rust_analyzer_ext.rs 🔗

@@ -0,0 +1,83 @@
+use ::serde::{Deserialize, Serialize};
+use gpui::{PromptLevel, WeakEntity};
+use lsp::LanguageServer;
+
+use crate::{LanguageServerPromptRequest, LspStore, LspStoreEvent};
+
+pub const RUST_ANALYZER_NAME: &str = "rust-analyzer";
+
+/// Experimental: Informs the end user about the state of the server
+///
+/// [Rust Analyzer Specification](https://github.com/rust-lang/rust-analyzer/blob/master/docs/dev/lsp-extensions.md#server-status)
+#[derive(Debug)]
+enum ServerStatus {}
+
+/// Other(String) variant to handle unknown values due to this still being experimental
+#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+enum ServerHealthStatus {
+    Ok,
+    Warning,
+    Error,
+    Other(String),
+}
+
+#[derive(Debug, PartialEq, Deserialize, Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+struct ServerStatusParams {
+    pub health: ServerHealthStatus,
+    pub message: Option<String>,
+}
+
+impl lsp::notification::Notification for ServerStatus {
+    type Params = ServerStatusParams;
+    const METHOD: &'static str = "experimental/serverStatus";
+}
+
+pub fn register_notifications(lsp_store: WeakEntity<LspStore>, language_server: &LanguageServer) {
+    let name = language_server.name();
+    let server_id = language_server.server_id();
+
+    let this = lsp_store;
+
+    language_server
+        .on_notification::<ServerStatus, _>({
+            let name = name.to_string();
+            move |params, mut cx| {
+                let this = this.clone();
+                let name = name.to_string();
+                if let Some(ref message) = params.message {
+                    let message = message.trim();
+                    if !message.is_empty() {
+                        let formatted_message = format!(
+                            "Language server {name} (id {server_id}) status update: {message}"
+                        );
+                        match params.health {
+                            ServerHealthStatus::Ok => log::info!("{}", formatted_message),
+                            ServerHealthStatus::Warning => log::warn!("{}", formatted_message),
+                            ServerHealthStatus::Error => {
+                                log::error!("{}", formatted_message);
+                                let (tx, _rx) = smol::channel::bounded(1);
+                                let request = LanguageServerPromptRequest {
+                                    level: PromptLevel::Critical,
+                                    message: params.message.unwrap_or_default(),
+                                    actions: Vec::new(),
+                                    response_channel: tx,
+                                    lsp_name: name.clone(),
+                                };
+                                let _ = this
+                                    .update(&mut cx, |_, cx| {
+                                        cx.emit(LspStoreEvent::LanguageServerPrompt(request));
+                                    })
+                                    .ok();
+                            }
+                            ServerHealthStatus::Other(status) => {
+                                log::info!("Unknown server health: {status}\n{formatted_message}")
+                            }
+                        }
+                    }
+                }
+            }
+        })
+        .detach();
+}

crates/project/src/project.rs 🔗

@@ -5,7 +5,6 @@ pub mod debounced_delay;
 pub mod git;
 pub mod image_store;
 pub mod lsp_command;
-pub mod lsp_ext_command;
 pub mod lsp_store;
 pub mod prettier_store;
 pub mod project_settings;