mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
new path picker (#11015)
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>
This commit is contained in:
parent
314b723292
commit
664f779eb4
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3815,6 +3815,7 @@ dependencies = [
|
|||||||
"ctor",
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"itertools 0.11.0",
|
"itertools 0.11.0",
|
||||||
|
51
README.md
51
README.md
@ -1,51 +0,0 @@
|
|||||||
# Zed
|
|
||||||
|
|
||||||
[![CI](https://github.com/zed-industries/zed/actions/workflows/ci.yml/badge.svg)](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
|
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
|
project_a
|
||||||
.update(cx_a, |project, cx| {
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -36,7 +36,6 @@ use std::{
|
|||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
mem,
|
mem,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
|
||||||
};
|
};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
pub use toolbar_controls::ToolbarControls;
|
pub use toolbar_controls::ToolbarControls;
|
||||||
@ -740,7 +739,7 @@ impl Item for ProjectDiagnosticsEditor {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<Project>,
|
_: Model<Project>,
|
||||||
_: PathBuf,
|
_: ProjectPath,
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
@ -13,7 +13,10 @@ use project::FakeFs;
|
|||||||
use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng};
|
use rand::{rngs::StdRng, seq::IteratorRandom as _, Rng};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{env, path::Path};
|
use std::{
|
||||||
|
env,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
use unindent::Unindent as _;
|
use unindent::Unindent as _;
|
||||||
use util::{post_inc, RandomCharIter};
|
use util::{post_inc, RandomCharIter};
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ use std::{
|
|||||||
cmp::{self, Ordering},
|
cmp::{self, Ordering},
|
||||||
iter,
|
iter,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
};
|
};
|
||||||
use text::{BufferId, Selection};
|
use text::{BufferId, Selection};
|
||||||
@ -750,7 +750,7 @@ impl Item for Editor {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
abs_path: PathBuf,
|
path: ProjectPath,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let buffer = self
|
let buffer = self
|
||||||
@ -759,14 +759,13 @@ impl Item for Editor {
|
|||||||
.as_singleton()
|
.as_singleton()
|
||||||
.expect("cannot call save_as on an excerpt list");
|
.expect("cannot call save_as on an excerpt list");
|
||||||
|
|
||||||
let file_extension = abs_path
|
let file_extension = path
|
||||||
|
.path
|
||||||
.extension()
|
.extension()
|
||||||
.map(|a| a.to_string_lossy().to_string());
|
.map(|a| a.to_string_lossy().to_string());
|
||||||
self.report_editor_event("save", file_extension, cx);
|
self.report_editor_event("save", file_extension, cx);
|
||||||
|
|
||||||
project.update(cx, |project, cx| {
|
project.update(cx, |project, cx| project.save_buffer_as(buffer, path, cx))
|
||||||
project.save_buffer_as(buffer, abs_path, cx)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
fn reload(&mut self, project: Model<Project>, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
|
||||||
|
@ -16,6 +16,7 @@ doctest = false
|
|||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
itertools = "0.11"
|
itertools = "0.11"
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod file_finder_tests;
|
mod file_finder_tests;
|
||||||
|
|
||||||
|
mod new_path_prompt;
|
||||||
|
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||||
@ -10,6 +12,7 @@ use gpui::{
|
|||||||
ViewContext, VisualContext, WeakView,
|
ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use new_path_prompt::NewPathPrompt;
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
@ -37,6 +40,7 @@ pub struct FileFinder {
|
|||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
cx.observe_new_views(FileFinder::register).detach();
|
cx.observe_new_views(FileFinder::register).detach();
|
||||||
|
cx.observe_new_views(NewPathPrompt::register).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FileFinder {
|
impl FileFinder {
|
||||||
@ -454,6 +458,7 @@ impl FileFinderDelegate {
|
|||||||
.root_entry()
|
.root_entry()
|
||||||
.map_or(false, |entry| entry.is_ignored),
|
.map_or(false, |entry| entry.is_ignored),
|
||||||
include_root_name,
|
include_root_name,
|
||||||
|
directories_only: false,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
463
crates/file_finder/src/new_path_prompt.rs
Normal file
463
crates/file_finder/src/new_path_prompt.rs
Normal file
@ -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
|
/// What kind of prompt styling to show
|
||||||
#[derive(Copy, Clone, Debug)]
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
pub enum PromptLevel {
|
pub enum PromptLevel {
|
||||||
/// A prompt that is shown when the user should be notified of something
|
/// A prompt that is shown when the user should be notified of something
|
||||||
Info,
|
Info,
|
||||||
@ -703,6 +703,10 @@ pub enum PromptLevel {
|
|||||||
|
|
||||||
/// A prompt that is shown when a critical problem has occurred
|
/// A prompt that is shown when a critical problem has occurred
|
||||||
Critical,
|
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)
|
/// The style of the cursor (pointer)
|
||||||
|
@ -904,7 +904,7 @@ impl PlatformWindow for MacWindow {
|
|||||||
let alert_style = match level {
|
let alert_style = match level {
|
||||||
PromptLevel::Info => 1,
|
PromptLevel::Info => 1,
|
||||||
PromptLevel::Warning => 0,
|
PromptLevel::Warning => 0,
|
||||||
PromptLevel::Critical => 2,
|
PromptLevel::Critical | PromptLevel::Destructive => 2,
|
||||||
};
|
};
|
||||||
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
let _: () = msg_send![alert, setAlertStyle: alert_style];
|
||||||
let _: () = msg_send![alert, setMessageText: ns_string(msg)];
|
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 button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
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 {
|
if let Some((ix, answer)) = latest_non_cancel_label {
|
||||||
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
let button: id = msg_send![alert, addButtonWithTitle: ns_string(answer)];
|
||||||
let _: () = msg_send![button, setTag: ix as NSInteger];
|
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();
|
let (done_tx, done_rx) = oneshot::channel();
|
||||||
|
@ -1455,7 +1455,7 @@ impl PlatformWindow for WindowsWindow {
|
|||||||
title = windows::core::w!("Warning");
|
title = windows::core::w!("Warning");
|
||||||
main_icon = TD_WARNING_ICON;
|
main_icon = TD_WARNING_ICON;
|
||||||
}
|
}
|
||||||
crate::PromptLevel::Critical => {
|
crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
|
||||||
title = windows::core::w!("Critical");
|
title = windows::core::w!("Critical");
|
||||||
main_icon = TD_ERROR_ICON;
|
main_icon = TD_ERROR_ICON;
|
||||||
}
|
}
|
||||||
|
@ -628,6 +628,13 @@ impl From<&TextStyle> for HighlightStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl 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.
|
/// Blend this highlight style with another.
|
||||||
/// Non-continuous properties, like font_weight and font_style, are overwritten.
|
/// Non-continuous properties, like font_weight and font_style, are overwritten.
|
||||||
pub fn highlight(&mut self, other: HighlightStyle) {
|
pub fn highlight(&mut self, other: HighlightStyle) {
|
||||||
|
@ -79,11 +79,18 @@ pub trait PickerDelegate: Sized + 'static {
|
|||||||
false
|
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>>);
|
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>);
|
||||||
/// Instead of interacting with currently selected entry, treats editor input literally,
|
/// Instead of interacting with currently selected entry, treats editor input literally,
|
||||||
/// performing some kind of action on it.
|
/// performing some kind of action on it.
|
||||||
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
|
fn confirm_input(&mut self, _secondary: bool, _: &mut ViewContext<Picker<Self>>) {}
|
||||||
fn dismissed(&mut self, cx: &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> {
|
fn selected_as_query(&self) -> Option<String> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
@ -267,9 +274,11 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
if self.delegate.should_dismiss() {
|
||||||
self.delegate.dismissed(cx);
|
self.delegate.dismissed(cx);
|
||||||
cx.emit(DismissEvent);
|
cx.emit(DismissEvent);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
if self.pending_update_matches.is_some()
|
if self.pending_update_matches.is_some()
|
||||||
@ -280,7 +289,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
self.confirm_on_update = Some(false)
|
self.confirm_on_update = Some(false)
|
||||||
} else {
|
} else {
|
||||||
self.pending_update_matches.take();
|
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)
|
self.confirm_on_update = Some(true)
|
||||||
} else {
|
} else {
|
||||||
self.delegate.confirm(true, cx);
|
self.do_confirm(true, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -311,7 +320,16 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
cx.prevent_default();
|
cx.prevent_default();
|
||||||
self.delegate.set_selected_index(ix, cx);
|
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(
|
fn on_input_editor_event(
|
||||||
@ -385,7 +403,7 @@ impl<D: PickerDelegate> Picker<D> {
|
|||||||
self.scroll_to_item_index(index);
|
self.scroll_to_item_index(index);
|
||||||
self.pending_update_matches = None;
|
self.pending_update_matches = None;
|
||||||
if let Some(secondary) = self.confirm_on_update.take() {
|
if let Some(secondary) = self.confirm_on_update.take() {
|
||||||
self.delegate.confirm(secondary, cx);
|
self.do_confirm(secondary, cx);
|
||||||
}
|
}
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ use futures::{
|
|||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
|
use fuzzy::CharBag;
|
||||||
use git::{blame::Blame, repository::GitRepository};
|
use git::{blame::Blame, repository::GitRepository};
|
||||||
use globset::{Glob, GlobSet, GlobSetBuilder};
|
use globset::{Glob, GlobSet, GlobSetBuilder};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
@ -370,6 +371,22 @@ pub struct ProjectPath {
|
|||||||
pub path: Arc<Path>,
|
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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct InlayHint {
|
pub struct InlayHint {
|
||||||
pub position: language::Anchor,
|
pub position: language::Anchor,
|
||||||
@ -2189,33 +2206,37 @@ impl Project {
|
|||||||
let path = file.path.clone();
|
let path = file.path.clone();
|
||||||
worktree.update(cx, |worktree, cx| match worktree {
|
worktree.update(cx, |worktree, cx| match worktree {
|
||||||
Worktree::Local(worktree) => worktree.save_buffer(buffer, path, false, cx),
|
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(
|
pub fn save_buffer_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer: Model<Buffer>,
|
buffer: Model<Buffer>,
|
||||||
abs_path: PathBuf,
|
path: ProjectPath,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> 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())
|
let old_file = File::from_dyn(buffer.read(cx).file())
|
||||||
.filter(|f| f.is_local())
|
.filter(|f| f.is_local())
|
||||||
.cloned();
|
.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 {
|
cx.spawn(move |this, mut cx| async move {
|
||||||
if let Some(old_file) = &old_file {
|
if let Some(old_file) = &old_file {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
|
this.unregister_buffer_from_language_servers(&buffer, old_file, cx);
|
||||||
})?;
|
})?;
|
||||||
}
|
}
|
||||||
let (worktree, path) = worktree_task.await?;
|
|
||||||
worktree
|
worktree
|
||||||
.update(&mut cx, |worktree, cx| match worktree {
|
.update(&mut cx, |worktree, cx| match worktree {
|
||||||
Worktree::Local(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?;
|
.await?;
|
||||||
|
|
||||||
@ -8676,8 +8697,17 @@ impl Project {
|
|||||||
.await?;
|
.await?;
|
||||||
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
||||||
|
|
||||||
|
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))?
|
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
|
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id: buffer_id.into(),
|
buffer_id: buffer_id.into(),
|
||||||
@ -10414,6 +10444,7 @@ pub struct PathMatchCandidateSet {
|
|||||||
pub snapshot: Snapshot,
|
pub snapshot: Snapshot,
|
||||||
pub include_ignored: bool,
|
pub include_ignored: bool,
|
||||||
pub include_root_name: bool,
|
pub include_root_name: bool,
|
||||||
|
pub directories_only: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> fuzzy::PathMatchCandidateSet<'a> for PathMatchCandidateSet {
|
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 {
|
fn candidates(&'a self, start: usize) -> Self::Candidates {
|
||||||
PathMatchCandidateSetIter {
|
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>;
|
type Item = fuzzy::PathMatchCandidate<'a>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
self.traversal.next().map(|entry| {
|
self.traversal.next().map(|entry| match entry.kind {
|
||||||
if let EntryKind::File(char_bag) = entry.kind {
|
EntryKind::Dir => fuzzy::PathMatchCandidate {
|
||||||
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,
|
path: &entry.path,
|
||||||
char_bag,
|
char_bag,
|
||||||
}
|
},
|
||||||
} else {
|
EntryKind::UnloadedDir | EntryKind::PendingDir => unreachable!(),
|
||||||
unreachable!()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2942,7 +2942,12 @@ async fn test_save_as(cx: &mut gpui::TestAppContext) {
|
|||||||
});
|
});
|
||||||
project
|
project
|
||||||
.update(cx, |project, cx| {
|
.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
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -887,7 +887,7 @@ impl ProjectPanel {
|
|||||||
|
|
||||||
let answer = (!action.skip_prompt).then(|| {
|
let answer = (!action.skip_prompt).then(|| {
|
||||||
cx.prompt(
|
cx.prompt(
|
||||||
PromptLevel::Info,
|
PromptLevel::Destructive,
|
||||||
&format!("Delete {file_name:?}?"),
|
&format!("Delete {file_name:?}?"),
|
||||||
None,
|
None,
|
||||||
&["Delete", "Cancel"],
|
&["Delete", "Cancel"],
|
||||||
|
@ -216,7 +216,7 @@ impl RemoteProjects {
|
|||||||
|
|
||||||
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
|
fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
|
||||||
let answer = cx.prompt(
|
let answer = cx.prompt(
|
||||||
gpui::PromptLevel::Info,
|
gpui::PromptLevel::Destructive,
|
||||||
"Are you sure?",
|
"Are you sure?",
|
||||||
Some("This will delete the dev server and all of its remote projects."),
|
Some("This will delete the dev server and all of its remote projects."),
|
||||||
&["Delete", "Cancel"],
|
&["Delete", "Cancel"],
|
||||||
|
@ -769,6 +769,12 @@ message SaveBuffer {
|
|||||||
uint64 project_id = 1;
|
uint64 project_id = 1;
|
||||||
uint64 buffer_id = 2;
|
uint64 buffer_id = 2;
|
||||||
repeated VectorClockEntry version = 3;
|
repeated VectorClockEntry version = 3;
|
||||||
|
optional ProjectPath new_path = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ProjectPath {
|
||||||
|
uint64 worktree_id = 1;
|
||||||
|
string path = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
message BufferSaved {
|
message BufferSaved {
|
||||||
|
@ -19,14 +19,14 @@ use gpui::{
|
|||||||
WeakModel, WeakView, WhiteSpace, WindowContext,
|
WeakModel, WeakView, WhiteSpace, WindowContext,
|
||||||
};
|
};
|
||||||
use menu::Confirm;
|
use menu::Confirm;
|
||||||
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project};
|
use project::{search::SearchQuery, search_history::SearchHistoryCursor, Project, ProjectPath};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use smol::stream::StreamExt;
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
mem,
|
mem,
|
||||||
ops::{Not, Range},
|
ops::{Not, Range},
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
};
|
};
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
@ -439,7 +439,7 @@ impl Item for ProjectSearchView {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<Project>,
|
_: Model<Project>,
|
||||||
_: PathBuf,
|
_: ProjectPath,
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
unreachable!("save_as should not have been called")
|
unreachable!("save_as should not have been called")
|
||||||
|
@ -50,18 +50,19 @@ impl LabelCommon for HighlightedLabel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for HighlightedLabel {
|
pub fn highlight_ranges(
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
text: &str,
|
||||||
let highlight_color = cx.theme().colors().text_accent;
|
indices: &Vec<usize>,
|
||||||
|
style: HighlightStyle,
|
||||||
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
|
) -> Vec<(Range<usize>, HighlightStyle)> {
|
||||||
|
let mut highlight_indices = indices.iter().copied().peekable();
|
||||||
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
let mut highlights: Vec<(Range<usize>, HighlightStyle)> = Vec::new();
|
||||||
|
|
||||||
while let Some(start_ix) = highlight_indices.next() {
|
while let Some(start_ix) = highlight_indices.next() {
|
||||||
let mut end_ix = start_ix;
|
let mut end_ix = start_ix;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8();
|
end_ix = end_ix + text[end_ix..].chars().next().unwrap().len_utf8();
|
||||||
if let Some(&next_ix) = highlight_indices.peek() {
|
if let Some(&next_ix) = highlight_indices.peek() {
|
||||||
if next_ix == end_ix {
|
if next_ix == end_ix {
|
||||||
end_ix = next_ix;
|
end_ix = next_ix;
|
||||||
@ -72,16 +73,26 @@ impl RenderOnce for HighlightedLabel {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
highlights.push((
|
highlights.push((start_ix..end_ix, style));
|
||||||
start_ix..end_ix,
|
}
|
||||||
|
|
||||||
|
highlights
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for HighlightedLabel {
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
let highlight_color = cx.theme().colors().text_accent;
|
||||||
|
|
||||||
|
let highlights = highlight_ranges(
|
||||||
|
&self.label,
|
||||||
|
&self.highlight_indices,
|
||||||
HighlightStyle {
|
HighlightStyle {
|
||||||
color: Some(highlight_color),
|
color: Some(highlight_color),
|
||||||
..Default::default()
|
..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);
|
text_style.color = self.base.color.color(cx);
|
||||||
|
|
||||||
self.base
|
self.base
|
||||||
|
@ -26,7 +26,6 @@ use std::{
|
|||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
ops::Range,
|
ops::Range,
|
||||||
path::PathBuf,
|
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
@ -196,7 +195,7 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
_project: Model<Project>,
|
_project: Model<Project>,
|
||||||
_abs_path: PathBuf,
|
_path: ProjectPath,
|
||||||
_cx: &mut ViewContext<Self>,
|
_cx: &mut ViewContext<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
unimplemented!("save_as() must be implemented if can_save() returns true")
|
unimplemented!("save_as() must be implemented if can_save() returns true")
|
||||||
@ -309,7 +308,7 @@ pub trait ItemHandle: 'static + Send {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&self,
|
&self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
abs_path: PathBuf,
|
path: ProjectPath,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<Result<()>>;
|
) -> Task<Result<()>>;
|
||||||
fn reload(&self, project: Model<Project>, 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(
|
fn save_as(
|
||||||
&self,
|
&self,
|
||||||
project: Model<Project>,
|
project: Model<Project>,
|
||||||
abs_path: PathBuf,
|
path: ProjectPath,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> 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<()>> {
|
fn reload(&self, project: Model<Project>, cx: &mut WindowContext) -> Task<Result<()>> {
|
||||||
@ -1126,7 +1125,7 @@ pub mod test {
|
|||||||
fn save_as(
|
fn save_as(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<Project>,
|
_: Model<Project>,
|
||||||
_: std::path::PathBuf,
|
_: ProjectPath,
|
||||||
_: &mut ViewContext<Self>,
|
_: &mut ViewContext<Self>,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<anyhow::Result<()>> {
|
||||||
self.save_as_count += 1;
|
self.save_as_count += 1;
|
||||||
|
@ -263,7 +263,7 @@ impl Render for LanguageServerPrompt {
|
|||||||
PromptLevel::Warning => {
|
PromptLevel::Warning => {
|
||||||
Some(DiagnosticSeverity::WARNING)
|
Some(DiagnosticSeverity::WARNING)
|
||||||
}
|
}
|
||||||
PromptLevel::Critical => {
|
PromptLevel::Critical | PromptLevel::Destructive => {
|
||||||
Some(DiagnosticSeverity::ERROR)
|
Some(DiagnosticSeverity::ERROR)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ use std::{
|
|||||||
any::Any,
|
any::Any,
|
||||||
cmp, fmt, mem,
|
cmp, fmt, mem,
|
||||||
ops::ControlFlow,
|
ops::ControlFlow,
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
rc::Rc,
|
rc::Rc,
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicUsize, Ordering},
|
atomic::{AtomicUsize, Ordering},
|
||||||
@ -1322,14 +1322,10 @@ impl Pane {
|
|||||||
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
|
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
} else if can_save_as {
|
} else if can_save_as {
|
||||||
let start_abs_path = project
|
let abs_path = pane.update(cx, |pane, cx| {
|
||||||
.update(cx, |project, cx| {
|
pane.workspace
|
||||||
let worktree = project.visible_worktrees(cx).next()?;
|
.update(cx, |workspace, cx| workspace.prompt_for_new_path(cx))
|
||||||
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))?;
|
|
||||||
if let Some(abs_path) = abs_path.await.ok().flatten() {
|
if let Some(abs_path) = abs_path.await.ok().flatten() {
|
||||||
pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
|
pane.update(cx, |_, cx| item.save_as(project, abs_path, cx))?
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -544,6 +544,10 @@ pub enum OpenVisible {
|
|||||||
OnlyDirectories,
|
OnlyDirectories,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PromptForNewPath = Box<
|
||||||
|
dyn Fn(&mut Workspace, &mut ViewContext<Workspace>) -> oneshot::Receiver<Option<ProjectPath>>,
|
||||||
|
>;
|
||||||
|
|
||||||
/// Collects everything project-related for a certain window opened.
|
/// 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`.
|
/// 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>,
|
bounds: Bounds<Pixels>,
|
||||||
centered_layout: bool,
|
centered_layout: bool,
|
||||||
bounds_save_task_queued: Option<Task<()>>,
|
bounds_save_task_queued: Option<Task<()>>,
|
||||||
|
on_prompt_for_new_path: Option<PromptForNewPath>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EventEmitter<Event> for Workspace {}
|
impl EventEmitter<Event> for Workspace {}
|
||||||
@ -875,6 +880,7 @@ impl Workspace {
|
|||||||
bounds: Default::default(),
|
bounds: Default::default(),
|
||||||
centered_layout: false,
|
centered_layout: false,
|
||||||
bounds_save_task_queued: None,
|
bounds_save_task_queued: None,
|
||||||
|
on_prompt_for_new_path: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1223,6 +1229,59 @@ impl Workspace {
|
|||||||
cx.notify();
|
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> {
|
pub fn titlebar_item(&self) -> Option<AnyView> {
|
||||||
self.titlebar_item.clone()
|
self.titlebar_item.clone()
|
||||||
}
|
}
|
||||||
|
@ -1625,6 +1625,7 @@ impl RemoteWorktree {
|
|||||||
pub fn save_buffer(
|
pub fn save_buffer(
|
||||||
&self,
|
&self,
|
||||||
buffer_handle: Model<Buffer>,
|
buffer_handle: Model<Buffer>,
|
||||||
|
new_path: Option<proto::ProjectPath>,
|
||||||
cx: &mut ModelContext<Worktree>,
|
cx: &mut ModelContext<Worktree>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let buffer = buffer_handle.read(cx);
|
let buffer = buffer_handle.read(cx);
|
||||||
@ -1637,6 +1638,7 @@ impl RemoteWorktree {
|
|||||||
.request(proto::SaveBuffer {
|
.request(proto::SaveBuffer {
|
||||||
project_id,
|
project_id,
|
||||||
buffer_id,
|
buffer_id,
|
||||||
|
new_path,
|
||||||
version: serialize_version(&version),
|
version: serialize_version(&version),
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
@ -1911,6 +1913,7 @@ impl Snapshot {
|
|||||||
|
|
||||||
fn traverse_from_offset(
|
fn traverse_from_offset(
|
||||||
&self,
|
&self,
|
||||||
|
include_files: bool,
|
||||||
include_dirs: bool,
|
include_dirs: bool,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
start_offset: usize,
|
start_offset: usize,
|
||||||
@ -1919,6 +1922,7 @@ impl Snapshot {
|
|||||||
cursor.seek(
|
cursor.seek(
|
||||||
&TraversalTarget::Count {
|
&TraversalTarget::Count {
|
||||||
count: start_offset,
|
count: start_offset,
|
||||||
|
include_files,
|
||||||
include_dirs,
|
include_dirs,
|
||||||
include_ignored,
|
include_ignored,
|
||||||
},
|
},
|
||||||
@ -1927,6 +1931,7 @@ impl Snapshot {
|
|||||||
);
|
);
|
||||||
Traversal {
|
Traversal {
|
||||||
cursor,
|
cursor,
|
||||||
|
include_files,
|
||||||
include_dirs,
|
include_dirs,
|
||||||
include_ignored,
|
include_ignored,
|
||||||
}
|
}
|
||||||
@ -1934,6 +1939,7 @@ impl Snapshot {
|
|||||||
|
|
||||||
fn traverse_from_path(
|
fn traverse_from_path(
|
||||||
&self,
|
&self,
|
||||||
|
include_files: bool,
|
||||||
include_dirs: bool,
|
include_dirs: bool,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
@ -1942,17 +1948,22 @@ impl Snapshot {
|
|||||||
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
|
cursor.seek(&TraversalTarget::Path(path), Bias::Left, &());
|
||||||
Traversal {
|
Traversal {
|
||||||
cursor,
|
cursor,
|
||||||
|
include_files,
|
||||||
include_dirs,
|
include_dirs,
|
||||||
include_ignored,
|
include_ignored,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn files(&self, include_ignored: bool, start: usize) -> Traversal {
|
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 {
|
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)> {
|
pub fn repositories(&self) -> impl Iterator<Item = (&Arc<Path>, &RepositoryEntry)> {
|
||||||
@ -2084,6 +2095,7 @@ impl Snapshot {
|
|||||||
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
|
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Right, &());
|
||||||
let traversal = Traversal {
|
let traversal = Traversal {
|
||||||
cursor,
|
cursor,
|
||||||
|
include_files: true,
|
||||||
include_dirs: true,
|
include_dirs: true,
|
||||||
include_ignored: true,
|
include_ignored: true,
|
||||||
};
|
};
|
||||||
@ -2103,6 +2115,7 @@ impl Snapshot {
|
|||||||
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
|
cursor.seek(&TraversalTarget::Path(parent_path), Bias::Left, &());
|
||||||
let mut traversal = Traversal {
|
let mut traversal = Traversal {
|
||||||
cursor,
|
cursor,
|
||||||
|
include_files: true,
|
||||||
include_dirs,
|
include_dirs,
|
||||||
include_ignored,
|
include_ignored,
|
||||||
};
|
};
|
||||||
@ -2141,7 +2154,7 @@ impl Snapshot {
|
|||||||
|
|
||||||
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
|
pub fn entry_for_path(&self, path: impl AsRef<Path>) -> Option<&Entry> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
self.traverse_from_path(true, true, path)
|
self.traverse_from_path(true, true, true, path)
|
||||||
.entry()
|
.entry()
|
||||||
.and_then(|entry| {
|
.and_then(|entry| {
|
||||||
if entry.path.as_ref() == path {
|
if entry.path.as_ref() == path {
|
||||||
@ -4532,12 +4545,15 @@ struct TraversalProgress<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> TraversalProgress<'a> {
|
impl<'a> TraversalProgress<'a> {
|
||||||
fn count(&self, include_dirs: bool, include_ignored: bool) -> usize {
|
fn count(&self, include_files: bool, include_dirs: bool, include_ignored: bool) -> usize {
|
||||||
match (include_ignored, include_dirs) {
|
match (include_files, include_dirs, include_ignored) {
|
||||||
(true, true) => self.count,
|
(true, true, true) => self.count,
|
||||||
(true, false) => self.file_count,
|
(true, true, false) => self.non_ignored_count,
|
||||||
(false, true) => self.non_ignored_count,
|
(true, false, true) => self.file_count,
|
||||||
(false, false) => self.non_ignored_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> {
|
pub struct Traversal<'a> {
|
||||||
cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
|
cursor: sum_tree::Cursor<'a, Entry, TraversalProgress<'a>>,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
|
include_files: bool,
|
||||||
include_dirs: bool,
|
include_dirs: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4609,6 +4626,7 @@ impl<'a> Traversal<'a> {
|
|||||||
&TraversalTarget::Count {
|
&TraversalTarget::Count {
|
||||||
count: self.end_offset() + 1,
|
count: self.end_offset() + 1,
|
||||||
include_dirs: self.include_dirs,
|
include_dirs: self.include_dirs,
|
||||||
|
include_files: self.include_files,
|
||||||
include_ignored: self.include_ignored,
|
include_ignored: self.include_ignored,
|
||||||
},
|
},
|
||||||
Bias::Left,
|
Bias::Left,
|
||||||
@ -4624,7 +4642,8 @@ impl<'a> Traversal<'a> {
|
|||||||
&(),
|
&(),
|
||||||
);
|
);
|
||||||
if let Some(entry) = self.cursor.item() {
|
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)
|
&& (self.include_ignored || !entry.is_ignored)
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@ -4641,13 +4660,13 @@ impl<'a> Traversal<'a> {
|
|||||||
pub fn start_offset(&self) -> usize {
|
pub fn start_offset(&self) -> usize {
|
||||||
self.cursor
|
self.cursor
|
||||||
.start()
|
.start()
|
||||||
.count(self.include_dirs, self.include_ignored)
|
.count(self.include_files, self.include_dirs, self.include_ignored)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn end_offset(&self) -> usize {
|
pub fn end_offset(&self) -> usize {
|
||||||
self.cursor
|
self.cursor
|
||||||
.end(&())
|
.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),
|
PathSuccessor(&'a Path),
|
||||||
Count {
|
Count {
|
||||||
count: usize,
|
count: usize,
|
||||||
|
include_files: bool,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
include_dirs: bool,
|
include_dirs: bool,
|
||||||
},
|
},
|
||||||
@ -4688,11 +4708,12 @@ impl<'a, 'b> SeekTarget<'a, EntrySummary, TraversalProgress<'a>> for TraversalTa
|
|||||||
}
|
}
|
||||||
TraversalTarget::Count {
|
TraversalTarget::Count {
|
||||||
count,
|
count,
|
||||||
|
include_files,
|
||||||
include_dirs,
|
include_dirs,
|
||||||
include_ignored,
|
include_ignored,
|
||||||
} => Ord::cmp(
|
} => Ord::cmp(
|
||||||
count,
|
count,
|
||||||
&cursor_location.count(*include_dirs, *include_ignored),
|
&cursor_location.count(*include_files, *include_dirs, *include_ignored),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user