Allow creating directories from the project panel
This commit is contained in:
parent
a2c22a5e43
commit
40e0f10195
4 changed files with 166 additions and 57 deletions
|
@ -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();
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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>,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue