mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-29 04:20:46 +00:00
Merge pull request #1302 from zed-industries/external-formatting
Introduce support for formatting via an external command
This commit is contained in:
commit
627d067e57
8 changed files with 254 additions and 130 deletions
|
@ -549,7 +549,7 @@ impl Client {
|
|||
client.respond_with_error(
|
||||
receipt,
|
||||
proto::Error {
|
||||
message: error.to_string(),
|
||||
message: format!("{:?}", error),
|
||||
},
|
||||
)?;
|
||||
Err(error)
|
||||
|
|
|
@ -35,7 +35,7 @@ use project::{
|
|||
use rand::prelude::*;
|
||||
use rpc::PeerId;
|
||||
use serde_json::json;
|
||||
use settings::Settings;
|
||||
use settings::{FormatOnSave, Settings};
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
|
@ -1912,7 +1912,6 @@ 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) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
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;
|
||||
|
@ -1932,11 +1931,15 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
|||
let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default());
|
||||
client_a.language_registry.add(Arc::new(language));
|
||||
|
||||
// Here we insert a fake tree with a directory that exists on disk. This is needed
|
||||
// because later we'll invoke a command, which requires passing a working directory
|
||||
// that points to a valid location on disk.
|
||||
let directory = env::current_dir().unwrap();
|
||||
client_a
|
||||
.fs
|
||||
.insert_tree("/a", json!({ "a.rs": "let one = two" }))
|
||||
.insert_tree(&directory, json!({ "a.rs": "let one = \"two\"" }))
|
||||
.await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project("/a", cx_a).await;
|
||||
let (project_a, worktree_id) = client_a.build_local_project(&directory, cx_a).await;
|
||||
let project_b = client_b.build_remote_project(&project_a, cx_a, cx_b).await;
|
||||
|
||||
let buffer_b = cx_b
|
||||
|
@ -1967,7 +1970,28 @@ async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppCon
|
|||
.unwrap();
|
||||
assert_eq!(
|
||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||
"let honey = two"
|
||||
"let honey = \"two\""
|
||||
);
|
||||
|
||||
// Ensure buffer can be formatted using an external command. Notice how the
|
||||
// host's configuration is honored as opposed to using the guest's settings.
|
||||
cx_a.update(|cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.language_settings.format_on_save = Some(FormatOnSave::External {
|
||||
command: "awk".to_string(),
|
||||
arguments: vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()],
|
||||
});
|
||||
});
|
||||
});
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.format(HashSet::from_iter([buffer_b.clone()]), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
buffer_b.read_with(cx_b, |buffer, _| buffer.text()),
|
||||
format!("let honey = \"{}/a.rs\"\n", directory.to_str().unwrap())
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -352,13 +352,8 @@ impl Item for Editor {
|
|||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let settings = cx.global::<Settings>();
|
||||
let buffer = self.buffer().clone();
|
||||
let mut 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 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));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
|
|
|
@ -273,7 +273,7 @@ pub struct Chunk<'a> {
|
|||
pub is_unnecessary: bool,
|
||||
}
|
||||
|
||||
pub(crate) struct Diff {
|
||||
pub struct Diff {
|
||||
base_version: clock::Global,
|
||||
new_text: Arc<str>,
|
||||
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 base_version = self.version();
|
||||
cx.background().spawn(async move {
|
||||
|
@ -979,11 +979,7 @@ impl Buffer {
|
|||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff(
|
||||
&mut self,
|
||||
diff: Diff,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&Transaction> {
|
||||
pub fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> Option<&Transaction> {
|
||||
if self.version == diff.base_version {
|
||||
self.finalize_last_transaction();
|
||||
self.start_transaction();
|
||||
|
|
|
@ -334,28 +334,6 @@ impl FakeFs {
|
|||
})
|
||||
}
|
||||
|
||||
pub async fn insert_dir(&self, path: impl AsRef<Path>) {
|
||||
let mut state = self.state.lock().await;
|
||||
let path = path.as_ref();
|
||||
state.validate_path(path).unwrap();
|
||||
|
||||
let inode = state.next_inode;
|
||||
state.next_inode += 1;
|
||||
state.entries.insert(
|
||||
path.to_path_buf(),
|
||||
FakeFsEntry {
|
||||
metadata: Metadata {
|
||||
inode,
|
||||
mtime: SystemTime::now(),
|
||||
is_dir: true,
|
||||
is_symlink: false,
|
||||
},
|
||||
content: None,
|
||||
},
|
||||
);
|
||||
state.emit_event(&[path]).await;
|
||||
}
|
||||
|
||||
pub async fn insert_file(&self, path: impl AsRef<Path>, content: String) {
|
||||
let mut state = self.state.lock().await;
|
||||
let path = path.as_ref();
|
||||
|
@ -392,7 +370,7 @@ impl FakeFs {
|
|||
|
||||
match tree {
|
||||
Object(map) => {
|
||||
self.insert_dir(path).await;
|
||||
self.create_dir(path).await.unwrap();
|
||||
for (name, contents) in map {
|
||||
let mut path = PathBuf::from(path);
|
||||
path.push(name);
|
||||
|
@ -400,7 +378,7 @@ impl FakeFs {
|
|||
}
|
||||
}
|
||||
Null => {
|
||||
self.insert_dir(&path).await;
|
||||
self.create_dir(&path).await.unwrap();
|
||||
}
|
||||
String(contents) => {
|
||||
self.insert_file(&path, contents).await;
|
||||
|
|
|
@ -12,7 +12,7 @@ use anyhow::{anyhow, Context, Result};
|
|||
use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
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 gpui::{
|
||||
AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle,
|
||||
|
@ -51,10 +51,12 @@ use std::{
|
|||
ffi::OsString,
|
||||
hash::Hash,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::Range,
|
||||
os::unix::{ffi::OsStrExt, prelude::OsStringExt},
|
||||
path::{Component, Path, PathBuf},
|
||||
rc::Rc,
|
||||
str,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst},
|
||||
Arc,
|
||||
|
@ -3025,78 +3027,50 @@ impl Project {
|
|||
}
|
||||
|
||||
for (buffer, buffer_abs_path, language_server) in local_buffers {
|
||||
let text_document = lsp::TextDocumentIdentifier::new(
|
||||
lsp::Url::from_file_path(&buffer_abs_path).unwrap(),
|
||||
);
|
||||
let capabilities = &language_server.capabilities();
|
||||
let tab_size = cx.update(|cx| {
|
||||
let language_name = buffer.read(cx).language().map(|language| language.name());
|
||||
cx.global::<Settings>().tab_size(language_name.as_deref())
|
||||
let (format_on_save, 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.tab_size(language_name.as_deref()),
|
||||
)
|
||||
});
|
||||
let lsp_edits = if capabilities
|
||||
.document_formatting_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||
{
|
||||
language_server
|
||||
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
||||
text_document,
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await?
|
||||
} else if capabilities
|
||||
.document_range_formatting_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||
{
|
||||
let buffer_start = lsp::Position::new(0, 0);
|
||||
let buffer_end =
|
||||
buffer.read_with(&cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
|
||||
language_server
|
||||
.request::<lsp::request::RangeFormatting>(
|
||||
lsp::DocumentRangeFormattingParams {
|
||||
text_document,
|
||||
range: lsp::Range::new(buffer_start, buffer_end),
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
},
|
||||
|
||||
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?
|
||||
} else {
|
||||
continue;
|
||||
.await
|
||||
.context(format!(
|
||||
"failed to format via external command {:?}",
|
||||
command
|
||||
))?
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(lsp_edits) = lsp_edits {
|
||||
let edits = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
||||
})
|
||||
.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], cx);
|
||||
}
|
||||
if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
if !push_to_history {
|
||||
buffer.forget_transaction(transaction.id);
|
||||
}
|
||||
project_transaction.0.insert(cx.handle(), transaction);
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3104,6 +3078,141 @@ impl Project {
|
|||
})
|
||||
}
|
||||
|
||||
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
|
||||
.document_formatting_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||
{
|
||||
language_server
|
||||
.request::<lsp::request::Formatting>(lsp::DocumentFormattingParams {
|
||||
text_document,
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await?
|
||||
} else if capabilities
|
||||
.document_range_formatting_provider
|
||||
.as_ref()
|
||||
.map_or(false, |provider| *provider != lsp::OneOf::Left(false))
|
||||
{
|
||||
let buffer_start = lsp::Position::new(0, 0);
|
||||
let buffer_end =
|
||||
buffer.read_with(cx, |buffer, _| point_to_lsp(buffer.max_point_utf16()));
|
||||
language_server
|
||||
.request::<lsp::request::RangeFormatting>(lsp::DocumentRangeFormattingParams {
|
||||
text_document,
|
||||
range: lsp::Range::new(buffer_start, buffer_end),
|
||||
options: lsp::FormattingOptions {
|
||||
tab_size: tab_size.into(),
|
||||
insert_spaces: true,
|
||||
insert_final_newline: Some(true),
|
||||
..Default::default()
|
||||
},
|
||||
work_done_progress_params: Default::default(),
|
||||
})
|
||||
.await?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(lsp_edits) = lsp_edits {
|
||||
let edits = this
|
||||
.update(cx, |this, cx| {
|
||||
this.edits_from_lsp(&buffer, lsp_edits, None, cx)
|
||||
})
|
||||
.await?;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.start_transaction();
|
||||
for (range, text) in edits {
|
||||
buffer.edit([(range, text)], cx);
|
||||
}
|
||||
if buffer.end_transaction(cx).is_some() {
|
||||
let transaction = buffer.finalize_last_transaction().unwrap().clone();
|
||||
Ok(Some(transaction))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
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>(
|
||||
&self,
|
||||
buffer: &ModelHandle<Buffer>,
|
||||
|
|
|
@ -38,7 +38,7 @@ pub struct LanguageSettings {
|
|||
pub hard_tabs: Option<bool>,
|
||||
pub soft_wrap: Option<SoftWrap>,
|
||||
pub preferred_line_length: Option<u32>,
|
||||
pub format_on_save: Option<bool>,
|
||||
pub format_on_save: Option<FormatOnSave>,
|
||||
pub enable_language_server: Option<bool>,
|
||||
}
|
||||
|
||||
|
@ -50,6 +50,17 @@ pub enum SoftWrap {
|
|||
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)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum Autosave {
|
||||
|
@ -72,7 +83,7 @@ pub struct SettingsFileContent {
|
|||
#[serde(default)]
|
||||
pub vim_mode: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub format_on_save: Option<bool>,
|
||||
pub format_on_save: Option<FormatOnSave>,
|
||||
#[serde(default)]
|
||||
pub autosave: Option<Autosave>,
|
||||
#[serde(default)]
|
||||
|
@ -136,9 +147,9 @@ impl Settings {
|
|||
.unwrap_or(80)
|
||||
}
|
||||
|
||||
pub fn format_on_save(&self, language: Option<&str>) -> bool {
|
||||
self.language_setting(language, |settings| settings.format_on_save)
|
||||
.unwrap_or(true)
|
||||
pub fn format_on_save(&self, language: Option<&str>) -> FormatOnSave {
|
||||
self.language_setting(language, |settings| settings.format_on_save.clone())
|
||||
.unwrap_or(FormatOnSave::LanguageServer)
|
||||
}
|
||||
|
||||
pub fn enable_language_server(&self, language: Option<&str>) -> bool {
|
||||
|
@ -215,7 +226,7 @@ impl Settings {
|
|||
merge(&mut self.autosave, data.autosave);
|
||||
merge_option(
|
||||
&mut self.language_settings.format_on_save,
|
||||
data.format_on_save,
|
||||
data.format_on_save.clone(),
|
||||
);
|
||||
merge_option(
|
||||
&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() {
|
||||
*target = value;
|
||||
}
|
||||
|
|
|
@ -554,7 +554,7 @@ mod tests {
|
|||
});
|
||||
|
||||
let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx));
|
||||
app_state.fs.as_fake().insert_dir("/root").await;
|
||||
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||
cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/the-new-name")));
|
||||
save_task.await.unwrap();
|
||||
editor.read_with(cx, |editor, cx| {
|
||||
|
@ -680,14 +680,25 @@ mod tests {
|
|||
async fn test_open_paths(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
|
||||
let fs = app_state.fs.as_fake();
|
||||
fs.insert_dir("/dir1").await;
|
||||
fs.insert_dir("/dir2").await;
|
||||
fs.insert_dir("/dir3").await;
|
||||
fs.insert_file("/dir1/a.txt", "".into()).await;
|
||||
fs.insert_file("/dir2/b.txt", "".into()).await;
|
||||
fs.insert_file("/dir3/c.txt", "".into()).await;
|
||||
fs.insert_file("/d.txt", "".into()).await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.insert_tree(
|
||||
"/",
|
||||
json!({
|
||||
"dir1": {
|
||||
"a.txt": ""
|
||||
},
|
||||
"dir2": {
|
||||
"b.txt": ""
|
||||
},
|
||||
"dir3": {
|
||||
"c.txt": ""
|
||||
},
|
||||
"d.txt": ""
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
|
||||
let (_, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
@ -891,7 +902,7 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_open_and_save_new_file(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
app_state.fs.as_fake().insert_dir("/root").await;
|
||||
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
|
@ -980,7 +991,7 @@ mod tests {
|
|||
#[gpui::test]
|
||||
async fn test_setting_language_when_saving_as_single_file_worktree(cx: &mut TestAppContext) {
|
||||
let app_state = init(cx);
|
||||
app_state.fs.as_fake().insert_dir("/root").await;
|
||||
app_state.fs.create_dir(Path::new("/root")).await.unwrap();
|
||||
|
||||
let project = Project::test(app_state.fs.clone(), [], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(rust_lang()));
|
||||
|
|
Loading…
Reference in a new issue