Allow formatting selections via LSP (#18752)

Ihnat Aลญtuลกka and Thorsten Ball created

Release Notes:

- Added a new `editor: format selections` action that allows formatting
only the currently selected text via the primary language server.

---------

Co-authored-by: Thorsten Ball <mrnugget@gmail.com>

Change summary

crates/collab/src/tests/integration_tests.rs |   5 
crates/editor/src/actions.rs                 |   1 
crates/editor/src/editor.rs                  |  34 +
crates/editor/src/editor_tests.rs            |  23 +
crates/editor/src/element.rs                 |   7 
crates/editor/src/items.rs                   |   8 
crates/editor/src/mouse_context_menu.rs      |  14 
crates/project/src/lsp_store.rs              | 388 +++++++++++++--------
crates/project/src/project.rs                |   3 
9 files changed, 323 insertions(+), 160 deletions(-)

Detailed changes

crates/collab/src/tests/integration_tests.rs ๐Ÿ”—

@@ -27,6 +27,7 @@ use language::{
 use live_kit_client::MacOSDisplay;
 use lsp::LanguageServerId;
 use parking_lot::Mutex;
+use project::lsp_store::FormatTarget;
 use project::{
     lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary,
     HoverBlockKind, Project, ProjectPath,
@@ -4417,6 +4418,7 @@ async fn test_formatting_buffer(
                 HashSet::from_iter([buffer_b.clone()]),
                 true,
                 FormatTrigger::Save,
+                FormatTarget::Buffer,
                 cx,
             )
         })
@@ -4450,6 +4452,7 @@ async fn test_formatting_buffer(
                 HashSet::from_iter([buffer_b.clone()]),
                 true,
                 FormatTrigger::Save,
+                FormatTarget::Buffer,
                 cx,
             )
         })
@@ -4555,6 +4558,7 @@ async fn test_prettier_formatting_buffer(
                 HashSet::from_iter([buffer_b.clone()]),
                 true,
                 FormatTrigger::Save,
+                FormatTarget::Buffer,
                 cx,
             )
         })
@@ -4574,6 +4578,7 @@ async fn test_prettier_formatting_buffer(
                 HashSet::from_iter([buffer_a.clone()]),
                 true,
                 FormatTrigger::Manual,
+                FormatTarget::Buffer,
                 cx,
             )
         })

crates/editor/src/actions.rs ๐Ÿ”—

@@ -237,6 +237,7 @@ gpui::actions!(
         ToggleFold,
         ToggleFoldRecursive,
         Format,
+        FormatSelections,
         GoToDeclaration,
         GoToDeclarationSplit,
         GoToDefinition,

crates/editor/src/editor.rs ๐Ÿ”—

@@ -122,7 +122,7 @@ use multi_buffer::{
 use ordered_float::OrderedFloat;
 use parking_lot::{Mutex, RwLock};
 use project::{
-    lsp_store::FormatTrigger,
+    lsp_store::{FormatTarget, FormatTrigger},
     project_settings::{GitGutterSetting, ProjectSettings},
     CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location,
     LocationLink, Project, ProjectPath, ProjectTransaction, TaskSourceKind,
@@ -10386,13 +10386,39 @@ impl Editor {
             None => return None,
         };
 
-        Some(self.perform_format(project, FormatTrigger::Manual, cx))
+        Some(self.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx))
+    }
+
+    fn format_selections(
+        &mut self,
+        _: &FormatSelections,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Task<Result<()>>> {
+        let project = match &self.project {
+            Some(project) => project.clone(),
+            None => return None,
+        };
+
+        let selections = self
+            .selections
+            .all_adjusted(cx)
+            .into_iter()
+            .filter(|s| !s.is_empty())
+            .collect_vec();
+
+        Some(self.perform_format(
+            project,
+            FormatTrigger::Manual,
+            FormatTarget::Ranges(selections),
+            cx,
+        ))
     }
 
     fn perform_format(
         &mut self,
         project: Model<Project>,
         trigger: FormatTrigger,
+        target: FormatTarget,
         cx: &mut ViewContext<Self>,
     ) -> Task<Result<()>> {
         let buffer = self.buffer().clone();
@@ -10402,7 +10428,9 @@ impl Editor {
         }
 
         let mut timeout = cx.background_executor().timer(FORMAT_TIMEOUT).fuse();
-        let format = project.update(cx, |project, cx| project.format(buffers, true, trigger, cx));
+        let format = project.update(cx, |project, cx| {
+            project.format(buffers, true, trigger, target, cx)
+        });
 
         cx.spawn(|_, mut cx| async move {
             let transaction = futures::select_biased! {

crates/editor/src/editor_tests.rs ๐Ÿ”—

@@ -7076,7 +7076,12 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
 
     let format = editor
         .update(cx, |editor, cx| {
-            editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+            editor.perform_format(
+                project.clone(),
+                FormatTrigger::Manual,
+                FormatTarget::Buffer,
+                cx,
+            )
         })
         .unwrap();
     fake_server
@@ -7112,7 +7117,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) {
     });
     let format = editor
         .update(cx, |editor, cx| {
-            editor.perform_format(project, FormatTrigger::Manual, cx)
+            editor.perform_format(project, FormatTrigger::Manual, FormatTarget::Buffer, cx)
         })
         .unwrap();
     cx.executor().advance_clock(super::FORMAT_TIMEOUT);
@@ -10309,7 +10314,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
 
     editor
         .update(cx, |editor, cx| {
-            editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+            editor.perform_format(
+                project.clone(),
+                FormatTrigger::Manual,
+                FormatTarget::Buffer,
+                cx,
+            )
         })
         .unwrap()
         .await;
@@ -10323,7 +10333,12 @@ async fn test_document_format_with_prettier(cx: &mut gpui::TestAppContext) {
         settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
     });
     let format = editor.update(cx, |editor, cx| {
-        editor.perform_format(project.clone(), FormatTrigger::Manual, cx)
+        editor.perform_format(
+            project.clone(),
+            FormatTrigger::Manual,
+            FormatTarget::Buffer,
+            cx,
+        )
     });
     format.await.unwrap();
     assert_eq!(

crates/editor/src/element.rs ๐Ÿ”—

@@ -376,6 +376,13 @@ impl EditorElement {
                 cx.propagate();
             }
         });
+        register_action(view, cx, |editor, action, cx| {
+            if let Some(task) = editor.format_selections(action, cx) {
+                task.detach_and_log_err(cx);
+            } else {
+                cx.propagate();
+            }
+        });
         register_action(view, cx, Editor::restart_language_server);
         register_action(view, cx, Editor::cancel_language_server_work);
         register_action(view, cx, Editor::show_character_palette);

crates/editor/src/items.rs ๐Ÿ”—

@@ -27,6 +27,7 @@ use rpc::proto::{self, update_view, PeerId};
 use settings::Settings;
 use workspace::item::{Dedup, ItemSettings, SerializableItem, TabContentParams};
 
+use project::lsp_store::FormatTarget;
 use std::{
     any::TypeId,
     borrow::Cow,
@@ -722,7 +723,12 @@ impl Item for Editor {
         cx.spawn(|this, mut cx| async move {
             if format {
                 this.update(&mut cx, |editor, cx| {
-                    editor.perform_format(project.clone(), FormatTrigger::Save, cx)
+                    editor.perform_format(
+                        project.clone(),
+                        FormatTrigger::Save,
+                        FormatTarget::Buffer,
+                        cx,
+                    )
                 })?
                 .await?;
             }

crates/editor/src/mouse_context_menu.rs ๐Ÿ”—

@@ -1,5 +1,4 @@
-use std::ops::Range;
-
+use crate::actions::FormatSelections;
 use crate::{
     actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
     DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
@@ -8,6 +7,8 @@ use crate::{
 };
 use gpui::prelude::FluentBuilder;
 use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
+use std::ops::Range;
+use text::PointUtf16;
 use workspace::OpenInTerminal;
 
 #[derive(Debug)]
@@ -164,6 +165,12 @@ pub fn deploy_context_menu(
         } else {
             "Reveal in File Manager"
         };
+        let has_selections = editor
+            .selections
+            .all::<PointUtf16>(cx)
+            .into_iter()
+            .any(|s| !s.is_empty());
+
         ui::ContextMenu::build(cx, |menu, _cx| {
             let builder = menu
                 .on_blur_subscription(Subscription::new(|| {}))
@@ -175,6 +182,9 @@ pub fn deploy_context_menu(
                 .separator()
                 .action("Rename Symbol", Box::new(Rename))
                 .action("Format Buffer", Box::new(Format))
+                .when(has_selections, |cx| {
+                    cx.action("Format Selections", Box::new(FormatSelections))
+                })
                 .action(
                     "Code Actions",
                     Box::new(ToggleCodeActions {

crates/project/src/lsp_store.rs ๐Ÿ”—

@@ -72,7 +72,7 @@ use std::{
     sync::Arc,
     time::{Duration, Instant},
 };
-use text::{Anchor, BufferId, LineEnding};
+use text::{Anchor, BufferId, LineEnding, Point, Selection};
 use util::{
     debug_panic, defer, maybe, merge_json_value_into, post_inc, ResultExt, TryFutureExt as _,
 };
@@ -96,6 +96,20 @@ pub enum FormatTrigger {
     Manual,
 }
 
+pub enum FormatTarget {
+    Buffer,
+    Ranges(Vec<Selection<Point>>),
+}
+
+impl FormatTarget {
+    pub fn as_selections(&self) -> Option<&[Selection<Point>]> {
+        match self {
+            FormatTarget::Buffer => None,
+            FormatTarget::Ranges(selections) => Some(selections.as_slice()),
+        }
+    }
+}
+
 // Currently, formatting operations are represented differently depending on
 // whether they come from a language server or an external command.
 #[derive(Debug)]
@@ -161,6 +175,7 @@ impl LocalLspStore {
         mut buffers: Vec<FormattableBuffer>,
         push_to_history: bool,
         trigger: FormatTrigger,
+        target: FormatTarget,
         mut cx: AsyncAppContext,
     ) -> anyhow::Result<ProjectTransaction> {
         // Do not allow multiple concurrent formatting requests for the
@@ -286,6 +301,7 @@ impl LocalLspStore {
                                             if prettier_settings.allowed {
                                                 Self::perform_format(
                                                     &Formatter::Prettier,
+                                                    &target,
                                                     server_and_buffer,
                                                     lsp_store.clone(),
                                                     buffer,
@@ -299,6 +315,7 @@ impl LocalLspStore {
                                             } else {
                                                 Self::perform_format(
                                                     &Formatter::LanguageServer { name: None },
+                                                    &target,
                                                     server_and_buffer,
                                                     lsp_store.clone(),
                                                     buffer,
@@ -310,9 +327,8 @@ impl LocalLspStore {
                                                 )
                                                 .await
                                             }
-                                        }
-                                        .log_err()
-                                        .flatten();
+                                        }?;
+
                                         if let Some(op) = diff {
                                             format_operations.push(op);
                                         }
@@ -321,6 +337,7 @@ impl LocalLspStore {
                                         for formatter in formatters.as_ref() {
                                             let diff = Self::perform_format(
                                                 formatter,
+                                                &target,
                                                 server_and_buffer,
                                                 lsp_store.clone(),
                                                 buffer,
@@ -330,9 +347,7 @@ impl LocalLspStore {
                                                 &mut project_transaction,
                                                 &mut cx,
                                             )
-                                            .await
-                                            .log_err()
-                                            .flatten();
+                                            .await?;
                                             if let Some(op) = diff {
                                                 format_operations.push(op);
                                             }
@@ -346,6 +361,7 @@ impl LocalLspStore {
                                 for formatter in formatters.as_ref() {
                                     let diff = Self::perform_format(
                                         formatter,
+                                        &target,
                                         server_and_buffer,
                                         lsp_store.clone(),
                                         buffer,
@@ -355,9 +371,7 @@ impl LocalLspStore {
                                         &mut project_transaction,
                                         &mut cx,
                                     )
-                                    .await
-                                    .log_err()
-                                    .flatten();
+                                    .await?;
                                     if let Some(op) = diff {
                                         format_operations.push(op);
                                     }
@@ -373,6 +387,7 @@ impl LocalLspStore {
                                     if prettier_settings.allowed {
                                         Self::perform_format(
                                             &Formatter::Prettier,
+                                            &target,
                                             server_and_buffer,
                                             lsp_store.clone(),
                                             buffer,
@@ -384,8 +399,14 @@ impl LocalLspStore {
                                         )
                                         .await
                                     } else {
+                                        let formatter = Formatter::LanguageServer {
+                                            name: primary_language_server
+                                                .as_ref()
+                                                .map(|server| server.name().to_string()),
+                                        };
                                         Self::perform_format(
-                                            &Formatter::LanguageServer { name: None },
+                                            &formatter,
+                                            &target,
                                             server_and_buffer,
                                             lsp_store.clone(),
                                             buffer,
@@ -397,9 +418,7 @@ impl LocalLspStore {
                                         )
                                         .await
                                     }
-                                }
-                                .log_err()
-                                .flatten();
+                                }?;
 
                                 if let Some(op) = diff {
                                     format_operations.push(op)
@@ -410,6 +429,7 @@ impl LocalLspStore {
                                     // format with formatter
                                     let diff = Self::perform_format(
                                         formatter,
+                                        &target,
                                         server_and_buffer,
                                         lsp_store.clone(),
                                         buffer,
@@ -419,9 +439,7 @@ impl LocalLspStore {
                                         &mut project_transaction,
                                         &mut cx,
                                     )
-                                    .await
-                                    .log_err()
-                                    .flatten();
+                                    .await?;
                                     if let Some(op) = diff {
                                         format_operations.push(op);
                                     }
@@ -483,6 +501,7 @@ impl LocalLspStore {
     #[allow(clippy::too_many_arguments)]
     async fn perform_format(
         formatter: &Formatter,
+        format_target: &FormatTarget,
         primary_server_and_buffer: Option<(&Arc<LanguageServer>, &PathBuf)>,
         lsp_store: WeakModel<LspStore>,
         buffer: &FormattableBuffer,
@@ -506,18 +525,33 @@ impl LocalLspStore {
                         language_server
                     };
 
-                    Some(FormatOperation::Lsp(
-                        LspStore::format_via_lsp(
-                            &lsp_store,
-                            &buffer.handle,
-                            buffer_abs_path,
-                            language_server,
-                            settings,
-                            cx,
-                        )
-                        .await
-                        .context("failed to format via language server")?,
-                    ))
+                    match format_target {
+                        FormatTarget::Buffer => Some(FormatOperation::Lsp(
+                            LspStore::format_via_lsp(
+                                &lsp_store,
+                                &buffer.handle,
+                                buffer_abs_path,
+                                language_server,
+                                settings,
+                                cx,
+                            )
+                            .await
+                            .context("failed to format via language server")?,
+                        )),
+                        FormatTarget::Ranges(selections) => Some(FormatOperation::Lsp(
+                            LspStore::format_range_via_lsp(
+                                &lsp_store,
+                                &buffer.handle,
+                                selections.as_slice(),
+                                buffer_abs_path,
+                                language_server,
+                                settings,
+                                cx,
+                            )
+                            .await
+                            .context("failed to format ranges via language server")?,
+                        )),
+                    }
                 } else {
                     None
                 }
@@ -1859,10 +1893,9 @@ impl LspStore {
         } else if matches!(range_formatting_provider, Some(p) if *p != OneOf::Left(false)) {
             let buffer_start = lsp::Position::new(0, 0);
             let buffer_end = buffer.update(cx, |b, _| point_to_lsp(b.max_point_utf16()))?;
-
             language_server
                 .request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
-                    text_document,
+                    text_document: text_document.clone(),
                     range: lsp::Range::new(buffer_start, buffer_end),
                     options: lsp_command::lsp_formatting_options(settings),
                     work_done_progress_params: Default::default(),
@@ -1878,7 +1911,62 @@ impl LspStore {
             })?
             .await
         } else {
-            Ok(Vec::new())
+            Ok(Vec::with_capacity(0))
+        }
+    }
+    pub async fn format_range_via_lsp(
+        this: &WeakModel<Self>,
+        buffer: &Model<Buffer>,
+        selections: &[Selection<Point>],
+        abs_path: &Path,
+        language_server: &Arc<LanguageServer>,
+        settings: &LanguageSettings,
+        cx: &mut AsyncAppContext,
+    ) -> Result<Vec<(Range<Anchor>, String)>> {
+        let capabilities = &language_server.capabilities();
+        let range_formatting_provider = capabilities.document_range_formatting_provider.as_ref();
+        if range_formatting_provider.map_or(false, |provider| provider == &OneOf::Left(false)) {
+            return Err(anyhow!(
+                "{} language server does not support range formatting",
+                language_server.name()
+            ));
+        }
+
+        let uri = lsp::Url::from_file_path(abs_path)
+            .map_err(|_| anyhow!("failed to convert abs path to uri"))?;
+        let text_document = lsp::TextDocumentIdentifier::new(uri);
+
+        let lsp_edits = {
+            let ranges = selections.into_iter().map(|s| {
+                let start = lsp::Position::new(s.start.row, s.start.column);
+                let end = lsp::Position::new(s.end.row, s.end.column);
+                lsp::Range::new(start, end)
+            });
+
+            let mut edits = None;
+            for range in ranges {
+                if let Some(mut edit) = language_server
+                    .request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
+                        text_document: text_document.clone(),
+                        range,
+                        options: lsp_command::lsp_formatting_options(settings),
+                        work_done_progress_params: Default::default(),
+                    })
+                    .await?
+                {
+                    edits.get_or_insert_with(Vec::new).append(&mut edit);
+                }
+            }
+            edits
+        };
+
+        if let Some(lsp_edits) = lsp_edits {
+            this.update(cx, |this, cx| {
+                this.edits_from_lsp(buffer, lsp_edits, language_server.server_id(), None, cx)
+            })?
+            .await
+        } else {
+            Ok(Vec::with_capacity(0))
         }
     }
 
@@ -2648,44 +2736,44 @@ impl LspStore {
                     };
 
                 requests.push(
-                    server
-                        .request::<lsp::request::WorkspaceSymbolRequest>(
-                            lsp::WorkspaceSymbolParams {
-                                query: query.to_string(),
-                                ..Default::default()
-                            },
-                        )
-                        .log_err()
-                        .map(move |response| {
-                            let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
-                                lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
-                                    flat_responses.into_iter().map(|lsp_symbol| {
-                                        (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
-                                    }).collect::<Vec<_>>()
-                                }
-                                lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
-                                    nested_responses.into_iter().filter_map(|lsp_symbol| {
-                                        let location = match lsp_symbol.location {
-                                            OneOf::Left(location) => location,
-                                            OneOf::Right(_) => {
-                                                log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
-                                                return None
-                                            }
-                                        };
-                                        Some((lsp_symbol.name, lsp_symbol.kind, location))
-                                    }).collect::<Vec<_>>()
+                        server
+                            .request::<lsp::request::WorkspaceSymbolRequest>(
+                                lsp::WorkspaceSymbolParams {
+                                    query: query.to_string(),
+                                    ..Default::default()
+                                },
+                            )
+                            .log_err()
+                            .map(move |response| {
+                                let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
+                                    lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
+                                        flat_responses.into_iter().map(|lsp_symbol| {
+                                            (lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
+                                        }).collect::<Vec<_>>()
+                                    }
+                                    lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
+                                        nested_responses.into_iter().filter_map(|lsp_symbol| {
+                                            let location = match lsp_symbol.location {
+                                                OneOf::Left(location) => location,
+                                                OneOf::Right(_) => {
+                                                    log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
+                                                    return None
+                                                }
+                                            };
+                                            Some((lsp_symbol.name, lsp_symbol.kind, location))
+                                        }).collect::<Vec<_>>()
+                                    }
+                                }).unwrap_or_default();
+
+                                WorkspaceSymbolsResult {
+                                    lsp_adapter,
+                                    language,
+                                    worktree: worktree_handle.downgrade(),
+                                    worktree_abs_path,
+                                    lsp_symbols,
                                 }
-                            }).unwrap_or_default();
-
-                            WorkspaceSymbolsResult {
-                                lsp_adapter,
-                                language,
-                                worktree: worktree_handle.downgrade(),
-                                worktree_abs_path,
-                                lsp_symbols,
-                            }
-                        }),
-                );
+                            }),
+                    );
             }
 
             cx.spawn(move |this, mut cx| async move {
@@ -4579,16 +4667,16 @@ impl LspStore {
 
             if registrations.remove(registration_id).is_some() {
                 log::info!(
-                "language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
-                language_server_id,
-                registration_id
-            );
+                    "language server {}: unregistered workspace/DidChangeWatchedFiles capability with id {}",
+                    language_server_id,
+                    registration_id
+                );
             } else {
                 log::warn!(
-                "language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
-                language_server_id,
-                registration_id
-            );
+                    "language server {}: failed to unregister workspace/DidChangeWatchedFiles capability with id {}. not registered.",
+                    language_server_id,
+                    registration_id
+                );
             }
 
             self.rebuild_watched_paths(language_server_id, cx);
@@ -5078,6 +5166,7 @@ impl LspStore {
         buffers: HashSet<Model<Buffer>>,
         push_to_history: bool,
         trigger: FormatTrigger,
+        target: FormatTarget,
         cx: &mut ModelContext<Self>,
     ) -> Task<anyhow::Result<ProjectTransaction>> {
         if let Some(_) = self.as_local() {
@@ -5114,6 +5203,7 @@ impl LspStore {
                     formattable_buffers,
                     push_to_history,
                     trigger,
+                    target,
                     cx.clone(),
                 )
                 .await;
@@ -5172,7 +5262,7 @@ impl LspStore {
                 buffers.insert(this.buffer_store.read(cx).get_existing(buffer_id)?);
             }
             let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
-            Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
+            Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, FormatTarget::Buffer, cx))
         })??;
 
         let project_transaction = format.await?;
@@ -6485,11 +6575,11 @@ impl LspStore {
                 })?;
 
             let found_snapshot = snapshots
-                .binary_search_by_key(&version, |e| e.version)
-                .map(|ix| snapshots[ix].snapshot.clone())
-                .map_err(|_| {
-                    anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
-                })?;
+                    .binary_search_by_key(&version, |e| e.version)
+                    .map(|ix| snapshots[ix].snapshot.clone())
+                    .map_err(|_| {
+                        anyhow!("snapshot not found for buffer {buffer_id} server {server_id} at version {version}")
+                    })?;
 
             snapshots.retain(|snapshot| snapshot.version + OLD_VERSIONS_TO_RETAIN >= version);
             Ok(found_snapshot)
@@ -7203,74 +7293,74 @@ impl LanguageServerWatchedPathsBuilder {
         let project = cx.weak_model();
 
         cx.new_model(|cx| {
-            let this_id = cx.entity_id();
-            const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
-            let abs_paths = self
-                .abs_paths
-                .into_iter()
-                .map(|(abs_path, globset)| {
-                    let task = cx.spawn({
-                        let abs_path = abs_path.clone();
-                        let fs = fs.clone();
-
-                        let lsp_store = project.clone();
-                        |_, mut cx| async move {
-                            maybe!(async move {
-                                let mut push_updates =
-                                    fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
-                                while let Some(update) = push_updates.0.next().await {
-                                    let action = lsp_store
-                                        .update(&mut cx, |this, cx| {
-                                            let Some(local) = this.as_local() else {
-                                                return ControlFlow::Break(());
-                                            };
-                                            let Some(watcher) = local
-                                                .language_server_watched_paths
-                                                .get(&language_server_id)
-                                            else {
-                                                return ControlFlow::Break(());
-                                            };
-                                            if watcher.entity_id() != this_id {
-                                                // This watcher is no longer registered on the project, which means that we should
-                                                // cease operations.
-                                                return ControlFlow::Break(());
-                                            }
-                                            let (globs, _) = watcher
-                                                .read(cx)
-                                                .abs_paths
-                                                .get(&abs_path)
-                                                .expect(
-                                                "Watched abs path is not registered with a watcher",
-                                            );
-                                            let matching_entries = update
-                                                .into_iter()
-                                                .filter(|event| globs.is_match(&event.path))
-                                                .collect::<Vec<_>>();
-                                            this.lsp_notify_abs_paths_changed(
-                                                language_server_id,
-                                                matching_entries,
-                                            );
-                                            ControlFlow::Continue(())
-                                        })
-                                        .ok()?;
+                let this_id = cx.entity_id();
+                const LSP_ABS_PATH_OBSERVE: Duration = Duration::from_millis(100);
+                let abs_paths = self
+                    .abs_paths
+                    .into_iter()
+                    .map(|(abs_path, globset)| {
+                        let task = cx.spawn({
+                            let abs_path = abs_path.clone();
+                            let fs = fs.clone();
+
+                            let lsp_store = project.clone();
+                            |_, mut cx| async move {
+                                maybe!(async move {
+                                    let mut push_updates =
+                                        fs.watch(&abs_path, LSP_ABS_PATH_OBSERVE).await;
+                                    while let Some(update) = push_updates.0.next().await {
+                                        let action = lsp_store
+                                            .update(&mut cx, |this, cx| {
+                                                let Some(local) = this.as_local() else {
+                                                    return ControlFlow::Break(());
+                                                };
+                                                let Some(watcher) = local
+                                                    .language_server_watched_paths
+                                                    .get(&language_server_id)
+                                                else {
+                                                    return ControlFlow::Break(());
+                                                };
+                                                if watcher.entity_id() != this_id {
+                                                    // This watcher is no longer registered on the project, which means that we should
+                                                    // cease operations.
+                                                    return ControlFlow::Break(());
+                                                }
+                                                let (globs, _) = watcher
+                                                    .read(cx)
+                                                    .abs_paths
+                                                    .get(&abs_path)
+                                                    .expect(
+                                                    "Watched abs path is not registered with a watcher",
+                                                );
+                                                let matching_entries = update
+                                                    .into_iter()
+                                                    .filter(|event| globs.is_match(&event.path))
+                                                    .collect::<Vec<_>>();
+                                                this.lsp_notify_abs_paths_changed(
+                                                    language_server_id,
+                                                    matching_entries,
+                                                );
+                                                ControlFlow::Continue(())
+                                            })
+                                            .ok()?;
 
-                                    if action.is_break() {
-                                        break;
+                                        if action.is_break() {
+                                            break;
+                                        }
                                     }
-                                }
-                                Some(())
-                            })
-                            .await;
-                        }
-                    });
-                    (abs_path, (globset, task))
-                })
-                .collect();
-            LanguageServerWatchedPaths {
-                worktree_paths: self.worktree_paths,
-                abs_paths,
-            }
-        })
+                                    Some(())
+                                })
+                                    .await;
+                            }
+                        });
+                        (abs_path, (globset, task))
+                    })
+                    .collect();
+                LanguageServerWatchedPaths {
+                    worktree_paths: self.worktree_paths,
+                    abs_paths,
+                }
+            })
     }
 }
 

crates/project/src/project.rs ๐Ÿ”—

@@ -2505,10 +2505,11 @@ impl Project {
         buffers: HashSet<Model<Buffer>>,
         push_to_history: bool,
         trigger: lsp_store::FormatTrigger,
+        target: lsp_store::FormatTarget,
         cx: &mut ModelContext<Project>,
     ) -> Task<anyhow::Result<ProjectTransaction>> {
         self.lsp_store.update(cx, |lsp_store, cx| {
-            lsp_store.format(buffers, push_to_history, trigger, cx)
+            lsp_store.format(buffers, push_to_history, trigger, target, cx)
         })
     }