diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index d5bcc6695e..ba9e3ae3cf 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/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> = 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; - ix_converter.advance_to_utf8_ix(utf8_end); - let cf_range = CFRange::init( - utf16_start as isize, - (ix_converter.utf16_ix - utf16_start) as isize, - ); + 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, (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 + } } diff --git a/zed/app-icon.png b/zed/app-icon.png index 8d8c77e8e7..6a5e263b3b 100644 Binary files a/zed/app-icon.png and b/zed/app-icon.png differ diff --git a/zed/app-icon@2x.png b/zed/app-icon@2x.png index 666d4e541e..913a5481c3 100644 Binary files a/zed/app-icon@2x.png and b/zed/app-icon@2x.png differ diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index f5bacbd452..7c9900c3f4 100644 --- a/zed/src/file_finder.rs +++ 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 { + 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( - settings.ui_font_family, - settings.ui_font_size, - Svg::new("icons/file-16.svg") - .with_color(style.label.text.color) - .boxed(), - ) + 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(), - ) - .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(), - ) - .boxed(), ) .boxed(), + ) + .with_padding_right(6.0) + .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") - }, + .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(), + ) + .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") } - fn labels_for_match( - &self, - path_match: &PathMatch, - cx: &AppContext, - ) -> Option<(String, Vec, String, Vec)> { - self.worktree(path_match.tree_id, cx).map(|tree| { - let prefix = if path_match.include_root_name { - tree.root_name() - } else { - "" - }; + fn labels_for_match(&self, path_match: &PathMatch) -> (String, Vec, String, Vec) { + 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 path_string = path_match.path.to_string_lossy(); - let full_path = [prefix, path_string.as_ref()].join(""); - let path_positions = path_match.positions.clone(); + 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(); - 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(); - - (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) { @@ -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"); diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 1006d5bc61..b01ff1faf9 100644 --- a/zed/src/workspace.rs +++ b/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::(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::() - .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)); }); } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 83ae844dba..55a0738f41 100644 --- a/zed/src/worktree.rs +++ b/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 = 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(), diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index a0f2c57edc..c565e56ed1 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -48,7 +48,7 @@ pub struct PathMatch { pub positions: Vec, pub tree_id: usize, pub path: Arc, - pub include_root_name: bool, + pub path_prefix: Arc, } #[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, -) -> Vec -where - T: Clone + Send + Iterator + 'a, -{ +) -> Vec { 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 = + 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, path_entries: impl Iterator>, results: &mut Vec, cancel_flag: &AtomicBool, ) { let tree_id = snapshot.id; - let prefix = if include_root_name { - snapshot.root_name() - } else { - "" - } - .chars() - .collect::>(); + let prefix = path_prefix.chars().collect::>(); 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,