Merge branch 'main' into theme-variables

Nathan Sobo created

Change summary

gpui/src/platform/mac/fonts.rs |  42 +++++++-
zed/app-icon.png               |   0 
zed/app-icon@2x.png            |   0 
zed/src/file_finder.rs         | 185 +++++++++++++++--------------------
zed/src/workspace.rs           |  25 ++++
zed/src/worktree.rs            |  49 +++-----
zed/src/worktree/fuzzy.rs      |  44 +++----
7 files changed, 176 insertions(+), 169 deletions(-)

Detailed changes

gpui/src/platform/mac/fonts.rs 🔗

@@ -23,7 +23,7 @@ use core_graphics::{
 use core_text::{line::CTLine, string_attributes::kCTFontAttributeName};
 use font_kit::{canvas::RasterizationOptions, hinting::HintingOptions, source::SystemSource};
 use parking_lot::RwLock;
-use std::{cell::RefCell, char, convert::TryFrom, ffi::c_void};
+use std::{cell::RefCell, char, cmp, convert::TryFrom, ffi::c_void};
 
 #[allow(non_upper_case_globals)]
 const kCGImageAlphaOnly: u32 = 7;
@@ -199,6 +199,7 @@ impl FontSystemState {
         let mut string = CFMutableAttributedString::new();
         {
             string.replace_str(&CFString::new(text), CFRange::init(0, 0));
+            let utf16_line_len = string.char_len() as usize;
 
             let last_run: RefCell<Option<(usize, FontId)>> = Default::default();
             let font_runs = runs
@@ -226,12 +227,16 @@ impl FontSystemState {
             for (run_len, font_id) in font_runs {
                 let utf8_end = ix_converter.utf8_ix + run_len;
                 let utf16_start = ix_converter.utf16_ix;
+
+                if utf16_start >= utf16_line_len {
+                    break;
+                }
+
                 ix_converter.advance_to_utf8_ix(utf8_end);
+                let utf16_end = cmp::min(ix_converter.utf16_ix, utf16_line_len);
 
-                let cf_range = CFRange::init(
-                    utf16_start as isize,
-                    (ix_converter.utf16_ix - utf16_start) as isize,
-                );
+                let cf_range =
+                    CFRange::init(utf16_start as isize, (utf16_end - utf16_start) as isize);
                 let font = &self.fonts[font_id.0];
                 unsafe {
                     string.set_attribute(
@@ -245,6 +250,10 @@ impl FontSystemState {
                         &CFNumber::from(font_id.0 as i64),
                     );
                 }
+
+                if utf16_end == utf16_line_len {
+                    break;
+                }
             }
         }
 
@@ -483,7 +492,7 @@ mod tests {
     }
 
     #[test]
-    fn test_layout_line() {
+    fn test_wrap_line() {
         let fonts = FontSystem::new();
         let font_ids = fonts.load_family("Helvetica").unwrap();
         let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
@@ -499,4 +508,25 @@ mod tests {
             &["aaa ααα ".len(), "aaa ααα ✋✋✋ ".len(),]
         );
     }
+
+    #[test]
+    fn test_layout_line_bom_char() {
+        let fonts = FontSystem::new();
+        let font_ids = fonts.load_family("Helvetica").unwrap();
+        let font_id = fonts.select_font(&font_ids, &Default::default()).unwrap();
+
+        let line = "\u{feff}";
+        let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]);
+        assert_eq!(layout.len, line.len());
+        assert!(layout.runs.is_empty());
+
+        let line = "a\u{feff}b";
+        let layout = fonts.layout_line(line, 16., &[(line.len(), font_id, Default::default())]);
+        assert_eq!(layout.len, line.len());
+        assert_eq!(layout.runs.len(), 1);
+        assert_eq!(layout.runs[0].glyphs.len(), 2);
+        assert_eq!(layout.runs[0].glyphs[0].id, 68); // a
+                                                     // There's no glyph for \u{feff}
+        assert_eq!(layout.runs[0].glyphs[1].id, 69); // b
+    }
 }

zed/src/file_finder.rs 🔗

@@ -3,7 +3,7 @@ use crate::{
     settings::Settings,
     util,
     workspace::Workspace,
-    worktree::{match_paths, PathMatch, Worktree},
+    worktree::{match_paths, PathMatch},
 };
 use gpui::{
     elements::*,
@@ -124,9 +124,12 @@ impl FileFinder {
                 let finder = finder.read(cx);
                 let start = range.start;
                 range.end = cmp::min(range.end, finder.matches.len());
-                items.extend(finder.matches[range].iter().enumerate().filter_map(
-                    move |(i, path_match)| finder.render_match(path_match, start + i, cx),
-                ));
+                items.extend(
+                    finder.matches[range]
+                        .iter()
+                        .enumerate()
+                        .map(move |(i, path_match)| finder.render_match(path_match, start + i)),
+                );
             },
         );
 
@@ -135,12 +138,7 @@ impl FileFinder {
             .named("matches")
     }
 
-    fn render_match(
-        &self,
-        path_match: &PathMatch,
-        index: usize,
-        cx: &AppContext,
-    ) -> Option<ElementBox> {
+    fn render_match(&self, path_match: &PathMatch, index: usize) -> ElementBox {
         let selected_index = self.selected_index();
         let settings = self.settings.borrow();
         let style = if index == selected_index {
@@ -148,102 +146,88 @@ impl FileFinder {
         } else {
             &settings.theme.ui.selector.item
         };
-        self.labels_for_match(path_match, cx).map(
-            |(file_name, file_name_positions, full_path, full_path_positions)| {
-                let container = Container::new(
-                    Flex::row()
-                        .with_child(
-                            Container::new(
-                                LineBox::new(
+        let (file_name, file_name_positions, full_path, full_path_positions) =
+            self.labels_for_match(path_match);
+        let container = Container::new(
+            Flex::row()
+                .with_child(
+                    Container::new(
+                        LineBox::new(
+                            settings.ui_font_family,
+                            settings.ui_font_size,
+                            Svg::new("icons/file-16.svg")
+                                .with_color(style.label.text.color)
+                                .boxed(),
+                        )
+                        .boxed(),
+                    )
+                    .with_padding_right(6.0)
+                    .boxed(),
+                )
+                .with_child(
+                    Expanded::new(
+                        1.0,
+                        Flex::column()
+                            .with_child(
+                                Label::new(
+                                    file_name.to_string(),
                                     settings.ui_font_family,
                                     settings.ui_font_size,
-                                    Svg::new("icons/file-16.svg")
-                                        .with_color(style.label.text.color)
-                                        .boxed(),
                                 )
+                                .with_style(&style.label)
+                                .with_highlights(file_name_positions)
                                 .boxed(),
                             )
-                            .with_padding_right(6.0)
-                            .boxed(),
-                        )
-                        .with_child(
-                            Expanded::new(
-                                1.0,
-                                Flex::column()
-                                    .with_child(
-                                        Label::new(
-                                            file_name.to_string(),
-                                            settings.ui_font_family,
-                                            settings.ui_font_size,
-                                        )
-                                        .with_style(&style.label)
-                                        .with_highlights(file_name_positions)
-                                        .boxed(),
-                                    )
-                                    .with_child(
-                                        Label::new(
-                                            full_path,
-                                            settings.ui_font_family,
-                                            settings.ui_font_size,
-                                        )
-                                        .with_style(&style.label)
-                                        .with_highlights(full_path_positions)
-                                        .boxed(),
-                                    )
-                                    .boxed(),
+                            .with_child(
+                                Label::new(
+                                    full_path,
+                                    settings.ui_font_family,
+                                    settings.ui_font_size,
+                                )
+                                .with_style(&style.label)
+                                .with_highlights(full_path_positions)
+                                .boxed(),
                             )
                             .boxed(),
-                        )
-                        .boxed(),
+                    )
+                    .boxed(),
                 )
-                .with_style(&style.container);
-
-                let entry = (path_match.tree_id, path_match.path.clone());
-                EventHandler::new(container.boxed())
-                    .on_mouse_down(move |cx| {
-                        cx.dispatch_action("file_finder:select", entry.clone());
-                        true
-                    })
-                    .named("match")
-            },
+                .boxed(),
         )
-    }
+        .with_style(&style.container);
 
-    fn labels_for_match(
-        &self,
-        path_match: &PathMatch,
-        cx: &AppContext,
-    ) -> Option<(String, Vec<usize>, String, Vec<usize>)> {
-        self.worktree(path_match.tree_id, cx).map(|tree| {
-            let prefix = if path_match.include_root_name {
-                tree.root_name()
-            } else {
-                ""
-            };
+        let entry = (path_match.tree_id, path_match.path.clone());
+        EventHandler::new(container.boxed())
+            .on_mouse_down(move |cx| {
+                cx.dispatch_action("file_finder:select", entry.clone());
+                true
+            })
+            .named("match")
+    }
 
-            let path_string = path_match.path.to_string_lossy();
-            let full_path = [prefix, path_string.as_ref()].join("");
-            let path_positions = path_match.positions.clone();
+    fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec<usize>, String, Vec<usize>) {
+        let path_string = path_match.path.to_string_lossy();
+        let full_path = [path_match.path_prefix.as_ref(), path_string.as_ref()].join("");
+        let path_positions = path_match.positions.clone();
 
-            let file_name = path_match.path.file_name().map_or_else(
-                || prefix.to_string(),
-                |file_name| file_name.to_string_lossy().to_string(),
-            );
-            let file_name_start =
-                prefix.chars().count() + path_string.chars().count() - file_name.chars().count();
-            let file_name_positions = path_positions
-                .iter()
-                .filter_map(|pos| {
-                    if pos >= &file_name_start {
-                        Some(pos - file_name_start)
-                    } else {
-                        None
-                    }
-                })
-                .collect();
+        let file_name = path_match.path.file_name().map_or_else(
+            || path_match.path_prefix.to_string(),
+            |file_name| file_name.to_string_lossy().to_string(),
+        );
+        let file_name_start = path_match.path_prefix.chars().count() + path_string.chars().count()
+            - file_name.chars().count();
+        let file_name_positions = path_positions
+            .iter()
+            .filter_map(|pos| {
+                if pos >= &file_name_start {
+                    Some(pos - file_name_start)
+                } else {
+                    None
+                }
+            })
+            .collect();
 
-            (file_name, file_name_positions, full_path, path_positions)
-        })
+        (file_name, file_name_positions, full_path, path_positions)
     }
 
     fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext<Workspace>) {
@@ -392,11 +376,9 @@ impl FileFinder {
         self.cancel_flag = Arc::new(AtomicBool::new(false));
         let cancel_flag = self.cancel_flag.clone();
         Some(cx.spawn(|this, mut cx| async move {
-            let include_root_name = snapshots.len() > 1;
             let matches = match_paths(
-                snapshots.iter(),
+                &snapshots,
                 &query,
-                include_root_name,
                 false,
                 false,
                 100,
@@ -429,15 +411,6 @@ impl FileFinder {
             cx.notify();
         }
     }
-
-    fn worktree<'a>(&'a self, tree_id: usize, cx: &'a AppContext) -> Option<&'a Worktree> {
-        self.workspace
-            .upgrade(cx)?
-            .read(cx)
-            .worktrees()
-            .get(&tree_id)
-            .map(|worktree| worktree.read(cx))
-    }
 }
 
 #[cfg(test)]
@@ -625,7 +598,7 @@ mod tests {
             assert_eq!(finder.matches.len(), 1);
 
             let (file_name, file_name_positions, full_path, full_path_positions) =
-                finder.labels_for_match(&finder.matches[0], cx).unwrap();
+                finder.labels_for_match(&finder.matches[0]);
             assert_eq!(file_name, "the-file");
             assert_eq!(file_name_positions, &[0, 1, 4]);
             assert_eq!(full_path, "the-file");

zed/src/workspace.rs 🔗

@@ -1322,14 +1322,31 @@ mod tests {
         cx.dispatch_global_action("workspace:new_file", app_state);
         let window_id = *cx.window_ids().first().unwrap();
         let workspace = cx.root_view::<Workspace>(window_id).unwrap();
-        workspace.update(&mut cx, |workspace, cx| {
-            let editor = workspace
+        let editor = workspace.update(&mut cx, |workspace, cx| {
+            workspace
                 .active_item(cx)
                 .unwrap()
                 .to_any()
                 .downcast::<Editor>()
-                .unwrap();
-            assert!(editor.update(cx, |editor, cx| editor.text(cx).is_empty()));
+                .unwrap()
+        });
+
+        editor.update(&mut cx, |editor, cx| {
+            assert!(editor.text(cx).is_empty());
+        });
+
+        workspace.update(&mut cx, |workspace, cx| workspace.save_active_item(&(), cx));
+
+        let dir = TempDir::new("test-new-empty-workspace").unwrap();
+        cx.simulate_new_path_selection(|_| {
+            Some(dir.path().canonicalize().unwrap().join("the-new-name"))
+        });
+
+        editor
+            .condition(&cx, |editor, cx| editor.title(cx) == "the-new-name")
+            .await;
+        editor.update(&mut cx, |editor, cx| {
+            assert!(!editor.is_dirty(cx));
         });
     }
 

zed/src/worktree.rs 🔗

@@ -586,17 +586,11 @@ impl LocalWorktree {
 
         // After determining whether the root entry is a file or a directory, populate the
         // snapshot's "root name", which will be used for the purpose of fuzzy matching.
-        let mut root_name = abs_path
+        let root_name = abs_path
             .file_name()
             .map_or(String::new(), |f| f.to_string_lossy().to_string());
         let root_char_bag = root_name.chars().map(|c| c.to_ascii_lowercase()).collect();
-        let metadata = fs
-            .metadata(&abs_path)
-            .await?
-            .ok_or_else(|| anyhow!("root entry does not exist"))?;
-        if metadata.is_dir {
-            root_name.push('/');
-        }
+        let metadata = fs.metadata(&abs_path).await?;
 
         let (scan_states_tx, scan_states_rx) = smol::channel::unbounded();
         let (mut last_scan_state_tx, last_scan_state_rx) = watch::channel_with(ScanState::Scanning);
@@ -613,12 +607,14 @@ impl LocalWorktree {
                 removed_entry_ids: Default::default(),
                 next_entry_id: Arc::new(next_entry_id),
             };
-            snapshot.insert_entry(Entry::new(
-                path.into(),
-                &metadata,
-                &snapshot.next_entry_id,
-                snapshot.root_char_bag,
-            ));
+            if let Some(metadata) = metadata {
+                snapshot.insert_entry(Entry::new(
+                    path.into(),
+                    &metadata,
+                    &snapshot.next_entry_id,
+                    snapshot.root_char_bag,
+                ));
+            }
 
             let tree = Self {
                 snapshot: snapshot.clone(),
@@ -1229,12 +1225,10 @@ impl Snapshot {
         ChildEntriesIter::new(path, self)
     }
 
-    pub fn root_entry(&self) -> &Entry {
-        self.entry_for_path("").unwrap()
+    pub fn root_entry(&self) -> Option<&Entry> {
+        self.entry_for_path("")
     }
 
-    /// Returns the filename of the snapshot's root, plus a trailing slash if the snapshot's root is
-    /// a directory.
     pub fn root_name(&self) -> &str {
         &self.root_name
     }
@@ -1856,8 +1850,8 @@ impl BackgroundScanner {
             let snapshot = self.snapshot.lock();
             root_char_bag = snapshot.root_char_bag;
             next_entry_id = snapshot.next_entry_id.clone();
-            is_dir = snapshot.root_entry().is_dir();
-        }
+            is_dir = snapshot.root_entry().map_or(false, |e| e.is_dir())
+        };
 
         if is_dir {
             let path: Arc<Path> = Arc::from(Path::new(""));
@@ -2605,25 +2599,23 @@ mod tests {
 
         cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
             .await;
-        let snapshot = cx.read(|cx| {
+        let snapshots = [cx.read(|cx| {
             let tree = tree.read(cx);
             assert_eq!(tree.file_count(), 5);
             assert_eq!(
                 tree.inode_for_path("fennel/grape"),
                 tree.inode_for_path("finnochio/grape")
             );
-
             tree.snapshot()
-        });
+        })];
         let cancel_flag = Default::default();
         let results = cx
             .read(|cx| {
                 match_paths(
-                    Some(&snapshot).into_iter(),
+                    &snapshots,
                     "bna",
                     false,
                     false,
-                    false,
                     10,
                     &cancel_flag,
                     cx.background().clone(),
@@ -2663,20 +2655,19 @@ mod tests {
 
         cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
             .await;
-        let snapshot = cx.read(|cx| {
+        let snapshots = [cx.read(|cx| {
             let tree = tree.read(cx);
             assert_eq!(tree.file_count(), 0);
             tree.snapshot()
-        });
+        })];
         let cancel_flag = Default::default();
         let results = cx
             .read(|cx| {
                 match_paths(
-                    Some(&snapshot).into_iter(),
+                    &snapshots,
                     "dir",
                     false,
                     false,
-                    false,
                     10,
                     &cancel_flag,
                     cx.background().clone(),

zed/src/worktree/fuzzy.rs 🔗

@@ -48,7 +48,7 @@ pub struct PathMatch {
     pub positions: Vec<usize>,
     pub tree_id: usize,
     pub path: Arc<Path>,
-    pub include_root_name: bool,
+    pub path_prefix: Arc<str>,
 }
 
 #[derive(Clone, Debug)]
@@ -207,23 +207,19 @@ pub async fn match_strings(
     results
 }
 
-pub async fn match_paths<'a, T>(
-    snapshots: T,
+pub async fn match_paths(
+    snapshots: &[Snapshot],
     query: &str,
-    include_root_name: bool,
     include_ignored: bool,
     smart_case: bool,
     max_results: usize,
     cancel_flag: &AtomicBool,
     background: Arc<executor::Background>,
-) -> Vec<PathMatch>
-where
-    T: Clone + Send + Iterator<Item = &'a Snapshot> + 'a,
-{
+) -> Vec<PathMatch> {
     let path_count: usize = if include_ignored {
-        snapshots.clone().map(Snapshot::file_count).sum()
+        snapshots.iter().map(Snapshot::file_count).sum()
     } else {
-        snapshots.clone().map(Snapshot::visible_file_count).sum()
+        snapshots.iter().map(Snapshot::visible_file_count).sum()
     };
     if path_count == 0 {
         return Vec::new();
@@ -245,7 +241,6 @@ where
     background
         .scoped(|scope| {
             for (segment_idx, results) in segment_results.iter_mut().enumerate() {
-                let snapshots = snapshots.clone();
                 scope.spawn(async move {
                     let segment_start = segment_idx * segment_size;
                     let segment_end = segment_start + segment_size;
@@ -265,9 +260,16 @@ where
                             tree_start + snapshot.visible_file_count()
                         };
 
-                        let include_root_name =
-                            include_root_name || snapshot.root_entry().is_file();
                         if tree_start < segment_end && segment_start < tree_end {
+                            let path_prefix: Arc<str> =
+                                if snapshot.root_entry().map_or(false, |e| e.is_file()) {
+                                    snapshot.root_name().into()
+                                } else if snapshots.len() > 1 {
+                                    format!("{}/", snapshot.root_name()).into()
+                                } else {
+                                    "".into()
+                                };
+
                             let start = max(tree_start, segment_start) - tree_start;
                             let end = min(tree_end, segment_end) - tree_start;
                             let entries = if include_ignored {
@@ -288,7 +290,7 @@ where
 
                             matcher.match_paths(
                                 snapshot,
-                                include_root_name,
+                                path_prefix,
                                 paths,
                                 results,
                                 &cancel_flag,
@@ -360,19 +362,13 @@ impl<'a> Matcher<'a> {
     fn match_paths(
         &mut self,
         snapshot: &Snapshot,
-        include_root_name: bool,
+        path_prefix: Arc<str>,
         path_entries: impl Iterator<Item = PathMatchCandidate<'a>>,
         results: &mut Vec<PathMatch>,
         cancel_flag: &AtomicBool,
     ) {
         let tree_id = snapshot.id;
-        let prefix = if include_root_name {
-            snapshot.root_name()
-        } else {
-            ""
-        }
-        .chars()
-        .collect::<Vec<_>>();
+        let prefix = path_prefix.chars().collect::<Vec<_>>();
         let lowercase_prefix = prefix
             .iter()
             .map(|c| c.to_ascii_lowercase())
@@ -388,7 +384,7 @@ impl<'a> Matcher<'a> {
                 tree_id,
                 positions: Vec::new(),
                 path: candidate.path.clone(),
-                include_root_name,
+                path_prefix: path_prefix.clone(),
             },
         )
     }
@@ -772,7 +768,7 @@ mod tests {
                 root_char_bag: Default::default(),
                 next_entry_id: Default::default(),
             },
-            false,
+            "".into(),
             path_entries.into_iter(),
             &mut results,
             &cancel_flag,