diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index cc8c35fbc3..e124fd6a7e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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, ) }) diff --git a/crates/editor/src/actions.rs b/crates/editor/src/actions.rs index b0c7877979..4955f00c38 100644 --- a/crates/editor/src/actions.rs +++ b/crates/editor/src/actions.rs @@ -237,6 +237,7 @@ gpui::actions!( ToggleFold, ToggleFoldRecursive, Format, + FormatSelections, GoToDeclaration, GoToDeclarationSplit, GoToDefinition, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d07002040c..5e33b38dac 100644 --- a/crates/editor/src/editor.rs +++ b/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, + ) -> Option>> { + 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, trigger: FormatTrigger, + target: FormatTarget, cx: &mut ViewContext, ) -> Task> { 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! { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 092afb394e..fdcfaab82f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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!( diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49bb9d7b92..a9dfe7e435 100644 --- a/crates/editor/src/element.rs +++ b/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); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b3f4cc813f..e84476fb08 100644 --- a/crates/editor/src/items.rs +++ b/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?; } diff --git a/crates/editor/src/mouse_context_menu.rs b/crates/editor/src/mouse_context_menu.rs index 936d95dccb..9abf4d990c 100644 --- a/crates/editor/src/mouse_context_menu.rs +++ b/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::(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 { diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index 8c4fb1f349..41da302e46 100644 --- a/crates/project/src/lsp_store.rs +++ b/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>), +} + +impl FormatTarget { + pub fn as_selections(&self) -> Option<&[Selection]> { + 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, push_to_history: bool, trigger: FormatTrigger, + target: FormatTarget, mut cx: AsyncAppContext, ) -> anyhow::Result { // 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, &PathBuf)>, lsp_store: WeakModel, 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::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, + buffer: &Model, + selections: &[Selection], + abs_path: &Path, + language_server: &Arc, + settings: &LanguageSettings, + cx: &mut AsyncAppContext, + ) -> Result, 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::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::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::>() - } - 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::>() - } - }).unwrap_or_default(); + server + .request::( + 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::>() + } + 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::>() + } + }).unwrap_or_default(); - WorkspaceSymbolsResult { - lsp_adapter, - language, - worktree: worktree_handle.downgrade(), - worktree_abs_path, - lsp_symbols, - } - }), - ); + 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>, push_to_history: bool, trigger: FormatTrigger, + target: FormatTarget, cx: &mut ModelContext, ) -> Task> { 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 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::>(); - this.lsp_notify_abs_paths_changed( - language_server_id, - matching_entries, - ); - ControlFlow::Continue(()) - }) - .ok()?; + 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::>(); + 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, + } + }) } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6e731e9cba..d26ee3a211 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2505,10 +2505,11 @@ impl Project { buffers: HashSet>, push_to_history: bool, trigger: lsp_store::FormatTrigger, + target: lsp_store::FormatTarget, cx: &mut ModelContext, ) -> Task> { 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) }) }