mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-26 10:40:54 +00:00
Add editor action to manually invoke buffer format
This commit is contained in:
parent
955ebc5499
commit
f3395cf4fd
9 changed files with 124 additions and 33 deletions
|
@ -93,6 +93,7 @@
|
|||
"cmd-shift-down": "editor::SelectToEnd",
|
||||
"cmd-a": "editor::SelectAll",
|
||||
"cmd-l": "editor::SelectLine",
|
||||
"cmd-shift-i": "editor::Format",
|
||||
"cmd-shift-left": [
|
||||
"editor::SelectToBeginningOfLine",
|
||||
{
|
||||
|
|
|
@ -42,21 +42,20 @@
|
|||
// 3. Position the dock full screen over the entire workspace"
|
||||
// "default_dock_anchor": "expanded"
|
||||
"default_dock_anchor": "right",
|
||||
// How to auto-format modified buffers when saving them. This
|
||||
// setting can take three values:
|
||||
// Whether or not to perform a buffer format before saving
|
||||
"format_on_save": true,
|
||||
// How to perform a buffer format. This setting can take two values:
|
||||
//
|
||||
// 1. Don't format code
|
||||
// "format_on_save": "off"
|
||||
// 2. Format code using the current language server:
|
||||
// 1. Format code using the current language server:
|
||||
// "format_on_save": "language_server"
|
||||
// 3. Format code using an external command:
|
||||
// 2. Format code using an external command:
|
||||
// "format_on_save": {
|
||||
// "external": {
|
||||
// "command": "prettier",
|
||||
// "arguments": ["--stdin-filepath", "{buffer_path}"]
|
||||
// }
|
||||
// }
|
||||
"format_on_save": "language_server",
|
||||
"formatter": "language_server",
|
||||
// How to soft-wrap long lines of text. This setting can take
|
||||
// three values:
|
||||
//
|
||||
|
|
|
@ -36,7 +36,7 @@ use project::{
|
|||
use rand::prelude::*;
|
||||
use rpc::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::{FormatOnSave, Settings};
|
||||
use settings::{Formatter, Settings};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
@ -1990,6 +1990,8 @@ async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut Te
|
|||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
use project::FormatTrigger;
|
||||
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
@ -2042,7 +2044,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
|||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
|
||||
project.format(
|
||||
HashSet::from_iter([buffer_b.clone()]),
|
||||
true,
|
||||
FormatTrigger::Save,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -2055,7 +2062,7 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
|||
// host's configuration is honored as opposed to using the guest's settings.
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.editor_defaults.format_on_save = Some(FormatOnSave::External {
|
||||
settings.editor_defaults.formatter = Some(Formatter::External {
|
||||
command: "awk".to_string(),
|
||||
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
|
||||
});
|
||||
|
@ -2063,7 +2070,12 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
|||
});
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
|
||||
project.format(
|
||||
HashSet::from_iter([buffer_b.clone()]),
|
||||
true,
|
||||
FormatTrigger::Save,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
|
@ -19,6 +19,7 @@ use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque};
|
|||
pub use display_map::DisplayPoint;
|
||||
use display_map::*;
|
||||
pub use element::*;
|
||||
use futures::FutureExt;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
|
@ -49,7 +50,7 @@ pub use multi_buffer::{
|
|||
};
|
||||
use multi_buffer::{MultiBufferChunks, ToOffsetUtf16};
|
||||
use ordered_float::OrderedFloat;
|
||||
use project::{LocationLink, Project, ProjectPath, ProjectTransaction};
|
||||
use project::{FormatTrigger, LocationLink, Project, ProjectPath, ProjectTransaction};
|
||||
use selections_collection::{resolve_multiple, MutableSelectionsCollection, SelectionsCollection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
|
@ -76,6 +77,8 @@ const MAX_LINE_LEN: usize = 1024;
|
|||
const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10;
|
||||
const MAX_SELECTION_HISTORY_LEN: usize = 1024;
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
|
||||
#[derive(Clone, Deserialize, PartialEq, Default)]
|
||||
pub struct SelectNext {
|
||||
#[serde(default)]
|
||||
|
@ -204,6 +207,7 @@ actions!(
|
|||
OpenExcerpts,
|
||||
RestartLanguageServer,
|
||||
Hover,
|
||||
Format,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -310,6 +314,7 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(Editor::toggle_code_actions);
|
||||
cx.add_action(Editor::open_excerpts);
|
||||
cx.add_action(Editor::jump);
|
||||
cx.add_async_action(Editor::format);
|
||||
cx.add_action(Editor::restart_language_server);
|
||||
cx.add_action(Editor::show_character_palette);
|
||||
cx.add_async_action(Editor::confirm_completion);
|
||||
|
@ -5173,6 +5178,43 @@ impl Editor {
|
|||
self.pending_rename.as_ref()
|
||||
}
|
||||
|
||||
fn format(&mut self, _: &Format, cx: &mut ViewContext<'_, Self>) -> Option<Task<Result<()>>> {
|
||||
let project = match &self.project {
|
||||
Some(project) => project,
|
||||
None => return None,
|
||||
};
|
||||
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
|
||||
let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse();
|
||||
let format = project.update(cx, |project, cx| {
|
||||
project.format(buffers, true, FormatTrigger::Manual, cx)
|
||||
});
|
||||
|
||||
Some(cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
_ = timeout => {
|
||||
log::warn!("timed out waiting for formatting");
|
||||
None
|
||||
}
|
||||
transaction = format.log_err().fuse() => transaction,
|
||||
};
|
||||
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
|
||||
fn restart_language_server(&mut self, _: &RestartLanguageServer, cx: &mut ViewContext<Self>) {
|
||||
if let Some(project) = self.project.clone() {
|
||||
self.buffer.update(cx, |multi_buffer, cx| {
|
||||
|
@ -10087,7 +10129,7 @@ mod tests {
|
|||
unreachable!()
|
||||
});
|
||||
let save = cx.update(|cx| editor.save(project.clone(), cx));
|
||||
cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
|
||||
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.foreground().start_waiting();
|
||||
save.await.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -10203,7 +10245,7 @@ mod tests {
|
|||
},
|
||||
);
|
||||
let save = cx.update(|cx| editor.save(project.clone(), cx));
|
||||
cx.foreground().advance_clock(items::FORMAT_TIMEOUT);
|
||||
cx.foreground().advance_clock(super::FORMAT_TIMEOUT);
|
||||
cx.foreground().start_waiting();
|
||||
save.await.unwrap();
|
||||
assert_eq!(
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::{
|
||||
display_map::ToDisplayPoint, link_go_to_definition::hide_link_definition,
|
||||
movement::surrounding_word, Anchor, Autoscroll, Editor, Event, ExcerptId, MultiBuffer,
|
||||
MultiBufferSnapshot, NavigationData, ToPoint as _,
|
||||
MultiBufferSnapshot, NavigationData, ToPoint as _, FORMAT_TIMEOUT,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use futures::FutureExt;
|
||||
|
@ -10,7 +10,7 @@ use gpui::{
|
|||
RenderContext, Subscription, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use language::{Bias, Buffer, File as _, OffsetRangeExt, SelectionGoal};
|
||||
use project::{File, Project, ProjectEntryId, ProjectPath};
|
||||
use project::{File, FormatTrigger, Project, ProjectEntryId, ProjectPath};
|
||||
use rpc::proto::{self, update_view};
|
||||
use settings::Settings;
|
||||
use smallvec::SmallVec;
|
||||
|
@ -20,7 +20,6 @@ use std::{
|
|||
fmt::Write,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
use text::{Point, Selection};
|
||||
use util::TryFutureExt;
|
||||
|
@ -30,7 +29,6 @@ use workspace::{
|
|||
ToolbarItemLocation,
|
||||
};
|
||||
|
||||
pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2);
|
||||
pub const MAX_TAB_TITLE_LEN: usize = 24;
|
||||
|
||||
impl FollowableItem for Editor {
|
||||
|
@ -409,7 +407,9 @@ impl Item for Editor {
|
|||
let buffer = self.buffer().clone();
|
||||
let buffers = buffer.read(cx).all_buffers();
|
||||
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, FormatTrigger::Save, cx)
|
||||
});
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let transaction = futures::select_biased! {
|
||||
_ = timeout => {
|
||||
|
|
|
@ -364,6 +364,22 @@ impl ProjectEntryId {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum FormatTrigger {
|
||||
Save,
|
||||
Manual,
|
||||
}
|
||||
|
||||
impl FormatTrigger {
|
||||
fn from_proto(value: i32) -> FormatTrigger {
|
||||
match value {
|
||||
0 => FormatTrigger::Save,
|
||||
1 => FormatTrigger::Manual,
|
||||
_ => FormatTrigger::Save,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Project {
|
||||
pub fn init(client: &Arc<Client>) {
|
||||
client.add_model_message_handler(Self::handle_request_join_project);
|
||||
|
@ -3047,6 +3063,7 @@ impl Project {
|
|||
&self,
|
||||
buffers: HashSet<ModelHandle<Buffer>>,
|
||||
push_to_history: bool,
|
||||
trigger: FormatTrigger,
|
||||
cx: &mut ModelContext<Project>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
let mut local_buffers = Vec::new();
|
||||
|
@ -3076,6 +3093,7 @@ impl Project {
|
|||
let response = client
|
||||
.request(proto::FormatBuffers {
|
||||
project_id,
|
||||
trigger: trigger as i32,
|
||||
buffer_ids: remote_buffers
|
||||
.iter()
|
||||
.map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id()))
|
||||
|
@ -3092,18 +3110,22 @@ impl Project {
|
|||
}
|
||||
|
||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
||||
let (format_on_save, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||
let (format_on_save, formatter, tab_size) = buffer.read_with(&cx, |buffer, cx| {
|
||||
let settings = cx.global::<Settings>();
|
||||
let language_name = buffer.language().map(|language| language.name());
|
||||
(
|
||||
settings.format_on_save(language_name.as_deref()),
|
||||
settings.formatter(language_name.as_deref()),
|
||||
settings.tab_size(language_name.as_deref()),
|
||||
)
|
||||
});
|
||||
|
||||
let transaction = match format_on_save {
|
||||
settings::FormatOnSave::Off => continue,
|
||||
settings::FormatOnSave::LanguageServer => Self::format_via_lsp(
|
||||
if trigger == FormatTrigger::Save && !format_on_save {
|
||||
continue;
|
||||
}
|
||||
|
||||
let transaction = match formatter {
|
||||
settings::Formatter::LanguageServer => Self::format_via_lsp(
|
||||
&this,
|
||||
&buffer,
|
||||
&buffer_abs_path,
|
||||
|
@ -3113,7 +3135,8 @@ impl Project {
|
|||
)
|
||||
.await
|
||||
.context("failed to format via language server")?,
|
||||
settings::FormatOnSave::External { command, arguments } => {
|
||||
|
||||
settings::Formatter::External { command, arguments } => {
|
||||
Self::format_via_external_command(
|
||||
&buffer,
|
||||
&buffer_abs_path,
|
||||
|
@ -5296,7 +5319,8 @@ impl Project {
|
|||
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
|
||||
);
|
||||
}
|
||||
Ok::<_, anyhow::Error>(this.format(buffers, false, cx))
|
||||
let trigger = FormatTrigger::from_proto(envelope.payload.trigger);
|
||||
Ok::<_, anyhow::Error>(this.format(buffers, false, trigger, cx))
|
||||
})?;
|
||||
|
||||
let project_transaction = format.await?;
|
||||
|
|
|
@ -420,9 +420,15 @@ message ReloadBuffersResponse {
|
|||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
enum FormatTrigger {
|
||||
Save = 0;
|
||||
Manual = 1;
|
||||
}
|
||||
|
||||
message FormatBuffers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
FormatTrigger trigger = 2;
|
||||
repeated uint64 buffer_ids = 3;
|
||||
}
|
||||
|
||||
message FormatBuffersResponse {
|
||||
|
|
|
@ -6,4 +6,4 @@ pub use conn::Connection;
|
|||
pub use peer::*;
|
||||
mod macros;
|
||||
|
||||
pub const PROTOCOL_VERSION: u32 = 31;
|
||||
pub const PROTOCOL_VERSION: u32 = 32;
|
||||
|
|
|
@ -58,7 +58,8 @@ pub struct EditorSettings {
|
|||
pub hard_tabs: Option<bool>,
|
||||
pub soft_wrap: Option<SoftWrap>,
|
||||
pub preferred_line_length: Option<u32>,
|
||||
pub format_on_save: Option<FormatOnSave>,
|
||||
pub format_on_save: Option<bool>,
|
||||
pub formatter: Option<Formatter>,
|
||||
pub enable_language_server: Option<bool>,
|
||||
}
|
||||
|
||||
|
@ -72,8 +73,7 @@ pub enum SoftWrap {
|
|||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum FormatOnSave {
|
||||
Off,
|
||||
pub enum Formatter {
|
||||
LanguageServer,
|
||||
External {
|
||||
command: String,
|
||||
|
@ -207,6 +207,7 @@ impl Settings {
|
|||
font_cache: &FontCache,
|
||||
themes: &ThemeRegistry,
|
||||
) -> Self {
|
||||
#[track_caller]
|
||||
fn required<T>(value: Option<T>) -> Option<T> {
|
||||
assert!(value.is_some(), "missing default setting value");
|
||||
value
|
||||
|
@ -236,6 +237,7 @@ impl Settings {
|
|||
soft_wrap: required(defaults.editor.soft_wrap),
|
||||
preferred_line_length: required(defaults.editor.preferred_line_length),
|
||||
format_on_save: required(defaults.editor.format_on_save),
|
||||
formatter: required(defaults.editor.formatter),
|
||||
enable_language_server: required(defaults.editor.enable_language_server),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
|
@ -322,8 +324,12 @@ impl Settings {
|
|||
self.language_setting(language, |settings| settings.preferred_line_length)
|
||||
}
|
||||
|
||||
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
|
||||
self.language_setting(language, |settings| settings.format_on_save.clone())
|
||||
pub fn format_on_save(&self, language: Option<&str>) -> bool {
|
||||
self.language_setting(language, |settings| settings.format_on_save)
|
||||
}
|
||||
|
||||
pub fn formatter(&self, language: Option<&str>) -> Formatter {
|
||||
self.language_setting(language, |settings| settings.formatter.clone())
|
||||
}
|
||||
|
||||
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
||||
|
@ -358,7 +364,8 @@ impl Settings {
|
|||
hard_tabs: Some(false),
|
||||
soft_wrap: Some(SoftWrap::None),
|
||||
preferred_line_length: Some(80),
|
||||
format_on_save: Some(FormatOnSave::LanguageServer),
|
||||
format_on_save: Some(true),
|
||||
formatter: Some(Formatter::LanguageServer),
|
||||
enable_language_server: Some(true),
|
||||
},
|
||||
editor_overrides: Default::default(),
|
||||
|
|
Loading…
Reference in a new issue