Support dynamic formatting capabilities [un]registration (#14478)

Kirill Bulatov created

Closes https://github.com/zed-industries/zed/issues/12661

Release Notes:

- Added dynamic [un]registration for LSP formatting capabilities
([#12661](https://github.com/zed-industries/zed/issues/12661))

Change summary

crates/lsp/src/lsp.rs             |  22 ++-
crates/project/src/lsp_command.rs |   2 
crates/project/src/project.rs     | 181 +++++++++++++++++++++++++++-----
3 files changed, 167 insertions(+), 38 deletions(-)

Detailed changes

crates/lsp/src/lsp.rs 🔗

@@ -7,7 +7,7 @@ use anyhow::{anyhow, Context, Result};
 use collections::HashMap;
 use futures::{channel::oneshot, io::BufWriter, select, AsyncRead, AsyncWrite, Future, FutureExt};
 use gpui::{AppContext, AsyncAppContext, BackgroundExecutor, Task};
-use parking_lot::Mutex;
+use parking_lot::{Mutex, RwLock};
 use postage::{barrier, prelude::Stream};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use serde_json::{json, value::RawValue, Value};
@@ -24,6 +24,7 @@ use std::{
     ffi::OsString,
     fmt,
     io::Write,
+    ops::DerefMut,
     path::PathBuf,
     pin::Pin,
     sync::{
@@ -69,7 +70,7 @@ pub struct LanguageServer {
     next_id: AtomicI32,
     outbound_tx: channel::Sender<String>,
     name: Arc<str>,
-    capabilities: ServerCapabilities,
+    capabilities: RwLock<ServerCapabilities>,
     code_action_kinds: Option<Vec<CodeActionKind>>,
     notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
     response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
@@ -640,10 +641,13 @@ impl LanguageServer {
                         ..Default::default()
                     }),
                     formatting: Some(DynamicRegistrationClientCapabilities {
-                        dynamic_registration: None,
+                        dynamic_registration: Some(true),
+                    }),
+                    range_formatting: Some(DynamicRegistrationClientCapabilities {
+                        dynamic_registration: Some(true),
                     }),
                     on_type_formatting: Some(DynamicRegistrationClientCapabilities {
-                        dynamic_registration: None,
+                        dynamic_registration: Some(true),
                     }),
                     signature_help: Some(SignatureHelpClientCapabilities {
                         signature_information: Some(SignatureInformationSettings {
@@ -693,7 +697,7 @@ impl LanguageServer {
             if let Some(info) = response.server_info {
                 self.name = info.name.into();
             }
-            self.capabilities = response.capabilities;
+            self.capabilities = RwLock::new(response.capabilities);
 
             self.notify::<notification::Initialized>(InitializedParams {})?;
             Ok(Arc::new(self))
@@ -908,8 +912,12 @@ impl LanguageServer {
     }
 
     /// Get the reported capabilities of the running language server.
-    pub fn capabilities(&self) -> &ServerCapabilities {
-        &self.capabilities
+    pub fn capabilities(&self) -> ServerCapabilities {
+        self.capabilities.read().clone()
+    }
+
+    pub fn update_capabilities(&self, update: impl FnOnce(&mut ServerCapabilities)) {
+        update(self.capabilities.write().deref_mut());
     }
 
     /// Get the id of the running language server.

crates/project/src/lsp_command.rs 🔗

@@ -2596,7 +2596,7 @@ impl LspCommand for InlayHints {
             lsp_adapter.name.0.as_ref() == "typescript-language-server";
 
         let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| {
-            let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) {
+            let resolve_state = if InlayHints::can_resolve_inlays(&lsp_server.capabilities()) {
                 ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone())
             } else {
                 ResolveState::Resolved

crates/project/src/project.rs 🔗

@@ -3174,7 +3174,7 @@ impl Project {
     }
 
     async fn setup_pending_language_server(
-        this: WeakModel<Self>,
+        project: WeakModel<Self>,
         override_options: Option<serde_json::Value>,
         pending_server: PendingLanguageServer,
         delegate: Arc<dyn LspAdapterDelegate>,
@@ -3193,7 +3193,7 @@ impl Project {
         language_server
             .on_notification::<lsp::notification::PublishDiagnostics, _>({
                 let adapter = adapter.clone();
-                let this = this.clone();
+                let this = project.clone();
                 move |mut params, mut cx| {
                     let adapter = adapter.clone();
                     if let Some(this) = this.upgrade() {
@@ -3247,7 +3247,7 @@ impl Project {
         // to these requests when initializing.
         language_server
             .on_request::<lsp::request::WorkDoneProgressCreate, _, _>({
-                let this = this.clone();
+                let this = project.clone();
                 move |params, mut cx| {
                     let this = this.clone();
                     async move {
@@ -3268,20 +3268,103 @@ impl Project {
 
         language_server
             .on_request::<lsp::request::RegisterCapability, _, _>({
-                let this = this.clone();
+                let project = project.clone();
                 move |params, mut cx| {
-                    let this = this.clone();
+                    let project = project.clone();
                     async move {
                         for reg in params.registrations {
-                            if reg.method == "workspace/didChangeWatchedFiles" {
-                                if let Some(options) = reg.register_options {
-                                    let options = serde_json::from_value(options)?;
-                                    this.update(&mut cx, |this, cx| {
-                                        this.on_lsp_did_change_watched_files(
-                                            server_id, &reg.id, options, cx,
-                                        );
-                                    })?;
+                            match reg.method.as_str() {
+                                "workspace/didChangeWatchedFiles" => {
+                                    if let Some(options) = reg.register_options {
+                                        let options = serde_json::from_value(options)?;
+                                        project.update(&mut cx, |project, cx| {
+                                            project.on_lsp_did_change_watched_files(
+                                                server_id, &reg.id, options, cx,
+                                            );
+                                        })?;
+                                    }
+                                }
+                                "textDocument/rangeFormatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            let options = reg
+                                                .register_options
+                                                .map(|options| {
+                                                    serde_json::from_value::<
+                                                        lsp::DocumentRangeFormattingOptions,
+                                                    >(
+                                                        options
+                                                    )
+                                                })
+                                                .transpose()?;
+                                            let provider = match options {
+                                                None => OneOf::Left(true),
+                                                Some(options) => OneOf::Right(options),
+                                            };
+                                            server.update_capabilities(|capabilities| {
+                                                capabilities.document_range_formatting_provider =
+                                                    Some(provider);
+                                            })
+                                        }
+                                        anyhow::Ok(())
+                                    })??;
                                 }
+                                "textDocument/onTypeFormatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            let options = reg
+                                                .register_options
+                                                .map(|options| {
+                                                    serde_json::from_value::<
+                                                        lsp::DocumentOnTypeFormattingOptions,
+                                                    >(
+                                                        options
+                                                    )
+                                                })
+                                                .transpose()?;
+                                            if let Some(options) = options {
+                                                server.update_capabilities(|capabilities| {
+                                                    capabilities
+                                                        .document_on_type_formatting_provider =
+                                                        Some(options);
+                                                })
+                                            }
+                                        }
+                                        anyhow::Ok(())
+                                    })??;
+                                }
+                                "textDocument/formatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            let options = reg
+                                                .register_options
+                                                .map(|options| {
+                                                    serde_json::from_value::<
+                                                        lsp::DocumentFormattingOptions,
+                                                    >(
+                                                        options
+                                                    )
+                                                })
+                                                .transpose()?;
+                                            let provider = match options {
+                                                None => OneOf::Left(true),
+                                                Some(options) => OneOf::Right(options),
+                                            };
+                                            server.update_capabilities(|capabilities| {
+                                                capabilities.document_formatting_provider =
+                                                    Some(provider);
+                                            })
+                                        }
+                                        anyhow::Ok(())
+                                    })??;
+                                }
+                                _ => log::warn!("unhandled capability registration: {reg:?}"),
                             }
                         }
                         Ok(())
@@ -3292,17 +3375,55 @@ impl Project {
 
         language_server
             .on_request::<lsp::request::UnregisterCapability, _, _>({
-                let this = this.clone();
+                let this = project.clone();
                 move |params, mut cx| {
-                    let this = this.clone();
+                    let project = this.clone();
                     async move {
                         for unreg in params.unregisterations.iter() {
-                            if unreg.method == "workspace/didChangeWatchedFiles" {
-                                this.update(&mut cx, |this, cx| {
-                                    this.on_lsp_unregister_did_change_watched_files(
-                                        server_id, &unreg.id, cx,
-                                    );
-                                })?;
+                            match unreg.method.as_str() {
+                                "workspace/didChangeWatchedFiles" => {
+                                    project.update(&mut cx, |project, cx| {
+                                        project.on_lsp_unregister_did_change_watched_files(
+                                            server_id, &unreg.id, cx,
+                                        );
+                                    })?;
+                                }
+                                "textDocument/rangeFormatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            server.update_capabilities(|capabilities| {
+                                                capabilities.document_range_formatting_provider =
+                                                    None
+                                            })
+                                        }
+                                    })?;
+                                }
+                                "textDocument/onTypeFormatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            server.update_capabilities(|capabilities| {
+                                                capabilities.document_on_type_formatting_provider =
+                                                    None;
+                                            })
+                                        }
+                                    })?;
+                                }
+                                "textDocument/formatting" => {
+                                    project.update(&mut cx, |project, _| {
+                                        if let Some(server) =
+                                            project.language_server_for_id(server_id)
+                                        {
+                                            server.update_capabilities(|capabilities| {
+                                                capabilities.document_formatting_provider = None;
+                                            })
+                                        }
+                                    })?;
+                                }
+                                _ => log::warn!("unhandled capability unregistration: {unreg:?}"),
                             }
                         }
                         Ok(())
@@ -3314,7 +3435,7 @@ impl Project {
         language_server
             .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
                 let adapter = adapter.clone();
-                let this = this.clone();
+                let this = project.clone();
                 move |params, cx| {
                     Self::on_lsp_workspace_edit(
                         this.clone(),
@@ -3329,7 +3450,7 @@ impl Project {
 
         language_server
             .on_request::<lsp::request::InlayHintRefreshRequest, _, _>({
-                let this = this.clone();
+                let this = project.clone();
                 move |(), mut cx| {
                     let this = this.clone();
                     async move {
@@ -3348,7 +3469,7 @@ impl Project {
 
         language_server
             .on_request::<lsp::request::ShowMessageRequest, _, _>({
-                let this = this.clone();
+                let this = project.clone();
                 let name = name.to_string();
                 move |params, mut cx| {
                     let this = this.clone();
@@ -3387,7 +3508,7 @@ impl Project {
 
         language_server
             .on_notification::<ServerStatus, _>({
-                let this = this.clone();
+                let this = project.clone();
                 let name = name.to_string();
                 move |params, mut cx| {
                     let this = this.clone();
@@ -3430,7 +3551,7 @@ impl Project {
             .detach();
         language_server
             .on_notification::<lsp::notification::ShowMessage, _>({
-                let this = this.clone();
+                let this = project.clone();
                 let name = name.to_string();
                 move |params, mut cx| {
                     let this = this.clone();
@@ -3457,7 +3578,7 @@ impl Project {
             .detach();
         language_server
             .on_notification::<lsp::notification::Progress, _>(move |params, mut cx| {
-                if let Some(this) = this.upgrade() {
+                if let Some(this) = project.upgrade() {
                     this.update(&mut cx, |this, cx| {
                         this.on_lsp_progress(
                             params,
@@ -6774,7 +6895,7 @@ impl Project {
             } else {
                 return Task::ready(Ok(hint));
             };
-            if !InlayHints::can_resolve_inlays(lang_server.capabilities()) {
+            if !InlayHints::can_resolve_inlays(&lang_server.capabilities()) {
                 return Task::ready(Ok(hint));
             }
 
@@ -7186,7 +7307,7 @@ impl Project {
                 let lsp_params = request.to_lsp(&file.abs_path(cx), buffer, &language_server, cx);
                 let status = request.status();
                 return cx.spawn(move |this, cx| async move {
-                    if !request.check_capabilities(language_server.capabilities()) {
+                    if !request.check_capabilities(&language_server.capabilities()) {
                         return Ok(Default::default());
                     }
 
@@ -7280,7 +7401,7 @@ impl Project {
         let scope = position.and_then(|position| snapshot.language_scope_at(position));
         let mut response_results = self
             .language_servers_for_buffer(buffer.read(cx), cx)
-            .filter(|(_, server)| server_capabilities_check(server.capabilities()))
+            .filter(|(_, server)| server_capabilities_check(&server.capabilities()))
             .filter(|(adapter, _)| {
                 scope
                     .as_ref()