Merge branch 'main' into unborked-git-zed2-diagnostics-view
This commit is contained in:
commit
189ddf9380
50 changed files with 3822 additions and 2611 deletions
|
@ -268,6 +268,19 @@
|
||||||
// Whether to show warnings or not by default.
|
// Whether to show warnings or not by default.
|
||||||
"include_warnings": true
|
"include_warnings": true
|
||||||
},
|
},
|
||||||
|
// Add files or globs of files that will be excluded by Zed entirely:
|
||||||
|
// they will be skipped during FS scan(s), file tree and file search
|
||||||
|
// will lack the corresponding file entries.
|
||||||
|
"file_scan_exclusions": [
|
||||||
|
"**/.git",
|
||||||
|
"**/.svn",
|
||||||
|
"**/.hg",
|
||||||
|
"**/CVS",
|
||||||
|
"**/.DS_Store",
|
||||||
|
"**/Thumbs.db",
|
||||||
|
"**/.classpath",
|
||||||
|
"**/.settings"
|
||||||
|
],
|
||||||
// Git gutter behavior configuration.
|
// Git gutter behavior configuration.
|
||||||
"git": {
|
"git": {
|
||||||
// Control whether the git gutter is shown. May take 2 values:
|
// Control whether the git gutter is shown. May take 2 values:
|
||||||
|
|
|
@ -5052,7 +5052,7 @@ async fn test_project_search(
|
||||||
let mut results = HashMap::default();
|
let mut results = HashMap::default();
|
||||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||||
project.search(
|
project.search(
|
||||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -869,7 +869,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
|
|
||||||
let mut search = project.update(cx, |project, cx| {
|
let mut search = project.update(cx, |project, cx| {
|
||||||
project.search(
|
project.search(
|
||||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||||
|
.unwrap(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -4599,7 +4599,7 @@ async fn test_project_search(
|
||||||
let mut results = HashMap::default();
|
let mut results = HashMap::default();
|
||||||
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
let mut search_rx = project_b.update(cx_b, |project, cx| {
|
||||||
project.search(
|
project.search(
|
||||||
SearchQuery::text("world", false, false, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("world", false, false, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -870,7 +870,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||||
|
|
||||||
let mut search = project.update(cx, |project, cx| {
|
let mut search = project.update(cx, |project, cx| {
|
||||||
project.search(
|
project.search(
|
||||||
SearchQuery::text(query, false, false, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text(query, false, false, false, Vec::new(), Vec::new())
|
||||||
|
.unwrap(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
|
||||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
static ref MENTIONS_SEARCH: SearchQuery =
|
||||||
"@[-_\\w]+",
|
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||||
false,
|
|
||||||
false,
|
|
||||||
Default::default(),
|
|
||||||
Default::default()
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
|
|
|
@ -14,14 +14,8 @@ use std::{sync::Arc, time::Duration};
|
||||||
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
const MENTIONS_DEBOUNCE_INTERVAL: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
static ref MENTIONS_SEARCH: SearchQuery = SearchQuery::regex(
|
static ref MENTIONS_SEARCH: SearchQuery =
|
||||||
"@[-_\\w]+",
|
SearchQuery::regex("@[-_\\w]+", false, false, false, Vec::new(), Vec::new()).unwrap();
|
||||||
false,
|
|
||||||
false,
|
|
||||||
Default::default(),
|
|
||||||
Default::default()
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct MessageEditor {
|
pub struct MessageEditor {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use collections::{CommandPaletteFilter, HashMap};
|
use collections::{CommandPaletteFilter, HashMap};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::*, Action, AppContext, Component, Dismiss, Div, FocusHandle, Keystroke,
|
actions, div, prelude::*, Action, AppContext, Component, Div, EventEmitter, FocusHandle,
|
||||||
ManagedView, ParentComponent, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
FocusableView, Keystroke, Manager, ParentComponent, Render, Styled, View, ViewContext,
|
||||||
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -68,7 +69,9 @@ impl CommandPalette {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedView for CommandPalette {
|
impl EventEmitter<Manager> for CommandPalette {}
|
||||||
|
|
||||||
|
impl FocusableView for CommandPalette {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.picker.focus_handle(cx)
|
self.picker.focus_handle(cx)
|
||||||
}
|
}
|
||||||
|
@ -266,7 +269,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||||
self.command_palette
|
self.command_palette
|
||||||
.update(cx, |_, cx| cx.emit(Dismiss))
|
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ use editor::{
|
||||||
use futures::future::try_join_all;
|
use futures::future::try_join_all;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
|
actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter,
|
||||||
FocusEvent, FocusHandle, Focusable, FocusableComponent, InteractiveComponent, ManagedView,
|
FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent,
|
||||||
Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext,
|
||||||
VisualContext, WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
|
@ -640,7 +640,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedView for ProjectDiagnosticsEditor {
|
impl FocusableView for ProjectDiagnosticsEditor {
|
||||||
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, _: &AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,9 @@ use collections::HashMap;
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Bias, Editor};
|
||||||
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
use fuzzy::{CharBag, PathMatch, PathMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, AppContext, Component, Dismiss, Div, FocusHandle, InteractiveComponent,
|
actions, div, AppContext, Component, Div, EventEmitter, FocusHandle, FocusableView,
|
||||||
ManagedView, Model, ParentComponent, Render, Styled, Task, View, ViewContext, VisualContext,
|
InteractiveComponent, Manager, Model, ParentComponent, Render, Styled, Task, View, ViewContext,
|
||||||
WeakView,
|
VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
use project::{PathMatchCandidateSet, Project, ProjectPath, WorktreeId};
|
||||||
|
@ -111,7 +111,8 @@ impl FileFinder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedView for FileFinder {
|
impl EventEmitter<Manager> for FileFinder {}
|
||||||
|
impl FocusableView for FileFinder {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.picker.focus_handle(cx)
|
self.picker.focus_handle(cx)
|
||||||
}
|
}
|
||||||
|
@ -688,7 +689,9 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finder.update(&mut cx, |_, cx| cx.emit(Dismiss)).ok()?;
|
finder
|
||||||
|
.update(&mut cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
})
|
})
|
||||||
|
@ -699,7 +702,7 @@ impl PickerDelegate for FileFinderDelegate {
|
||||||
|
|
||||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<FileFinderDelegate>>) {
|
||||||
self.file_finder
|
self.file_finder
|
||||||
.update(cx, |_, cx| cx.emit(Dismiss))
|
.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||||
.log_err();
|
.log_err();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
use editor::{display_map::ToDisplayPoint, scroll::autoscroll::Autoscroll, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, prelude::*, AppContext, Dismiss, Div, FocusHandle, ManagedView, ParentComponent,
|
actions, div, prelude::*, AppContext, Div, EventEmitter, FocusHandle, FocusableView, Manager,
|
||||||
Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext, WindowContext,
|
ParentComponent, Render, SharedString, Styled, Subscription, View, ViewContext, VisualContext,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use text::{Bias, Point};
|
use text::{Bias, Point};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
@ -23,11 +24,12 @@ pub struct GoToLine {
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedView for GoToLine {
|
impl FocusableView for GoToLine {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
||||||
self.line_editor.focus_handle(cx)
|
self.active_editor.focus_handle(cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl EventEmitter<Manager> for GoToLine {}
|
||||||
|
|
||||||
impl GoToLine {
|
impl GoToLine {
|
||||||
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
|
@ -87,7 +89,7 @@ impl GoToLine {
|
||||||
) {
|
) {
|
||||||
match event {
|
match event {
|
||||||
// todo!() this isn't working...
|
// todo!() this isn't working...
|
||||||
editor::EditorEvent::Blurred => cx.emit(Dismiss),
|
editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss),
|
||||||
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +124,7 @@ impl GoToLine {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(Dismiss);
|
cx.emit(Manager::Dismiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -139,7 +141,7 @@ impl GoToLine {
|
||||||
self.prev_scroll_position.take();
|
self.prev_scroll_position.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
cx.emit(Dismiss);
|
cx.emit(Manager::Dismiss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
AnyView, AnyWindowHandle, AppCell, AppContext, BackgroundExecutor, Context, FocusableView,
|
||||||
ForegroundExecutor, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
ForegroundExecutor, Manager, Model, ModelContext, Render, Result, Task, View, ViewContext,
|
||||||
VisualContext, WindowContext, WindowHandle,
|
VisualContext, WindowContext, WindowHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Context as _};
|
use anyhow::{anyhow, Context as _};
|
||||||
|
@ -320,4 +320,13 @@ impl VisualContext for AsyncWindowContext {
|
||||||
view.read(cx).focus_handle(cx).clone().focus(cx);
|
view.read(cx).focus_handle(cx).clone().focus(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: crate::ManagedView,
|
||||||
|
{
|
||||||
|
self.window.update(self, |_, cx| {
|
||||||
|
view.update(cx, |_, cx| cx.emit(Manager::Dismiss))
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,11 +71,12 @@ impl EntityMap {
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
pub fn lease<'a, T>(&mut self, model: &'a Model<T>) -> Lease<'a, T> {
|
||||||
self.assert_valid_context(model);
|
self.assert_valid_context(model);
|
||||||
let entity = Some(
|
let entity = Some(self.entities.remove(model.entity_id).unwrap_or_else(|| {
|
||||||
self.entities
|
panic!(
|
||||||
.remove(model.entity_id)
|
"Circular entity lease of {}. Is it already being updated?",
|
||||||
.expect("Circular entity lease. Is the entity already being updated?"),
|
std::any::type_name::<T>()
|
||||||
);
|
)
|
||||||
|
}));
|
||||||
Lease {
|
Lease {
|
||||||
model,
|
model,
|
||||||
entity,
|
entity,
|
||||||
|
|
|
@ -605,6 +605,17 @@ impl<'a> VisualContext for VisualTestContext<'a> {
|
||||||
})
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: crate::ManagedView,
|
||||||
|
{
|
||||||
|
self.window
|
||||||
|
.update(self.cx, |_, cx| {
|
||||||
|
view.update(cx, |_, cx| cx.emit(crate::Manager::Dismiss))
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AnyWindowHandle {
|
impl AnyWindowHandle {
|
||||||
|
|
|
@ -1124,9 +1124,14 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// if self.hover_style.is_some() {
|
||||||
if bounds.contains_point(&mouse_position) {
|
if bounds.contains_point(&mouse_position) {
|
||||||
|
// eprintln!("div hovered {bounds:?} {mouse_position:?}");
|
||||||
style.refine(&self.hover_style);
|
style.refine(&self.hover_style);
|
||||||
|
} else {
|
||||||
|
// eprintln!("div NOT hovered {bounds:?} {mouse_position:?}");
|
||||||
}
|
}
|
||||||
|
// }
|
||||||
|
|
||||||
if let Some(drag) = cx.active_drag.take() {
|
if let Some(drag) = cx.active_drag.take() {
|
||||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||||
|
|
|
@ -81,7 +81,7 @@ impl<V> Element<V> for Img<V> {
|
||||||
if let Some(data) = image_future
|
if let Some(data) = image_future
|
||||||
.clone()
|
.clone()
|
||||||
.now_or_never()
|
.now_or_never()
|
||||||
.and_then(ResultExt::log_err)
|
.and_then(|result| result.ok())
|
||||||
{
|
{
|
||||||
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size());
|
||||||
cx.with_z_index(1, |cx| {
|
cx.with_z_index(1, |cx| {
|
||||||
|
@ -90,7 +90,7 @@ impl<V> Element<V> for Img<V> {
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cx.spawn(|_, mut cx| async move {
|
cx.spawn(|_, mut cx| async move {
|
||||||
if image_future.await.log_err().is_some() {
|
if image_future.await.ok().is_some() {
|
||||||
cx.on_next_frame(|cx| cx.notify());
|
cx.on_next_frame(|cx| cx.notify());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -141,6 +141,10 @@ pub trait VisualContext: Context {
|
||||||
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
fn focus_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
where
|
where
|
||||||
V: FocusableView;
|
V: FocusableView;
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: ManagedView;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait Entity<T>: Sealed {
|
pub trait Entity<T>: Sealed {
|
||||||
|
|
|
@ -2,7 +2,7 @@ use crate::{ImageData, ImageId, SharedString};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::{
|
use futures::{
|
||||||
future::{BoxFuture, Shared},
|
future::{BoxFuture, Shared},
|
||||||
AsyncReadExt, FutureExt,
|
AsyncReadExt, FutureExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
use image::ImageError;
|
use image::ImageError;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -88,6 +88,14 @@ impl ImageCache {
|
||||||
Ok(Arc::new(ImageData::new(image)))
|
Ok(Arc::new(ImageData::new(image)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.map_err({
|
||||||
|
let uri = uri.clone();
|
||||||
|
|
||||||
|
move |error| {
|
||||||
|
log::log!(log::Level::Error, "{:?} {:?}", &uri, &error);
|
||||||
|
error
|
||||||
|
}
|
||||||
|
})
|
||||||
.boxed()
|
.boxed()
|
||||||
.shared();
|
.shared();
|
||||||
|
|
||||||
|
|
|
@ -1205,10 +1205,7 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
|
||||||
|
|
||||||
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
InputEvent::MouseMove(_) if !(is_active || lock.kind == WindowKind::PopUp) => return,
|
||||||
|
|
||||||
InputEvent::MouseUp(MouseUpEvent {
|
InputEvent::MouseUp(MouseUpEvent { .. }) => {
|
||||||
button: MouseButton::Left,
|
|
||||||
..
|
|
||||||
}) => {
|
|
||||||
lock.synthetic_drag_counter += 1;
|
lock.synthetic_drag_counter += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -191,6 +191,10 @@ impl AnyView {
|
||||||
self.model.entity_type
|
self.model.entity_type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn entity_id(&self) -> EntityId {
|
||||||
|
self.model.entity_id()
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn draw(
|
pub(crate) fn draw(
|
||||||
&self,
|
&self,
|
||||||
origin: Point<Pixels>,
|
origin: Point<Pixels>,
|
||||||
|
|
|
@ -193,17 +193,12 @@ pub trait FocusableView: Render {
|
||||||
|
|
||||||
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
/// ManagedView is a view (like a Modal, Popover, Menu, etc.)
|
||||||
/// where the lifecycle of the view is handled by another view.
|
/// where the lifecycle of the view is handled by another view.
|
||||||
pub trait ManagedView: Render {
|
pub trait ManagedView: FocusableView + EventEmitter<Manager> {}
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Dismiss;
|
impl<M: FocusableView + EventEmitter<Manager>> ManagedView for M {}
|
||||||
impl<T: ManagedView> EventEmitter<Dismiss> for T {}
|
|
||||||
|
|
||||||
impl<T: ManagedView> FocusableView for T {
|
pub enum Manager {
|
||||||
fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
|
Dismiss,
|
||||||
self.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Holds the state for a specific window.
|
// Holds the state for a specific window.
|
||||||
|
@ -1582,6 +1577,13 @@ impl VisualContext for WindowContext<'_> {
|
||||||
view.focus_handle(cx).clone().focus(cx);
|
view.focus_handle(cx).clone().focus(cx);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<V>(&mut self, view: &View<V>) -> Self::Result<()>
|
||||||
|
where
|
||||||
|
V: ManagedView,
|
||||||
|
{
|
||||||
|
self.update_view(view, |_, cx| cx.emit(Manager::Dismiss))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> std::ops::Deref for WindowContext<'a> {
|
impl<'a> std::ops::Deref for WindowContext<'a> {
|
||||||
|
@ -2275,6 +2277,13 @@ impl<'a, V: 'static> ViewContext<'a, V> {
|
||||||
{
|
{
|
||||||
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
self.defer(|view, cx| view.focus_handle(cx).focus(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn dismiss_self(&mut self)
|
||||||
|
where
|
||||||
|
V: ManagedView,
|
||||||
|
{
|
||||||
|
self.defer(|_, cx| cx.emit(Manager::Dismiss))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<V> Context for ViewContext<'_, V> {
|
impl<V> Context for ViewContext<'_, V> {
|
||||||
|
@ -2354,6 +2363,10 @@ impl<V: 'static> VisualContext for ViewContext<'_, V> {
|
||||||
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
fn focus_view<W: FocusableView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
||||||
self.window_cx.focus_view(view)
|
self.window_cx.focus_view(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn dismiss_view<W: ManagedView>(&mut self, view: &View<W>) -> Self::Result<()> {
|
||||||
|
self.window_cx.dismiss_view(view)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
|
impl<'a, V> std::ops::Deref for ViewContext<'a, V> {
|
||||||
|
|
|
@ -20,10 +20,6 @@ impl IgnoreStack {
|
||||||
Arc::new(Self::All)
|
Arc::new(Self::All)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_all(&self) -> bool {
|
|
||||||
matches!(self, IgnoreStack::All)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||||
match self.as_ref() {
|
match self.as_ref() {
|
||||||
IgnoreStack::All => self,
|
IgnoreStack::All => self,
|
||||||
|
|
|
@ -5548,7 +5548,16 @@ impl Project {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let background = cx.background().clone();
|
let background = cx.background().clone();
|
||||||
let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
|
let path_count: usize = snapshots
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
if query.include_ignored() {
|
||||||
|
s.file_count()
|
||||||
|
} else {
|
||||||
|
s.visible_file_count()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
if path_count == 0 {
|
if path_count == 0 {
|
||||||
let (_, rx) = smol::channel::bounded(1024);
|
let (_, rx) = smol::channel::bounded(1024);
|
||||||
return rx;
|
return rx;
|
||||||
|
@ -5561,8 +5570,16 @@ impl Project {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, b)| {
|
.filter_map(|(_, b)| {
|
||||||
let buffer = b.upgrade(cx)?;
|
let buffer = b.upgrade(cx)?;
|
||||||
let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
|
let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
|
||||||
if let Some(path) = snapshot.file().map(|file| file.path()) {
|
let is_ignored = buffer
|
||||||
|
.project_path(cx)
|
||||||
|
.and_then(|path| self.entry_for_path(&path, cx))
|
||||||
|
.map_or(false, |entry| entry.is_ignored);
|
||||||
|
(is_ignored, buffer.snapshot())
|
||||||
|
});
|
||||||
|
if is_ignored && !query.include_ignored() {
|
||||||
|
return None;
|
||||||
|
} else if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||||
Some((path.clone(), (buffer, snapshot)))
|
Some((path.clone(), (buffer, snapshot)))
|
||||||
} else {
|
} else {
|
||||||
unnamed_files.push(buffer);
|
unnamed_files.push(buffer);
|
||||||
|
@ -5735,7 +5752,12 @@ impl Project {
|
||||||
let mut snapshot_start_ix = 0;
|
let mut snapshot_start_ix = 0;
|
||||||
let mut abs_path = PathBuf::new();
|
let mut abs_path = PathBuf::new();
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
|
let snapshot_end_ix = snapshot_start_ix
|
||||||
|
+ if query.include_ignored() {
|
||||||
|
snapshot.file_count()
|
||||||
|
} else {
|
||||||
|
snapshot.visible_file_count()
|
||||||
|
};
|
||||||
if worker_end_ix <= snapshot_start_ix {
|
if worker_end_ix <= snapshot_start_ix {
|
||||||
break;
|
break;
|
||||||
} else if worker_start_ix > snapshot_end_ix {
|
} else if worker_start_ix > snapshot_end_ix {
|
||||||
|
@ -5748,7 +5770,7 @@ impl Project {
|
||||||
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
||||||
|
|
||||||
for entry in snapshot
|
for entry in snapshot
|
||||||
.files(false, start_in_snapshot)
|
.files(query.include_ignored(), start_in_snapshot)
|
||||||
.take(end_in_snapshot - start_in_snapshot)
|
.take(end_in_snapshot - start_in_snapshot)
|
||||||
{
|
{
|
||||||
if matching_paths_tx.is_closed() {
|
if matching_paths_tx.is_closed() {
|
||||||
|
|
|
@ -10,6 +10,8 @@ pub struct ProjectSettings {
|
||||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub git: GitSettings,
|
pub git: GitSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub file_scan_exclusions: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
|
@ -3598,7 +3598,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(
|
search(
|
||||||
&project,
|
&project,
|
||||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx
|
cx
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -3623,7 +3623,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(
|
search(
|
||||||
&project,
|
&project,
|
||||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx
|
cx
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -3662,6 +3662,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
Vec::new()
|
Vec::new()
|
||||||
)
|
)
|
||||||
|
@ -3681,6 +3682,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.rs").unwrap()],
|
vec![PathMatcher::new("*.rs").unwrap()],
|
||||||
Vec::new()
|
Vec::new()
|
||||||
)
|
)
|
||||||
|
@ -3703,6 +3705,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap(),
|
PathMatcher::new("*.odd").unwrap(),
|
||||||
|
@ -3727,6 +3730,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.rs").unwrap(),
|
PathMatcher::new("*.rs").unwrap(),
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
|
@ -3774,6 +3778,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -3798,6 +3803,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![PathMatcher::new("*.rs").unwrap()],
|
vec![PathMatcher::new("*.rs").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -3820,6 +3826,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
|
@ -3844,6 +3851,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.rs").unwrap(),
|
PathMatcher::new("*.rs").unwrap(),
|
||||||
|
@ -3885,6 +3893,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -3904,6 +3913,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.ts").unwrap()],
|
vec![PathMatcher::new("*.ts").unwrap()],
|
||||||
vec![PathMatcher::new("*.ts").unwrap()],
|
vec![PathMatcher::new("*.ts").unwrap()],
|
||||||
).unwrap(),
|
).unwrap(),
|
||||||
|
@ -3922,6 +3932,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap()
|
PathMatcher::new("*.odd").unwrap()
|
||||||
|
@ -3947,6 +3958,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap()
|
PathMatcher::new("*.odd").unwrap()
|
||||||
|
|
|
@ -39,6 +39,7 @@ pub enum SearchQuery {
|
||||||
replacement: Option<String>,
|
replacement: Option<String>,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
inner: SearchInputs,
|
inner: SearchInputs,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ pub enum SearchQuery {
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
inner: SearchInputs,
|
inner: SearchInputs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -57,6 +59,7 @@ impl SearchQuery {
|
||||||
query: impl ToString,
|
query: impl ToString,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
files_to_include: Vec<PathMatcher>,
|
files_to_include: Vec<PathMatcher>,
|
||||||
files_to_exclude: Vec<PathMatcher>,
|
files_to_exclude: Vec<PathMatcher>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -74,6 +77,7 @@ impl SearchQuery {
|
||||||
replacement: None,
|
replacement: None,
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
|
include_ignored,
|
||||||
inner,
|
inner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -82,6 +86,7 @@ impl SearchQuery {
|
||||||
query: impl ToString,
|
query: impl ToString,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
files_to_include: Vec<PathMatcher>,
|
files_to_include: Vec<PathMatcher>,
|
||||||
files_to_exclude: Vec<PathMatcher>,
|
files_to_exclude: Vec<PathMatcher>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -111,6 +116,7 @@ impl SearchQuery {
|
||||||
multiline,
|
multiline,
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
|
include_ignored,
|
||||||
inner,
|
inner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -121,6 +127,7 @@ impl SearchQuery {
|
||||||
message.query,
|
message.query,
|
||||||
message.whole_word,
|
message.whole_word,
|
||||||
message.case_sensitive,
|
message.case_sensitive,
|
||||||
|
message.include_ignored,
|
||||||
deserialize_path_matches(&message.files_to_include)?,
|
deserialize_path_matches(&message.files_to_include)?,
|
||||||
deserialize_path_matches(&message.files_to_exclude)?,
|
deserialize_path_matches(&message.files_to_exclude)?,
|
||||||
)
|
)
|
||||||
|
@ -129,6 +136,7 @@ impl SearchQuery {
|
||||||
message.query,
|
message.query,
|
||||||
message.whole_word,
|
message.whole_word,
|
||||||
message.case_sensitive,
|
message.case_sensitive,
|
||||||
|
message.include_ignored,
|
||||||
deserialize_path_matches(&message.files_to_include)?,
|
deserialize_path_matches(&message.files_to_include)?,
|
||||||
deserialize_path_matches(&message.files_to_exclude)?,
|
deserialize_path_matches(&message.files_to_exclude)?,
|
||||||
)
|
)
|
||||||
|
@ -156,6 +164,7 @@ impl SearchQuery {
|
||||||
regex: self.is_regex(),
|
regex: self.is_regex(),
|
||||||
whole_word: self.whole_word(),
|
whole_word: self.whole_word(),
|
||||||
case_sensitive: self.case_sensitive(),
|
case_sensitive: self.case_sensitive(),
|
||||||
|
include_ignored: self.include_ignored(),
|
||||||
files_to_include: self
|
files_to_include: self
|
||||||
.files_to_include()
|
.files_to_include()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -336,6 +345,17 @@ impl SearchQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn include_ignored(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Text {
|
||||||
|
include_ignored, ..
|
||||||
|
} => *include_ignored,
|
||||||
|
Self::Regex {
|
||||||
|
include_ignored, ..
|
||||||
|
} => *include_ignored,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_regex(&self) -> bool {
|
pub fn is_regex(&self) -> bool {
|
||||||
matches!(self, Self::Regex { .. })
|
matches!(self, Self::Regex { .. })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
|
copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
|
||||||
|
ProjectEntryId, RemoveOptions,
|
||||||
};
|
};
|
||||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
@ -21,7 +22,10 @@ use futures::{
|
||||||
};
|
};
|
||||||
use fuzzy::CharBag;
|
use fuzzy::CharBag;
|
||||||
use git::{DOT_GIT, GITIGNORE};
|
use git::{DOT_GIT, GITIGNORE};
|
||||||
use gpui::{executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task};
|
use gpui::{
|
||||||
|
executor, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Subscription, Task,
|
||||||
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||||
|
@ -36,6 +40,7 @@ use postage::{
|
||||||
prelude::{Sink as _, Stream as _},
|
prelude::{Sink as _, Stream as _},
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
|
use settings::SettingsStore;
|
||||||
use smol::channel::{self, Sender};
|
use smol::channel::{self, Sender};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
@ -55,7 +60,10 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||||
use util::{paths::HOME, ResultExt};
|
use util::{
|
||||||
|
paths::{PathMatcher, HOME},
|
||||||
|
ResultExt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
pub struct WorktreeId(usize);
|
pub struct WorktreeId(usize);
|
||||||
|
@ -70,7 +78,8 @@ pub struct LocalWorktree {
|
||||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||||
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
||||||
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
||||||
_background_scanner_task: Task<()>,
|
_settings_subscription: Subscription,
|
||||||
|
_background_scanner_tasks: Vec<Task<()>>,
|
||||||
share: Option<ShareState>,
|
share: Option<ShareState>,
|
||||||
diagnostics: HashMap<
|
diagnostics: HashMap<
|
||||||
Arc<Path>,
|
Arc<Path>,
|
||||||
|
@ -216,6 +225,7 @@ pub struct LocalSnapshot {
|
||||||
/// All of the git repositories in the worktree, indexed by the project entry
|
/// All of the git repositories in the worktree, indexed by the project entry
|
||||||
/// id of their parent directory.
|
/// id of their parent directory.
|
||||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||||
|
file_scan_exclusions: Vec<PathMatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackgroundScannerState {
|
struct BackgroundScannerState {
|
||||||
|
@ -299,17 +309,54 @@ impl Worktree {
|
||||||
.await
|
.await
|
||||||
.context("failed to stat worktree path")?;
|
.context("failed to stat worktree path")?;
|
||||||
|
|
||||||
|
let closure_fs = Arc::clone(&fs);
|
||||||
|
let closure_next_entry_id = Arc::clone(&next_entry_id);
|
||||||
|
let closure_abs_path = abs_path.to_path_buf();
|
||||||
Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
Ok(cx.add_model(move |cx: &mut ModelContext<Worktree>| {
|
||||||
|
let settings_subscription = cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
||||||
|
if let Self::Local(this) = this {
|
||||||
|
let new_file_scan_exclusions =
|
||||||
|
file_scan_exclusions(settings::get::<ProjectSettings>(cx));
|
||||||
|
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
|
||||||
|
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
||||||
|
log::info!(
|
||||||
|
"Re-scanning directories, new scan exclude files: {:?}",
|
||||||
|
this.snapshot
|
||||||
|
.file_scan_exclusions
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||||
|
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
|
||||||
|
channel::unbounded();
|
||||||
|
this.scan_requests_tx = scan_requests_tx;
|
||||||
|
this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
|
||||||
|
this._background_scanner_tasks = start_background_scan_tasks(
|
||||||
|
&closure_abs_path,
|
||||||
|
this.snapshot(),
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
Arc::clone(&closure_next_entry_id),
|
||||||
|
Arc::clone(&closure_fs),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
this.is_scanning = watch::channel_with(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let root_name = abs_path
|
let root_name = abs_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||||
|
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
|
file_scan_exclusions: file_scan_exclusions(settings::get::<ProjectSettings>(cx)),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
git_repositories: Default::default(),
|
git_repositories: Default::default(),
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
id: WorktreeId::from_usize(cx.model_id()),
|
id: WorktreeId::from_usize(cx.model_id()),
|
||||||
abs_path: abs_path.clone(),
|
abs_path: abs_path.to_path_buf().into(),
|
||||||
root_name: root_name.clone(),
|
root_name: root_name.clone(),
|
||||||
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
||||||
entries_by_path: Default::default(),
|
entries_by_path: Default::default(),
|
||||||
|
@ -334,60 +381,23 @@ impl Worktree {
|
||||||
|
|
||||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
||||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
let task_snapshot = snapshot.clone();
|
||||||
|
|
||||||
cx.spawn_weak(|this, mut cx| async move {
|
|
||||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let this = this.as_local_mut().unwrap();
|
|
||||||
match state {
|
|
||||||
ScanState::Started => {
|
|
||||||
*this.is_scanning.0.borrow_mut() = true;
|
|
||||||
}
|
|
||||||
ScanState::Updated {
|
|
||||||
snapshot,
|
|
||||||
changes,
|
|
||||||
barrier,
|
|
||||||
scanning,
|
|
||||||
} => {
|
|
||||||
*this.is_scanning.0.borrow_mut() = scanning;
|
|
||||||
this.set_snapshot(snapshot, changes, cx);
|
|
||||||
drop(barrier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let background_scanner_task = cx.background().spawn({
|
|
||||||
let fs = fs.clone();
|
|
||||||
let snapshot = snapshot.clone();
|
|
||||||
let background = cx.background().clone();
|
|
||||||
async move {
|
|
||||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
|
||||||
BackgroundScanner::new(
|
|
||||||
snapshot,
|
|
||||||
next_entry_id,
|
|
||||||
fs,
|
|
||||||
scan_states_tx,
|
|
||||||
background,
|
|
||||||
scan_requests_rx,
|
|
||||||
path_prefixes_to_scan_rx,
|
|
||||||
)
|
|
||||||
.run(events)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Worktree::Local(LocalWorktree {
|
Worktree::Local(LocalWorktree {
|
||||||
snapshot,
|
snapshot,
|
||||||
is_scanning: watch::channel_with(true),
|
is_scanning: watch::channel_with(true),
|
||||||
share: None,
|
share: None,
|
||||||
scan_requests_tx,
|
scan_requests_tx,
|
||||||
path_prefixes_to_scan_tx,
|
path_prefixes_to_scan_tx,
|
||||||
_background_scanner_task: background_scanner_task,
|
_settings_subscription: settings_subscription,
|
||||||
|
_background_scanner_tasks: start_background_scan_tasks(
|
||||||
|
&abs_path,
|
||||||
|
task_snapshot,
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
Arc::clone(&next_entry_id),
|
||||||
|
Arc::clone(&fs),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
diagnostics: Default::default(),
|
diagnostics: Default::default(),
|
||||||
diagnostic_summaries: Default::default(),
|
diagnostic_summaries: Default::default(),
|
||||||
client,
|
client,
|
||||||
|
@ -584,6 +594,76 @@ impl Worktree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_background_scan_tasks(
|
||||||
|
abs_path: &Path,
|
||||||
|
snapshot: LocalSnapshot,
|
||||||
|
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||||
|
path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
|
||||||
|
next_entry_id: Arc<AtomicUsize>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: &mut ModelContext<'_, Worktree>,
|
||||||
|
) -> Vec<Task<()>> {
|
||||||
|
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||||
|
let background_scanner = cx.background().spawn({
|
||||||
|
let abs_path = abs_path.to_path_buf();
|
||||||
|
let background = cx.background().clone();
|
||||||
|
async move {
|
||||||
|
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||||
|
BackgroundScanner::new(
|
||||||
|
snapshot,
|
||||||
|
next_entry_id,
|
||||||
|
fs,
|
||||||
|
scan_states_tx,
|
||||||
|
background,
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
)
|
||||||
|
.run(events)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let scan_state_updater = cx.spawn_weak(|this, mut cx| async move {
|
||||||
|
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade(&cx)) {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let this = this.as_local_mut().unwrap();
|
||||||
|
match state {
|
||||||
|
ScanState::Started => {
|
||||||
|
*this.is_scanning.0.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
ScanState::Updated {
|
||||||
|
snapshot,
|
||||||
|
changes,
|
||||||
|
barrier,
|
||||||
|
scanning,
|
||||||
|
} => {
|
||||||
|
*this.is_scanning.0.borrow_mut() = scanning;
|
||||||
|
this.set_snapshot(snapshot, changes, cx);
|
||||||
|
drop(barrier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vec![background_scanner, scan_state_updater]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
|
||||||
|
project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
|
||||||
|
.sorted()
|
||||||
|
.filter_map(|pattern| {
|
||||||
|
PathMatcher::new(pattern)
|
||||||
|
.map(Some)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl LocalWorktree {
|
impl LocalWorktree {
|
||||||
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
||||||
path.starts_with(&self.abs_path)
|
path.starts_with(&self.abs_path)
|
||||||
|
@ -1481,7 +1561,7 @@ impl Snapshot {
|
||||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||||
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
||||||
let old_entry = self.entries_by_id.insert_or_replace(
|
let old_entry = self.entries_by_id.insert_or_replace(
|
||||||
PathEntry {
|
PathEntry {
|
||||||
|
@ -2145,6 +2225,12 @@ impl LocalSnapshot {
|
||||||
paths.sort_by(|a, b| a.0.cmp(b.0));
|
paths.sort_by(|a, b| a.0.cmp(b.0));
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
||||||
|
self.file_scan_exclusions
|
||||||
|
.iter()
|
||||||
|
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundScannerState {
|
impl BackgroundScannerState {
|
||||||
|
@ -2167,7 +2253,7 @@ impl BackgroundScannerState {
|
||||||
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
||||||
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
||||||
let mut containing_repository = None;
|
let mut containing_repository = None;
|
||||||
if !ignore_stack.is_all() {
|
if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
|
||||||
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
||||||
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
||||||
containing_repository = Some((
|
containing_repository = Some((
|
||||||
|
@ -2378,18 +2464,30 @@ impl BackgroundScannerState {
|
||||||
|
|
||||||
// Remove any git repositories whose .git entry no longer exists.
|
// Remove any git repositories whose .git entry no longer exists.
|
||||||
let snapshot = &mut self.snapshot;
|
let snapshot = &mut self.snapshot;
|
||||||
let mut repositories = mem::take(&mut snapshot.git_repositories);
|
let mut ids_to_preserve = HashSet::default();
|
||||||
let mut repository_entries = mem::take(&mut snapshot.repository_entries);
|
for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
|
||||||
repositories.retain(|work_directory_id, _| {
|
let exists_in_snapshot = snapshot
|
||||||
snapshot
|
.entry_for_id(work_directory_id)
|
||||||
.entry_for_id(*work_directory_id)
|
|
||||||
.map_or(false, |entry| {
|
.map_or(false, |entry| {
|
||||||
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
||||||
})
|
|
||||||
});
|
});
|
||||||
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
|
if exists_in_snapshot {
|
||||||
snapshot.git_repositories = repositories;
|
ids_to_preserve.insert(work_directory_id);
|
||||||
snapshot.repository_entries = repository_entries;
|
} else {
|
||||||
|
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||||
|
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||||
|
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||||
|
{
|
||||||
|
ids_to_preserve.insert(work_directory_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
|
.git_repositories
|
||||||
|
.retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
|
||||||
|
snapshot
|
||||||
|
.repository_entries
|
||||||
|
.retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_git_repository(
|
fn build_git_repository(
|
||||||
|
@ -3094,7 +3192,7 @@ impl BackgroundScanner {
|
||||||
let ignore_stack = state
|
let ignore_stack = state
|
||||||
.snapshot
|
.snapshot
|
||||||
.ignore_stack_for_abs_path(&root_abs_path, true);
|
.ignore_stack_for_abs_path(&root_abs_path, true);
|
||||||
if ignore_stack.is_all() {
|
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
|
||||||
root_entry.is_ignored = true;
|
root_entry.is_ignored = true;
|
||||||
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
||||||
}
|
}
|
||||||
|
@ -3231,6 +3329,7 @@ impl BackgroundScanner {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !is_git_related(&abs_path) {
|
||||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||||
snapshot
|
snapshot
|
||||||
.entry_for_path(parent)
|
.entry_for_path(parent)
|
||||||
|
@ -3240,6 +3339,13 @@ impl BackgroundScanner {
|
||||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if snapshot.is_abs_path_excluded(abs_path) {
|
||||||
|
log::debug!(
|
||||||
|
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
relative_paths.push(relative_path);
|
relative_paths.push(relative_path);
|
||||||
true
|
true
|
||||||
|
@ -3401,18 +3507,26 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||||
log::debug!("scan directory {:?}", job.path);
|
let root_abs_path;
|
||||||
|
let mut ignore_stack;
|
||||||
let mut ignore_stack = job.ignore_stack.clone();
|
let mut new_ignore;
|
||||||
let mut new_ignore = None;
|
let root_char_bag;
|
||||||
let (root_abs_path, root_char_bag, next_entry_id) = {
|
let next_entry_id;
|
||||||
let snapshot = &self.state.lock().snapshot;
|
{
|
||||||
(
|
let state = self.state.lock();
|
||||||
snapshot.abs_path().clone(),
|
let snapshot = &state.snapshot;
|
||||||
snapshot.root_char_bag,
|
root_abs_path = snapshot.abs_path().clone();
|
||||||
self.next_entry_id.clone(),
|
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||||
)
|
log::error!("skipping excluded directory {:?}", job.path);
|
||||||
};
|
return Ok(());
|
||||||
|
}
|
||||||
|
log::debug!("scanning directory {:?}", job.path);
|
||||||
|
ignore_stack = job.ignore_stack.clone();
|
||||||
|
new_ignore = None;
|
||||||
|
root_char_bag = snapshot.root_char_bag;
|
||||||
|
next_entry_id = self.next_entry_id.clone();
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
|
||||||
let mut dotgit_path = None;
|
let mut dotgit_path = None;
|
||||||
let mut root_canonical_path = None;
|
let mut root_canonical_path = None;
|
||||||
|
@ -3427,18 +3541,8 @@ impl BackgroundScanner {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let child_name = child_abs_path.file_name().unwrap();
|
let child_name = child_abs_path.file_name().unwrap();
|
||||||
let child_path: Arc<Path> = job.path.join(child_name).into();
|
let child_path: Arc<Path> = job.path.join(child_name).into();
|
||||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
|
||||||
Ok(Some(metadata)) => metadata,
|
|
||||||
Ok(None) => continue,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("error processing {:?}: {:?}", child_abs_path, err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
||||||
if child_name == *GITIGNORE {
|
if child_name == *GITIGNORE {
|
||||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||||
|
@ -3482,6 +3586,26 @@ impl BackgroundScanner {
|
||||||
dotgit_path = Some(child_path.clone());
|
dotgit_path = Some(child_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
||||||
|
let relative_path = job.path.join(child_name);
|
||||||
|
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||||
|
state.remove_path(&relative_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||||
|
Ok(Some(metadata)) => metadata,
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("error processing {child_abs_path:?}: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut child_entry = Entry::new(
|
let mut child_entry = Entry::new(
|
||||||
child_path.clone(),
|
child_path.clone(),
|
||||||
&child_metadata,
|
&child_metadata,
|
||||||
|
@ -3662,14 +3786,12 @@ impl BackgroundScanner {
|
||||||
self.next_entry_id.as_ref(),
|
self.next_entry_id.as_ref(),
|
||||||
state.snapshot.root_char_bag,
|
state.snapshot.root_char_bag,
|
||||||
);
|
);
|
||||||
fs_entry.is_ignored = ignore_stack.is_all();
|
let is_dir = fs_entry.is_dir();
|
||||||
|
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||||
|
|
||||||
if !fs_entry.is_ignored {
|
if !is_dir && !fs_entry.is_ignored {
|
||||||
if !fs_entry.is_dir() {
|
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
|
||||||
if let Some((work_dir, repo)) =
|
|
||||||
state.snapshot.local_repo_for_path(&path)
|
|
||||||
{
|
|
||||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||||
let repo_path = RepoPath(repo_path.into());
|
let repo_path = RepoPath(repo_path.into());
|
||||||
let repo = repo.repo_ptr.lock();
|
let repo = repo.repo_ptr.lock();
|
||||||
|
@ -3677,7 +3799,6 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
|
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
|
||||||
if state.should_scan_directory(&fs_entry) {
|
if state.should_scan_directory(&fs_entry) {
|
||||||
|
@ -3833,8 +3954,7 @@ impl BackgroundScanner {
|
||||||
ignore_stack.clone()
|
ignore_stack.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan any directories that were previously ignored and weren't
|
// Scan any directories that were previously ignored and weren't previously scanned.
|
||||||
// previously scanned.
|
|
||||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
if state.should_scan_directory(&entry) {
|
if state.should_scan_directory(&entry) {
|
||||||
|
@ -4010,6 +4130,12 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_git_related(abs_path: &Path) -> bool {
|
||||||
|
abs_path
|
||||||
|
.components()
|
||||||
|
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
||||||
|
}
|
||||||
|
|
||||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||||
let mut result = root_char_bag;
|
let mut result = root_char_bag;
|
||||||
result.extend(
|
result.extend(
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
|
project_settings::ProjectSettings,
|
||||||
worktree::{Event, Snapshot, WorktreeModelHandle},
|
worktree::{Event, Snapshot, WorktreeModelHandle},
|
||||||
Entry, EntryKind, PathChange, Worktree,
|
Entry, EntryKind, PathChange, Project, Worktree,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
|
@ -12,6 +13,7 @@ use postage::stream::Stream;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
env,
|
env,
|
||||||
fmt::Write,
|
fmt::Write,
|
||||||
|
@ -23,6 +25,7 @@ use util::{http::FakeHttpClient, test::temp_tree, ResultExt};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_traversal(cx: &mut TestAppContext) {
|
async fn test_traversal(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -78,6 +81,7 @@ async fn test_traversal(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_descendent_entries(cx: &mut TestAppContext) {
|
async fn test_descendent_entries(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -185,6 +189,7 @@ async fn test_descendent_entries(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
|
async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -264,6 +269,7 @@ async fn test_circular_symlinks(executor: Arc<Deterministic>, cx: &mut TestAppCo
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -439,6 +445,7 @@ async fn test_symlinks_pointing_outside(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -599,6 +606,7 @@ async fn test_open_gitignored_files(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -722,6 +730,14 @@ async fn test_dirs_no_longer_ignored(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test(iterations = 10)]
|
#[gpui::test(iterations = 10)]
|
||||||
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -827,6 +843,7 @@ async fn test_rescan_with_gitignore(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_write_file(cx: &mut TestAppContext) {
|
async fn test_write_file(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let dir = temp_tree(json!({
|
let dir = temp_tree(json!({
|
||||||
".git": {},
|
".git": {},
|
||||||
".gitignore": "ignored-dir\n",
|
".gitignore": "ignored-dir\n",
|
||||||
|
@ -877,8 +894,105 @@ async fn test_write_file(cx: &mut TestAppContext) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_file_scan_exclusions(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let dir = temp_tree(json!({
|
||||||
|
".gitignore": "**/target\n/node_modules\n",
|
||||||
|
"target": {
|
||||||
|
"index": "blah2"
|
||||||
|
},
|
||||||
|
"node_modules": {
|
||||||
|
".DS_Store": "",
|
||||||
|
"prettier": {
|
||||||
|
"package.json": "{}",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"src": {
|
||||||
|
".DS_Store": "",
|
||||||
|
"foo": {
|
||||||
|
"foo.rs": "mod another;\n",
|
||||||
|
"another.rs": "// another",
|
||||||
|
},
|
||||||
|
"bar": {
|
||||||
|
"bar.rs": "// bar",
|
||||||
|
},
|
||||||
|
"lib.rs": "mod foo;\nmod bar;\n",
|
||||||
|
},
|
||||||
|
".DS_Store": "",
|
||||||
|
}));
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/foo/**".to_string(), "**/.DS_Store".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let tree = Worktree::local(
|
||||||
|
build_client(cx),
|
||||||
|
dir.path(),
|
||||||
|
true,
|
||||||
|
Arc::new(RealFs),
|
||||||
|
Default::default(),
|
||||||
|
&mut cx.to_async(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
|
||||||
|
.await;
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
tree.read_with(cx, |tree, _| {
|
||||||
|
check_worktree_entries(
|
||||||
|
tree,
|
||||||
|
&[
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"src/.DS_Store",
|
||||||
|
".DS_Store",
|
||||||
|
],
|
||||||
|
&["target", "node_modules"],
|
||||||
|
&["src/lib.rs", "src/bar/bar.rs", ".gitignore"],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/node_modules/**".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
tree.flush_fs_events(cx).await;
|
||||||
|
cx.foreground().run_until_parked();
|
||||||
|
tree.read_with(cx, |tree, _| {
|
||||||
|
check_worktree_entries(
|
||||||
|
tree,
|
||||||
|
&[
|
||||||
|
"node_modules/prettier/package.json",
|
||||||
|
"node_modules/.DS_Store",
|
||||||
|
"node_modules",
|
||||||
|
],
|
||||||
|
&["target"],
|
||||||
|
&[
|
||||||
|
".gitignore",
|
||||||
|
"src/lib.rs",
|
||||||
|
"src/bar/bar.rs",
|
||||||
|
"src/foo/foo.rs",
|
||||||
|
"src/foo/another.rs",
|
||||||
|
"src/.DS_Store",
|
||||||
|
".DS_Store",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -938,6 +1052,7 @@ async fn test_create_directory_during_initial_scan(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
async fn test_create_dir_all_on_create_entry(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
let client_fake = cx.read(|cx| Client::new(FakeHttpClient::with_404_response(), cx));
|
||||||
|
|
||||||
let fs_fake = FakeFs::new(cx.background());
|
let fs_fake = FakeFs::new(cx.background());
|
||||||
|
@ -1054,6 +1169,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
||||||
cx: &mut TestAppContext,
|
cx: &mut TestAppContext,
|
||||||
mut rng: StdRng,
|
mut rng: StdRng,
|
||||||
) {
|
) {
|
||||||
|
init_test(cx);
|
||||||
let operations = env::var("OPERATIONS")
|
let operations = env::var("OPERATIONS")
|
||||||
.map(|o| o.parse().unwrap())
|
.map(|o| o.parse().unwrap())
|
||||||
.unwrap_or(5);
|
.unwrap_or(5);
|
||||||
|
@ -1143,6 +1259,7 @@ async fn test_random_worktree_operations_during_initial_scan(
|
||||||
|
|
||||||
#[gpui::test(iterations = 100)]
|
#[gpui::test(iterations = 100)]
|
||||||
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
|
async fn test_random_worktree_changes(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||||
|
init_test(cx);
|
||||||
let operations = env::var("OPERATIONS")
|
let operations = env::var("OPERATIONS")
|
||||||
.map(|o| o.parse().unwrap())
|
.map(|o| o.parse().unwrap())
|
||||||
.unwrap_or(40);
|
.unwrap_or(40);
|
||||||
|
@ -1557,6 +1674,7 @@ fn random_filename(rng: &mut impl Rng) -> String {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let root = temp_tree(json!({
|
let root = temp_tree(json!({
|
||||||
"projects": {
|
"projects": {
|
||||||
"project1": {
|
"project1": {
|
||||||
|
@ -1627,6 +1745,7 @@ async fn test_rename_work_directory(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let root = temp_tree(json!({
|
let root = temp_tree(json!({
|
||||||
"c.txt": "",
|
"c.txt": "",
|
||||||
"dir1": {
|
"dir1": {
|
||||||
|
@ -1747,6 +1866,15 @@ async fn test_git_repository_for_path(cx: &mut TestAppContext) {
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/.git".to_string(), "**/.gitignore".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
const IGNORE_RULE: &'static str = "**/target";
|
const IGNORE_RULE: &'static str = "**/target";
|
||||||
|
|
||||||
let root = temp_tree(json!({
|
let root = temp_tree(json!({
|
||||||
|
@ -1935,6 +2063,7 @@ async fn test_git_status(deterministic: Arc<Deterministic>, cx: &mut TestAppCont
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
async fn test_propagate_git_statuses(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
let fs = FakeFs::new(cx.background());
|
let fs = FakeFs::new(cx.background());
|
||||||
fs.insert_tree(
|
fs.insert_tree(
|
||||||
"/root",
|
"/root",
|
||||||
|
@ -2139,3 +2268,44 @@ fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Sta
|
||||||
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
.map(|status| (status.path().unwrap().to_string(), status.status()))
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[track_caller]
|
||||||
|
fn check_worktree_entries(
|
||||||
|
tree: &Worktree,
|
||||||
|
expected_excluded_paths: &[&str],
|
||||||
|
expected_ignored_paths: &[&str],
|
||||||
|
expected_tracked_paths: &[&str],
|
||||||
|
) {
|
||||||
|
for path in expected_excluded_paths {
|
||||||
|
let entry = tree.entry_for_path(path);
|
||||||
|
assert!(
|
||||||
|
entry.is_none(),
|
||||||
|
"expected path '{path}' to be excluded, but got entry: {entry:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for path in expected_ignored_paths {
|
||||||
|
let entry = tree
|
||||||
|
.entry_for_path(path)
|
||||||
|
.unwrap_or_else(|| panic!("Missing entry for expected ignored path '{path}'"));
|
||||||
|
assert!(
|
||||||
|
entry.is_ignored,
|
||||||
|
"expected path '{path}' to be ignored, but got entry: {entry:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for path in expected_tracked_paths {
|
||||||
|
let entry = tree
|
||||||
|
.entry_for_path(path)
|
||||||
|
.unwrap_or_else(|| panic!("Missing entry for expected tracked path '{path}'"));
|
||||||
|
assert!(
|
||||||
|
!entry.is_ignored,
|
||||||
|
"expected path '{path}' to be tracked, but got entry: {entry:?}",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.set_global(SettingsStore::test(cx));
|
||||||
|
Project::init_settings(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -20,10 +20,6 @@ impl IgnoreStack {
|
||||||
Arc::new(Self::All)
|
Arc::new(Self::All)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_all(&self) -> bool {
|
|
||||||
matches!(self, IgnoreStack::All)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
pub fn append(self: Arc<Self>, abs_base_path: Arc<Path>, ignore: Arc<Gitignore>) -> Arc<Self> {
|
||||||
match self.as_ref() {
|
match self.as_ref() {
|
||||||
IgnoreStack::All => self,
|
IgnoreStack::All => self,
|
||||||
|
|
|
@ -5618,7 +5618,16 @@ impl Project {
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let background = cx.background_executor().clone();
|
let background = cx.background_executor().clone();
|
||||||
let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum();
|
let path_count: usize = snapshots
|
||||||
|
.iter()
|
||||||
|
.map(|s| {
|
||||||
|
if query.include_ignored() {
|
||||||
|
s.file_count()
|
||||||
|
} else {
|
||||||
|
s.visible_file_count()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.sum();
|
||||||
if path_count == 0 {
|
if path_count == 0 {
|
||||||
let (_, rx) = smol::channel::bounded(1024);
|
let (_, rx) = smol::channel::bounded(1024);
|
||||||
return rx;
|
return rx;
|
||||||
|
@ -5631,8 +5640,16 @@ impl Project {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(_, b)| {
|
.filter_map(|(_, b)| {
|
||||||
let buffer = b.upgrade()?;
|
let buffer = b.upgrade()?;
|
||||||
let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
|
let (is_ignored, snapshot) = buffer.update(cx, |buffer, cx| {
|
||||||
if let Some(path) = snapshot.file().map(|file| file.path()) {
|
let is_ignored = buffer
|
||||||
|
.project_path(cx)
|
||||||
|
.and_then(|path| self.entry_for_path(&path, cx))
|
||||||
|
.map_or(false, |entry| entry.is_ignored);
|
||||||
|
(is_ignored, buffer.snapshot())
|
||||||
|
});
|
||||||
|
if is_ignored && !query.include_ignored() {
|
||||||
|
return None;
|
||||||
|
} else if let Some(path) = snapshot.file().map(|file| file.path()) {
|
||||||
Some((path.clone(), (buffer, snapshot)))
|
Some((path.clone(), (buffer, snapshot)))
|
||||||
} else {
|
} else {
|
||||||
unnamed_files.push(buffer);
|
unnamed_files.push(buffer);
|
||||||
|
@ -5806,7 +5823,12 @@ impl Project {
|
||||||
let mut snapshot_start_ix = 0;
|
let mut snapshot_start_ix = 0;
|
||||||
let mut abs_path = PathBuf::new();
|
let mut abs_path = PathBuf::new();
|
||||||
for snapshot in snapshots {
|
for snapshot in snapshots {
|
||||||
let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count();
|
let snapshot_end_ix = snapshot_start_ix
|
||||||
|
+ if query.include_ignored() {
|
||||||
|
snapshot.file_count()
|
||||||
|
} else {
|
||||||
|
snapshot.visible_file_count()
|
||||||
|
};
|
||||||
if worker_end_ix <= snapshot_start_ix {
|
if worker_end_ix <= snapshot_start_ix {
|
||||||
break;
|
break;
|
||||||
} else if worker_start_ix > snapshot_end_ix {
|
} else if worker_start_ix > snapshot_end_ix {
|
||||||
|
@ -5819,7 +5841,7 @@ impl Project {
|
||||||
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix;
|
||||||
|
|
||||||
for entry in snapshot
|
for entry in snapshot
|
||||||
.files(false, start_in_snapshot)
|
.files(query.include_ignored(), start_in_snapshot)
|
||||||
.take(end_in_snapshot - start_in_snapshot)
|
.take(end_in_snapshot - start_in_snapshot)
|
||||||
{
|
{
|
||||||
if matching_paths_tx.is_closed() {
|
if matching_paths_tx.is_closed() {
|
||||||
|
|
|
@ -11,6 +11,8 @@ pub struct ProjectSettings {
|
||||||
pub lsp: HashMap<Arc<str>, LspSettings>,
|
pub lsp: HashMap<Arc<str>, LspSettings>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub git: GitSettings,
|
pub git: GitSettings,
|
||||||
|
#[serde(default)]
|
||||||
|
pub file_scan_exclusions: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
|
|
@ -3730,7 +3730,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(
|
search(
|
||||||
&project,
|
&project,
|
||||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx
|
cx
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -3755,7 +3755,7 @@ async fn test_search(cx: &mut gpui::TestAppContext) {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
search(
|
search(
|
||||||
&project,
|
&project,
|
||||||
SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
|
SearchQuery::text("TWO", false, true, false, Vec::new(), Vec::new()).unwrap(),
|
||||||
cx
|
cx
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
@ -3794,6 +3794,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
Vec::new()
|
Vec::new()
|
||||||
)
|
)
|
||||||
|
@ -3813,6 +3814,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.rs").unwrap()],
|
vec![PathMatcher::new("*.rs").unwrap()],
|
||||||
Vec::new()
|
Vec::new()
|
||||||
)
|
)
|
||||||
|
@ -3835,6 +3837,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap(),
|
PathMatcher::new("*.odd").unwrap(),
|
||||||
|
@ -3859,6 +3862,7 @@ async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.rs").unwrap(),
|
PathMatcher::new("*.rs").unwrap(),
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
|
@ -3906,6 +3910,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -3930,6 +3935,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![PathMatcher::new("*.rs").unwrap()],
|
vec![PathMatcher::new("*.rs").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -3952,6 +3958,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
|
@ -3976,6 +3983,7 @@ async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.rs").unwrap(),
|
PathMatcher::new("*.rs").unwrap(),
|
||||||
|
@ -4017,6 +4025,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
vec![PathMatcher::new("*.odd").unwrap()],
|
vec![PathMatcher::new("*.odd").unwrap()],
|
||||||
)
|
)
|
||||||
|
@ -4036,6 +4045,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![PathMatcher::new("*.ts").unwrap()],
|
vec![PathMatcher::new("*.ts").unwrap()],
|
||||||
vec![PathMatcher::new("*.ts").unwrap()],
|
vec![PathMatcher::new("*.ts").unwrap()],
|
||||||
).unwrap(),
|
).unwrap(),
|
||||||
|
@ -4054,6 +4064,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap()
|
PathMatcher::new("*.odd").unwrap()
|
||||||
|
@ -4079,6 +4090,7 @@ async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContex
|
||||||
search_query,
|
search_query,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
false,
|
||||||
vec![
|
vec![
|
||||||
PathMatcher::new("*.ts").unwrap(),
|
PathMatcher::new("*.ts").unwrap(),
|
||||||
PathMatcher::new("*.odd").unwrap()
|
PathMatcher::new("*.odd").unwrap()
|
||||||
|
|
|
@ -39,6 +39,7 @@ pub enum SearchQuery {
|
||||||
replacement: Option<String>,
|
replacement: Option<String>,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
inner: SearchInputs,
|
inner: SearchInputs,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -48,6 +49,7 @@ pub enum SearchQuery {
|
||||||
multiline: bool,
|
multiline: bool,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
inner: SearchInputs,
|
inner: SearchInputs,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -57,6 +59,7 @@ impl SearchQuery {
|
||||||
query: impl ToString,
|
query: impl ToString,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
files_to_include: Vec<PathMatcher>,
|
files_to_include: Vec<PathMatcher>,
|
||||||
files_to_exclude: Vec<PathMatcher>,
|
files_to_exclude: Vec<PathMatcher>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -74,6 +77,7 @@ impl SearchQuery {
|
||||||
replacement: None,
|
replacement: None,
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
|
include_ignored,
|
||||||
inner,
|
inner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -82,6 +86,7 @@ impl SearchQuery {
|
||||||
query: impl ToString,
|
query: impl ToString,
|
||||||
whole_word: bool,
|
whole_word: bool,
|
||||||
case_sensitive: bool,
|
case_sensitive: bool,
|
||||||
|
include_ignored: bool,
|
||||||
files_to_include: Vec<PathMatcher>,
|
files_to_include: Vec<PathMatcher>,
|
||||||
files_to_exclude: Vec<PathMatcher>,
|
files_to_exclude: Vec<PathMatcher>,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -111,6 +116,7 @@ impl SearchQuery {
|
||||||
multiline,
|
multiline,
|
||||||
whole_word,
|
whole_word,
|
||||||
case_sensitive,
|
case_sensitive,
|
||||||
|
include_ignored,
|
||||||
inner,
|
inner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -121,6 +127,7 @@ impl SearchQuery {
|
||||||
message.query,
|
message.query,
|
||||||
message.whole_word,
|
message.whole_word,
|
||||||
message.case_sensitive,
|
message.case_sensitive,
|
||||||
|
message.include_ignored,
|
||||||
deserialize_path_matches(&message.files_to_include)?,
|
deserialize_path_matches(&message.files_to_include)?,
|
||||||
deserialize_path_matches(&message.files_to_exclude)?,
|
deserialize_path_matches(&message.files_to_exclude)?,
|
||||||
)
|
)
|
||||||
|
@ -129,6 +136,7 @@ impl SearchQuery {
|
||||||
message.query,
|
message.query,
|
||||||
message.whole_word,
|
message.whole_word,
|
||||||
message.case_sensitive,
|
message.case_sensitive,
|
||||||
|
message.include_ignored,
|
||||||
deserialize_path_matches(&message.files_to_include)?,
|
deserialize_path_matches(&message.files_to_include)?,
|
||||||
deserialize_path_matches(&message.files_to_exclude)?,
|
deserialize_path_matches(&message.files_to_exclude)?,
|
||||||
)
|
)
|
||||||
|
@ -156,6 +164,7 @@ impl SearchQuery {
|
||||||
regex: self.is_regex(),
|
regex: self.is_regex(),
|
||||||
whole_word: self.whole_word(),
|
whole_word: self.whole_word(),
|
||||||
case_sensitive: self.case_sensitive(),
|
case_sensitive: self.case_sensitive(),
|
||||||
|
include_ignored: self.include_ignored(),
|
||||||
files_to_include: self
|
files_to_include: self
|
||||||
.files_to_include()
|
.files_to_include()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -336,6 +345,17 @@ impl SearchQuery {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn include_ignored(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Text {
|
||||||
|
include_ignored, ..
|
||||||
|
} => *include_ignored,
|
||||||
|
Self::Regex {
|
||||||
|
include_ignored, ..
|
||||||
|
} => *include_ignored,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_regex(&self) -> bool {
|
pub fn is_regex(&self) -> bool {
|
||||||
matches!(self, Self::Regex { .. })
|
matches!(self, Self::Regex { .. })
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
copy_recursive, ignore::IgnoreStack, DiagnosticSummary, ProjectEntryId, RemoveOptions,
|
copy_recursive, ignore::IgnoreStack, project_settings::ProjectSettings, DiagnosticSummary,
|
||||||
|
ProjectEntryId, RemoveOptions,
|
||||||
};
|
};
|
||||||
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
use ::ignore::gitignore::{Gitignore, GitignoreBuilder};
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
|
@ -25,6 +26,7 @@ use gpui::{
|
||||||
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
AppContext, AsyncAppContext, BackgroundExecutor, Context, EventEmitter, Model, ModelContext,
|
||||||
Task,
|
Task,
|
||||||
};
|
};
|
||||||
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
deserialize_fingerprint, deserialize_version, serialize_fingerprint, serialize_line_ending,
|
||||||
|
@ -39,6 +41,7 @@ use postage::{
|
||||||
prelude::{Sink as _, Stream as _},
|
prelude::{Sink as _, Stream as _},
|
||||||
watch,
|
watch,
|
||||||
};
|
};
|
||||||
|
use settings::{Settings, SettingsStore};
|
||||||
use smol::channel::{self, Sender};
|
use smol::channel::{self, Sender};
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
|
@ -58,7 +61,10 @@ use std::{
|
||||||
time::{Duration, SystemTime},
|
time::{Duration, SystemTime},
|
||||||
};
|
};
|
||||||
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap, TreeSet};
|
||||||
use util::{paths::HOME, ResultExt};
|
use util::{
|
||||||
|
paths::{PathMatcher, HOME},
|
||||||
|
ResultExt,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash, PartialOrd, Ord)]
|
||||||
pub struct WorktreeId(usize);
|
pub struct WorktreeId(usize);
|
||||||
|
@ -73,7 +79,7 @@ pub struct LocalWorktree {
|
||||||
scan_requests_tx: channel::Sender<ScanRequest>,
|
scan_requests_tx: channel::Sender<ScanRequest>,
|
||||||
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
path_prefixes_to_scan_tx: channel::Sender<Arc<Path>>,
|
||||||
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
is_scanning: (watch::Sender<bool>, watch::Receiver<bool>),
|
||||||
_background_scanner_task: Task<()>,
|
_background_scanner_tasks: Vec<Task<()>>,
|
||||||
share: Option<ShareState>,
|
share: Option<ShareState>,
|
||||||
diagnostics: HashMap<
|
diagnostics: HashMap<
|
||||||
Arc<Path>,
|
Arc<Path>,
|
||||||
|
@ -219,6 +225,7 @@ pub struct LocalSnapshot {
|
||||||
/// All of the git repositories in the worktree, indexed by the project entry
|
/// All of the git repositories in the worktree, indexed by the project entry
|
||||||
/// id of their parent directory.
|
/// id of their parent directory.
|
||||||
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
git_repositories: TreeMap<ProjectEntryId, LocalRepositoryEntry>,
|
||||||
|
file_scan_exclusions: Vec<PathMatcher>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct BackgroundScannerState {
|
struct BackgroundScannerState {
|
||||||
|
@ -302,17 +309,56 @@ impl Worktree {
|
||||||
.await
|
.await
|
||||||
.context("failed to stat worktree path")?;
|
.context("failed to stat worktree path")?;
|
||||||
|
|
||||||
|
let closure_fs = Arc::clone(&fs);
|
||||||
|
let closure_next_entry_id = Arc::clone(&next_entry_id);
|
||||||
|
let closure_abs_path = abs_path.to_path_buf();
|
||||||
cx.build_model(move |cx: &mut ModelContext<Worktree>| {
|
cx.build_model(move |cx: &mut ModelContext<Worktree>| {
|
||||||
|
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||||
|
if let Self::Local(this) = this {
|
||||||
|
let new_file_scan_exclusions =
|
||||||
|
file_scan_exclusions(ProjectSettings::get_global(cx));
|
||||||
|
if new_file_scan_exclusions != this.snapshot.file_scan_exclusions {
|
||||||
|
this.snapshot.file_scan_exclusions = new_file_scan_exclusions;
|
||||||
|
log::info!(
|
||||||
|
"Re-scanning directories, new scan exclude files: {:?}",
|
||||||
|
this.snapshot
|
||||||
|
.file_scan_exclusions
|
||||||
|
.iter()
|
||||||
|
.map(ToString::to_string)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||||
|
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) =
|
||||||
|
channel::unbounded();
|
||||||
|
this.scan_requests_tx = scan_requests_tx;
|
||||||
|
this.path_prefixes_to_scan_tx = path_prefixes_to_scan_tx;
|
||||||
|
this._background_scanner_tasks = start_background_scan_tasks(
|
||||||
|
&closure_abs_path,
|
||||||
|
this.snapshot(),
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
Arc::clone(&closure_next_entry_id),
|
||||||
|
Arc::clone(&closure_fs),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
this.is_scanning = watch::channel_with(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
let root_name = abs_path
|
let root_name = abs_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
.map_or(String::new(), |f| f.to_string_lossy().to_string());
|
||||||
|
|
||||||
let mut snapshot = LocalSnapshot {
|
let mut snapshot = LocalSnapshot {
|
||||||
|
file_scan_exclusions: file_scan_exclusions(ProjectSettings::get_global(cx)),
|
||||||
ignores_by_parent_abs_path: Default::default(),
|
ignores_by_parent_abs_path: Default::default(),
|
||||||
git_repositories: Default::default(),
|
git_repositories: Default::default(),
|
||||||
snapshot: Snapshot {
|
snapshot: Snapshot {
|
||||||
id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize),
|
id: WorktreeId::from_usize(cx.entity_id().as_u64() as usize),
|
||||||
abs_path: abs_path.clone(),
|
abs_path: abs_path.to_path_buf().into(),
|
||||||
root_name: root_name.clone(),
|
root_name: root_name.clone(),
|
||||||
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
root_char_bag: root_name.chars().map(|c| c.to_ascii_lowercase()).collect(),
|
||||||
entries_by_path: Default::default(),
|
entries_by_path: Default::default(),
|
||||||
|
@ -337,61 +383,22 @@ impl Worktree {
|
||||||
|
|
||||||
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
let (scan_requests_tx, scan_requests_rx) = channel::unbounded();
|
||||||
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
let (path_prefixes_to_scan_tx, path_prefixes_to_scan_rx) = channel::unbounded();
|
||||||
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
let task_snapshot = snapshot.clone();
|
||||||
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let this = this.as_local_mut().unwrap();
|
|
||||||
match state {
|
|
||||||
ScanState::Started => {
|
|
||||||
*this.is_scanning.0.borrow_mut() = true;
|
|
||||||
}
|
|
||||||
ScanState::Updated {
|
|
||||||
snapshot,
|
|
||||||
changes,
|
|
||||||
barrier,
|
|
||||||
scanning,
|
|
||||||
} => {
|
|
||||||
*this.is_scanning.0.borrow_mut() = scanning;
|
|
||||||
this.set_snapshot(snapshot, changes, cx);
|
|
||||||
drop(barrier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let background_scanner_task = cx.background_executor().spawn({
|
|
||||||
let fs = fs.clone();
|
|
||||||
let snapshot = snapshot.clone();
|
|
||||||
let background = cx.background_executor().clone();
|
|
||||||
async move {
|
|
||||||
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
|
||||||
BackgroundScanner::new(
|
|
||||||
snapshot,
|
|
||||||
next_entry_id,
|
|
||||||
fs,
|
|
||||||
scan_states_tx,
|
|
||||||
background,
|
|
||||||
scan_requests_rx,
|
|
||||||
path_prefixes_to_scan_rx,
|
|
||||||
)
|
|
||||||
.run(events)
|
|
||||||
.await;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
Worktree::Local(LocalWorktree {
|
Worktree::Local(LocalWorktree {
|
||||||
snapshot,
|
snapshot,
|
||||||
is_scanning: watch::channel_with(true),
|
is_scanning: watch::channel_with(true),
|
||||||
share: None,
|
share: None,
|
||||||
scan_requests_tx,
|
scan_requests_tx,
|
||||||
path_prefixes_to_scan_tx,
|
path_prefixes_to_scan_tx,
|
||||||
_background_scanner_task: background_scanner_task,
|
_background_scanner_tasks: start_background_scan_tasks(
|
||||||
|
&abs_path,
|
||||||
|
task_snapshot,
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
Arc::clone(&next_entry_id),
|
||||||
|
Arc::clone(&fs),
|
||||||
|
cx,
|
||||||
|
),
|
||||||
diagnostics: Default::default(),
|
diagnostics: Default::default(),
|
||||||
diagnostic_summaries: Default::default(),
|
diagnostic_summaries: Default::default(),
|
||||||
client,
|
client,
|
||||||
|
@ -584,6 +591,77 @@ impl Worktree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_background_scan_tasks(
|
||||||
|
abs_path: &Path,
|
||||||
|
snapshot: LocalSnapshot,
|
||||||
|
scan_requests_rx: channel::Receiver<ScanRequest>,
|
||||||
|
path_prefixes_to_scan_rx: channel::Receiver<Arc<Path>>,
|
||||||
|
next_entry_id: Arc<AtomicUsize>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: &mut ModelContext<'_, Worktree>,
|
||||||
|
) -> Vec<Task<()>> {
|
||||||
|
let (scan_states_tx, mut scan_states_rx) = mpsc::unbounded();
|
||||||
|
let background_scanner = cx.background_executor().spawn({
|
||||||
|
let abs_path = abs_path.to_path_buf();
|
||||||
|
let background = cx.background_executor().clone();
|
||||||
|
async move {
|
||||||
|
let events = fs.watch(&abs_path, Duration::from_millis(100)).await;
|
||||||
|
BackgroundScanner::new(
|
||||||
|
snapshot,
|
||||||
|
next_entry_id,
|
||||||
|
fs,
|
||||||
|
scan_states_tx,
|
||||||
|
background,
|
||||||
|
scan_requests_rx,
|
||||||
|
path_prefixes_to_scan_rx,
|
||||||
|
)
|
||||||
|
.run(events)
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let scan_state_updater = cx.spawn(|this, mut cx| async move {
|
||||||
|
while let Some((state, this)) = scan_states_rx.next().await.zip(this.upgrade()) {
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
let this = this.as_local_mut().unwrap();
|
||||||
|
match state {
|
||||||
|
ScanState::Started => {
|
||||||
|
*this.is_scanning.0.borrow_mut() = true;
|
||||||
|
}
|
||||||
|
ScanState::Updated {
|
||||||
|
snapshot,
|
||||||
|
changes,
|
||||||
|
barrier,
|
||||||
|
scanning,
|
||||||
|
} => {
|
||||||
|
*this.is_scanning.0.borrow_mut() = scanning;
|
||||||
|
this.set_snapshot(snapshot, changes, cx);
|
||||||
|
drop(barrier);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
vec![background_scanner, scan_state_updater]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn file_scan_exclusions(project_settings: &ProjectSettings) -> Vec<PathMatcher> {
|
||||||
|
project_settings.file_scan_exclusions.as_deref().unwrap_or(&[]).iter()
|
||||||
|
.sorted()
|
||||||
|
.filter_map(|pattern| {
|
||||||
|
PathMatcher::new(pattern)
|
||||||
|
.map(Some)
|
||||||
|
.unwrap_or_else(|e| {
|
||||||
|
log::error!(
|
||||||
|
"Skipping pattern {pattern} in `file_scan_exclusions` project settings due to parsing error: {e:#}"
|
||||||
|
);
|
||||||
|
None
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl LocalWorktree {
|
impl LocalWorktree {
|
||||||
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
pub fn contains_abs_path(&self, path: &Path) -> bool {
|
||||||
path.starts_with(&self.abs_path)
|
path.starts_with(&self.abs_path)
|
||||||
|
@ -1482,7 +1560,7 @@ impl Snapshot {
|
||||||
self.entries_by_id.get(&entry_id, &()).is_some()
|
self.entries_by_id.get(&entry_id, &()).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
fn insert_entry(&mut self, entry: proto::Entry) -> Result<Entry> {
|
||||||
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
let entry = Entry::try_from((&self.root_char_bag, entry))?;
|
||||||
let old_entry = self.entries_by_id.insert_or_replace(
|
let old_entry = self.entries_by_id.insert_or_replace(
|
||||||
PathEntry {
|
PathEntry {
|
||||||
|
@ -2143,6 +2221,12 @@ impl LocalSnapshot {
|
||||||
paths.sort_by(|a, b| a.0.cmp(b.0));
|
paths.sort_by(|a, b| a.0.cmp(b.0));
|
||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_abs_path_excluded(&self, abs_path: &Path) -> bool {
|
||||||
|
self.file_scan_exclusions
|
||||||
|
.iter()
|
||||||
|
.any(|exclude_matcher| exclude_matcher.is_match(abs_path))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BackgroundScannerState {
|
impl BackgroundScannerState {
|
||||||
|
@ -2165,7 +2249,7 @@ impl BackgroundScannerState {
|
||||||
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true);
|
||||||
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path);
|
||||||
let mut containing_repository = None;
|
let mut containing_repository = None;
|
||||||
if !ignore_stack.is_all() {
|
if !ignore_stack.is_abs_path_ignored(&abs_path, true) {
|
||||||
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) {
|
||||||
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) {
|
||||||
containing_repository = Some((
|
containing_repository = Some((
|
||||||
|
@ -2376,18 +2460,30 @@ impl BackgroundScannerState {
|
||||||
|
|
||||||
// Remove any git repositories whose .git entry no longer exists.
|
// Remove any git repositories whose .git entry no longer exists.
|
||||||
let snapshot = &mut self.snapshot;
|
let snapshot = &mut self.snapshot;
|
||||||
let mut repositories = mem::take(&mut snapshot.git_repositories);
|
let mut ids_to_preserve = HashSet::default();
|
||||||
let mut repository_entries = mem::take(&mut snapshot.repository_entries);
|
for (&work_directory_id, entry) in snapshot.git_repositories.iter() {
|
||||||
repositories.retain(|work_directory_id, _| {
|
let exists_in_snapshot = snapshot
|
||||||
snapshot
|
.entry_for_id(work_directory_id)
|
||||||
.entry_for_id(*work_directory_id)
|
|
||||||
.map_or(false, |entry| {
|
.map_or(false, |entry| {
|
||||||
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
snapshot.entry_for_path(entry.path.join(*DOT_GIT)).is_some()
|
||||||
})
|
|
||||||
});
|
});
|
||||||
repository_entries.retain(|_, entry| repositories.get(&entry.work_directory.0).is_some());
|
if exists_in_snapshot {
|
||||||
snapshot.git_repositories = repositories;
|
ids_to_preserve.insert(work_directory_id);
|
||||||
snapshot.repository_entries = repository_entries;
|
} else {
|
||||||
|
let git_dir_abs_path = snapshot.abs_path().join(&entry.git_dir_path);
|
||||||
|
if snapshot.is_abs_path_excluded(&git_dir_abs_path)
|
||||||
|
&& !matches!(smol::block_on(fs.metadata(&git_dir_abs_path)), Ok(None))
|
||||||
|
{
|
||||||
|
ids_to_preserve.insert(work_directory_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshot
|
||||||
|
.git_repositories
|
||||||
|
.retain(|work_directory_id, _| ids_to_preserve.contains(work_directory_id));
|
||||||
|
snapshot
|
||||||
|
.repository_entries
|
||||||
|
.retain(|_, entry| ids_to_preserve.contains(&entry.work_directory.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_git_repository(
|
fn build_git_repository(
|
||||||
|
@ -3085,7 +3181,7 @@ impl BackgroundScanner {
|
||||||
let ignore_stack = state
|
let ignore_stack = state
|
||||||
.snapshot
|
.snapshot
|
||||||
.ignore_stack_for_abs_path(&root_abs_path, true);
|
.ignore_stack_for_abs_path(&root_abs_path, true);
|
||||||
if ignore_stack.is_all() {
|
if ignore_stack.is_abs_path_ignored(&root_abs_path, true) {
|
||||||
root_entry.is_ignored = true;
|
root_entry.is_ignored = true;
|
||||||
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
state.insert_entry(root_entry.clone(), self.fs.as_ref());
|
||||||
}
|
}
|
||||||
|
@ -3222,6 +3318,7 @@ impl BackgroundScanner {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if !is_git_related(&abs_path) {
|
||||||
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
let parent_dir_is_loaded = relative_path.parent().map_or(true, |parent| {
|
||||||
snapshot
|
snapshot
|
||||||
.entry_for_path(parent)
|
.entry_for_path(parent)
|
||||||
|
@ -3231,6 +3328,13 @@ impl BackgroundScanner {
|
||||||
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
log::debug!("ignoring event {relative_path:?} within unloaded directory");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if snapshot.is_abs_path_excluded(abs_path) {
|
||||||
|
log::debug!(
|
||||||
|
"ignoring FS event for path {relative_path:?} within excluded directory"
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
relative_paths.push(relative_path);
|
relative_paths.push(relative_path);
|
||||||
true
|
true
|
||||||
|
@ -3392,18 +3496,26 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
async fn scan_dir(&self, job: &ScanJob) -> Result<()> {
|
||||||
log::debug!("scan directory {:?}", job.path);
|
let root_abs_path;
|
||||||
|
let mut ignore_stack;
|
||||||
let mut ignore_stack = job.ignore_stack.clone();
|
let mut new_ignore;
|
||||||
let mut new_ignore = None;
|
let root_char_bag;
|
||||||
let (root_abs_path, root_char_bag, next_entry_id) = {
|
let next_entry_id;
|
||||||
let snapshot = &self.state.lock().snapshot;
|
{
|
||||||
(
|
let state = self.state.lock();
|
||||||
snapshot.abs_path().clone(),
|
let snapshot = &state.snapshot;
|
||||||
snapshot.root_char_bag,
|
root_abs_path = snapshot.abs_path().clone();
|
||||||
self.next_entry_id.clone(),
|
if snapshot.is_abs_path_excluded(&job.abs_path) {
|
||||||
)
|
log::error!("skipping excluded directory {:?}", job.path);
|
||||||
};
|
return Ok(());
|
||||||
|
}
|
||||||
|
log::debug!("scanning directory {:?}", job.path);
|
||||||
|
ignore_stack = job.ignore_stack.clone();
|
||||||
|
new_ignore = None;
|
||||||
|
root_char_bag = snapshot.root_char_bag;
|
||||||
|
next_entry_id = self.next_entry_id.clone();
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
|
||||||
let mut dotgit_path = None;
|
let mut dotgit_path = None;
|
||||||
let mut root_canonical_path = None;
|
let mut root_canonical_path = None;
|
||||||
|
@ -3418,18 +3530,8 @@ impl BackgroundScanner {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let child_name = child_abs_path.file_name().unwrap();
|
let child_name = child_abs_path.file_name().unwrap();
|
||||||
let child_path: Arc<Path> = job.path.join(child_name).into();
|
let child_path: Arc<Path> = job.path.join(child_name).into();
|
||||||
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
|
||||||
Ok(Some(metadata)) => metadata,
|
|
||||||
Ok(None) => continue,
|
|
||||||
Err(err) => {
|
|
||||||
log::error!("error processing {:?}: {:?}", child_abs_path, err);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
// If we find a .gitignore, add it to the stack of ignores used to determine which paths are ignored
|
||||||
if child_name == *GITIGNORE {
|
if child_name == *GITIGNORE {
|
||||||
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
match build_gitignore(&child_abs_path, self.fs.as_ref()).await {
|
||||||
|
@ -3473,6 +3575,26 @@ impl BackgroundScanner {
|
||||||
dotgit_path = Some(child_path.clone());
|
dotgit_path = Some(child_path.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
let mut state = self.state.lock();
|
||||||
|
if state.snapshot.is_abs_path_excluded(&child_abs_path) {
|
||||||
|
let relative_path = job.path.join(child_name);
|
||||||
|
log::debug!("skipping excluded child entry {relative_path:?}");
|
||||||
|
state.remove_path(&relative_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
drop(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
let child_metadata = match self.fs.metadata(&child_abs_path).await {
|
||||||
|
Ok(Some(metadata)) => metadata,
|
||||||
|
Ok(None) => continue,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("error processing {child_abs_path:?}: {err:?}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let mut child_entry = Entry::new(
|
let mut child_entry = Entry::new(
|
||||||
child_path.clone(),
|
child_path.clone(),
|
||||||
&child_metadata,
|
&child_metadata,
|
||||||
|
@ -3653,14 +3775,12 @@ impl BackgroundScanner {
|
||||||
self.next_entry_id.as_ref(),
|
self.next_entry_id.as_ref(),
|
||||||
state.snapshot.root_char_bag,
|
state.snapshot.root_char_bag,
|
||||||
);
|
);
|
||||||
fs_entry.is_ignored = ignore_stack.is_all();
|
let is_dir = fs_entry.is_dir();
|
||||||
|
fs_entry.is_ignored = ignore_stack.is_abs_path_ignored(&abs_path, is_dir);
|
||||||
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
fs_entry.is_external = !canonical_path.starts_with(&root_canonical_path);
|
||||||
|
|
||||||
if !fs_entry.is_ignored {
|
if !is_dir && !fs_entry.is_ignored {
|
||||||
if !fs_entry.is_dir() {
|
if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) {
|
||||||
if let Some((work_dir, repo)) =
|
|
||||||
state.snapshot.local_repo_for_path(&path)
|
|
||||||
{
|
|
||||||
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
if let Ok(repo_path) = path.strip_prefix(work_dir.0) {
|
||||||
let repo_path = RepoPath(repo_path.into());
|
let repo_path = RepoPath(repo_path.into());
|
||||||
let repo = repo.repo_ptr.lock();
|
let repo = repo.repo_ptr.lock();
|
||||||
|
@ -3668,7 +3788,6 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
|
if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) {
|
||||||
if state.should_scan_directory(&fs_entry) {
|
if state.should_scan_directory(&fs_entry) {
|
||||||
|
@ -3824,8 +3943,7 @@ impl BackgroundScanner {
|
||||||
ignore_stack.clone()
|
ignore_stack.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scan any directories that were previously ignored and weren't
|
// Scan any directories that were previously ignored and weren't previously scanned.
|
||||||
// previously scanned.
|
|
||||||
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() {
|
||||||
let state = self.state.lock();
|
let state = self.state.lock();
|
||||||
if state.should_scan_directory(&entry) {
|
if state.should_scan_directory(&entry) {
|
||||||
|
@ -4001,6 +4119,12 @@ impl BackgroundScanner {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn is_git_related(abs_path: &Path) -> bool {
|
||||||
|
abs_path
|
||||||
|
.components()
|
||||||
|
.any(|c| c.as_os_str() == *DOT_GIT || c.as_os_str() == *GITIGNORE)
|
||||||
|
}
|
||||||
|
|
||||||
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
fn char_bag_for_path(root_char_bag: CharBag, path: &Path) -> CharBag {
|
||||||
let mut result = root_char_bag;
|
let mut result = root_char_bag;
|
||||||
result.extend(
|
result.extend(
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1732,7 +1732,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
|
use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use project::FakeFs;
|
use project::{project_settings::ProjectSettings, FakeFs};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -1832,6 +1832,123 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
".dockerignore": "",
|
||||||
|
".git": {
|
||||||
|
"HEAD": "",
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"0": { "q": "", "r": "", "s": "" },
|
||||||
|
"1": { "t": "", "u": "" },
|
||||||
|
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"3": { "Q": "" },
|
||||||
|
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"5": {},
|
||||||
|
"6": { "V": "", "W": "" },
|
||||||
|
"7": { "X": "" },
|
||||||
|
"8": { "Y": {}, "Z": "" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root2",
|
||||||
|
json!({
|
||||||
|
"d": {
|
||||||
|
"4": ""
|
||||||
|
},
|
||||||
|
"e": {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||||
|
let workspace = cx
|
||||||
|
.add_window(|cx| Workspace::test_new(project.clone(), cx))
|
||||||
|
.root(cx);
|
||||||
|
let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx));
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root1/b", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b <== selected",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root2/d", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" v d <== selected",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root2/e", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" v d",
|
||||||
|
" v e <== selected",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2929,6 +3046,12 @@ mod tests {
|
||||||
workspace::init_settings(cx);
|
workspace::init_settings(cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
|
||||||
|
cx.update_global::<SettingsStore, _, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
pub mod file_associations;
|
pub mod file_associations;
|
||||||
mod project_panel_settings;
|
mod project_panel_settings;
|
||||||
use settings::Settings;
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
||||||
use db::kvp::KEY_VALUE_STORE;
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor};
|
||||||
|
@ -34,7 +34,7 @@ use ui::{h_stack, v_stack, IconElement, Label};
|
||||||
use unicase::UniCase;
|
use unicase::UniCase;
|
||||||
use util::{maybe, ResultExt, TryFutureExt};
|
use util::{maybe, ResultExt, TryFutureExt};
|
||||||
use workspace::{
|
use workspace::{
|
||||||
dock::{DockPosition, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
Workspace,
|
Workspace,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -148,7 +148,6 @@ pub enum Event {
|
||||||
SplitEntry {
|
SplitEntry {
|
||||||
entry_id: ProjectEntryId,
|
entry_id: ProjectEntryId,
|
||||||
},
|
},
|
||||||
DockPositionChanged,
|
|
||||||
Focus,
|
Focus,
|
||||||
NewSearchInDirectory {
|
NewSearchInDirectory {
|
||||||
dir_entry: Entry,
|
dir_entry: Entry,
|
||||||
|
@ -245,16 +244,17 @@ impl ProjectPanel {
|
||||||
this.update_visible_entries(None, cx);
|
this.update_visible_entries(None, cx);
|
||||||
|
|
||||||
// Update the dock position when the setting changes.
|
// Update the dock position when the setting changes.
|
||||||
// todo!()
|
let mut old_dock_position = this.position(cx);
|
||||||
// let mut old_dock_position = this.position(cx);
|
ProjectPanelSettings::register(cx);
|
||||||
// cx.observe_global::<SettingsStore, _>(move |this, cx| {
|
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||||
// let new_dock_position = this.position(cx);
|
dbg!("OLA!");
|
||||||
// if new_dock_position != old_dock_position {
|
let new_dock_position = this.position(cx);
|
||||||
// old_dock_position = new_dock_position;
|
if new_dock_position != old_dock_position {
|
||||||
// cx.emit(Event::DockPositionChanged);
|
old_dock_position = new_dock_position;
|
||||||
// }
|
cx.emit(PanelEvent::ChangePosition);
|
||||||
// })
|
}
|
||||||
// .detach();
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
this
|
this
|
||||||
});
|
});
|
||||||
|
@ -1486,7 +1486,7 @@ impl EventEmitter<Event> for ProjectPanel {}
|
||||||
|
|
||||||
impl EventEmitter<PanelEvent> for ProjectPanel {}
|
impl EventEmitter<PanelEvent> for ProjectPanel {}
|
||||||
|
|
||||||
impl workspace::dock::Panel for ProjectPanel {
|
impl Panel for ProjectPanel {
|
||||||
fn position(&self, cx: &WindowContext) -> DockPosition {
|
fn position(&self, cx: &WindowContext) -> DockPosition {
|
||||||
match ProjectPanelSettings::get_global(cx).dock {
|
match ProjectPanelSettings::get_global(cx).dock {
|
||||||
ProjectPanelDockPosition::Left => DockPosition::Left,
|
ProjectPanelDockPosition::Left => DockPosition::Left,
|
||||||
|
@ -1572,7 +1572,7 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
|
use gpui::{TestAppContext, View, VisualTestContext, WindowHandle};
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use project::FakeFs;
|
use project::{project_settings::ProjectSettings, FakeFs};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::SettingsStore;
|
use settings::SettingsStore;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -1673,6 +1673,124 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions =
|
||||||
|
Some(vec!["**/.git".to_string(), "**/4/**".to_string()]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let fs = FakeFs::new(cx.background_executor.clone());
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root1",
|
||||||
|
json!({
|
||||||
|
".dockerignore": "",
|
||||||
|
".git": {
|
||||||
|
"HEAD": "",
|
||||||
|
},
|
||||||
|
"a": {
|
||||||
|
"0": { "q": "", "r": "", "s": "" },
|
||||||
|
"1": { "t": "", "u": "" },
|
||||||
|
"2": { "v": "", "w": "", "x": "", "y": "" },
|
||||||
|
},
|
||||||
|
"b": {
|
||||||
|
"3": { "Q": "" },
|
||||||
|
"4": { "R": "", "S": "", "T": "", "U": "" },
|
||||||
|
},
|
||||||
|
"C": {
|
||||||
|
"5": {},
|
||||||
|
"6": { "V": "", "W": "" },
|
||||||
|
"7": { "X": "" },
|
||||||
|
"8": { "Y": {}, "Z": "" }
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
fs.insert_tree(
|
||||||
|
"/root2",
|
||||||
|
json!({
|
||||||
|
"d": {
|
||||||
|
"4": ""
|
||||||
|
},
|
||||||
|
"e": {}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
|
||||||
|
let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
|
||||||
|
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||||
|
let panel = workspace
|
||||||
|
.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" > b",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root1/b", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b <== selected",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" > d",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root2/d", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" v d <== selected",
|
||||||
|
" > e",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
toggle_expand_dir(&panel, "root2/e", cx);
|
||||||
|
assert_eq!(
|
||||||
|
visible_entries_as_strings(&panel, 0..50, cx),
|
||||||
|
&[
|
||||||
|
"v root1",
|
||||||
|
" > a",
|
||||||
|
" v b",
|
||||||
|
" > 3",
|
||||||
|
" > C",
|
||||||
|
" .dockerignore",
|
||||||
|
"v root2",
|
||||||
|
" v d",
|
||||||
|
" v e <== selected",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test(iterations = 30)]
|
#[gpui::test(iterations = 30)]
|
||||||
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
async fn test_editing_files(cx: &mut gpui::TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
@ -2793,6 +2911,12 @@ mod tests {
|
||||||
workspace::init_settings(cx);
|
workspace::init_settings(cx);
|
||||||
client::init_settings(cx);
|
client::init_settings(cx);
|
||||||
Project::init_settings(cx);
|
Project::init_settings(cx);
|
||||||
|
|
||||||
|
cx.update_global::<SettingsStore, _>(|store, cx| {
|
||||||
|
store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
|
||||||
|
project_settings.file_scan_exclusions = Some(Vec::new());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -884,6 +884,7 @@ message SearchProject {
|
||||||
bool case_sensitive = 5;
|
bool case_sensitive = 5;
|
||||||
string files_to_include = 6;
|
string files_to_include = 6;
|
||||||
string files_to_exclude = 7;
|
string files_to_exclude = 7;
|
||||||
|
bool include_ignored = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchProjectResponse {
|
message SearchProjectResponse {
|
||||||
|
|
|
@ -884,6 +884,7 @@ message SearchProject {
|
||||||
bool case_sensitive = 5;
|
bool case_sensitive = 5;
|
||||||
string files_to_include = 6;
|
string files_to_include = 6;
|
||||||
string files_to_exclude = 7;
|
string files_to_exclude = 7;
|
||||||
|
bool include_ignored = 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SearchProjectResponse {
|
message SearchProjectResponse {
|
||||||
|
|
|
@ -805,6 +805,7 @@ impl BufferSearchBar {
|
||||||
query,
|
query,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
) {
|
) {
|
||||||
|
@ -820,6 +821,7 @@ impl BufferSearchBar {
|
||||||
query,
|
query,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
false,
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
Vec::new(),
|
Vec::new(),
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -4,7 +4,7 @@ use crate::{
|
||||||
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
|
search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button},
|
||||||
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
|
ActivateRegexMode, ActivateSemanticMode, ActivateTextMode, CycleMode, NextHistoryQuery,
|
||||||
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
|
PreviousHistoryQuery, ReplaceAll, ReplaceNext, SearchOptions, SelectNextMatch, SelectPrevMatch,
|
||||||
ToggleCaseSensitive, ToggleReplace, ToggleWholeWord,
|
ToggleCaseSensitive, ToggleIncludeIgnored, ToggleReplace, ToggleWholeWord,
|
||||||
};
|
};
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
|
@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.capture_action(ProjectSearchView::replace_next);
|
cx.capture_action(ProjectSearchView::replace_next);
|
||||||
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
add_toggle_option_action::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, cx);
|
||||||
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
add_toggle_option_action::<ToggleWholeWord>(SearchOptions::WHOLE_WORD, cx);
|
||||||
|
add_toggle_option_action::<ToggleIncludeIgnored>(SearchOptions::INCLUDE_IGNORED, cx);
|
||||||
add_toggle_filters_action::<ToggleFilters>(cx);
|
add_toggle_filters_action::<ToggleFilters>(cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1192,6 +1193,7 @@ impl ProjectSearchView {
|
||||||
text,
|
text,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||||
included_files,
|
included_files,
|
||||||
excluded_files,
|
excluded_files,
|
||||||
) {
|
) {
|
||||||
|
@ -1210,6 +1212,7 @@ impl ProjectSearchView {
|
||||||
text,
|
text,
|
||||||
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
self.search_options.contains(SearchOptions::WHOLE_WORD),
|
||||||
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
self.search_options.contains(SearchOptions::CASE_SENSITIVE),
|
||||||
|
self.search_options.contains(SearchOptions::INCLUDE_IGNORED),
|
||||||
included_files,
|
included_files,
|
||||||
excluded_files,
|
excluded_files,
|
||||||
) {
|
) {
|
||||||
|
@ -1764,6 +1767,17 @@ impl View for ProjectSearchBar {
|
||||||
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let mut include_ignored = is_semantic_disabled.then(|| {
|
||||||
|
render_option_button_icon(
|
||||||
|
// TODO proper icon
|
||||||
|
"icons/case_insensitive.svg",
|
||||||
|
SearchOptions::INCLUDE_IGNORED,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
// TODO not implemented yet
|
||||||
|
let _ = include_ignored.take();
|
||||||
|
|
||||||
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
let search_button_for_mode = |mode, side, cx: &mut ViewContext<ProjectSearchBar>| {
|
||||||
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
let is_active = if let Some(search) = self.active_project_search.as_ref() {
|
||||||
let search = search.read(cx);
|
let search = search.read(cx);
|
||||||
|
@ -1877,9 +1891,17 @@ impl View for ProjectSearchBar {
|
||||||
.flex(1., false),
|
.flex(1., false),
|
||||||
)
|
)
|
||||||
.with_children(search.filters_enabled.then(|| {
|
.with_children(search.filters_enabled.then(|| {
|
||||||
|
Flex::row()
|
||||||
|
.with_child(
|
||||||
Flex::row()
|
Flex::row()
|
||||||
.with_child(
|
.with_child(
|
||||||
ChildView::new(&search.included_files_editor, cx)
|
ChildView::new(&search.included_files_editor, cx)
|
||||||
|
.contained()
|
||||||
|
.constrained()
|
||||||
|
.with_height(theme.search.search_bar_row_height)
|
||||||
|
.flex(1., true),
|
||||||
|
)
|
||||||
|
.with_children(include_ignored)
|
||||||
.contained()
|
.contained()
|
||||||
.with_style(include_container_style)
|
.with_style(include_container_style)
|
||||||
.constrained()
|
.constrained()
|
||||||
|
|
|
@ -29,6 +29,7 @@ actions!(
|
||||||
CycleMode,
|
CycleMode,
|
||||||
ToggleWholeWord,
|
ToggleWholeWord,
|
||||||
ToggleCaseSensitive,
|
ToggleCaseSensitive,
|
||||||
|
ToggleIncludeIgnored,
|
||||||
ToggleReplace,
|
ToggleReplace,
|
||||||
SelectNextMatch,
|
SelectNextMatch,
|
||||||
SelectPrevMatch,
|
SelectPrevMatch,
|
||||||
|
@ -49,31 +50,35 @@ bitflags! {
|
||||||
const NONE = 0b000;
|
const NONE = 0b000;
|
||||||
const WHOLE_WORD = 0b001;
|
const WHOLE_WORD = 0b001;
|
||||||
const CASE_SENSITIVE = 0b010;
|
const CASE_SENSITIVE = 0b010;
|
||||||
|
const INCLUDE_IGNORED = 0b100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SearchOptions {
|
impl SearchOptions {
|
||||||
pub fn label(&self) -> &'static str {
|
pub fn label(&self) -> &'static str {
|
||||||
match *self {
|
match *self {
|
||||||
SearchOptions::WHOLE_WORD => "Match Whole Word",
|
Self::WHOLE_WORD => "Match Whole Word",
|
||||||
SearchOptions::CASE_SENSITIVE => "Match Case",
|
Self::CASE_SENSITIVE => "Match Case",
|
||||||
_ => panic!("{:?} is not a named SearchOption", self),
|
Self::INCLUDE_IGNORED => "Include Ignored",
|
||||||
|
_ => panic!("{self:?} is not a named SearchOption"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn icon(&self) -> &'static str {
|
pub fn icon(&self) -> &'static str {
|
||||||
match *self {
|
match *self {
|
||||||
SearchOptions::WHOLE_WORD => "icons/word_search.svg",
|
Self::WHOLE_WORD => "icons/word_search.svg",
|
||||||
SearchOptions::CASE_SENSITIVE => "icons/case_insensitive.svg",
|
Self::CASE_SENSITIVE => "icons/case_insensitive.svg",
|
||||||
_ => panic!("{:?} is not a named SearchOption", self),
|
Self::INCLUDE_IGNORED => "icons/case_insensitive.svg",
|
||||||
|
_ => panic!("{self:?} is not a named SearchOption"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_toggle_action(&self) -> Box<dyn Action> {
|
pub fn to_toggle_action(&self) -> Box<dyn Action> {
|
||||||
match *self {
|
match *self {
|
||||||
SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord),
|
Self::WHOLE_WORD => Box::new(ToggleWholeWord),
|
||||||
SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
Self::CASE_SENSITIVE => Box::new(ToggleCaseSensitive),
|
||||||
_ => panic!("{:?} is not a named SearchOption", self),
|
Self::INCLUDE_IGNORED => Box::new(ToggleIncludeIgnored),
|
||||||
|
_ => panic!("{self:?} is not a named SearchOption"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,6 +90,7 @@ impl SearchOptions {
|
||||||
let mut options = SearchOptions::NONE;
|
let mut options = SearchOptions::NONE;
|
||||||
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
options.set(SearchOptions::WHOLE_WORD, query.whole_word());
|
||||||
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive());
|
||||||
|
options.set(SearchOptions::INCLUDE_IGNORED, query.include_ignored());
|
||||||
options
|
options
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -77,6 +77,7 @@ pub fn handle_settings_file_changes(
|
||||||
});
|
});
|
||||||
cx.spawn(move |mut cx| async move {
|
cx.spawn(move |mut cx| async move {
|
||||||
while let Some(user_settings_content) = user_settings_file_rx.next().await {
|
while let Some(user_settings_content) = user_settings_file_rx.next().await {
|
||||||
|
eprintln!("settings file changed");
|
||||||
let result = cx.update_global(|store: &mut SettingsStore, cx| {
|
let result = cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
store
|
store
|
||||||
.set_user_settings(&user_settings_content, cx)
|
.set_user_settings(&user_settings_content, cx)
|
||||||
|
|
|
@ -9,11 +9,10 @@ pub mod terminal_panel;
|
||||||
// use crate::terminal_element::TerminalElement;
|
// use crate::terminal_element::TerminalElement;
|
||||||
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
use editor::{scroll::autoscroll::Autoscroll, Editor};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
actions, div, img, red, Action, AnyElement, AppContext, Component, DispatchPhase, Div,
|
actions, div, Action, AnyElement, AppContext, Component, DispatchPhase, Div, EventEmitter,
|
||||||
EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView,
|
FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InputHandler,
|
||||||
InputHandler, InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton,
|
InteractiveComponent, KeyDownEvent, Keystroke, Model, MouseButton, ParentComponent, Pixels,
|
||||||
ParentComponent, Pixels, Render, SharedString, Styled, Task, View, ViewContext, VisualContext,
|
Render, SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView,
|
||||||
WeakView,
|
|
||||||
};
|
};
|
||||||
use language::Bias;
|
use language::Bias;
|
||||||
use persistence::TERMINAL_DB;
|
use persistence::TERMINAL_DB;
|
||||||
|
@ -32,7 +31,7 @@ use workspace::{
|
||||||
notifications::NotifyResultExt,
|
notifications::NotifyResultExt,
|
||||||
register_deserializable_item,
|
register_deserializable_item,
|
||||||
searchable::{SearchEvent, SearchOptions, SearchableItem},
|
searchable::{SearchEvent, SearchOptions, SearchableItem},
|
||||||
ui::{ContextMenu, Label},
|
ui::{ContextMenu, Icon, IconElement, Label, ListEntry},
|
||||||
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -85,7 +84,7 @@ pub struct TerminalView {
|
||||||
has_new_content: bool,
|
has_new_content: bool,
|
||||||
//Currently using iTerm bell, show bell emoji in tab until input is received
|
//Currently using iTerm bell, show bell emoji in tab until input is received
|
||||||
has_bell: bool,
|
has_bell: bool,
|
||||||
context_menu: Option<View<ContextMenu>>,
|
context_menu: Option<View<ContextMenu<Self>>>,
|
||||||
blink_state: bool,
|
blink_state: bool,
|
||||||
blinking_on: bool,
|
blinking_on: bool,
|
||||||
blinking_paused: bool,
|
blinking_paused: bool,
|
||||||
|
@ -300,11 +299,10 @@ impl TerminalView {
|
||||||
position: gpui::Point<Pixels>,
|
position: gpui::Point<Pixels>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) {
|
) {
|
||||||
self.context_menu = Some(cx.build_view(|cx| {
|
self.context_menu = Some(ContextMenu::build(cx, |menu, _| {
|
||||||
ContextMenu::new(cx)
|
menu.action(ListEntry::new(Label::new("Clear")), Box::new(Clear))
|
||||||
.entry(Label::new("Clear"), Box::new(Clear))
|
.action(
|
||||||
.entry(
|
ListEntry::new(Label::new("Close")),
|
||||||
Label::new("Close"),
|
|
||||||
Box::new(CloseActiveItem { save_intent: None }),
|
Box::new(CloseActiveItem { save_intent: None }),
|
||||||
)
|
)
|
||||||
}));
|
}));
|
||||||
|
@ -756,7 +754,7 @@ impl Item for TerminalView {
|
||||||
let title = self.terminal().read(cx).title();
|
let title = self.terminal().read(cx).title();
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.child(img().uri("icons/terminal.svg").bg(red()))
|
.child(IconElement::new(Icon::Terminal))
|
||||||
.child(title)
|
.child(title)
|
||||||
.render()
|
.render()
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,57 +4,91 @@ use std::rc::Rc;
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
|
use crate::{v_stack, Label, List, ListEntry, ListItem, ListSeparator, ListSubHeader};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
overlay, px, Action, AnchorCorner, AnyElement, Bounds, Dismiss, DispatchPhase, Div,
|
overlay, px, Action, AnchorCorner, AnyElement, AppContext, Bounds, DispatchPhase, Div,
|
||||||
FocusHandle, LayoutId, ManagedView, MouseButton, MouseDownEvent, Pixels, Point, Render, View,
|
EventEmitter, FocusHandle, FocusableView, LayoutId, ManagedView, Manager, MouseButton,
|
||||||
|
MouseDownEvent, Pixels, Point, Render, View, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct ContextMenu {
|
pub enum ContextMenuItem<V> {
|
||||||
items: Vec<ListItem>,
|
Separator(ListSeparator),
|
||||||
focus_handle: FocusHandle,
|
Header(ListSubHeader),
|
||||||
|
Entry(
|
||||||
|
ListEntry<ContextMenu<V>>,
|
||||||
|
Rc<dyn Fn(&mut V, &mut ViewContext<V>)>,
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ManagedView for ContextMenu {
|
pub struct ContextMenu<V> {
|
||||||
fn focus_handle(&self, cx: &gpui::AppContext) -> FocusHandle {
|
items: Vec<ContextMenuItem<V>>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
handle: WeakView<V>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<V: Render> FocusableView for ContextMenu<V> {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
|
||||||
self.focus_handle.clone()
|
self.focus_handle.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextMenu {
|
impl<V: Render> EventEmitter<Manager> for ContextMenu<V> {}
|
||||||
pub fn new(cx: &mut WindowContext) -> Self {
|
|
||||||
|
impl<V: Render> ContextMenu<V> {
|
||||||
|
pub fn build(
|
||||||
|
cx: &mut ViewContext<V>,
|
||||||
|
f: impl FnOnce(Self, &mut ViewContext<Self>) -> Self,
|
||||||
|
) -> View<Self> {
|
||||||
|
let handle = cx.view().downgrade();
|
||||||
|
cx.build_view(|cx| {
|
||||||
|
f(
|
||||||
Self {
|
Self {
|
||||||
|
handle,
|
||||||
items: Default::default(),
|
items: Default::default(),
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
}
|
},
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
|
pub fn header(mut self, title: impl Into<SharedString>) -> Self {
|
||||||
self.items.push(ListItem::Header(ListSubHeader::new(title)));
|
self.items
|
||||||
|
.push(ContextMenuItem::Header(ListSubHeader::new(title)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn separator(mut self) -> Self {
|
pub fn separator(mut self) -> Self {
|
||||||
self.items.push(ListItem::Separator(ListSeparator));
|
self.items.push(ContextMenuItem::Separator(ListSeparator));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entry(mut self, label: Label, action: Box<dyn Action>) -> Self {
|
pub fn entry(
|
||||||
self.items.push(ListEntry::new(label).action(action).into());
|
mut self,
|
||||||
|
view: ListEntry<Self>,
|
||||||
|
on_click: impl Fn(&mut V, &mut ViewContext<V>) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.items
|
||||||
|
.push(ContextMenuItem::Entry(view, Rc::new(on_click)));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn action(self, view: ListEntry<Self>, action: Box<dyn Action>) -> Self {
|
||||||
|
// todo: add the keybindings to the list entry
|
||||||
|
self.entry(view, move |_, cx| cx.dispatch_action(action.boxed_clone()))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
// todo!()
|
// todo!()
|
||||||
cx.emit(Dismiss);
|
cx.emit(Manager::Dismiss);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
pub fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
cx.emit(Dismiss);
|
cx.emit(Manager::Dismiss);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Render for ContextMenu {
|
impl<V: Render> Render for ContextMenu<V> {
|
||||||
type Element = Div<Self>;
|
type Element = Div<Self>;
|
||||||
// todo!()
|
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
div().elevation_2(cx).flex().flex_row().child(
|
div().elevation_2(cx).flex().flex_row().child(
|
||||||
v_stack()
|
v_stack()
|
||||||
|
@ -71,7 +105,25 @@ impl Render for ContextMenu {
|
||||||
// .bg(cx.theme().colors().elevated_surface_background)
|
// .bg(cx.theme().colors().elevated_surface_background)
|
||||||
// .border()
|
// .border()
|
||||||
// .border_color(cx.theme().colors().border)
|
// .border_color(cx.theme().colors().border)
|
||||||
.child(List::new(self.items.clone())),
|
.child(List::new(
|
||||||
|
self.items
|
||||||
|
.iter()
|
||||||
|
.map(|item| match item {
|
||||||
|
ContextMenuItem::Separator(separator) => {
|
||||||
|
ListItem::Separator(separator.clone())
|
||||||
|
}
|
||||||
|
ContextMenuItem::Header(header) => ListItem::Header(header.clone()),
|
||||||
|
ContextMenuItem::Entry(entry, callback) => {
|
||||||
|
let callback = callback.clone();
|
||||||
|
let handle = self.handle.clone();
|
||||||
|
ListItem::Entry(entry.clone().on_click(move |this, cx| {
|
||||||
|
handle.update(cx, |view, cx| callback(view, cx)).ok();
|
||||||
|
cx.emit(Manager::Dismiss);
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -226,12 +278,13 @@ impl<V: 'static, M: ManagedView> Element<V> for MenuHandle<V, M> {
|
||||||
let new_menu = (builder)(view_state, cx);
|
let new_menu = (builder)(view_state, cx);
|
||||||
let menu2 = menu.clone();
|
let menu2 = menu.clone();
|
||||||
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
|
cx.subscribe(&new_menu, move |this, modal, e, cx| match e {
|
||||||
&Dismiss => {
|
&Manager::Dismiss => {
|
||||||
*menu2.borrow_mut() = None;
|
*menu2.borrow_mut() = None;
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
cx.focus_view(&new_menu);
|
||||||
*menu.borrow_mut() = Some(new_menu);
|
*menu.borrow_mut() = Some(new_menu);
|
||||||
|
|
||||||
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
|
*position.borrow_mut() = if attach.is_some() && child_layout_id.is_some() {
|
||||||
|
@ -260,16 +313,25 @@ pub use stories::*;
|
||||||
mod stories {
|
mod stories {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::story::Story;
|
use crate::story::Story;
|
||||||
use gpui::{actions, Div, Render, VisualContext};
|
use gpui::{actions, Div, Render};
|
||||||
|
|
||||||
actions!(PrintCurrentDate);
|
actions!(PrintCurrentDate, PrintBestFood);
|
||||||
|
|
||||||
fn build_menu(cx: &mut WindowContext, header: impl Into<SharedString>) -> View<ContextMenu> {
|
fn build_menu<V: Render>(
|
||||||
cx.build_view(|cx| {
|
cx: &mut ViewContext<V>,
|
||||||
ContextMenu::new(cx).header(header).separator().entry(
|
header: impl Into<SharedString>,
|
||||||
Label::new("Print current time"),
|
) -> View<ContextMenu<V>> {
|
||||||
PrintCurrentDate.boxed_clone(),
|
let handle = cx.view().clone();
|
||||||
)
|
ContextMenu::build(cx, |menu, _| {
|
||||||
|
menu.header(header)
|
||||||
|
.separator()
|
||||||
|
.entry(ListEntry::new(Label::new("Print current time")), |v, cx| {
|
||||||
|
println!("dispatching PrintCurrentTime action");
|
||||||
|
cx.dispatch_action(PrintCurrentDate.boxed_clone())
|
||||||
|
})
|
||||||
|
.entry(ListEntry::new(Label::new("Print best food")), |v, cx| {
|
||||||
|
cx.dispatch_action(PrintBestFood.boxed_clone())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -281,10 +343,14 @@ mod stories {
|
||||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
Story::container(cx)
|
Story::container(cx)
|
||||||
.on_action(|_, _: &PrintCurrentDate, _| {
|
.on_action(|_, _: &PrintCurrentDate, _| {
|
||||||
|
println!("printing unix time!");
|
||||||
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
|
if let Ok(unix_time) = std::time::UNIX_EPOCH.elapsed() {
|
||||||
println!("Current Unix time is {:?}", unix_time.as_secs());
|
println!("Current Unix time is {:?}", unix_time.as_secs());
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.on_action(|_, _: &PrintBestFood, _| {
|
||||||
|
println!("burrito");
|
||||||
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.flex_row()
|
.flex_row()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
use gpui::{div, Action};
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use gpui::{div, Div, Stateful, StatefulInteractiveComponent};
|
||||||
|
|
||||||
use crate::settings::user_settings;
|
use crate::settings::user_settings;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -172,35 +174,35 @@ pub enum ListEntrySize {
|
||||||
Medium,
|
Medium,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component, Clone)]
|
#[derive(Clone)]
|
||||||
pub enum ListItem {
|
pub enum ListItem<V: 'static> {
|
||||||
Entry(ListEntry),
|
Entry(ListEntry<V>),
|
||||||
Separator(ListSeparator),
|
Separator(ListSeparator),
|
||||||
Header(ListSubHeader),
|
Header(ListSubHeader),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ListEntry> for ListItem {
|
impl<V: 'static> From<ListEntry<V>> for ListItem<V> {
|
||||||
fn from(entry: ListEntry) -> Self {
|
fn from(entry: ListEntry<V>) -> Self {
|
||||||
Self::Entry(entry)
|
Self::Entry(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ListSeparator> for ListItem {
|
impl<V: 'static> From<ListSeparator> for ListItem<V> {
|
||||||
fn from(entry: ListSeparator) -> Self {
|
fn from(entry: ListSeparator) -> Self {
|
||||||
Self::Separator(entry)
|
Self::Separator(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ListSubHeader> for ListItem {
|
impl<V: 'static> From<ListSubHeader> for ListItem<V> {
|
||||||
fn from(entry: ListSubHeader) -> Self {
|
fn from(entry: ListSubHeader) -> Self {
|
||||||
Self::Header(entry)
|
Self::Header(entry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListItem {
|
impl<V: 'static> ListItem<V> {
|
||||||
fn render<V: 'static>(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
fn render(self, view: &mut V, ix: usize, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
match self {
|
match self {
|
||||||
ListItem::Entry(entry) => div().child(entry.render(view, cx)),
|
ListItem::Entry(entry) => div().child(entry.render(ix, cx)),
|
||||||
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
|
ListItem::Separator(separator) => div().child(separator.render(view, cx)),
|
||||||
ListItem::Header(header) => div().child(header.render(view, cx)),
|
ListItem::Header(header) => div().child(header.render(view, cx)),
|
||||||
}
|
}
|
||||||
|
@ -210,7 +212,7 @@ impl ListItem {
|
||||||
Self::Entry(ListEntry::new(label))
|
Self::Entry(ListEntry::new(label))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_entry(&mut self) -> Option<&mut ListEntry> {
|
pub fn as_entry(&mut self) -> Option<&mut ListEntry<V>> {
|
||||||
if let Self::Entry(entry) = self {
|
if let Self::Entry(entry) = self {
|
||||||
Some(entry)
|
Some(entry)
|
||||||
} else {
|
} else {
|
||||||
|
@ -219,8 +221,7 @@ impl ListItem {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
pub struct ListEntry<V> {
|
||||||
pub struct ListEntry {
|
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
// TODO: Reintroduce this
|
// TODO: Reintroduce this
|
||||||
// disclosure_control_style: DisclosureControlVisibility,
|
// disclosure_control_style: DisclosureControlVisibility,
|
||||||
|
@ -231,15 +232,13 @@ pub struct ListEntry {
|
||||||
size: ListEntrySize,
|
size: ListEntrySize,
|
||||||
toggle: Toggle,
|
toggle: Toggle,
|
||||||
variant: ListItemVariant,
|
variant: ListItemVariant,
|
||||||
on_click: Option<Box<dyn Action>>,
|
on_click: Option<Rc<dyn Fn(&mut V, &mut ViewContext<V>) + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Clone for ListEntry {
|
impl<V> Clone for ListEntry<V> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
disabled: self.disabled,
|
disabled: self.disabled,
|
||||||
// TODO: Reintroduce this
|
|
||||||
// disclosure_control_style: DisclosureControlVisibility,
|
|
||||||
indent_level: self.indent_level,
|
indent_level: self.indent_level,
|
||||||
label: self.label.clone(),
|
label: self.label.clone(),
|
||||||
left_slot: self.left_slot.clone(),
|
left_slot: self.left_slot.clone(),
|
||||||
|
@ -247,12 +246,12 @@ impl Clone for ListEntry {
|
||||||
size: self.size,
|
size: self.size,
|
||||||
toggle: self.toggle,
|
toggle: self.toggle,
|
||||||
variant: self.variant,
|
variant: self.variant,
|
||||||
on_click: self.on_click.as_ref().map(|opt| opt.boxed_clone()),
|
on_click: self.on_click.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListEntry {
|
impl<V: 'static> ListEntry<V> {
|
||||||
pub fn new(label: Label) -> Self {
|
pub fn new(label: Label) -> Self {
|
||||||
Self {
|
Self {
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
@ -267,8 +266,8 @@ impl ListEntry {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn action(mut self, action: impl Into<Box<dyn Action>>) -> Self {
|
pub fn on_click(mut self, handler: impl Fn(&mut V, &mut ViewContext<V>) + 'static) -> Self {
|
||||||
self.on_click = Some(action.into());
|
self.on_click = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,7 +306,7 @@ impl ListEntry {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
fn render(self, ix: usize, cx: &mut ViewContext<V>) -> Stateful<V, Div<V>> {
|
||||||
let settings = user_settings(cx);
|
let settings = user_settings(cx);
|
||||||
|
|
||||||
let left_content = match self.left_slot.clone() {
|
let left_content = match self.left_slot.clone() {
|
||||||
|
@ -328,21 +327,21 @@ impl ListEntry {
|
||||||
ListEntrySize::Medium => div().h_7(),
|
ListEntrySize::Medium => div().h_7(),
|
||||||
};
|
};
|
||||||
div()
|
div()
|
||||||
|
.id(ix)
|
||||||
.relative()
|
.relative()
|
||||||
.hover(|mut style| {
|
.hover(|mut style| {
|
||||||
style.background = Some(cx.theme().colors().editor_background.into());
|
style.background = Some(cx.theme().colors().editor_background.into());
|
||||||
style
|
style
|
||||||
})
|
})
|
||||||
.on_mouse_down(gpui::MouseButton::Left, {
|
.on_click({
|
||||||
let action = self.on_click.map(|action| action.boxed_clone());
|
let on_click = self.on_click.clone();
|
||||||
|
|
||||||
move |entry: &mut V, event, cx| {
|
move |view: &mut V, event, cx| {
|
||||||
if let Some(action) = action.as_ref() {
|
if let Some(on_click) = &on_click {
|
||||||
cx.dispatch_action(action.boxed_clone());
|
(on_click)(view, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.group("")
|
|
||||||
.bg(cx.theme().colors().surface_background)
|
.bg(cx.theme().colors().surface_background)
|
||||||
// TODO: Add focus state
|
// TODO: Add focus state
|
||||||
// .when(self.state == InteractionState::Focused, |this| {
|
// .when(self.state == InteractionState::Focused, |this| {
|
||||||
|
@ -391,8 +390,8 @@ impl ListSeparator {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Component)]
|
#[derive(Component)]
|
||||||
pub struct List {
|
pub struct List<V: 'static> {
|
||||||
items: Vec<ListItem>,
|
items: Vec<ListItem<V>>,
|
||||||
/// Message to display when the list is empty
|
/// Message to display when the list is empty
|
||||||
/// Defaults to "No items"
|
/// Defaults to "No items"
|
||||||
empty_message: SharedString,
|
empty_message: SharedString,
|
||||||
|
@ -400,8 +399,8 @@ pub struct List {
|
||||||
toggle: Toggle,
|
toggle: Toggle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl List {
|
impl<V: 'static> List<V> {
|
||||||
pub fn new(items: Vec<ListItem>) -> Self {
|
pub fn new(items: Vec<ListItem<V>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
items,
|
items,
|
||||||
empty_message: "No items".into(),
|
empty_message: "No items".into(),
|
||||||
|
@ -425,9 +424,14 @@ impl List {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render<V: 'static>(self, _view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> impl Component<V> {
|
||||||
let list_content = match (self.items.is_empty(), self.toggle) {
|
let list_content = match (self.items.is_empty(), self.toggle) {
|
||||||
(false, _) => div().children(self.items),
|
(false, _) => div().children(
|
||||||
|
self.items
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(ix, item)| item.render(view, ix, cx)),
|
||||||
|
),
|
||||||
(true, Toggle::Toggled(false)) => div(),
|
(true, Toggle::Toggled(false)) => div(),
|
||||||
(true, _) => {
|
(true, _) => {
|
||||||
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
|
div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted))
|
||||||
|
|
|
@ -478,7 +478,7 @@ pub fn static_new_notification_items_2<V: 'static>() -> Vec<Notification<V>> {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn static_project_panel_project_items() -> Vec<ListItem> {
|
pub fn static_project_panel_project_items<V>() -> Vec<ListItem<V>> {
|
||||||
vec![
|
vec![
|
||||||
ListEntry::new(Label::new("zed"))
|
ListEntry::new(Label::new("zed"))
|
||||||
.left_icon(Icon::FolderOpen.into())
|
.left_icon(Icon::FolderOpen.into())
|
||||||
|
@ -605,7 +605,7 @@ pub fn static_project_panel_project_items() -> Vec<ListItem> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn static_project_panel_single_items() -> Vec<ListItem> {
|
pub fn static_project_panel_single_items<V>() -> Vec<ListItem<V>> {
|
||||||
vec![
|
vec![
|
||||||
ListEntry::new(Label::new("todo.md"))
|
ListEntry::new(Label::new("todo.md"))
|
||||||
.left_icon(Icon::FileDoc.into())
|
.left_icon(Icon::FileDoc.into())
|
||||||
|
@ -622,7 +622,7 @@ pub fn static_project_panel_single_items() -> Vec<ListItem> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn static_collab_panel_current_call() -> Vec<ListItem> {
|
pub fn static_collab_panel_current_call<V>() -> Vec<ListItem<V>> {
|
||||||
vec![
|
vec![
|
||||||
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
|
ListEntry::new(Label::new("as-cii")).left_avatar("http://github.com/as-cii.png?s=50"),
|
||||||
ListEntry::new(Label::new("nathansobo"))
|
ListEntry::new(Label::new("nathansobo"))
|
||||||
|
@ -635,7 +635,7 @@ pub fn static_collab_panel_current_call() -> Vec<ListItem> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn static_collab_panel_channels() -> Vec<ListItem> {
|
pub fn static_collab_panel_channels<V>() -> Vec<ListItem<V>> {
|
||||||
vec![
|
vec![
|
||||||
ListEntry::new(Label::new("zed"))
|
ListEntry::new(Label::new("zed"))
|
||||||
.left_icon(Icon::Hash.into())
|
.left_icon(Icon::Hash.into())
|
||||||
|
|
|
@ -202,6 +202,14 @@ impl std::fmt::Display for PathMatcher {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialEq for PathMatcher {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
self.maybe_path.eq(&other.maybe_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for PathMatcher {}
|
||||||
|
|
||||||
impl PathMatcher {
|
impl PathMatcher {
|
||||||
pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
|
pub fn new(maybe_glob: &str) -> Result<Self, globset::Error> {
|
||||||
Ok(PathMatcher {
|
Ok(PathMatcher {
|
||||||
|
@ -211,7 +219,19 @@ impl PathMatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
|
pub fn is_match<P: AsRef<Path>>(&self, other: P) -> bool {
|
||||||
other.as_ref().starts_with(&self.maybe_path) || self.glob.is_match(other)
|
other.as_ref().starts_with(&self.maybe_path)
|
||||||
|
|| self.glob.is_match(&other)
|
||||||
|
|| self.check_with_end_separator(other.as_ref())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_with_end_separator(&self, path: &Path) -> bool {
|
||||||
|
let path_str = path.to_string_lossy();
|
||||||
|
let separator = std::path::MAIN_SEPARATOR_STR;
|
||||||
|
if path_str.ends_with(separator) {
|
||||||
|
self.glob.is_match(path)
|
||||||
|
} else {
|
||||||
|
self.glob.is_match(path_str.to_string() + separator)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -388,4 +408,14 @@ mod tests {
|
||||||
let path = Path::new("/a/b/c/.eslintrc.js");
|
let path = Path::new("/a/b/c/.eslintrc.js");
|
||||||
assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
|
assert_eq!(path.extension_or_hidden_file_name(), Some("js"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edge_of_glob() {
|
||||||
|
let path = Path::new("/work/node_modules");
|
||||||
|
let path_matcher = PathMatcher::new("**/node_modules/**").unwrap();
|
||||||
|
assert!(
|
||||||
|
path_matcher.is_match(&path),
|
||||||
|
"Path matcher {path_matcher} should match {path:?}"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme2::ActiveTheme;
|
use theme2::ActiveTheme;
|
||||||
use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
|
use ui::{
|
||||||
|
h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Label, ListEntry, Tooltip,
|
||||||
|
};
|
||||||
|
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
ChangePosition,
|
ChangePosition,
|
||||||
|
@ -40,7 +42,7 @@ pub trait Panel: FocusableView + EventEmitter<PanelEvent> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait PanelHandle: Send + Sync {
|
pub trait PanelHandle: Send + Sync {
|
||||||
fn id(&self) -> EntityId;
|
fn entity_id(&self) -> EntityId;
|
||||||
fn persistent_name(&self) -> &'static str;
|
fn persistent_name(&self) -> &'static str;
|
||||||
fn position(&self, cx: &WindowContext) -> DockPosition;
|
fn position(&self, cx: &WindowContext) -> DockPosition;
|
||||||
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
|
fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool;
|
||||||
|
@ -62,8 +64,8 @@ impl<T> PanelHandle for View<T>
|
||||||
where
|
where
|
||||||
T: Panel,
|
T: Panel,
|
||||||
{
|
{
|
||||||
fn id(&self) -> EntityId {
|
fn entity_id(&self) -> EntityId {
|
||||||
self.entity_id()
|
Entity::entity_id(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn persistent_name(&self) -> &'static str {
|
fn persistent_name(&self) -> &'static str {
|
||||||
|
@ -254,20 +256,19 @@ impl Dock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// todo!()
|
pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
|
||||||
// pub fn set_panel_zoomed(&mut self, panel: &AnyView, zoomed: bool, cx: &mut ViewContext<Self>) {
|
for entry in &mut self.panel_entries {
|
||||||
// for entry in &mut self.panel_entries {
|
if entry.panel.entity_id() == panel.entity_id() {
|
||||||
// if entry.panel.as_any() == panel {
|
if zoomed != entry.panel.is_zoomed(cx) {
|
||||||
// if zoomed != entry.panel.is_zoomed(cx) {
|
entry.panel.set_zoomed(zoomed, cx);
|
||||||
// entry.panel.set_zoomed(zoomed, cx);
|
}
|
||||||
// }
|
} else if entry.panel.is_zoomed(cx) {
|
||||||
// } else if entry.panel.is_zoomed(cx) {
|
entry.panel.set_zoomed(false, cx);
|
||||||
// entry.panel.set_zoomed(false, cx);
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
|
|
||||||
// cx.notify();
|
cx.notify();
|
||||||
// }
|
}
|
||||||
|
|
||||||
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
|
pub fn zoom_out(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
for entry in &mut self.panel_entries {
|
for entry in &mut self.panel_entries {
|
||||||
|
@ -277,42 +278,91 @@ impl Dock {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add_panel<T: Panel>(&mut self, panel: View<T>, cx: &mut ViewContext<Self>) {
|
pub(crate) fn add_panel<T: Panel>(
|
||||||
|
&mut self,
|
||||||
|
panel: View<T>,
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
let subscriptions = [
|
let subscriptions = [
|
||||||
cx.observe(&panel, |_, _, cx| cx.notify()),
|
cx.observe(&panel, |_, _, cx| cx.notify()),
|
||||||
cx.subscribe(&panel, |this, panel, event, cx| {
|
cx.subscribe(&panel, move |this, panel, event, cx| match event {
|
||||||
match event {
|
|
||||||
PanelEvent::ChangePosition => {
|
PanelEvent::ChangePosition => {
|
||||||
//todo!()
|
let new_position = panel.read(cx).position(cx);
|
||||||
// see: Workspace::add_panel_with_extra_event_handler
|
|
||||||
|
let Ok(new_dock) = workspace.update(cx, |workspace, cx| {
|
||||||
|
if panel.is_zoomed(cx) {
|
||||||
|
workspace.zoomed_position = Some(new_position);
|
||||||
|
}
|
||||||
|
match new_position {
|
||||||
|
DockPosition::Left => &workspace.left_dock,
|
||||||
|
DockPosition::Bottom => &workspace.bottom_dock,
|
||||||
|
DockPosition::Right => &workspace.right_dock,
|
||||||
|
}
|
||||||
|
.clone()
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let was_visible = this.is_open()
|
||||||
|
&& this.visible_panel().map_or(false, |active_panel| {
|
||||||
|
active_panel.entity_id() == Entity::entity_id(&panel)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.remove_panel(&panel, cx);
|
||||||
|
|
||||||
|
new_dock.update(cx, |new_dock, cx| {
|
||||||
|
new_dock.add_panel(panel.clone(), workspace.clone(), cx);
|
||||||
|
if was_visible {
|
||||||
|
new_dock.set_open(true, cx);
|
||||||
|
new_dock.activate_panel(this.panels_len() - 1, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
PanelEvent::ZoomIn => {
|
PanelEvent::ZoomIn => {
|
||||||
//todo!()
|
this.set_panel_zoomed(&panel.to_any(), true, cx);
|
||||||
// see: Workspace::add_panel_with_extra_event_handler
|
if !panel.has_focus(cx) {
|
||||||
|
cx.focus_view(&panel);
|
||||||
|
}
|
||||||
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
workspace.zoomed = Some(panel.downgrade().into());
|
||||||
|
workspace.zoomed_position = Some(panel.read(cx).position(cx));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
PanelEvent::ZoomOut => {
|
PanelEvent::ZoomOut => {
|
||||||
// todo!()
|
this.set_panel_zoomed(&panel.to_any(), false, cx);
|
||||||
// // see: Workspace::add_panel_with_extra_event_handler
|
workspace
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
if workspace.zoomed_position == Some(this.position) {
|
||||||
|
workspace.zoomed = None;
|
||||||
|
workspace.zoomed_position = None;
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
PanelEvent::Activate => {
|
PanelEvent::Activate => {
|
||||||
if let Some(ix) = this
|
if let Some(ix) = this
|
||||||
.panel_entries
|
.panel_entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|entry| entry.panel.id() == panel.id())
|
.position(|entry| entry.panel.entity_id() == Entity::entity_id(&panel))
|
||||||
{
|
{
|
||||||
this.set_open(true, cx);
|
this.set_open(true, cx);
|
||||||
this.activate_panel(ix, cx);
|
this.activate_panel(ix, cx);
|
||||||
//` todo!()
|
cx.focus_view(&panel);
|
||||||
// cx.focus(&panel);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PanelEvent::Close => {
|
PanelEvent::Close => {
|
||||||
if this.visible_panel().map_or(false, |p| p.id() == panel.id()) {
|
if this
|
||||||
|
.visible_panel()
|
||||||
|
.map_or(false, |p| p.entity_id() == Entity::entity_id(&panel))
|
||||||
|
{
|
||||||
this.set_open(false, cx);
|
this.set_open(false, cx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
PanelEvent::Focus => todo!(),
|
PanelEvent::Focus => todo!(),
|
||||||
}
|
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -335,7 +385,7 @@ impl Dock {
|
||||||
if let Some(panel_ix) = self
|
if let Some(panel_ix) = self
|
||||||
.panel_entries
|
.panel_entries
|
||||||
.iter()
|
.iter()
|
||||||
.position(|entry| entry.panel.id() == panel.id())
|
.position(|entry| entry.panel.entity_id() == Entity::entity_id(panel))
|
||||||
{
|
{
|
||||||
if panel_ix == self.active_panel_index {
|
if panel_ix == self.active_panel_index {
|
||||||
self.active_panel_index = 0;
|
self.active_panel_index = 0;
|
||||||
|
@ -396,7 +446,7 @@ impl Dock {
|
||||||
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
|
pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option<f32> {
|
||||||
self.panel_entries
|
self.panel_entries
|
||||||
.iter()
|
.iter()
|
||||||
.find(|entry| entry.panel.id() == panel.id())
|
.find(|entry| entry.panel.entity_id() == panel.entity_id())
|
||||||
.map(|entry| entry.panel.size(cx))
|
.map(|entry| entry.panel.size(cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -620,6 +670,7 @@ impl Render for PanelButtons {
|
||||||
let dock = self.dock.read(cx);
|
let dock = self.dock.read(cx);
|
||||||
let active_index = dock.active_panel_index;
|
let active_index = dock.active_panel_index;
|
||||||
let is_open = dock.is_open;
|
let is_open = dock.is_open;
|
||||||
|
let dock_position = dock.position;
|
||||||
|
|
||||||
let (menu_anchor, menu_attach) = match dock.position {
|
let (menu_anchor, menu_attach) = match dock.position {
|
||||||
DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
|
DockPosition::Left => (AnchorCorner::BottomLeft, AnchorCorner::TopLeft),
|
||||||
|
@ -632,9 +683,10 @@ impl Render for PanelButtons {
|
||||||
.panel_entries
|
.panel_entries
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter_map(|(i, panel)| {
|
.filter_map(|(i, entry)| {
|
||||||
let icon = panel.panel.icon(cx)?;
|
let icon = entry.panel.icon(cx)?;
|
||||||
let name = panel.panel.persistent_name();
|
let name = entry.panel.persistent_name();
|
||||||
|
let panel = entry.panel.clone();
|
||||||
|
|
||||||
let mut button: IconButton<Self> = if i == active_index && is_open {
|
let mut button: IconButton<Self> = if i == active_index && is_open {
|
||||||
let action = dock.toggle_action();
|
let action = dock.toggle_action();
|
||||||
|
@ -645,7 +697,7 @@ impl Render for PanelButtons {
|
||||||
.action(action.boxed_clone())
|
.action(action.boxed_clone())
|
||||||
.tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
.tooltip(move |_, cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
||||||
} else {
|
} else {
|
||||||
let action = panel.panel.toggle_action(cx);
|
let action = entry.panel.toggle_action(cx);
|
||||||
|
|
||||||
IconButton::new(name, icon)
|
IconButton::new(name, icon)
|
||||||
.action(action.boxed_clone())
|
.action(action.boxed_clone())
|
||||||
|
@ -656,7 +708,30 @@ impl Render for PanelButtons {
|
||||||
menu_handle()
|
menu_handle()
|
||||||
.id(name)
|
.id(name)
|
||||||
.menu(move |_, cx| {
|
.menu(move |_, cx| {
|
||||||
cx.build_view(|cx| ContextMenu::new(cx).header("SECTION"))
|
const POSITIONS: [DockPosition; 3] = [
|
||||||
|
DockPosition::Left,
|
||||||
|
DockPosition::Right,
|
||||||
|
DockPosition::Bottom,
|
||||||
|
];
|
||||||
|
ContextMenu::build(cx, |mut menu, cx| {
|
||||||
|
for position in POSITIONS {
|
||||||
|
if position != dock_position
|
||||||
|
&& panel.position_is_valid(position, cx)
|
||||||
|
{
|
||||||
|
let panel = panel.clone();
|
||||||
|
menu = menu.entry(
|
||||||
|
ListEntry::new(Label::new(format!(
|
||||||
|
"Dock {}",
|
||||||
|
position.to_label()
|
||||||
|
))),
|
||||||
|
move |_, cx| {
|
||||||
|
panel.set_position(position, cx);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
menu
|
||||||
|
})
|
||||||
})
|
})
|
||||||
.anchor(menu_anchor)
|
.anchor(menu_anchor)
|
||||||
.attach(menu_attach)
|
.attach(menu_attach)
|
||||||
|
|
|
@ -813,7 +813,9 @@ impl Workspace {
|
||||||
DockPosition::Right => &self.right_dock,
|
DockPosition::Right => &self.right_dock,
|
||||||
};
|
};
|
||||||
|
|
||||||
dock.update(cx, |dock, cx| dock.add_panel(panel, cx));
|
dock.update(cx, |dock, cx| {
|
||||||
|
dock.add_panel(panel, self.weak_self.clone(), cx)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status_bar(&self) -> &View<StatusBar> {
|
pub fn status_bar(&self) -> &View<StatusBar> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue