Allow creating directories from the project panel

This commit is contained in:
Max Brunsfeld 2022-05-04 16:47:11 -07:00
parent a2c22a5e43
commit 40e0f10195
4 changed files with 166 additions and 57 deletions

View file

@ -1849,7 +1849,9 @@ mod tests {
let entry = project_b let entry = project_b
.update(cx_b, |project, cx| { .update(cx_b, |project, cx| {
project.create_file((worktree_id, "c.txt"), cx).unwrap() project
.create_entry((worktree_id, "c.txt"), false, cx)
.unwrap()
}) })
.await .await
.unwrap(); .unwrap();

View file

@ -690,33 +690,31 @@ impl Project {
.map(|worktree| worktree.read(cx).id()) .map(|worktree| worktree.read(cx).id())
} }
pub fn create_file( pub fn create_entry(
&mut self, &mut self,
project_path: impl Into<ProjectPath>, project_path: impl Into<ProjectPath>,
is_directory: bool,
cx: &mut ModelContext<Self>, cx: &mut ModelContext<Self>,
) -> Option<Task<Result<Entry>>> { ) -> Option<Task<Result<Entry>>> {
let project_path = project_path.into(); let project_path = project_path.into();
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
if self.is_local() { if self.is_local() {
Some(worktree.update(cx, |worktree, cx| { Some(worktree.update(cx, |worktree, cx| {
worktree.as_local_mut().unwrap().write_file( worktree
project_path.path, .as_local_mut()
Default::default(), .unwrap()
cx, .create_entry(project_path.path, is_directory, cx)
)
})) }))
} else { } else {
let client = self.client.clone(); let client = self.client.clone();
let project_id = self.remote_id().unwrap(); let project_id = self.remote_id().unwrap();
Some(cx.spawn_weak(|_, mut cx| async move { Some(cx.spawn_weak(|_, mut cx| async move {
let response = client let response = client
.request(proto::CreateProjectEntry { .request(proto::CreateProjectEntry {
worktree_id: project_path.worktree_id.to_proto(), worktree_id: project_path.worktree_id.to_proto(),
project_id, project_id,
path: project_path.path.as_os_str().as_bytes().to_vec(), path: project_path.path.as_os_str().as_bytes().to_vec(),
is_directory: false, is_directory,
}) })
.await?; .await?;
let entry = response let entry = response

View file

@ -686,32 +686,30 @@ impl LocalWorktree {
}) })
} }
pub fn create_entry(
&self,
path: impl Into<Arc<Path>>,
is_dir: bool,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
self.write_entry_internal(
path,
if is_dir {
None
} else {
Some(Default::default())
},
cx,
)
}
pub fn write_file( pub fn write_file(
&self, &self,
path: impl Into<Arc<Path>>, path: impl Into<Arc<Path>>,
text: Rope, text: Rope,
cx: &mut ModelContext<Worktree>, cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> { ) -> Task<Result<Entry>> {
let path = path.into(); self.write_entry_internal(path, Some(text), cx)
let abs_path = self.absolutize(&path);
let save = cx.background().spawn({
let fs = self.fs.clone();
let abs_path = abs_path.clone();
async move { fs.save(&abs_path, &text).await }
});
cx.spawn(|this, mut cx| async move {
save.await?;
let entry = this
.update(&mut cx, |this, _| {
this.as_local_mut()
.unwrap()
.refresh_entry(path, abs_path, None)
})
.await?;
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
Ok(entry)
})
} }
pub fn rename_entry( pub fn rename_entry(
@ -749,6 +747,40 @@ impl LocalWorktree {
})) }))
} }
fn write_entry_internal(
&self,
path: impl Into<Arc<Path>>,
text_if_file: Option<Rope>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<Entry>> {
let path = path.into();
let abs_path = self.absolutize(&path);
let write = cx.background().spawn({
let fs = self.fs.clone();
let abs_path = abs_path.clone();
async move {
if let Some(text) = text_if_file {
fs.save(&abs_path, &text).await
} else {
fs.create_dir(&abs_path).await
}
}
});
cx.spawn(|this, mut cx| async move {
write.await?;
let entry = this
.update(&mut cx, |this, _| {
this.as_local_mut()
.unwrap()
.refresh_entry(path, abs_path, None)
})
.await?;
this.update(&mut cx, |this, cx| this.poll_snapshot(cx));
Ok(entry)
})
}
fn refresh_entry( fn refresh_entry(
&self, &self,
path: Arc<Path>, path: Arc<Path>,

View file

@ -25,7 +25,7 @@ use workspace::{
Workspace, Workspace,
}; };
const NEW_FILE_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX;
pub struct ProjectPanel { pub struct ProjectPanel {
project: ModelHandle<Project>, project: ModelHandle<Project>,
@ -48,7 +48,8 @@ struct Selection {
struct EditState { struct EditState {
worktree_id: WorktreeId, worktree_id: WorktreeId,
entry_id: ProjectEntryId, entry_id: ProjectEntryId,
new_file: bool, is_new_entry: bool,
is_dir: bool,
processing_filename: Option<String>, processing_filename: Option<String>,
} }
@ -71,7 +72,13 @@ pub struct Open(pub ProjectEntryId);
actions!( actions!(
project_panel, project_panel,
[ExpandSelectedEntry, CollapseSelectedEntry, AddFile, Rename] [
ExpandSelectedEntry,
CollapseSelectedEntry,
AddDirectory,
AddFile,
Rename
]
); );
impl_internal_actions!(project_panel, [Open, ToggleExpanded]); impl_internal_actions!(project_panel, [Open, ToggleExpanded]);
@ -83,6 +90,7 @@ pub fn init(cx: &mut MutableAppContext) {
cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::select_next);
cx.add_action(ProjectPanel::open_entry); cx.add_action(ProjectPanel::open_entry);
cx.add_action(ProjectPanel::add_file); cx.add_action(ProjectPanel::add_file);
cx.add_action(ProjectPanel::add_directory);
cx.add_action(ProjectPanel::rename); cx.add_action(ProjectPanel::rename);
cx.add_async_action(ProjectPanel::confirm); cx.add_async_action(ProjectPanel::confirm);
cx.add_action(ProjectPanel::cancel); cx.add_action(ProjectPanel::cancel);
@ -278,15 +286,15 @@ impl ProjectPanel {
let edit_task; let edit_task;
let edited_entry_id; let edited_entry_id;
if edit_state.new_file { if edit_state.is_new_entry {
self.selection = Some(Selection { self.selection = Some(Selection {
worktree_id, worktree_id,
entry_id: NEW_FILE_ENTRY_ID, entry_id: NEW_ENTRY_ID,
}); });
let new_path = entry.path.join(&filename); let new_path = entry.path.join(&filename);
edited_entry_id = NEW_FILE_ENTRY_ID; edited_entry_id = NEW_ENTRY_ID;
edit_task = self.project.update(cx, |project, cx| { edit_task = self.project.update(cx, |project, cx| {
project.create_file((edit_state.worktree_id, new_path), cx) project.create_entry((edit_state.worktree_id, new_path), edit_state.is_dir, cx)
})?; })?;
} else { } else {
let new_path = if let Some(parent) = entry.path.clone().parent() { let new_path = if let Some(parent) = entry.path.clone().parent() {
@ -332,6 +340,14 @@ impl ProjectPanel {
} }
fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext<Self>) { fn add_file(&mut self, _: &AddFile, cx: &mut ViewContext<Self>) {
self.add_entry(false, cx)
}
fn add_directory(&mut self, _: &AddDirectory, cx: &mut ViewContext<Self>) {
self.add_entry(true, cx)
}
fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
if let Some(Selection { if let Some(Selection {
worktree_id, worktree_id,
entry_id, entry_id,
@ -373,13 +389,14 @@ impl ProjectPanel {
self.edit_state = Some(EditState { self.edit_state = Some(EditState {
worktree_id, worktree_id,
entry_id: directory_id, entry_id: directory_id,
new_file: true, is_new_entry: true,
is_dir,
processing_filename: None, processing_filename: None,
}); });
self.filename_editor self.filename_editor
.update(cx, |editor, cx| editor.clear(cx)); .update(cx, |editor, cx| editor.clear(cx));
cx.focus(&self.filename_editor); cx.focus(&self.filename_editor);
self.update_visible_entries(None, cx); self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx);
cx.notify(); cx.notify();
} }
} }
@ -395,7 +412,8 @@ impl ProjectPanel {
self.edit_state = Some(EditState { self.edit_state = Some(EditState {
worktree_id, worktree_id,
entry_id, entry_id,
new_file: false, is_new_entry: false,
is_dir: entry.is_dir(),
processing_filename: None, processing_filename: None,
}); });
let filename = entry let filename = entry
@ -526,22 +544,27 @@ impl ProjectPanel {
} }
}; };
let new_file_parent_id = self.edit_state.as_ref().and_then(|edit_state| { let mut new_entry_parent_id = None;
if edit_state.worktree_id == worktree_id && edit_state.new_file { let mut new_entry_kind = EntryKind::Dir;
Some(edit_state.entry_id) if let Some(edit_state) = &self.edit_state {
} else { if edit_state.worktree_id == worktree_id && edit_state.is_new_entry {
None new_entry_parent_id = Some(edit_state.entry_id);
new_entry_kind = if edit_state.is_dir {
EntryKind::Dir
} else {
EntryKind::File(Default::default())
};
} }
}); }
let mut visible_worktree_entries = Vec::new(); let mut visible_worktree_entries = Vec::new();
let mut entry_iter = snapshot.entries(false); let mut entry_iter = snapshot.entries(false);
while let Some(entry) = entry_iter.entry() { while let Some(entry) = entry_iter.entry() {
visible_worktree_entries.push(entry.clone()); visible_worktree_entries.push(entry.clone());
if Some(entry.id) == new_file_parent_id { if Some(entry.id) == new_entry_parent_id {
visible_worktree_entries.push(Entry { visible_worktree_entries.push(Entry {
id: NEW_FILE_ENTRY_ID, id: NEW_ENTRY_ID,
kind: project::EntryKind::File(Default::default()), kind: new_entry_kind,
path: entry.path.join("\0").into(), path: entry.path.join("\0").into(),
inode: 0, inode: 0,
mtime: entry.mtime, mtime: entry.mtime,
@ -669,8 +692,8 @@ impl ProjectPanel {
is_processing: false, is_processing: false,
}; };
if let Some(edit_state) = &self.edit_state { if let Some(edit_state) = &self.edit_state {
let is_edited_entry = if edit_state.new_file { let is_edited_entry = if edit_state.is_new_entry {
entry.id == NEW_FILE_ENTRY_ID entry.id == NEW_ENTRY_ID
} else { } else {
entry.id == edit_state.entry_id entry.id == edit_state.entry_id
}; };
@ -680,7 +703,7 @@ impl ProjectPanel {
details.filename.clear(); details.filename.clear();
details.filename.push_str(&processing_filename); details.filename.push_str(&processing_filename);
} else { } else {
if edit_state.new_file { if edit_state.is_new_entry {
details.filename.clear(); details.filename.clear();
} }
details.is_editing = true; details.is_editing = true;
@ -983,11 +1006,11 @@ mod tests {
assert_eq!( assert_eq!(
visible_entries_as_strings(&panel, 0..10, cx), visible_entries_as_strings(&panel, 0..10, cx),
&[ &[
"v root1 <== selected", "v root1",
" > a", " > a",
" > b", " > b",
" > C", " > C",
" [EDITOR: '']", " [EDITOR: ''] <== selected",
" .dockerignore", " .dockerignore",
"v root2", "v root2",
" > d", " > d",
@ -1039,10 +1062,10 @@ mod tests {
&[ &[
"v root1", "v root1",
" > a", " > a",
" v b <== selected", " v b",
" > 3", " > 3",
" > 4", " > 4",
" [EDITOR: '']", " [EDITOR: ''] <== selected",
" > C", " > C",
" .dockerignore", " .dockerignore",
" the-new-filename", " the-new-filename",
@ -1126,6 +1149,60 @@ mod tests {
" the-new-filename", " the-new-filename",
] ]
); );
panel.update(cx, |panel, cx| panel.add_directory(&AddDirectory, cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..9, cx),
&[
"v root1",
" > a",
" v b",
" > [EDITOR: ''] <== selected",
" > 3",
" > 4",
" a-different-filename",
" > C",
" .dockerignore",
]
);
let confirm = panel.update(cx, |panel, cx| {
panel
.filename_editor
.update(cx, |editor, cx| editor.set_text("new-dir", cx));
panel.confirm(&Confirm, cx).unwrap()
});
panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx));
assert_eq!(
visible_entries_as_strings(&panel, 0..9, cx),
&[
"v root1",
" > a",
" v b",
" > [PROCESSING: 'new-dir']",
" > 3 <== selected",
" > 4",
" a-different-filename",
" > C",
" .dockerignore",
]
);
confirm.await.unwrap();
assert_eq!(
visible_entries_as_strings(&panel, 0..9, cx),
&[
"v root1",
" > a",
" v b",
" > 3 <== selected",
" > 4",
" > new-dir",
" a-different-filename",
" > C",
" .dockerignore",
]
);
} }
fn toggle_expand_dir( fn toggle_expand_dir(
@ -1192,7 +1269,7 @@ mod tests {
} }
let indent = " ".repeat(details.depth); let indent = " ".repeat(details.depth);
let icon = if details.kind == EntryKind::Dir { let icon = if matches!(details.kind, EntryKind::Dir | EntryKind::PendingDir) {
if details.is_expanded { if details.is_expanded {
"v " "v "
} else { } else {