Allow to include gitignored files into project search (#3394)

Kirill Bulatov created

Change summary

.github/workflows/ci.yml               |   2 
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 
crates/project/src/project.rs          | 123 ++++++++++++++++++++++++++-
crates/project/src/worktree.rs         |   2 
crates/project2/src/project2.rs        | 118 +++++++++++++++++++++++++
crates/project2/src/worktree.rs        |   2 
crates/search/src/project_search.rs    |   7 -
crates/search2/src/project_search.rs   |  21 ++++
crates/util/src/channel.rs             |   1 
crates/zed/Cargo.toml                  |   1 
script/bump-zed-minor-versions         |   1 
14 files changed, 293 insertions(+), 30 deletions(-)

Detailed changes

.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

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<Arc<Language>>>,
         cx: &mut ViewContext<'_, '_, Editor>,
     ) -> HashMap<ExcerptId, (ModelHandle<Buffer>, Global, Range<usize>)> {
+        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) {

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<Editor>,
             cx: &mut gpui::TestAppContext,
         ) -> Range<Point> {
-            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,

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<Arc<Language>>>,
         cx: &mut ViewContext<Editor>,
     ) -> HashMap<ExcerptId, (Model<Buffer>, clock::Global, Range<usize>)> {
+        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) {

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<Point> {
             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(),

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<Path>,
     },
 }
@@ -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)
                             })

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))

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<Path>,
     },
 }
@@ -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)
                             })?

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))

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<ProjectSearchBar>| {
                 let is_active = if let Some(search) = self.active_project_search.as_ref() {

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::<ToggleCaseSensitive>(SearchOptions::CASE_SENSITIVE, 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);
 }
 
@@ -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<ProjectSearchBar>| {
                 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()

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/",

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"

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