From e4080ef565f71c15a4cd89f6b0d565b82ec53f7d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 23 Sep 2024 14:33:28 -0600 Subject: [PATCH] Move formatting to LSP store (#18242) Release Notes: - ssh-remoting: Fixed format on save --------- Co-authored-by: Mikayla --- .../src/activity_indicator.rs | 2 +- crates/collab/src/tests/integration_tests.rs | 4 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/items.rs | 4 +- crates/project/src/lsp_store.rs | 649 +++++++++++++++++- crates/project/src/prettier_store.rs | 8 +- crates/project/src/project.rs | 639 +---------------- crates/project/src/project_tests.rs | 2 +- 8 files changed, 655 insertions(+), 657 deletions(-) diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index a9ae7d075d10c..fee0ef73f7bee 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -280,7 +280,7 @@ impl ActivityIndicator { } // Show any formatting failure - if let Some(failure) = self.project.read(cx).last_formatting_failure() { + if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) { return Some(Content { icon: Some( Icon::new(IconName::Warning) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 51593e081e46c..d5cef3589cce3 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -28,8 +28,8 @@ use live_kit_client::MacOSDisplay; use lsp::LanguageServerId; use parking_lot::Mutex; use project::{ - search::SearchQuery, search::SearchResult, DiagnosticSummary, FormatTrigger, HoverBlockKind, - Project, ProjectPath, + lsp_store::FormatTrigger, search::SearchQuery, search::SearchResult, DiagnosticSummary, + HoverBlockKind, Project, ProjectPath, }; use rand::prelude::*; use serde_json::json; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbc272d995213..dc536471023f0 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -122,8 +122,8 @@ use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::project_settings::{GitGutterSetting, ProjectSettings}; use project::{ - CodeAction, Completion, CompletionIntent, FormatTrigger, Item, Location, Project, ProjectPath, - ProjectTransaction, TaskSourceKind, + lsp_store::FormatTrigger, CodeAction, Completion, CompletionIntent, Item, Location, Project, + ProjectPath, ProjectTransaction, TaskSourceKind, }; use rand::prelude::*; use rpc::{proto::*, ErrorExt}; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1d301f2ee68cd..b3f4cc813fe8a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -20,8 +20,8 @@ use language::{ }; use multi_buffer::AnchorRangeExt; use project::{ - project_settings::ProjectSettings, search::SearchQuery, FormatTrigger, Item as _, Project, - ProjectPath, + lsp_store::FormatTrigger, project_settings::ProjectSettings, search::SearchQuery, Item as _, + Project, ProjectPath, }; use rpc::proto::{self, update_view, PeerId}; use settings::Settings; diff --git a/crates/project/src/lsp_store.rs b/crates/project/src/lsp_store.rs index b2920bc791c47..6673f9da1ddd7 100644 --- a/crates/project/src/lsp_store.rs +++ b/crates/project/src/lsp_store.rs @@ -1,5 +1,6 @@ use crate::{ buffer_store::{BufferStore, BufferStoreEvent}, + deserialize_code_actions, environment::ProjectEnvironment, lsp_command::{self, *}, lsp_ext_command, @@ -19,7 +20,7 @@ use futures::{ future::{join_all, BoxFuture, Shared}, select, stream::FuturesUnordered, - Future, FutureExt, StreamExt, + AsyncWriteExt, Future, FutureExt, StreamExt, }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ @@ -29,12 +30,13 @@ use gpui::{ use http_client::{AsyncBody, HttpClient, Request, Response, Uri}; use language::{ language_settings::{ - all_language_settings, language_settings, AllLanguageSettings, LanguageSettings, + all_language_settings, language_settings, AllLanguageSettings, FormatOnSave, Formatter, + LanguageSettings, SelectedFormatter, }, markdown, point_to_lsp, prepare_completion_documentation, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeLabel, Diagnostic, - DiagnosticEntry, DiagnosticSet, Documentation, File as _, Language, LanguageConfig, + DiagnosticEntry, DiagnosticSet, Diff, Documentation, File as _, Language, LanguageConfig, LanguageMatcher, LanguageName, LanguageRegistry, LanguageServerName, LocalFile, LspAdapter, LspAdapterDelegate, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, ToPointUtf16, Transaction, Unclipped, @@ -90,12 +92,38 @@ const SERVER_REINSTALL_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); const SERVER_LAUNCHING_BEFORE_SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5); pub const SERVER_PROGRESS_THROTTLE_TIMEOUT: Duration = Duration::from_millis(100); +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FormatTrigger { + Save, + Manual, +} + +// Currently, formatting operations are represented differently depending on +// whether they come from a language server or an external command. +#[derive(Debug)] +pub enum FormatOperation { + Lsp(Vec<(Range, String)>), + External(Diff), + Prettier(Diff), +} + +impl FormatTrigger { + fn from_proto(value: i32) -> FormatTrigger { + match value { + 0 => FormatTrigger::Save, + 1 => FormatTrigger::Manual, + _ => FormatTrigger::Save, + } + } +} + pub struct LocalLspStore { http_client: Option>, environment: Model, fs: Arc, yarn: Model, pub language_servers: HashMap, + buffers_being_formatted: HashSet, last_workspace_edits_by_language_server: HashMap, language_server_watched_paths: HashMap>, language_server_watcher_registrations: @@ -104,6 +132,7 @@ pub struct LocalLspStore { HashMap)>, prettier_store: Model, current_lsp_settings: HashMap, + last_formatting_failure: Option, _subscription: gpui::Subscription, } @@ -128,6 +157,485 @@ impl LocalLspStore { futures::future::join_all(shutdown_futures).await; } } + async fn format_locally( + lsp_store: WeakModel, + mut buffers_with_paths: Vec<(Model, Option)>, + push_to_history: bool, + trigger: FormatTrigger, + mut cx: AsyncAppContext, + ) -> anyhow::Result { + // Do not allow multiple concurrent formatting requests for the + // same buffer. + lsp_store.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + buffers_with_paths.retain(|(buffer, _)| { + this.buffers_being_formatted + .insert(buffer.read(cx).remote_id()) + }); + })?; + + let _cleanup = defer({ + let this = lsp_store.clone(); + let mut cx = cx.clone(); + let buffers = &buffers_with_paths; + move || { + this.update(&mut cx, |this, cx| { + let this = this.as_local_mut().unwrap(); + for (buffer, _) in buffers { + this.buffers_being_formatted + .remove(&buffer.read(cx).remote_id()); + } + }) + .ok(); + } + }); + + let mut project_transaction = ProjectTransaction::default(); + for (buffer, buffer_abs_path) in &buffers_with_paths { + let (primary_adapter_and_server, adapters_and_servers) = + lsp_store.update(&mut cx, |lsp_store, cx| { + let buffer = buffer.read(cx); + + let adapters_and_servers = lsp_store + .language_servers_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) + .collect::>(); + + let primary_adapter = lsp_store + .primary_language_server_for_buffer(buffer, cx) + .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())); + + (primary_adapter, adapters_and_servers) + })?; + + let settings = buffer.update(&mut cx, |buffer, cx| { + language_settings(buffer.language(), buffer.file(), cx).clone() + })?; + + let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; + let ensure_final_newline = settings.ensure_final_newline_on_save; + + // First, format buffer's whitespace according to the settings. + let trailing_whitespace_diff = if remove_trailing_whitespace { + Some( + buffer + .update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))? + .await, + ) + } else { + None + }; + let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| { + buffer.finalize_last_transaction(); + buffer.start_transaction(); + if let Some(diff) = trailing_whitespace_diff { + buffer.apply_diff(diff, cx); + } + if ensure_final_newline { + buffer.ensure_final_newline(cx); + } + buffer.end_transaction(cx) + })?; + + // Apply the `code_actions_on_format` before we run the formatter. + let code_actions = deserialize_code_actions(&settings.code_actions_on_format); + #[allow(clippy::nonminimal_bool)] + if !code_actions.is_empty() + && !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off) + { + LspStore::execute_code_actions_on_servers( + &lsp_store, + &adapters_and_servers, + code_actions, + buffer, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await?; + } + + // Apply language-specific formatting using either the primary language server + // or external command. + // Except for code actions, which are applied with all connected language servers. + let primary_language_server = + primary_adapter_and_server.map(|(_adapter, server)| server.clone()); + let server_and_buffer = primary_language_server + .as_ref() + .zip(buffer_abs_path.as_ref()); + + let prettier_settings = buffer.read_with(&cx, |buffer, cx| { + language_settings(buffer.language(), buffer.file(), cx) + .prettier + .clone() + })?; + + let mut format_operations: Vec = vec![]; + { + match trigger { + FormatTrigger::Save => { + match &settings.format_on_save { + FormatOnSave::Off => { + // nothing + } + FormatOnSave::On => { + match &settings.formatter { + SelectedFormatter::Auto => { + // do the auto-format: prefer prettier, fallback to primary language server + let diff = { + if prettier_settings.allowed { + Self::perform_format( + &Formatter::Prettier, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + } else { + Self::perform_format( + &Formatter::LanguageServer { name: None }, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + } + } + .log_err() + .flatten(); + if let Some(op) = diff { + format_operations.push(op); + } + } + SelectedFormatter::List(formatters) => { + for formatter in formatters.as_ref() { + let diff = Self::perform_format( + formatter, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + .log_err() + .flatten(); + if let Some(op) = diff { + format_operations.push(op); + } + + // format with formatter + } + } + } + } + FormatOnSave::List(formatters) => { + for formatter in formatters.as_ref() { + let diff = Self::perform_format( + formatter, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + .log_err() + .flatten(); + if let Some(op) = diff { + format_operations.push(op); + } + } + } + } + } + FormatTrigger::Manual => { + match &settings.formatter { + SelectedFormatter::Auto => { + // do the auto-format: prefer prettier, fallback to primary language server + let diff = { + if prettier_settings.allowed { + Self::perform_format( + &Formatter::Prettier, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + } else { + Self::perform_format( + &Formatter::LanguageServer { name: None }, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + } + } + .log_err() + .flatten(); + + if let Some(op) = diff { + format_operations.push(op) + } + } + SelectedFormatter::List(formatters) => { + for formatter in formatters.as_ref() { + // format with formatter + let diff = Self::perform_format( + formatter, + server_and_buffer, + lsp_store.clone(), + buffer, + buffer_abs_path, + &settings, + &adapters_and_servers, + push_to_history, + &mut project_transaction, + &mut cx, + ) + .await + .log_err() + .flatten(); + if let Some(op) = diff { + format_operations.push(op); + } + } + } + } + } + } + } + + buffer.update(&mut cx, |b, cx| { + // If the buffer had its whitespace formatted and was edited while the language-specific + // formatting was being computed, avoid applying the language-specific formatting, because + // it can't be grouped with the whitespace formatting in the undo history. + if let Some(transaction_id) = whitespace_transaction_id { + if b.peek_undo_stack() + .map_or(true, |e| e.transaction_id() != transaction_id) + { + format_operations.clear(); + } + } + + // Apply any language-specific formatting, and group the two formatting operations + // in the buffer's undo history. + for operation in format_operations { + match operation { + FormatOperation::Lsp(edits) => { + b.edit(edits, None, cx); + } + FormatOperation::External(diff) => { + b.apply_diff(diff, cx); + } + FormatOperation::Prettier(diff) => { + b.apply_diff(diff, cx); + } + } + + if let Some(transaction_id) = whitespace_transaction_id { + b.group_until_transaction(transaction_id); + } else if let Some(transaction) = project_transaction.0.get(buffer) { + b.group_until_transaction(transaction.id) + } + } + + if let Some(transaction) = b.finalize_last_transaction().cloned() { + if !push_to_history { + b.forget_transaction(transaction.id); + } + project_transaction.0.insert(buffer.clone(), transaction); + } + })?; + } + + Ok(project_transaction) + } + + #[allow(clippy::too_many_arguments)] + async fn perform_format( + formatter: &Formatter, + primary_server_and_buffer: Option<(&Arc, &PathBuf)>, + lsp_store: WeakModel, + buffer: &Model, + buffer_abs_path: &Option, + settings: &LanguageSettings, + adapters_and_servers: &[(Arc, Arc)], + push_to_history: bool, + transaction: &mut ProjectTransaction, + cx: &mut AsyncAppContext, + ) -> Result, anyhow::Error> { + let result = match formatter { + Formatter::LanguageServer { name } => { + if let Some((language_server, buffer_abs_path)) = primary_server_and_buffer { + let language_server = if let Some(name) = name { + adapters_and_servers + .iter() + .find_map(|(adapter, server)| { + adapter.name.0.as_ref().eq(name.as_str()).then_some(server) + }) + .unwrap_or(language_server) + } else { + language_server + }; + + Some(FormatOperation::Lsp( + LspStore::format_via_lsp( + &lsp_store, + buffer, + buffer_abs_path, + language_server, + settings, + cx, + ) + .await + .context("failed to format via language server")?, + )) + } else { + None + } + } + Formatter::Prettier => { + let prettier = lsp_store.update(cx, |lsp_store, _cx| { + lsp_store.prettier_store().unwrap().downgrade() + })?; + prettier_store::format_with_prettier(&prettier, buffer, cx) + .await + .transpose() + .ok() + .flatten() + } + Formatter::External { command, arguments } => { + let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path()); + Self::format_via_external_command(buffer, buffer_abs_path, command, arguments, cx) + .await + .context(format!( + "failed to format via external command {:?}", + command + ))? + .map(FormatOperation::External) + } + Formatter::CodeActions(code_actions) => { + let code_actions = deserialize_code_actions(code_actions); + if !code_actions.is_empty() { + LspStore::execute_code_actions_on_servers( + &lsp_store, + adapters_and_servers, + code_actions, + buffer, + push_to_history, + transaction, + cx, + ) + .await?; + } + None + } + }; + anyhow::Ok(result) + } + + async fn format_via_external_command( + buffer: &Model, + buffer_abs_path: Option<&Path>, + command: &str, + arguments: &[String], + cx: &mut AsyncAppContext, + ) -> Result> { + let working_dir_path = buffer.update(cx, |buffer, cx| { + let file = File::from_dyn(buffer.file())?; + let worktree = file.worktree.read(cx); + let mut worktree_path = worktree.abs_path().to_path_buf(); + if worktree.root_entry()?.is_file() { + worktree_path.pop(); + } + Some(worktree_path) + })?; + + let mut child = smol::process::Command::new(command); + #[cfg(target_os = "windows")] + { + use smol::process::windows::CommandExt; + child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); + } + + if let Some(working_dir_path) = working_dir_path { + child.current_dir(working_dir_path); + } + + let mut child = child + .args(arguments.iter().map(|arg| { + if let Some(buffer_abs_path) = buffer_abs_path { + arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) + } else { + arg.replace("{buffer_path}", "Untitled") + } + })) + .stdin(smol::process::Stdio::piped()) + .stdout(smol::process::Stdio::piped()) + .stderr(smol::process::Stdio::piped()) + .spawn()?; + + let stdin = child + .stdin + .as_mut() + .ok_or_else(|| anyhow!("failed to acquire stdin"))?; + let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?; + for chunk in text.chunks() { + stdin.write_all(chunk.as_bytes()).await?; + } + stdin.flush().await?; + + let output = child.output().await?; + if !output.status.success() { + return Err(anyhow!( + "command failed with exit code {:?}:\nstdout: {}\nstderr: {}", + output.status.code(), + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + )); + } + + let stdout = String::from_utf8(output.stdout)?; + Ok(Some( + buffer + .update(cx, |buffer, cx| buffer.diff(stdout, cx))? + .await, + )) + } } pub struct RemoteLspStore { @@ -221,8 +729,6 @@ pub enum LspStoreEvent { edits: Vec<(lsp::Range, Snippet)>, most_recent_edit: clock::Lamport, }, - StartFormattingLocalBuffer(BufferId), - FinishFormattingLocalBuffer(BufferId), } #[derive(Clone, Debug, Serialize)] @@ -251,6 +757,7 @@ impl LspStore { client.add_model_message_handler(Self::handle_start_language_server); client.add_model_message_handler(Self::handle_update_language_server); client.add_model_message_handler(Self::handle_update_diagnostic_summary); + client.add_model_request_handler(Self::handle_format_buffers); client.add_model_request_handler(Self::handle_resolve_completion_documentation); client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_inlay_hints); @@ -366,6 +873,8 @@ impl LspStore { language_server_watched_paths: Default::default(), language_server_watcher_registrations: Default::default(), current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(), + buffers_being_formatted: Default::default(), + last_formatting_failure: None, prettier_store, environment, http_client, @@ -387,6 +896,7 @@ impl LspStore { diagnostic_summaries: Default::default(), diagnostics: Default::default(), active_entry: None, + _maintain_workspace_config: Self::maintain_workspace_config(cx), _maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx), } @@ -1276,7 +1786,7 @@ impl LspStore { } fn apply_on_type_formatting( - &self, + &mut self, buffer: Model, position: Anchor, trigger: String, @@ -1298,25 +1808,18 @@ impl LspStore { .map(language::proto::deserialize_transaction) .transpose() }) - } else { + } else if let Some(local) = self.as_local_mut() { + let buffer_id = buffer.read(cx).remote_id(); + local.buffers_being_formatted.insert(buffer_id); cx.spawn(move |this, mut cx| async move { - // Do not allow multiple concurrent formatting requests for the - // same buffer. - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::StartFormattingLocalBuffer( - buffer.read(cx).remote_id(), - )); - })?; - let _cleanup = defer({ let this = this.clone(); let mut cx = cx.clone(); - let closure_buffer = buffer.clone(); move || { - this.update(&mut cx, |_, cx| { - cx.emit(LspStoreEvent::FinishFormattingLocalBuffer( - closure_buffer.read(cx).remote_id(), - )) + this.update(&mut cx, |this, _| { + if let Some(local) = this.as_local_mut() { + local.buffers_being_formatted.remove(&buffer_id); + } }) .ok(); } @@ -1333,6 +1836,8 @@ impl LspStore { })? .await }) + } else { + Task::ready(Err(anyhow!("No upstream client or local language server"))) } } @@ -4708,6 +5213,110 @@ impl LspStore { .map(language::proto::serialize_transaction), }) } + pub fn last_formatting_failure(&self) -> Option<&str> { + self.as_local() + .and_then(|local| local.last_formatting_failure.as_deref()) + } + + pub fn format( + &mut self, + buffers: HashSet>, + push_to_history: bool, + trigger: FormatTrigger, + cx: &mut ModelContext, + ) -> Task> { + if let Some(_) = self.as_local() { + let buffers_with_paths = buffers + .into_iter() + .map(|buffer_handle| { + let buffer = buffer_handle.read(cx); + let buffer_abs_path = File::from_dyn(buffer.file()) + .and_then(|file| file.as_local().map(|f| f.abs_path(cx))); + (buffer_handle, buffer_abs_path) + }) + .collect::>(); + + cx.spawn(move |lsp_store, mut cx| async move { + let result = LocalLspStore::format_locally( + lsp_store.clone(), + buffers_with_paths, + push_to_history, + trigger, + cx.clone(), + ) + .await; + + lsp_store.update(&mut cx, |lsp_store, _| { + let local = lsp_store.as_local_mut().unwrap(); + match &result { + Ok(_) => local.last_formatting_failure = None, + Err(error) => { + local.last_formatting_failure.replace(error.to_string()); + } + } + })?; + + result + }) + } else if let Some((client, project_id)) = self.upstream_client() { + cx.spawn(move |this, mut cx| async move { + let response = client + .request(proto::FormatBuffers { + project_id, + trigger: trigger as i32, + buffer_ids: buffers + .iter() + .map(|buffer| { + buffer.update(&mut cx, |buffer, _| buffer.remote_id().into()) + }) + .collect::>()?, + }) + .await? + .transaction + .ok_or_else(|| anyhow!("missing transaction"))?; + BufferStore::deserialize_project_transaction( + this.read_with(&cx, |this, _| this.buffer_store.downgrade())?, + response, + push_to_history, + cx, + ) + .await + }) + } else { + Task::ready(Ok(ProjectTransaction::default())) + } + } + + async fn handle_format_buffers( + this: Model, + envelope: TypedEnvelope, + mut cx: AsyncAppContext, + ) -> Result { + let sender_id = envelope.original_sender_id().unwrap_or_default(); + let format = this.update(&mut cx, |this, cx| { + let mut buffers = HashSet::default(); + for buffer_id in &envelope.payload.buffer_ids { + let buffer_id = BufferId::new(*buffer_id)?; + 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)) + })??; + + let project_transaction = format.await?; + let project_transaction = this.update(&mut cx, |this, cx| { + this.buffer_store.update(cx, |buffer_store, cx| { + buffer_store.serialize_project_transaction_for_peer( + project_transaction, + sender_id, + cx, + ) + }) + })?; + Ok(proto::FormatBuffersResponse { + transaction: Some(project_transaction), + }) + } fn language_settings<'a>( &'a self, diff --git a/crates/project/src/prettier_store.rs b/crates/project/src/prettier_store.rs index 75d70c1d3f72f..82bd8464b2e53 100644 --- a/crates/project/src/prettier_store.rs +++ b/crates/project/src/prettier_store.rs @@ -25,8 +25,8 @@ use smol::stream::StreamExt; use util::{ResultExt, TryFutureExt}; use crate::{ - worktree_store::WorktreeStore, File, FormatOperation, PathChange, ProjectEntryId, Worktree, - WorktreeId, + lsp_store::WorktreeId, worktree_store::WorktreeStore, File, PathChange, ProjectEntryId, + Worktree, }; pub struct PrettierStore { @@ -644,7 +644,7 @@ pub(super) async fn format_with_prettier( prettier_store: &WeakModel, buffer: &Model, cx: &mut AsyncAppContext, -) -> Option> { +) -> Option> { let prettier_instance = prettier_store .update(cx, |prettier_store, cx| { prettier_store.prettier_instance_for_buffer(buffer, cx) @@ -671,7 +671,7 @@ pub(super) async fn format_with_prettier( let format_result = prettier .format(buffer, buffer_path, cx) .await - .map(FormatOperation::Prettier) + .map(crate::lsp_store::FormatOperation::Prettier) .with_context(|| format!("{} failed to format buffer", prettier_description)); Some(format_result) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b1347c6d063f2..dc9337674b7eb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -31,7 +31,7 @@ pub use environment::ProjectEnvironment; use futures::{ channel::mpsc::{self, UnboundedReceiver}, future::try_join_all, - AsyncWriteExt, StreamExt, + StreamExt, }; use git::{blame::Blame, repository::GitRepository}; @@ -41,17 +41,14 @@ use gpui::{ }; use itertools::Itertools; use language::{ - language_settings::{ - language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings, - SelectedFormatter, - }, + language_settings::InlayHintKind, proto::{ deserialize_anchor, serialize_anchor, serialize_line_ending, serialize_version, split_operations, }, Buffer, BufferEvent, CachedLspAdapter, Capability, CodeLabel, ContextProvider, DiagnosticEntry, - Diff, Documentation, File as _, Language, LanguageRegistry, LanguageServerName, PointUtf16, - ToOffset, ToPointUtf16, Transaction, Unclipped, + Documentation, File as _, Language, LanguageRegistry, LanguageServerName, PointUtf16, ToOffset, + ToPointUtf16, Transaction, Unclipped, }; use lsp::{CompletionContext, DocumentHighlightKind, LanguageServer, LanguageServerId}; use lsp_command::*; @@ -84,7 +81,7 @@ use task::{ }; use terminals::Terminals; use text::{Anchor, BufferId}; -use util::{defer, paths::compare_paths, ResultExt as _}; +use util::{paths::compare_paths, ResultExt as _}; use worktree::{CreatedEntry, Snapshot, Traversal}; use worktree_store::{WorktreeStore, WorktreeStoreEvent}; @@ -164,8 +161,6 @@ pub struct Project { search_included_history: SearchHistory, search_excluded_history: SearchHistory, snippets: Model, - last_formatting_failure: Option, - buffers_being_formatted: HashSet, environment: Model, settings_observer: Model, } @@ -477,31 +472,6 @@ impl Hover { } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum FormatTrigger { - Save, - Manual, -} - -// Currently, formatting operations are represented differently depending on -// whether they come from a language server or an external command. -#[derive(Debug)] -enum FormatOperation { - Lsp(Vec<(Range, String)>), - External(Diff), - Prettier(Diff), -} - -impl FormatTrigger { - fn from_proto(value: i32) -> FormatTrigger { - match value { - 0 => FormatTrigger::Save, - 1 => FormatTrigger::Manual, - _ => FormatTrigger::Save, - } - } -} - enum EntitySubscription { Project(PendingEntitySubscription), BufferStore(PendingEntitySubscription), @@ -591,7 +561,7 @@ impl Project { client.add_model_message_handler(Self::handle_update_worktree); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); - client.add_model_request_handler(Self::handle_format_buffers); + client.add_model_request_handler(Self::handle_search_project); client.add_model_request_handler(Self::handle_search_candidate_buffers); client.add_model_request_handler(Self::handle_open_buffer_by_id); @@ -695,8 +665,7 @@ impl Project { search_history: Self::new_search_history(), environment, remotely_created_models: Default::default(), - last_formatting_failure: None, - buffers_being_formatted: Default::default(), + search_included_history: Self::new_search_history(), search_excluded_history: Self::new_search_history(), } @@ -779,8 +748,7 @@ impl Project { search_history: Self::new_search_history(), environment, remotely_created_models: Default::default(), - last_formatting_failure: None, - buffers_being_formatted: Default::default(), + search_included_history: Self::new_search_history(), search_excluded_history: Self::new_search_history(), }; @@ -967,8 +935,6 @@ impl Project { search_excluded_history: Self::new_search_history(), environment: ProjectEnvironment::new(&worktree_store, None, cx), remotely_created_models: Arc::new(Mutex::new(RemotelyCreatedModels::default())), - last_formatting_failure: None, - buffers_being_formatted: Default::default(), }; this.set_role(role, cx); for worktree in worktrees { @@ -2061,12 +2027,6 @@ impl Project { cx.emit(Event::SnippetEdit(*buffer_id, edits.clone())) } } - LspStoreEvent::StartFormattingLocalBuffer(buffer_id) => { - self.buffers_being_formatted.insert(*buffer_id); - } - LspStoreEvent::FinishFormattingLocalBuffer(buffer_id) => { - self.buffers_being_formatted.remove(buffer_id); - } } } @@ -2352,8 +2312,8 @@ impl Project { self.lsp_store.read(cx).language_server_statuses() } - pub fn last_formatting_failure(&self) -> Option<&str> { - self.last_formatting_failure.as_deref() + pub fn last_formatting_failure<'a>(&self, cx: &'a AppContext) -> Option<&'a str> { + self.lsp_store.read(cx).last_formatting_failure() } pub fn update_diagnostics( @@ -2455,558 +2415,12 @@ impl Project { &mut self, buffers: HashSet>, push_to_history: bool, - trigger: FormatTrigger, + trigger: lsp_store::FormatTrigger, cx: &mut ModelContext, ) -> Task> { - if self.is_local_or_ssh() { - let buffers_with_paths = buffers - .into_iter() - .map(|buffer_handle| { - let buffer = buffer_handle.read(cx); - let buffer_abs_path = File::from_dyn(buffer.file()) - .and_then(|file| file.as_local().map(|f| f.abs_path(cx))); - (buffer_handle, buffer_abs_path) - }) - .collect::>(); - - cx.spawn(move |project, mut cx| async move { - let result = Self::format_locally( - project.clone(), - buffers_with_paths, - push_to_history, - trigger, - cx.clone(), - ) - .await; - - project.update(&mut cx, |project, _| match &result { - Ok(_) => project.last_formatting_failure = None, - Err(error) => { - project.last_formatting_failure.replace(error.to_string()); - } - })?; - - result - }) - } else { - let remote_id = self.remote_id(); - let client = self.client.clone(); - cx.spawn(move |this, mut cx| async move { - if let Some(project_id) = remote_id { - let response = client - .request(proto::FormatBuffers { - project_id, - trigger: trigger as i32, - buffer_ids: buffers - .iter() - .map(|buffer| { - buffer.update(&mut cx, |buffer, _| buffer.remote_id().into()) - }) - .collect::>()?, - }) - .await? - .transaction - .ok_or_else(|| anyhow!("missing transaction"))?; - BufferStore::deserialize_project_transaction( - this.read_with(&cx, |this, _| this.buffer_store.downgrade())?, - response, - push_to_history, - cx, - ) - .await - } else { - Ok(ProjectTransaction::default()) - } - }) - } - } - - async fn format_locally( - project: WeakModel, - mut buffers_with_paths: Vec<(Model, Option)>, - push_to_history: bool, - trigger: FormatTrigger, - mut cx: AsyncAppContext, - ) -> anyhow::Result { - // Do not allow multiple concurrent formatting requests for the - // same buffer. - let lsp_store = project.update(&mut cx, |this, cx| { - buffers_with_paths.retain(|(buffer, _)| { - this.buffers_being_formatted - .insert(buffer.read(cx).remote_id()) - }); - this.lsp_store.downgrade() - })?; - - let _cleanup = defer({ - let this = project.clone(); - let mut cx = cx.clone(); - let buffers = &buffers_with_paths; - move || { - this.update(&mut cx, |this, cx| { - for (buffer, _) in buffers { - this.buffers_being_formatted - .remove(&buffer.read(cx).remote_id()); - } - }) - .ok(); - } - }); - - let mut project_transaction = ProjectTransaction::default(); - for (buffer, buffer_abs_path) in &buffers_with_paths { - let (primary_adapter_and_server, adapters_and_servers) = - project.update(&mut cx, |project, cx| { - let buffer = buffer.read(cx); - - let adapters_and_servers = project - .language_servers_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())) - .collect::>(); - - let primary_adapter = project - .lsp_store - .read(cx) - .primary_language_server_for_buffer(buffer, cx) - .map(|(adapter, lsp)| (adapter.clone(), lsp.clone())); - - (primary_adapter, adapters_and_servers) - })?; - - let settings = buffer.update(&mut cx, |buffer, cx| { - language_settings(buffer.language(), buffer.file(), cx).clone() - })?; - - let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save; - let ensure_final_newline = settings.ensure_final_newline_on_save; - - // First, format buffer's whitespace according to the settings. - let trailing_whitespace_diff = if remove_trailing_whitespace { - Some( - buffer - .update(&mut cx, |b, cx| b.remove_trailing_whitespace(cx))? - .await, - ) - } else { - None - }; - let whitespace_transaction_id = buffer.update(&mut cx, |buffer, cx| { - buffer.finalize_last_transaction(); - buffer.start_transaction(); - if let Some(diff) = trailing_whitespace_diff { - buffer.apply_diff(diff, cx); - } - if ensure_final_newline { - buffer.ensure_final_newline(cx); - } - buffer.end_transaction(cx) - })?; - - // Apply the `code_actions_on_format` before we run the formatter. - let code_actions = deserialize_code_actions(&settings.code_actions_on_format); - #[allow(clippy::nonminimal_bool)] - if !code_actions.is_empty() - && !(trigger == FormatTrigger::Save && settings.format_on_save == FormatOnSave::Off) - { - LspStore::execute_code_actions_on_servers( - &lsp_store, - &adapters_and_servers, - code_actions, - buffer, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await?; - } - - // Apply language-specific formatting using either the primary language server - // or external command. - // Except for code actions, which are applied with all connected language servers. - let primary_language_server = - primary_adapter_and_server.map(|(_adapter, server)| server.clone()); - let server_and_buffer = primary_language_server - .as_ref() - .zip(buffer_abs_path.as_ref()); - - let prettier_settings = buffer.read_with(&cx, |buffer, cx| { - language_settings(buffer.language(), buffer.file(), cx) - .prettier - .clone() - })?; - - let mut format_operations: Vec = vec![]; - { - match trigger { - FormatTrigger::Save => { - match &settings.format_on_save { - FormatOnSave::Off => { - // nothing - } - FormatOnSave::On => { - match &settings.formatter { - SelectedFormatter::Auto => { - // do the auto-format: prefer prettier, fallback to primary language server - let diff = { - if prettier_settings.allowed { - Self::perform_format( - &Formatter::Prettier, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - } else { - Self::perform_format( - &Formatter::LanguageServer { name: None }, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - } - } - .log_err() - .flatten(); - if let Some(op) = diff { - format_operations.push(op); - } - } - SelectedFormatter::List(formatters) => { - for formatter in formatters.as_ref() { - let diff = Self::perform_format( - formatter, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - .log_err() - .flatten(); - if let Some(op) = diff { - format_operations.push(op); - } - - // format with formatter - } - } - } - } - FormatOnSave::List(formatters) => { - for formatter in formatters.as_ref() { - let diff = Self::perform_format( - formatter, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - .log_err() - .flatten(); - if let Some(op) = diff { - format_operations.push(op); - } - } - } - } - } - FormatTrigger::Manual => { - match &settings.formatter { - SelectedFormatter::Auto => { - // do the auto-format: prefer prettier, fallback to primary language server - let diff = { - if prettier_settings.allowed { - Self::perform_format( - &Formatter::Prettier, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - } else { - Self::perform_format( - &Formatter::LanguageServer { name: None }, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - } - } - .log_err() - .flatten(); - - if let Some(op) = diff { - format_operations.push(op) - } - } - SelectedFormatter::List(formatters) => { - for formatter in formatters.as_ref() { - // format with formatter - let diff = Self::perform_format( - formatter, - server_and_buffer, - project.clone(), - buffer, - buffer_abs_path, - &settings, - &adapters_and_servers, - push_to_history, - &mut project_transaction, - &mut cx, - ) - .await - .log_err() - .flatten(); - if let Some(op) = diff { - format_operations.push(op); - } - } - } - } - } - } - } - - buffer.update(&mut cx, |b, cx| { - // If the buffer had its whitespace formatted and was edited while the language-specific - // formatting was being computed, avoid applying the language-specific formatting, because - // it can't be grouped with the whitespace formatting in the undo history. - if let Some(transaction_id) = whitespace_transaction_id { - if b.peek_undo_stack() - .map_or(true, |e| e.transaction_id() != transaction_id) - { - format_operations.clear(); - } - } - - // Apply any language-specific formatting, and group the two formatting operations - // in the buffer's undo history. - for operation in format_operations { - match operation { - FormatOperation::Lsp(edits) => { - b.edit(edits, None, cx); - } - FormatOperation::External(diff) => { - b.apply_diff(diff, cx); - } - FormatOperation::Prettier(diff) => { - b.apply_diff(diff, cx); - } - } - - if let Some(transaction_id) = whitespace_transaction_id { - b.group_until_transaction(transaction_id); - } else if let Some(transaction) = project_transaction.0.get(buffer) { - b.group_until_transaction(transaction.id) - } - } - - if let Some(transaction) = b.finalize_last_transaction().cloned() { - if !push_to_history { - b.forget_transaction(transaction.id); - } - project_transaction.0.insert(buffer.clone(), transaction); - } - })?; - } - - Ok(project_transaction) - } - - #[allow(clippy::too_many_arguments)] - async fn perform_format( - formatter: &Formatter, - primary_server_and_buffer: Option<(&Arc, &PathBuf)>, - project: WeakModel, - buffer: &Model, - buffer_abs_path: &Option, - settings: &LanguageSettings, - adapters_and_servers: &[(Arc, Arc)], - push_to_history: bool, - transaction: &mut ProjectTransaction, - cx: &mut AsyncAppContext, - ) -> Result, anyhow::Error> { - let result = match formatter { - Formatter::LanguageServer { name } => { - if let Some((language_server, buffer_abs_path)) = primary_server_and_buffer { - let language_server = if let Some(name) = name { - adapters_and_servers - .iter() - .find_map(|(adapter, server)| { - adapter.name.0.as_ref().eq(name.as_str()).then_some(server) - }) - .unwrap_or(language_server) - } else { - language_server - }; - - let lsp_store = project.update(cx, |p, _| p.lsp_store.downgrade())?; - Some(FormatOperation::Lsp( - LspStore::format_via_lsp( - &lsp_store, - buffer, - buffer_abs_path, - language_server, - settings, - cx, - ) - .await - .context("failed to format via language server")?, - )) - } else { - None - } - } - Formatter::Prettier => { - let prettier = project.update(cx, |project, cx| { - project - .lsp_store - .read(cx) - .prettier_store() - .unwrap() - .downgrade() - })?; - prettier_store::format_with_prettier(&prettier, buffer, cx) - .await - .transpose() - .ok() - .flatten() - } - Formatter::External { command, arguments } => { - let buffer_abs_path = buffer_abs_path.as_ref().map(|path| path.as_path()); - Self::format_via_external_command(buffer, buffer_abs_path, command, arguments, cx) - .await - .context(format!( - "failed to format via external command {:?}", - command - ))? - .map(FormatOperation::External) - } - Formatter::CodeActions(code_actions) => { - let code_actions = deserialize_code_actions(code_actions); - let lsp_store = project.update(cx, |p, _| p.lsp_store.downgrade())?; - if !code_actions.is_empty() { - LspStore::execute_code_actions_on_servers( - &lsp_store, - adapters_and_servers, - code_actions, - buffer, - push_to_history, - transaction, - cx, - ) - .await?; - } - None - } - }; - anyhow::Ok(result) - } - - async fn format_via_external_command( - buffer: &Model, - buffer_abs_path: Option<&Path>, - command: &str, - arguments: &[String], - cx: &mut AsyncAppContext, - ) -> Result> { - let working_dir_path = buffer.update(cx, |buffer, cx| { - let file = File::from_dyn(buffer.file())?; - let worktree = file.worktree.read(cx); - let mut worktree_path = worktree.abs_path().to_path_buf(); - if worktree.root_entry()?.is_file() { - worktree_path.pop(); - } - Some(worktree_path) - })?; - - let mut child = smol::process::Command::new(command); - #[cfg(target_os = "windows")] - { - use smol::process::windows::CommandExt; - child.creation_flags(windows::Win32::System::Threading::CREATE_NO_WINDOW.0); - } - - if let Some(working_dir_path) = working_dir_path { - child.current_dir(working_dir_path); - } - - let mut child = child - .args(arguments.iter().map(|arg| { - if let Some(buffer_abs_path) = buffer_abs_path { - arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy()) - } else { - arg.replace("{buffer_path}", "Untitled") - } - })) - .stdin(smol::process::Stdio::piped()) - .stdout(smol::process::Stdio::piped()) - .stderr(smol::process::Stdio::piped()) - .spawn()?; - - let stdin = child - .stdin - .as_mut() - .ok_or_else(|| anyhow!("failed to acquire stdin"))?; - let text = buffer.update(cx, |buffer, _| buffer.as_rope().clone())?; - for chunk in text.chunks() { - stdin.write_all(chunk.as_bytes()).await?; - } - stdin.flush().await?; - - let output = child.output().await?; - if !output.status.success() { - return Err(anyhow!( - "command failed with exit code {:?}:\nstdout: {}\nstderr: {}", - output.status.code(), - String::from_utf8_lossy(&output.stdout), - String::from_utf8_lossy(&output.stderr), - )); - } - - let stdout = String::from_utf8(output.stdout)?; - Ok(Some( - buffer - .update(cx, |buffer, cx| buffer.diff(stdout, cx))? - .await, - )) + self.lsp_store.update(cx, |lsp_store, cx| { + lsp_store.format(buffers, push_to_history, trigger, cx) + }) } #[inline(never)] @@ -4210,31 +3624,6 @@ impl Project { Ok(response) } - async fn handle_format_buffers( - this: Model, - envelope: TypedEnvelope, - mut cx: AsyncAppContext, - ) -> Result { - let sender_id = envelope.original_sender_id()?; - let format = this.update(&mut cx, |this, cx| { - let mut buffers = HashSet::default(); - for buffer_id in &envelope.payload.buffer_ids { - let buffer_id = BufferId::new(*buffer_id)?; - 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)) - })??; - - let project_transaction = format.await?; - let project_transaction = this.update(&mut cx, |this, cx| { - this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx) - })?; - Ok(proto::FormatBuffersResponse { - transaction: Some(project_transaction), - }) - } - async fn handle_task_context_for_location( project: Model, envelope: TypedEnvelope, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a7d2e6766c233..9e58caa244243 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -4,7 +4,7 @@ use futures::{future, StreamExt}; use gpui::{AppContext, SemanticVersion, UpdateGlobal}; use http_client::Url; use language::{ - language_settings::{AllLanguageSettings, LanguageSettingsContent}, + language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent}, tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticSet, FakeLspAdapter, LanguageConfig, LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint, };