Cargo.lock 🔗
@@ -3815,6 +3815,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
+ "futures 0.3.28",
"fuzzy",
"gpui",
"itertools 0.11.0",
Conrad Irwin , Nathan , and Bennet created
Still TODO:
* Disable the new save-as for local projects
* Wire up sending the new path to the remote server
Release Notes:
- Added the ability to "Save-as" in remote projects
---------
Co-authored-by: Nathan <nathan@zed.dev>
Co-authored-by: Bennet <bennetbo@gmx.de>
Cargo.lock | 1
README.md | 51 -
crates/collab/src/tests/dev_server_tests.rs | 32 +
crates/collab/src/tests/integration_tests.rs | 7
crates/diagnostics/src/diagnostics.rs | 3
crates/diagnostics/src/diagnostics_tests.rs | 5
crates/editor/src/items.rs | 11
crates/file_finder/Cargo.toml | 1
crates/file_finder/src/file_finder.rs | 5
crates/file_finder/src/new_path_prompt.rs | 463 +++++++++++++++
crates/gpui/src/platform.rs | 6
crates/gpui/src/platform/mac/window.rs | 9
crates/gpui/src/platform/windows/window.rs | 2
crates/gpui/src/style.rs | 7
crates/picker/src/picker.rs | 30
crates/project/src/project.rs | 70 +
crates/project/src/project_tests.rs | 7
crates/project_panel/src/project_panel.rs | 2
crates/recent_projects/src/remote_projects.rs | 2
crates/rpc/proto/zed.proto | 6
crates/search/src/project_search.rs | 6
crates/ui/src/components/label/highlighted_label.rs | 65 +
crates/workspace/src/item.rs | 11
crates/workspace/src/notifications.rs | 2
crates/workspace/src/pane.rs | 14
crates/workspace/src/workspace.rs | 59 +
crates/worktree/src/worktree.rs | 47 +
27 files changed, 775 insertions(+), 149 deletions(-)
@@ -3815,6 +3815,7 @@ dependencies = [
"ctor",
"editor",
"env_logger",
+ "futures 0.3.28",
"fuzzy",
"gpui",
"itertools 0.11.0",
@@ -1,51 +0,0 @@
-# Zed
-
-[](https://github.com/zed-industries/zed/actions/workflows/ci.yml)
-
-Welcome to Zed, a high-performance, multiplayer code editor from the creators of [Atom](https://github.com/atom/atom) and [Tree-sitter](https://github.com/tree-sitter/tree-sitter).
-
-## Installation
-
-You can [download](https://zed.dev/download) Zed today for macOS (v10.15+).
-
-Support for additional platforms is on our [roadmap](https://zed.dev/roadmap):
-
-- Linux ([tracking issue](https://github.com/zed-industries/zed/issues/7015))
-- Windows ([tracking issue](https://github.com/zed-industries/zed/issues/5394))
-- Web ([tracking issue](https://github.com/zed-industries/zed/issues/5396))
-
-For macOS users, you can also install Zed using [Homebrew](https://brew.sh/):
-
-```sh
-brew install --cask zed
-```
-
-Alternatively, to install the Preview release:
-
-```sh
-brew tap homebrew/cask-versions
-brew install zed-preview
-```
-
-## Developing Zed
-
-- [Building Zed for macOS](./docs/src/developing_zed__building_zed_macos.md)
-- [Building Zed for Linux](./docs/src/developing_zed__building_zed_linux.md)
-- [Building Zed for Windows](./docs/src/developing_zed__building_zed_windows.md)
-- [Running Collaboration Locally](./docs/src/developing_zed__local_collaboration.md)
-
-## Contributing
-
-See [CONTRIBUTING.md](./CONTRIBUTING.md) for ways you can contribute to Zed.
-
-Also... we're hiring! Check out our [jobs](https://zed.dev/jobs) page for open roles.
-
-## Licensing
-
-License information for third party dependencies must be correctly provided for CI to pass.
-
-We use [`cargo-about`](https://github.com/EmbarkStudios/cargo-about) to automatically comply with open source licenses. If CI is failing, check the following:
-
-- Is it showing a `no license specified` error for a crate you've created? If so, add `publish = false` under `[package]` in your crate's Cargo.toml.
-- Is the error `failed to satisfy license requirements` for a dependency? If so, first determine what license the project has and whether this system is sufficient to comply with this license's requirements. If you're unsure, ask a lawyer. Once you've verified that this system is acceptable add the license's SPDX identifier to the `accepted` array in `script/licenses/zed-licenses.toml`.
-- Is `cargo-about` unable to find the license for a dependency? If so, add a clarification field at the end of `script/licenses/zed-licenses.toml`, as specified in the [cargo-about book](https://embarkstudios.github.io/cargo-about/cli/generate/config.html#crate-configuration).
@@ -366,3 +366,35 @@ async fn test_create_remote_project_path_validation(
ErrorCode::RemoteProjectPathDoesNotExist
));
}
+
+#[gpui::test]
+async fn test_save_as_remote(cx1: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppContext) {
+ let (server, client1) = TestServer::start1(cx1).await;
+
+ // Creating a project with a path that does exist should not fail
+ let (dev_server, remote_workspace) =
+ create_remote_project(&server, client1.app_state.clone(), cx1, cx2).await;
+
+ let mut cx = VisualTestContext::from_window(remote_workspace.into(), cx1);
+
+ cx.simulate_keystrokes("cmd-p 1 enter");
+ cx.simulate_keystrokes("cmd-shift-s");
+ cx.simulate_input("2.txt");
+ cx.simulate_keystrokes("enter");
+
+ cx.executor().run_until_parked();
+
+ let title = remote_workspace
+ .update(&mut cx, |ws, cx| {
+ ws.active_item(cx).unwrap().tab_description(0, &cx).unwrap()
+ })
+ .unwrap();
+
+ assert_eq!(title, "2.txt");
+
+ let path = Path::new("/remote/2.txt");
+ assert_eq!(
+ dev_server.fs().load(&path).await.unwrap(),
+ "remote\nremote\nremote"
+ );
+}
@@ -2468,7 +2468,12 @@ async fn test_propagate_saves_and_fs_changes(
});
project_a
.update(cx_a, |project, cx| {
- project.save_buffer_as(new_buffer_a.clone(), "/a/file3.rs".into(), cx)
+ let path = ProjectPath {
+ path: Arc::from(Path::new("file3.rs")),
+ worktree_id: worktree_a.read(cx).id(),
+ };
+
+ project.save_buffer_as(new_buffer_a.clone(), path, cx)
})
.await
.unwrap();
@@ -36,7 +36,6 @@ use std::{
cmp::Ordering,
mem,
ops::Range,
- path::PathBuf,
};
use theme::ActiveTheme;
pub use toolbar_controls::ToolbarControls;
@@ -740,7 +739,7 @@ impl Item for ProjectDiagnosticsEditor {
fn save_as(
&mut self,
_: Model<Project>,
- _: PathBuf,
+ _: ProjectPath,
_: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unreachable!()
@@ -13,7 +13,10 @@ use project::FakeFs;
use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng};
use serde_json::json;
use settings::SettingsStore;
-use std::{env, path::Path};
+use std::{
+ env,
+ path::{Path, PathBuf},
+};
use unindent::Unindent as _;
use util::{post_inc, RandomCharIter};
@@ -26,7 +26,7 @@ use std::{
cmp::{self, Ordering},
iter,
ops::Range,
- path::{Path, PathBuf},
+ path::Path,
sync::Arc,
};
use text::{BufferId, Selection};
@@ -750,7 +750,7 @@ impl Item for Editor {
fn save_as(
&mut self,
project: Model<Project>,
- abs_path: PathBuf,
+ path: ProjectPath,
cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
let buffer = self
@@ -759,14 +759,13 @@ impl Item for Editor {
.as_singleton()
.expect("cannot call save_as on an excerpt list");
- let file_extension = abs_path
+ let file_extension = path
+ .path
.extension()
.map(|a| a.to_string_lossy().to_string());
self.report_editor_event("save", file_extension, cx);
- project.update(cx, |project, cx| {
- project.save_buffer_as(buffer, abs_path, cx)
- })
+ project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
}
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
@@ -16,6 +16,7 @@ doctest = false
anyhow.workspace = true
collections.workspace = true
editor.workspace = true
+futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
itertools = "0.11"
@@ -1,6 +1,8 @@
#[cfg(test)]
mod file_finder_tests;
+mod new_path_prompt;
+
use collections::{HashMap, HashSet};
use editor::{scroll::Autoscroll, Bias, Editor};
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
@@ -10,6 +12,7 @@ use gpui::{
ViewContext, VisualContext, WeakView,
};
use itertools::Itertools;
+use new_path_prompt::NewPathPrompt;
use picker::{Picker, PickerDelegate};
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
use settings::Settings;
@@ -37,6 +40,7 @@ pub struct FileFinder {
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(FileFinder::register).detach();
+ cx.observe_new_views(NewPathPrompt::register).detach();
}
impl FileFinder {
@@ -454,6 +458,7 @@ impl FileFinderDelegate {
.root_entry()
.map_or(false, |entry| entry.is_ignored),
include_root_name,
+ directories_only: false,
}
})
.collect::<Vec<_>>();
@@ -0,0 +1,463 @@
+use futures::channel::oneshot;
+use fuzzy::PathMatch;
+use gpui::{HighlightStyle, Model, StyledText};
+use picker::{Picker, PickerDelegate};
+use project::{Entry, PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
+use std::{
+ path::PathBuf,
+ sync::{
+ atomic::{self, AtomicBool},
+ Arc,
+ },
+};
+use ui::{highlight_ranges, prelude::*, LabelLike, ListItemSpacing};
+use ui::{ListItem, ViewContext};
+use util::ResultExt;
+use workspace::Workspace;
+
+pub(crate) struct NewPathPrompt;
+
+#[derive(Debug, Clone)]
+struct Match {
+ path_match: Option<PathMatch>,
+ suffix: Option<String>,
+}
+
+impl Match {
+ fn entry<'a>(&'a self, project: &'a Project, cx: &'a WindowContext) -> Option<&'a Entry> {
+ if let Some(suffix) = &self.suffix {
+ let (worktree, path) = if let Some(path_match) = &self.path_match {
+ (
+ project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx),
+ path_match.path.join(suffix),
+ )
+ } else {
+ (project.worktrees().next(), PathBuf::from(suffix))
+ };
+
+ worktree.and_then(|worktree| worktree.read(cx).entry_for_path(path))
+ } else if let Some(path_match) = &self.path_match {
+ let worktree =
+ project.worktree_for_id(WorktreeId::from_usize(path_match.worktree_id), cx)?;
+ worktree.read(cx).entry_for_path(path_match.path.as_ref())
+ } else {
+ None
+ }
+ }
+
+ fn is_dir(&self, project: &Project, cx: &WindowContext) -> bool {
+ self.entry(project, cx).is_some_and(|e| e.is_dir())
+ || self.suffix.as_ref().is_some_and(|s| s.ends_with('/'))
+ }
+
+ fn relative_path(&self) -> String {
+ if let Some(path_match) = &self.path_match {
+ if let Some(suffix) = &self.suffix {
+ format!(
+ "{}/{}",
+ path_match.path.to_string_lossy(),
+ suffix.trim_end_matches('/')
+ )
+ } else {
+ path_match.path.to_string_lossy().to_string()
+ }
+ } else if let Some(suffix) = &self.suffix {
+ suffix.trim_end_matches('/').to_string()
+ } else {
+ "".to_string()
+ }
+ }
+
+ fn project_path(&self, project: &Project, cx: &WindowContext) -> Option<ProjectPath> {
+ let worktree_id = if let Some(path_match) = &self.path_match {
+ WorktreeId::from_usize(path_match.worktree_id)
+ } else {
+ project.worktrees().next()?.read(cx).id()
+ };
+
+ let path = PathBuf::from(self.relative_path());
+
+ Some(ProjectPath {
+ worktree_id,
+ path: Arc::from(path),
+ })
+ }
+
+ fn existing_prefix(&self, project: &Project, cx: &WindowContext) -> Option<PathBuf> {
+ let worktree = project.worktrees().next()?.read(cx);
+ let mut prefix = PathBuf::new();
+ let parts = self.suffix.as_ref()?.split('/');
+ for part in parts {
+ if worktree.entry_for_path(prefix.join(&part)).is_none() {
+ return Some(prefix);
+ }
+ prefix = prefix.join(part);
+ }
+
+ None
+ }
+
+ fn styled_text(&self, project: &Project, cx: &WindowContext) -> StyledText {
+ let mut text = "./".to_string();
+ let mut highlights = Vec::new();
+ let mut offset = text.as_bytes().len();
+
+ let separator = '/';
+ let dir_indicator = "[…]";
+
+ if let Some(path_match) = &self.path_match {
+ text.push_str(&path_match.path.to_string_lossy());
+ for (range, style) in highlight_ranges(
+ &path_match.path.to_string_lossy(),
+ &path_match.positions,
+ gpui::HighlightStyle::color(Color::Accent.color(cx)),
+ ) {
+ highlights.push((range.start + offset..range.end + offset, style))
+ }
+ text.push(separator);
+ offset = text.as_bytes().len();
+
+ if let Some(suffix) = &self.suffix {
+ text.push_str(suffix);
+ let entry = self.entry(project, cx);
+ let color = if let Some(entry) = entry {
+ if entry.is_dir() {
+ Color::Accent
+ } else {
+ Color::Conflict
+ }
+ } else {
+ Color::Created
+ };
+ highlights.push((
+ offset..offset + suffix.as_bytes().len(),
+ HighlightStyle::color(color.color(cx)),
+ ));
+ offset += suffix.as_bytes().len();
+ if entry.is_some_and(|e| e.is_dir()) {
+ text.push(separator);
+ offset += separator.len_utf8();
+
+ text.push_str(dir_indicator);
+ highlights.push((
+ offset..offset + dir_indicator.bytes().len(),
+ HighlightStyle::color(Color::Muted.color(cx)),
+ ));
+ }
+ } else {
+ text.push_str(dir_indicator);
+ highlights.push((
+ offset..offset + dir_indicator.bytes().len(),
+ HighlightStyle::color(Color::Muted.color(cx)),
+ ))
+ }
+ } else if let Some(suffix) = &self.suffix {
+ text.push_str(suffix);
+ let existing_prefix_len = self
+ .existing_prefix(project, cx)
+ .map(|prefix| prefix.to_string_lossy().as_bytes().len())
+ .unwrap_or(0);
+
+ if existing_prefix_len > 0 {
+ highlights.push((
+ offset..offset + existing_prefix_len,
+ HighlightStyle::color(Color::Accent.color(cx)),
+ ));
+ }
+ highlights.push((
+ offset + existing_prefix_len..offset + suffix.as_bytes().len(),
+ HighlightStyle::color(if self.entry(project, cx).is_some() {
+ Color::Conflict.color(cx)
+ } else {
+ Color::Created.color(cx)
+ }),
+ ));
+ offset += suffix.as_bytes().len();
+ if suffix.ends_with('/') {
+ text.push_str(dir_indicator);
+ highlights.push((
+ offset..offset + dir_indicator.bytes().len(),
+ HighlightStyle::color(Color::Muted.color(cx)),
+ ));
+ }
+ }
+
+ StyledText::new(text).with_highlights(&cx.text_style().clone(), highlights)
+ }
+}
+
+pub struct NewPathDelegate {
+ project: Model<Project>,
+ tx: Option<oneshot::Sender<Option<ProjectPath>>>,
+ selected_index: usize,
+ matches: Vec<Match>,
+ last_selected_dir: Option<String>,
+ cancel_flag: Arc<AtomicBool>,
+ should_dismiss: bool,
+}
+
+impl NewPathPrompt {
+ pub(crate) fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
+ if workspace.project().read(cx).is_remote() {
+ workspace.set_prompt_for_new_path(Box::new(|workspace, cx| {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ Self::prompt_for_new_path(workspace, tx, cx);
+ rx
+ }));
+ }
+ }
+
+ fn prompt_for_new_path(
+ workspace: &mut Workspace,
+ tx: oneshot::Sender<Option<ProjectPath>>,
+ cx: &mut ViewContext<Workspace>,
+ ) {
+ let project = workspace.project().clone();
+ workspace.toggle_modal(cx, |cx| {
+ let delegate = NewPathDelegate {
+ project,
+ tx: Some(tx),
+ selected_index: 0,
+ matches: vec![],
+ cancel_flag: Arc::new(AtomicBool::new(false)),
+ last_selected_dir: None,
+ should_dismiss: true,
+ };
+
+ Picker::uniform_list(delegate, cx).width(rems(34.))
+ });
+ }
+}
+
+impl PickerDelegate for NewPathDelegate {
+ type ListItem = ui::ListItem;
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<picker::Picker<Self>>) {
+ self.selected_index = ix;
+ cx.notify();
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<picker::Picker<Self>>,
+ ) -> gpui::Task<()> {
+ let query = query.trim().trim_start_matches('/');
+ let (dir, suffix) = if let Some(index) = query.rfind('/') {
+ let suffix = if index + 1 < query.len() {
+ Some(query[index + 1..].to_string())
+ } else {
+ None
+ };
+ (query[0..index].to_string(), suffix)
+ } else {
+ (query.to_string(), None)
+ };
+
+ let worktrees = self
+ .project
+ .read(cx)
+ .visible_worktrees(cx)
+ .collect::<Vec<_>>();
+ let include_root_name = worktrees.len() > 1;
+ let candidate_sets = worktrees
+ .into_iter()
+ .map(|worktree| {
+ let worktree = worktree.read(cx);
+ PathMatchCandidateSet {
+ snapshot: worktree.snapshot(),
+ include_ignored: worktree
+ .root_entry()
+ .map_or(false, |entry| entry.is_ignored),
+ include_root_name,
+ directories_only: true,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ self.cancel_flag.store(true, atomic::Ordering::Relaxed);
+ self.cancel_flag = Arc::new(AtomicBool::new(false));
+
+ let cancel_flag = self.cancel_flag.clone();
+ let query = query.to_string();
+ let prefix = dir.clone();
+ cx.spawn(|picker, mut cx| async move {
+ let matches = fuzzy::match_path_sets(
+ candidate_sets.as_slice(),
+ &dir,
+ None,
+ false,
+ 100,
+ &cancel_flag,
+ cx.background_executor().clone(),
+ )
+ .await;
+ let did_cancel = cancel_flag.load(atomic::Ordering::Relaxed);
+ if did_cancel {
+ return;
+ }
+ picker
+ .update(&mut cx, |picker, cx| {
+ picker
+ .delegate
+ .set_search_matches(query, prefix, suffix, matches, cx)
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm_update_query(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
+ let m = self.matches.get(self.selected_index)?;
+ if m.is_dir(self.project.read(cx), cx) {
+ let path = m.relative_path();
+ self.last_selected_dir = Some(path.clone());
+ Some(format!("{}/", path))
+ } else {
+ None
+ }
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<picker::Picker<Self>>) {
+ let Some(m) = self.matches.get(self.selected_index) else {
+ return;
+ };
+
+ let exists = m.entry(self.project.read(cx), cx).is_some();
+ if exists {
+ self.should_dismiss = false;
+ let answer = cx.prompt(
+ gpui::PromptLevel::Destructive,
+ &format!("{} already exists. Do you want to replace it?", m.relative_path()),
+ Some(
+ "A file or folder with the same name already eixsts. Replacing it will overwrite its current contents.",
+ ),
+ &["Replace", "Cancel"],
+ );
+ let m = m.clone();
+ cx.spawn(|picker, mut cx| async move {
+ let answer = answer.await.ok();
+ picker
+ .update(&mut cx, |picker, cx| {
+ picker.delegate.should_dismiss = true;
+ if answer != Some(0) {
+ return;
+ }
+ if let Some(path) = m.project_path(picker.delegate.project.read(cx), cx) {
+ if let Some(tx) = picker.delegate.tx.take() {
+ tx.send(Some(path)).ok();
+ }
+ }
+ cx.emit(gpui::DismissEvent);
+ })
+ .ok();
+ })
+ .detach();
+ return;
+ }
+
+ if let Some(path) = m.project_path(self.project.read(cx), cx) {
+ if let Some(tx) = self.tx.take() {
+ tx.send(Some(path)).ok();
+ }
+ }
+ cx.emit(gpui::DismissEvent);
+ }
+
+ fn should_dismiss(&self) -> bool {
+ self.should_dismiss
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<picker::Picker<Self>>) {
+ if let Some(tx) = self.tx.take() {
+ tx.send(None).ok();
+ }
+ cx.emit(gpui::DismissEvent)
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<picker::Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let m = self.matches.get(ix)?;
+
+ Some(
+ ListItem::new(ix)
+ .spacing(ListItemSpacing::Sparse)
+ .inset(true)
+ .selected(selected)
+ .child(LabelLike::new().child(m.styled_text(self.project.read(cx), cx))),
+ )
+ }
+
+ fn no_matches_text(&self, _cx: &mut WindowContext) -> SharedString {
+ "Type a path...".into()
+ }
+
+ fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
+ Arc::from("[directory/]filename.ext")
+ }
+}
+
+impl NewPathDelegate {
+ fn set_search_matches(
+ &mut self,
+ query: String,
+ prefix: String,
+ suffix: Option<String>,
+ matches: Vec<PathMatch>,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) {
+ cx.notify();
+ if query.is_empty() {
+ self.matches = vec![];
+ return;
+ }
+
+ let mut directory_exists = false;
+
+ self.matches = matches
+ .into_iter()
+ .map(|m| {
+ if m.path.as_ref().to_string_lossy() == prefix {
+ directory_exists = true
+ }
+ Match {
+ path_match: Some(m),
+ suffix: suffix.clone(),
+ }
+ })
+ .collect();
+
+ if !directory_exists {
+ if suffix.is_none()
+ || self
+ .last_selected_dir
+ .as_ref()
+ .is_some_and(|d| query.starts_with(d))
+ {
+ self.matches.insert(
+ 0,
+ Match {
+ path_match: None,
+ suffix: Some(query.clone()),
+ },
+ )
+ } else {
+ self.matches.push(Match {
+ path_match: None,
+ suffix: Some(query.clone()),
+ })
+ }
+ }
+ }
+}
@@ -693,7 +693,7 @@ pub struct PathPromptOptions {
}
/// What kind of prompt styling to show
-#[derive(Copy, Clone, Debug)]
+#[derive(Copy, Clone, Debug, PartialEq)]
pub enum PromptLevel {
/// A prompt that is shown when the user should be notified of something
Info,
@@ -703,6 +703,10 @@ pub enum PromptLevel {
/// A prompt that is shown when a critical problem has occurred
Critical,
+
+ /// A prompt that is shown when asking the user to confirm a potentially destructive action
+ /// (overwriting a file for example)
+ Destructive,
}
/// The style of the cursor (pointer)
@@ -904,7 +904,7 @@ impl PlatformWindow for MacWindow {
let alert_style = match level {
PromptLevel::Info => 1,
PromptLevel::Warning => 0,
- PromptLevel::Critical => 2,
+ PromptLevel::Critical | PromptLevel::Destructive => 2,
};
let _: () = msg_send![alert, setAlertStyle: alert_style];
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
@@ -919,10 +919,17 @@ impl PlatformWindow for MacWindow {
{
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let _: () = msg_send![button, setTag: ix as NSInteger];
+ if level == PromptLevel::Destructive && answer != &"Cancel" {
+ let _: () = msg_send![button, setHasDestructiveAction: YES];
+ }
}
if let Some((ix, answer)) = latest_non_cancel_label {
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
let _: () = msg_send![button, setTag: ix as NSInteger];
+ let _: () = msg_send![button, setHasDestructiveAction: YES];
+ if level == PromptLevel::Destructive {
+ let _: () = msg_send![button, setHasDestructiveAction: YES];
+ }
}
let (done_tx, done_rx) = oneshot::channel();
@@ -1455,7 +1455,7 @@ impl PlatformWindow for WindowsWindow {
title = windows::core::w!("Warning");
main_icon = TD_WARNING_ICON;
}
- crate::PromptLevel::Critical => {
+ crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
title = windows::core::w!("Critical");
main_icon = TD_ERROR_ICON;
}
@@ -628,6 +628,13 @@ impl From<&TextStyle> for HighlightStyle {
}
impl HighlightStyle {
+ /// Create a highlight style with just a color
+ pub fn color(color: Hsla) -> Self {
+ Self {
+ color: Some(color),
+ ..Default::default()
+ }
+ }
/// Blend this highlight style with another.
/// Non-continuous properties, like font_weight and font_style, are overwritten.
pub fn highlight(&mut self, other: HighlightStyle) {
@@ -79,11 +79,18 @@ pub trait PickerDelegate: Sized + 'static {
false
}
+ fn confirm_update_query(&mut self, _cx: &mut ViewContext<Picker<Self>>) -> Option<String> {
+ None
+ }
+
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
/// Instead of interacting with currently selected entry, treats editor input literally,
/// performing some kind of action on it.
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>);
+ fn should_dismiss(&self) -> bool {
+ true
+ }
fn selected_as_query(&self) -> Option<String> {
None
}
@@ -267,8 +274,10 @@ impl<D: PickerDelegate> Picker<D> {
}
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- self.delegate.dismissed(cx);
- cx.emit(DismissEvent);
+ if self.delegate.should_dismiss() {
+ self.delegate.dismissed(cx);
+ cx.emit(DismissEvent);
+ }
}
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
@@ -280,7 +289,7 @@ impl<D: PickerDelegate> Picker<D> {
self.confirm_on_update = Some(false)
} else {
self.pending_update_matches.take();
- self.delegate.confirm(false, cx);
+ self.do_confirm(false, cx);
}
}
@@ -292,7 +301,7 @@ impl<D: PickerDelegate> Picker<D> {
{
self.confirm_on_update = Some(true)
} else {
- self.delegate.confirm(true, cx);
+ self.do_confirm(true, cx);
}
}
@@ -311,7 +320,16 @@ impl<D: PickerDelegate> Picker<D> {
cx.stop_propagation();
cx.prevent_default();
self.delegate.set_selected_index(ix, cx);
- self.delegate.confirm(secondary, cx);
+ self.do_confirm(secondary, cx)
+ }
+
+ fn do_confirm(&mut self, secondary: bool, cx: &mut ViewContext<Self>) {
+ if let Some(update_query) = self.delegate.confirm_update_query(cx) {
+ self.set_query(update_query, cx);
+ self.delegate.set_selected_index(0, cx);
+ } else {
+ self.delegate.confirm(secondary, cx)
+ }
}
fn on_input_editor_event(
@@ -385,7 +403,7 @@ impl<D: PickerDelegate> Picker<D> {
self.scroll_to_item_index(index);
self.pending_update_matches = None;
if let Some(secondary) = self.confirm_on_update.take() {
- self.delegate.confirm(secondary, cx);
+ self.do_confirm(secondary, cx);
}
cx.notify();
}
@@ -32,6 +32,7 @@ use futures::{
stream::FuturesUnordered,
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
};
+use fuzzy::CharBag;
use git::{blame::Blame, repository::GitRepository};
use globset::{Glob, GlobSet, GlobSetBuilder};
use gpui::{
@@ -370,6 +371,22 @@ pub struct ProjectPath {
pub path: Arc<Path>,
}
+impl ProjectPath {
+ pub fn from_proto(p: proto::ProjectPath) -> Self {
+ Self {
+ worktree_id: WorktreeId::from_proto(p.worktree_id),
+ path: Arc::from(PathBuf::from(p.path)),
+ }
+ }
+
+ pub fn to_proto(&self) -> proto::ProjectPath {
+ proto::ProjectPath {
+ worktree_id: self.worktree_id.to_proto(),
+ path: self.path.to_string_lossy().to_string(),
+ }
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InlayHint {
pub position: language::Anchor,
@@ -2189,33 +2206,37 @@ impl Project {
let path = file.path.clone();
worktree.update(cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx),
- Worktree::Remote(worktree) => worktree.save_buffer(buffer, cx),
+ Worktree::Remote(worktree) => worktree.save_buffer(buffer, None, cx),
})
}
pub fn save_buffer_as(
&mut self,
buffer: Model<Buffer>,
- abs_path: PathBuf,
+ path: ProjectPath,
cx: &mut ModelContext<Self>,
) -> Task<Result<()>> {
- let worktree_task = self.find_or_create_local_worktree(&abs_path, true, cx);
let old_file = File::from_dyn(buffer.read(cx).file())
.filter(|f| f.is_local())
.cloned();
+ let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) else {
+ return Task::ready(Err(anyhow!("worktree does not exist")));
+ };
+
cx.spawn(move |this, mut cx| async move {
if let Some(old_file) = &old_file {
this.update(&mut cx, |this, cx| {
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
})?;
}
- let (worktree, path) = worktree_task.await?;
worktree
.update(&mut cx, |worktree, cx| match worktree {
Worktree::Local(worktree) => {
- worktree.save_buffer(buffer.clone(), path.into(), true, cx)
+ worktree.save_buffer(buffer.clone(), path.path, true, cx)
+ }
+ Worktree::Remote(worktree) => {
+ worktree.save_buffer(buffer.clone(), Some(path.to_proto()), cx)
}
- Worktree::Remote(_) => panic!("cannot remote buffers as new files"),
})?
.await?;
@@ -8676,8 +8697,17 @@ impl Project {
.await?;
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
- this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
+ if let Some(new_path) = envelope.payload.new_path {
+ let new_path = ProjectPath::from_proto(new_path);
+ this.update(&mut cx, |this, cx| {
+ this.save_buffer_as(buffer.clone(), new_path, cx)
+ })?
.await?;
+ } else {
+ this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
+ .await?;
+ }
+
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
project_id,
buffer_id: buffer_id.into(),
@@ -10414,6 +10444,7 @@ pub struct PathMatchCandidateSet {
pub snapshot: Snapshot,
pub include_ignored: bool,
pub include_root_name: bool,
+ pub directories_only: bool,
}
impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
@@ -10443,7 +10474,11 @@ impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
fn candidates(&'a self, start: usize) -> Self::Candidates {
PathMatchCandidateSetIter {
- traversal: self.snapshot.files(self.include_ignored, start),
+ traversal: if self.directories_only {
+ self.snapshot.directories(self.include_ignored, start)
+ } else {
+ self.snapshot.files(self.include_ignored, start)
+ },
}
}
}
@@ -10456,15 +10491,16 @@ impl<'a> Iterator for PathMatchCandidateSetIter<'a> {
type Item = fuzzy::PathMatchCandidate<'a>;
fn next(&mut self) -> Option<Self::Item> {
- self.traversal.next().map(|entry| {
- if let EntryKind::File(char_bag) = entry.kind {
- fuzzy::PathMatchCandidate {
- path: &entry.path,
- char_bag,
- }
- } else {
- unreachable!()
- }
+ self.traversal.next().map(|entry| match entry.kind {
+ EntryKind::Dir => fuzzy::PathMatchCandidate {
+ path: &entry.path,
+ char_bag: CharBag::from_iter(entry.path.to_string_lossy().to_lowercase().chars()),
+ },
+ EntryKind::File(char_bag) => fuzzy::PathMatchCandidate {
+ path: &entry.path,
+ char_bag,
+ },
+ EntryKind::UnloadedDir | EntryKind::PendingDir => unreachable!(),
})
}
}
@@ -2942,7 +2942,12 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
});
project
.update(cx, |project, cx| {
- project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
+ let worktree_id = project.worktrees().next().unwrap().read(cx).id();
+ let path = ProjectPath {
+ worktree_id,
+ path: Arc::from(Path::new("file1.rs")),
+ };
+ project.save_buffer_as(buffer.clone(), path, cx)
})
.await
.unwrap();
@@ -887,7 +887,7 @@ impl ProjectPanel {
let answer = (!action.skip_prompt).then(|| {
cx.prompt(
- PromptLevel::Info,
+ PromptLevel::Destructive,
&format!("Delete {file_name:?}?"),
None,
&["Delete", "Cancel"],
@@ -216,7 +216,7 @@ impl RemoteProjects {
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
let answer = cx.prompt(
- gpui::PromptLevel::Info,
+ gpui::PromptLevel::Destructive,
"Are you sure?",
Some("This will delete the dev server and all of its remote projects."),
&["Delete", "Cancel"],
@@ -769,6 +769,12 @@ message SaveBuffer {
uint64 project_id = 1;
uint64 buffer_id = 2;
repeated VectorClockEntry version = 3;
+ optional ProjectPath new_path = 4;
+}
+
+message ProjectPath {
+ uint64 worktree_id = 1;
+ string path = 2;
}
message BufferSaved {
@@ -19,14 +19,14 @@ use gpui::{
WeakModel, WeakView, WhiteSpace, WindowContext,
};
use menu::Confirm;
-use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project};
+use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
use settings::Settings;
use smol::stream::StreamExt;
use std::{
any::{Any, TypeId},
mem,
ops::{Not, Range},
- path::{Path, PathBuf},
+ path::Path,
};
use theme::ThemeSettings;
use ui::{
@@ -439,7 +439,7 @@ impl Item for ProjectSearchView {
fn save_as(
&mut self,
_: Model<Project>,
- _: PathBuf,
+ _: ProjectPath,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
unreachable!("save_as should not have been called")
@@ -50,38 +50,49 @@ impl LabelCommon for HighlightedLabel {
}
}
-impl RenderOnce for HighlightedLabel {
- fn render(self, cx: &mut WindowContext) -> impl IntoElement {
- let highlight_color = cx.theme().colors().text_accent;
+pub fn highlight_ranges(
+ text: &str,
+ indices: &Vec<usize>,
+ style: HighlightStyle,
+) -> Vec<(Range<usize>, HighlightStyle)> {
+ let mut highlight_indices = indices.iter().copied().peekable();
+ let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
+
+ while let Some(start_ix) = highlight_indices.next() {
+ let mut end_ix = start_ix;
+
+ loop {
+ end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8();
+ if let Some(&next_ix) = highlight_indices.peek() {
+ if next_ix == end_ix {
+ end_ix = next_ix;
+ highlight_indices.next();
+ continue;
+ }
+ }
+ break;
+ }
- let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
- let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
+ highlights.push((start_ix..end_ix, style));
+ }
- while let Some(start_ix) = highlight_indices.next() {
- let mut end_ix = start_ix;
+ highlights
+}
- loop {
- end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
- if let Some(&next_ix) = highlight_indices.peek() {
- if next_ix == end_ix {
- end_ix = next_ix;
- highlight_indices.next();
- continue;
- }
- }
- break;
- }
+impl RenderOnce for HighlightedLabel {
+ fn render(self, cx: &mut WindowContext) -> impl IntoElement {
+ let highlight_color = cx.theme().colors().text_accent;
- highlights.push((
- start_ix..end_ix,
- HighlightStyle {
- color: Some(highlight_color),
- ..Default::default()
- },
- ));
- }
+ let highlights = highlight_ranges(
+ &self.label,
+ &self.highlight_indices,
+ HighlightStyle {
+ color: Some(highlight_color),
+ ..Default::default()
+ },
+ );
- let mut text_style = cx.text_style().clone();
+ let mut text_style = cx.text_style();
text_style.color = self.base.color.color(cx);
self.base
@@ -26,7 +26,6 @@ use std::{
any::{Any, TypeId},
cell::RefCell,
ops::Range,
- path::PathBuf,
rc::Rc,
sync::Arc,
time::Duration,
@@ -196,7 +195,7 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn save_as(
&mut self,
_project: Model<Project>,
- _abs_path: PathBuf,
+ _path: ProjectPath,
_cx: &mut ViewContext<Self>,
) -> Task<Result<()>> {
unimplemented!("save_as() must be implemented if can_save() returns true")
@@ -309,7 +308,7 @@ pub trait ItemHandle: 'static + Send {
fn save_as(
&self,
project: Model<Project>,
- abs_path: PathBuf,
+ path: ProjectPath,
cx: &mut WindowContext,
) -> Task<Result<()>>;
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>>;
@@ -647,10 +646,10 @@ impl<T: Item> ItemHandle for View<T> {
fn save_as(
&self,
project: Model<Project>,
- abs_path: PathBuf,
+ path: ProjectPath,
cx: &mut WindowContext,
) -> Task<anyhow::Result<()>> {
- self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
+ self.update(cx, |item, cx| item.save_as(project, path, cx))
}
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
@@ -1126,7 +1125,7 @@ pub mod test {
fn save_as(
&mut self,
_: Model<Project>,
- _: std::path::PathBuf,
+ _: ProjectPath,
_: &mut ViewContext<Self>,
) -> Task<anyhow::Result<()>> {
self.save_as_count += 1;
@@ -263,7 +263,7 @@ impl Render for LanguageServerPrompt {
PromptLevel::Warning => {
Some(DiagnosticSeverity::WARNING)
}
- PromptLevel::Critical => {
+ PromptLevel::Critical | PromptLevel::Destructive => {
Some(DiagnosticSeverity::ERROR)
}
}
@@ -26,7 +26,7 @@ use std::{
any::Any,
cmp, fmt, mem,
ops::ControlFlow,
- path::{Path, PathBuf},
+ path::PathBuf,
rc::Rc,
sync::{
atomic::{AtomicUsize, Ordering},
@@ -1322,14 +1322,10 @@ impl Pane {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?;
} else if can_save_as {
- let start_abs_path = project
- .update(cx, |project, cx| {
- let worktree = project.visible_worktrees(cx).next()?;
- Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
- })?
- .unwrap_or_else(|| Path::new("").into());
-
- let abs_path = cx.update(|cx| cx.prompt_for_new_path(&start_abs_path))?;
+ let abs_path = pane.update(cx, |pane, cx| {
+ pane.workspace
+ .update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
+ })??;
if let Some(abs_path) = abs_path.await.ok().flatten() {
pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
.await?;
@@ -544,6 +544,10 @@ pub enum OpenVisible {
OnlyDirectories,
}
+type PromptForNewPath = Box<
+ dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
+>;
+
/// Collects everything project-related for a certain window opened.
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
///
@@ -585,6 +589,7 @@ pub struct Workspace {
bounds: Bounds<Pixels>,
centered_layout: bool,
bounds_save_task_queued: Option<Task<()>>,
+ on_prompt_for_new_path: Option<PromptForNewPath>,
}
impl EventEmitter<Event> for Workspace {}
@@ -875,6 +880,7 @@ impl Workspace {
bounds: Default::default(),
centered_layout: false,
bounds_save_task_queued: None,
+ on_prompt_for_new_path: None,
}
}
@@ -1223,6 +1229,59 @@ impl Workspace {
cx.notify();
}
+ pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
+ self.on_prompt_for_new_path = Some(prompt)
+ }
+
+ pub fn prompt_for_new_path(
+ &mut self,
+ cx: &mut ViewContext<Self>,
+ ) -> oneshot::Receiver<Option<ProjectPath>> {
+ if let Some(prompt) = self.on_prompt_for_new_path.take() {
+ let rx = prompt(self, cx);
+ self.on_prompt_for_new_path = Some(prompt);
+ rx
+ } else {
+ let start_abs_path = self
+ .project
+ .update(cx, |project, cx| {
+ let worktree = project.visible_worktrees(cx).next()?;
+ Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
+ })
+ .unwrap_or_else(|| Path::new("").into());
+
+ let (tx, rx) = oneshot::channel();
+ let abs_path = cx.prompt_for_new_path(&start_abs_path);
+ cx.spawn(|this, mut cx| async move {
+ let abs_path = abs_path.await?;
+ let project_path = abs_path.and_then(|abs_path| {
+ this.update(&mut cx, |this, cx| {
+ this.project.update(cx, |project, cx| {
+ project.find_or_create_local_worktree(abs_path, true, cx)
+ })
+ })
+ .ok()
+ });
+
+ if let Some(project_path) = project_path {
+ let (worktree, path) = project_path.await?;
+ let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
+ tx.send(Some(ProjectPath {
+ worktree_id,
+ path: path.into(),
+ }))
+ .ok();
+ } else {
+ tx.send(None).ok();
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+
+ rx
+ }
+ }
+
pub fn titlebar_item(&self) -> Option<AnyView> {
self.titlebar_item.clone()
}
@@ -1625,6 +1625,7 @@ impl RemoteWorktree {
pub fn save_buffer(
&self,
buffer_handle: Model<Buffer>,
+ new_path: Option<proto::ProjectPath>,
cx: &mut ModelContext<Worktree>,
) -> Task<Result<()>> {
let buffer = buffer_handle.read(cx);
@@ -1637,6 +1638,7 @@ impl RemoteWorktree {
.request(proto::SaveBuffer {
project_id,
buffer_id,
+ new_path,
version: serialize_version(&version),
})
.await?;
@@ -1911,6 +1913,7 @@ impl Snapshot {
fn traverse_from_offset(
&self,
+ include_files: bool,
include_dirs: bool,
include_ignored: bool,
start_offset: usize,
@@ -1919,6 +1922,7 @@ impl Snapshot {
cursor.seek(
&TraversalTarget::Count {
count: start_offset,
+ include_files,
include_dirs,
include_ignored,
},
@@ -1927,6 +1931,7 @@ impl Snapshot {
);
Traversal {
cursor,
+ include_files,
include_dirs,
include_ignored,
}
@@ -1934,6 +1939,7 @@ impl Snapshot {
fn traverse_from_path(
&self,
+ include_files: bool,
include_dirs: bool,
include_ignored: bool,
path: &Path,
@@ -1942,17 +1948,22 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
Traversal {
cursor,
+ include_files,
include_dirs,
include_ignored,
}
}
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
- self.traverse_from_offset(false, include_ignored, start)
+ self.traverse_from_offset(true, false, include_ignored, start)
+ }
+
+ pub fn directories(&self, include_ignored: bool, start: usize) -> Traversal {
+ self.traverse_from_offset(false, true, include_ignored, start)
}
pub fn entries(&self, include_ignored: bool) -> Traversal {
- self.traverse_from_offset(true, include_ignored, 0)
+ self.traverse_from_offset(true, true, include_ignored, 0)
}
pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
@@ -2084,6 +2095,7 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
let traversal = Traversal {
cursor,
+ include_files: true,
include_dirs: true,
include_ignored: true,
};
@@ -2103,6 +2115,7 @@ impl Snapshot {
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
let mut traversal = Traversal {
cursor,
+ include_files: true,
include_dirs,
include_ignored,
};
@@ -2141,7 +2154,7 @@ impl Snapshot {
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
let path = path.as_ref();
- self.traverse_from_path(true, true, path)
+ self.traverse_from_path(true, true, true, path)
.entry()
.and_then(|entry| {
if entry.path.as_ref() == path {
@@ -4532,12 +4545,15 @@ struct TraversalProgress<'a> {
}
impl<'a> TraversalProgress<'a> {
- fn count(&self, include_dirs: bool, include_ignored: bool) -> usize {
- match (include_ignored, include_dirs) {
- (true, true) => self.count,
- (true, false) => self.file_count,
- (false, true) => self.non_ignored_count,
- (false, false) => self.non_ignored_file_count,
+ fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
+ match (include_files, include_dirs, include_ignored) {
+ (true, true, true) => self.count,
+ (true, true, false) => self.non_ignored_count,
+ (true, false, true) => self.file_count,
+ (true, false, false) => self.non_ignored_file_count,
+ (false, true, true) => self.count - self.file_count,
+ (false, true, false) => self.non_ignored_count - self.non_ignored_file_count,
+ (false, false, _) => 0,
}
}
}
@@ -4600,6 +4616,7 @@ impl<'a> sum_tree::Dimension<'a, EntrySummary> for GitStatuses {
pub struct Traversal<'a> {
cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
include_ignored: bool,
+ include_files: bool,
include_dirs: bool,
}
@@ -4609,6 +4626,7 @@ impl<'a> Traversal<'a> {
&TraversalTarget::Count {
count: self.end_offset() + 1,
include_dirs: self.include_dirs,
+ include_files: self.include_files,
include_ignored: self.include_ignored,
},
Bias::Left,
@@ -4624,7 +4642,8 @@ impl<'a> Traversal<'a> {
&(),
);
if let Some(entry) = self.cursor.item() {
- if (self.include_dirs || !entry.is_dir())
+ if (self.include_files || !entry.is_file())
+ && (self.include_dirs || !entry.is_dir())
&& (self.include_ignored || !entry.is_ignored)
{
return true;
@@ -4641,13 +4660,13 @@ impl<'a> Traversal<'a> {
pub fn start_offset(&self) -> usize {
self.cursor
.start()
- .count(self.include_dirs, self.include_ignored)
+ .count(self.include_files, self.include_dirs, self.include_ignored)
}
pub fn end_offset(&self) -> usize {
self.cursor
.end(&())
- .count(self.include_dirs, self.include_ignored)
+ .count(self.include_files, self.include_dirs, self.include_ignored)
}
}
@@ -4670,6 +4689,7 @@ enum TraversalTarget<'a> {
PathSuccessor(&'a Path),
Count {
count: usize,
+ include_files: bool,
include_ignored: bool,
include_dirs: bool,
},
@@ -4688,11 +4708,12 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa
}
TraversalTarget::Count {
count,
+ include_files,
include_dirs,
include_ignored,
} => Ord::cmp(
count,
- &cursor_location.count(*include_dirs, *include_ignored),
+ &cursor_location.count(*include_files, *include_dirs, *include_ignored),
),
}
}