Move formatting to LSP store (#18242)
Release Notes: - ssh-remoting: Fixed format on save --------- Co-authored-by: Mikayla <mikayla@zed.dev>
This commit is contained in:
parent
e95e1c9ae5
commit
e4080ef565
8 changed files with 655 additions and 657 deletions
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<Anchor>, 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<Arc<dyn HttpClient>>,
|
||||
environment: Model<ProjectEnvironment>,
|
||||
fs: Arc<dyn Fs>,
|
||||
yarn: Model<YarnPathStore>,
|
||||
pub language_servers: HashMap<LanguageServerId, LanguageServerState>,
|
||||
buffers_being_formatted: HashSet<BufferId>,
|
||||
last_workspace_edits_by_language_server: HashMap<LanguageServerId, ProjectTransaction>,
|
||||
language_server_watched_paths: HashMap<LanguageServerId, Model<LanguageServerWatchedPaths>>,
|
||||
language_server_watcher_registrations:
|
||||
|
@ -104,6 +132,7 @@ pub struct LocalLspStore {
|
|||
HashMap<LanguageServerId, (LanguageServerName, Arc<LanguageServer>)>,
|
||||
prettier_store: Model<PrettierStore>,
|
||||
current_lsp_settings: HashMap<LanguageServerName, LspSettings>,
|
||||
last_formatting_failure: Option<String>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
|
@ -128,6 +157,485 @@ impl LocalLspStore {
|
|||
futures::future::join_all(shutdown_futures).await;
|
||||
}
|
||||
}
|
||||
async fn format_locally(
|
||||
lsp_store: WeakModel<LspStore>,
|
||||
mut buffers_with_paths: Vec<(Model<Buffer>, Option<PathBuf>)>,
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> anyhow::Result<ProjectTransaction> {
|
||||
// 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::<Vec<_>>();
|
||||
|
||||
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<FormatOperation> = 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<LanguageServer>, &PathBuf)>,
|
||||
lsp_store: WeakModel<LspStore>,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_abs_path: &Option<PathBuf>,
|
||||
settings: &LanguageSettings,
|
||||
adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
|
||||
push_to_history: bool,
|
||||
transaction: &mut ProjectTransaction,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<FormatOperation>, 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>,
|
||||
buffer_abs_path: Option<&Path>,
|
||||
command: &str,
|
||||
arguments: &[String],
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<Diff>> {
|
||||
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<Buffer>,
|
||||
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<Model<Buffer>>,
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Result<_>>()?,
|
||||
})
|
||||
.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<Self>,
|
||||
envelope: TypedEnvelope<proto::FormatBuffers>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::FormatBuffersResponse> {
|
||||
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,
|
||||
|
|
|
@ -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<PrettierStore>,
|
||||
buffer: &Model<Buffer>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Option<Result<FormatOperation>> {
|
||||
) -> Option<Result<crate::lsp_store::FormatOperation>> {
|
||||
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)
|
||||
|
|
|
@ -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<SnippetProvider>,
|
||||
last_formatting_failure: Option<String>,
|
||||
buffers_being_formatted: HashSet<BufferId>,
|
||||
environment: Model<ProjectEnvironment>,
|
||||
settings_observer: Model<SettingsObserver>,
|
||||
}
|
||||
|
@ -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<Anchor>, 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<Project>),
|
||||
BufferStore(PendingEntitySubscription<BufferStore>),
|
||||
|
@ -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<Model<Buffer>>,
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
trigger: lsp_store::FormatTrigger,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<anyhow::Result<ProjectTransaction>> {
|
||||
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::<Vec<_>>();
|
||||
|
||||
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::<Result<_>>()?,
|
||||
})
|
||||
.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<Project>,
|
||||
mut buffers_with_paths: Vec<(Model<Buffer>, Option<PathBuf>)>,
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> anyhow::Result<ProjectTransaction> {
|
||||
// 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::<Vec<_>>();
|
||||
|
||||
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<FormatOperation> = 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<LanguageServer>, &PathBuf)>,
|
||||
project: WeakModel<Project>,
|
||||
buffer: &Model<Buffer>,
|
||||
buffer_abs_path: &Option<PathBuf>,
|
||||
settings: &LanguageSettings,
|
||||
adapters_and_servers: &[(Arc<CachedLspAdapter>, Arc<LanguageServer>)],
|
||||
push_to_history: bool,
|
||||
transaction: &mut ProjectTransaction,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<FormatOperation>, 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>,
|
||||
buffer_abs_path: Option<&Path>,
|
||||
command: &str,
|
||||
arguments: &[String],
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Option<Diff>> {
|
||||
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<Self>,
|
||||
envelope: TypedEnvelope<proto::FormatBuffers>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::FormatBuffersResponse> {
|
||||
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<Self>,
|
||||
envelope: TypedEnvelope<proto::TaskContextForLocation>,
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue