From 92953fb53d8cec4a66b1137928413cbe892250c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Nov 2023 16:41:02 +0200 Subject: [PATCH 1/6] If enabled, search in ignored files during project search --- .github/workflows/ci.yml | 2 - crates/project/src/project.rs | 83 ++++++++++++++++++++++++++++- crates/project/src/worktree.rs | 2 +- crates/search/src/project_search.rs | 5 +- crates/util/src/channel.rs | 1 - crates/zed/Cargo.toml | 1 - script/bump-zed-minor-versions | 1 - 7 files changed, 85 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 208d538976..ab156ec311 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -134,8 +134,6 @@ jobs: - uses: softprops/action-gh-release@v1 name: Upload app bundle to release - # TODO kb seems that zed.dev relies on GitHub releases for release version tracking. - # Find alternatives for `nightly` or just go on with more releases? if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} with: draft: true diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c24fb5eea1..8fe96516dc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -13,7 +13,7 @@ mod worktree_tests; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; use futures::{ channel::{ @@ -5742,8 +5742,89 @@ impl Project { .await .log_err(); } + background .scoped(|scope| { + if query.include_ignored() { + scope.spawn(async move { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let mut ignored_paths_to_process = + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); + while let Some(ignored_abs_path) = + ignored_paths_to_process.pop_front() + { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_abs_path_excluded(&ignored_abs_path) + { + continue; + } + if let Some(fs_metadata) = fs + .metadata(&ignored_abs_path) + .await + .with_context(|| { + format!("fetching fs metadata for {ignored_abs_path:?}") + }) + .log_err() + .flatten() + { + if fs_metadata.is_dir { + if let Some(mut subfiles) = fs + .read_dir(&ignored_abs_path) + .await + .with_context(|| { + format!( + "listing ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + while let Some(subfile) = subfiles.next().await { + if let Some(subfile) = subfile.log_err() { + ignored_paths_to_process + .push_front(subfile); + } + } + } + } else if !fs_metadata.is_symlink { + let matches = if let Some(file) = fs + .open_sync(&ignored_abs_path) + .await + .with_context(|| { + format!( + "Opening ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: ignored_entry.path.clone(), + }; + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + return; + } + } + } + } + } + } + } + }); + } + for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 82fa5d6020..c2ff8b19eb 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2226,7 +2226,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + pub fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5f3a6db6d4..d45b69ac2a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -219,6 +219,7 @@ impl ProjectSearch { this.no_results = Some(true); }); + // TODO kb check for cancellations here and actually stop the search while let Some((buffer, anchors)) = matches.next().await { let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); @@ -1767,7 +1768,7 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) }); - let mut include_ignored = is_semantic_disabled.then(|| { + let include_ignored = is_semantic_disabled.then(|| { render_option_button_icon( // TODO proper icon "icons/case_insensitive.svg", @@ -1775,8 +1776,6 @@ impl View for ProjectSearchBar { cx, ) }); - // TODO not implemented yet - let _ = include_ignored.take(); let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = if let Some(search) = self.active_project_search.as_ref() { diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 55f13df084..6ae5fbe844 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -59,7 +59,6 @@ impl ReleaseChannel { pub fn link_prefix(&self) -> &'static str { match self { ReleaseChannel::Dev => "https://zed.dev/dev/", - // TODO kb need to add server handling ReleaseChannel::Nightly => "https://zed.dev/nightly/", ReleaseChannel::Preview => "https://zed.dev/preview/", ReleaseChannel::Stable => "https://zed.dev/", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ab8d5b7efe..5ade25a271 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -171,7 +171,6 @@ osx_info_plist_exts = ["resources/info/*"] osx_url_schemes = ["zed-dev"] [package.metadata.bundle-nightly] -# TODO kb different icon? icon = ["resources/app-icon-preview@2x.png", "resources/app-icon-preview.png"] identifier = "dev.zed.Zed-Nightly" name = "Zed Nightly" diff --git a/script/bump-zed-minor-versions b/script/bump-zed-minor-versions index 9e03d8a70c..79cdf9ed82 100755 --- a/script/bump-zed-minor-versions +++ b/script/bump-zed-minor-versions @@ -59,7 +59,6 @@ if ! git show-ref --quiet refs/heads/${prev_minor_branch_name}; then echo "previous branch ${minor_branch_name} doesn't exist" exit 1 fi -# TODO kb anything else for RELEASE_CHANNEL == nightly needs to be done below? if [[ $(git show ${prev_minor_branch_name}:crates/zed/RELEASE_CHANNEL) != preview ]]; then echo "release channel on branch ${prev_minor_branch_name} should be preview" exit 1 From 566857b0b7b776b84f28d493ec7191387915f6d4 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Nov 2023 19:03:27 +0200 Subject: [PATCH 2/6] Output non-ignored files first --- crates/project/src/project.rs | 190 ++++++++++++++++------------ crates/search/src/project_search.rs | 2 +- 2 files changed, 108 insertions(+), 84 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 8fe96516dc..fed18a7643 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -557,6 +557,7 @@ enum SearchMatchCandidate { }, Path { worktree_id: WorktreeId, + is_ignored: bool, path: Arc, }, } @@ -5743,88 +5744,9 @@ impl Project { .log_err(); } + // TODO kb parallelize directory traversal background .scoped(|scope| { - if query.include_ignored() { - scope.spawn(async move { - for snapshot in snapshots { - for ignored_entry in snapshot - .entries(query.include_ignored()) - .filter(|e| e.is_ignored) - { - let mut ignored_paths_to_process = - VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); - while let Some(ignored_abs_path) = - ignored_paths_to_process.pop_front() - { - if !query.file_matches(Some(&ignored_abs_path)) - || snapshot.is_abs_path_excluded(&ignored_abs_path) - { - continue; - } - if let Some(fs_metadata) = fs - .metadata(&ignored_abs_path) - .await - .with_context(|| { - format!("fetching fs metadata for {ignored_abs_path:?}") - }) - .log_err() - .flatten() - { - if fs_metadata.is_dir { - if let Some(mut subfiles) = fs - .read_dir(&ignored_abs_path) - .await - .with_context(|| { - format!( - "listing ignored path {ignored_abs_path:?}" - ) - }) - .log_err() - { - while let Some(subfile) = subfiles.next().await { - if let Some(subfile) = subfile.log_err() { - ignored_paths_to_process - .push_front(subfile); - } - } - } - } else if !fs_metadata.is_symlink { - let matches = if let Some(file) = fs - .open_sync(&ignored_abs_path) - .await - .with_context(|| { - format!( - "Opening ignored path {ignored_abs_path:?}" - ) - }) - .log_err() - { - query.detect(file).unwrap_or(false) - } else { - false - }; - if matches { - let project_path = SearchMatchCandidate::Path { - worktree_id: snapshot.id(), - path: ignored_entry.path.clone(), - }; - if matching_paths_tx - .send(project_path) - .await - .is_err() - { - return; - } - } - } - } - } - } - } - }); - } - for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; @@ -5878,6 +5800,7 @@ impl Project { let project_path = SearchMatchCandidate::Path { worktree_id: snapshot.id(), path: entry.path.clone(), + is_ignored: entry.is_ignored, }; if matching_paths_tx.send(project_path).await.is_err() { break; @@ -5890,6 +5813,92 @@ impl Project { } }); } + + if query.include_ignored() { + scope.spawn(async move { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let mut ignored_paths_to_process = + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); + while let Some(ignored_abs_path) = + ignored_paths_to_process.pop_front() + { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_abs_path_excluded(&ignored_abs_path) + { + continue; + } + if let Some(fs_metadata) = fs + .metadata(&ignored_abs_path) + .await + .with_context(|| { + format!("fetching fs metadata for {ignored_abs_path:?}") + }) + .log_err() + .flatten() + { + if fs_metadata.is_dir { + if let Some(mut subfiles) = fs + .read_dir(&ignored_abs_path) + .await + .with_context(|| { + format!( + "listing ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + while let Some(subfile) = subfiles.next().await { + if let Some(subfile) = subfile.log_err() { + ignored_paths_to_process.push_back(subfile); + } + } + } + } else if !fs_metadata.is_symlink { + let matches = if let Some(file) = fs + .open_sync(&ignored_abs_path) + .await + .with_context(|| { + format!( + "Opening ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: Arc::from( + ignored_abs_path + .strip_prefix(snapshot.abs_path()) + .expect( + "scanning worktree-related files", + ), + ), + is_ignored: true, + }; + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + return; + } + } + } + } + } + } + } + }); + } }) .await; } @@ -5998,11 +6007,24 @@ impl Project { let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); cx.spawn(|this, cx| async move { - let mut buffers = vec![]; + let mut buffers = Vec::new(); + let mut ignored_buffers = Vec::new(); while let Some(entry) = matching_paths_rx.next().await { - buffers.push(entry); + if matches!( + entry, + SearchMatchCandidate::Path { + is_ignored: true, + .. + } + ) { + ignored_buffers.push(entry); + } else { + buffers.push(entry); + } } buffers.sort_by_key(|candidate| candidate.path()); + ignored_buffers.sort_by_key(|candidate| candidate.path()); + buffers.extend(ignored_buffers); let matching_paths = buffers.clone(); let _ = sorted_buffers_tx.send(buffers); for (index, candidate) in matching_paths.into_iter().enumerate() { @@ -6014,7 +6036,9 @@ impl Project { cx.spawn(|mut cx| async move { let buffer = match candidate { SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), - SearchMatchCandidate::Path { worktree_id, path } => this + SearchMatchCandidate::Path { + worktree_id, path, .. + } => this .update(&mut cx, |this, cx| { this.open_buffer((worktree_id, path), cx) }) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d45b69ac2a..07e620717a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -219,7 +219,7 @@ impl ProjectSearch { this.no_results = Some(true); }); - // TODO kb check for cancellations here and actually stop the search + // TODO kb check for cancellations here and actually stop the search? while let Some((buffer, anchors)) = matches.next().await { let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); From 71e9bd8fa3536fa3f478a0bcaa2200961f84161c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 22 Nov 2023 19:09:10 +0200 Subject: [PATCH 3/6] Use a git file icon for toggle gitignored search option --- crates/search/src/project_search.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 07e620717a..b630a05822 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1770,8 +1770,7 @@ impl View for ProjectSearchBar { let include_ignored = is_semantic_disabled.then(|| { render_option_button_icon( - // TODO proper icon - "icons/case_insensitive.svg", + "icons/file_icons/git.svg", SearchOptions::INCLUDE_IGNORED, cx, ) From c2751c717ed351feeac07cab1461c33d79509315 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Nov 2023 09:52:57 +0200 Subject: [PATCH 4/6] Parallelize ignored entries for search lookup --- crates/project/src/project.rs | 28 ++++++++++++++++++---------- crates/search/src/project_search.rs | 1 - 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fed18a7643..7d5c08be07 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -62,7 +62,10 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; -use smol::channel::{Receiver, Sender}; +use smol::{ + channel::{Receiver, Sender}, + lock::Semaphore, +}; use std::{ cmp::{self, Ordering}, convert::TryInto, @@ -5744,14 +5747,17 @@ impl Project { .log_err(); } - // TODO kb parallelize directory traversal background .scoped(|scope| { + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); + for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; let unnamed_buffers = opened_buffers.clone(); + let limiter = Arc::clone(&max_concurrent_workers); scope.spawn(async move { + let _guard = limiter.acquire().await; let mut snapshot_start_ix = 0; let mut abs_path = PathBuf::new(); for snapshot in snapshots { @@ -5815,12 +5821,14 @@ impl Project { } if query.include_ignored() { - scope.spawn(async move { - for snapshot in snapshots { - for ignored_entry in snapshot - .entries(query.include_ignored()) - .filter(|e| e.is_ignored) - { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let limiter = Arc::clone(&max_concurrent_workers); + scope.spawn(async move { + let _guard = limiter.acquire().await; let mut ignored_paths_to_process = VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); while let Some(ignored_abs_path) = @@ -5895,9 +5903,9 @@ impl Project { } } } - } + }); } - }); + } } }) .await; diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b630a05822..42969ecbb6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -219,7 +219,6 @@ impl ProjectSearch { this.no_results = Some(true); }); - // TODO kb check for cancellations here and actually stop the search? while let Some((buffer, anchors)) = matches.next().await { let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); From eee63835fb8b7ae93b9d1002d2bfe6283b9a308c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Nov 2023 10:25:17 +0200 Subject: [PATCH 5/6] Exclude more ignored/worktree-less/project-less buffers from inlay hint requests --- crates/editor/src/editor.rs | 16 ++++++++++++++-- crates/editor/src/inlay_hint_cache.rs | 6 ++++-- crates/editor2/src/editor.rs | 17 +++++++++++++++-- crates/editor2/src/inlay_hint_cache.rs | 6 ++++-- 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2558aec121..ab5d7e00af 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3423,7 +3423,7 @@ impl Editor { to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.excerpt_visible_offsets(required_languages.as_ref(), cx), + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), invalidate_cache, cx, ) { @@ -3442,11 +3442,15 @@ impl Editor { .collect() } - pub fn excerpt_visible_offsets( + pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, ) -> HashMap, Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_visible_start = self @@ -3466,6 +3470,14 @@ impl Editor { .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { let buffer = buffer_handle.read(cx); + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 6b2712e7bf..47d8a4cf1f 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { Some((_, _, current_visible_range)) => { let visible_offset_length = current_visible_range.len(); let double_visible_range = current_visible_range @@ -2237,7 +2237,9 @@ pub mod tests { editor: &ViewHandle, cx: &mut gpui::TestAppContext, ) -> Range { - let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + let ranges = editor.update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }); assert_eq!( ranges.len(), 1, diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index fa7bfd77cc..7937516598 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -3444,7 +3444,7 @@ impl Editor { to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( reason_description, - self.excerpt_visible_offsets(required_languages.as_ref(), cx), + self.excerpts_for_inlay_hints_query(required_languages.as_ref(), cx), invalidate_cache, cx, ) { @@ -3463,11 +3463,15 @@ impl Editor { .collect() } - pub fn excerpt_visible_offsets( + pub fn excerpts_for_inlay_hints_query( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext, ) -> HashMap, clock::Global, Range)> { + let Some(project) = self.project.as_ref() else { + return HashMap::default(); + }; + let project = project.read(cx); let multi_buffer = self.buffer().read(cx); let multi_buffer_snapshot = multi_buffer.snapshot(cx); let multi_buffer_visible_start = self @@ -3487,6 +3491,15 @@ impl Editor { .filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty()) .filter_map(|(buffer_handle, excerpt_visible_range, excerpt_id)| { let buffer = buffer_handle.read(cx); + let buffer_file = project::worktree::File::from_dyn(buffer.file())?; + let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?; + let worktree_entry = buffer_worktree + .read(cx) + .entry_for_id(buffer_file.project_entry_id(cx)?)?; + if worktree_entry.is_ignored { + return None; + } + let language = buffer.language()?; if let Some(restrict_to_languages) = restrict_to_languages { if !restrict_to_languages.contains(language) { diff --git a/crates/editor2/src/inlay_hint_cache.rs b/crates/editor2/src/inlay_hint_cache.rs index eba49ccbf7..1610c4826e 100644 --- a/crates/editor2/src/inlay_hint_cache.rs +++ b/crates/editor2/src/inlay_hint_cache.rs @@ -861,7 +861,7 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + let query_not_around_visible_range = match editor.excerpts_for_inlay_hints_query(None, cx).remove(&query.excerpt_id) { Some((_, _, current_visible_range)) => { let visible_offset_length = current_visible_range.len(); let double_visible_range = current_visible_range @@ -2201,7 +2201,9 @@ pub mod tests { cx: &mut gpui::TestAppContext, ) -> Range { let ranges = editor - .update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)) + .update(cx, |editor, cx| { + editor.excerpts_for_inlay_hints_query(None, cx) + }) .unwrap(); assert_eq!( ranges.len(), From cee6fd8dd3caed8dd4f695bc9d86a8b736de4524 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 23 Nov 2023 10:44:05 +0200 Subject: [PATCH 6/6] Port to gpui2 --- crates/project2/src/project2.rs | 118 ++++++++++++++++++++++++++- crates/project2/src/worktree.rs | 2 +- crates/search2/src/project_search.rs | 21 ++++- 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/crates/project2/src/project2.rs b/crates/project2/src/project2.rs index 3f7c9b7188..2057b20818 100644 --- a/crates/project2/src/project2.rs +++ b/crates/project2/src/project2.rs @@ -13,7 +13,7 @@ mod worktree_tests; use anyhow::{anyhow, Context as _, Result}; use client::{proto, Client, Collaborator, TypedEnvelope, UserStore}; use clock::ReplicaId; -use collections::{hash_map, BTreeMap, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet, VecDeque}; use copilot::Copilot; use futures::{ channel::{ @@ -63,6 +63,7 @@ use settings::{Settings, SettingsStore}; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; use smol::channel::{Receiver, Sender}; +use smol::lock::Semaphore; use std::{ cmp::{self, Ordering}, convert::TryInto, @@ -557,6 +558,7 @@ enum SearchMatchCandidate { }, Path { worktree_id: WorktreeId, + is_ignored: bool, path: Arc, }, } @@ -5815,11 +5817,15 @@ impl Project { } executor .scoped(|scope| { + let max_concurrent_workers = Arc::new(Semaphore::new(workers)); + for worker_ix in 0..workers { let worker_start_ix = worker_ix * paths_per_worker; let worker_end_ix = worker_start_ix + paths_per_worker; let unnamed_buffers = opened_buffers.clone(); + let limiter = Arc::clone(&max_concurrent_workers); scope.spawn(async move { + let _guard = limiter.acquire().await; let mut snapshot_start_ix = 0; let mut abs_path = PathBuf::new(); for snapshot in snapshots { @@ -5868,6 +5874,7 @@ impl Project { let project_path = SearchMatchCandidate::Path { worktree_id: snapshot.id(), path: entry.path.clone(), + is_ignored: entry.is_ignored, }; if matching_paths_tx.send(project_path).await.is_err() { break; @@ -5880,6 +5887,94 @@ impl Project { } }); } + + if query.include_ignored() { + for snapshot in snapshots { + for ignored_entry in snapshot + .entries(query.include_ignored()) + .filter(|e| e.is_ignored) + { + let limiter = Arc::clone(&max_concurrent_workers); + scope.spawn(async move { + let _guard = limiter.acquire().await; + let mut ignored_paths_to_process = + VecDeque::from([snapshot.abs_path().join(&ignored_entry.path)]); + while let Some(ignored_abs_path) = + ignored_paths_to_process.pop_front() + { + if !query.file_matches(Some(&ignored_abs_path)) + || snapshot.is_abs_path_excluded(&ignored_abs_path) + { + continue; + } + if let Some(fs_metadata) = fs + .metadata(&ignored_abs_path) + .await + .with_context(|| { + format!("fetching fs metadata for {ignored_abs_path:?}") + }) + .log_err() + .flatten() + { + if fs_metadata.is_dir { + if let Some(mut subfiles) = fs + .read_dir(&ignored_abs_path) + .await + .with_context(|| { + format!( + "listing ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + while let Some(subfile) = subfiles.next().await { + if let Some(subfile) = subfile.log_err() { + ignored_paths_to_process.push_back(subfile); + } + } + } + } else if !fs_metadata.is_symlink { + let matches = if let Some(file) = fs + .open_sync(&ignored_abs_path) + .await + .with_context(|| { + format!( + "Opening ignored path {ignored_abs_path:?}" + ) + }) + .log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: Arc::from( + ignored_abs_path + .strip_prefix(snapshot.abs_path()) + .expect( + "scanning worktree-related files", + ), + ), + is_ignored: true, + }; + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + return; + } + } + } + } + } + }); + } + } + } }) .await; } @@ -5986,11 +6081,24 @@ impl Project { let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); cx.spawn(move |this, cx| async move { - let mut buffers = vec![]; + let mut buffers = Vec::new(); + let mut ignored_buffers = Vec::new(); while let Some(entry) = matching_paths_rx.next().await { - buffers.push(entry); + if matches!( + entry, + SearchMatchCandidate::Path { + is_ignored: true, + .. + } + ) { + ignored_buffers.push(entry); + } else { + buffers.push(entry); + } } buffers.sort_by_key(|candidate| candidate.path()); + ignored_buffers.sort_by_key(|candidate| candidate.path()); + buffers.extend(ignored_buffers); let matching_paths = buffers.clone(); let _ = sorted_buffers_tx.send(buffers); for (index, candidate) in matching_paths.into_iter().enumerate() { @@ -6002,7 +6110,9 @@ impl Project { cx.spawn(move |mut cx| async move { let buffer = match candidate { SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), - SearchMatchCandidate::Path { worktree_id, path } => this + SearchMatchCandidate::Path { + worktree_id, path, .. + } => this .update(&mut cx, |this, cx| { this.open_buffer((worktree_id, path), cx) })? diff --git a/crates/project2/src/worktree.rs b/crates/project2/src/worktree.rs index fcb64c40b4..23aaa2a623 100644 --- a/crates/project2/src/worktree.rs +++ b/crates/project2/src/worktree.rs @@ -2222,7 +2222,7 @@ impl LocalSnapshot { paths } - fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { + pub fn is_abs_path_excluded(&self, abs_path: &Path) -> bool { self.file_scan_exclusions .iter() .any(|exclude_matcher| exclude_matcher.is_match(abs_path)) diff --git a/crates/search2/src/project_search.rs b/crates/search2/src/project_search.rs index f6e17bbee5..41dd87d4d3 100644 --- a/crates/search2/src/project_search.rs +++ b/crates/search2/src/project_search.rs @@ -85,6 +85,7 @@ pub fn init(cx: &mut AppContext) { cx.capture_action(ProjectSearchView::replace_next); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_option_action::(SearchOptions::INCLUDE_IGNORED, cx); add_toggle_filters_action::(cx); } @@ -1192,6 +1193,7 @@ impl ProjectSearchView { text, self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::CASE_SENSITIVE), + self.search_options.contains(SearchOptions::INCLUDE_IGNORED), included_files, excluded_files, ) { @@ -1210,6 +1212,7 @@ impl ProjectSearchView { text, self.search_options.contains(SearchOptions::WHOLE_WORD), self.search_options.contains(SearchOptions::CASE_SENSITIVE), + self.search_options.contains(SearchOptions::INCLUDE_IGNORED), included_files, excluded_files, ) { @@ -1764,6 +1767,14 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search.svg", SearchOptions::WHOLE_WORD, cx) }); + let include_ignored = is_semantic_disabled.then(|| { + render_option_button_icon( + "icons/file_icons/git.svg", + SearchOptions::INCLUDE_IGNORED, + cx, + ) + }); + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = if let Some(search) = self.active_project_search.as_ref() { let search = search.read(cx); @@ -1879,7 +1890,15 @@ impl View for ProjectSearchBar { .with_children(search.filters_enabled.then(|| { Flex::row() .with_child( - ChildView::new(&search.included_files_editor, cx) + Flex::row() + .with_child( + 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() .with_style(include_container_style) .constrained()