Allow providing an external format in format_on_save setting

This commit is contained in:
Antonio Scandurra 2022-07-07 11:03:37 +02:00
parent 4ec2d6e50d
commit c6254247c3
4 changed files with 200 additions and 89 deletions

View file

@ -352,13 +352,8 @@ impl Item for Editor {
project: ModelHandle<Project>, project: ModelHandle<Project>,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) -> Task<Result<()>> { ) -> Task<Result<()>> {
let settings = cx.global::<Settings>();
let buffer = self.buffer().clone(); let buffer = self.buffer().clone();
let mut buffers = buffer.read(cx).all_buffers(); let buffers = buffer.read(cx).all_buffers();
buffers.retain(|buffer| {
let language_name = buffer.read(cx).language().map(|l| l.name());
settings.format_on_save(language_name.as_deref())
});
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); let format = project.update(cx, |project, cx| project.format(buffers, true, cx));
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {

View file

@ -273,7 +273,7 @@ pub struct Chunk<'a> {
pub is_unnecessary: bool, pub is_unnecessary: bool,
} }
pub(crate) struct Diff { pub struct Diff {
base_version: clock::Global, base_version: clock::Global,
new_text: Arc<str>, new_text: Arc<str>,
changes: Vec<(ChangeTag, usize)>, changes: Vec<(ChangeTag, usize)>,
@ -958,7 +958,7 @@ impl Buffer {
} }
} }
pub(crate) fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> { pub fn diff(&self, mut new_text: String, cx: &AppContext) -> Task<Diff> {
let old_text = self.as_rope().clone(); let old_text = self.as_rope().clone();
let base_version = self.version(); let base_version = self.version();
cx.background().spawn(async move { cx.background().spawn(async move {
@ -979,11 +979,7 @@ impl Buffer {
}) })
} }
pub(crate) fn apply_diff( pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
&mut self,
diff: Diff,
cx: &mut ModelContext<Self>,
) -> Option<&Transaction> {
if self.version == diff.base_version { if self.version == diff.base_version {
self.finalize_last_transaction(); self.finalize_last_transaction();
self.start_transaction(); self.start_transaction();

View file

@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
use clock::ReplicaId; use clock::ReplicaId;
use collections::{hash_map, BTreeMap, HashMap, HashSet}; use collections::{hash_map, BTreeMap, HashMap, HashSet};
use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use futures::{future::Shared, AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt};
use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet};
use gpui::{ use gpui::{
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
@ -51,10 +51,12 @@ use std::{
ffi::OsString, ffi::OsString,
hash::Hash, hash::Hash,
mem, mem,
num::NonZeroU32,
ops::Range, ops::Range,
os::unix::{ffi::OsStrExt, prelude::OsStringExt}, os::unix::{ffi::OsStrExt, prelude::OsStringExt},
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
rc::Rc, rc::Rc,
str,
sync::{ sync::{
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
Arc, Arc,
@ -3025,14 +3027,68 @@ impl Project {
} }
for (buffer, buffer_abs_path, language_server) in local_buffers { for (buffer, buffer_abs_path, language_server) in local_buffers {
let text_document = lsp::TextDocumentIdentifier::new( let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
lsp::Url::from_file_path(&buffer_abs_path).unwrap(), let settings = cx.global::<Settings>();
); let language_name = buffer.language().map(|language| language.name());
let capabilities = &language_server.capabilities(); (
let tab_size = cx.update(|cx| { settings.format_on_save(language_name.as_deref()),
let language_name = buffer.read(cx).language().map(|language| language.name()); settings.tab_size(language_name.as_deref()),
cx.global::<Settings>().tab_size(language_name.as_deref()) )
}); });
let transaction = match format_on_save {
settings::FormatOnSave::Off => continue,
settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
&this,
&buffer,
&buffer_abs_path,
&language_server,
tab_size,
&mut cx,
)
.await
.context("failed to format via language server")?,
settings::FormatOnSave::External { command, arguments } => {
Self::format_via_external_command(
&buffer,
&buffer_abs_path,
&command,
&arguments,
&mut cx,
)
.await
.context(format!(
"failed to format via external command {:?}",
command
))?
}
};
if let Some(transaction) = transaction {
if !push_to_history {
buffer.update(&mut cx, |buffer, _| {
buffer.forget_transaction(transaction.id)
});
}
project_transaction.0.insert(buffer, transaction);
}
}
Ok(project_transaction)
})
}
async fn format_via_lsp(
this: &ModelHandle<Self>,
buffer: &ModelHandle<Buffer>,
abs_path: &Path,
language_server: &Arc<LanguageServer>,
tab_size: NonZeroU32,
cx: &mut AsyncAppContext,
) -> Result<Option<Transaction>> {
let text_document =
lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(abs_path).unwrap());
let capabilities = &language_server.capabilities();
let lsp_edits = if capabilities let lsp_edits = if capabilities
.document_formatting_provider .document_formatting_provider
.as_ref() .as_ref()
@ -3057,10 +3113,9 @@ impl Project {
{ {
let buffer_start = lsp::Position::new(0, 0); let buffer_start = lsp::Position::new(0, 0);
let buffer_end = let buffer_end =
buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16())); buffer.read_with(cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
language_server language_server
.request::<lsp::request::RangeFormatting>( .request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
lsp::DocumentRangeFormattingParams {
text_document, text_document,
range: lsp::Range::new(buffer_start, buffer_end), range: lsp::Range::new(buffer_start, buffer_end),
options: lsp::FormattingOptions { options: lsp::FormattingOptions {
@ -3070,20 +3125,19 @@ impl Project {
..Default::default() ..Default::default()
}, },
work_done_progress_params: Default::default(), work_done_progress_params: Default::default(),
}, })
)
.await? .await?
} else { } else {
continue; None
}; };
if let Some(lsp_edits) = lsp_edits { if let Some(lsp_edits) = lsp_edits {
let edits = this let edits = this
.update(&mut cx, |this, cx| { .update(cx, |this, cx| {
this.edits_from_lsp(&buffer, lsp_edits, None, cx) this.edits_from_lsp(&buffer, lsp_edits, None, cx)
}) })
.await?; .await?;
buffer.update(&mut cx, |buffer, cx| { buffer.update(cx, |buffer, cx| {
buffer.finalize_last_transaction(); buffer.finalize_last_transaction();
buffer.start_transaction(); buffer.start_transaction();
for (range, text) in edits { for (range, text) in edits {
@ -3091,17 +3145,72 @@ impl Project {
} }
if buffer.end_transaction(cx).is_some() { if buffer.end_transaction(cx).is_some() {
let transaction = buffer.finalize_last_transaction().unwrap().clone(); let transaction = buffer.finalize_last_transaction().unwrap().clone();
if !push_to_history { Ok(Some(transaction))
buffer.forget_transaction(transaction.id); } else {
Ok(None)
} }
project_transaction.0.insert(cx.handle(), transaction); })
} } else {
}); Ok(None)
} }
} }
Ok(project_transaction) async fn format_via_external_command(
}) buffer: &ModelHandle<Buffer>,
buffer_abs_path: &Path,
command: &str,
arguments: &[String],
cx: &mut AsyncAppContext,
) -> Result<Option<Transaction>> {
let working_dir_path = buffer.read_with(cx, |buffer, cx| {
let file = File::from_dyn(buffer.file())?;
let worktree = file.worktree.read(cx).as_local()?;
let mut worktree_path = worktree.abs_path().to_path_buf();
if worktree.root_entry()?.is_file() {
worktree_path.pop();
}
Some(worktree_path)
});
if let Some(working_dir_path) = working_dir_path {
let mut child =
smol::process::Command::new(command)
.args(arguments.iter().map(|arg| {
arg.replace("{buffer_path}", &buffer_abs_path.to_string_lossy())
}))
.current_dir(&working_dir_path)
.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.read_with(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)?;
let diff = buffer
.read_with(cx, |buffer, cx| buffer.diff(stdout, cx))
.await;
Ok(buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx).cloned()))
} else {
Ok(None)
}
} }
pub fn definition<T: ToPointUtf16>( pub fn definition<T: ToPointUtf16>(

View file

@ -38,7 +38,7 @@ pub struct LanguageSettings {
pub hard_tabs: Option<bool>, pub hard_tabs: Option<bool>,
pub soft_wrap: Option<SoftWrap>, pub soft_wrap: Option<SoftWrap>,
pub preferred_line_length: Option<u32>, pub preferred_line_length: Option<u32>,
pub format_on_save: Option<bool>, pub format_on_save: Option<FormatOnSave>,
pub enable_language_server: Option<bool>, pub enable_language_server: Option<bool>,
} }
@ -50,6 +50,17 @@ pub enum SoftWrap {
PreferredLineLength, PreferredLineLength,
} }
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum FormatOnSave {
Off,
LanguageServer,
External {
command: String,
arguments: Vec<String>,
},
}
#[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)] #[derive(Copy, Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum Autosave { pub enum Autosave {
@ -72,7 +83,7 @@ pub struct SettingsFileContent {
#[serde(default)] #[serde(default)]
pub vim_mode: Option<bool>, pub vim_mode: Option<bool>,
#[serde(default)] #[serde(default)]
pub format_on_save: Option<bool>, pub format_on_save: Option<FormatOnSave>,
#[serde(default)] #[serde(default)]
pub autosave: Option<Autosave>, pub autosave: Option<Autosave>,
#[serde(default)] #[serde(default)]
@ -136,9 +147,9 @@ impl Settings {
.unwrap_or(80) .unwrap_or(80)
} }
pub fn format_on_save(&self, language: Option<&str>) -> bool { pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
self.language_setting(language, |settings| settings.format_on_save) self.language_setting(language, |settings| settings.format_on_save.clone())
.unwrap_or(true) .unwrap_or(FormatOnSave::LanguageServer)
} }
pub fn enable_language_server(&self, language: Option<&str>) -> bool { pub fn enable_language_server(&self, language: Option<&str>) -> bool {
@ -215,7 +226,7 @@ impl Settings {
merge(&mut self.autosave, data.autosave); merge(&mut self.autosave, data.autosave);
merge_option( merge_option(
&mut self.language_settings.format_on_save, &mut self.language_settings.format_on_save,
data.format_on_save, data.format_on_save.clone(),
); );
merge_option( merge_option(
&mut self.language_settings.enable_language_server, &mut self.language_settings.enable_language_server,
@ -339,7 +350,7 @@ fn merge<T: Copy>(target: &mut T, value: Option<T>) {
} }
} }
fn merge_option<T: Copy>(target: &mut Option<T>, value: Option<T>) { fn merge_option<T>(target: &mut Option<T>, value: Option<T>) {
if value.is_some() { if value.is_some() {
*target = value; *target = value;
} }