diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ef718a4fa9..6bfc0ab683 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/editor/src/editor.rs b/crates/editor/src/editor.rs index 17712b7e78..76dec3e1b6 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 049b77116d..2bf478bef5 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -3440,7 +3440,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, ) { @@ -3459,11 +3459,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 @@ -3483,6 +3487,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(), diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c24fb5eea1..7d5c08be07 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::{ @@ -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, @@ -557,6 +560,7 @@ enum SearchMatchCandidate { }, Path { worktree_id: WorktreeId, + is_ignored: bool, path: Arc, }, } @@ -5742,13 +5746,18 @@ impl Project { .await .log_err(); } + 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 { @@ -5797,6 +5806,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; @@ -5809,6 +5819,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; } @@ -5917,11 +6015,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() { @@ -5933,7 +6044,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/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/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/search/src/project_search.rs b/crates/search/src/project_search.rs index 5f3a6db6d4..42969ecbb6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1767,16 +1767,13 @@ 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", + "icons/file_icons/git.svg", SearchOptions::INCLUDE_IGNORED, 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/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() 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 0512eaca7a..3603f1c7fd 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