From 20d8a2a1ec8f01f71fc1402b04f5d14014b0c688 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 27 Jun 2023 11:52:04 -0600 Subject: [PATCH 001/124] vim: indent in visual mode uses only one < Fixes: zed-industries/community#1562 --- assets/keymaps/vim.json | 4 ++-- crates/vim/src/test.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 639daef614..84f9689c2c 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -314,8 +314,8 @@ "vim::SwitchMode", "Normal" ], - "> >": "editor::Indent", - "< <": "editor::Outdent" + ">": "editor::Indent", + "<": "editor::Outdent" } }, { diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index a6efbd4da1..95962f85b8 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -137,7 +137,7 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { cx.assert_editor_state("aa\nbˇb\ncc"); // works in visuial mode - cx.simulate_keystrokes(["shift-v", "down", ">", ">"]); + cx.simulate_keystrokes(["shift-v", "down", ">"]); cx.assert_editor_state("aa\n b«b\n cˇ»c"); } From 75fe77c11dc85a2f8ecbe42e9c6039eecc3f5f6c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 27 Jun 2023 21:46:08 -0600 Subject: [PATCH 002/124] search: Allow running a search with different options Refactor search options to use bitflags so that we can represent the entire set of settings in one place. --- Cargo.lock | 1 + crates/search/Cargo.toml | 1 + crates/search/src/buffer_search.rs | 181 +++++++++++++++++++++------- crates/search/src/project_search.rs | 70 ++++------- crates/search/src/search.rs | 43 ++++--- 5 files changed, 196 insertions(+), 100 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60ed830683..c6f5e70c9f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6428,6 +6428,7 @@ name = "search" version = "0.1.0" dependencies = [ "anyhow", + "bitflags", "client", "collections", "editor", diff --git a/crates/search/Cargo.toml b/crates/search/Cargo.toml index 7ef388f7c0..b37d0a46ad 100644 --- a/crates/search/Cargo.toml +++ b/crates/search/Cargo.toml @@ -9,6 +9,7 @@ path = "src/search.rs" doctest = false [dependencies] +bitflags = "1" collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 59d25c2659..140f5accab 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,5 +1,5 @@ use crate::{ - SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use collections::HashMap; @@ -42,12 +42,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::select_next_match_on_pane); cx.add_action(BufferSearchBar::select_prev_match_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); - add_toggle_option_action::(SearchOption::CaseSensitive, cx); - add_toggle_option_action::(SearchOption::WholeWord, cx); - add_toggle_option_action::(SearchOption::Regex, cx); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_option_action::(SearchOptions::REGEX, cx); } -fn add_toggle_option_action(option: SearchOption, cx: &mut AppContext) { +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) { @@ -69,9 +69,8 @@ pub struct BufferSearchBar { seachable_items_with_matches: HashMap, Vec>>, pending_search: Option>, - case_sensitive: bool, - whole_word: bool, - regex: bool, + search_options: SearchOptions, + default_options: SearchOptions, query_contains_error: bool, dismissed: bool, } @@ -153,19 +152,19 @@ impl View for BufferSearchBar { .with_children(self.render_search_option( supported_options.case, "Case", - SearchOption::CaseSensitive, + SearchOptions::CASE_SENSITIVE, cx, )) .with_children(self.render_search_option( supported_options.word, "Word", - SearchOption::WholeWord, + SearchOptions::WHOLE_WORD, cx, )) .with_children(self.render_search_option( supported_options.regex, "Regex", - SearchOption::Regex, + SearchOptions::REGEX, cx, )) .contained() @@ -250,9 +249,8 @@ impl BufferSearchBar { active_searchable_item_subscription: None, active_match_index: None, seachable_items_with_matches: Default::default(), - case_sensitive: false, - whole_word: false, - regex: false, + default_options: SearchOptions::NONE, + search_options: SearchOptions::NONE, pending_search: None, query_contains_error: false, dismissed: true, @@ -280,6 +278,17 @@ impl BufferSearchBar { } pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { + self.show_with_options(focus, suggest_query, self.default_options, cx) + } + + pub fn show_with_options( + &mut self, + focus: bool, + suggest_query: bool, + search_option: SearchOptions, + cx: &mut ViewContext, + ) -> bool { + self.search_options = search_option; let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -320,7 +329,7 @@ impl BufferSearchBar { &self, option_supported: bool, icon: &'static str, - option: SearchOption, + option: SearchOptions, cx: &mut ViewContext, ) -> Option> { if !option_supported { @@ -328,9 +337,9 @@ impl BufferSearchBar { } let tooltip_style = theme::current(cx).tooltip.clone(); - let is_active = self.is_search_option_enabled(option); + let is_active = self.search_options.contains(option); Some( - MouseEventHandler::::new(option as usize, cx, |state, cx| { + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -346,7 +355,7 @@ impl BufferSearchBar { }) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( - option as usize, + option.bits as usize, format!("Toggle {}", option.label()), Some(option.to_toggle_action()), tooltip_style, @@ -461,21 +470,10 @@ impl BufferSearchBar { } } - fn is_search_option_enabled(&self, search_option: SearchOption) -> bool { - match search_option { - SearchOption::WholeWord => self.whole_word, - SearchOption::CaseSensitive => self.case_sensitive, - SearchOption::Regex => self.regex, - } - } + fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { + self.search_options.toggle(search_option); + self.default_options = self.search_options; - fn toggle_search_option(&mut self, search_option: SearchOption, cx: &mut ViewContext) { - let value = match search_option { - SearchOption::WholeWord => &mut self.whole_word, - SearchOption::CaseSensitive => &mut self.case_sensitive, - SearchOption::Regex => &mut self.regex, - }; - *value = !*value; self.update_matches(false, cx); cx.notify(); } @@ -571,11 +569,11 @@ impl BufferSearchBar { self.active_match_index.take(); active_searchable_item.clear_matches(cx); } else { - let query = if self.regex { + let query = if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( query, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), Vec::new(), Vec::new(), ) { @@ -589,8 +587,8 @@ impl BufferSearchBar { } else { SearchQuery::text( query, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), Vec::new(), Vec::new(), ) @@ -656,8 +654,7 @@ mod tests { use language::Buffer; use unindent::Unindent as _; - #[gpui::test] - async fn test_search_simple(cx: &mut TestAppContext) { + fn init_test(cx: &mut TestAppContext) -> (ViewHandle, ViewHandle) { crate::project_search::tests::init_test(cx); let buffer = cx.add_model(|cx| { @@ -684,6 +681,13 @@ mod tests { search_bar }); + (editor, search_bar) + } + + #[gpui::test] + async fn test_search_simple(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + // Search for a string that appears with different casing. // By default, search is case-insensitive. search_bar.update(cx, |search_bar, cx| { @@ -708,7 +712,7 @@ mod tests { // Switch to a case sensitive search. search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOption::CaseSensitive, cx); + search_bar.toggle_search_option(SearchOptions::CASE_SENSITIVE, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -765,7 +769,7 @@ mod tests { // Switch to a whole word search. search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOption::WholeWord, cx); + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -966,4 +970,99 @@ mod tests { assert_eq!(search_bar.active_match_index, Some(2)); }); } + + #[gpui::test] + async fn test_search_with_options(cx: &mut TestAppContext) { + let (editor, search_bar) = init_test(cx); + + // show with options should make current search case sensitive + search_bar.update(cx, |search_bar, cx| { + search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); + search_bar.set_query("us", cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + )] + ); + }); + + // show should return to the default options (case insensitive) + search_bar.update(cx, |search_bar, cx| { + search_bar.show(true, true, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[ + ( + DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), + Color::red(), + ), + ( + DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), + Color::red(), + ) + ] + ); + }); + + // toggling a search option (even in show_with_options mode) should update the defaults + search_bar.update(cx, |search_bar, cx| { + search_bar.set_query("regex", cx); + search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ),] + ); + }); + + // defaults should still include whole word + search_bar.update(cx, |search_bar, cx| { + search_bar.show(true, true, cx); + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ),] + ); + }); + + // removing whole word changes the search again + search_bar.update(cx, |search_bar, cx| { + search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) + }); + editor.next_notification(cx).await; + editor.update(cx, |editor, cx| { + assert_eq!( + editor.all_background_highlights(cx), + &[ + ( + DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), + Color::red(), + ), + ( + DisplayPoint::new(0, 44)..DisplayPoint::new(0, 49), + Color::red() + ) + ] + ); + }); + } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 135194df6a..4e485eaaab 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,5 +1,5 @@ use crate::{ - SearchOption, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, + SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use anyhow::Result; @@ -51,12 +51,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::select_prev_match); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); - add_toggle_option_action::(SearchOption::CaseSensitive, cx); - add_toggle_option_action::(SearchOption::WholeWord, cx); - add_toggle_option_action::(SearchOption::Regex, cx); + add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); + add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); + add_toggle_option_action::(SearchOptions::REGEX, cx); } -fn add_toggle_option_action(option: SearchOption, cx: &mut AppContext) { +fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { if search_bar.update(cx, |search_bar, cx| { @@ -89,9 +89,7 @@ pub struct ProjectSearchView { model: ModelHandle, query_editor: ViewHandle, results_editor: ViewHandle, - case_sensitive: bool, - whole_word: bool, - regex: bool, + search_options: SearchOptions, panels_with_errors: HashSet, active_match_index: Option, search_id: usize, @@ -408,9 +406,7 @@ impl ProjectSearchView { let project; let excerpts; let mut query_text = String::new(); - let mut regex = false; - let mut case_sensitive = false; - let mut whole_word = false; + let mut options = SearchOptions::NONE; { let model = model.read(cx); @@ -418,9 +414,7 @@ impl ProjectSearchView { excerpts = model.excerpts.clone(); if let Some(active_query) = model.active_query.as_ref() { query_text = active_query.as_str().to_string(); - regex = active_query.is_regex(); - case_sensitive = active_query.case_sensitive(); - whole_word = active_query.whole_word(); + options = SearchOptions::from_query(active_query); } } cx.observe(&model, |this, _, cx| this.model_changed(cx)) @@ -496,9 +490,7 @@ impl ProjectSearchView { model, query_editor, results_editor, - case_sensitive, - whole_word, - regex, + search_options: options, panels_with_errors: HashSet::new(), active_match_index: None, query_editor_was_focused: false, @@ -594,11 +586,11 @@ impl ProjectSearchView { return None; } }; - if self.regex { + if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( text, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, ) { @@ -615,8 +607,8 @@ impl ProjectSearchView { } else { Some(SearchQuery::text( text, - self.whole_word, - self.case_sensitive, + self.search_options.contains(SearchOptions::WHOLE_WORD), + self.search_options.contains(SearchOptions::CASE_SENSITIVE), included_files, excluded_files, )) @@ -765,9 +757,7 @@ impl ProjectSearchBar { search_view.query_editor.update(cx, |editor, cx| { editor.set_text(old_query.as_str(), cx); }); - search_view.regex = old_query.is_regex(); - search_view.whole_word = old_query.whole_word(); - search_view.case_sensitive = old_query.case_sensitive(); + search_view.search_options = SearchOptions::from_query(&old_query); } } new_query @@ -855,15 +845,10 @@ impl ProjectSearchBar { }); } - fn toggle_search_option(&mut self, option: SearchOption, cx: &mut ViewContext) -> bool { + fn toggle_search_option(&mut self, option: SearchOptions, cx: &mut ViewContext) -> bool { if let Some(search_view) = self.active_project_search.as_ref() { search_view.update(cx, |search_view, cx| { - let value = match option { - SearchOption::WholeWord => &mut search_view.whole_word, - SearchOption::CaseSensitive => &mut search_view.case_sensitive, - SearchOption::Regex => &mut search_view.regex, - }; - *value = !*value; + search_view.search_options.toggle(option); search_view.search(cx); }); cx.notify(); @@ -920,12 +905,12 @@ impl ProjectSearchBar { fn render_option_button( &self, icon: &'static str, - option: SearchOption, + option: SearchOptions, cx: &mut ViewContext, ) -> AnyElement { let tooltip_style = theme::current(cx).tooltip.clone(); let is_active = self.is_option_enabled(option, cx); - MouseEventHandler::::new(option as usize, cx, |state, cx| { + MouseEventHandler::::new(option.bits as usize, cx, |state, cx| { let theme = theme::current(cx); let style = theme .search @@ -941,7 +926,7 @@ impl ProjectSearchBar { }) .with_cursor_style(CursorStyle::PointingHand) .with_tooltip::( - option as usize, + option.bits as usize, format!("Toggle {}", option.label()), Some(option.to_toggle_action()), tooltip_style, @@ -950,14 +935,9 @@ impl ProjectSearchBar { .into_any() } - fn is_option_enabled(&self, option: SearchOption, cx: &AppContext) -> bool { + fn is_option_enabled(&self, option: SearchOptions, cx: &AppContext) -> bool { if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - match option { - SearchOption::WholeWord => search.whole_word, - SearchOption::CaseSensitive => search.case_sensitive, - SearchOption::Regex => search.regex, - } + search.read(cx).search_options.contains(option) } else { false } @@ -1048,17 +1028,17 @@ impl View for ProjectSearchBar { Flex::row() .with_child(self.render_option_button( "Case", - SearchOption::CaseSensitive, + SearchOptions::CASE_SENSITIVE, cx, )) .with_child(self.render_option_button( "Word", - SearchOption::WholeWord, + SearchOptions::WHOLE_WORD, cx, )) .with_child(self.render_option_button( "Regex", - SearchOption::Regex, + SearchOptions::REGEX, cx, )) .contained() diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 90ea508cc6..efec4c2516 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -1,5 +1,7 @@ +use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; +use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; pub mod buffer_search; @@ -21,27 +23,40 @@ actions!( ] ); -#[derive(Clone, Copy, PartialEq)] -pub enum SearchOption { - WholeWord, - CaseSensitive, - Regex, +bitflags! { + #[derive(Default)] + pub struct SearchOptions: u8 { + const NONE = 0b000; + const WHOLE_WORD = 0b001; + const CASE_SENSITIVE = 0b010; + const REGEX = 0b100; + } } -impl SearchOption { +impl SearchOptions { pub fn label(&self) -> &'static str { - match self { - SearchOption::WholeWord => "Match Whole Word", - SearchOption::CaseSensitive => "Match Case", - SearchOption::Regex => "Use Regular Expression", + match *self { + SearchOptions::WHOLE_WORD => "Match Whole Word", + SearchOptions::CASE_SENSITIVE => "Match Case", + SearchOptions::REGEX => "Use Regular Expression", + _ => panic!("{:?} is not a named SearchOption", self), } } pub fn to_toggle_action(&self) -> Box { - match self { - SearchOption::WholeWord => Box::new(ToggleWholeWord), - SearchOption::CaseSensitive => Box::new(ToggleCaseSensitive), - SearchOption::Regex => Box::new(ToggleRegex), + match *self { + SearchOptions::WHOLE_WORD => Box::new(ToggleWholeWord), + SearchOptions::CASE_SENSITIVE => Box::new(ToggleCaseSensitive), + SearchOptions::REGEX => Box::new(ToggleRegex), + _ => panic!("{:?} is not a named SearchOption", self), } } + + pub fn from_query(query: &SearchQuery) -> SearchOptions { + let mut options = SearchOptions::NONE; + options.set(SearchOptions::WHOLE_WORD, query.whole_word()); + options.set(SearchOptions::CASE_SENSITIVE, query.case_sensitive()); + options.set(SearchOptions::REGEX, query.is_regex()); + options + } } From 2ffce24ef09715e754e078f61f4cba6ffdc29f9a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 11:47:14 -0600 Subject: [PATCH 003/124] vim: Don't enter visual mode in search/go to definition Fixes: zed-industries/community#1514 Contributes: zed-industries/community#1284 --- crates/editor/src/editor.rs | 15 +++++++++++++++ crates/editor/src/items.rs | 5 ++--- crates/vim/src/vim.rs | 4 ++++ 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d7b8ffad6..259037c3ff 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -544,6 +544,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + collapse_matches: bool, workspace: Option<(WeakViewHandle, i64)>, keymap_context_layers: BTreeMap, input_enabled: bool, @@ -1376,6 +1377,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + collapse_matches: false, workspace: None, keymap_context_layers: Default::default(), input_enabled: true, @@ -1515,6 +1517,17 @@ impl Editor { cx.notify(); } + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } + + fn range_for_match(&self, range: &Range) -> Range { + if self.collapse_matches { + return range.start..range.start; + } + range.clone() + } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { if self.display_map.read(cx).clip_at_line_ends != clip { self.display_map @@ -6233,6 +6246,7 @@ impl Editor { .to_offset(definition.target.buffer.read(cx)); if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { + let range = self.range_for_match(&range); self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); @@ -6245,6 +6259,7 @@ impl Editor { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. pane.update(cx, |pane, _| pane.disable_history()); + let range = target_editor.range_for_match(&range); target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([range]); }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 74b8e0ddb6..86f26bcf94 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -936,9 +936,8 @@ impl SearchableItem for Editor { cx: &mut ViewContext, ) { self.unfold_ranges([matches[index].clone()], false, true, cx); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([matches[index].clone()]) - }); + let range = self.range_for_match(&matches[index]); + self.change_selections(Some(Autoscroll::fit()), cx, |s| s.select_ranges([range])); } fn match_index_for_direction( diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 2bcc2254ee..ada8f2c1de 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -295,11 +295,15 @@ impl Vim { if self.enabled && editor.mode() == EditorMode::Full { editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(state.clip_at_line_end(), cx); + editor.set_collapse_matches(true); editor.set_input_enabled(!state.vim_controlled()); editor.selections.line_mode = matches!(state.mode, Mode::Visual { line: true }); let context_layer = state.keymap_context_layer(); editor.set_keymap_context_layer::(context_layer, cx); } else { + // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur, + // but we need collapse_matches to persist when the search bar is focused. + editor.set_collapse_matches(false); Self::unhook_vim_settings(editor, cx); } }); From 96ce0bb78338d185bf0e2c223696f748e3c319a1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 12:48:15 -0600 Subject: [PATCH 004/124] vim: Enter/n/N to navigate search results --- assets/keymaps/vim.json | 8 ++++++++ crates/vim/src/test.rs | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 84f9689c2c..72aa719408 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -60,6 +60,8 @@ "ignorePunctuation": true } ], + "n": "search::SelectNextMatch", + "shift-n": "search::SelectPrevMatch", "%": "vim::Matching", "f": [ "vim::PushOperator", @@ -335,5 +337,11 @@ "Normal" ] } + }, + { + "context": "BufferSearchBar", + "bindings": { + "enter": "buffer_search::FocusEditor" + } } ] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 95962f85b8..d9d24ec30e 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -5,6 +5,7 @@ mod vim_binding_test_context; mod vim_test_context; use command_palette::CommandPalette; +use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; pub use vim_binding_test_context::*; @@ -153,3 +154,44 @@ async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { assert!(!cx.workspace(|workspace, _| workspace.modal::().is_some())); cx.assert_state("aˇbc\n", Mode::Insert); } + +#[gpui::test] +async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state(indoc! {"aa\nbˇb\ncc\ncc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["/", "c", "c"]); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + }); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.update_editor(|editor, cx| { + let highlights = editor.all_background_highlights(cx); + assert_eq!(3, highlights.len()); + assert_eq!( + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), + highlights[0].0 + ) + }); + cx.simulate_keystrokes(["enter"]); + + cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["n"]); + cx.assert_state(indoc! {"aa\nbb\ncc\nˇcc\ncc\n"}, Mode::Normal); + cx.simulate_keystrokes(["shift-n"]); + cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); +} From dbec2ed1f137dc36dfd359f548da2f10d768dfeb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 13:13:02 -0600 Subject: [PATCH 005/124] vim: add */#/g*/g# for jumping to next word As in vim, this toggles the normal search experience. --- assets/keymaps/vim.json | 16 ++++- crates/search/src/buffer_search.rs | 28 +++++++- crates/vim/src/normal.rs | 4 ++ crates/vim/src/normal/search.rs | 108 +++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 4 deletions(-) create mode 100644 crates/vim/src/normal/search.rs diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 72aa719408..20b831b0e5 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -101,6 +101,8 @@ "vim::SwitchMode", "Normal" ], + "*": "vim::MoveToNext", + "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -240,7 +242,19 @@ "vim::SwitchMode", "Normal" ], - "d": "editor::GoToDefinition" + "d": "editor::GoToDefinition", + "*": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "#": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ] } }, { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 140f5accab..fb4d5da764 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -65,6 +65,7 @@ pub struct BufferSearchBar { pub query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, + pending_match_direction: Option, active_searchable_item_subscription: Option, seachable_items_with_matches: HashMap, Vec>>, @@ -252,6 +253,7 @@ impl BufferSearchBar { default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, + pending_match_direction: None, query_contains_error: false, dismissed: true, } @@ -285,10 +287,10 @@ impl BufferSearchBar { &mut self, focus: bool, suggest_query: bool, - search_option: SearchOptions, + search_options: SearchOptions, cx: &mut ViewContext, ) -> bool { - self.search_options = search_option; + self.search_options = search_options; let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { @@ -486,6 +488,17 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } + pub fn select_word_under_cursor( + &mut self, + direction: Direction, + options: SearchOptions, + cx: &mut ViewContext, + ) { + self.active_match_index = None; + self.pending_match_direction = Some(direction); + self.show_with_options(false, true, options, cx); + } + pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { @@ -567,6 +580,7 @@ impl BufferSearchBar { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); + self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); } else { let query = if self.search_options.contains(SearchOptions::REGEX) { @@ -614,7 +628,15 @@ impl BufferSearchBar { .unwrap(); active_searchable_item.update_matches(matches, cx); if select_closest_match { - if let Some(match_ix) = this.active_match_index { + if let Some(mut match_ix) = this.active_match_index { + if let Some(direction) = this.pending_match_direction.take() + { + match_ix += match direction { + Direction::Next => 1, + Direction::Prev => matches.len() - 1, + }; + match_ix = match_ix % matches.len(); + } active_searchable_item .activate_match(match_ix, matches, cx); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 1227afbb85..c382a08b5c 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -2,6 +2,7 @@ mod case; mod change; mod delete; mod scroll; +mod search; mod substitute; mod yank; @@ -27,6 +28,7 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, + search::{move_to_next, move_to_prev}, substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -57,6 +59,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); + cx.add_action(move_to_next); + cx.add_action(move_to_prev); cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs new file mode 100644 index 0000000000..49eb97c9ac --- /dev/null +++ b/crates/vim/src/normal/search.rs @@ -0,0 +1,108 @@ +use gpui::{impl_actions, ViewContext}; +use search::{BufferSearchBar, SearchOptions}; +use serde_derive::Deserialize; +use workspace::{searchable::Direction, Workspace}; + +use crate::Vim; + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToNext { + #[serde(default)] + partial_word: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +pub(crate) struct MoveToPrev { + #[serde(default)] + partial_word: bool, +} + +impl_actions!(vim, [MoveToNext, MoveToPrev]); + +pub(crate) fn move_to_next( + workspace: &mut Workspace, + action: &MoveToNext, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Next, !action.partial_word, cx) +} + +pub(crate) fn move_to_prev( + workspace: &mut Workspace, + action: &MoveToPrev, + cx: &mut ViewContext, +) { + move_to_internal(workspace, Direction::Prev, !action.partial_word, cx) +} + +fn move_to_internal( + workspace: &mut Workspace, + direction: Direction, + whole_word: bool, + cx: &mut ViewContext, +) { + Vim::update(cx, |vim, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let mut options = SearchOptions::CASE_SENSITIVE; + options.set(SearchOptions::WHOLE_WORD, whole_word); + search_bar.select_word_under_cursor(direction, options, cx); + }); + } + }); + vim.clear_operator(cx); + }); +} + +#[cfg(test)] +mod test { + use search::BufferSearchBar; + + use crate::{state::Mode, test::VimTestContext}; + + #[gpui::test] + async fn test_move_to_next(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["g", "*"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + + cx.simulate_keystrokes(["n"]); + cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); + + cx.simulate_keystrokes(["g", "#"]); + search_bar.next_notification(&cx).await; + cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); + } +} From d70f415e8ea81c40083623b976cec2120cee44c7 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 28 Jun 2023 13:17:48 -0600 Subject: [PATCH 006/124] vim: add gD to go to type definition --- assets/keymaps/vim.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 20b831b0e5..b9497394ab 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -243,6 +243,7 @@ "Normal" ], "d": "editor::GoToDefinition", + "shift-d": "editor::GoToTypeDefinition", "*": [ "vim::MoveToNext", { From 6cf13c62d11334dbe145c248d816a1831f08be87 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 6 Jul 2023 13:21:01 -0600 Subject: [PATCH 007/124] vim: ? to search backwards, and / to repeat search --- assets/keymaps/vim.json | 10 ++- crates/vim/src/normal.rs | 4 +- crates/vim/src/normal/search.rs | 110 +++++++++++++++++++++--- crates/vim/src/test.rs | 2 +- crates/vim/src/test/vim_test_context.rs | 1 + 5 files changed, 106 insertions(+), 21 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index b9497394ab..4a215dcef3 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -197,10 +197,11 @@ "p": "vim::Paste", "u": "editor::Undo", "ctrl-r": "editor::Redo", - "/": [ - "buffer_search::Deploy", + "/": "vim::Search", + "?": [ + "vim::Search", { - "focus": true + "backwards": true, } ], "ctrl-f": "vim::PageDown", @@ -356,7 +357,8 @@ { "context": "BufferSearchBar", "bindings": { - "enter": "buffer_search::FocusEditor" + "enter": "buffer_search::FocusEditor", + "escape": "buffer_search::Dismiss" } } ] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index c382a08b5c..8dcaa5008e 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -28,7 +28,6 @@ use self::{ case::change_case, change::{change_motion, change_object}, delete::{delete_motion, delete_object}, - search::{move_to_next, move_to_prev}, substitute::substitute, yank::{yank_motion, yank_object}, }; @@ -59,8 +58,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(insert_line_above); cx.add_action(insert_line_below); cx.add_action(change_case); - cx.add_action(move_to_next); - cx.add_action(move_to_prev); + search::init(cx); cx.add_action(|_: &mut Workspace, _: &Substitute, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 49eb97c9ac..5f0ed9e7b9 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,4 +1,4 @@ -use gpui::{impl_actions, ViewContext}; +use gpui::{impl_actions, AppContext, ViewContext}; use search::{BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; use workspace::{searchable::Direction, Workspace}; @@ -19,25 +19,47 @@ pub(crate) struct MoveToPrev { partial_word: bool, } -impl_actions!(vim, [MoveToNext, MoveToPrev]); +#[derive(Clone, Deserialize, PartialEq)] +pub(crate) struct Search { + #[serde(default)] + backwards: bool, +} -pub(crate) fn move_to_next( - workspace: &mut Workspace, - action: &MoveToNext, - cx: &mut ViewContext, -) { +impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); + +pub(crate) fn init(cx: &mut AppContext) { + cx.add_action(move_to_next); + cx.add_action(move_to_prev); + cx.add_action(search); +} + +fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { move_to_internal(workspace, Direction::Next, !action.partial_word, cx) } -pub(crate) fn move_to_prev( - workspace: &mut Workspace, - action: &MoveToPrev, - cx: &mut ViewContext, -) { +fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewContext) { move_to_internal(workspace, Direction::Prev, !action.partial_word, cx) } -fn move_to_internal( +fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + search_bar.select_match(direction, cx); + search_bar.show_with_options(true, false, options, cx); + }) + } + }) +} + +pub fn move_to_internal( workspace: &mut Workspace, direction: Direction, whole_word: bool, @@ -60,6 +82,7 @@ fn move_to_internal( #[cfg(test)] mod test { + use editor::DisplayPoint; use search::BufferSearchBar; use crate::{state::Mode, test::VimTestContext}; @@ -105,4 +128,65 @@ mod test { search_bar.next_notification(&cx).await; cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); } + + #[gpui::test] + async fn test_search(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "c", "c"]); + + let search_bar = cx.workspace(|workspace, cx| { + workspace + .active_pane() + .read(cx) + .toolbar() + .read(cx) + .item_of_type::() + .expect("Buffer search bar should be deployed") + }); + + search_bar.read_with(cx.cx, |bar, cx| { + assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + }); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.update_editor(|editor, cx| { + let highlights = editor.all_background_highlights(cx); + assert_eq!(3, highlights.len()); + assert_eq!( + DisplayPoint::new(2, 0)..DisplayPoint::new(2, 2), + highlights[0].0 + ) + }); + + cx.simulate_keystrokes(["enter"]); + + // n to go to next/N to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["n"]); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["shift-n"]); + + // ? to go to previous + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + cx.simulate_keystrokes(["?", "enter"]); + + // / to go to next + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); + cx.simulate_keystrokes(["/", "enter"]); + cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); + + // ?{search} to search backwards + cx.simulate_keystrokes(["?", "b", "enter"]); + + // wait for the query editor change event to fire. + search_bar.next_notification(&cx).await; + + cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index d9d24ec30e..8ed649e61b 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -97,7 +97,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "jumps"); + assert_eq!(bar.query_editor.read(cx).text(cx), ""); }) } diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index f9ba577231..56ca654644 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -90,6 +90,7 @@ impl<'a> VimTestContext<'a> { self.cx.set_state(text) } + #[track_caller] pub fn assert_state(&mut self, text: &str, mode: Mode) { self.assert_editor_state(text); assert_eq!(self.mode(), mode, "{}", self.assertion_context()); From dea728a7e58f746d5a1429894451286febd85d66 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 09:10:11 -0600 Subject: [PATCH 008/124] Better waiting in tests --- crates/vim/src/normal/search.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 5f0ed9e7b9..bb57045add 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -82,13 +82,18 @@ pub fn move_to_internal( #[cfg(test)] mod test { + use std::sync::Arc; + use editor::DisplayPoint; use search::BufferSearchBar; use crate::{state::Mode, test::VimTestContext}; #[gpui::test] - async fn test_move_to_next(cx: &mut gpui::TestAppContext) { + async fn test_move_to_next( + cx: &mut gpui::TestAppContext, + deterministic: Arc, + ) { let mut cx = VimTestContext::new(cx, true).await; let search_bar = cx.workspace(|workspace, cx| { workspace @@ -102,30 +107,30 @@ mod test { cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); cx.simulate_keystrokes(["#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["g", "*"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["n"]); cx.assert_state("hi\nhigh\nˇhi\n", Mode::Normal); cx.simulate_keystrokes(["g", "#"]); - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); } From 232d14a3ae6a86f027edcf907fbf74f3bd44e8e1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 10:55:32 -0600 Subject: [PATCH 009/124] Make search less magic Co-Authored-By: Antonio --- crates/ai/src/assistant.rs | 18 ++- crates/search/src/buffer_search.rs | 189 ++++++++++++++++------------- crates/vim/src/normal/search.rs | 13 +- 3 files changed, 129 insertions(+), 91 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 4d300230e1..3cc97468c3 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -298,12 +298,22 @@ impl AssistantPanel { } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) { - return; - } + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false + } + }); + } + if propagate_action { + cx.propagate_action(); } - cx.propagate_action(); } fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index fb4d5da764..c8d1c58b6f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -4,12 +4,14 @@ use crate::{ }; use collections::HashMap; use editor::Editor; +use futures::channel::oneshot; use gpui::{ actions, elements::*, impl_actions, platform::{CursorStyle, MouseButton}, Action, AnyViewHandle, AppContext, Entity, Subscription, Task, View, ViewContext, ViewHandle, + WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -50,12 +52,11 @@ pub fn init(cx: &mut AppContext) { fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContext) { cx.add_action(move |pane: &mut Pane, _: &A, cx: &mut ViewContext| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(false, false, cx)) { - search_bar.update(cx, |search_bar, cx| { + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { search_bar.toggle_search_option(option, cx); - }); - return; - } + } + }); } cx.propagate_action(); }); @@ -209,7 +210,7 @@ impl ToolbarItemView for BufferSearchBar { )); self.active_searchable_item = Some(searchable_item_handle); - self.update_matches(false, cx); + let _ = self.update_matches(cx); if !self.dismissed { return ToolbarItemLocation::Secondary; } @@ -279,54 +280,75 @@ impl BufferSearchBar { cx.notify(); } - pub fn show(&mut self, focus: bool, suggest_query: bool, cx: &mut ViewContext) -> bool { - self.show_with_options(focus, suggest_query, self.default_options, cx) - } - - pub fn show_with_options( - &mut self, - focus: bool, - suggest_query: bool, - search_options: SearchOptions, - cx: &mut ViewContext, - ) -> bool { - self.search_options = search_options; + pub fn show(&mut self, cx: &mut ViewContext) -> bool { let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { SearchableItemHandle::boxed_clone(searchable_item.as_ref()) } else { return false; }; - if suggest_query { - let text = searchable_item.query_suggestion(cx); - if !text.is_empty() { - self.set_query(&text, cx); - } - } - - if focus { - let query_editor = self.query_editor.clone(); - query_editor.update(cx, |query_editor, cx| { - query_editor.select_all(&editor::SelectAll, cx); - }); - cx.focus_self(); - } - self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); true } - fn set_query(&mut self, query: &str, cx: &mut ViewContext) { + pub fn search_suggested(&mut self, cx: &mut ViewContext) { + let search = self + .query_suggestion(cx) + .map(|suggestion| self.search(&suggestion, self.default_options, cx)); + + if let Some(search) = search { + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| { + if let Some(match_ix) = this.active_match_index { + if let Some(active_searchable_item) = this.active_searchable_item.as_ref() { + if let Some(matches) = this + .seachable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx); + } + } + } + }) + }) + .detach_and_log_err(cx); + } + } + + pub fn select_query(&mut self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { - query_editor.buffer().update(cx, |query_buffer, cx| { - let len = query_buffer.len(cx); - query_buffer.edit([(0..len, query)], None, cx); - }); + query_editor.select_all(&Default::default(), cx); }); } + pub fn query_suggestion(&self, cx: &mut ViewContext) -> Option { + Some(self.active_searchable_item.as_ref()?.query_suggestion(cx)) + } + + fn search( + &mut self, + query: &str, + options: SearchOptions, + cx: &mut ViewContext, + ) -> oneshot::Receiver<()> { + if query != self.query_editor.read(cx).text(cx) || self.search_options != options { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.buffer().update(cx, |query_buffer, cx| { + let len = query_buffer.len(cx); + query_buffer.edit([(0..len, query)], None, cx); + }); + }); + self.search_options = options; + self.query_contains_error = false; + self.clear_matches(cx); + cx.notify(); + } + self.update_matches(cx) + } + fn render_search_option( &self, option_supported: bool, @@ -448,12 +470,23 @@ impl BufferSearchBar { } fn deploy(pane: &mut Pane, action: &Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - if search_bar.update(cx, |search_bar, cx| search_bar.show(action.focus, true, cx)) { - return; - } + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false; + } + }); + } + + if propagate_action { + cx.propagate_action(); } - cx.propagate_action(); } fn handle_editor_cancel(pane: &mut Pane, _: &editor::Cancel, cx: &mut ViewContext) { @@ -475,8 +508,7 @@ impl BufferSearchBar { fn toggle_search_option(&mut self, search_option: SearchOptions, cx: &mut ViewContext) { self.search_options.toggle(search_option); self.default_options = self.search_options; - - self.update_matches(false, cx); + let _ = self.update_matches(cx); cx.notify(); } @@ -488,17 +520,6 @@ impl BufferSearchBar { self.select_match(Direction::Prev, cx); } - pub fn select_word_under_cursor( - &mut self, - direction: Direction, - options: SearchOptions, - cx: &mut ViewContext, - ) { - self.active_match_index = None; - self.pending_match_direction = Some(direction); - self.show_with_options(false, true, options, cx); - } - pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { @@ -541,17 +562,26 @@ impl BufferSearchBar { event: &editor::Event, cx: &mut ViewContext, ) { - if let editor::Event::BufferEdited { .. } = event { + if let editor::Event::Edited { .. } = event { + let query = self.query_editor.read(cx).text(cx); + let search = self.search(&query, self.search_options, cx); self.query_contains_error = false; self.clear_matches(cx); - self.update_matches(true, cx); - cx.notify(); + let search = self.update_matches(cx); + cx.spawn(|this, mut cx| async move { + search.await?; + this.update(&mut cx, |this, cx| this.select_match(Direction::Next, cx))?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } fn on_active_searchable_item_event(&mut self, event: SearchEvent, cx: &mut ViewContext) { match event { - SearchEvent::MatchesInvalidated => self.update_matches(false, cx), + SearchEvent::MatchesInvalidated => { + let _ = self.update_matches(cx); + } SearchEvent::ActiveMatchChanged => self.update_match_index(cx), } } @@ -574,7 +604,8 @@ impl BufferSearchBar { .extend(active_item_matches); } - fn update_matches(&mut self, select_closest_match: bool, cx: &mut ViewContext) { + fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { + let (done_tx, done_rx) = oneshot::channel(); let query = self.query_editor.read(cx).text(cx); self.pending_search.take(); if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { @@ -582,6 +613,7 @@ impl BufferSearchBar { self.active_match_index.take(); self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); + let _ = done_tx.send(()); } else { let query = if self.search_options.contains(SearchOptions::REGEX) { match SearchQuery::regex( @@ -595,7 +627,7 @@ impl BufferSearchBar { Err(_) => { self.query_contains_error = true; cx.notify(); - return; + return done_rx; } } } else { @@ -627,20 +659,7 @@ impl BufferSearchBar { .get(&active_searchable_item.downgrade()) .unwrap(); active_searchable_item.update_matches(matches, cx); - if select_closest_match { - if let Some(mut match_ix) = this.active_match_index { - if let Some(direction) = this.pending_match_direction.take() - { - match_ix += match direction { - Direction::Next => 1, - Direction::Prev => matches.len() - 1, - }; - match_ix = match_ix % matches.len(); - } - active_searchable_item - .activate_match(match_ix, matches, cx); - } - } + let _ = done_tx.send(()); } cx.notify(); } @@ -649,6 +668,7 @@ impl BufferSearchBar { })); } } + done_rx } fn update_match_index(&mut self, cx: &mut ViewContext) { @@ -699,7 +719,7 @@ mod tests { let search_bar = cx.add_view(window_id, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(false, true, cx); + search_bar.show(cx); search_bar }); @@ -712,10 +732,11 @@ mod tests { // Search for a string that appears with different casing. // By default, search is case-insensitive. - search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("us", cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("us", search_bar.default_options, cx) + }) + .await; editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -750,7 +771,7 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("or", cx); + search_bar.search("or", search_bar.default_options, cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -993,6 +1014,7 @@ mod tests { }); } + /* #[gpui::test] async fn test_search_with_options(cx: &mut TestAppContext) { let (editor, search_bar) = init_test(cx); @@ -1000,7 +1022,7 @@ mod tests { // show with options should make current search case sensitive search_bar.update(cx, |search_bar, cx| { search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); - search_bar.set_query("us", cx); + search_bar.search("us", cx); }); editor.next_notification(cx).await; editor.update(cx, |editor, cx| { @@ -1036,7 +1058,7 @@ mod tests { // toggling a search option (even in show_with_options mode) should update the defaults search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("regex", cx); + search_bar.search("regex", cx); search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); @@ -1087,4 +1109,5 @@ mod tests { ); }); } + */ } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index bb57045add..f23de53c37 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -53,7 +53,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext() { search_bar.update(cx, |search_bar, cx| { - let mut options = SearchOptions::CASE_SENSITIVE; - options.set(SearchOptions::WHOLE_WORD, whole_word); - search_bar.select_word_under_cursor(direction, options, cx); + // let mut options = SearchOptions::CASE_SENSITIVE; + // options.set(SearchOptions::WHOLE_WORD, whole_word); + // search_bar.show(false, false, cx); + // let word = search_bar.query_suggestion(); + // search_bar.show() + // search_bar.search(word, options) + + // search_bar.select_word_under_cursor(direction, options, cx); }); } }); From b4b0f622de531a58a6fa6b1f8c80743105e6ec32 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 7 Jul 2023 11:33:15 -0600 Subject: [PATCH 010/124] Rebuild vim search experience on refactored code --- assets/keymaps/vim.json | 2 +- crates/ai/src/assistant.rs | 4 +- crates/editor/src/items.rs | 22 ++-- crates/search/src/buffer_search.rs | 99 +++++++++------- crates/search/src/project_search.rs | 2 +- crates/vim/src/normal/search.rs | 172 ++++++++++++++++++++-------- crates/vim/src/state.rs | 18 +++ crates/workspace/src/searchable.rs | 23 ++-- 8 files changed, 226 insertions(+), 116 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 4a215dcef3..40ebe13558 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -357,7 +357,7 @@ { "context": "BufferSearchBar", "bindings": { - "enter": "buffer_search::FocusEditor", + "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" } } diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 3cc97468c3..4ca771ebcb 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -330,13 +330,13 @@ impl AssistantPanel { fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, None, cx)); } } fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, None, cx)); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 86f26bcf94..6b2cdacaa2 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -937,7 +937,9 @@ impl SearchableItem for Editor { ) { self.unfold_ranges([matches[index].clone()], false, true, cx); let range = self.range_for_match(&matches[index]); - self.change_selections(Some(Autoscroll::fit()), cx, |s| s.select_ranges([range])); + self.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }) } fn match_index_for_direction( @@ -945,11 +947,12 @@ impl SearchableItem for Editor { matches: &Vec>, mut current_index: usize, direction: Direction, + count: Option, cx: &mut ViewContext, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); let cursor = self.selections.newest_anchor().head(); - if matches[current_index].start.cmp(&cursor, &buffer).is_gt() { + if count.is_none() && matches[current_index].start.cmp(&cursor, &buffer).is_gt() { if direction == Direction::Prev { if current_index == 0 { current_index = matches.len() - 1; @@ -957,22 +960,19 @@ impl SearchableItem for Editor { current_index -= 1; } } - } else if matches[current_index].end.cmp(&cursor, &buffer).is_lt() { + } else if count.is_none() && matches[current_index].end.cmp(&cursor, &buffer).is_lt() { if direction == Direction::Next { current_index = 0; } } else if direction == Direction::Prev { - if current_index == 0 { - current_index = matches.len() - 1; + let count = count.unwrap_or(1) % matches.len(); + if current_index >= count { + current_index = current_index - count; } else { - current_index -= 1; + current_index = matches.len() - (count - current_index); } } else if direction == Direction::Next { - if current_index == matches.len() - 1 { - current_index = 0 - } else { - current_index += 1; - } + current_index = (current_index + count.unwrap_or(1)) % matches.len() }; current_index } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index c8d1c58b6f..2bd765f8bb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -66,7 +66,6 @@ pub struct BufferSearchBar { pub query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, - pending_match_direction: Option, active_searchable_item_subscription: Option, seachable_items_with_matches: HashMap, Vec>>, @@ -254,7 +253,6 @@ impl BufferSearchBar { default_options: SearchOptions::NONE, search_options: SearchOptions::NONE, pending_search: None, - pending_match_direction: None, query_contains_error: false, dismissed: true, } @@ -281,12 +279,9 @@ impl BufferSearchBar { } pub fn show(&mut self, cx: &mut ViewContext) -> bool { - let searchable_item = if let Some(searchable_item) = &self.active_searchable_item { - SearchableItemHandle::boxed_clone(searchable_item.as_ref()) - } else { + if self.active_searchable_item.is_none() { return false; - }; - + } self.dismissed = false; cx.notify(); cx.emit(Event::UpdateLocation); @@ -296,44 +291,53 @@ impl BufferSearchBar { pub fn search_suggested(&mut self, cx: &mut ViewContext) { let search = self .query_suggestion(cx) - .map(|suggestion| self.search(&suggestion, self.default_options, cx)); + .map(|suggestion| self.search(&suggestion, Some(self.default_options), cx)); if let Some(search) = search { cx.spawn(|this, mut cx| async move { search.await?; - this.update(&mut cx, |this, cx| { - if let Some(match_ix) = this.active_match_index { - if let Some(active_searchable_item) = this.active_searchable_item.as_ref() { - if let Some(matches) = this - .seachable_items_with_matches - .get(&active_searchable_item.downgrade()) - { - active_searchable_item.activate_match(match_ix, matches, cx); - } - } - } - }) + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) }) .detach_and_log_err(cx); } } + pub fn activate_current_match(&mut self, cx: &mut ViewContext) { + if let Some(match_ix) = self.active_match_index { + if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { + if let Some(matches) = self + .seachable_items_with_matches + .get(&active_searchable_item.downgrade()) + { + active_searchable_item.activate_match(match_ix, matches, cx) + } + } + } + } + pub fn select_query(&mut self, cx: &mut ViewContext) { self.query_editor.update(cx, |query_editor, cx| { query_editor.select_all(&Default::default(), cx); }); } - pub fn query_suggestion(&self, cx: &mut ViewContext) -> Option { - Some(self.active_searchable_item.as_ref()?.query_suggestion(cx)) + pub fn query(&self, cx: &WindowContext) -> String { + self.query_editor.read(cx).text(cx) } - fn search( + pub fn query_suggestion(&mut self, cx: &mut ViewContext) -> Option { + self.active_searchable_item + .as_ref() + .map(|searchable_item| searchable_item.query_suggestion(cx)) + } + + pub fn search( &mut self, query: &str, - options: SearchOptions, + options: Option, cx: &mut ViewContext, ) -> oneshot::Receiver<()> { + let options = options.unwrap_or(self.default_options); if query != self.query_editor.read(cx).text(cx) || self.search_options != options { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { @@ -499,7 +503,7 @@ impl BufferSearchBar { cx.propagate_action(); } - fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { + pub fn focus_editor(&mut self, _: &FocusEditor, cx: &mut ViewContext) { if let Some(active_editor) = self.active_searchable_item.as_ref() { cx.focus(active_editor.as_any()); } @@ -512,23 +516,37 @@ impl BufferSearchBar { cx.notify(); } + pub fn set_search_options( + &mut self, + search_options: SearchOptions, + cx: &mut ViewContext, + ) { + self.search_options = search_options; + cx.notify(); + } + fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { - self.select_match(Direction::Next, cx); + self.select_match(Direction::Next, None, cx); } fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { - self.select_match(Direction::Prev, cx); + self.select_match(Direction::Prev, None, cx); } - pub fn select_match(&mut self, direction: Direction, cx: &mut ViewContext) { + pub fn select_match( + &mut self, + direction: Direction, + count: Option, + cx: &mut ViewContext, + ) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self .seachable_items_with_matches .get(&searchable_item.downgrade()) { - let new_match_index = - searchable_item.match_index_for_direction(matches, index, direction, cx); + let new_match_index = searchable_item + .match_index_for_direction(matches, index, direction, count, cx); searchable_item.update_matches(matches, cx); searchable_item.activate_match(new_match_index, matches, cx); } @@ -563,15 +581,12 @@ impl BufferSearchBar { cx: &mut ViewContext, ) { if let editor::Event::Edited { .. } = event { - let query = self.query_editor.read(cx).text(cx); - let search = self.search(&query, self.search_options, cx); self.query_contains_error = false; self.clear_matches(cx); let search = self.update_matches(cx); cx.spawn(|this, mut cx| async move { search.await?; - this.update(&mut cx, |this, cx| this.select_match(Direction::Next, cx))?; - anyhow::Ok(()) + this.update(&mut cx, |this, cx| this.activate_current_match(cx)) }) .detach_and_log_err(cx); } @@ -611,7 +626,6 @@ impl BufferSearchBar { if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { self.active_match_index.take(); - self.pending_match_direction.take(); active_searchable_item.clear_matches(cx); let _ = done_tx.send(()); } else { @@ -733,10 +747,9 @@ mod tests { // Search for a string that appears with different casing. // By default, search is case-insensitive. search_bar - .update(cx, |search_bar, cx| { - search_bar.search("us", search_bar.default_options, cx) - }) - .await; + .update(cx, |search_bar, cx| search_bar.search("us", None, cx)) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -770,10 +783,10 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. - search_bar.update(cx, |search_bar, cx| { - search_bar.search("or", search_bar.default_options, cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| search_bar.search("or", None, cx)) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 4e485eaaab..76350f1812 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -627,7 +627,7 @@ impl ProjectSearchView { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, cx) + editor.match_index_for_direction(&match_ranges, index, direction, None, cx) }); let range_to_select = match_ranges[new_index].clone(); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index f23de53c37..70e397bcb0 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -1,9 +1,9 @@ -use gpui::{impl_actions, AppContext, ViewContext}; -use search::{BufferSearchBar, SearchOptions}; +use gpui::{actions, impl_actions, AppContext, ViewContext}; +use search::{buffer_search, BufferSearchBar, SearchOptions}; use serde_derive::Deserialize; -use workspace::{searchable::Direction, Workspace}; +use workspace::{searchable::Direction, Pane, Workspace}; -use crate::Vim; +use crate::{state::SearchState, Vim}; #[derive(Clone, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -26,11 +26,14 @@ pub(crate) struct Search { } impl_actions!(vim, [MoveToNext, MoveToPrev, Search]); +actions!(vim, [SearchSubmit]); pub(crate) fn init(cx: &mut AppContext) { cx.add_action(move_to_next); cx.add_action(move_to_prev); cx.add_action(search); + cx.add_action(search_submit); + cx.add_action(search_deploy); } fn move_to_next(workspace: &mut Workspace, action: &MoveToNext, cx: &mut ViewContext) { @@ -43,19 +46,68 @@ fn move_to_prev(workspace: &mut Workspace, action: &MoveToPrev, cx: &mut ViewCon fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext) { let pane = workspace.active_pane().clone(); - pane.update(cx, |pane, cx| { - if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - let options = SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX; - let direction = if action.backwards { - Direction::Prev - } else { - Direction::Next - }; - search_bar.select_match(direction, cx); - // search_bar.show_with_options(true, false, options, cx); - }) - } + let direction = if action.backwards { + Direction::Prev + } else { + Direction::Next + }; + Vim::update(cx, |vim, cx| { + let count = vim.pop_number_operator(cx).unwrap_or(1); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if !search_bar.show(cx) { + return; + } + let query = search_bar.query(cx); + + search_bar.select_query(cx); + cx.focus_self(); + + if query.is_empty() { + search_bar.set_search_options( + SearchOptions::CASE_SENSITIVE | SearchOptions::REGEX, + cx, + ); + } + vim.state.search = SearchState { + direction, + count, + initial_query: query, + }; + }); + } + }) + }) +} + +fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { + Vim::update(cx, |vim, _| vim.state.search = Default::default()); + cx.propagate_action(); +} + +fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewContext) { + Vim::update(cx, |vim, cx| { + let pane = workspace.active_pane().clone(); + pane.update(cx, |pane, cx| { + if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + let mut state = &mut vim.state.search; + let mut count = state.count; + + // in the case that the query has changed, the search bar + // will have selected the next match already. + if (search_bar.query(cx) != state.initial_query) + && state.direction == Direction::Next + { + count = count.saturating_sub(1); + } + search_bar.select_match(state.direction, Some(count), cx); + state.count = 1; + search_bar.focus_editor(&Default::default(), cx); + }); + } + }); }) } @@ -67,18 +119,32 @@ pub fn move_to_internal( ) { Vim::update(cx, |vim, cx| { let pane = workspace.active_pane().clone(); + let count = vim.pop_number_operator(cx).unwrap_or(1); pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { - search_bar.update(cx, |search_bar, cx| { - // let mut options = SearchOptions::CASE_SENSITIVE; - // options.set(SearchOptions::WHOLE_WORD, whole_word); - // search_bar.show(false, false, cx); - // let word = search_bar.query_suggestion(); - // search_bar.show() - // search_bar.search(word, options) - - // search_bar.select_word_under_cursor(direction, options, cx); + let search = search_bar.update(cx, |search_bar, cx| { + let mut options = SearchOptions::CASE_SENSITIVE; + options.set(SearchOptions::WHOLE_WORD, whole_word); + if search_bar.show(cx) { + search_bar + .query_suggestion(cx) + .map(|query| search_bar.search(&query, Some(options), cx)) + } else { + None + } }); + + if let Some(search) = search { + let search_bar = search_bar.downgrade(); + cx.spawn(|_, mut cx| async move { + search.await?; + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(direction, Some(count), cx) + })?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } } }); vim.clear_operator(cx); @@ -100,15 +166,6 @@ mod test { deterministic: Arc, ) { let mut cx = VimTestContext::new(cx, true).await; - let search_bar = cx.workspace(|workspace, cx| { - workspace - .active_pane() - .read(cx) - .toolbar() - .read(cx) - .item_of_type::() - .expect("Buffer search bar should be deployed") - }); cx.set_state("ˇhi\nhigh\nhi\n", Mode::Normal); cx.simulate_keystrokes(["*"]); @@ -127,6 +184,10 @@ mod test { deterministic.run_until_parked(); cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + cx.simulate_keystrokes(["2", "*"]); + deterministic.run_until_parked(); + cx.assert_state("ˇhi\nhigh\nhi\n", Mode::Normal); + cx.simulate_keystrokes(["g", "*"]); deterministic.run_until_parked(); cx.assert_state("hi\nˇhigh\nhi\n", Mode::Normal); @@ -140,7 +201,10 @@ mod test { } #[gpui::test] - async fn test_search(cx: &mut gpui::TestAppContext) { + async fn test_search( + cx: &mut gpui::TestAppContext, + deterministic: Arc, + ) { let mut cx = VimTestContext::new(cx, true).await; cx.set_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); @@ -160,8 +224,7 @@ mod test { assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); }); - // wait for the query editor change event to fire. - search_bar.next_notification(&cx).await; + deterministic.run_until_parked(); cx.update_editor(|editor, cx| { let highlights = editor.all_background_highlights(cx); @@ -173,30 +236,49 @@ mod test { }); cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); // n to go to next/N to go to previous - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["n"]); cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["shift-n"]); + cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); // ? to go to previous - cx.assert_state("aa\nbb\nˇcc\ncc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["?", "enter"]); + deterministic.run_until_parked(); cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); cx.simulate_keystrokes(["?", "enter"]); + deterministic.run_until_parked(); + cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); // / to go to next - cx.assert_state("aa\nbb\ncc\nˇcc\ncc\n", Mode::Normal); cx.simulate_keystrokes(["/", "enter"]); + deterministic.run_until_parked(); cx.assert_state("aa\nbb\ncc\ncc\nˇcc\n", Mode::Normal); // ?{search} to search backwards cx.simulate_keystrokes(["?", "b", "enter"]); - - // wait for the query editor change event to fire. - search_bar.next_notification(&cx).await; - + deterministic.run_until_parked(); cx.assert_state("aa\nbˇb\ncc\ncc\ncc\n", Mode::Normal); + + // works with counts + cx.simulate_keystrokes(["4", "/", "c"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\ncc\ncˇc\ncc\n", Mode::Normal); + + // check that searching resumes from cursor, not previous match + cx.set_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); + cx.simulate_keystrokes(["/", "d"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nbb\nˇdd\ncc\nbb\n", Mode::Normal); + cx.update_editor(|editor, cx| editor.move_to_beginning(&Default::default(), cx)); + cx.assert_state("ˇaa\nbb\ndd\ncc\nbb\n", Mode::Normal); + cx.simulate_keystrokes(["/", "b"]); + deterministic.run_until_parked(); + cx.simulate_keystrokes(["enter"]); + cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index e1a06fce59..6434b710b2 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -1,6 +1,7 @@ use gpui::keymap_matcher::KeymapContext; use language::CursorShape; use serde::{Deserialize, Serialize}; +use workspace::searchable::Direction; #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum Mode { @@ -38,6 +39,23 @@ pub enum Operator { pub struct VimState { pub mode: Mode, pub operator_stack: Vec, + pub search: SearchState, +} + +pub struct SearchState { + pub direction: Direction, + pub count: usize, + pub initial_query: String, +} + +impl Default for SearchState { + fn default() -> Self { + Self { + direction: Direction::Next, + count: 1, + initial_query: "".to_string(), + } + } } impl VimState { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 7e3f7227b0..bdbed072b0 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -50,26 +50,21 @@ pub trait SearchableItem: Item { fn match_index_for_direction( &mut self, matches: &Vec, - mut current_index: usize, + current_index: usize, direction: Direction, + count: Option, _: &mut ViewContext, ) -> usize { match direction { Direction::Prev => { - if current_index == 0 { - matches.len() - 1 + let count = count.unwrap_or(1) % matches.len(); + if current_index >= count { + current_index - count } else { - current_index - 1 - } - } - Direction::Next => { - current_index += 1; - if current_index == matches.len() { - 0 - } else { - current_index + matches.len() - (count - current_index) } } + Direction::Next => (current_index + count.unwrap_or(1)) % matches.len(), } } fn find_matches( @@ -107,6 +102,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &Vec>, current_index: usize, direction: Direction, + count: Option, cx: &mut WindowContext, ) -> usize; fn find_matches( @@ -170,11 +166,12 @@ impl SearchableItemHandle for ViewHandle { matches: &Vec>, current_index: usize, direction: Direction, + count: Option, cx: &mut WindowContext, ) -> usize { let matches = downcast_matches(matches); self.update(cx, |this, cx| { - this.match_index_for_direction(&matches, current_index, direction, cx) + this.match_index_for_direction(&matches, current_index, direction, count, cx) }) } fn find_matches( From 9a1a9813cbc5936f0da02088b3b70d5f5c445083 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 13 Jul 2023 11:56:53 -0400 Subject: [PATCH 011/124] WIP --- Cargo.lock | 10 ++++ crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/bash/brackets.scm | 3 ++ crates/zed/src/languages/bash/config.toml | 7 +++ crates/zed/src/languages/bash/highlights.scm | 56 ++++++++++++++++++++ 6 files changed, 78 insertions(+) create mode 100644 crates/zed/src/languages/bash/brackets.scm create mode 100644 crates/zed/src/languages/bash/config.toml create mode 100644 crates/zed/src/languages/bash/highlights.scm diff --git a/Cargo.lock b/Cargo.lock index 0ac6a2ee89..ec674aee2c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7953,6 +7953,15 @@ dependencies = [ "regex", ] +[[package]] +name = "tree-sitter-bash" +version = "0.19.0" +source = "git+https://github.com/tree-sitter/tree-sitter-bash?rev=1b0321ee85701d5036c334a6f04761cdc672e64c#1b0321ee85701d5036c334a6f04761cdc672e64c" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-c" version = "0.20.2" @@ -9539,6 +9548,7 @@ dependencies = [ "tiny_http", "toml", "tree-sitter", + "tree-sitter-bash", "tree-sitter-c", "tree-sitter-cpp", "tree-sitter-css", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 597e40161f..fb7cd6adda 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -104,6 +104,7 @@ thiserror.workspace = true tiny_http = "0.8" toml.workspace = true tree-sitter.workspace = true +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 820f564151..225bc4d0bc 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -38,6 +38,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { languages.register(name, load_config(name), grammar, adapters, load_queries) }; + language("bash", tree_sitter_bash::language(), vec![]); language( "c", tree_sitter_c::language(), diff --git a/crates/zed/src/languages/bash/brackets.scm b/crates/zed/src/languages/bash/brackets.scm new file mode 100644 index 0000000000..191fd9c084 --- /dev/null +++ b/crates/zed/src/languages/bash/brackets.scm @@ -0,0 +1,3 @@ +("(" @open ")" @close) +("[" @open "]" @close) +("{" @open "}" @close) diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml new file mode 100644 index 0000000000..f3dcee9b73 --- /dev/null +++ b/crates/zed/src/languages/bash/config.toml @@ -0,0 +1,7 @@ +name = "Shell Script" +path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"] +brackets = [ + { start = "[", end = "]", close = true, newline = false }, + { start = "(", end = ")", close = true, newline = false }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, +] diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm new file mode 100644 index 0000000000..f33a7c2d3a --- /dev/null +++ b/crates/zed/src/languages/bash/highlights.scm @@ -0,0 +1,56 @@ +[ + (string) + (raw_string) + (heredoc_body) + (heredoc_start) +] @string + +(command_name) @function + +(variable_name) @property + +[ + "case" + "do" + "done" + "elif" + "else" + "esac" + "export" + "fi" + "for" + "function" + "if" + "in" + "select" + "then" + "unset" + "until" + "while" +] @keyword + +(comment) @comment + +(function_definition name: (word) @function) + +(file_descriptor) @number + +[ + (command_substitution) + (process_substitution) + (expansion) +]@embedded + +[ + "$" + "&&" + ">" + ">>" + "<" + "|" +] @operator + +( + (command (_) @constant) + (#match? @constant "^-") +) From ef7aa66959ca44fe99293d8352360f94d569e33c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 13 Jul 2023 12:09:43 -0400 Subject: [PATCH 012/124] Add first line pattern --- crates/zed/src/languages/bash/config.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml index f3dcee9b73..80b8753d80 100644 --- a/crates/zed/src/languages/bash/config.toml +++ b/crates/zed/src/languages/bash/config.toml @@ -1,5 +1,6 @@ name = "Shell Script" path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"] +first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" brackets = [ { start = "[", end = "]", close = true, newline = false }, { start = "(", end = ")", close = true, newline = false }, From c9bf4074311dcb2cf23da842bb19c354efca026d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 17 Jul 2023 12:02:10 -0600 Subject: [PATCH 013/124] Avoid optional on select_match --- crates/ai/src/assistant.rs | 4 +- crates/editor/src/items.rs | 70 ++++++++++--------- crates/search/src/buffer_search.rs | 100 +++++++++------------------- crates/search/src/project_search.rs | 3 +- crates/vim/src/normal/search.rs | 7 +- crates/workspace/src/searchable.rs | 10 +-- 6 files changed, 80 insertions(+), 114 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 5d587401e5..8a4c04d338 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -330,13 +330,13 @@ impl AssistantPanel { fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, None, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); } } fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { - search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, None, cx)); + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c7a93e754a..0ce41a97c9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -954,15 +954,19 @@ impl SearchableItem for Editor { fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { self.unfold_ranges(matches.clone(), false, false, cx); - self.change_selections(None, cx, |s| s.select_ranges(matches)); + let mut ranges = Vec::new(); + for m in &matches { + ranges.push(self.range_for_match(&m)) + } + self.change_selections(None, cx, |s| s.select_ranges(ranges)); } fn match_index_for_direction( &mut self, matches: &Vec>, - mut current_index: usize, + current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut ViewContext, ) -> usize { let buffer = self.buffer().read(cx).snapshot(cx); @@ -971,39 +975,39 @@ impl SearchableItem for Editor { } else { matches[current_index].start }; - if count.is_none() - && matches[current_index] - .start - .cmp(¤t_index_position, &buffer) - .is_gt() - { - if direction == Direction::Prev { - if current_index == 0 { - current_index = matches.len() - 1; + + let mut count = count % matches.len(); + if count == 0 { + return current_index; + } + match direction { + Direction::Next => { + if matches[current_index] + .start + .cmp(¤t_index_position, &buffer) + .is_gt() + { + count = count - 1 + } + + (current_index + count) % matches.len() + } + Direction::Prev => { + if matches[current_index] + .end + .cmp(¤t_index_position, &buffer) + .is_lt() + { + count = count - 1; + } + + if current_index >= count { + current_index - count } else { - current_index -= 1; + matches.len() - (count - current_index) } } - } else if count.is_none() - && matches[current_index] - .end - .cmp(¤t_index_position, &buffer) - .is_lt() - { - if direction == Direction::Next { - current_index = 0; - } - } else if direction == Direction::Prev { - let count = count.unwrap_or(1) % matches.len(); - if current_index >= count { - current_index = current_index - count; - } else { - current_index = matches.len() - (count - current_index); - } - } else if direction == Direction::Next { - current_index = (current_index + count.unwrap_or(1)) % matches.len() - }; - current_index + } } fn find_matches( diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7d50794108..7fade13a50 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -560,11 +560,11 @@ impl BufferSearchBar { } fn select_next_match(&mut self, _: &SelectNextMatch, cx: &mut ViewContext) { - self.select_match(Direction::Next, None, cx); + self.select_match(Direction::Next, 1, cx); } fn select_prev_match(&mut self, _: &SelectPrevMatch, cx: &mut ViewContext) { - self.select_match(Direction::Prev, None, cx); + self.select_match(Direction::Prev, 1, cx); } fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { @@ -581,12 +581,7 @@ impl BufferSearchBar { } } - pub fn select_match( - &mut self, - direction: Direction, - count: Option, - cx: &mut ViewContext, - ) { + pub fn select_match(&mut self, direction: Direction, count: usize, cx: &mut ViewContext) { if let Some(index) = self.active_match_index { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self @@ -1086,15 +1081,17 @@ mod tests { } #[gpui::test] - async fn test_search_with_options(cx: &mut TestAppContext) { + async fn test_search_option_handling(cx: &mut TestAppContext) { let (editor, search_bar) = init_test(cx); // show with options should make current search case sensitive - search_bar.update(cx, |search_bar, cx| { - search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); - search_bar.search("us", cx); - }); - editor.next_notification(cx).await; + search_bar + .update(cx, |search_bar, cx| { + search_bar.show(cx); + search_bar.search("us", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); editor.update(cx, |editor, cx| { assert_eq!( editor.all_background_highlights(cx), @@ -1105,31 +1102,20 @@ mod tests { ); }); - // show should return to the default options (case insensitive) + // search_suggested should restore default options search_bar.update(cx, |search_bar, cx| { - search_bar.show(true, true, cx); - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { - assert_eq!( - editor.all_background_highlights(cx), - &[ - ( - DisplayPoint::new(2, 17)..DisplayPoint::new(2, 19), - Color::red(), - ), - ( - DisplayPoint::new(2, 43)..DisplayPoint::new(2, 45), - Color::red(), - ) - ] - ); + search_bar.search_suggested(cx); + assert_eq!(search_bar.search_options, SearchOptions::NONE) }); - // toggling a search option (even in show_with_options mode) should update the defaults + // toggling a search option should update the defaults + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("regex", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); search_bar.update(cx, |search_bar, cx| { - search_bar.search("regex", cx); - search_bar.show_with_options(false, false, SearchOptions::CASE_SENSITIVE, cx); search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) }); editor.next_notification(cx).await; @@ -1145,38 +1131,11 @@ mod tests { // defaults should still include whole word search_bar.update(cx, |search_bar, cx| { - search_bar.show(true, true, cx); - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { + search_bar.search_suggested(cx); assert_eq!( - editor.all_background_highlights(cx), - &[( - DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), - ),] - ); - }); - - // removing whole word changes the search again - search_bar.update(cx, |search_bar, cx| { - search_bar.toggle_search_option(SearchOptions::WHOLE_WORD, cx) - }); - editor.next_notification(cx).await; - editor.update(cx, |editor, cx| { - assert_eq!( - editor.all_background_highlights(cx), - &[ - ( - DisplayPoint::new(0, 35)..DisplayPoint::new(0, 40), - Color::red(), - ), - ( - DisplayPoint::new(0, 44)..DisplayPoint::new(0, 49), - Color::red() - ) - ] - ); + search_bar.search_options, + SearchOptions::CASE_SENSITIVE | SearchOptions::WHOLE_WORD + ) }); } @@ -1207,15 +1166,18 @@ mod tests { let search_bar = cx.add_view(window_id, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); - search_bar.show(false, true, cx); + search_bar.show(cx); search_bar }); + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); search_bar.update(cx, |search_bar, cx| { - search_bar.set_query("a", cx); + search_bar.activate_current_match(cx); }); - editor.next_notification(cx).await; let initial_selections = editor.update(cx, |editor, cx| { let initial_selections = editor.selections.display_ranges(cx); assert_eq!( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 5bdcf72a97..abebb9a48f 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -627,7 +627,7 @@ impl ProjectSearchView { if let Some(index) = self.active_match_index { let match_ranges = self.model.read(cx).match_ranges.clone(); let new_index = self.results_editor.update(cx, |editor, cx| { - editor.match_index_for_direction(&match_ranges, index, direction, None, cx) + editor.match_index_for_direction(&match_ranges, index, direction, 1, cx) }); let range_to_select = match_ranges[new_index].clone(); @@ -668,7 +668,6 @@ impl ProjectSearchView { self.active_match_index = None; } else { self.active_match_index = Some(0); - self.select_match(Direction::Next, cx); self.update_match_index(cx); let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); let is_new_search = self.search_id != prev_search_id; diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 70e397bcb0..cae64a40a6 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -81,6 +81,7 @@ fn search(workspace: &mut Workspace, action: &Search, cx: &mut ViewContext find. fn search_deploy(_: &mut Pane, _: &buffer_search::Deploy, cx: &mut ViewContext) { Vim::update(cx, |vim, _| vim.state.search = Default::default()); cx.propagate_action(); @@ -100,9 +101,9 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte if (search_bar.query(cx) != state.initial_query) && state.direction == Direction::Next { - count = count.saturating_sub(1); + count = count.saturating_sub(1) } - search_bar.select_match(state.direction, Some(count), cx); + search_bar.select_match(state.direction, count, cx); state.count = 1; search_bar.focus_editor(&Default::default(), cx); }); @@ -139,7 +140,7 @@ pub fn move_to_internal( cx.spawn(|_, mut cx| async move { search.await?; search_bar.update(&mut cx, |search_bar, cx| { - search_bar.select_match(direction, Some(count), cx) + search_bar.select_match(direction, count, cx) })?; anyhow::Ok(()) }) diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 4f5e7099fa..ae95838a74 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -57,19 +57,19 @@ pub trait SearchableItem: Item { matches: &Vec, current_index: usize, direction: Direction, - count: Option, + count: usize, _: &mut ViewContext, ) -> usize { match direction { Direction::Prev => { - let count = count.unwrap_or(1) % matches.len(); + let count = count % matches.len(); if current_index >= count { current_index - count } else { matches.len() - (count - current_index) } } - Direction::Next => (current_index + count.unwrap_or(1)) % matches.len(), + Direction::Next => (current_index + count) % matches.len(), } } fn find_matches( @@ -108,7 +108,7 @@ pub trait SearchableItemHandle: ItemHandle { matches: &Vec>, current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut WindowContext, ) -> usize; fn find_matches( @@ -179,7 +179,7 @@ impl SearchableItemHandle for ViewHandle { matches: &Vec>, current_index: usize, direction: Direction, - count: Option, + count: usize, cx: &mut WindowContext, ) -> usize { let matches = downcast_matches(matches); From bf2dcd4582adb2332c1298eff7d5d34185baf9cc Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 18 Jul 2023 12:15:03 -0400 Subject: [PATCH 014/124] Update cargo.toml --- Cargo.toml | 1 + crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 7a32938c37..fa824115cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ tree-sitter = "0.20" unindent = { version = "0.1.7" } pretty_assertions = "1.3.0" +tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 478cdf2266..f749fb6e68 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -104,7 +104,7 @@ thiserror.workspace = true tiny_http = "0.8" toml.workspace = true tree-sitter.workspace = true -tree-sitter-bash = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "1b0321ee85701d5036c334a6f04761cdc672e64c" } +tree-sitter-bash.workspace = true tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true From 018eb06091d437fe293024cd6d9a75c129c59b8d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 18 Jul 2023 12:32:53 -0400 Subject: [PATCH 015/124] Add is_staff to events --- crates/client/src/telemetry.rs | 2 ++ script/start-local-collaboration | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 959f4cc783..dc5154d96f 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -40,6 +40,7 @@ lazy_static! { struct ClickhouseEventRequestBody { token: &'static str, installation_id: Option>, + is_staff: Option, app_version: Option>, os_name: &'static str, os_version: Option>, @@ -224,6 +225,7 @@ impl Telemetry { &ClickhouseEventRequestBody { token: ZED_SECRET_CLIENT_TOKEN, installation_id: state.installation_id.clone(), + is_staff: state.is_staff.clone(), app_version: state.app_version.clone(), os_name: state.os_name, os_version: state.os_version.clone(), diff --git a/script/start-local-collaboration b/script/start-local-collaboration index b702fb4e02..6c256abefe 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -45,7 +45,7 @@ position_2=${half_width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:8080 +export ZED_SERVER_URL=http://localhost:3000 export ZED_WINDOW_SIZE=${half_width},${height} cargo build From 7cb5326ba08f33e31f44d8780860f955e743eced Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 18 Jul 2023 12:43:27 -0400 Subject: [PATCH 016/124] Fix ZED_SERVER_URL port number This change accidentally slipped into https://github.com/zed-industries/zed/pull/2746 --- script/start-local-collaboration | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/start-local-collaboration b/script/start-local-collaboration index 6c256abefe..b702fb4e02 100755 --- a/script/start-local-collaboration +++ b/script/start-local-collaboration @@ -45,7 +45,7 @@ position_2=${half_width},${y} # Authenticate using the collab server's admin secret. export ZED_STATELESS=1 export ZED_ADMIN_API_TOKEN=secret -export ZED_SERVER_URL=http://localhost:3000 +export ZED_SERVER_URL=http://localhost:8080 export ZED_WINDOW_SIZE=${half_width},${height} cargo build From 9aeb970f09845061d03fe1e33c64f6311ae7f7fe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 18:50:55 +0300 Subject: [PATCH 017/124] Ignore empty hover contents, trim final hover label text --- crates/editor/src/hover_popover.rs | 116 ++++++++++++++++++++++++++++- crates/project/src/project.rs | 6 ++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 7a203d54a9..92ed9ef77d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -198,7 +198,7 @@ fn show_hover( // Construct new hover popover from hover request let hover_popover = hover_request.await.ok().flatten().and_then(|hover_result| { - if hover_result.contents.is_empty() { + if hover_result.is_empty() { return None; } @@ -420,7 +420,7 @@ fn render_blocks( RenderedInfo { theme_id, - text, + text: text.trim().to_string(), highlights, region_ranges, regions, @@ -816,6 +816,118 @@ mod tests { }); } + #[gpui::test] + async fn test_empty_hovers_filtered(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + cx.handle_request::(move |_, _, _| async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Array(vec![ + lsp::MarkedString::String("regular text for hover to show".to_string()), + lsp::MarkedString::String("".to_string()), + lsp::MarkedString::LanguageString(lsp::LanguageString { + language: "Rust".to_string(), + value: "".to_string(), + }), + ]), + range: Some(symbol_range), + })) + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, _| { + assert_eq!( + editor.hover_state.info_popover.clone().unwrap().blocks, + vec![HoverBlock { + text: "regular text for hover to show".to_string(), + kind: HoverBlockKind::Markdown, + }], + "No empty string hovers should be shown" + ); + }); + } + + #[gpui::test] + async fn test_line_ends_trimmed(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + hover_provider: Some(lsp::HoverProviderCapability::Simple(true)), + ..Default::default() + }, + cx, + ) + .await; + + // Hover with keyboard has no delay + cx.set_state(indoc! {" + fˇn test() { println!(); } + "}); + cx.update_editor(|editor, cx| hover(editor, &Hover, cx)); + let symbol_range = cx.lsp_range(indoc! {" + «fn» test() { println!(); } + "}); + + let code_str = "\nlet hovered_point: Vector2F // size = 8, align = 0x4\n"; + let markdown_string = format!("\n```rust\n{code_str}```"); + + let closure_markdown_string = markdown_string.clone(); + cx.handle_request::(move |_, _, _| { + let future_markdown_string = closure_markdown_string.clone(); + async move { + Ok(Some(lsp::Hover { + contents: lsp::HoverContents::Markup(lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: future_markdown_string, + }), + range: Some(symbol_range), + })) + } + }) + .next() + .await; + + cx.condition(|editor, _| editor.hover_state.visible()).await; + cx.editor(|editor, cx| { + let blocks = editor.hover_state.info_popover.clone().unwrap().blocks; + assert_eq!( + blocks, + vec![HoverBlock { + text: markdown_string, + kind: HoverBlockKind::Markdown, + }], + ); + + let style = editor.style(cx); + let rendered = render_blocks(0, &blocks, &Default::default(), None, &style); + assert_eq!( + rendered.text, + code_str.trim(), + "Should not have extra line breaks at end of rendered hover" + ); + }); + } + #[gpui::test] async fn test_hover_diagnostic_and_info_popovers(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b931560d25..5bb8af3f38 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -425,6 +425,12 @@ pub struct Hover { pub language: Option>, } +impl Hover { + pub fn is_empty(&self) -> bool { + self.contents.iter().all(|block| block.text.is_empty()) + } +} + #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); From 75d900704eb838ea2e07f90f012e4871049b398c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 12 Jul 2023 22:08:44 +0300 Subject: [PATCH 018/124] Refactor terminal highlights and open mechanisms Co-authored-by: Mikayla --- crates/terminal/src/terminal.rs | 14 ++++++++------ crates/terminal_view/src/terminal_element.rs | 4 ++++ crates/terminal_view/src/terminal_view.rs | 8 +++++++- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 39e77b590b..739f52db01 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -75,11 +75,11 @@ const DEBUG_LINE_HEIGHT: f32 = 5.; // Regex Copied from alacritty's ui_config.rs lazy_static! { - static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } ///Upward flowing events, for changing the title and such -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] pub enum Event { TitleChanged, BreadcrumbsChanged, @@ -88,6 +88,7 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, + Open(String), } #[derive(Clone)] @@ -806,6 +807,7 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hyperlink(); } + // We need to change this to a word boundary check InternalEvent::FindHyperlink(position, open) => { let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); @@ -848,7 +850,7 @@ impl Terminal { let url_match = min_index..=max_index; Some((url, url_match)) - } else if let Some(url_match) = regex_match_at(term, point, &URL_REGEX) { + } else if let Some(url_match) = regex_match_at(term, point, &WORD_REGEX) { let url = term.bounds_to_string(*url_match.start(), *url_match.end()); Some((url, url_match)) @@ -858,7 +860,7 @@ impl Terminal { if let Some((url, url_match)) = found_url { if *open { - cx.platform().open_url(url.as_str()); + cx.emit(Event::Open(url)) } else { self.update_hyperlink(prev_hyperlink, url, url_match); } @@ -1089,7 +1091,7 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else { + } else if e.cmd { self.hyperlink_from_position(Some(position)); } } @@ -1208,7 +1210,7 @@ impl Terminal { let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { cx.platform().open_url(link.uri()); - } else { + } else if e.cmd { self.events .push_back(InternalEvent::FindHyperlink(position, true)); } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b92059f5d6..72517b5a10 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -392,6 +392,10 @@ impl TerminalElement { let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); + // 1. Get file:linenumber syntax working ✔️ + // 2. Switch terminal to look for word boundaries, on cmd-hover + // 3. Send those query strings to the resolver thing above + // Terminal Emulator controlled behavior: region = region // Start selections diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3dd401e392..5f6fabcf1c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -158,7 +158,13 @@ impl TerminalView { .detach(); } } - _ => cx.emit(*event), + Event::Open(url) => { + // Get a workspace pointer from the new() function above + // Guess for project path or url + // Either run open buffer action OR platform open depending on whatever happens + cx.platform().open_url(url); + } + _ => cx.emit(event.clone()), }) .detach(); From f52722b6a4bf1a5db39bbffa73c858f1a1ccdc11 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 18:26:35 +0300 Subject: [PATCH 019/124] Properly handle Cmd press for terminal highlights --- crates/terminal/src/terminal.rs | 24 ++++++++++++++++---- crates/terminal_view/src/terminal_element.rs | 4 ---- crates/terminal_view/src/terminal_view.rs | 17 +++++++++++++- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 739f52db01..fcbd02096c 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -51,7 +51,7 @@ use gpui::{ fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, - platform::{MouseButton, MouseMovedEvent, TouchPhase}, + platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, AppContext, ClipboardItem, Entity, ModelContext, Task, }; @@ -494,6 +494,7 @@ impl TerminalBuilder { last_mouse_position: None, next_link_id: 0, selection_phase: SelectionPhase::Ended, + cmd_pressed: false, }; Ok(TerminalBuilder { @@ -638,6 +639,7 @@ pub struct Terminal { scroll_px: f32, next_link_id: usize, selection_phase: SelectionPhase, + cmd_pressed: bool, } impl Terminal { @@ -807,7 +809,6 @@ impl Terminal { term.scroll_to_point(*point); self.refresh_hyperlink(); } - // We need to change this to a word boundary check InternalEvent::FindHyperlink(position, open) => { let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); @@ -966,6 +967,21 @@ impl Terminal { } } + pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { + let cmd = modifiers.cmd; + let changed = self.cmd_pressed != cmd; + if changed { + self.cmd_pressed = cmd; + if cmd { + self.refresh_hyperlink(); + } else { + self.last_content.last_hovered_hyperlink.take(); + } + } + + changed + } + ///Paste text into the terminal pub fn paste(&mut self, text: &str) { let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) { @@ -1091,7 +1107,7 @@ impl Terminal { self.pty_tx.notify(bytes); } } - } else if e.cmd { + } else if self.cmd_pressed { self.hyperlink_from_position(Some(position)); } } @@ -1210,7 +1226,7 @@ impl Terminal { let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size); if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() { cx.platform().open_url(link.uri()); - } else if e.cmd { + } else if self.cmd_pressed { self.events .push_back(InternalEvent::FindHyperlink(position, true)); } diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 72517b5a10..b92059f5d6 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -392,10 +392,6 @@ impl TerminalElement { let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); - // 1. Get file:linenumber syntax working ✔️ - // 2. Switch terminal to look for word boundaries, on cmd-hover - // 3. Send those query strings to the resolver thing above - // Terminal Emulator controlled behavior: region = region // Start selections diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 5f6fabcf1c..4dbeb19033 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -11,7 +11,7 @@ use gpui::{ geometry::vector::Vector2F, impl_actions, keymap_matcher::{KeymapContext, Keystroke}, - platform::KeyDownEvent, + platform::{KeyDownEvent, ModifiersChangedEvent}, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -159,6 +159,7 @@ impl TerminalView { } } Event::Open(url) => { + // TODO kb // Get a workspace pointer from the new() function above // Guess for project path or url // Either run open buffer action OR platform open depending on whatever happens @@ -399,6 +400,20 @@ impl View for TerminalView { cx.notify(); } + fn modifiers_changed( + &mut self, + event: &ModifiersChangedEvent, + cx: &mut ViewContext, + ) -> bool { + let handled = self + .terminal() + .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); + if handled { + cx.notify(); + } + handled + } + fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext) -> bool { self.clear_bel(cx); self.pause_cursor_blinking(cx); From 23f25562b535abcb49dc938fefe1ab33bb70cb01 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 14 Jul 2023 19:34:41 +0300 Subject: [PATCH 020/124] Map initial approach to string opening --- crates/terminal/src/terminal.rs | 5 ++- crates/terminal_view/src/terminal_panel.rs | 12 ++++-- crates/terminal_view/src/terminal_view.rs | 46 +++++++++++++++++----- crates/zed/src/main.rs | 9 ++++- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fcbd02096c..9d73f7d126 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -72,9 +72,10 @@ const DEBUG_TERMINAL_HEIGHT: f32 = 30.; const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; -// Regex Copied from alacritty's ui_config.rs - lazy_static! { + // Regex Copied from alacritty's ui_config.rs + pub static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index ad61903a9d..6ad321c735 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -261,10 +261,14 @@ impl TerminalPanel { .create_terminal(working_directory, window_id, cx) .log_err() }) { - let terminal = - Box::new(cx.add_view(|cx| { - TerminalView::new(terminal, workspace.database_id(), cx) - })); + let terminal = Box::new(cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + })); pane.update(cx, |pane, cx| { let focus = pane.has_focus(); pane.add_item(terminal, true, focus, None, cx); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 4dbeb19033..7038eb284b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -32,7 +32,7 @@ use terminal::{ }, Event, Terminal, TerminalBlink, WorkingDirectory, }; -use util::ResultExt; +use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, @@ -117,19 +117,27 @@ impl TerminalView { .notify_err(workspace, cx); if let Some(terminal) = terminal { - let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + let view = cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); workspace.add_item(Box::new(view), cx) } } pub fn new( terminal: ModelHandle, + workspace: WeakViewHandle, workspace_id: WorkspaceId, cx: &mut ViewContext, ) -> Self { let view_id = cx.view_id(); cx.observe(&terminal, |_, _, cx| cx.notify()).detach(); - cx.subscribe(&terminal, |this, _, event, cx| match event { + cx.subscribe(&terminal, move |this, _, event, cx| match event { Event::Wakeup => { if !cx.is_self_focused() { this.has_new_content = true; @@ -158,12 +166,30 @@ impl TerminalView { .detach(); } } - Event::Open(url) => { - // TODO kb - // Get a workspace pointer from the new() function above - // Guess for project path or url - // Either run open buffer action OR platform open depending on whatever happens - cx.platform().open_url(url); + Event::Open(maybe_url_or_path) => { + // TODO kb, what is the API for this? + // terminal::URL_REGEX.matches(maybe_url_or_path) + if maybe_url_or_path.starts_with("http") { + cx.platform().open_url(maybe_url_or_path); + } else if let Some(workspace) = workspace.upgrade(cx) { + let path_like = + PathLikeWithPosition::parse_str(maybe_url_or_path.as_str(), |path_str| { + Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) + }) + .expect("infallible"); + let maybe_path = path_like.path_like; + workspace.update(cx, |workspace, cx| { + if false { //&& workspace.contains_path() { + // + } else if maybe_path.exists() { + workspace + .open_abs_path(maybe_path, true, cx) + .detach_and_log_err(cx); + } + }); + } + + // TODO kb let terminal know if we cannot open the string } _ => cx.emit(event.clone()), }) @@ -639,7 +665,7 @@ impl Item for TerminalView { project.create_terminal(cwd, window_id, cx) })?; Ok(pane.update(&mut cx, |_, cx| { - cx.add_view(|cx| TerminalView::new(terminal, workspace_id, cx)) + cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) })?) }) } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ccf381b5b1..8f528771c9 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -895,7 +895,14 @@ pub fn dock_default_item_factory( }) .notify_err(workspace, cx)?; - let terminal_view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx)); + let terminal_view = cx.add_view(|cx| { + TerminalView::new( + terminal, + workspace.weak_handle(), + workspace.database_id(), + cx, + ) + }); Some(Box::new(terminal_view)) } From 6123c67de94cec57f687af5746420a94a5bc947b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 15 Jul 2023 01:11:20 +0300 Subject: [PATCH 021/124] Detect and open URLs properly --- crates/terminal/src/terminal.rs | 92 +++++++++++++------- crates/terminal_view/src/terminal_element.rs | 18 ++-- crates/terminal_view/src/terminal_view.rs | 14 +-- 3 files changed, 79 insertions(+), 45 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9d73f7d126..fae79eda1d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -74,7 +74,7 @@ const DEBUG_LINE_HEIGHT: f32 = 5.; lazy_static! { // Regex Copied from alacritty's ui_config.rs - pub static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } @@ -89,7 +89,10 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - Open(String), + Open { + is_url: bool, + maybe_url_or_path: String, + }, } #[derive(Clone)] @@ -592,7 +595,14 @@ pub struct TerminalContent { pub cursor: RenderableCursor, pub cursor_char: char, pub size: TerminalSize, - pub last_hovered_hyperlink: Option<(String, RangeInclusive, usize)>, + pub last_hovered_word: Option, +} + +#[derive(Clone)] +pub struct HoveredWord { + pub word: String, + pub word_match: RangeInclusive, + pub id: usize, } impl Default for TerminalContent { @@ -609,7 +619,7 @@ impl Default for TerminalContent { }, cursor_char: Default::default(), size: Default::default(), - last_hovered_hyperlink: None, + last_hovered_word: None, } } } @@ -626,7 +636,7 @@ pub struct Terminal { events: VecDeque, /// This is only used for mouse mode cell change detection last_mouse: Option<(Point, AlacDirection)>, - /// This is only used for terminal hyperlink checking + /// This is only used for terminal hovered word checking last_mouse_position: Option, pub matches: Vec>, pub last_content: TerminalContent, @@ -773,7 +783,7 @@ impl Terminal { } InternalEvent::Scroll(scroll) => { term.scroll_display(*scroll); - self.refresh_hyperlink(); + self.refresh_hovered_word(); } InternalEvent::SetSelection(selection) => { term.selection = selection.as_ref().map(|(sel, _)| sel.clone()); @@ -808,10 +818,10 @@ impl Terminal { } InternalEvent::ScrollToPoint(point) => { term.scroll_to_point(*point); - self.refresh_hyperlink(); + self.refresh_hovered_word(); } InternalEvent::FindHyperlink(position, open) => { - let prev_hyperlink = self.last_content.last_hovered_hyperlink.take(); + let prev_hovered_word = self.last_content.last_hovered_word.take(); let point = grid_point( *position, @@ -851,41 +861,57 @@ impl Terminal { let url = link.unwrap().uri().to_owned(); let url_match = min_index..=max_index; - Some((url, url_match)) - } else if let Some(url_match) = regex_match_at(term, point, &WORD_REGEX) { - let url = term.bounds_to_string(*url_match.start(), *url_match.end()); + Some((url, true, url_match)) + } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { + let maybe_url_or_path = + term.bounds_to_string(*word_match.start(), *word_match.end()); + let is_url = regex_match_at(term, point, &URL_REGEX).is_some(); - Some((url, url_match)) + Some((maybe_url_or_path, is_url, word_match)) } else { None }; - if let Some((url, url_match)) = found_url { + if let Some((maybe_url_or_path, is_url, url_match)) = found_url { if *open { - cx.emit(Event::Open(url)) + cx.emit(Event::Open { + is_url, + maybe_url_or_path, + }) } else { - self.update_hyperlink(prev_hyperlink, url, url_match); + self.update_selected_word(prev_hovered_word, maybe_url_or_path, url_match); } } } } } - fn update_hyperlink( + fn update_selected_word( &mut self, - prev_hyperlink: Option<(String, RangeInclusive, usize)>, - url: String, - url_match: RangeInclusive, + prev_word: Option, + word: String, + word_match: RangeInclusive, ) { - if let Some(prev_hyperlink) = prev_hyperlink { - if prev_hyperlink.0 == url && prev_hyperlink.1 == url_match { - self.last_content.last_hovered_hyperlink = Some((url, url_match, prev_hyperlink.2)); + if let Some(prev_word) = prev_word { + if prev_word.word == word && prev_word.word_match == word_match { + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: prev_word.id, + }); } else { - self.last_content.last_hovered_hyperlink = - Some((url, url_match, self.next_link_id())); + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: self.next_link_id(), + }); } } else { - self.last_content.last_hovered_hyperlink = Some((url, url_match, self.next_link_id())); + self.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: self.next_link_id(), + }); } } @@ -974,9 +1000,9 @@ impl Terminal { if changed { self.cmd_pressed = cmd; if cmd { - self.refresh_hyperlink(); + self.refresh_hovered_word(); } else { - self.last_content.last_hovered_hyperlink.take(); + self.last_content.last_hovered_word.take(); } } @@ -1054,7 +1080,7 @@ impl Terminal { cursor: content.cursor, cursor_char: term.grid()[content.cursor.point].c, size: last_content.size, - last_hovered_hyperlink: last_content.last_hovered_hyperlink.clone(), + last_hovered_word: last_content.last_hovered_word.clone(), } } @@ -1109,13 +1135,13 @@ impl Terminal { } } } else if self.cmd_pressed { - self.hyperlink_from_position(Some(position)); + self.word_from_position(Some(position)); } } - fn hyperlink_from_position(&mut self, position: Option) { + fn word_from_position(&mut self, position: Option) { if self.selection_phase == SelectionPhase::Selecting { - self.last_content.last_hovered_hyperlink = None; + self.last_content.last_hovered_word = None; } else if let Some(position) = position { self.events .push_back(InternalEvent::FindHyperlink(position, false)); @@ -1274,8 +1300,8 @@ impl Terminal { } } - pub fn refresh_hyperlink(&mut self) { - self.hyperlink_from_position(self.last_mouse_position); + pub fn refresh_hovered_word(&mut self) { + self.word_from_position(self.last_mouse_position); } fn determine_scroll_lines(&mut self, e: &MouseScrollWheel, mouse_mode: bool) -> Option { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index b92059f5d6..aabfb99922 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -583,17 +583,23 @@ impl Element for TerminalElement { let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); - terminal.last_content.last_hovered_hyperlink.clone() + terminal.last_content.last_hovered_word.clone() }); - let hyperlink_tooltip = last_hovered_hyperlink.map(|(uri, _, id)| { + let hyperlink_tooltip = last_hovered_hyperlink.map(|hovered_word| { let mut tooltip = Overlay::new( Empty::new() .contained() .constrained() .with_width(dimensions.width()) .with_height(dimensions.height()) - .with_tooltip::(id, uri, None, tooltip_style, cx), + .with_tooltip::( + hovered_word.id, + hovered_word.word, + None, + tooltip_style, + cx, + ), ) .with_position_mode(gpui::elements::OverlayPositionMode::Local) .into_any(); @@ -613,7 +619,7 @@ impl Element for TerminalElement { cursor_char, selection, cursor, - last_hovered_hyperlink, + last_hovered_word, .. } = { &terminal_handle.read(cx).last_content }; @@ -634,9 +640,9 @@ impl Element for TerminalElement { &terminal_theme, cx.text_layout_cache(), cx.font_cache(), - last_hovered_hyperlink + last_hovered_word .as_ref() - .map(|(_, range, _)| (link_style, range)), + .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), ); //Layout cursor. Rectangle is used for IME, so we should lay it out even diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7038eb284b..476ff49a57 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -166,10 +166,11 @@ impl TerminalView { .detach(); } } - Event::Open(maybe_url_or_path) => { - // TODO kb, what is the API for this? - // terminal::URL_REGEX.matches(maybe_url_or_path) - if maybe_url_or_path.starts_with("http") { + Event::Open { + is_url, + maybe_url_or_path, + } => { + if *is_url { cx.platform().open_url(maybe_url_or_path); } else if let Some(workspace) = workspace.upgrade(cx) { let path_like = @@ -180,10 +181,11 @@ impl TerminalView { let maybe_path = path_like.path_like; workspace.update(cx, |workspace, cx| { if false { //&& workspace.contains_path() { - // + // TODO kb } else if maybe_path.exists() { + let visible = maybe_path.is_dir(); workspace - .open_abs_path(maybe_path, true, cx) + .open_abs_path(maybe_path, visible, cx) .detach_and_log_err(cx); } }); From 6349d90cac88808809184ca012d8f5ecf4a68953 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 00:37:03 +0300 Subject: [PATCH 022/124] Properly open project directories --- crates/project/src/project.rs | 4 +++- crates/terminal_view/src/terminal_view.rs | 24 ++++++++++++++++------- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5bb8af3f38..b3255df812 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1915,7 +1915,9 @@ impl Project { return; } - let uri = lsp::Url::from_file_path(file.abs_path(cx)).unwrap(); + let abs_path = file.abs_path(cx); + let uri = lsp::Url::from_file_path(&abs_path) + .unwrap_or_else(|()| panic!("Failed to register file {abs_path:?}")); let initial_snapshot = buffer.text_snapshot(); let language = buffer.language().cloned(); let worktree_id = file.worktree_id(cx); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 476ff49a57..49f334b2d9 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -180,18 +180,28 @@ impl TerminalView { .expect("infallible"); let maybe_path = path_like.path_like; workspace.update(cx, |workspace, cx| { - if false { //&& workspace.contains_path() { - // TODO kb - } else if maybe_path.exists() { - let visible = maybe_path.is_dir(); + let potential_abs_paths = if maybe_path.is_absolute() { + vec![maybe_path] + } else { workspace - .open_abs_path(maybe_path, visible, cx) - .detach_and_log_err(cx); + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) + .collect() + }; + + for path in potential_abs_paths { + if path.exists() { + let visible = path.is_dir(); + workspace + .open_abs_path(path, visible, cx) + .detach_and_log_err(cx); + break; + } } }); } - // TODO kb let terminal know if we cannot open the string + // TODO kb let terminal know if we cannot open the string + remove the error message when folder open returns None } _ => cx.emit(event.clone()), }) From 82a9d53c8aca67ca511baf0c94300608ab6a0ec8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 01:13:27 +0300 Subject: [PATCH 023/124] Only highlight the openable things --- crates/terminal/src/terminal.rs | 50 ++++++++++--- crates/terminal_view/src/terminal_view.rs | 85 ++++++++++++++--------- 2 files changed, 94 insertions(+), 41 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index fae79eda1d..17bfa1550e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -33,6 +33,7 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use smol::channel::Sender; use util::truncate_and_trailoff; use std::{ @@ -89,10 +90,12 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - Open { - is_url: bool, - maybe_url_or_path: String, + OpenUrl(String), + ProbePathOpen { + maybe_path: String, + can_open_tx: Sender, }, + OpenPath(String), } #[derive(Clone)] @@ -874,12 +877,43 @@ impl Terminal { if let Some((maybe_url_or_path, is_url, url_match)) = found_url { if *open { - cx.emit(Event::Open { - is_url, - maybe_url_or_path, - }) + let event = if is_url { + Event::OpenUrl(maybe_url_or_path) + } else { + Event::OpenPath(maybe_url_or_path) + }; + cx.emit(event); } else { - self.update_selected_word(prev_hovered_word, maybe_url_or_path, url_match); + if is_url { + self.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + ); + } else { + let (can_open_tx, can_open_rx) = smol::channel::bounded(1); + cx.emit(Event::ProbePathOpen { + maybe_path: maybe_url_or_path.clone(), + can_open_tx, + }); + + cx.spawn(|terminal, mut cx| async move { + let can_open = can_open_rx.recv().await.unwrap_or(false); + terminal.update(&mut cx, |terminal, cx| { + if can_open { + terminal.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + ); + } else { + terminal.last_content.last_hovered_word.take(); + } + cx.notify(); + }); + }) + .detach(); + }; } } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 49f334b2d9..f0371fcb3e 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -166,42 +166,27 @@ impl TerminalView { .detach(); } } - Event::Open { - is_url, - maybe_url_or_path, + Event::ProbePathOpen { + maybe_path, + can_open_tx, } => { - if *is_url { - cx.platform().open_url(maybe_url_or_path); - } else if let Some(workspace) = workspace.upgrade(cx) { - let path_like = - PathLikeWithPosition::parse_str(maybe_url_or_path.as_str(), |path_str| { - Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) - }) - .expect("infallible"); - let maybe_path = path_like.path_like; - workspace.update(cx, |workspace, cx| { - let potential_abs_paths = if maybe_path.is_absolute() { - vec![maybe_path] - } else { + let can_open = !possible_open_targets(&workspace, maybe_path, cx).is_empty(); + can_open_tx.send_blocking(can_open).ok(); + } + Event::OpenUrl(url) => cx.platform().open_url(url), + Event::OpenPath(maybe_path) => { + let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); + if let Some(path) = potential_abs_paths.into_iter().next() { + // TODO kb change selections using path_like row & column + let visible = path.path_like.is_dir(); + if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { workspace - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) - .collect() - }; - - for path in potential_abs_paths { - if path.exists() { - let visible = path.is_dir(); - workspace - .open_abs_path(path, visible, cx) - .detach_and_log_err(cx); - break; - } - } - }); + .open_abs_path(path.path_like, visible, cx) + .detach_and_log_err(cx); + }); + } } - - // TODO kb let terminal know if we cannot open the string + remove the error message when folder open returns None } _ => cx.emit(event.clone()), }) @@ -389,6 +374,40 @@ impl TerminalView { } } +fn possible_open_targets( + workspace: &WeakViewHandle, + maybe_path: &String, + cx: &mut ViewContext<'_, '_, TerminalView>, +) -> Vec> { + let path_like = PathLikeWithPosition::parse_str(maybe_path.as_str(), |path_str| { + Ok::<_, std::convert::Infallible>(Path::new(path_str).to_path_buf()) + }) + .expect("infallible"); + let maybe_path = path_like.path_like; + let potential_abs_paths = if maybe_path.is_absolute() { + vec![maybe_path] + } else if let Some(workspace) = workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path)) + .collect() + }) + } else { + Vec::new() + }; + + potential_abs_paths + .into_iter() + .filter(|path| path.exists()) + .map(|path| PathLikeWithPosition { + path_like: path, + row: path_like.row, + column: path_like.column, + }) + .collect() +} + pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option { let searcher = match query { project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query), From 94358ffb16491c204b14fc6976709d6f2ec189ad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 01:31:14 +0300 Subject: [PATCH 024/124] Use lines and columns from the file url strings --- crates/terminal_view/src/terminal_view.rs | 46 +++++++++++++++++++---- 1 file changed, 38 insertions(+), 8 deletions(-) diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index f0371fcb3e..7c79a76cff 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -3,8 +3,10 @@ pub mod terminal_element; pub mod terminal_panel; use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; +use anyhow::Context; use context_menu::{ContextMenu, ContextMenuItem}; use dirs::home_dir; +use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ actions, elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack}, @@ -15,6 +17,7 @@ use gpui::{ AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::Bias; use project::{LocalWorktree, Project}; use serde::Deserialize; use smallvec::{smallvec, SmallVec}; @@ -177,15 +180,42 @@ impl TerminalView { Event::OpenPath(maybe_path) => { let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); if let Some(path) = potential_abs_paths.into_iter().next() { - // TODO kb change selections using path_like row & column let visible = path.path_like.is_dir(); - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace - .open_abs_path(path.path_like, visible, cx) - .detach_and_log_err(cx); - }); - } + let task_workspace = workspace.clone(); + cx.spawn(|_, mut cx| async move { + let opened_item = task_workspace + .update(&mut cx, |workspace, cx| { + workspace.open_abs_path(path.path_like, visible, cx) + }) + .context("workspace update")? + .await + .context("workspace update")?; + if let Some(row) = path.row { + let col = path.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + Bias::Left, + ); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } _ => cx.emit(event.clone()), From 6f7a6e57fcefa7e7c8f78a27dd6e604d73a76e9c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 02:03:27 +0300 Subject: [PATCH 025/124] Avoid excessive blinking on cmd-hover --- crates/terminal/src/terminal.rs | 62 +++++++++++++++++---------------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 17bfa1550e..256cbe652d 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -884,36 +884,13 @@ impl Terminal { }; cx.emit(event); } else { - if is_url { - self.update_selected_word( - prev_hovered_word, - maybe_url_or_path, - url_match, - ); - } else { - let (can_open_tx, can_open_rx) = smol::channel::bounded(1); - cx.emit(Event::ProbePathOpen { - maybe_path: maybe_url_or_path.clone(), - can_open_tx, - }); - - cx.spawn(|terminal, mut cx| async move { - let can_open = can_open_rx.recv().await.unwrap_or(false); - terminal.update(&mut cx, |terminal, cx| { - if can_open { - terminal.update_selected_word( - prev_hovered_word, - maybe_url_or_path, - url_match, - ); - } else { - terminal.last_content.last_hovered_word.take(); - } - cx.notify(); - }); - }) - .detach(); - }; + self.update_selected_word( + prev_hovered_word, + maybe_url_or_path, + url_match, + !is_url, + cx, + ); } } } @@ -925,6 +902,8 @@ impl Terminal { prev_word: Option, word: String, word_match: RangeInclusive, + should_probe_word: bool, + cx: &mut ModelContext, ) { if let Some(prev_word) = prev_word { if prev_word.word == word && prev_word.word_match == word_match { @@ -933,6 +912,29 @@ impl Terminal { word_match, id: prev_word.id, }); + } else if should_probe_word { + let (can_open_tx, can_open_rx) = smol::channel::bounded(1); + cx.emit(Event::ProbePathOpen { + maybe_path: word.clone(), + can_open_tx, + }); + + cx.spawn(|terminal, mut cx| async move { + let can_open = can_open_rx.recv().await.unwrap_or(false); + terminal.update(&mut cx, |terminal, cx| { + if can_open { + terminal.last_content.last_hovered_word = Some(HoveredWord { + word, + word_match, + id: terminal.next_link_id(), + }); + } else { + terminal.last_content.last_hovered_word.take(); + } + cx.notify(); + }); + }) + .detach(); } else { self.last_content.last_hovered_word = Some(HoveredWord { word, From 10db05f87f1a00a616bd8fcfcbc37ca427d46d6b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 13:28:44 +0300 Subject: [PATCH 026/124] Rework terminal highlight event flow --- crates/terminal/src/terminal.rs | 109 ++++++++----------- crates/terminal_view/src/terminal_element.rs | 14 ++- crates/terminal_view/src/terminal_view.rs | 105 ++++++++++-------- 3 files changed, 112 insertions(+), 116 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 256cbe652d..438224c81e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -33,7 +33,6 @@ use mappings::mouse::{ use procinfo::LocalProcessInfo; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use smol::channel::Sender; use util::truncate_and_trailoff; use std::{ @@ -90,12 +89,18 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - OpenUrl(String), - ProbePathOpen { - maybe_path: String, - can_open_tx: Sender, - }, - OpenPath(String), + NewNavigationTarget(MaybeNavigationTarget), + Open(MaybeNavigationTarget), +} + +/// A string inside terminal, potentially useful as a URI that can be opened. +#[derive(Clone, Debug)] +pub enum MaybeNavigationTarget { + /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex. + Url(String), + /// File system path, absolute or relative, existing or not. + /// Might have line and column number(s) attached as `file.rs:1:23` + PathLike(String), } #[derive(Clone)] @@ -502,6 +507,7 @@ impl TerminalBuilder { next_link_id: 0, selection_phase: SelectionPhase::Ended, cmd_pressed: false, + found_word: false, }; Ok(TerminalBuilder { @@ -654,6 +660,7 @@ pub struct Terminal { next_link_id: usize, selection_phase: SelectionPhase, cmd_pressed: bool, + found_word: bool, } impl Terminal { @@ -834,7 +841,7 @@ impl Terminal { .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor); let link = term.grid().index(point).hyperlink(); - let found_url = if link.is_some() { + let found_word = if link.is_some() { let mut min_index = point; loop { let new_min_index = @@ -875,20 +882,21 @@ impl Terminal { None }; - if let Some((maybe_url_or_path, is_url, url_match)) = found_url { + self.found_word = found_word.is_some(); + if let Some((maybe_url_or_path, is_url, url_match)) = found_word { if *open { - let event = if is_url { - Event::OpenUrl(maybe_url_or_path) + let target = if is_url { + MaybeNavigationTarget::Url(maybe_url_or_path) } else { - Event::OpenPath(maybe_url_or_path) + MaybeNavigationTarget::PathLike(maybe_url_or_path) }; - cx.emit(event); + cx.emit(Event::Open(target)); } else { self.update_selected_word( prev_hovered_word, - maybe_url_or_path, url_match, - !is_url, + maybe_url_or_path, + is_url, cx, ); } @@ -900,9 +908,9 @@ impl Terminal { fn update_selected_word( &mut self, prev_word: Option, - word: String, word_match: RangeInclusive, - should_probe_word: bool, + word: String, + is_url: bool, cx: &mut ModelContext, ) { if let Some(prev_word) = prev_word { @@ -912,43 +920,21 @@ impl Terminal { word_match, id: prev_word.id, }); - } else if should_probe_word { - let (can_open_tx, can_open_rx) = smol::channel::bounded(1); - cx.emit(Event::ProbePathOpen { - maybe_path: word.clone(), - can_open_tx, - }); - - cx.spawn(|terminal, mut cx| async move { - let can_open = can_open_rx.recv().await.unwrap_or(false); - terminal.update(&mut cx, |terminal, cx| { - if can_open { - terminal.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: terminal.next_link_id(), - }); - } else { - terminal.last_content.last_hovered_word.take(); - } - cx.notify(); - }); - }) - .detach(); - } else { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); + return; } - } else { - self.last_content.last_hovered_word = Some(HoveredWord { - word, - word_match, - id: self.next_link_id(), - }); } + + self.last_content.last_hovered_word = Some(HoveredWord { + word: word.clone(), + word_match, + id: self.next_link_id(), + }); + let navigation_target = if is_url { + MaybeNavigationTarget::Url(word) + } else { + MaybeNavigationTarget::PathLike(word) + }; + cx.emit(Event::NewNavigationTarget(navigation_target)); } fn next_link_id(&mut self) -> usize { @@ -1031,17 +1017,8 @@ impl Terminal { } pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { - let cmd = modifiers.cmd; - let changed = self.cmd_pressed != cmd; - if changed { - self.cmd_pressed = cmd; - if cmd { - self.refresh_hovered_word(); - } else { - self.last_content.last_hovered_word.take(); - } - } - + let changed = self.cmd_pressed != modifiers.cmd; + self.cmd_pressed = modifiers.cmd; changed } @@ -1336,7 +1313,7 @@ impl Terminal { } } - pub fn refresh_hovered_word(&mut self) { + fn refresh_hovered_word(&mut self) { self.word_from_position(self.last_mouse_position); } @@ -1415,6 +1392,10 @@ impl Terminal { }) .unwrap_or_else(|| "Terminal".to_string()) } + + pub fn can_navigate_to_selected_word(&self) -> bool { + self.cmd_pressed && self.found_word + } } impl Drop for Terminal { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index aabfb99922..e29beb3ad5 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -163,6 +163,7 @@ pub struct TerminalElement { terminal: WeakModelHandle, focused: bool, cursor_visible: bool, + can_navigate_to_selected_word: bool, } impl TerminalElement { @@ -170,11 +171,13 @@ impl TerminalElement { terminal: WeakModelHandle, focused: bool, cursor_visible: bool, + can_navigate_to_selected_word: bool, ) -> TerminalElement { TerminalElement { terminal, focused, cursor_visible, + can_navigate_to_selected_word, } } @@ -580,13 +583,17 @@ impl Element for TerminalElement { let background_color = terminal_theme.background; let terminal_handle = self.terminal.upgrade(cx).unwrap(); - let last_hovered_hyperlink = terminal_handle.update(cx, |terminal, cx| { + let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); - terminal.last_content.last_hovered_word.clone() + if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { + terminal.last_content.last_hovered_word.clone() + } else { + None + } }); - let hyperlink_tooltip = last_hovered_hyperlink.map(|hovered_word| { + let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { let mut tooltip = Overlay::new( Empty::new() .contained() @@ -619,7 +626,6 @@ impl Element for TerminalElement { cursor_char, selection, cursor, - last_hovered_word, .. } = { &terminal_handle.read(cx).last_content }; diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 7c79a76cff..3f4101d16c 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,7 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, Terminal, TerminalBlink, WorkingDirectory, + Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -93,6 +93,7 @@ pub struct TerminalView { blinking_on: bool, blinking_paused: bool, blink_epoch: usize, + can_navigate_to_selected_word: bool, workspace_id: WorkspaceId, } @@ -169,55 +170,61 @@ impl TerminalView { .detach(); } } - Event::ProbePathOpen { - maybe_path, - can_open_tx, - } => { - let can_open = !possible_open_targets(&workspace, maybe_path, cx).is_empty(); - can_open_tx.send_blocking(can_open).ok(); - } - Event::OpenUrl(url) => cx.platform().open_url(url), - Event::OpenPath(maybe_path) => { - let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); - if let Some(path) = potential_abs_paths.into_iter().next() { - let visible = path.path_like.is_dir(); - let task_workspace = workspace.clone(); - cx.spawn(|_, mut cx| async move { - let opened_item = task_workspace - .update(&mut cx, |workspace, cx| { - workspace.open_abs_path(path.path_like, visible, cx) - }) - .context("workspace update")? - .await - .context("workspace update")?; - if let Some(row) = path.row { - let col = path.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - Bias::Left, - ); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); - } - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + Event::NewNavigationTarget(maybe_navigation_target) => { + this.can_navigate_to_selected_word = match maybe_navigation_target { + MaybeNavigationTarget::Url(_) => true, + MaybeNavigationTarget::PathLike(maybe_path) => { + !possible_open_targets(&workspace, maybe_path, cx).is_empty() + } } } + Event::Open(maybe_navigation_target) => match maybe_navigation_target { + MaybeNavigationTarget::Url(url) => cx.platform().open_url(url), + MaybeNavigationTarget::PathLike(maybe_path) => { + if !this.can_navigate_to_selected_word { + return; + } + let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); + if let Some(path) = potential_abs_paths.into_iter().next() { + let visible = path.path_like.is_dir(); + let task_workspace = workspace.clone(); + cx.spawn(|_, mut cx| async move { + let opened_item = task_workspace + .update(&mut cx, |workspace, cx| { + workspace.open_abs_path(path.path_like, visible, cx) + }) + .context("workspace update")? + .await + .context("workspace update")?; + if let Some(row) = path.row { + let col = path.column.unwrap_or(0); + if let Some(active_editor) = opened_item.downcast::() { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + Bias::Left, + ); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + }, _ => cx.emit(event.clone()), }) .detach(); @@ -231,6 +238,7 @@ impl TerminalView { blinking_on: false, blinking_paused: false, blink_epoch: 0, + can_navigate_to_selected_word: false, workspace_id, } } @@ -466,6 +474,7 @@ impl View for TerminalView { terminal_handle, focused, self.should_show_cursor(focused, cx), + self.can_navigate_to_selected_word, ) .contained(), ) From 6ed7820f7ca2d73d2a2c2e1baf84fa5c7325cb25 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 16:08:17 +0300 Subject: [PATCH 027/124] Consider all terminal when searching for words --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 438224c81e..450ca35b44 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -838,7 +838,7 @@ impl Terminal { self.last_content.size, term.grid().display_offset(), ) - .grid_clamp(term, alacritty_terminal::index::Boundary::Cursor); + .grid_clamp(term, alacritty_terminal::index::Boundary::Grid); let link = term.grid().index(point).hyperlink(); let found_word = if link.is_some() { From 33921183dc31170585b09d598a9ca6799724bbc2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 18 Jul 2023 17:05:52 +0300 Subject: [PATCH 028/124] Avoid extra blinking on mouse moves --- crates/terminal/src/terminal.rs | 53 ++++++++++++++--------- crates/terminal_view/src/terminal_view.rs | 5 ++- 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 450ca35b44..3a64cff24f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -89,7 +89,7 @@ pub enum Event { Wakeup, BlinkChanged, SelectionsChanged, - NewNavigationTarget(MaybeNavigationTarget), + NewNavigationTarget(Option), Open(MaybeNavigationTarget), } @@ -507,7 +507,7 @@ impl TerminalBuilder { next_link_id: 0, selection_phase: SelectionPhase::Ended, cmd_pressed: false, - found_word: false, + hovered_word: false, }; Ok(TerminalBuilder { @@ -660,7 +660,7 @@ pub struct Terminal { next_link_id: usize, selection_phase: SelectionPhase, cmd_pressed: bool, - found_word: bool, + hovered_word: bool, } impl Terminal { @@ -882,23 +882,31 @@ impl Terminal { None }; - self.found_word = found_word.is_some(); - if let Some((maybe_url_or_path, is_url, url_match)) = found_word { - if *open { - let target = if is_url { - MaybeNavigationTarget::Url(maybe_url_or_path) + match found_word { + Some((maybe_url_or_path, is_url, url_match)) => { + if *open { + let target = if is_url { + MaybeNavigationTarget::Url(maybe_url_or_path) + } else { + MaybeNavigationTarget::PathLike(maybe_url_or_path) + }; + cx.emit(Event::Open(target)); } else { - MaybeNavigationTarget::PathLike(maybe_url_or_path) - }; - cx.emit(Event::Open(target)); - } else { - self.update_selected_word( - prev_hovered_word, - url_match, - maybe_url_or_path, - is_url, - cx, - ); + self.update_selected_word( + prev_hovered_word, + url_match, + maybe_url_or_path, + is_url, + cx, + ); + } + self.hovered_word = true; + } + None => { + if self.hovered_word { + cx.emit(Event::NewNavigationTarget(None)); + } + self.hovered_word = false; } } } @@ -934,7 +942,7 @@ impl Terminal { } else { MaybeNavigationTarget::PathLike(word) }; - cx.emit(Event::NewNavigationTarget(navigation_target)); + cx.emit(Event::NewNavigationTarget(Some(navigation_target))); } fn next_link_id(&mut self) -> usize { @@ -1018,6 +1026,9 @@ impl Terminal { pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool { let changed = self.cmd_pressed != modifiers.cmd; + if !self.cmd_pressed && modifiers.cmd { + self.refresh_hovered_word(); + } self.cmd_pressed = modifiers.cmd; changed } @@ -1394,7 +1405,7 @@ impl Terminal { } pub fn can_navigate_to_selected_word(&self) -> bool { - self.cmd_pressed && self.found_word + self.cmd_pressed && self.hovered_word } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3f4101d16c..cdb1d40efc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -172,10 +172,11 @@ impl TerminalView { } Event::NewNavigationTarget(maybe_navigation_target) => { this.can_navigate_to_selected_word = match maybe_navigation_target { - MaybeNavigationTarget::Url(_) => true, - MaybeNavigationTarget::PathLike(maybe_path) => { + Some(MaybeNavigationTarget::Url(_)) => true, + Some(MaybeNavigationTarget::PathLike(maybe_path)) => { !possible_open_targets(&workspace, maybe_path, cx).is_empty() } + None => false, } } Event::Open(maybe_navigation_target) => match maybe_navigation_target { From 3058a96deeac15d3ccb1c17d14eb7712ae397e4b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 19 Jul 2023 14:06:00 +0300 Subject: [PATCH 029/124] Clean up stale conflicting hints --- crates/editor/src/editor.rs | 9 +- crates/editor/src/inlay_hint_cache.rs | 375 ++++++++++++++++++++++---- 2 files changed, 323 insertions(+), 61 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b8a7853a93..379f43e2e5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2659,11 +2659,16 @@ impl Editor { InlayRefreshReason::RefreshRequested => (InvalidationStrategy::RefreshRequested, None), }; - self.inlay_hint_cache.refresh_inlay_hints( + if let Some(InlaySplice { + to_remove, + to_insert, + }) = self.inlay_hint_cache.spawn_hint_refresh( self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, - ) + ) { + self.splice_inlay_hints(to_remove, to_insert, cx); + } } fn visible_inlay_hints(&self, cx: &ViewContext<'_, '_, Editor>) -> Vec { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52473f9971..63076ba234 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -195,20 +195,41 @@ impl InlayHintCache { } } - pub fn refresh_inlay_hints( + pub fn spawn_hint_refresh( &mut self, mut excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, - ) { - if !self.enabled || excerpts_to_query.is_empty() { - return; + ) -> Option { + if !self.enabled { + return None; } + let update_tasks = &mut self.update_tasks; + let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - update_tasks - .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); + let mut changed = false; + update_tasks.retain(|task_excerpt_id, _| { + let retain = excerpts_to_query.contains_key(task_excerpt_id); + changed |= !retain; + retain + }); + self.hints.retain(|cached_excerpt, cached_hints| { + let retain = excerpts_to_query.contains_key(cached_excerpt); + changed |= !retain; + if !retain { + invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); + } + retain + }); + if changed { + self.version += 1; + } } + if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { + return None; + } + let cache_version = self.version; excerpts_to_query.retain(|visible_excerpt_id, _| { match update_tasks.entry(*visible_excerpt_id) { @@ -229,6 +250,15 @@ impl InlayHintCache { .ok(); }) .detach(); + + if invalidated_hints.is_empty() { + None + } else { + Some(InlaySplice { + to_remove: invalidated_hints, + to_insert: Vec::new(), + }) + } } fn new_allowed_hint_kinds_splice( @@ -684,7 +714,7 @@ async fn fetch_and_update_hints( if query.invalidate.should_invalidate() { let mut outdated_excerpt_caches = HashSet::default(); - for (excerpt_id, excerpt_hints) in editor.inlay_hint_cache().hints.iter() { + for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { let excerpt_hints = excerpt_hints.read(); if excerpt_hints.buffer_id == query.buffer_id && excerpt_id != &query.excerpt_id @@ -1022,9 +1052,9 @@ mod tests { "Should get its first hints when opening the editor" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "The editor update the cache version after every cache/view change" ); }); @@ -1053,9 +1083,9 @@ mod tests { "Should not update hints while the work task is running" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Should not update the cache while the work task is running" ); }); @@ -1077,9 +1107,9 @@ mod tests { "New hints should be queried after the work task is done" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Cache version should udpate once after the work task is done" ); }); @@ -1194,9 +1224,9 @@ mod tests { "Should get its first hints when opening the editor" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 1, + editor.inlay_hint_cache().version, + 1, "Rust editor update the cache version after every cache/view change" ); }); @@ -1252,8 +1282,7 @@ mod tests { "Markdown editor should have a separate verison, repeating Rust editor rules" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 1); + assert_eq!(editor.inlay_hint_cache().version, 1); }); rs_editor.update(cx, |editor, cx| { @@ -1269,9 +1298,9 @@ mod tests { "Rust inlay cache should change after the edit" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, + 2, "Every time hint cache changes, cache version should be incremented" ); }); @@ -1283,8 +1312,7 @@ mod tests { "Markdown editor should not be affected by Rust editor changes" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 1); + assert_eq!(editor.inlay_hint_cache().version, 1); }); md_editor.update(cx, |editor, cx| { @@ -1300,8 +1328,7 @@ mod tests { "Rust editor should not be affected by Markdown editor changes" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 2); + assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { let expected_layers = vec!["1".to_string()]; @@ -1311,8 +1338,7 @@ mod tests { "Markdown editor should also change independently" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 2); + assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -1433,9 +1459,9 @@ mod tests { vec!["other hint".to_string(), "type hint".to_string()], visible_hint_labels(editor, cx) ); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, + edits_made, "Should not update cache version due to new loaded hints being the same" ); }); @@ -1568,9 +1594,8 @@ mod tests { ); assert!(cached_hint_labels(editor).is_empty()); assert!(visible_hint_labels(editor, cx).is_empty()); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + editor.inlay_hint_cache().version, edits_made, "The editor should not update the cache version after /refresh query without updates" ); }); @@ -1641,8 +1666,7 @@ mod tests { vec!["parameter hint".to_string()], visible_hint_labels(editor, cx), ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(editor.inlay_hint_cache().version, edits_made); }); } @@ -1720,9 +1744,8 @@ mod tests { "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 1, + editor.inlay_hint_cache().version, 1, "Only one update should be registered in the cache after all cancellations" ); }); @@ -1766,9 +1789,9 @@ mod tests { "Should get hints from the last edit landed only" ); assert_eq!(expected_hints, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, + 2, "Should update the cache version once more, for the new change" ); }); @@ -1886,9 +1909,8 @@ mod tests { "Should have hints from both LSP requests made for a big file" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 2, + editor.inlay_hint_cache().version, 2, "Both LSP queries should've bumped the cache version" ); }); @@ -1918,8 +1940,7 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "Should have hints from the new LSP response after edit"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 5, "Should update the cache for every LSP response with hints added"); }); } @@ -2075,6 +2096,7 @@ mod tests { panic!("unexpected uri: {:?}", params.text_document.uri); }; + // one hint per excerpt let positions = [ lsp::Position::new(0, 2), lsp::Position::new(4, 2), @@ -2138,8 +2160,7 @@ mod tests { "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 4, "Every visible excerpt hints should bump the verison"); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2169,8 +2190,8 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 9); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); editor.update(cx, |editor, cx| { @@ -2179,7 +2200,7 @@ mod tests { }); }); cx.foreground().run_until_parked(); - editor.update(cx, |editor, cx| { + let last_scroll_update_version = editor.update(cx, |editor, cx| { let expected_layers = vec![ "main hint #0".to_string(), "main hint #1".to_string(), @@ -2197,8 +2218,8 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 12); + assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); + expected_layers.len() }); editor.update(cx, |editor, cx| { @@ -2225,12 +2246,14 @@ mod tests { assert_eq!(expected_layers, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 12, "No updates should happen during scrolling already scolled buffer"); + assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); editor_edited.store(true, Ordering::Release); editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + }); editor.handle_input("++++more text++++", cx); }); cx.foreground().run_until_parked(); @@ -2240,19 +2263,253 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), - "other hint #0".to_string(), - "other hint #1".to_string(), - "other hint #2".to_string(), - "other hint #3".to_string(), - "other hint #4".to_string(), - "other hint #5".to_string(), + "main hint(edited) #4".to_string(), + "main hint(edited) #5".to_string(), + "other hint(edited) #0".to_string(), + "other hint(edited) #1".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), - "After multibuffer was edited, hints for the edited buffer (1st) should be invalidated and requeried for all of its visible excerpts, \ -unedited (2nd) buffer should have the same hint"); + assert_eq!( + expected_layers, + cached_hint_labels(editor), + "After multibuffer edit, editor gets scolled back to the last selection; \ +all hints should be invalidated and requeried for all of its visible excerpts" + ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, 16); + assert_eq!( + editor.inlay_hint_cache().version, + last_scroll_update_version + expected_layers.len() + 1, + "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + ); + }); + } + + #[gpui::test] + async fn test_excerpts_removed( + deterministic: Arc, + cx: &mut gpui::TestAppContext, + ) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: false, + show_parameter_hints: false, + show_other_hints: false, + }) + }); + + let mut language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let language = Arc::new(language); + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|i| format!("let i = {i};\n")).collect::>().join("")), + "other.rs": format!("fn main() {{\n{}\n}}", (0..501).map(|j| format!("let j = {j};\n")).collect::>().join("")), + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::clone(&language)) + }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let buffer_1 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "main.rs"), cx) + }) + .await + .unwrap(); + let buffer_2 = project + .update(cx, |project, cx| { + project.open_buffer((worktree_id, "other.rs"), cx) + }) + .await + .unwrap(); + let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); + let (buffer_1_excerpts, buffer_2_excerpts) = multibuffer.update(cx, |multibuffer, cx| { + let buffer_1_excerpts = multibuffer.push_excerpts( + buffer_1.clone(), + [ExcerptRange { + context: Point::new(0, 0)..Point::new(2, 0), + primary: None, + }], + cx, + ); + let buffer_2_excerpts = multibuffer.push_excerpts( + buffer_2.clone(), + [ExcerptRange { + context: Point::new(0, 1)..Point::new(2, 1), + primary: None, + }], + cx, + ); + (buffer_1_excerpts, buffer_2_excerpts) + }); + + assert!(!buffer_1_excerpts.is_empty()); + assert!(!buffer_2_excerpts.is_empty()); + + deterministic.run_until_parked(); + cx.foreground().run_until_parked(); + let (_, editor) = + cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor_edited = Arc::new(AtomicBool::new(false)); + let fake_server = fake_servers.next().await.unwrap(); + let closure_editor_edited = Arc::clone(&editor_edited); + fake_server + .handle_request::(move |params, _| { + let task_editor_edited = Arc::clone(&closure_editor_edited); + async move { + let hint_text = if params.text_document.uri + == lsp::Url::from_file_path("/a/main.rs").unwrap() + { + "main hint" + } else if params.text_document.uri + == lsp::Url::from_file_path("/a/other.rs").unwrap() + { + "other hint" + } else { + panic!("unexpected uri: {:?}", params.text_document.uri); + }; + + let positions = [ + lsp::Position::new(0, 2), + lsp::Position::new(4, 2), + lsp::Position::new(22, 2), + lsp::Position::new(44, 2), + lsp::Position::new(56, 2), + lsp::Position::new(67, 2), + ]; + let out_of_range_hint = lsp::InlayHint { + position: lsp::Position::new( + params.range.start.line + 99, + params.range.start.character + 99, + ), + label: lsp::InlayHintLabel::String( + "out of excerpt range, should be ignored".to_string(), + ), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }; + + let edited = task_editor_edited.load(Ordering::Acquire); + Ok(Some( + std::iter::once(out_of_range_hint) + .chain(positions.into_iter().enumerate().map(|(i, position)| { + lsp::InlayHint { + position, + label: lsp::InlayHintLabel::String(format!( + "{hint_text}{} #{i}", + if edited { "(edited)" } else { "" }, + )), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + } + })) + .collect(), + )) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string(), "other hint #0".to_string()], + cached_hint_labels(editor), + "Cache should update for both excerpts despite hints display was disabled" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 2, + "Cache should update once per excerpt query" + ); + }); + + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts(buffer_2_excerpts, cx) + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + assert_eq!( + vec!["main hint #0".to_string()], + cached_hint_labels(editor), + "For the removed excerpt, should clean corresponding cached hints" + ); + assert!( + visible_hint_labels(editor, cx).is_empty(), + "All hints are disabled and should not be shown despite being present in the cache" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 3, + "Excerpt removal should trigger cache update" + ); + }); + + update_test_language_settings(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + let expected_hints = vec!["main hint #0".to_string()]; + assert_eq!( + expected_hints, + cached_hint_labels(editor), + "Hint display settings change should not change the cache" + ); + assert_eq!( + expected_hints, + visible_hint_labels(editor, cx), + "Settings change should make cached hints visible" + ); + assert_eq!( + editor.inlay_hint_cache().version, + 4, + "Settings change should trigger cache update" + ); }); } From dd8863d0dea9726fa3e517292bc8d29c9dc1682c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 10:24:23 -0600 Subject: [PATCH 030/124] Make tab non-functional in vim mode Fixes: zed-industries/community#988 Fixes: zed-industries/community#897 --- assets/keymaps/vim.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 2c406e3eb0..aa1e235414 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -30,6 +30,8 @@ "j": "vim::Down", "down": "vim::Down", "enter": "vim::NextLineStart", + "tab": "vim::Tab", + "shift-tab": "vim::Tab", "k": "vim::Up", "up": "vim::Up", "l": "vim::Right", @@ -205,7 +207,7 @@ "?": [ "vim::Search", { - "backwards": true, + "backwards": true } ], "ctrl-f": "vim::PageDown", @@ -282,7 +284,7 @@ "bindings": { "t": "editor::ScrollCursorTop", "z": "editor::ScrollCursorCenter", - "b": "editor::ScrollCursorBottom", + "b": "editor::ScrollCursorBottom" } }, { @@ -337,7 +339,7 @@ "bindings": { "escape": "vim::NormalBefore", "ctrl-c": "vim::NormalBefore", - "ctrl-[": "vim::NormalBefore", + "ctrl-[": "vim::NormalBefore" } }, { From d26f76ba90d5ca314fea8d63d1f0ccf1b1f92905 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 13 Jul 2023 16:56:39 -0700 Subject: [PATCH 031/124] Add suffix based file icons --- crates/language/src/language.rs | 19 ++++++++++ crates/project/src/project.rs | 5 +++ crates/project_panel/src/project_panel.rs | 45 +++++++++++++++++------ crates/theme/src/theme.rs | 3 +- crates/zed/src/languages/toml/config.toml | 1 + styles/src/style_tree/project_panel.ts | 3 +- 6 files changed, 62 insertions(+), 14 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 50a0b4b161..f416422fa2 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -342,6 +342,8 @@ pub struct LanguageConfig { pub block_comment: Option<(Arc, Arc)>, #[serde(default)] pub overrides: HashMap, + #[serde(default)] + pub icon_path: Option>, } #[derive(Debug, Default)] @@ -408,6 +410,7 @@ impl Default for LanguageConfig { line_comment: Default::default(), block_comment: Default::default(), overrides: Default::default(), + icon_path: Default::default(), } } } @@ -752,6 +755,22 @@ impl LanguageRegistry { self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) } + pub fn icon_for_suffix( + self: &Arc, + suffix: &str, + ) -> Option> { + let state = self.state.read(); + state.available_languages + .iter() + .find(|langauge| { + langauge.config.path_suffixes.iter().any(|s| s == suffix) + }) + .map(|language| { + language.config.icon_path.clone() + }) + .flatten() + } + pub fn language_for_name_or_extension( self: &Arc, string: &str, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5bb8af3f38..1158f2fa56 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2474,6 +2474,11 @@ impl Project { }) } + pub fn icon_for_path(&self, path: &Path) -> Option> { + self.languages + .icon_for_suffix(path.extension()?.to_str()?) + } + fn detect_language_for_buffer( &mut self, buffer_handle: &ModelHandle, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5442a8be74..ce86b1e768 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -44,6 +44,7 @@ use workspace::{ const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; +const TEXT_FILE_ASSET: &'static str = "icons/radix/file-text.svg"; pub struct ProjectPanel { project: ModelHandle, @@ -94,6 +95,7 @@ pub enum ClipboardEntry { #[derive(Debug, PartialEq, Eq)] pub struct EntryDetails { filename: String, + icon: Option>, path: Arc, depth: usize, kind: EntryKind, @@ -1180,6 +1182,15 @@ impl ProjectPanel { for entry in visible_worktree_entries[entry_range].iter() { let status = git_status_setting.then(|| entry.git_status).flatten(); + let icon = match entry.kind { + EntryKind::File(_) => self + .project + .read(cx) + .icon_for_path(&entry.path) + .or_else(|| Some(TEXT_FILE_ASSET.into())), + _ => None, + }; + let mut details = EntryDetails { filename: entry .path @@ -1187,6 +1198,7 @@ impl ProjectPanel { .unwrap_or(root_name) .to_string_lossy() .to_string(), + icon, path: entry.path.clone(), depth: entry.path.components().count(), kind: entry.kind, @@ -1254,23 +1266,32 @@ impl ProjectPanel { .unwrap_or(style.text.color); Flex::row() - .with_child( - if kind.is_dir() { - if details.is_expanded { - Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color) - } else { - Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color) - } - .constrained() + .with_child(if kind.is_dir() { + if details.is_expanded { + Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color) + } else { + Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color) + } + .constrained() + .with_max_width(style.directory_icon_size) + .with_max_height(style.directory_icon_size) + .aligned() + .constrained() + .with_width(style.directory_icon_size) + } else { + if let Some(icon) = &details.icon { + Svg::new(icon.to_string()) + .with_color(style.icon_color) + .constrained() } else { Empty::new().constrained() } - .with_max_width(style.icon_size) - .with_max_height(style.icon_size) + .with_max_width(style.file_icon_size) + .with_max_height(style.file_icon_size) .aligned() .constrained() - .with_width(style.icon_size), - ) + .with_width(style.file_icon_size) + }) .with_child(if show_editor && editor.is_some() { ChildView::new(editor.as_ref().unwrap(), cx) .contained() diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index b7a7408bef..53a0629198 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -481,7 +481,8 @@ pub struct ProjectPanelEntry { pub container: ContainerStyle, pub text: TextStyle, pub icon_color: Color, - pub icon_size: f32, + pub directory_icon_size: f32, + pub file_icon_size: f32, pub icon_spacing: f32, pub status: EntryStatus, } diff --git a/crates/zed/src/languages/toml/config.toml b/crates/zed/src/languages/toml/config.toml index 4e89f5cabd..5a3fc9d8b8 100644 --- a/crates/zed/src/languages/toml/config.toml +++ b/crates/zed/src/languages/toml/config.toml @@ -1,5 +1,6 @@ name = "TOML" path_suffixes = ["toml"] +icon_path = "icons/radix/gear.svg" line_comment = "# " autoclose_before = ",]}" brackets = [ diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index af997d0a6e..c2719e935e 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -47,7 +47,8 @@ export default function project_panel(): any { height: 22, background: background(theme.middle), icon_color: foreground(theme.middle, "variant"), - icon_size: 7, + directory_icon_size: 7, + file_icon_size: 14, icon_spacing: 5, text: text(theme.middle, "sans", "variant", { size: "sm" }), status: { From d023189bda9d13a463980830ff352d1e445b1702 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 13 Jul 2023 17:02:19 -0700 Subject: [PATCH 032/124] Add settings --- assets/settings/default.json | 2 + crates/language/src/language.rs | 16 +++----- crates/project/src/project.rs | 3 +- crates/project_panel/src/project_panel.rs | 41 ++++++++++++------- .../src/project_panel_settings.rs | 2 + 5 files changed, 36 insertions(+), 28 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 5cf5f59f76..e1f2d93270 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -99,6 +99,8 @@ "project_panel": { // Whether to show the git status in the project panel. "git_status": true, + // Whether to show file icons in the project panel. + "file_icons": true, // Where to dock project panel. Can be 'left' or 'right'. "dock": "left", // Default width of the project panel. diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index f416422fa2..60f14d9fa7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -755,19 +755,13 @@ impl LanguageRegistry { self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) } - pub fn icon_for_suffix( - self: &Arc, - suffix: &str, - ) -> Option> { + pub fn icon_for_suffix(self: &Arc, suffix: &str) -> Option> { let state = self.state.read(); - state.available_languages + state + .available_languages .iter() - .find(|langauge| { - langauge.config.path_suffixes.iter().any(|s| s == suffix) - }) - .map(|language| { - language.config.icon_path.clone() - }) + .find(|langauge| langauge.config.path_suffixes.iter().any(|s| s == suffix)) + .map(|language| language.config.icon_path.clone()) .flatten() } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1158f2fa56..80c098baa6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2475,8 +2475,7 @@ impl Project { } pub fn icon_for_path(&self, path: &Path) -> Option> { - self.languages - .icon_for_suffix(path.extension()?.to_str()?) + self.languages.icon_for_suffix(path.extension()?.to_str()?) } fn detect_language_for_buffer( diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index ce86b1e768..06c6c1540c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1168,7 +1168,10 @@ impl ProjectPanel { } let end_ix = range.end.min(ix + visible_worktree_entries.len()); - let git_status_setting = settings::get::(cx).git_status; + let (git_status_setting, show_file_icons) = { + let settings = settings::get::(cx); + (settings.git_status, settings.file_icons) + }; if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { let snapshot = worktree.read(cx).snapshot(); let root_name = OsStr::new(snapshot.root_name()); @@ -1182,14 +1185,16 @@ impl ProjectPanel { for entry in visible_worktree_entries[entry_range].iter() { let status = git_status_setting.then(|| entry.git_status).flatten(); - let icon = match entry.kind { - EntryKind::File(_) => self - .project - .read(cx) - .icon_for_path(&entry.path) - .or_else(|| Some(TEXT_FILE_ASSET.into())), - _ => None, - }; + let icon = show_file_icons + .then(|| match entry.kind { + EntryKind::File(_) => self + .project + .read(cx) + .icon_for_path(&entry.path) + .or_else(|| Some(TEXT_FILE_ASSET.into())), + _ => None, + }) + .flatten(); let mut details = EntryDetails { filename: entry @@ -1283,14 +1288,20 @@ impl ProjectPanel { Svg::new(icon.to_string()) .with_color(style.icon_color) .constrained() + .with_max_width(style.file_icon_size) + .with_max_height(style.file_icon_size) + .aligned() + .constrained() + .with_width(style.file_icon_size) } else { - Empty::new().constrained() + Empty::new() + .constrained() + .with_max_width(style.directory_icon_size) + .with_max_height(style.directory_icon_size) + .aligned() + .constrained() + .with_width(style.directory_icon_size) } - .with_max_width(style.file_icon_size) - .with_max_height(style.file_icon_size) - .aligned() - .constrained() - .with_width(style.file_icon_size) }) .with_child(if show_editor && editor.is_some() { ChildView::new(editor.as_ref().unwrap(), cx) diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 1d6c590710..f0d60d7f4f 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -13,6 +13,7 @@ pub enum ProjectPanelDockPosition { #[derive(Deserialize, Debug)] pub struct ProjectPanelSettings { pub git_status: bool, + pub file_icons: bool, pub dock: ProjectPanelDockPosition, pub default_width: f32, } @@ -20,6 +21,7 @@ pub struct ProjectPanelSettings { #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { pub git_status: Option, + pub file_icons: Option, pub dock: Option, pub default_width: Option, } From fd72f4526d7398df009172d786280c1572c71328 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 13 Jul 2023 20:46:24 -0700 Subject: [PATCH 033/124] Added file suffix and icon associations data --- Cargo.lock | 1 + assets/icons/file_icons/file_types.json | 107 ++++++++++++++++++ assets/icons/file_icons/quill/anchor.svg | 11 ++ assets/icons/file_icons/quill/archive.svg | 12 ++ assets/icons/file_icons/quill/book.svg | 11 ++ assets/icons/file_icons/quill/camera.svg | 12 ++ assets/icons/file_icons/quill/code.svg | 12 ++ assets/icons/file_icons/quill/database.svg | 11 ++ assets/icons/file_icons/quill/eslint.svg | 10 ++ assets/icons/file_icons/quill/file.svg | 11 ++ assets/icons/file_icons/quill/folder-open.svg | 12 ++ assets/icons/file_icons/quill/folder.svg | 11 ++ assets/icons/file_icons/quill/git.svg | 11 ++ assets/icons/file_icons/quill/hash.svg | 11 ++ assets/icons/file_icons/quill/html.svg | 12 ++ assets/icons/file_icons/quill/image.svg | 12 ++ assets/icons/file_icons/quill/info.svg | 12 ++ assets/icons/file_icons/quill/lock.svg | 11 ++ assets/icons/file_icons/quill/package.svg | 11 ++ assets/icons/file_icons/quill/prettier.svg | 22 ++++ assets/icons/file_icons/quill/settings.svg | 11 ++ assets/icons/file_icons/quill/terminal.svg | 9 ++ crates/language/src/language.rs | 13 --- crates/project/src/project.rs | 4 - crates/project_panel/Cargo.toml | 1 + crates/project_panel/src/file_associations.rs | 64 +++++++++++ crates/project_panel/src/project_panel.rs | 71 ++++++------ crates/theme/src/theme.rs | 5 +- crates/zed/src/languages/toml/config.toml | 1 - crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- styles/src/style_tree/project_panel.ts | 7 +- 32 files changed, 451 insertions(+), 62 deletions(-) create mode 100644 assets/icons/file_icons/file_types.json create mode 100644 assets/icons/file_icons/quill/anchor.svg create mode 100644 assets/icons/file_icons/quill/archive.svg create mode 100644 assets/icons/file_icons/quill/book.svg create mode 100644 assets/icons/file_icons/quill/camera.svg create mode 100644 assets/icons/file_icons/quill/code.svg create mode 100644 assets/icons/file_icons/quill/database.svg create mode 100644 assets/icons/file_icons/quill/eslint.svg create mode 100644 assets/icons/file_icons/quill/file.svg create mode 100644 assets/icons/file_icons/quill/folder-open.svg create mode 100644 assets/icons/file_icons/quill/folder.svg create mode 100644 assets/icons/file_icons/quill/git.svg create mode 100644 assets/icons/file_icons/quill/hash.svg create mode 100644 assets/icons/file_icons/quill/html.svg create mode 100644 assets/icons/file_icons/quill/image.svg create mode 100644 assets/icons/file_icons/quill/info.svg create mode 100644 assets/icons/file_icons/quill/lock.svg create mode 100644 assets/icons/file_icons/quill/package.svg create mode 100644 assets/icons/file_icons/quill/prettier.svg create mode 100644 assets/icons/file_icons/quill/settings.svg create mode 100644 assets/icons/file_icons/quill/terminal.svg create mode 100644 crates/project_panel/src/file_associations.rs diff --git a/Cargo.lock b/Cargo.lock index dca182a38f..fd99fe7ef7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5360,6 +5360,7 @@ version = "0.1.0" dependencies = [ "anyhow", "client", + "collections", "context_menu", "db", "drag_and_drop", diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json new file mode 100644 index 0000000000..8803647857 --- /dev/null +++ b/assets/icons/file_icons/file_types.json @@ -0,0 +1,107 @@ +{ + "suffixes": { + "jpg": "image", + "jpeg": "image", + "tiff": "image", + "svg": "image", + "psd": "image", + "png": "image", + "gif": "image", + "bmp": "image", + "ico": "image", + "mp4": "video", + "webm": "video", + "ogg": "video", + "mp3": "audio", + "wav": "audio", + "flac": "audio", + "aac": "audio", + "pdf": "document", + "doc": "document", + "docx": "document", + "xls": "document", + "xlsx": "document", + "ppt": "document", + "pptx": "document", + "odt": "document", + "ods": "document", + "odp": "document", + "txt": "document", + "rtf": "document", + "md": "document", + "html": "template", + "htm": "template", + "xml": "template", + "hbs": "template", + "handlebars": "template", + "js": "code", + "css": "code", + "php": "code", + "c": "code", + "cpp": "code", + "h": "code", + "hpp": "code", + "java": "code", + "py": "code", + "swift": "code", + "go": "code", + "rb": "code", + "rs": "code", + "rkt": "code", + "scm": "code", + "sql": "code", + "json": "settings", + "ini": "settings", + "yaml": "settings", + "yml": "settings", + "toml": "settings", + "conf": "settings", + "lock": "settings", + "gitignore": "vcs", + "gitattributes": "vcs", + "ps1": "terminal", + "sh": "terminal", + "bash": "terminal", + "zsh": "terminal", + "fish": "terminal", + "log": "log" + }, + "types": { + "directory": { + "icon": "icons/file_icons/quill/folder.svg" + }, + "expanded_directory": { + "icon": "icons/file_icons/quill/folder-open.svg" + }, + "image": { + "icon": "icons/file_icons/quill/image.svg" + }, + "video": { + "icon": "icons/file_icons/quill/file.svg" + }, + "audio": { + "icon": "icons/file_icons/quill/file.svg" + }, + "document": { + "icon": "icons/file_icons/quill/book.svg" + }, + "template": { + "icon": "icons/file_icons/quill/html.svg" + }, + "code": { + "icon": "icons/file_icons/quill/code.svg" + }, + "settings": { + "icon": "icons/file_icons/quill/settings.svg" + }, + "vcs": { + "icon": "icons/file_icons/quill/git.svg" + }, + "terminal": { + "icon": "icons/file_icons/quill/terminal.svg" + }, + "log": { + "icon": "icons/file_icons/quill/info.svg" + } + } +} diff --git a/assets/icons/file_icons/quill/anchor.svg b/assets/icons/file_icons/quill/anchor.svg new file mode 100644 index 0000000000..4828578ee0 --- /dev/null +++ b/assets/icons/file_icons/quill/anchor.svg @@ -0,0 +1,11 @@ + + + + anchor_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/archive.svg b/assets/icons/file_icons/quill/archive.svg new file mode 100644 index 0000000000..c78ca0cff6 --- /dev/null +++ b/assets/icons/file_icons/quill/archive.svg @@ -0,0 +1,12 @@ + + + + archive_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/book.svg b/assets/icons/file_icons/quill/book.svg new file mode 100644 index 0000000000..af918b5c61 --- /dev/null +++ b/assets/icons/file_icons/quill/book.svg @@ -0,0 +1,11 @@ + + + + book_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/camera.svg b/assets/icons/file_icons/quill/camera.svg new file mode 100644 index 0000000000..f861af607c --- /dev/null +++ b/assets/icons/file_icons/quill/camera.svg @@ -0,0 +1,12 @@ + + + + camera_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/code.svg b/assets/icons/file_icons/quill/code.svg new file mode 100644 index 0000000000..a844740f1a --- /dev/null +++ b/assets/icons/file_icons/quill/code.svg @@ -0,0 +1,12 @@ + + + + code_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/database.svg b/assets/icons/file_icons/quill/database.svg new file mode 100644 index 0000000000..8c98d5ac16 --- /dev/null +++ b/assets/icons/file_icons/quill/database.svg @@ -0,0 +1,11 @@ + + + + database_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/eslint.svg b/assets/icons/file_icons/quill/eslint.svg new file mode 100644 index 0000000000..880689293a --- /dev/null +++ b/assets/icons/file_icons/quill/eslint.svg @@ -0,0 +1,10 @@ + + + + eslint_dark + Created with Sketch. + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/file.svg b/assets/icons/file_icons/quill/file.svg new file mode 100644 index 0000000000..492c383ab6 --- /dev/null +++ b/assets/icons/file_icons/quill/file.svg @@ -0,0 +1,11 @@ + + + + file_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/folder-open.svg b/assets/icons/file_icons/quill/folder-open.svg new file mode 100644 index 0000000000..00a94c199f --- /dev/null +++ b/assets/icons/file_icons/quill/folder-open.svg @@ -0,0 +1,12 @@ + + + + folder-open_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/folder.svg b/assets/icons/file_icons/quill/folder.svg new file mode 100644 index 0000000000..9cc5b4a8c9 --- /dev/null +++ b/assets/icons/file_icons/quill/folder.svg @@ -0,0 +1,11 @@ + + + + folder_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/git.svg b/assets/icons/file_icons/quill/git.svg new file mode 100644 index 0000000000..830a7f9565 --- /dev/null +++ b/assets/icons/file_icons/quill/git.svg @@ -0,0 +1,11 @@ + + + + git_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/hash.svg b/assets/icons/file_icons/quill/hash.svg new file mode 100644 index 0000000000..36366625fe --- /dev/null +++ b/assets/icons/file_icons/quill/hash.svg @@ -0,0 +1,11 @@ + + + + hash_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/html.svg b/assets/icons/file_icons/quill/html.svg new file mode 100644 index 0000000000..7704575f24 --- /dev/null +++ b/assets/icons/file_icons/quill/html.svg @@ -0,0 +1,12 @@ + + + + html_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/image.svg b/assets/icons/file_icons/quill/image.svg new file mode 100644 index 0000000000..0ec9583edd --- /dev/null +++ b/assets/icons/file_icons/quill/image.svg @@ -0,0 +1,12 @@ + + + + image_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/info.svg b/assets/icons/file_icons/quill/info.svg new file mode 100644 index 0000000000..af3fa9d39d --- /dev/null +++ b/assets/icons/file_icons/quill/info.svg @@ -0,0 +1,12 @@ + + + + info_dark + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/lock.svg b/assets/icons/file_icons/quill/lock.svg new file mode 100644 index 0000000000..a1e36e6c12 --- /dev/null +++ b/assets/icons/file_icons/quill/lock.svg @@ -0,0 +1,11 @@ + + + + lock_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/package.svg b/assets/icons/file_icons/quill/package.svg new file mode 100644 index 0000000000..9bda169cf5 --- /dev/null +++ b/assets/icons/file_icons/quill/package.svg @@ -0,0 +1,11 @@ + + + + package_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/prettier.svg b/assets/icons/file_icons/quill/prettier.svg new file mode 100644 index 0000000000..ba7b340654 --- /dev/null +++ b/assets/icons/file_icons/quill/prettier.svg @@ -0,0 +1,22 @@ + + + + prettier_dark + Created with Sketch. + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/settings.svg b/assets/icons/file_icons/quill/settings.svg new file mode 100644 index 0000000000..f0209bf3c2 --- /dev/null +++ b/assets/icons/file_icons/quill/settings.svg @@ -0,0 +1,11 @@ + + + + settings_dark + Created with Sketch. + + + + + + \ No newline at end of file diff --git a/assets/icons/file_icons/quill/terminal.svg b/assets/icons/file_icons/quill/terminal.svg new file mode 100644 index 0000000000..964f44251f --- /dev/null +++ b/assets/icons/file_icons/quill/terminal.svg @@ -0,0 +1,9 @@ + + + + terminal_dark + Created with Sketch. + + + + \ No newline at end of file diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 60f14d9fa7..50a0b4b161 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -342,8 +342,6 @@ pub struct LanguageConfig { pub block_comment: Option<(Arc, Arc)>, #[serde(default)] pub overrides: HashMap, - #[serde(default)] - pub icon_path: Option>, } #[derive(Debug, Default)] @@ -410,7 +408,6 @@ impl Default for LanguageConfig { line_comment: Default::default(), block_comment: Default::default(), overrides: Default::default(), - icon_path: Default::default(), } } } @@ -755,16 +752,6 @@ impl LanguageRegistry { self.get_or_load_language(|config| UniCase::new(config.name.as_ref()) == name) } - pub fn icon_for_suffix(self: &Arc, suffix: &str) -> Option> { - let state = self.state.read(); - state - .available_languages - .iter() - .find(|langauge| langauge.config.path_suffixes.iter().any(|s| s == suffix)) - .map(|language| language.config.icon_path.clone()) - .flatten() - } - pub fn language_for_name_or_extension( self: &Arc, string: &str, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 80c098baa6..5bb8af3f38 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2474,10 +2474,6 @@ impl Project { }) } - pub fn icon_for_path(&self, path: &Path) -> Option> { - self.languages.icon_for_suffix(path.extension()?.to_str()?) - } - fn detect_language_for_buffer( &mut self, buffer_handle: &ModelHandle, diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 33606fccc4..4fe5372a51 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] context_menu = { path = "../context_menu" } +collections = { path = "../collections" } db = { path = "../db" } drag_and_drop = { path = "../drag_and_drop" } editor = { path = "../editor" } diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs new file mode 100644 index 0000000000..ea8e59685b --- /dev/null +++ b/crates/project_panel/src/file_associations.rs @@ -0,0 +1,64 @@ +use std::{path::Path, str, sync::Arc}; + +use collections::HashMap; + +use gpui::{AppContext, AssetSource}; +use serde_derive::Deserialize; + +#[derive(Deserialize, Debug)] +struct TypeConfig { + icon: Arc, +} + +#[derive(Deserialize, Debug)] +pub struct FileAssociations { + suffixes: HashMap, + types: HashMap, +} + +pub const TEXT_FILE_ASSET: &'static str = "icons/file_icons/quill/file.svg"; +const DIRECTORY_TYPE: &'static str = "directory"; +const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_directory"; + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + cx.set_global(FileAssociations::new(assets)) +} + +impl FileAssociations { + pub fn new(assets: impl AssetSource) -> Self { + let file = assets.load("icons/file_icons/file_types.json").unwrap(); + serde_json::from_str::(str::from_utf8(&file).unwrap()).unwrap() + } + + pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { + if !cx.has_global::() { + return None; + } + + let this = cx.global::(); + let suffix = path.extension()?.to_str()?; + + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) + } + + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { + if !cx.has_global::() { + return None; + } + + let this = cx.global::(); + + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + DIRECTORY_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + } +} diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 06c6c1540c..dd40fdd561 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,9 +1,11 @@ +mod file_associations; mod project_panel_settings; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use drag_and_drop::{DragAndDrop, Draggable}; use editor::{Cancel, Editor}; +use file_associations::{FileAssociations, TEXT_FILE_ASSET}; use futures::stream::StreamExt; use gpui::{ actions, @@ -15,8 +17,8 @@ use gpui::{ geometry::vector::Vector2F, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton, PromptLevel}, - Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, - Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity, + ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -44,7 +46,6 @@ use workspace::{ const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; -const TEXT_FILE_ASSET: &'static str = "icons/radix/file-text.svg"; pub struct ProjectPanel { project: ModelHandle, @@ -131,8 +132,9 @@ pub fn init_settings(cx: &mut AppContext) { settings::register::(cx); } -pub fn init(cx: &mut AppContext) { +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); + file_associations::init(assets, cx); cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry); cx.add_action(ProjectPanel::select_prev); @@ -1184,15 +1186,12 @@ impl ProjectPanel { let entry_range = range.start.saturating_sub(ix)..end_ix - ix; for entry in visible_worktree_entries[entry_range].iter() { let status = git_status_setting.then(|| entry.git_status).flatten(); - + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let icon = show_file_icons .then(|| match entry.kind { - EntryKind::File(_) => self - .project - .read(cx) - .icon_for_path(&entry.path) + EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx) .or_else(|| Some(TEXT_FILE_ASSET.into())), - _ => None, + _ => FileAssociations::get_folder_icon(is_expanded, cx), }) .flatten(); @@ -1208,7 +1207,7 @@ impl ProjectPanel { depth: entry.path.components().count(), kind: entry.kind, is_ignored: entry.is_ignored, - is_expanded: expanded_entry_ids.binary_search(&entry.id).is_ok(), + is_expanded, is_selected: self.selection.map_or(false, |e| { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), @@ -1271,37 +1270,35 @@ impl ProjectPanel { .unwrap_or(style.text.color); Flex::row() - .with_child(if kind.is_dir() { + .with_child(if let Some(icon) = &details.icon { + Svg::new(icon.to_string()) + .with_color(style.icon_color) + .constrained() + .with_max_width(style.icon_size) + .with_max_height(style.icon_size) + .aligned() + .constrained() + .with_width(style.icon_size) + } else if kind.is_dir() { if details.is_expanded { - Svg::new("icons/chevron_down_8.svg").with_color(style.icon_color) + Svg::new("icons/chevron_down_8.svg").with_color(style.chevron_color) } else { - Svg::new("icons/chevron_right_8.svg").with_color(style.icon_color) + Svg::new("icons/chevron_right_8.svg").with_color(style.chevron_color) } .constrained() - .with_max_width(style.directory_icon_size) - .with_max_height(style.directory_icon_size) + .with_max_width(style.chevron_size) + .with_max_height(style.chevron_size) .aligned() .constrained() - .with_width(style.directory_icon_size) + .with_width(style.chevron_size) } else { - if let Some(icon) = &details.icon { - Svg::new(icon.to_string()) - .with_color(style.icon_color) - .constrained() - .with_max_width(style.file_icon_size) - .with_max_height(style.file_icon_size) - .aligned() - .constrained() - .with_width(style.file_icon_size) - } else { - Empty::new() - .constrained() - .with_max_width(style.directory_icon_size) - .with_max_height(style.directory_icon_size) - .aligned() - .constrained() - .with_width(style.directory_icon_size) - } + Empty::new() + .constrained() + .with_max_width(style.chevron_size) + .with_max_height(style.chevron_size) + .aligned() + .constrained() + .with_width(style.chevron_size) }) .with_child(if show_editor && editor.is_some() { ChildView::new(editor.as_ref().unwrap(), cx) @@ -2613,7 +2610,7 @@ mod tests { theme::init((), cx); language::init(cx); editor::init_settings(cx); - crate::init(cx); + crate::init((), cx); workspace::init_settings(cx); Project::init_settings(cx); }); @@ -2628,7 +2625,7 @@ mod tests { language::init(cx); editor::init(cx); pane::init(cx); - crate::init(cx); + crate::init((), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); }); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 53a0629198..29c0d9ce8e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -480,9 +480,10 @@ pub struct ProjectPanelEntry { #[serde(flatten)] pub container: ContainerStyle, pub text: TextStyle, + pub icon_size: f32, pub icon_color: Color, - pub directory_icon_size: f32, - pub file_icon_size: f32, + pub chevron_color: Color, + pub chevron_size: f32, pub icon_spacing: f32, pub status: EntryStatus, } diff --git a/crates/zed/src/languages/toml/config.toml b/crates/zed/src/languages/toml/config.toml index 5a3fc9d8b8..4e89f5cabd 100644 --- a/crates/zed/src/languages/toml/config.toml +++ b/crates/zed/src/languages/toml/config.toml @@ -1,6 +1,5 @@ name = "TOML" path_suffixes = ["toml"] -icon_path = "icons/radix/gear.svg" line_comment = "# " autoclose_before = ",]}" brackets = [ diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ccf381b5b1..e85113d57d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -154,7 +154,7 @@ fn main() { file_finder::init(cx); outline::init(cx); project_symbols::init(cx); - project_panel::init(cx); + project_panel::init(Assets, cx); diagnostics::init(cx); search::init(cx); vector_store::init(fs.clone(), http.clone(), languages.clone(), cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9dffc644ae..6bbba0bd02 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2334,7 +2334,7 @@ mod tests { editor::init(cx); project_panel::init_settings(cx); pane::init(cx); - project_panel::init(cx); + project_panel::init((), cx); terminal_view::init(cx); ai::init(cx); app_state diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index c2719e935e..c3e82de8b0 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -46,9 +46,10 @@ export default function project_panel(): any { const base_properties = { height: 22, background: background(theme.middle), - icon_color: foreground(theme.middle, "variant"), - directory_icon_size: 7, - file_icon_size: 14, + chevron_color: foreground(theme.middle, "variant"), + icon_color: foreground(theme.middle, "active"), + chevron_size: 7, + icon_size: 14, icon_spacing: 5, text: text(theme.middle, "sans", "variant", { size: "sm" }), status: { From 929a9f97b28e0654ad8b9b942f34c66d2a4fd73b Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 13 Jul 2023 20:58:26 -0700 Subject: [PATCH 034/124] Fix tests --- crates/project_panel/src/file_associations.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index ea8e59685b..e1fb9b3c64 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -26,8 +26,15 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { impl FileAssociations { pub fn new(assets: impl AssetSource) -> Self { - let file = assets.load("icons/file_icons/file_types.json").unwrap(); - serde_json::from_str::(str::from_utf8(&file).unwrap()).unwrap() + assets + .load("icons/file_icons/file_types.json") + .map(|file| { + serde_json::from_str::(str::from_utf8(&file).unwrap()).unwrap() + }) + .unwrap_or_else(|_| FileAssociations { + suffixes: HashMap::default(), + types: HashMap::default(), + }) } pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { From 96ef6ab3268e9827bdb87fe6bbe81a2990385e62 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 13 Jul 2023 21:04:47 -0700 Subject: [PATCH 035/124] Add willow license --- script/generate-licenses | 9 ++++++++- script/licenses/willow_license.txt | 21 +++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 script/licenses/willow_license.txt diff --git a/script/generate-licenses b/script/generate-licenses index 14c9d4c79f..b08aa0b024 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -6,6 +6,13 @@ OUTPUT_FILE=$(pwd)/assets/licenses.md > $OUTPUT_FILE +echo -e "# ###### ICON LICENSES ######\n" >> $OUTPUT_FILE + +echo "Generating icon licenses" + +cat script/licenses/willow_license.txt >> $OUTPUT_FILE +echo -e "" >> $OUTPUT_FILE + echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE echo "Generating theme licenses" @@ -26,4 +33,4 @@ sed -i '' 's/'/'\''/g' $OUTPUT_FILE # The ` '\'' ` thing ends the string, a sed -i '' 's/=/=/g' $OUTPUT_FILE sed -i '' 's/`/`/g' $OUTPUT_FILE sed -i '' 's/<//g' $OUTPUT_FILE \ No newline at end of file +sed -i '' 's/>/>/g' $OUTPUT_FILE diff --git a/script/licenses/willow_license.txt b/script/licenses/willow_license.txt new file mode 100644 index 0000000000..56bbd45a45 --- /dev/null +++ b/script/licenses/willow_license.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Chad Donohue + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 8c855680e7813d10b31f2e8ed92d64cbdfea4b4f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 17 Jul 2023 13:09:47 -0700 Subject: [PATCH 036/124] Make file types live reload --- assets/icons/file_icons/file_types.json | 3 + crates/project_panel/src/file_associations.rs | 58 ++++++++++--------- crates/project_panel/src/project_panel.rs | 15 +++-- crates/zed/src/main.rs | 25 ++++++++ 4 files changed, 68 insertions(+), 33 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 8803647857..401e6d1686 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -67,6 +67,9 @@ "log": "log" }, "types": { + "default": { + "icon": "icons/file_icons/quill/file.svg" + }, "directory": { "icon": "icons/file_icons/quill/folder.svg" }, diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index e1fb9b3c64..2d3413ef34 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -4,6 +4,7 @@ use collections::HashMap; use gpui::{AppContext, AssetSource}; use serde_derive::Deserialize; +use util::iife; #[derive(Deserialize, Debug)] struct TypeConfig { @@ -16,9 +17,9 @@ pub struct FileAssociations { types: HashMap, } -pub const TEXT_FILE_ASSET: &'static str = "icons/file_icons/quill/file.svg"; const DIRECTORY_TYPE: &'static str = "directory"; const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_directory"; +pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; pub fn init(assets: impl AssetSource, cx: &mut AppContext) { cx.set_global(FileAssociations::new(assets)) @@ -28,8 +29,9 @@ impl FileAssociations { pub fn new(assets: impl AssetSource) -> Self { assets .load("icons/file_icons/file_types.json") - .map(|file| { - serde_json::from_str::(str::from_utf8(&file).unwrap()).unwrap() + .and_then(|file| { + serde_json::from_str::(str::from_utf8(&file).unwrap()) + .map_err(Into::into) }) .unwrap_or_else(|_| FileAssociations { suffixes: HashMap::default(), @@ -37,35 +39,37 @@ impl FileAssociations { }) } - pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { - if !cx.has_global::() { - return None; - } + pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { + iife!({ + let this = cx.has_global::().then(|| cx.global::())?; - let this = cx.global::(); - let suffix = path.extension()?.to_str()?; + iife!({ + let suffix = path.extension()?.to_str()?; - this.suffixes - .get(suffix) - .and_then(|type_str| this.types.get(type_str)) - .map(|type_config| type_config.icon.clone()) + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) + }) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + }) + .unwrap_or_else(|| Arc::from("".to_string())) } - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { - if !cx.has_global::() { - return None; - } + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { + iife!({ + let this = cx.has_global::().then(|| cx.global::())?; - let this = cx.global::(); + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + DIRECTORY_TYPE + }; - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - DIRECTORY_TYPE - }; - - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index dd40fdd561..f8e1b223d3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,11 +1,12 @@ -mod file_associations; +pub mod file_associations; mod project_panel_settings; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use drag_and_drop::{DragAndDrop, Draggable}; use editor::{Cancel, Editor}; -use file_associations::{FileAssociations, TEXT_FILE_ASSET}; +use file_associations::FileAssociations; + use futures::stream::StreamExt; use gpui::{ actions, @@ -234,6 +235,10 @@ impl ProjectPanel { }) .detach(); + cx.observe_global::(|_, cx| { + cx.notify(); + }).detach(); + let view_id = cx.view_id(); let mut this = Self { project: project.clone(), @@ -1189,11 +1194,9 @@ impl ProjectPanel { let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let icon = show_file_icons .then(|| match entry.kind { - EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx) - .or_else(|| Some(TEXT_FILE_ASSET.into())), + EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx), _ => FileAssociations::get_folder_icon(is_expanded, cx), - }) - .flatten(); + }); let mut details = EntryDetails { filename: entry diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e85113d57d..87e00a70c2 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -166,6 +166,7 @@ fn main() { cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) .detach(); + watch_file_types(fs.clone(), cx); languages.set_theme(theme::current(cx).clone()); cx.observe_global::({ @@ -685,6 +686,25 @@ async fn watch_languages(fs: Arc, languages: Arc) -> O Some(()) } +#[cfg(debug_assertions)] +fn watch_file_types(fs: Arc, cx: &mut AppContext) { + cx.spawn(|mut cx| async move { + let mut events = fs + .watch( + "assets/icons/file_icons/file_types.json".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + cx.update(|cx| { + cx.update_global(|file_types, _| { + *file_types = project_panel::file_associations::FileAssociations::new(Assets); + }); + }) + } + }).detach() +} + #[cfg(not(debug_assertions))] async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { None @@ -695,6 +715,11 @@ async fn watch_languages(_: Arc, _: Arc) -> Option<()> None } +#[cfg(not(debug_assertions))] +fn watch_file_types(fs: Arc, cx: &mut AppContext) { + None +} + fn connect_to_cli( server_name: &str, ) -> Result<(mpsc::Receiver, IpcSender)> { From aacc4bb8b00f3c17975d51ed024841e8bc48db83 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 17 Jul 2023 13:12:14 -0700 Subject: [PATCH 037/124] fmt --- crates/project_panel/src/project_panel.rs | 12 ++++++------ crates/zed/src/main.rs | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index f8e1b223d3..d97c47a339 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -237,7 +237,8 @@ impl ProjectPanel { cx.observe_global::(|_, cx| { cx.notify(); - }).detach(); + }) + .detach(); let view_id = cx.view_id(); let mut this = Self { @@ -1192,11 +1193,10 @@ impl ProjectPanel { for entry in visible_worktree_entries[entry_range].iter() { let status = git_status_setting.then(|| entry.git_status).flatten(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - let icon = show_file_icons - .then(|| match entry.kind { - EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx), - _ => FileAssociations::get_folder_icon(is_expanded, cx), - }); + let icon = show_file_icons.then(|| match entry.kind { + EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx), + _ => FileAssociations::get_folder_icon(is_expanded, cx), + }); let mut details = EntryDetails { filename: entry diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 87e00a70c2..901724ae89 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -702,7 +702,8 @@ fn watch_file_types(fs: Arc, cx: &mut AppContext) { }); }) } - }).detach() + }) + .detach() } #[cfg(not(debug_assertions))] From c754c1e9e24a8c8a7b0b0897d68eb8be367e1e2e Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Wed, 19 Jul 2023 11:28:33 -0600 Subject: [PATCH 038/124] Update icons to new zed file icon set --- assets/icons/file_icons/archive.svg | 5 + assets/icons/file_icons/book.svg | 4 + assets/icons/file_icons/camera.svg | 4 + assets/icons/file_icons/code.svg | 4 + assets/icons/file_icons/database.svg | 5 + assets/icons/file_icons/eslint.svg | 4 + assets/icons/file_icons/file.svg | 5 + assets/icons/file_icons/file_types.json | 188 ++++++++++-------- assets/icons/file_icons/folder-open.svg | 4 + assets/icons/file_icons/folder.svg | 4 + assets/icons/file_icons/git.svg | 6 + assets/icons/file_icons/hash.svg | 6 + assets/icons/file_icons/html.svg | 5 + assets/icons/file_icons/image.svg | 6 + assets/icons/file_icons/info.svg | 5 + assets/icons/file_icons/lock.svg | 6 + assets/icons/file_icons/notebook.svg | 6 + assets/icons/file_icons/package.svg | 3 + assets/icons/file_icons/prettier.svg | 12 ++ assets/icons/file_icons/quill/anchor.svg | 11 - assets/icons/file_icons/quill/archive.svg | 12 -- assets/icons/file_icons/quill/book.svg | 11 - assets/icons/file_icons/quill/camera.svg | 12 -- assets/icons/file_icons/quill/code.svg | 12 -- assets/icons/file_icons/quill/database.svg | 11 - assets/icons/file_icons/quill/eslint.svg | 10 - assets/icons/file_icons/quill/file.svg | 11 - assets/icons/file_icons/quill/folder-open.svg | 12 -- assets/icons/file_icons/quill/folder.svg | 11 - assets/icons/file_icons/quill/git.svg | 11 - assets/icons/file_icons/quill/hash.svg | 11 - assets/icons/file_icons/quill/html.svg | 12 -- assets/icons/file_icons/quill/image.svg | 12 -- assets/icons/file_icons/quill/info.svg | 12 -- assets/icons/file_icons/quill/lock.svg | 11 - assets/icons/file_icons/quill/package.svg | 11 - assets/icons/file_icons/quill/prettier.svg | 22 -- assets/icons/file_icons/quill/settings.svg | 11 - assets/icons/file_icons/quill/terminal.svg | 9 - assets/icons/file_icons/rust.svg | 4 + assets/icons/file_icons/settings.svg | 4 + assets/icons/file_icons/terminal.svg | 5 + assets/icons/file_icons/typescript.svg | 5 + script/generate-licenses | 7 - script/licenses/willow_license.txt | 21 -- styles/src/style_tree/project_panel.ts | 20 +- 46 files changed, 228 insertions(+), 355 deletions(-) create mode 100644 assets/icons/file_icons/archive.svg create mode 100644 assets/icons/file_icons/book.svg create mode 100644 assets/icons/file_icons/camera.svg create mode 100644 assets/icons/file_icons/code.svg create mode 100644 assets/icons/file_icons/database.svg create mode 100644 assets/icons/file_icons/eslint.svg create mode 100644 assets/icons/file_icons/file.svg create mode 100644 assets/icons/file_icons/folder-open.svg create mode 100644 assets/icons/file_icons/folder.svg create mode 100644 assets/icons/file_icons/git.svg create mode 100644 assets/icons/file_icons/hash.svg create mode 100644 assets/icons/file_icons/html.svg create mode 100644 assets/icons/file_icons/image.svg create mode 100644 assets/icons/file_icons/info.svg create mode 100644 assets/icons/file_icons/lock.svg create mode 100644 assets/icons/file_icons/notebook.svg create mode 100644 assets/icons/file_icons/package.svg create mode 100644 assets/icons/file_icons/prettier.svg delete mode 100644 assets/icons/file_icons/quill/anchor.svg delete mode 100644 assets/icons/file_icons/quill/archive.svg delete mode 100644 assets/icons/file_icons/quill/book.svg delete mode 100644 assets/icons/file_icons/quill/camera.svg delete mode 100644 assets/icons/file_icons/quill/code.svg delete mode 100644 assets/icons/file_icons/quill/database.svg delete mode 100644 assets/icons/file_icons/quill/eslint.svg delete mode 100644 assets/icons/file_icons/quill/file.svg delete mode 100644 assets/icons/file_icons/quill/folder-open.svg delete mode 100644 assets/icons/file_icons/quill/folder.svg delete mode 100644 assets/icons/file_icons/quill/git.svg delete mode 100644 assets/icons/file_icons/quill/hash.svg delete mode 100644 assets/icons/file_icons/quill/html.svg delete mode 100644 assets/icons/file_icons/quill/image.svg delete mode 100644 assets/icons/file_icons/quill/info.svg delete mode 100644 assets/icons/file_icons/quill/lock.svg delete mode 100644 assets/icons/file_icons/quill/package.svg delete mode 100644 assets/icons/file_icons/quill/prettier.svg delete mode 100644 assets/icons/file_icons/quill/settings.svg delete mode 100644 assets/icons/file_icons/quill/terminal.svg create mode 100644 assets/icons/file_icons/rust.svg create mode 100644 assets/icons/file_icons/settings.svg create mode 100644 assets/icons/file_icons/terminal.svg create mode 100644 assets/icons/file_icons/typescript.svg delete mode 100644 script/licenses/willow_license.txt diff --git a/assets/icons/file_icons/archive.svg b/assets/icons/file_icons/archive.svg new file mode 100644 index 0000000000..f11115cdce --- /dev/null +++ b/assets/icons/file_icons/archive.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg new file mode 100644 index 0000000000..890b8988a3 --- /dev/null +++ b/assets/icons/file_icons/book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/camera.svg b/assets/icons/file_icons/camera.svg new file mode 100644 index 0000000000..d8b9cf459c --- /dev/null +++ b/assets/icons/file_icons/camera.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg new file mode 100644 index 0000000000..2733e4b535 --- /dev/null +++ b/assets/icons/file_icons/code.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg new file mode 100644 index 0000000000..9072e091b5 --- /dev/null +++ b/assets/icons/file_icons/database.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg new file mode 100644 index 0000000000..ec5051d447 --- /dev/null +++ b/assets/icons/file_icons/eslint.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg new file mode 100644 index 0000000000..cc422734e7 --- /dev/null +++ b/assets/icons/file_icons/file.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 401e6d1686..d1b604f05e 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,110 +1,134 @@ { "suffixes": { - "jpg": "image", - "jpeg": "image", - "tiff": "image", - "svg": "image", - "psd": "image", - "png": "image", - "gif": "image", - "bmp": "image", - "ico": "image", - "mp4": "video", - "webm": "video", - "ogg": "video", - "mp3": "audio", - "wav": "audio", - "flac": "audio", "aac": "audio", - "pdf": "document", + "bash": "terminal", + "bmp": "image", + "c": "code", + "conf": "settings", + "cpp": "code", + "css": "code", "doc": "document", "docx": "document", - "xls": "document", - "xlsx": "document", - "ppt": "document", - "pptx": "document", - "odt": "document", + "eslintrc": "eslint", + "eslintrc.js": "eslint", + "eslintrc.json": "eslint", + "flac": "audio", + "fish": "terminal", + "gitattributes": "vcs", + "gitignore": "vcs", + "gif": "image", + "go": "code", + "h": "code", + "handlebars": "code", + "hbs": "template", + "htm": "template", + "html": "template", + "hpp": "code", + "ico": "image", + "ini": "settings", + "java": "code", + "jpeg": "image", + "jpg": "image", + "js": "code", + "json": "storage", + "lock": "lock", + "log": "log", + "md": "document", + "mp3": "audio", + "mp4": "video", "ods": "document", "odp": "document", - "txt": "document", - "rtf": "document", - "md": "document", - "html": "template", - "htm": "template", - "xml": "template", - "hbs": "template", - "handlebars": "template", - "js": "code", - "css": "code", + "odt": "document", + "ogg": "video", + "pdf": "document", "php": "code", - "c": "code", - "cpp": "code", - "h": "code", - "hpp": "code", - "java": "code", + "png": "image", + "ppt": "document", + "pptx": "document", + "prettierrc": "prettier", + "ps1": "terminal", + "psd": "image", "py": "code", - "swift": "code", - "go": "code", "rb": "code", - "rs": "code", "rkt": "code", + "rs": "rust", + "rtf": "document", "scm": "code", + "sh": "terminal", "sql": "code", - "json": "settings", - "ini": "settings", + "svg": "image", + "swift": "code", + "tiff": "image", + "toml": "settings", + "ts": "typescript", + "tsx": "code", + "txt": "document", + "wav": "audio", + "webm": "video", + "xls": "document", + "xlsx": "document", + "xml": "template", "yaml": "settings", "yml": "settings", - "toml": "settings", - "conf": "settings", - "lock": "settings", - "gitignore": "vcs", - "gitattributes": "vcs", - "ps1": "terminal", - "sh": "terminal", - "bash": "terminal", - "zsh": "terminal", - "fish": "terminal", - "log": "log" + "zsh": "terminal" }, "types": { - "default": { - "icon": "icons/file_icons/quill/file.svg" - }, - "directory": { - "icon": "icons/file_icons/quill/folder.svg" - }, - "expanded_directory": { - "icon": "icons/file_icons/quill/folder-open.svg" - }, - "image": { - "icon": "icons/file_icons/quill/image.svg" - }, - "video": { - "icon": "icons/file_icons/quill/file.svg" - }, "audio": { - "icon": "icons/file_icons/quill/file.svg" - }, - "document": { - "icon": "icons/file_icons/quill/book.svg" - }, - "template": { - "icon": "icons/file_icons/quill/html.svg" + "icon": "icons/file_icons/file.svg" }, "code": { - "icon": "icons/file_icons/quill/code.svg" + "icon": "icons/file_icons/code.svg" }, - "settings": { - "icon": "icons/file_icons/quill/settings.svg" + "default": { + "icon": "icons/file_icons/file.svg" }, - "vcs": { - "icon": "icons/file_icons/quill/git.svg" + "directory": { + "icon": "icons/file_icons/folder.svg" }, - "terminal": { - "icon": "icons/file_icons/quill/terminal.svg" + "document": { + "icon": "icons/file_icons/book.svg" + }, + "eslint": { + "icon": "icons/file_icons/eslint.svg" + }, + "expanded_directory": { + "icon": "icons/file_icons/folder-open.svg" + }, + "image": { + "icon": "icons/file_icons/image.svg" + }, + "lock": { + "icon": "icons/file_icons/lock.svg" }, "log": { - "icon": "icons/file_icons/quill/info.svg" + "icon": "icons/file_icons/info.svg" + }, + "prettier": { + "icon": "icons/file_icons/prettier.svg" + }, + "rust": { + "icon": "icons/file_icons/rust.svg" + }, + "settings": { + "icon": "icons/file_icons/settings.svg" + }, + "storage": { + "icon": "icons/file_icons/database.svg" + }, + "template": { + "icon": "icons/file_icons/html.svg" + }, + "terminal": { + "icon": "icons/file_icons/terminal.svg" + }, + "typescript": { + "icon": "icons/file_icons/typescript.svg" + }, + "vcs": { + "icon": "icons/file_icons/git.svg" + }, + "video": { + "icon": "icons/file_icons/file.svg" } } } diff --git a/assets/icons/file_icons/folder-open.svg b/assets/icons/file_icons/folder-open.svg new file mode 100644 index 0000000000..65c5744049 --- /dev/null +++ b/assets/icons/file_icons/folder-open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg new file mode 100644 index 0000000000..5157bae839 --- /dev/null +++ b/assets/icons/file_icons/folder.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg new file mode 100644 index 0000000000..82d8c8f57c --- /dev/null +++ b/assets/icons/file_icons/git.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/hash.svg b/assets/icons/file_icons/hash.svg new file mode 100644 index 0000000000..edd0462678 --- /dev/null +++ b/assets/icons/file_icons/hash.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/html.svg b/assets/icons/file_icons/html.svg new file mode 100644 index 0000000000..ba9ec14299 --- /dev/null +++ b/assets/icons/file_icons/html.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg new file mode 100644 index 0000000000..ee5e49f2d4 --- /dev/null +++ b/assets/icons/file_icons/image.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/info.svg b/assets/icons/file_icons/info.svg new file mode 100644 index 0000000000..e84ae7c628 --- /dev/null +++ b/assets/icons/file_icons/info.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg new file mode 100644 index 0000000000..3051bbf801 --- /dev/null +++ b/assets/icons/file_icons/lock.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg new file mode 100644 index 0000000000..6eaec16d0a --- /dev/null +++ b/assets/icons/file_icons/notebook.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg new file mode 100644 index 0000000000..2a692ba4b4 --- /dev/null +++ b/assets/icons/file_icons/package.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg new file mode 100644 index 0000000000..2d2c6ee719 --- /dev/null +++ b/assets/icons/file_icons/prettier.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/file_icons/quill/anchor.svg b/assets/icons/file_icons/quill/anchor.svg deleted file mode 100644 index 4828578ee0..0000000000 --- a/assets/icons/file_icons/quill/anchor.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - anchor_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/archive.svg b/assets/icons/file_icons/quill/archive.svg deleted file mode 100644 index c78ca0cff6..0000000000 --- a/assets/icons/file_icons/quill/archive.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - archive_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/book.svg b/assets/icons/file_icons/quill/book.svg deleted file mode 100644 index af918b5c61..0000000000 --- a/assets/icons/file_icons/quill/book.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - book_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/camera.svg b/assets/icons/file_icons/quill/camera.svg deleted file mode 100644 index f861af607c..0000000000 --- a/assets/icons/file_icons/quill/camera.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - camera_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/code.svg b/assets/icons/file_icons/quill/code.svg deleted file mode 100644 index a844740f1a..0000000000 --- a/assets/icons/file_icons/quill/code.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - code_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/database.svg b/assets/icons/file_icons/quill/database.svg deleted file mode 100644 index 8c98d5ac16..0000000000 --- a/assets/icons/file_icons/quill/database.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - database_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/eslint.svg b/assets/icons/file_icons/quill/eslint.svg deleted file mode 100644 index 880689293a..0000000000 --- a/assets/icons/file_icons/quill/eslint.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - eslint_dark - Created with Sketch. - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/file.svg b/assets/icons/file_icons/quill/file.svg deleted file mode 100644 index 492c383ab6..0000000000 --- a/assets/icons/file_icons/quill/file.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - file_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/folder-open.svg b/assets/icons/file_icons/quill/folder-open.svg deleted file mode 100644 index 00a94c199f..0000000000 --- a/assets/icons/file_icons/quill/folder-open.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - folder-open_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/folder.svg b/assets/icons/file_icons/quill/folder.svg deleted file mode 100644 index 9cc5b4a8c9..0000000000 --- a/assets/icons/file_icons/quill/folder.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - folder_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/git.svg b/assets/icons/file_icons/quill/git.svg deleted file mode 100644 index 830a7f9565..0000000000 --- a/assets/icons/file_icons/quill/git.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - git_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/hash.svg b/assets/icons/file_icons/quill/hash.svg deleted file mode 100644 index 36366625fe..0000000000 --- a/assets/icons/file_icons/quill/hash.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - hash_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/html.svg b/assets/icons/file_icons/quill/html.svg deleted file mode 100644 index 7704575f24..0000000000 --- a/assets/icons/file_icons/quill/html.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - html_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/image.svg b/assets/icons/file_icons/quill/image.svg deleted file mode 100644 index 0ec9583edd..0000000000 --- a/assets/icons/file_icons/quill/image.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - image_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/info.svg b/assets/icons/file_icons/quill/info.svg deleted file mode 100644 index af3fa9d39d..0000000000 --- a/assets/icons/file_icons/quill/info.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - info_dark - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/lock.svg b/assets/icons/file_icons/quill/lock.svg deleted file mode 100644 index a1e36e6c12..0000000000 --- a/assets/icons/file_icons/quill/lock.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - lock_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/package.svg b/assets/icons/file_icons/quill/package.svg deleted file mode 100644 index 9bda169cf5..0000000000 --- a/assets/icons/file_icons/quill/package.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - package_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/prettier.svg b/assets/icons/file_icons/quill/prettier.svg deleted file mode 100644 index ba7b340654..0000000000 --- a/assets/icons/file_icons/quill/prettier.svg +++ /dev/null @@ -1,22 +0,0 @@ - - - - prettier_dark - Created with Sketch. - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/settings.svg b/assets/icons/file_icons/quill/settings.svg deleted file mode 100644 index f0209bf3c2..0000000000 --- a/assets/icons/file_icons/quill/settings.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - settings_dark - Created with Sketch. - - - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/quill/terminal.svg b/assets/icons/file_icons/quill/terminal.svg deleted file mode 100644 index 964f44251f..0000000000 --- a/assets/icons/file_icons/quill/terminal.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - terminal_dark - Created with Sketch. - - - - \ No newline at end of file diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg new file mode 100644 index 0000000000..1802f0e190 --- /dev/null +++ b/assets/icons/file_icons/rust.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg new file mode 100644 index 0000000000..e827055d19 --- /dev/null +++ b/assets/icons/file_icons/settings.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/file_icons/terminal.svg b/assets/icons/file_icons/terminal.svg new file mode 100644 index 0000000000..939587c53e --- /dev/null +++ b/assets/icons/file_icons/terminal.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/typescript.svg b/assets/icons/file_icons/typescript.svg new file mode 100644 index 0000000000..179b3d8572 --- /dev/null +++ b/assets/icons/file_icons/typescript.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/script/generate-licenses b/script/generate-licenses index b08aa0b024..9a2fe8921a 100755 --- a/script/generate-licenses +++ b/script/generate-licenses @@ -6,13 +6,6 @@ OUTPUT_FILE=$(pwd)/assets/licenses.md > $OUTPUT_FILE -echo -e "# ###### ICON LICENSES ######\n" >> $OUTPUT_FILE - -echo "Generating icon licenses" - -cat script/licenses/willow_license.txt >> $OUTPUT_FILE -echo -e "" >> $OUTPUT_FILE - echo -e "# ###### THEME LICENSES ######\n" >> $OUTPUT_FILE echo "Generating theme licenses" diff --git a/script/licenses/willow_license.txt b/script/licenses/willow_license.txt deleted file mode 100644 index 56bbd45a45..0000000000 --- a/script/licenses/willow_license.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2019 Chad Donohue - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index c3e82de8b0..4a252572d5 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -50,7 +50,7 @@ export default function project_panel(): any { icon_color: foreground(theme.middle, "active"), chevron_size: 7, icon_size: 14, - icon_spacing: 5, + icon_spacing: 6, text: text(theme.middle, "sans", "variant", { size: "sm" }), status: { ...git_status, @@ -64,17 +64,17 @@ export default function project_panel(): any { const unselected_default_style = merge( base_properties, unselected?.default ?? {}, - {} + {}, ) const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {} + unselected?.hovered ?? {}, ) const unselected_clicked_style = merge( base_properties, { background: background(theme.middle, "pressed") }, - unselected?.clicked ?? {} + unselected?.clicked ?? {}, ) const selected_default_style = merge( base_properties, @@ -82,7 +82,7 @@ export default function project_panel(): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {} + selected_style?.default ?? {}, ) const selected_hovered_style = merge( base_properties, @@ -90,7 +90,7 @@ export default function project_panel(): any { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.hovered ?? {} + selected_style?.hovered ?? {}, ) const selected_clicked_style = merge( base_properties, @@ -98,7 +98,7 @@ export default function project_panel(): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {} + selected_style?.clicked ?? {}, ) return toggleable({ @@ -157,7 +157,7 @@ export default function project_panel(): any { }), background: background(theme.middle), padding: { left: 6, right: 6, top: 0, bottom: 6 }, - indent_width: 12, + indent_width: 20, entry: default_entry, dragged_entry: { ...default_entry.inactive.default, @@ -175,7 +175,7 @@ export default function project_panel(): any { default: { icon_color: foreground(theme.middle, "variant"), }, - } + }, ), cut_entry: entry( { @@ -190,7 +190,7 @@ export default function project_panel(): any { size: "sm", }), }, - } + }, ), filename_editor: { background: background(theme.middle, "on"), From f4413b0969b26361a822754f8e00aea9f3fd1229 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Wed, 19 Jul 2023 12:07:00 -0600 Subject: [PATCH 039/124] =?UTF-8?q?Fix=20files=20that=20don=E2=80=99t=20ha?= =?UTF-8?q?ve=20a=20prefix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/icons/file_icons/file_types.json | 1 + crates/project_panel/src/file_associations.rs | 9 ++++++++- styles/src/style_tree/project_panel.ts | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index d1b604f05e..b1da28d2d6 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -46,6 +46,7 @@ "ppt": "document", "pptx": "document", "prettierrc": "prettier", + "prettierignore": "prettier", "ps1": "terminal", "psd": "image", "py": "code", diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 2d3413ef34..e692031704 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -44,7 +44,14 @@ impl FileAssociations { let this = cx.has_global::().then(|| cx.global::())?; iife!({ - let suffix = path.extension()?.to_str()?; + let suffix = + path.file_name() + .and_then(|os_str| os_str.to_str()) + .and_then(|file_name| { + file_name + .find('.') + .and_then(|dot_index| file_name.get(dot_index + 1..)) + } )?; this.suffixes .get(suffix) diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index 4a252572d5..e239f9a840 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -47,7 +47,7 @@ export default function project_panel(): any { height: 22, background: background(theme.middle), chevron_color: foreground(theme.middle, "variant"), - icon_color: foreground(theme.middle, "active"), + icon_color: with_opacity(foreground(theme.middle, "active"), 0.3), chevron_size: 7, icon_size: 14, icon_spacing: 6, From e3f9a01f6b22f82f065b9a583854c58426d60ef5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 11:10:01 -0700 Subject: [PATCH 040/124] fmt --- crates/project_panel/src/file_associations.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index e692031704..3aaa1689e0 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -44,14 +44,14 @@ impl FileAssociations { let this = cx.has_global::().then(|| cx.global::())?; iife!({ - let suffix = - path.file_name() - .and_then(|os_str| os_str.to_str()) - .and_then(|file_name| { - file_name - .find('.') - .and_then(|dot_index| file_name.get(dot_index + 1..)) - } )?; + let suffix = path + .file_name() + .and_then(|os_str| os_str.to_str()) + .and_then(|file_name| { + file_name + .find('.') + .and_then(|dot_index| file_name.get(dot_index + 1..)) + })?; this.suffixes .get(suffix) From 9c9ce15afc80ebb125b5b0ab8b92fe74491c890f Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 11:14:31 -0700 Subject: [PATCH 041/124] Add a few more spare associations --- assets/icons/file_icons/file_types.json | 14 ++++++++++++++ crates/project_panel/src/file_associations.rs | 2 ++ 2 files changed, 16 insertions(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index b1da28d2d6..4f3f8160d7 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -6,6 +6,7 @@ "c": "code", "conf": "settings", "cpp": "code", + "cc": "code", "css": "code", "doc": "document", "docx": "document", @@ -23,6 +24,7 @@ "hbs": "template", "htm": "template", "html": "template", + "svelte": "template", "hpp": "code", "ico": "image", "ini": "settings", @@ -34,6 +36,7 @@ "lock": "lock", "log": "log", "md": "document", + "mdx": "document", "mp3": "audio", "mp4": "video", "ods": "document", @@ -56,6 +59,17 @@ "rtf": "document", "scm": "code", "sh": "terminal", + "bashrc": "terminal", + "bash_profile": "terminal", + "bash_aliases": "terminal", + "bash_logout": "terminal", + "profile": "terminal", + "zshrc": "terminal", + "zshenv": "terminal", + "zsh_profile": "terminal", + "zsh_aliases": "terminal", + "zsh_histfile": "terminal", + "zlogin": "terminal", "sql": "code", "svg": "image", "swift": "code", diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 3aaa1689e0..6e2e373d76 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -43,6 +43,8 @@ impl FileAssociations { iife!({ let this = cx.has_global::().then(|| cx.global::())?; + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations iife!({ let suffix = path .file_name() From 64d134a0dc1d7ba220591d83b7fa8847b5599df9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 19 Jul 2023 15:32:41 -0400 Subject: [PATCH 042/124] Update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 3ce94d08b9..5a3161c888 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6470,7 +6470,7 @@ name = "search" version = "0.1.0" dependencies = [ "anyhow", - "bitflags 1.3.2", + "bitflags", "client", "collections", "editor", From 34488ca8631c47c6a65402761ab084b8bba69a03 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 19 Jul 2023 15:33:58 -0400 Subject: [PATCH 043/124] v0.97.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5a3161c888..e2e038fb53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9450,7 +9450,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.96.0" +version = "0.97.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index f749fb6e68..c5bf313701 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.96.0" +version = "0.97.0" publish = false [lib] From 5ceb258b3e9f346909336a75a12479bef786edb0 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 12:34:24 -0700 Subject: [PATCH 044/124] Mute mics by default Fix bug when file ends in line with 1 more digit displayed than previous lines Remove stale UI elements from voice call development --- Cargo.lock | 6 ++- assets/settings/default.json | 5 ++ crates/call/Cargo.toml | 4 ++ crates/call/src/call.rs | 4 ++ crates/call/src/call_settings.rs | 27 +++++++++++ crates/call/src/room.rs | 41 +++++++++++++---- crates/collab_ui/src/collab_titlebar_item.rs | 8 ++-- crates/collab_ui/src/collab_ui.rs | 16 +------ crates/editor/src/element.rs | 2 +- .../Sources/LiveKitBridge/LiveKitBridge.swift | 46 +++++++++++++------ crates/live_kit_client/examples/test_app.rs | 2 +- crates/live_kit_client/src/prod.rs | 23 ++++++++-- crates/live_kit_client/src/test.rs | 14 +++++- 13 files changed, 147 insertions(+), 51 deletions(-) create mode 100644 crates/call/src/call_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 3ce94d08b9..91b165ca68 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1052,6 +1052,10 @@ dependencies = [ "media", "postage", "project", + "schemars", + "serde", + "serde_derive", + "serde_json", "settings", "util", ] @@ -6470,7 +6474,7 @@ name = "search" version = "0.1.0" dependencies = [ "anyhow", - "bitflags 1.3.2", + "bitflags", "client", "collections", "editor", diff --git a/assets/settings/default.json b/assets/settings/default.json index e1f2d93270..d35049a84d 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -66,6 +66,11 @@ // 3. Draw all invisible symbols: // "all" "show_whitespaces": "selection", + // Settings related to calls in Zed + "calls": { + // Join calls with the microphone muted by default + "mute_on_join": true + }, // Scrollbar related settings "scrollbar": { // When to show the scrollbar in the editor. diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index 61f3593247..eb448d8d8d 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -36,6 +36,10 @@ anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true postage.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +serde_derive.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index cf6dd1799c..1ad7dbc1fc 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,9 +1,11 @@ pub mod participant; pub mod room; +pub mod call_settings; use std::sync::Arc; use anyhow::{anyhow, Result}; +use call_settings::CallSettings; use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; @@ -19,6 +21,8 @@ pub use participant::ParticipantLocation; pub use room::Room; pub fn init(client: Arc, user_store: ModelHandle, cx: &mut AppContext) { + settings::register::(cx); + let active_call = cx.add_model(|cx| ActiveCall::new(client, user_store, cx)); cx.set_global(active_call); } diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs new file mode 100644 index 0000000000..356ae3ef19 --- /dev/null +++ b/crates/call/src/call_settings.rs @@ -0,0 +1,27 @@ +use schemars::JsonSchema; +use serde_derive::{Serialize, Deserialize}; +use settings::Setting; + +#[derive(Deserialize, Debug)] +pub struct CallSettings { + pub mute_on_join: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct CallSettingsContent { + pub mute_on_join: Option, +} + +impl Setting for CallSettings { + const KEY: Option<&'static str> = Some("calls"); + + type FileContent = CallSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 87e6faf988..d57d4f711b 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -1,4 +1,5 @@ use crate::{ + call_settings::CallSettings, participant::{LocalParticipant, ParticipantLocation, RemoteParticipant, RemoteVideoTrack}, IncomingCall, }; @@ -19,7 +20,7 @@ use live_kit_client::{ }; use postage::stream::Stream; use project::Project; -use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; +use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration, panic::Location}; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -153,8 +154,10 @@ impl Room { cx.spawn(|this, mut cx| async move { connect.await?; - this.update(&mut cx, |this, cx| this.share_microphone(cx)) - .await?; + if !cx.read(|cx| settings::get::(cx).mute_on_join) { + this.update(&mut cx, |this, cx| this.share_microphone(cx)) + .await?; + } anyhow::Ok(()) }) @@ -656,7 +659,7 @@ impl Room { peer_id, projects: participant.projects, location, - muted: false, + muted: true, speaking: false, video_tracks: Default::default(), audio_tracks: Default::default(), @@ -670,6 +673,10 @@ impl Room { live_kit.room.remote_video_tracks(&user.id.to_string()); let audio_tracks = live_kit.room.remote_audio_tracks(&user.id.to_string()); + let publications = live_kit + .room + .remote_audio_track_publications(&user.id.to_string()); + for track in video_tracks { this.remote_video_track_updated( RemoteVideoTrackUpdate::Subscribed(track), @@ -677,9 +684,15 @@ impl Room { ) .log_err(); } - for track in audio_tracks { + + for (track, publication) in + audio_tracks.iter().zip(publications.iter()) + { this.remote_audio_track_updated( - RemoteAudioTrackUpdate::Subscribed(track), + RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + ), cx, ) .log_err(); @@ -819,8 +832,8 @@ impl Room { cx.notify(); } RemoteAudioTrackUpdate::MuteChanged { track_id, muted } => { + let mut found = false; for participant in &mut self.remote_participants.values_mut() { - let mut found = false; for track in participant.audio_tracks.values() { if track.sid() == track_id { found = true; @@ -832,16 +845,20 @@ impl Room { break; } } + cx.notify(); } - RemoteAudioTrackUpdate::Subscribed(track) => { + RemoteAudioTrackUpdate::Subscribed(track, publication) => { let user_id = track.publisher_id().parse()?; let track_id = track.sid().to_string(); let participant = self .remote_participants .get_mut(&user_id) .ok_or_else(|| anyhow!("subscribed to track by unknown participant"))?; + participant.audio_tracks.insert(track_id.clone(), track); + participant.muted = publication.is_muted(); + cx.emit(Event::RemoteAudioTracksChanged { participant_id: participant.peer_id, }); @@ -1053,7 +1070,7 @@ impl Room { self.live_kit .as_ref() .and_then(|live_kit| match &live_kit.microphone_track { - LocalTrack::None => None, + LocalTrack::None => Some(true), LocalTrack::Pending { muted, .. } => Some(*muted), LocalTrack::Published { muted, .. } => Some(*muted), }) @@ -1070,7 +1087,9 @@ impl Room { self.live_kit.as_ref().map(|live_kit| live_kit.deafened) } + #[track_caller] pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { + dbg!(Location::caller()); if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } else if self.is_sharing_mic() { @@ -1244,6 +1263,10 @@ impl Room { pub fn toggle_mute(&mut self, cx: &mut ModelContext) -> Result>> { let should_mute = !self.is_muted(); if let Some(live_kit) = self.live_kit.as_mut() { + if matches!(live_kit.microphone_track, LocalTrack::None) { + return Ok(self.share_microphone(cx)); + } + let (ret_task, old_muted) = live_kit.set_mute(should_mute, cx)?; live_kit.muted_by_user = should_mute; diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 6cfc9d8e30..ce8d10d655 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -652,10 +652,10 @@ impl CollabTitlebarItem { let is_muted = room.read(cx).is_muted(); if is_muted { icon = "icons/radix/mic-mute.svg"; - tooltip = "Unmute microphone\nRight click for options"; + tooltip = "Unmute microphone"; } else { icon = "icons/radix/mic.svg"; - tooltip = "Mute microphone\nRight click for options"; + tooltip = "Mute microphone"; } let titlebar = &theme.titlebar; @@ -705,10 +705,10 @@ impl CollabTitlebarItem { let is_deafened = room.read(cx).is_deafened().unwrap_or(false); if is_deafened { icon = "icons/radix/speaker-off.svg"; - tooltip = "Unmute speakers\nRight click for options"; + tooltip = "Unmute speakers"; } else { icon = "icons/radix/speaker-loud.svg"; - tooltip = "Mute speakers\nRight click for options"; + tooltip = "Mute speakers"; } let titlebar = &theme.titlebar; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 7608fdbfee..dbdeb45573 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -18,13 +18,7 @@ use workspace::AppState; actions!( collab, - [ - ToggleScreenSharing, - ToggleMute, - ToggleDeafen, - LeaveCall, - ShareMicrophone - ] + [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] ); pub fn init(app_state: &Arc, cx: &mut AppContext) { @@ -40,7 +34,6 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { cx.add_global_action(toggle_screen_sharing); cx.add_global_action(toggle_mute); cx.add_global_action(toggle_deafen); - cx.add_global_action(share_microphone); } pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { @@ -85,10 +78,3 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) { .log_err(); } } - -pub fn share_microphone(_: &ShareMicrophone, cx: &mut AppContext) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, Room::share_microphone) - .detach_and_log_err(cx) - } -} diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4f4aa7477d..4962b08db2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1311,7 +1311,7 @@ impl EditorElement { } fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { - let digit_count = (snapshot.max_buffer_row() as f32).log10().floor() as usize + 1; + let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; let style = &self.style; cx.text_layout_cache() diff --git a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift index 40d3641db2..5f22acf581 100644 --- a/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift +++ b/crates/live_kit_client/LiveKitBridge/Sources/LiveKitBridge/LiveKitBridge.swift @@ -6,7 +6,7 @@ import ScreenCaptureKit class LKRoomDelegate: RoomDelegate { var data: UnsafeRawPointer var onDidDisconnect: @convention(c) (UnsafeRawPointer) -> Void - var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void + var onDidSubscribeToRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void var onDidUnsubscribeFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void var onMuteChangedFromRemoteAudioTrack: @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void var onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void @@ -16,7 +16,7 @@ class LKRoomDelegate: RoomDelegate { init( data: UnsafeRawPointer, onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, - onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onActiveSpeakersChanged: @convention(c) (UnsafeRawPointer, CFArray) -> Void, @@ -43,7 +43,7 @@ class LKRoomDelegate: RoomDelegate { if track.kind == .video { self.onDidSubscribeToRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) } else if track.kind == .audio { - self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque()) + self.onDidSubscribeToRemoteAudioTrack(self.data, participant.identity as CFString, track.sid! as CFString, Unmanaged.passUnretained(track).toOpaque(), Unmanaged.passUnretained(publication).toOpaque()) } } @@ -52,12 +52,12 @@ class LKRoomDelegate: RoomDelegate { self.onMuteChangedFromRemoteAudioTrack(self.data, publication.sid as CFString, muted) } } - + func room(_ room: Room, didUpdate speakers: [Participant]) { guard let speaker_ids = speakers.compactMap({ $0.identity as CFString }) as CFArray? else { return } self.onActiveSpeakersChanged(self.data, speaker_ids) } - + func room(_ room: Room, participant: RemoteParticipant, didUnsubscribe publication: RemoteTrackPublication, track: Track) { if track.kind == .video { self.onDidUnsubscribeFromRemoteVideoTrack(self.data, participant.identity as CFString, track.sid! as CFString) @@ -104,7 +104,7 @@ class LKVideoRenderer: NSObject, VideoRenderer { public func LKRoomDelegateCreate( data: UnsafeRawPointer, onDidDisconnect: @escaping @convention(c) (UnsafeRawPointer) -> Void, - onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer) -> Void, + onDidSubscribeToRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString, UnsafeRawPointer, UnsafeRawPointer) -> Void, onDidUnsubscribeFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, CFString) -> Void, onMuteChangedFromRemoteAudioTrack: @escaping @convention(c) (UnsafeRawPointer, CFString, Bool) -> Void, onActiveSpeakerChanged: @escaping @convention(c) (UnsafeRawPointer, CFArray) -> Void, @@ -180,39 +180,39 @@ public func LKRoomUnpublishTrack(room: UnsafeRawPointer, publication: UnsafeRawP @_cdecl("LKRoomAudioTracksForRemoteParticipant") public func LKRoomAudioTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { let room = Unmanaged.fromOpaque(room).takeUnretainedValue() - + for (_, participant) in room.remoteParticipants { if participant.identity == participantId as String { return participant.audioTracks.compactMap { $0.track as? RemoteAudioTrack } as CFArray? } } - + return nil; } @_cdecl("LKRoomAudioTrackPublicationsForRemoteParticipant") public func LKRoomAudioTrackPublicationsForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { let room = Unmanaged.fromOpaque(room).takeUnretainedValue() - + for (_, participant) in room.remoteParticipants { if participant.identity == participantId as String { return participant.audioTracks.compactMap { $0 as? RemoteTrackPublication } as CFArray? } } - + return nil; } @_cdecl("LKRoomVideoTracksForRemoteParticipant") public func LKRoomVideoTracksForRemoteParticipant(room: UnsafeRawPointer, participantId: CFString) -> CFArray? { let room = Unmanaged.fromOpaque(room).takeUnretainedValue() - + for (_, participant) in room.remoteParticipants { if participant.identity == participantId as String { return participant.videoTracks.compactMap { $0.track as? RemoteVideoTrack } as CFArray? } } - + return nil; } @@ -222,7 +222,7 @@ public func LKLocalAudioTrackCreateTrack() -> UnsafeMutableRawPointer { echoCancellation: true, noiseSuppression: true )) - + return Unmanaged.passRetained(track).toOpaque() } @@ -276,7 +276,7 @@ public func LKLocalTrackPublicationSetMute( callback_data: UnsafeRawPointer ) { let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() - + if muted { publication.mute().then { on_complete(callback_data, nil) @@ -307,3 +307,21 @@ public func LKRemoteTrackPublicationSetEnabled( on_complete(callback_data, error.localizedDescription as CFString) } } + +@_cdecl("LKRemoteTrackPublicationIsMuted") +public func LKRemoteTrackPublicationIsMuted( + publication: UnsafeRawPointer +) -> Bool { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + return publication.muted +} + +@_cdecl("LKRemoteTrackPublicationGetSid") +public func LKRemoteTrackPublicationGetSid( + publication: UnsafeRawPointer +) -> CFString { + let publication = Unmanaged.fromOpaque(publication).takeUnretainedValue() + + return publication.sid as CFString +} diff --git a/crates/live_kit_client/examples/test_app.rs b/crates/live_kit_client/examples/test_app.rs index f5f6d0e46f..f2169d7f30 100644 --- a/crates/live_kit_client/examples/test_app.rs +++ b/crates/live_kit_client/examples/test_app.rs @@ -63,7 +63,7 @@ fn main() { let audio_track = LocalAudioTrack::create(); let audio_track_publication = room_a.publish_audio_track(&audio_track).await.unwrap(); - if let RemoteAudioTrackUpdate::Subscribed(track) = + if let RemoteAudioTrackUpdate::Subscribed(track, _) = audio_track_updates.next().await.unwrap() { let remote_tracks = room_b.remote_audio_tracks("test-participant-1"); diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index 6daa0601ca..2b5148e4a3 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -26,6 +26,7 @@ extern "C" { publisher_id: CFStringRef, track_id: CFStringRef, remote_track: *const c_void, + remote_publication: *const c_void, ), on_did_unsubscribe_from_remote_audio_track: extern "C" fn( callback_data: *mut c_void, @@ -125,6 +126,9 @@ extern "C" { on_complete: extern "C" fn(callback_data: *mut c_void, error: CFStringRef), callback_data: *mut c_void, ); + + fn LKRemoteTrackPublicationIsMuted(publication: *const c_void) -> bool; + fn LKRemoteTrackPublicationGetSid(publication: *const c_void) -> CFStringRef; } pub type Sid = String; @@ -372,10 +376,11 @@ impl Room { rx } - fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack) { + fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack, publication: RemoteTrackPublication) { let track = Arc::new(track); + let publication = Arc::new(publication); self.remote_audio_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone())) + tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone(), publication.clone())) .is_ok() }); } @@ -501,13 +506,15 @@ impl RoomDelegate { publisher_id: CFStringRef, track_id: CFStringRef, track: *const c_void, + publication: *const c_void, ) { let room = unsafe { Weak::from_raw(room as *mut Room) }; let publisher_id = unsafe { CFString::wrap_under_get_rule(publisher_id).to_string() }; let track_id = unsafe { CFString::wrap_under_get_rule(track_id).to_string() }; let track = RemoteAudioTrack::new(track, track_id, publisher_id); + let publication = RemoteTrackPublication::new(publication); if let Some(room) = room.upgrade() { - room.did_subscribe_to_remote_audio_track(track); + room.did_subscribe_to_remote_audio_track(track, publication); } let _ = Weak::into_raw(room); } @@ -682,6 +689,14 @@ impl RemoteTrackPublication { Self(native_track_publication) } + pub fn sid(&self) -> String { + unsafe { CFString::wrap_under_get_rule(LKRemoteTrackPublicationGetSid(self.0)).to_string() } + } + + pub fn is_muted(&self) -> bool { + unsafe { LKRemoteTrackPublicationIsMuted(self.0) } + } + pub fn set_enabled(&self, enabled: bool) -> impl Future> { let (tx, rx) = futures::channel::oneshot::channel(); @@ -832,7 +847,7 @@ pub enum RemoteVideoTrackUpdate { pub enum RemoteAudioTrackUpdate { ActiveSpeakersChanged { speakers: Vec }, MuteChanged { track_id: Sid, muted: bool }, - Subscribed(Arc), + Subscribed(Arc, Arc), Unsubscribed { publisher_id: Sid, track_id: Sid }, } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index ada864fc44..3b91e0ef89 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -216,6 +216,8 @@ impl TestServer { publisher_id: identity.clone(), }); + let publication = Arc::new(RemoteTrackPublication); + room.audio_tracks.push(track.clone()); for (id, client_room) in &room.client_rooms { @@ -225,7 +227,7 @@ impl TestServer { .lock() .audio_track_updates .0 - .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone())) + .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone(), publication.clone())) .unwrap(); } } @@ -501,6 +503,14 @@ impl RemoteTrackPublication { pub fn set_enabled(&self, _enabled: bool) -> impl Future> { async { Ok(()) } } + + pub fn is_muted(&self) -> bool { + false + } + + pub fn sid(&self) -> String { + "".to_string() + } } #[derive(Clone)] @@ -579,7 +589,7 @@ pub enum RemoteVideoTrackUpdate { pub enum RemoteAudioTrackUpdate { ActiveSpeakersChanged { speakers: Vec }, MuteChanged { track_id: Sid, muted: bool }, - Subscribed(Arc), + Subscribed(Arc, Arc), Unsubscribed { publisher_id: Sid, track_id: Sid }, } From 1e4bddd276fba6227d5e4cde8218ca2c8c86d2f5 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 12:34:56 -0700 Subject: [PATCH 045/124] fmt --- crates/call/src/call.rs | 2 +- crates/call/src/call_settings.rs | 2 +- crates/call/src/room.rs | 2 +- crates/live_kit_client/src/prod.rs | 13 ++++++++++--- crates/live_kit_client/src/test.rs | 5 ++++- 5 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 1ad7dbc1fc..3fc76b964d 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -1,6 +1,6 @@ +pub mod call_settings; pub mod participant; pub mod room; -pub mod call_settings; use std::sync::Arc; diff --git a/crates/call/src/call_settings.rs b/crates/call/src/call_settings.rs index 356ae3ef19..2808a99617 100644 --- a/crates/call/src/call_settings.rs +++ b/crates/call/src/call_settings.rs @@ -1,5 +1,5 @@ use schemars::JsonSchema; -use serde_derive::{Serialize, Deserialize}; +use serde_derive::{Deserialize, Serialize}; use settings::Setting; #[derive(Deserialize, Debug)] diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index d57d4f711b..08ac8befc4 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -20,7 +20,7 @@ use live_kit_client::{ }; use postage::stream::Stream; use project::Project; -use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration, panic::Location}; +use std::{future::Future, mem, panic::Location, pin::Pin, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); diff --git a/crates/live_kit_client/src/prod.rs b/crates/live_kit_client/src/prod.rs index 2b5148e4a3..d8d0277440 100644 --- a/crates/live_kit_client/src/prod.rs +++ b/crates/live_kit_client/src/prod.rs @@ -376,12 +376,19 @@ impl Room { rx } - fn did_subscribe_to_remote_audio_track(&self, track: RemoteAudioTrack, publication: RemoteTrackPublication) { + fn did_subscribe_to_remote_audio_track( + &self, + track: RemoteAudioTrack, + publication: RemoteTrackPublication, + ) { let track = Arc::new(track); let publication = Arc::new(publication); self.remote_audio_track_subscribers.lock().retain(|tx| { - tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed(track.clone(), publication.clone())) - .is_ok() + tx.unbounded_send(RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + )) + .is_ok() }); } diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 3b91e0ef89..704760bab7 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -227,7 +227,10 @@ impl TestServer { .lock() .audio_track_updates .0 - .try_broadcast(RemoteAudioTrackUpdate::Subscribed(track.clone(), publication.clone())) + .try_broadcast(RemoteAudioTrackUpdate::Subscribed( + track.clone(), + publication.clone(), + )) .unwrap(); } } From 2e3aa703d9a10b8c037aef63024517ad03bde64b Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 19 Jul 2023 15:43:45 -0400 Subject: [PATCH 046/124] In macOS platform layer map a ctrl-click to a right click --- crates/gpui/src/platform/mac/window.rs | 46 ++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 381a4fbaaa..c0f0ade7b9 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -10,8 +10,8 @@ use crate::{ mac::{ platform::NSViewLayerContentsRedrawDuringViewResize, renderer::Renderer, screen::Screen, }, - Event, InputHandler, KeyDownEvent, ModifiersChangedEvent, MouseButton, MouseButtonEvent, - MouseMovedEvent, Scene, WindowBounds, WindowKind, + Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton, + MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind, }, }; use block::ConcreteBlock; @@ -1053,7 +1053,44 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let window_height = window_state_borrow.content_size().y(); let event = unsafe { Event::from_native(native_event, Some(window_height)) }; - if let Some(event) = event { + + if let Some(mut event) = event { + let synthesized_second_event = match &mut event { + Event::MouseDown( + event @ MouseButtonEvent { + button: MouseButton::Left, + modifiers: Modifiers { ctrl: true, .. }, + .. + }, + ) => { + *event = MouseButtonEvent { + button: MouseButton::Right, + modifiers: Modifiers { + ctrl: false, + ..event.modifiers + }, + click_count: 1, + ..*event + }; + + Some(Event::MouseUp(MouseButtonEvent { + button: MouseButton::Right, + ..*event + })) + } + + // Because we map a ctrl-left_down to a right_down -> right_up let's ignore + // the ctrl-left_up to avoid having a mismatch in button down/up events if the + // user is still holding ctrl when releasing the left mouse button + Event::MouseUp(MouseButtonEvent { + button: MouseButton::Left, + modifiers: Modifiers { ctrl: true, .. }, + .. + }) => return, + + _ => None, + }; + match &event { Event::MouseMoved( event @ MouseMovedEvent { @@ -1105,6 +1142,9 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { if let Some(mut callback) = window_state_borrow.event_callback.take() { drop(window_state_borrow); callback(event); + if let Some(event) = synthesized_second_event { + callback(event); + } window_state.borrow_mut().event_callback = Some(callback); } } From 7e904183bfc0dc6bab320a973608e4b17c55956e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 19 Jul 2023 16:24:29 -0400 Subject: [PATCH 047/124] Fix return type in watch_file_types() --- crates/zed/src/main.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index ea019a0fdd..3f89ab3f1f 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -717,9 +717,7 @@ async fn watch_languages(_: Arc, _: Arc) -> Option<()> } #[cfg(not(debug_assertions))] -fn watch_file_types(fs: Arc, cx: &mut AppContext) { - None -} +fn watch_file_types(fs: Arc, cx: &mut AppContext) {} fn connect_to_cli( server_name: &str, From aa67413abc9e8d187a1380ad58cd86f87173f746 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 16:55:28 -0700 Subject: [PATCH 048/124] Raise minimum line height to 1.1 Disable buffer_line_height setting in non-buffer editors --- crates/editor/src/editor.rs | 14 ++++++++++++-- crates/editor/src/element.rs | 1 + crates/theme/src/theme_settings.rs | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbf3d1a173..6a22a1f1f2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1300,7 +1300,7 @@ impl Editor { let editor_view_id = cx.view_id(); let display_map = cx.add_model(|cx| { let settings = settings::get::(cx); - let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); + let style = build_style(settings, get_field_editor_theme.as_deref(), None, &mode, cx); DisplayMap::new( buffer.clone(), style.text.font_id, @@ -1500,6 +1500,7 @@ impl Editor { settings::get::(cx), self.get_field_editor_theme.as_deref(), self.override_text_style.as_deref(), + &self.mode, cx, ) } @@ -8152,10 +8153,11 @@ fn build_style( settings: &ThemeSettings, get_field_editor_theme: Option<&GetFieldEditorTheme>, override_text_style: Option<&OverrideTextStyle>, + mode: &EditorMode, cx: &AppContext, ) -> EditorStyle { let font_cache = cx.font_cache(); - let line_height_scalar = settings.line_height(); + let mut line_height_scalar = settings.line_height(); let theme_id = settings.theme.meta.id; let mut theme = settings.theme.editor.clone(); let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { @@ -8166,6 +8168,14 @@ fn build_style( .container .background_color .unwrap_or_default(); + + line_height_scalar = match mode { + EditorMode::Full => line_height_scalar, + EditorMode::AutoHeight { .. } | EditorMode::SingleLine => { + cx.font_cache().line_height(field_editor_theme.text.font_size) + } + }; + EditorStyle { text: field_editor_theme.text, placeholder_text: field_editor_theme.placeholder_text, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4962b08db2..59a2b222d8 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1977,6 +1977,7 @@ impl Element for EditorElement { let snapshot = editor.snapshot(cx); let style = self.style.clone(); + let line_height = (style.text.font_size * style.line_height_scalar).round(); let gutter_padding; diff --git a/crates/theme/src/theme_settings.rs b/crates/theme/src/theme_settings.rs index b576391e14..ab302a123a 100644 --- a/crates/theme/src/theme_settings.rs +++ b/crates/theme/src/theme_settings.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use util::ResultExt as _; const MIN_FONT_SIZE: f32 = 6.0; -const MIN_LINE_HEIGHT: f32 = 1.0; +const MIN_LINE_HEIGHT: f32 = 1.1; #[derive(Clone, JsonSchema)] pub struct ThemeSettings { From cb97f5a69c1a5b45faae60f56e04a7e74e6b71d6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Wed, 19 Jul 2023 16:56:49 -0700 Subject: [PATCH 049/124] fmt --- crates/editor/src/editor.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6a22a1f1f2..47f31f2c94 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8171,9 +8171,9 @@ fn build_style( line_height_scalar = match mode { EditorMode::Full => line_height_scalar, - EditorMode::AutoHeight { .. } | EditorMode::SingleLine => { - cx.font_cache().line_height(field_editor_theme.text.font_size) - } + EditorMode::AutoHeight { .. } | EditorMode::SingleLine => cx + .font_cache() + .line_height(field_editor_theme.text.font_size), }; EditorStyle { From e1379f0ef051cf43b524d292741c0065dbbd1e40 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 17:58:21 -0600 Subject: [PATCH 050/124] Add support for activating a pane by direction Contributes: zed-industries/community#476 Contributes: zed-industries/community#478 --- crates/editor/src/editor.rs | 2 + crates/editor/src/element.rs | 14 ++++++ crates/editor/src/items.rs | 10 +++- crates/workspace/src/item.rs | 9 ++++ crates/workspace/src/pane.rs | 6 +++ crates/workspace/src/pane_group.rs | 74 +++++++++++++++++++++++++++++- crates/workspace/src/workspace.rs | 47 ++++++++++++++++++- 7 files changed, 157 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cbf3d1a173..b790543ee5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -563,6 +563,7 @@ pub struct Editor { inlay_hint_cache: InlayHintCache, next_inlay_id: usize, _subscriptions: Vec, + pixel_position_of_newest_cursor: Option, } pub struct EditorSnapshot { @@ -1394,6 +1395,7 @@ impl Editor { copilot_state: Default::default(), inlay_hint_cache: InlayHintCache::new(inlay_hint_settings), gutter_hovered: false, + pixel_position_of_newest_cursor: None, _subscriptions: vec![ cx.observe(&buffer, Self::on_buffer_changed), cx.subscribe(&buffer, Self::on_buffer_event), diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 4962b08db2..7a53289232 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -61,6 +61,7 @@ enum FoldMarkers {} struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, + is_newest: bool, range: Range, } @@ -70,6 +71,7 @@ impl SelectionLayout { line_mode: bool, cursor_shape: CursorShape, map: &DisplaySnapshot, + is_newest: bool, ) -> Self { if line_mode { let selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); @@ -77,6 +79,7 @@ impl SelectionLayout { Self { head: selection.head().to_display_point(map), cursor_shape, + is_newest, range: point_range.start.to_display_point(map) ..point_range.end.to_display_point(map), } @@ -85,6 +88,7 @@ impl SelectionLayout { Self { head: selection.head(), cursor_shape, + is_newest, range: selection.range(), } } @@ -864,6 +868,12 @@ impl EditorElement { let x = cursor_character_x - scroll_left; let y = cursor_position.row() as f32 * layout.position_map.line_height - scroll_top; + if selection.is_newest { + editor.pixel_position_of_newest_cursor = Some(vec2f( + bounds.origin_x() + x + block_width / 2., + bounds.origin_y() + y + layout.position_map.line_height / 2., + )); + } cursors.push(Cursor { color: selection_style.cursor, block_width, @@ -2108,6 +2118,7 @@ impl Element for EditorElement { line_mode, cursor_shape, &snapshot.display_snapshot, + false, )); } selections.extend(remote_selections); @@ -2117,6 +2128,7 @@ impl Element for EditorElement { .selections .disjoint_in_range(start_anchor..end_anchor, cx); local_selections.extend(editor.selections.pending(cx)); + let newest = editor.selections.newest(cx); for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; @@ -2139,11 +2151,13 @@ impl Element for EditorElement { local_selections .into_iter() .map(|selection| { + let is_newest = selection == newest; SelectionLayout::new( selection, editor.selections.line_mode, editor.cursor_shape, &snapshot.display_snapshot, + is_newest, ) }) .collect(), diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0ce41a97c9..7c8fe12aa0 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -7,8 +7,10 @@ use anyhow::{Context, Result}; use collections::HashSet; use futures::future::try_join_all; use gpui::{ - elements::*, geometry::vector::vec2f, AppContext, AsyncAppContext, Entity, ModelHandle, - Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, + elements::*, + geometry::vector::{vec2f, Vector2F}, + AppContext, AsyncAppContext, Entity, ModelHandle, Subscription, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use language::{ proto::serialize_anchor as serialize_text_anchor, Bias, Buffer, OffsetRangeExt, Point, @@ -750,6 +752,10 @@ impl Item for Editor { Some(Box::new(handle.clone())) } + fn pixel_position_of_cursor(&self) -> Option { + self.pixel_position_of_newest_cursor + } + fn breadcrumb_location(&self) -> ToolbarItemLocation { ToolbarItemLocation::PrimaryLeft { flex: None } } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 460698efb8..f0af080d4a 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -5,6 +5,7 @@ use crate::{ use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; use anyhow::Result; use client::{proto, Client}; +use gpui::geometry::vector::Vector2F; use gpui::{ fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -203,6 +204,9 @@ pub trait Item: View { fn show_toolbar(&self) -> bool { true } + fn pixel_position_of_cursor(&self) -> Option { + None + } } pub trait ItemHandle: 'static + fmt::Debug { @@ -271,6 +275,7 @@ pub trait ItemHandle: 'static + fmt::Debug { fn breadcrumbs(&self, theme: &Theme, cx: &AppContext) -> Option>; fn serialized_item_kind(&self) -> Option<&'static str>; fn show_toolbar(&self, cx: &AppContext) -> bool; + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option; } pub trait WeakItemHandle { @@ -615,6 +620,10 @@ impl ItemHandle for ViewHandle { fn show_toolbar(&self, cx: &AppContext) -> bool { self.read(cx).show_toolbar() } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.read(cx).pixel_position_of_cursor() + } } impl From> for AnyViewHandle { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index f5b96fd421..2972c307f2 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -542,6 +542,12 @@ impl Pane { self.items.get(self.active_item_index).cloned() } + pub fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.items + .get(self.active_item_index)? + .pixel_position_of_cursor(cx) + } + pub fn item_for_entry( &self, entry_id: ProjectEntryId, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 52761b06c8..baa654d967 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -54,6 +54,20 @@ impl PaneGroup { } } + pub fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + match &self.root { + Member::Pane(_) => None, + Member::Axis(axis) => axis.bounding_box_for_pane(pane), + } + } + + pub fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + match &self.root { + Member::Pane(pane) => Some(pane), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + } + } + /// Returns: /// - Ok(true) if it found and removed a pane /// - Ok(false) if it found but did not remove the pane @@ -309,15 +323,18 @@ pub(crate) struct PaneAxis { pub axis: Axis, pub members: Vec, pub flexes: Rc>>, + pub bounding_boxes: Rc>>>, } impl PaneAxis { pub fn new(axis: Axis, members: Vec) -> Self { let flexes = Rc::new(RefCell::new(vec![1.; members.len()])); + let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); Self { axis, members, flexes, + bounding_boxes, } } @@ -326,10 +343,12 @@ impl PaneAxis { debug_assert!(members.len() == flexes.len()); let flexes = Rc::new(RefCell::new(flexes)); + let bounding_boxes = Rc::new(RefCell::new(vec![None; members.len()])); Self { axis, members, flexes, + bounding_boxes, } } @@ -409,6 +428,40 @@ impl PaneAxis { } } + fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + for (idx, member) in self.members.iter().enumerate() { + match member { + Member::Pane(found) => { + if pane == found { + return self.bounding_boxes.borrow()[idx]; + } + } + Member::Axis(axis) => { + if let Some(rect) = axis.bounding_box_for_pane(pane) { + return Some(rect); + } + } + } + } + None + } + + fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + let bounding_boxes = self.bounding_boxes.borrow(); + + for (idx, member) in self.members.iter().enumerate() { + if let Some(coordinates) = bounding_boxes[idx] { + if coordinates.contains_point(coordinate) { + return match member { + Member::Pane(found) => Some(found), + Member::Axis(axis) => axis.pane_at_pixel_position(coordinate), + }; + } + } + } + None + } + fn render( &self, project: &ModelHandle, @@ -423,7 +476,12 @@ impl PaneAxis { ) -> AnyElement { debug_assert!(self.members.len() == self.flexes.borrow().len()); - let mut pane_axis = PaneAxisElement::new(self.axis, basis, self.flexes.clone()); + let mut pane_axis = PaneAxisElement::new( + self.axis, + basis, + self.flexes.clone(), + self.bounding_boxes.clone(), + ); let mut active_pane_ix = None; let mut members = self.members.iter().enumerate().peekable(); @@ -546,14 +604,21 @@ mod element { active_pane_ix: Option, flexes: Rc>>, children: Vec>, + bounding_boxes: Rc>>>, } impl PaneAxisElement { - pub fn new(axis: Axis, basis: usize, flexes: Rc>>) -> Self { + pub fn new( + axis: Axis, + basis: usize, + flexes: Rc>>, + bounding_boxes: Rc>>>, + ) -> Self { Self { axis, basis, flexes, + bounding_boxes, active_pane_ix: None, children: Default::default(), } @@ -708,11 +773,16 @@ mod element { let mut child_origin = bounds.origin(); + let mut bounding_boxes = self.bounding_boxes.borrow_mut(); + bounding_boxes.clear(); + let mut children_iter = self.children.iter_mut().enumerate().peekable(); while let Some((ix, child)) = children_iter.next() { let child_start = child_origin.clone(); child.paint(scene, child_origin, visible_bounds, view, cx); + bounding_boxes.push(Some(RectF::new(child_origin, child.size()))); + match self.axis { Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3e62af8ea6..e87e8f1855 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -152,6 +152,9 @@ pub struct OpenPaths { #[derive(Clone, Deserialize, PartialEq)] pub struct ActivatePane(pub usize); +#[derive(Clone, Deserialize, PartialEq)] +pub struct ActivatePaneInDirection(pub SplitDirection); + #[derive(Deserialize)] pub struct Toast { id: usize, @@ -197,7 +200,7 @@ impl Clone for Toast { } } -impl_actions!(workspace, [ActivatePane, Toast]); +impl_actions!(workspace, [ActivatePane, ActivatePaneInDirection, Toast]); pub type WorkspaceId = i64; @@ -262,6 +265,13 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); + + cx.add_action( + |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { + workspace.activate_pane_in_direction(action.0, cx) + }, + ); + cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { workspace.toggle_dock(DockPosition::Left, cx); }); @@ -2054,6 +2064,40 @@ impl Workspace { } } + pub fn activate_pane_in_direction( + &mut self, + direction: SplitDirection, + cx: &mut ViewContext, + ) { + let bounding_box = match self.center.bounding_box_for_pane(&self.active_pane) { + Some(coordinates) => coordinates, + None => { + return; + } + }; + let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); + let center = match cursor { + Some(cursor) => cursor, + None => bounding_box.center(), + }; + + // currently there's a small gap between panes, so we can't just look "1px to the left" + // instead of trying to calcuate this exactly, we assume it'll always be smaller than + // "pane_gap" pixels (and that no-one uses panes smaller in any dimension than pane_gap). + let pane_gap = 20.; + + let target = match direction { + SplitDirection::Left => vec2f(bounding_box.origin_x() - pane_gap, center.y()), + SplitDirection::Right => vec2f(bounding_box.max_x() + pane_gap, center.y()), + SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - pane_gap), + SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + pane_gap), + }; + + if let Some(pane) = self.center.pane_at_pixel_position(target) { + cx.focus(pane); + } + } + fn handle_pane_focused(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane = pane.clone(); @@ -3030,6 +3074,7 @@ impl Workspace { axis, members, flexes, + bounding_boxes: _, }) => SerializedPaneGroup::Group { axis: *axis, children: members From 2762f9b1c681a0af0cf3c6e08e8f1a3855352faf Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 18:29:13 -0600 Subject: [PATCH 051/124] vim: Add support for ctrl-w commands Primarily {h,j,k,l,left,right,up,down} for moving to a pane by direction; but also {w,W,p} for going forward/back, and {v,s} for splitting a pane vertically/horizontally, and {c,q} to close a pane. There are a large number of ctrl-w commands that are not supported, and which fall into three buckets: * switch this pane with that one (VScode also has this, and it's a requested feature) * move to top/bottom/leftmost/rightmost * counts on any of these * jump to "definition/file-under-cursor/etc.etc." in a new pane. --- assets/keymaps/vim.json | 70 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index aa1e235414..8fb174c71e 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -145,7 +145,75 @@ "9": [ "vim::Number", 9 - ] + ], + // window related commands (ctrl-w X) + "ctrl-w left": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w right": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w up": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w down": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w h": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w l": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w k": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w j": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w ctrl-h": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "ctrl-w ctrl-l": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "ctrl-w ctrl-k": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "ctrl-w ctrl-j": [ + "workspace::ActivatePaneInDirection", + "Down" + ], + "ctrl-w g t": "pane::ActivateNextItem", + "ctrl-w ctrl-g t": "pane::ActivateNextItem", + "ctrl-w g shift-t": "pane::ActivatePrevItem", + "ctrl-w ctrl-g shift-t": "pane::ActivatePrevItem", + "ctrl-w w": "workspace::ActivateNextPane", + "ctrl-w ctrl-w": "workspace::ActivateNextPane", + "ctrl-w p": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-p": "workspace::ActivatePreviousPane", + "ctrl-w shift-w": "workspace::ActivatePreviousPane", + "ctrl-w ctrl-shift-w": "workspace::ActivatePreviousPane", + "ctrl-w v": "pane::SplitLeft", + "ctrl-w ctrl-v": "pane::SplitLeft", + "ctrl-w s": "pane::SplitUp", + "ctrl-w shift-s": "pane::SplitUp", + "ctrl-w ctrl-s": "pane::SplitUp", + "ctrl-w c": "pane::CloseAllItems", + "ctrl-w ctrl-c": "pane::CloseAllItems", + "ctrl-w q": "pane::CloseAllItems", + "ctrl-w ctrl-q": "pane::CloseAllItems" } }, { From 15dc8b43c476a6da0a44457bf7d8581228851b2a Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 18:33:08 -0600 Subject: [PATCH 052/124] Default keybindings for activating pane by direction Breaking change: previously cmd-k cmd-{left,right} moved to the {previous,next} pane; now they will move in the specified direction. --- assets/keymaps/default.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1a13d8cdb3..883b0c1872 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -446,8 +446,22 @@ }, { "bindings": { - "cmd-k cmd-left": "workspace::ActivatePreviousPane", - "cmd-k cmd-right": "workspace::ActivateNextPane" + "cmd-k cmd-left": [ + "workspace::ActivatePaneInDirection", + "Left" + ], + "cmd-k cmd-right": [ + "workspace::ActivatePaneInDirection", + "Right" + ], + "cmd-k cmd-up": [ + "workspace::ActivatePaneInDirection", + "Up" + ], + "cmd-k cmd-down": [ + "workspace::ActivatePaneInDirection", + "Down" + ] } }, // Bindings from Atom From a5e63fbf77f3092f1d32a3fe5057e318d3f82bd2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 19 Jul 2023 16:47:08 +0300 Subject: [PATCH 053/124] Properly display keybindings in context menus --- crates/call/src/room.rs | 3 +-- crates/gpui/src/app.rs | 18 +++++++----------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 08ac8befc4..328a94506c 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -20,7 +20,7 @@ use live_kit_client::{ }; use postage::stream::Stream; use project::Project; -use std::{future::Future, mem, panic::Location, pin::Pin, sync::Arc, time::Duration}; +use std::{future::Future, mem, pin::Pin, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; pub const RECONNECT_TIMEOUT: Duration = Duration::from_secs(30); @@ -1089,7 +1089,6 @@ impl Room { #[track_caller] pub fn share_microphone(&mut self, cx: &mut ModelContext) -> Task> { - dbg!(Location::caller()); if self.status.is_offline() { return Task::ready(Err(anyhow!("room is offline"))); } else if self.is_sharing_mic() { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b40a67db61..7af363d596 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3411,18 +3411,14 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { handler_depth = Some(contexts.len()) } + let action_contexts = if let Some(depth) = handler_depth { + &contexts[depth..] + } else { + &contexts + }; + self.keystroke_matcher - .bindings_for_action(action.id()) - .find_map(|b| { - let highest_handler = handler_depth?; - if action.eq(b.action()) - && (0..=highest_handler).any(|depth| b.match_context(&contexts[depth..])) - { - Some(b.keystrokes().into()) - } else { - None - } - }) + .keystrokes_for_action(action, action_contexts) } fn notify_if_view_ancestors_change(&mut self, view_id: usize) { From 0e6048a85d0ed547774f34cdfd15b565d82407df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 20 Jul 2023 13:42:11 +0300 Subject: [PATCH 054/124] Keep basic line height for single line editors --- crates/editor/src/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 47f31f2c94..f06425fc0b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8170,8 +8170,8 @@ fn build_style( .unwrap_or_default(); line_height_scalar = match mode { - EditorMode::Full => line_height_scalar, - EditorMode::AutoHeight { .. } | EditorMode::SingleLine => cx + EditorMode::SingleLine | EditorMode::Full => line_height_scalar, + EditorMode::AutoHeight { .. } => cx .font_cache() .line_height(field_editor_theme.text.font_size), }; From 0237276557a2eff7345e53e72a9537b0ead3734c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 20 Jul 2023 15:39:22 +0300 Subject: [PATCH 055/124] Fully revert the line height change --- crates/editor/src/editor.rs | 14 ++------------ crates/theme/src/theme_settings.rs | 2 +- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f06425fc0b..cbf3d1a173 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1300,7 +1300,7 @@ impl Editor { let editor_view_id = cx.view_id(); let display_map = cx.add_model(|cx| { let settings = settings::get::(cx); - let style = build_style(settings, get_field_editor_theme.as_deref(), None, &mode, cx); + let style = build_style(settings, get_field_editor_theme.as_deref(), None, cx); DisplayMap::new( buffer.clone(), style.text.font_id, @@ -1500,7 +1500,6 @@ impl Editor { settings::get::(cx), self.get_field_editor_theme.as_deref(), self.override_text_style.as_deref(), - &self.mode, cx, ) } @@ -8153,11 +8152,10 @@ fn build_style( settings: &ThemeSettings, get_field_editor_theme: Option<&GetFieldEditorTheme>, override_text_style: Option<&OverrideTextStyle>, - mode: &EditorMode, cx: &AppContext, ) -> EditorStyle { let font_cache = cx.font_cache(); - let mut line_height_scalar = settings.line_height(); + let line_height_scalar = settings.line_height(); let theme_id = settings.theme.meta.id; let mut theme = settings.theme.editor.clone(); let mut style = if let Some(get_field_editor_theme) = get_field_editor_theme { @@ -8168,14 +8166,6 @@ fn build_style( .container .background_color .unwrap_or_default(); - - line_height_scalar = match mode { - EditorMode::SingleLine | EditorMode::Full => line_height_scalar, - EditorMode::AutoHeight { .. } => cx - .font_cache() - .line_height(field_editor_theme.text.font_size), - }; - EditorStyle { text: field_editor_theme.text, placeholder_text: field_editor_theme.placeholder_text, diff --git a/crates/theme/src/theme_settings.rs b/crates/theme/src/theme_settings.rs index ab302a123a..b576391e14 100644 --- a/crates/theme/src/theme_settings.rs +++ b/crates/theme/src/theme_settings.rs @@ -13,7 +13,7 @@ use std::sync::Arc; use util::ResultExt as _; const MIN_FONT_SIZE: f32 = 6.0; -const MIN_LINE_HEIGHT: f32 = 1.1; +const MIN_LINE_HEIGHT: f32 = 1.0; #[derive(Clone, JsonSchema)] pub struct ThemeSettings { From a90b151d52e1fa026c338c2102567e167d8894c9 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 10:07:32 -0600 Subject: [PATCH 056/124] Updated icons with additions --- assets/icons/file_icons/archive.svg | 6 +++--- assets/icons/file_icons/audio.svg | 6 ++++++ assets/icons/file_icons/book.svg | 6 ++++-- assets/icons/file_icons/camera.svg | 4 ++-- assets/icons/file_icons/database.svg | 2 +- assets/icons/file_icons/eslint.svg | 2 +- assets/icons/file_icons/file.svg | 4 ++-- assets/icons/file_icons/file_types.json | 9 ++++++--- assets/icons/file_icons/folder-open.svg | 7 ++++--- assets/icons/file_icons/folder.svg | 5 +++-- assets/icons/file_icons/git.svg | 2 +- assets/icons/file_icons/image.svg | 7 ++++--- assets/icons/file_icons/lock.svg | 4 ++-- assets/icons/file_icons/notebook.svg | 6 ++++-- assets/icons/file_icons/package.svg | 3 ++- assets/icons/file_icons/prettier.svg | 10 +++++----- assets/icons/file_icons/rust.svg | 2 +- assets/icons/file_icons/settings.svg | 4 ++-- assets/icons/file_icons/terminal.svg | 2 +- assets/icons/file_icons/toml.svg | 5 +++++ assets/icons/file_icons/typescript.svg | 6 +++--- assets/icons/file_icons/video.svg | 4 ++++ 22 files changed, 66 insertions(+), 40 deletions(-) create mode 100644 assets/icons/file_icons/audio.svg create mode 100644 assets/icons/file_icons/toml.svg create mode 100644 assets/icons/file_icons/video.svg diff --git a/assets/icons/file_icons/archive.svg b/assets/icons/file_icons/archive.svg index f11115cdce..820c5846ba 100644 --- a/assets/icons/file_icons/archive.svg +++ b/assets/icons/file_icons/archive.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/audio.svg b/assets/icons/file_icons/audio.svg new file mode 100644 index 0000000000..c2275efb63 --- /dev/null +++ b/assets/icons/file_icons/audio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/icons/file_icons/book.svg b/assets/icons/file_icons/book.svg index 890b8988a3..c9aa764d72 100644 --- a/assets/icons/file_icons/book.svg +++ b/assets/icons/file_icons/book.svg @@ -1,4 +1,6 @@ - - + + + + diff --git a/assets/icons/file_icons/camera.svg b/assets/icons/file_icons/camera.svg index d8b9cf459c..bc1993ad63 100644 --- a/assets/icons/file_icons/camera.svg +++ b/assets/icons/file_icons/camera.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/database.svg b/assets/icons/file_icons/database.svg index 9072e091b5..812d147717 100644 --- a/assets/icons/file_icons/database.svg +++ b/assets/icons/file_icons/database.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/eslint.svg b/assets/icons/file_icons/eslint.svg index ec5051d447..14ac83df96 100644 --- a/assets/icons/file_icons/eslint.svg +++ b/assets/icons/file_icons/eslint.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg index cc422734e7..e05a47548e 100644 --- a/assets/icons/file_icons/file.svg +++ b/assets/icons/file_icons/file.svg @@ -1,5 +1,5 @@ - + - + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 4f3f8160d7..edc398a295 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -74,7 +74,7 @@ "svg": "image", "swift": "code", "tiff": "image", - "toml": "settings", + "toml": "toml", "ts": "typescript", "tsx": "code", "txt": "document", @@ -89,7 +89,7 @@ }, "types": { "audio": { - "icon": "icons/file_icons/file.svg" + "icon": "icons/file_icons/audio.svg" }, "code": { "icon": "icons/file_icons/code.svg" @@ -136,6 +136,9 @@ "terminal": { "icon": "icons/file_icons/terminal.svg" }, + "toml": { + "icon": "icons/file_icons/toml.svg" + }, "typescript": { "icon": "icons/file_icons/typescript.svg" }, @@ -143,7 +146,7 @@ "icon": "icons/file_icons/git.svg" }, "video": { - "icon": "icons/file_icons/file.svg" + "icon": "icons/file_icons/video.svg" } } } diff --git a/assets/icons/file_icons/folder-open.svg b/assets/icons/file_icons/folder-open.svg index 65c5744049..405d0b8308 100644 --- a/assets/icons/file_icons/folder-open.svg +++ b/assets/icons/file_icons/folder-open.svg @@ -1,4 +1,5 @@ - - - + + + + diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index 5157bae839..4ef944a69c 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,4 +1,5 @@ - - + + + diff --git a/assets/icons/file_icons/git.svg b/assets/icons/file_icons/git.svg index 82d8c8f57c..a30b47fb86 100644 --- a/assets/icons/file_icons/git.svg +++ b/assets/icons/file_icons/git.svg @@ -1,6 +1,6 @@ - + diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg index ee5e49f2d4..c258170dae 100644 --- a/assets/icons/file_icons/image.svg +++ b/assets/icons/file_icons/image.svg @@ -1,6 +1,7 @@ - - - + + + + diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg index 3051bbf801..b6aa3394ef 100644 --- a/assets/icons/file_icons/lock.svg +++ b/assets/icons/file_icons/lock.svg @@ -1,6 +1,6 @@ - - + + diff --git a/assets/icons/file_icons/notebook.svg b/assets/icons/file_icons/notebook.svg index 6eaec16d0a..4f55ceac58 100644 --- a/assets/icons/file_icons/notebook.svg +++ b/assets/icons/file_icons/notebook.svg @@ -1,6 +1,8 @@ - + + - + + diff --git a/assets/icons/file_icons/package.svg b/assets/icons/file_icons/package.svg index 2a692ba4b4..a46126e3e9 100644 --- a/assets/icons/file_icons/package.svg +++ b/assets/icons/file_icons/package.svg @@ -1,3 +1,4 @@ - + + diff --git a/assets/icons/file_icons/prettier.svg b/assets/icons/file_icons/prettier.svg index 2d2c6ee719..23cefe0efc 100644 --- a/assets/icons/file_icons/prettier.svg +++ b/assets/icons/file_icons/prettier.svg @@ -1,12 +1,12 @@ - - + + - + - + - + diff --git a/assets/icons/file_icons/rust.svg b/assets/icons/file_icons/rust.svg index 1802f0e190..91982b3eeb 100644 --- a/assets/icons/file_icons/rust.svg +++ b/assets/icons/file_icons/rust.svg @@ -1,4 +1,4 @@ - + diff --git a/assets/icons/file_icons/settings.svg b/assets/icons/file_icons/settings.svg index e827055d19..35af7e1899 100644 --- a/assets/icons/file_icons/settings.svg +++ b/assets/icons/file_icons/settings.svg @@ -1,4 +1,4 @@ - - + + diff --git a/assets/icons/file_icons/terminal.svg b/assets/icons/file_icons/terminal.svg index 939587c53e..15dd705b0b 100644 --- a/assets/icons/file_icons/terminal.svg +++ b/assets/icons/file_icons/terminal.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/toml.svg b/assets/icons/file_icons/toml.svg new file mode 100644 index 0000000000..496c41e755 --- /dev/null +++ b/assets/icons/file_icons/toml.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/file_icons/typescript.svg b/assets/icons/file_icons/typescript.svg index 179b3d8572..f7748a86c4 100644 --- a/assets/icons/file_icons/typescript.svg +++ b/assets/icons/file_icons/typescript.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/assets/icons/file_icons/video.svg b/assets/icons/file_icons/video.svg new file mode 100644 index 0000000000..c7ebf98af6 --- /dev/null +++ b/assets/icons/file_icons/video.svg @@ -0,0 +1,4 @@ + + + + From f051e66231608969a3eb1063180d6ea50c0c3e4e Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 10:15:20 -0600 Subject: [PATCH 057/124] code icon adjustment --- assets/icons/file_icons/code.svg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/icons/file_icons/code.svg b/assets/icons/file_icons/code.svg index 2733e4b535..5e59cbe58f 100644 --- a/assets/icons/file_icons/code.svg +++ b/assets/icons/file_icons/code.svg @@ -1,4 +1,4 @@ - - + + From d6a463afb868e5616ee365030d2b516a207f5413 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:06:16 -0600 Subject: [PATCH 058/124] Better calculation of pane distance --- crates/workspace/src/workspace.rs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e87e8f1855..2f3f2f9010 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2081,16 +2081,13 @@ impl Workspace { None => bounding_box.center(), }; - // currently there's a small gap between panes, so we can't just look "1px to the left" - // instead of trying to calcuate this exactly, we assume it'll always be smaller than - // "pane_gap" pixels (and that no-one uses panes smaller in any dimension than pane_gap). - let pane_gap = 20.; + let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; let target = match direction { - SplitDirection::Left => vec2f(bounding_box.origin_x() - pane_gap, center.y()), - SplitDirection::Right => vec2f(bounding_box.max_x() + pane_gap, center.y()), - SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - pane_gap), - SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + pane_gap), + SplitDirection::Left => vec2f(bounding_box.origin_x() - distance_to_next, center.y()), + SplitDirection::Right => vec2f(bounding_box.max_x() + distance_to_next, center.y()), + SplitDirection::Up => vec2f(center.x(), bounding_box.origin_y() - distance_to_next), + SplitDirection::Down => vec2f(center.x(), bounding_box.max_y() + distance_to_next), }; if let Some(pane) = self.center.pane_at_pixel_position(target) { From 464cc2e71affec45785a04a123707a4931464a82 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:11:37 -0600 Subject: [PATCH 059/124] Assertions for assumptions --- crates/workspace/src/pane_group.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index baa654d967..7b7c845616 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -429,6 +429,8 @@ impl PaneAxis { } fn bounding_box_for_pane(&self, pane: &ViewHandle) -> Option { + debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + for (idx, member) in self.members.iter().enumerate() { match member { Member::Pane(found) => { @@ -447,6 +449,8 @@ impl PaneAxis { } fn pane_at_pixel_position(&self, coordinate: Vector2F) -> Option<&ViewHandle> { + debug_assert!(self.members.len() == self.bounding_boxes.borrow().len()); + let bounding_boxes = self.bounding_boxes.borrow(); for (idx, member) in self.members.iter().enumerate() { From 0e984e1e69e7125ed38397556f67fcde1e80a415 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 11:11:47 -0600 Subject: [PATCH 060/124] Ignore off-screen cursors --- crates/workspace/src/workspace.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2f3f2f9010..0ebd01e1f7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2077,8 +2077,8 @@ impl Workspace { }; let cursor = self.active_pane.read(cx).pixel_position_of_cursor(cx); let center = match cursor { - Some(cursor) => cursor, - None => bounding_box.center(), + Some(cursor) if bounding_box.contains_point(cursor) => cursor, + _ => bounding_box.center(), }; let distance_to_next = theme::current(cx).workspace.pane_divider.width + 1.; From 5d22a300c3c4e600ae2aa4b3a30ea0685c904134 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 20 Jul 2023 13:18:15 -0400 Subject: [PATCH 061/124] Add the `local` and `declare` keywords to bash syntax highlighting --- crates/zed/src/languages/bash/highlights.scm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm index f33a7c2d3a..a72c5468ed 100644 --- a/crates/zed/src/languages/bash/highlights.scm +++ b/crates/zed/src/languages/bash/highlights.scm @@ -27,6 +27,8 @@ "unset" "until" "while" + "local" + "declare" ] @keyword (comment) @comment From 4d1dbb8aa3e4fe0bcebd7c5ea98aa8d463f1c8f2 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 20 Jul 2023 10:33:28 -0700 Subject: [PATCH 062/124] Add a double click to reset resized splits --- crates/workspace/src/pane_group.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7b7c845616..abcb609dd1 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -826,10 +826,11 @@ mod element { let child_size = child.size(); let next_child_size = next_child.size(); let drag_bounds = visible_bounds.clone(); - let flexes = self.flexes.clone(); - let current_flex = flexes.borrow()[ix]; + let flexes = self.flexes.borrow(); + let current_flex = flexes[ix]; let next_ix = *next_ix; - let next_flex = flexes.borrow()[next_ix]; + let next_flex = flexes[next_ix]; + drop(flexes); enum ResizeHandle {} let mut mouse_region = MouseRegion::new::( cx.view_id(), @@ -838,7 +839,9 @@ mod element { ); mouse_region = mouse_region.on_drag( MouseButton::Left, - move |drag, workspace: &mut Workspace, cx| { + { + let flexes = self.flexes.clone(); + move |drag, workspace: &mut Workspace, cx| { let min_size = match axis { Axis::Horizontal => HORIZONTAL_MIN_SIZE, Axis::Vertical => VERTICAL_MIN_SIZE, @@ -881,8 +884,17 @@ mod element { workspace.schedule_serialize(cx); cx.notify(); - }, - ); + }}, + ).on_click(MouseButton::Left, { + let flexes = self.flexes.clone(); + move |e, v: &mut Workspace, cx| { + if e.click_count >= 2 { + let mut borrow = flexes.borrow_mut(); + *borrow = vec![1.; borrow.len()]; + v.schedule_serialize(cx); + cx.notify(); + } + }}); scene.push_mouse_region(mouse_region); scene.pop_stacking_context(); From d84d663ac3e6c4473f522f2f84da24ca724221d7 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 20 Jul 2023 10:36:23 -0700 Subject: [PATCH 063/124] fmt --- crates/workspace/src/pane_group.rs | 112 +++++++++++++++-------------- 1 file changed, 58 insertions(+), 54 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index abcb609dd1..e60f6deb2f 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -837,64 +837,68 @@ mod element { self.basis + ix, handle_bounds, ); - mouse_region = mouse_region.on_drag( - MouseButton::Left, - { + mouse_region = mouse_region + .on_drag(MouseButton::Left, { let flexes = self.flexes.clone(); move |drag, workspace: &mut Workspace, cx| { - let min_size = match axis { - Axis::Horizontal => HORIZONTAL_MIN_SIZE, - Axis::Vertical => VERTICAL_MIN_SIZE, - }; - // Don't allow resizing to less than the minimum size, if elements are already too small - if min_size - 1. > child_size.along(axis) - || min_size - 1. > next_child_size.along(axis) - { - return; + let min_size = match axis { + Axis::Horizontal => HORIZONTAL_MIN_SIZE, + Axis::Vertical => VERTICAL_MIN_SIZE, + }; + // Don't allow resizing to less than the minimum size, if elements are already too small + if min_size - 1. > child_size.along(axis) + || min_size - 1. > next_child_size.along(axis) + { + return; + } + + let mut current_target_size = + (drag.position - child_start).along(axis); + + let proposed_current_pixel_change = + current_target_size - child_size.along(axis); + + if proposed_current_pixel_change < 0. { + current_target_size = f32::max(current_target_size, min_size); + } else if proposed_current_pixel_change > 0. { + // TODO: cascade this change to other children if current item is at min size + let next_target_size = f32::max( + next_child_size.along(axis) - proposed_current_pixel_change, + min_size, + ); + current_target_size = f32::min( + current_target_size, + child_size.along(axis) + next_child_size.along(axis) + - next_target_size, + ); + } + + let current_pixel_change = + current_target_size - child_size.along(axis); + let flex_change = + current_pixel_change / drag_bounds.length_along(axis); + let current_target_flex = current_flex + flex_change; + let next_target_flex = next_flex - flex_change; + + let mut borrow = flexes.borrow_mut(); + *borrow.get_mut(ix).unwrap() = current_target_flex; + *borrow.get_mut(next_ix).unwrap() = next_target_flex; + + workspace.schedule_serialize(cx); + cx.notify(); } - - let mut current_target_size = (drag.position - child_start).along(axis); - - let proposed_current_pixel_change = - current_target_size - child_size.along(axis); - - if proposed_current_pixel_change < 0. { - current_target_size = f32::max(current_target_size, min_size); - } else if proposed_current_pixel_change > 0. { - // TODO: cascade this change to other children if current item is at min size - let next_target_size = f32::max( - next_child_size.along(axis) - proposed_current_pixel_change, - min_size, - ); - current_target_size = f32::min( - current_target_size, - child_size.along(axis) + next_child_size.along(axis) - - next_target_size, - ); + }) + .on_click(MouseButton::Left, { + let flexes = self.flexes.clone(); + move |e, v: &mut Workspace, cx| { + if e.click_count >= 2 { + let mut borrow = flexes.borrow_mut(); + *borrow = vec![1.; borrow.len()]; + v.schedule_serialize(cx); + cx.notify(); + } } - - let current_pixel_change = current_target_size - child_size.along(axis); - let flex_change = current_pixel_change / drag_bounds.length_along(axis); - let current_target_flex = current_flex + flex_change; - let next_target_flex = next_flex - flex_change; - - let mut borrow = flexes.borrow_mut(); - *borrow.get_mut(ix).unwrap() = current_target_flex; - *borrow.get_mut(next_ix).unwrap() = next_target_flex; - - workspace.schedule_serialize(cx); - cx.notify(); - }}, - ).on_click(MouseButton::Left, { - let flexes = self.flexes.clone(); - move |e, v: &mut Workspace, cx| { - if e.click_count >= 2 { - let mut borrow = flexes.borrow_mut(); - *borrow = vec![1.; borrow.len()]; - v.schedule_serialize(cx); - cx.notify(); - } - }}); + }); scene.push_mouse_region(mouse_region); scene.pop_stacking_context(); From 8f0b24b264c1d0f621ca7acfc041cfa64c073896 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:01:41 -0600 Subject: [PATCH 064/124] Add moar icons --- assets/icons/file_icons/chevron_down.svg | 3 +++ assets/icons/file_icons/chevron_left.svg | 3 +++ assets/icons/file_icons/chevron_right.svg | 3 +++ assets/icons/file_icons/chevron_up.svg | 3 +++ assets/icons/file_icons/file_types.json | 16 ++++++++---- assets/icons/file_icons/folder.svg | 2 +- .../{folder-open.svg => folder_open.svg} | 2 +- crates/project_panel/src/file_associations.rs | 25 ++++++++++++++++--- 8 files changed, 47 insertions(+), 10 deletions(-) create mode 100644 assets/icons/file_icons/chevron_down.svg create mode 100644 assets/icons/file_icons/chevron_left.svg create mode 100644 assets/icons/file_icons/chevron_right.svg create mode 100644 assets/icons/file_icons/chevron_up.svg rename assets/icons/file_icons/{folder-open.svg => folder_open.svg} (89%) diff --git a/assets/icons/file_icons/chevron_down.svg b/assets/icons/file_icons/chevron_down.svg new file mode 100644 index 0000000000..b971555cfa --- /dev/null +++ b/assets/icons/file_icons/chevron_down.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/chevron_left.svg b/assets/icons/file_icons/chevron_left.svg new file mode 100644 index 0000000000..8e61beed5d --- /dev/null +++ b/assets/icons/file_icons/chevron_left.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/chevron_right.svg b/assets/icons/file_icons/chevron_right.svg new file mode 100644 index 0000000000..fcd9d83fc2 --- /dev/null +++ b/assets/icons/file_icons/chevron_right.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/chevron_up.svg b/assets/icons/file_icons/chevron_up.svg new file mode 100644 index 0000000000..171cdd61c0 --- /dev/null +++ b/assets/icons/file_icons/chevron_up.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index edc398a295..b53d263063 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -94,20 +94,26 @@ "code": { "icon": "icons/file_icons/code.svg" }, + "collapsed_chevron": { + "icon": "icons/file_icons/chevron_right.svg" + }, + "collapsed_folder": { + "icon": "icons/file_icons/folder.svg" + }, "default": { "icon": "icons/file_icons/file.svg" }, - "directory": { - "icon": "icons/file_icons/folder.svg" - }, "document": { "icon": "icons/file_icons/book.svg" }, "eslint": { "icon": "icons/file_icons/eslint.svg" }, - "expanded_directory": { - "icon": "icons/file_icons/folder-open.svg" + "expanded_chevron": { + "icon": "icons/file_icons/chevron_down.svg" + }, + "expanded_folder": { + "icon": "icons/file_icons/folder_open.svg" }, "image": { "icon": "icons/file_icons/image.svg" diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index 4ef944a69c..d890160100 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/folder-open.svg b/assets/icons/file_icons/folder_open.svg similarity index 89% rename from assets/icons/file_icons/folder-open.svg rename to assets/icons/file_icons/folder_open.svg index 405d0b8308..bf64f6ee39 100644 --- a/assets/icons/file_icons/folder-open.svg +++ b/assets/icons/file_icons/folder_open.svg @@ -1,5 +1,5 @@ - + diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 6e2e373d76..2694fa1697 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -17,8 +17,10 @@ pub struct FileAssociations { types: HashMap, } -const DIRECTORY_TYPE: &'static str = "directory"; -const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_directory"; +const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; +const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; +const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; +const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; pub fn init(assets: impl AssetSource, cx: &mut AppContext) { @@ -72,7 +74,24 @@ impl FileAssociations { let key = if expanded { EXPANDED_DIRECTORY_TYPE } else { - DIRECTORY_TYPE + COLLAPSED_DIRECTORY_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { + iife!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE }; this.types From a7695c47bf1e59aca8a4ebaa6b37b0485f7945f0 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:03:07 -0600 Subject: [PATCH 065/124] Update default settings --- assets/settings/default.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index d35049a84d..343abbf077 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -106,6 +106,8 @@ "git_status": true, // Whether to show file icons in the project panel. "file_icons": true, + // Whether to show folder icons or chevrons for directories in the project panel. + "folder_icons": true, // Where to dock project panel. Can be 'left' or 'right'. "dock": "left", // Default width of the project panel. @@ -203,9 +205,7 @@ "copilot": { // The set of glob patterns for which copilot should be disabled // in any matching file. - "disabled_globs": [ - ".env" - ] + "disabled_globs": [".env"] }, // Settings specific to journaling "journal": { From 13ae1249f509a1644fd0d9ce676e12c90b38968a Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:23:28 -0600 Subject: [PATCH 066/124] Allow for folders or chevrons --- crates/project_panel/src/project_panel.rs | 41 ++++++++++------------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index d97c47a339..014649f9a8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1176,9 +1176,9 @@ impl ProjectPanel { } let end_ix = range.end.min(ix + visible_worktree_entries.len()); - let (git_status_setting, show_file_icons) = { + let (git_status_setting, show_file_icons, show_folder_icons) = { let settings = settings::get::(cx); - (settings.git_status, settings.file_icons) + (settings.git_status, settings.file_icons, settings.folder_icons) }; if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { let snapshot = worktree.read(cx).snapshot(); @@ -1193,10 +1193,18 @@ impl ProjectPanel { for entry in visible_worktree_entries[entry_range].iter() { let status = git_status_setting.then(|| entry.git_status).flatten(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - let icon = show_file_icons.then(|| match entry.kind { - EntryKind::File(_) => FileAssociations::get_icon(&entry.path, cx), - _ => FileAssociations::get_folder_icon(is_expanded, cx), - }); + let icon = match entry.kind { + EntryKind::File(_) => if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + _ => if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + }, + }; let mut details = EntryDetails { filename: entry @@ -1258,7 +1266,6 @@ impl ProjectPanel { style: &ProjectPanelEntry, cx: &mut ViewContext, ) -> AnyElement { - let kind = details.kind; let show_editor = details.is_editing && !details.is_processing; let mut filename_text_style = style.text.clone(); @@ -1282,26 +1289,14 @@ impl ProjectPanel { .aligned() .constrained() .with_width(style.icon_size) - } else if kind.is_dir() { - if details.is_expanded { - Svg::new("icons/chevron_down_8.svg").with_color(style.chevron_color) - } else { - Svg::new("icons/chevron_right_8.svg").with_color(style.chevron_color) - } - .constrained() - .with_max_width(style.chevron_size) - .with_max_height(style.chevron_size) - .aligned() - .constrained() - .with_width(style.chevron_size) - } else { + } else { Empty::new() .constrained() - .with_max_width(style.chevron_size) - .with_max_height(style.chevron_size) + .with_max_width(style.icon_size) + .with_max_height(style.icon_size) .aligned() .constrained() - .with_width(style.chevron_size) + .with_width(style.icon_size) }) .with_child(if show_editor && editor.is_some() { ChildView::new(editor.as_ref().unwrap(), cx) From 0e068a644f6448bd50cf939ea5e027c69bf412fc Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:23:47 -0600 Subject: [PATCH 067/124] organize settings --- .../project_panel/src/project_panel_settings.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index f0d60d7f4f..126433e5a3 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -12,18 +12,22 @@ pub enum ProjectPanelDockPosition { #[derive(Deserialize, Debug)] pub struct ProjectPanelSettings { - pub git_status: bool, - pub file_icons: bool, - pub dock: ProjectPanelDockPosition, pub default_width: f32, + pub dock: ProjectPanelDockPosition, + pub file_icons: bool, + pub folder_icons: bool, + pub git_status: bool, + pub indent_size: f32, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { - pub git_status: Option, - pub file_icons: Option, - pub dock: Option, pub default_width: Option, + pub dock: Option, + pub file_icons: Option, + pub folder_icons: Option, + pub git_status: Option, + pub indent_size: Option, } impl Setting for ProjectPanelSettings { From c56d62fd847d9b3ba611c9fc8e20be061b145c6d Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:29:50 -0600 Subject: [PATCH 068/124] gitmodules to git icon --- assets/icons/file_icons/file_types.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index b53d263063..0ccf9c2bb7 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -17,6 +17,7 @@ "fish": "terminal", "gitattributes": "vcs", "gitignore": "vcs", + "gitmodules": "vcs", "gif": "image", "go": "code", "h": "code", From 95947f6d3a2d18e25e7e2fef28e6d0fec5c944ab Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:29:57 -0600 Subject: [PATCH 069/124] icon adjustment --- assets/icons/file_icons/file.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/icons/file_icons/file.svg b/assets/icons/file_icons/file.svg index e05a47548e..bfffe03684 100644 --- a/assets/icons/file_icons/file.svg +++ b/assets/icons/file_icons/file.svg @@ -1,5 +1,5 @@ - + From abb145da702637e37fd54b0244056547f3f5ae65 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 12:30:35 -0600 Subject: [PATCH 070/124] add indent size to project panel settings --- assets/settings/default.json | 14 ++++++++------ crates/project_panel/src/project_panel.rs | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 343abbf077..6dc573ddb6 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -102,16 +102,18 @@ "show_other_hints": true }, "project_panel": { - // Whether to show the git status in the project panel. - "git_status": true, + // Default width of the project panel. + "default_width": 240, + // Where to dock project panel. Can be 'left' or 'right'. + "dock": "left", // Whether to show file icons in the project panel. "file_icons": true, // Whether to show folder icons or chevrons for directories in the project panel. "folder_icons": true, - // Where to dock project panel. Can be 'left' or 'right'. - "dock": "left", - // Default width of the project panel. - "default_width": 240 + // Whether to show the git status in the project panel. + "git_status": true, + // Amount of indentation for nested items. + "indent_size": 20 }, "assistant": { // Where to dock the assistant. Can be 'left', 'right' or 'bottom'. diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 014649f9a8..8164b2860a 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1332,7 +1332,8 @@ impl ProjectPanel { ) -> AnyElement { let kind = details.kind; let path = details.path.clone(); - let padding = theme.container.padding.left + details.depth as f32 * theme.indent_width; + let settings = settings::get::(cx); + let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size; let entry_style = if details.is_cut { &theme.cut_entry From 1242b5b4a20ba2a04b918dd79844b196119730b1 Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 13:13:44 -0600 Subject: [PATCH 071/124] Solid tab on folder icon --- assets/icons/file_icons/folder.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/icons/file_icons/folder.svg b/assets/icons/file_icons/folder.svg index d890160100..fd45ab1c44 100644 --- a/assets/icons/file_icons/folder.svg +++ b/assets/icons/file_icons/folder.svg @@ -1,5 +1,5 @@ - + From 7d3d54652bd17a3eea52d25e81459fccbde7d4bc Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 20 Jul 2023 15:54:26 -0400 Subject: [PATCH 072/124] Remove unused method --- crates/call/src/call.rs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 3fc76b964d..2defd6b40f 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -284,21 +284,6 @@ impl ActiveCall { } } - pub fn toggle_screen_sharing(&self, cx: &mut AppContext) { - if let Some(room) = self.room().cloned() { - let toggle_screen_sharing = room.update(cx, |room, cx| { - if room.is_screen_sharing() { - self.report_call_event("disable screen share", cx); - Task::ready(room.unshare_screen(cx)) - } else { - self.report_call_event("enable screen share", cx); - room.share_screen(cx) - } - }); - toggle_screen_sharing.detach_and_log_err(cx); - } - } - pub fn share_project( &mut self, project: ModelHandle, From 429daf5f8cba347937f928a30afad91eb7807bea Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 20 Jul 2023 16:00:11 -0400 Subject: [PATCH 073/124] Add microphone events to calls --- crates/collab_ui/src/collab_ui.rs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index dbdeb45573..7d1aa5d1b0 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -64,10 +64,24 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { } pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { + let call = ActiveCall::global(cx).read(cx); if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - room.update(cx, Room::toggle_mute) - .map(|task| task.detach_and_log_err(cx)) - .log_err(); + let client = call.client(); + room.update(cx, |room, cx| { + if room.is_muted() { + ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx); + } else { + ActiveCall::report_call_event_for_room( + "disable microphone", + room.id(), + &client, + cx, + ); + } + room.toggle_mute(cx) + }) + .map(|task| task.detach_and_log_err(cx)) + .log_err(); } } From 719c56734ab5cde6907dfbd3d002779286a01aa2 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 20 Jul 2023 16:21:21 -0400 Subject: [PATCH 074/124] Reuse previously-obtained call object --- crates/collab_ui/src/collab_ui.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 7d1aa5d1b0..df4b502391 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -65,7 +65,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { let call = ActiveCall::global(cx).read(cx); - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { + if let Some(room) = call.room().cloned() { let client = call.client(); room.update(cx, |room, cx| { if room.is_muted() { From 6b95ac9b26982348e53b517607342ae51c48a202 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 20 Jul 2023 13:45:19 -0700 Subject: [PATCH 075/124] fmt --- crates/project_panel/src/project_panel.rs | 30 ++++++++++++++--------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8164b2860a..8097f5ecfd 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1178,7 +1178,11 @@ impl ProjectPanel { let end_ix = range.end.min(ix + visible_worktree_entries.len()); let (git_status_setting, show_file_icons, show_folder_icons) = { let settings = settings::get::(cx); - (settings.git_status, settings.file_icons, settings.folder_icons) + ( + settings.git_status, + settings.file_icons, + settings.folder_icons, + ) }; if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { let snapshot = worktree.read(cx).snapshot(); @@ -1194,16 +1198,20 @@ impl ProjectPanel { let status = git_status_setting.then(|| entry.git_status).flatten(); let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); let icon = match entry.kind { - EntryKind::File(_) => if show_file_icons { - Some(FileAssociations::get_icon(&entry.path, cx)) - } else { - None + EntryKind::File(_) => { + if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + } + _ => { + if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + } } - _ => if show_folder_icons { - Some(FileAssociations::get_folder_icon(is_expanded, cx)) - } else { - Some(FileAssociations::get_chevron_icon(is_expanded, cx)) - }, }; let mut details = EntryDetails { @@ -1289,7 +1297,7 @@ impl ProjectPanel { .aligned() .constrained() .with_width(style.icon_size) - } else { + } else { Empty::new() .constrained() .with_max_width(style.icon_size) From 0769458ae462e711a984d04aad1b75e368ac76df Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 15:04:23 -0600 Subject: [PATCH 076/124] Detail adjustments --- assets/icons/file_icons/archive.svg | 2 +- assets/icons/file_icons/folder_open.svg | 2 +- assets/icons/file_icons/lock.svg | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/assets/icons/file_icons/archive.svg b/assets/icons/file_icons/archive.svg index 820c5846ba..35e3dc59bd 100644 --- a/assets/icons/file_icons/archive.svg +++ b/assets/icons/file_icons/archive.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/folder_open.svg b/assets/icons/file_icons/folder_open.svg index bf64f6ee39..55c7d51649 100644 --- a/assets/icons/file_icons/folder_open.svg +++ b/assets/icons/file_icons/folder_open.svg @@ -1,5 +1,5 @@ - + diff --git a/assets/icons/file_icons/lock.svg b/assets/icons/file_icons/lock.svg index b6aa3394ef..14fed3941a 100644 --- a/assets/icons/file_icons/lock.svg +++ b/assets/icons/file_icons/lock.svg @@ -1,6 +1,6 @@ - + From 1d1da74d72d705070fd931ccc21fc162cef68e4b Mon Sep 17 00:00:00 2001 From: Derek Briggs Date: Thu, 20 Jul 2023 15:05:26 -0600 Subject: [PATCH 077/124] Adjustment --- assets/icons/file_icons/image.svg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/icons/file_icons/image.svg b/assets/icons/file_icons/image.svg index c258170dae..d9d5b82af1 100644 --- a/assets/icons/file_icons/image.svg +++ b/assets/icons/file_icons/image.svg @@ -1,6 +1,6 @@ - + From a9bfe973617bc088d84c08326309264f882642bf Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 20 Jul 2023 16:39:13 -0700 Subject: [PATCH 078/124] Add wrap guides and associated settings --- assets/settings/default.json | 13 ++++---- crates/editor/src/editor.rs | 14 +++++++++ crates/editor/src/element.rs | 39 +++++++++++++++++++++--- crates/language/src/language_settings.rs | 9 ++++++ crates/theme/src/theme.rs | 2 ++ styles/src/style_tree/editor.ts | 2 ++ 6 files changed, 69 insertions(+), 10 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 6dc573ddb6..2ae8d5c4a8 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -50,6 +50,13 @@ // Whether to pop the completions menu while typing in an editor without // explicitly requesting it. "show_completions_on_input": true, + // Whether to show wrap guides in the editor. Setting this to true will + // show a guide at the 'preferred_line_length' value if softwrap is set to + // 'preferred_line_length', and will show any additional guides as specified + // by the 'wrap_guides' setting. + "show_wrap_guides": true, + // Character counts at which to show wrap guides in the editor. + "wrap_guides": [], // Whether to use additional LSP queries to format (and amend) the code after // every "trigger" symbol input, defined by LSP server capabilities. "use_on_type_format": true, @@ -356,12 +363,6 @@ // LSP Specific settings. "lsp": { // Specify the LSP name as a key here. - // As of 8/10/22, supported LSPs are: - // pyright - // gopls - // rust-analyzer - // typescript-language-server - // vscode-json-languageserver // "rust-analyzer": { // //These initialization options are merged into Zed's defaults // "initialization_options": { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b790543ee5..87ba250a88 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7086,6 +7086,20 @@ impl Editor { .text() } + pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { + let mut wrap_guides = smallvec::smallvec![]; + + let settings = self.buffer.read(cx).settings_at(0, cx); + if settings.show_wrap_guides { + if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { + wrap_guides.push((soft_wrap as usize, true)); + } + wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false))) + } + + wrap_guides + } + pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { let settings = self.buffer.read(cx).settings_at(0, cx); let mode = self diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1189503c58..3f84d7e503 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -541,6 +541,25 @@ impl EditorElement { corner_radius: 0., }); } + + for (wrap_position, active) in layout.wrap_guides.iter() { + let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.; + let color = if *active { + self.style.active_wrap_guide + } else { + self.style.wrap_guide + }; + scene.push_quad(Quad { + bounds: RectF::new( + vec2f(x, text_bounds.origin_y()), + vec2f(1., text_bounds.height()), + ), + background: Some(color), + border: Border::new(0., Color::transparent_black()), + corner_radius: 0., + }); + } + } } @@ -1320,16 +1339,15 @@ impl EditorElement { } } - fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { - let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; + fn column_pixels(&self, column: usize, cx: &ViewContext) -> f32 { let style = &self.style; cx.text_layout_cache() .layout_str( - "1".repeat(digit_count).as_str(), + " ".repeat(column).as_str(), style.text.font_size, &[( - digit_count, + column, RunStyle { font_id: style.text.font_id, color: Color::black(), @@ -1340,6 +1358,11 @@ impl EditorElement { .width() } + fn max_line_number_width(&self, snapshot: &EditorSnapshot, cx: &ViewContext) -> f32 { + let digit_count = (snapshot.max_buffer_row() as f32 + 1.).log10().floor() as usize + 1; + self.column_pixels(digit_count, cx) + } + //Folds contained in a hunk are ignored apart from shrinking visual size //If a fold contains any hunks then that fold line is marked as modified fn layout_git_gutters( @@ -2025,6 +2048,12 @@ impl Element for EditorElement { } }; + let wrap_guides = editor + .wrap_guides(cx) + .iter() + .map(|(guide, active)| (self.column_pixels(*guide, cx), *active)) + .collect(); + let scroll_height = (snapshot.max_point().row() + 1) as f32 * line_height; if let EditorMode::AutoHeight { max_lines } = snapshot.mode { size.set_y( @@ -2385,6 +2414,7 @@ impl Element for EditorElement { snapshot, }), visible_display_row_range: start_row..end_row, + wrap_guides, gutter_size, gutter_padding, text_size, @@ -2535,6 +2565,7 @@ pub struct LayoutState { gutter_margin: f32, text_size: Vector2F, mode: EditorMode, + wrap_guides: SmallVec<[(f32, bool); 2]>, visible_display_row_range: Range, active_rows: BTreeMap, highlighted_rows: Option>, diff --git a/crates/language/src/language_settings.rs b/crates/language/src/language_settings.rs index 820217567a..c3f706802a 100644 --- a/crates/language/src/language_settings.rs +++ b/crates/language/src/language_settings.rs @@ -44,6 +44,8 @@ pub struct LanguageSettings { pub hard_tabs: bool, pub soft_wrap: SoftWrap, pub preferred_line_length: u32, + pub show_wrap_guides: bool, + pub wrap_guides: Vec, pub format_on_save: FormatOnSave, pub remove_trailing_whitespace_on_save: bool, pub ensure_final_newline_on_save: bool, @@ -84,6 +86,10 @@ pub struct LanguageSettingsContent { #[serde(default)] pub preferred_line_length: Option, #[serde(default)] + pub show_wrap_guides: Option, + #[serde(default)] + pub wrap_guides: Option>, + #[serde(default)] pub format_on_save: Option, #[serde(default)] pub remove_trailing_whitespace_on_save: Option, @@ -378,6 +384,9 @@ fn merge_settings(settings: &mut LanguageSettings, src: &LanguageSettingsContent merge(&mut settings.tab_size, src.tab_size); merge(&mut settings.hard_tabs, src.hard_tabs); merge(&mut settings.soft_wrap, src.soft_wrap); + merge(&mut settings.show_wrap_guides, src.show_wrap_guides); + merge(&mut settings.wrap_guides, src.wrap_guides.clone()); + merge( &mut settings.preferred_line_length, src.preferred_line_length, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 29c0d9ce8e..81ae7a65ca 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -691,6 +691,8 @@ pub struct Editor { pub document_highlight_read_background: Color, pub document_highlight_write_background: Color, pub diff: DiffStyle, + pub wrap_guide: Color, + pub active_wrap_guide: Color, pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 7e20f09b32..0f87420610 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -170,6 +170,8 @@ export default function editor(): any { line_number: with_opacity(foreground(layer), 0.35), line_number_active: foreground(layer), rename_fade: 0.6, + wrap_guide: with_opacity(foreground(layer), 0.1), + active_wrap_guide: with_opacity(foreground(layer), 0.1), unnecessary_code_fade: 0.5, selection: theme.players[0], whitespace: theme.ramps.neutral(0.5).hex(), From 05a84093635f9b7f76d9a8f664c23de8b71e8e16 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Thu, 20 Jul 2023 16:45:41 -0700 Subject: [PATCH 079/124] bump the brightness of the active wrap guide --- crates/editor/src/element.rs | 1 - styles/src/style_tree/editor.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3f84d7e503..b48fa5b56d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -559,7 +559,6 @@ impl EditorElement { corner_radius: 0., }); } - } } diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 0f87420610..acf983e8be 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -171,7 +171,7 @@ export default function editor(): any { line_number_active: foreground(layer), rename_fade: 0.6, wrap_guide: with_opacity(foreground(layer), 0.1), - active_wrap_guide: with_opacity(foreground(layer), 0.1), + active_wrap_guide: with_opacity(foreground(layer), 0.2), unnecessary_code_fade: 0.5, selection: theme.players[0], whitespace: theme.ramps.neutral(0.5).hex(), From 73379100340272b8ae748bd0c2f7aff69f990b4c Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 19:41:38 -0600 Subject: [PATCH 080/124] Fix enter in search --- assets/keymaps/vim.json | 2 +- crates/vim/src/editor_events.rs | 4 ++-- crates/vim/src/normal/search.rs | 15 +++++++++++++++ crates/vim/src/state.rs | 1 + crates/vim/src/vim.rs | 21 ++++++++++++++++----- 5 files changed, 35 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 8fb174c71e..7312c1e2f8 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -426,7 +426,7 @@ } }, { - "context": "BufferSearchBar", + "context": "BufferSearchBar > VimEnabled", "bindings": { "enter": "vim::SearchSubmit", "escape": "buffer_search::Dismiss" diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index a11f1cc182..60e63f9823 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -13,7 +13,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { cx.update_window(previously_active_editor.window_id(), |cx| { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |previously_active_editor, cx| { - Vim::unhook_vim_settings(previously_active_editor, cx); + vim.unhook_vim_settings(previously_active_editor, cx) }); }); }); @@ -35,7 +35,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) { } } - editor.update(cx, |editor, cx| Vim::unhook_vim_settings(editor, cx)) + editor.update(cx, |editor, cx| vim.unhook_vim_settings(editor, cx)) }); }); } diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index cae64a40a6..1e3a64c3a5 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -282,4 +282,19 @@ mod test { cx.simulate_keystrokes(["enter"]); cx.assert_state("aa\nˇbb\ndd\ncc\nbb\n", Mode::Normal); } + + #[gpui::test] + async fn test_non_vim_search( + cx: &mut gpui::TestAppContext, + deterministic: Arc, + ) { + let mut cx = VimTestContext::new(cx, false).await; + cx.set_state("ˇone one one one", Mode::Normal); + cx.simulate_keystrokes(["cmd-f"]); + deterministic.run_until_parked(); + + cx.assert_editor_state("«oneˇ» one one one"); + cx.simulate_keystrokes(["enter"]); + cx.assert_editor_state("one «oneˇ» one one"); + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 6434b710b2..23471066cd 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -91,6 +91,7 @@ impl VimState { pub fn keymap_context_layer(&self) -> KeymapContext { let mut context = KeymapContext::default(); + context.add_identifier("VimEnabled"); context.add_key( "vim_mode", match self.mode { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index ada8f2c1de..69b94428dd 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -14,8 +14,8 @@ use anyhow::Result; use collections::CommandPaletteFilter; use editor::{Bias, Editor, EditorMode, Event}; use gpui::{ - actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, - WindowContext, + actions, impl_actions, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, }; use language::CursorShape; use motion::Motion; @@ -304,17 +304,28 @@ impl Vim { // Note: set_collapse_matches is not in unhook_vim_settings, as that method is called on blur, // but we need collapse_matches to persist when the search bar is focused. editor.set_collapse_matches(false); - Self::unhook_vim_settings(editor, cx); + self.unhook_vim_settings(editor, cx); } }); } - fn unhook_vim_settings(editor: &mut Editor, cx: &mut ViewContext) { + fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext) { editor.set_cursor_shape(CursorShape::Bar, cx); editor.set_clip_at_line_ends(false, cx); editor.set_input_enabled(true); editor.selections.line_mode = false; - editor.remove_keymap_context_layer::(cx); + + // we set the VimEnabled context on all editors so that we + // can distinguish between vim mode and non-vim mode in the BufferSearchBar. + // This is a bit of a hack, but currently the search crate does not depend on vim, + // and it seems nice to keep it that way. + if self.enabled { + let mut context = KeymapContext::default(); + context.add_identifier("VimEnabled"); + editor.set_keymap_context_layer::(context, cx) + } else { + editor.remove_keymap_context_layer::(cx); + } } } From d98fcc4402ca030194181c35e41eb80d433b954f Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 21 Jul 2023 02:44:44 -0400 Subject: [PATCH 081/124] Add key binding to close all docks --- assets/keymaps/default.json | 1 + crates/workspace/src/workspace.rs | 18 ++++++++++++++++++ crates/zed/src/menus.rs | 1 + 3 files changed, 20 insertions(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 883b0c1872..7f420e6d93 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -406,6 +406,7 @@ "cmd-b": "workspace::ToggleLeftDock", "cmd-r": "workspace::ToggleRightDock", "cmd-j": "workspace::ToggleBottomDock", + "alt-cmd-y": "workspace::CloseAllDocks", "cmd-shift-f": "workspace::NewSearch", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 0ebd01e1f7..c64b5189e1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -141,6 +141,7 @@ actions!( ToggleLeftDock, ToggleRightDock, ToggleBottomDock, + CloseAllDocks, ] ); @@ -281,6 +282,9 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { workspace.toggle_dock(DockPosition::Bottom, cx); }); + cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + workspace.close_all_docks(cx); + }); cx.add_action(Workspace::activate_pane_at_index); cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { workspace.reopen_closed_item(cx).detach(); @@ -1670,6 +1674,20 @@ impl Workspace { self.serialize_workspace(cx); } + pub fn close_all_docks(&mut self, cx: &mut ViewContext) { + let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; + + for dock in docks { + dock.update(cx, |dock, cx| { + dock.set_open(false, cx); + }); + } + + cx.focus_self(); + cx.notify(); + self.serialize_workspace(cx); + } + /// Transfer focus to the panel of the given type. pub fn focus_panel(&mut self, cx: &mut ViewContext) -> Option> { self.focus_or_unfocus_panel::(cx, |_, _| true)? diff --git a/crates/zed/src/menus.rs b/crates/zed/src/menus.rs index 9112cd207b..22a260b588 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -93,6 +93,7 @@ pub fn menus() -> Vec> { MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), + MenuItem::action("Close All Docks", workspace::CloseAllDocks), MenuItem::submenu(Menu { name: "Editor Layout", items: vec![ From cd3620692b1c7a64379ebc0226c6a1d4e05a0962 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 21 Jul 2023 11:28:56 +0300 Subject: [PATCH 082/124] Do not highlight fake URLs in terminal --- crates/terminal/src/terminal.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3a64cff24f..f81af1319e 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -74,7 +74,7 @@ const DEBUG_LINE_HEIGHT: f32 = 5.; lazy_static! { // Regex Copied from alacritty's ui_config.rs - static ref URL_REGEX: RegexSearch = RegexSearch::new("(ipfs:|ipns:|magnet:|mailto:|gemini:|gopher:|https:|http:|news:|file:|git:|ssh:|ftp:)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>\"\\s{-}\\^⟨⟩`]+").unwrap(); + static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); } @@ -875,8 +875,10 @@ impl Terminal { } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { let maybe_url_or_path = term.bounds_to_string(*word_match.start(), *word_match.end()); - let is_url = regex_match_at(term, point, &URL_REGEX).is_some(); - + let is_url = match regex_match_at(term, point, &URL_REGEX) { + Some(url_match) => url_match == word_match, + None => false, + }; Some((maybe_url_or_path, is_url, word_match)) } else { None From 807279208d2fd3c08da0a10bca47c3f1d5df0b9d Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 21 Jul 2023 09:08:40 -0600 Subject: [PATCH 083/124] Fix shift-enter in search If you want to type a newline in an auto_height editor, ctrl and ctrl-shift are your friends. --- assets/keymaps/default.json | 4 ++-- crates/vim/src/normal/search.rs | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 883b0c1872..7ebf5004dd 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -195,8 +195,8 @@ { "context": "Editor && mode == auto_height", "bindings": { - "shift-enter": "editor::Newline", - "cmd-shift-enter": "editor::NewlineBelow" + "ctrl-enter": "editor::Newline", + "ctrl-shift-enter": "editor::NewlineBelow" } }, { diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 1e3a64c3a5..d584c575d2 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -296,5 +296,7 @@ mod test { cx.assert_editor_state("«oneˇ» one one one"); cx.simulate_keystrokes(["enter"]); cx.assert_editor_state("one «oneˇ» one one"); + cx.simulate_keystrokes(["shift-enter"]); + cx.assert_editor_state("«oneˇ» one one one"); } } From 8ba69c15d1f5014d31fccb8a69eb19cdc330c6ac Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 19 Jul 2023 18:54:29 -0600 Subject: [PATCH 084/124] refactor: Remove G/Z Namespace support This previously enabled things like `d g g` to work, but we can fix that instead by not clearing out pending vim state on change. Either way, it is unnecessary and causes some user-confusion (zed-industries/community#176), so remove this code for now; and use comments to organize the file a bit instead. --- assets/keymaps/vim.json | 67 ++++++++++++++-------------------------- crates/vim/src/motion.rs | 5 ++- crates/vim/src/normal.rs | 25 ++------------- crates/vim/src/state.rs | 9 ------ crates/vim/src/vim.rs | 7 +++-- 5 files changed, 34 insertions(+), 79 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 7312c1e2f8..5aa448e9d1 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -2,12 +2,6 @@ { "context": "Editor && VimControl && !VimWaiting && !menu", "bindings": { - "g": [ - "vim::PushOperator", - { - "Namespace": "G" - } - ], "i": [ "vim::PushOperator", { @@ -110,6 +104,30 @@ "*": "vim::MoveToNext", "#": "vim::MoveToPrev", "0": "vim::StartOfLine", // When no number operator present, use start of line motion + // "g" commands + "g g": "vim::StartOfDocument", + "g h": "editor::Hover", + "g t": "pane::ActivateNextItem", + "g shift-t": "pane::ActivatePrevItem", + "g d": "editor::GoToDefinition", + "g shift-d": "editor::GoToTypeDefinition", + "g *": [ + "vim::MoveToNext", + { + "partialWord": true + } + ], + "g #": [ + "vim::MoveToPrev", + { + "partialWord": true + } + ], + // z commands + "z t": "editor::ScrollCursorTop", + "z z": "editor::ScrollCursorCenter", + "z b": "editor::ScrollCursorBottom", + // Count support "1": [ "vim::Number", 1 @@ -234,12 +252,6 @@ "vim::PushOperator", "Yank" ], - "z": [ - "vim::PushOperator", - { - "Namespace": "Z" - } - ], "i": [ "vim::SwitchMode", "Insert" @@ -306,29 +318,6 @@ ] } }, - { - "context": "Editor && vim_operator == g", - "bindings": { - "g": "vim::StartOfDocument", - "h": "editor::Hover", - "t": "pane::ActivateNextItem", - "shift-t": "pane::ActivatePrevItem", - "d": "editor::GoToDefinition", - "shift-d": "editor::GoToTypeDefinition", - "*": [ - "vim::MoveToNext", - { - "partialWord": true - } - ], - "#": [ - "vim::MoveToPrev", - { - "partialWord": true - } - ] - } - }, { "context": "Editor && vim_operator == c", "bindings": { @@ -347,14 +336,6 @@ "y": "vim::CurrentLine" } }, - { - "context": "Editor && vim_operator == z", - "bindings": { - "t": "editor::ScrollCursorTop", - "z": "editor::ScrollCursorCenter", - "b": "editor::ScrollCursorBottom" - } - }, { "context": "Editor && VimObject", "bindings": { diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 07b095dd5e..fb742af3ab 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -127,9 +127,8 @@ pub fn init(cx: &mut AppContext) { } pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { - if let Some(Operator::Namespace(_)) - | Some(Operator::FindForward { .. }) - | Some(Operator::FindBackward { .. }) = Vim::read(cx).active_operator() + if let Some(Operator::FindForward { .. }) | Some(Operator::FindBackward { .. }) = + Vim::read(cx).active_operator() { Vim::update(cx, |vim, cx| vim.pop_operator(cx)); } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 8dcaa5008e..e0765839a0 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -107,7 +107,7 @@ pub fn normal_motion( Some(Operator::Delete) => delete_motion(vim, motion, times, cx), Some(Operator::Yank) => yank_motion(vim, motion, times, cx), Some(operator) => { - // Can't do anything for text objects or namespace operators. Ignoring + // Can't do anything for text objects, Ignoring error!("Unexpected normal mode motion operator: {:?}", operator) } } @@ -441,11 +441,8 @@ mod test { use indoc::indoc; use crate::{ - state::{ - Mode::{self, *}, - Namespace, Operator, - }, - test::{ExemptionFeatures, NeovimBackedTestContext, VimTestContext}, + state::Mode::{self, *}, + test::{ExemptionFeatures, NeovimBackedTestContext}, }; #[gpui::test] @@ -610,22 +607,6 @@ mod test { .await; } - #[gpui::test] - async fn test_g_prefix_and_abort(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; - - // Can abort with escape to get back to normal mode - cx.simulate_keystroke("g"); - assert_eq!(cx.mode(), Normal); - assert_eq!( - cx.active_operator(), - Some(Operator::Namespace(Namespace::G)) - ); - cx.simulate_keystroke("escape"); - assert_eq!(cx.mode(), Normal); - assert_eq!(cx.active_operator(), None); - } - #[gpui::test] async fn test_gg(cx: &mut gpui::TestAppContext) { let mut cx = NeovimBackedTestContext::new(cx).await; diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index 23471066cd..bf644c08cc 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -16,16 +16,9 @@ impl Default for Mode { } } -#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] -pub enum Namespace { - G, - Z, -} - #[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize)] pub enum Operator { Number(usize), - Namespace(Namespace), Change, Delete, Yank, @@ -126,8 +119,6 @@ impl Operator { pub fn id(&self) -> &'static str { match self { Operator::Number(_) => "n", - Operator::Namespace(Namespace::G) => "g", - Operator::Namespace(Namespace::Z) => "z", Operator::Object { around: false } => "i", Operator::Object { around: true } => "a", Operator::Change => "c", diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 69b94428dd..82d2e752c3 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -14,7 +14,7 @@ use anyhow::Result; use collections::CommandPaletteFilter; use editor::{Bias, Editor, EditorMode, Event}; use gpui::{ - actions, impl_actions, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext, + actions, impl_actions,keymap_matcher::MatchResult, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::CursorShape; @@ -90,7 +90,10 @@ pub fn init(cx: &mut AppContext) { } pub fn observe_keystrokes(cx: &mut WindowContext) { - cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| { + cx.observe_keystrokes(|_keystroke, result, handled_by, cx| { + if result == &MatchResult::Pending { + return true; + } if let Some(handled_by) = handled_by { // Keystroke is handled by the vim system, so continue forward if handled_by.namespace() == "vim" { From a50d30bf8e47d50a12ff3ab5803dc1ad6577c5af Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 09:08:07 -0600 Subject: [PATCH 085/124] Quality of life shortcuts for code actions --- assets/keymaps/vim.json | 5 ++++- crates/vim/src/normal.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 5aa448e9d1..da850662f2 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -111,6 +111,8 @@ "g shift-t": "pane::ActivatePrevItem", "g d": "editor::GoToDefinition", "g shift-d": "editor::GoToTypeDefinition", + "g .": "editor::ToggleCodeActions", // zed specific + "g shift-a": "editor::FindAllReferences", // zed specific "g *": [ "vim::MoveToNext", { @@ -321,7 +323,8 @@ { "context": "Editor && vim_operator == c", "bindings": { - "c": "vim::CurrentLine" + "c": "vim::CurrentLine", + "d": "editor::Rename" // zed specific } }, { diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e0765839a0..79c990ffeb 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -441,7 +441,7 @@ mod test { use indoc::indoc; use crate::{ - state::Mode::{self, *}, + state::Mode::{self}, test::{ExemptionFeatures, NeovimBackedTestContext}, }; From 4772e4ccee6497af670942011ee1c41fde6fb433 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 15:22:15 -0600 Subject: [PATCH 086/124] vim: add , and ; --- assets/keymaps/vim.json | 7 ++ crates/vim/src/motion.rs | 64 ++++++++++++++++++- crates/vim/src/state.rs | 4 ++ crates/vim/src/vim.rs | 12 ++-- .../vim/test_data/test_comma_semicolon.json | 17 +++++ 5 files changed, 98 insertions(+), 6 deletions(-) create mode 100644 crates/vim/test_data/test_comma_semicolon.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index da850662f2..94a271f037 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -292,6 +292,13 @@ "backwards": true } ], + ";": "vim::RepeatFind", + ",": [ + "vim::RepeatFind", + { + "backwards": true + } + ], "ctrl-f": "vim::PageDown", "pagedown": "vim::PageDown", "ctrl-b": "vim::PageUp", diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index fb742af3ab..b8bd256d8a 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -62,6 +62,12 @@ struct PreviousWordStart { ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +struct RepeatFind { + #[serde(default)] + backwards: bool, +} + actions!( vim, [ @@ -82,7 +88,10 @@ actions!( NextLineStart, ] ); -impl_actions!(vim, [NextWordStart, NextWordEnd, PreviousWordStart]); +impl_actions!( + vim, + [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] +); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); @@ -123,7 +132,10 @@ pub fn init(cx: &mut AppContext) { &PreviousWordStart { ignore_punctuation }: &PreviousWordStart, cx: _| { motion(Motion::PreviousWordStart { ignore_punctuation }, cx) }, ); - cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx)) + cx.add_action(|_: &mut Workspace, &NextLineStart, cx: _| motion(Motion::NextLineStart, cx)); + cx.add_action(|_: &mut Workspace, action: &RepeatFind, cx: _| { + repeat_motion(action.backwards, cx) + }) } pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { @@ -145,6 +157,35 @@ pub(crate) fn motion(motion: Motion, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| vim.clear_operator(cx)); } +fn repeat_motion(backwards: bool, cx: &mut WindowContext) { + let find = match Vim::read(cx).state.last_find.clone() { + Some(Motion::FindForward { before, text }) => { + if backwards { + Motion::FindBackward { + after: before, + text, + } + } else { + Motion::FindForward { before, text } + } + } + + Some(Motion::FindBackward { after, text }) => { + if backwards { + Motion::FindForward { + before: after, + text, + } + } else { + Motion::FindBackward { after, text } + } + } + _ => return, + }; + + motion(find, cx) +} + // Motion handling is specified here: // https://github.com/vim/vim/blob/master/runtime/doc/motion.txt impl Motion { @@ -742,4 +783,23 @@ mod test { cx.simulate_shared_keystrokes(["%"]).await; cx.assert_shared_state("func boop(ˇ) {\n}").await; } + + #[gpui::test] + async fn test_comma_semicolon(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("ˇone two three four").await; + cx.simulate_shared_keystrokes(["f", "o"]).await; + cx.assert_shared_state("one twˇo three four").await; + cx.simulate_shared_keystrokes([","]).await; + cx.assert_shared_state("ˇone two three four").await; + cx.simulate_shared_keystrokes(["2", ";"]).await; + cx.assert_shared_state("one two three fˇour").await; + cx.simulate_shared_keystrokes(["shift-t", "e"]).await; + cx.assert_shared_state("one two threeˇ four").await; + cx.simulate_shared_keystrokes(["3", ";"]).await; + cx.assert_shared_state("oneˇ two three four").await; + cx.simulate_shared_keystrokes([","]).await; + cx.assert_shared_state("one two thˇree four").await; + } } diff --git a/crates/vim/src/state.rs b/crates/vim/src/state.rs index bf644c08cc..eb52945ced 100644 --- a/crates/vim/src/state.rs +++ b/crates/vim/src/state.rs @@ -3,6 +3,8 @@ use language::CursorShape; use serde::{Deserialize, Serialize}; use workspace::searchable::Direction; +use crate::motion::Motion; + #[derive(Clone, Copy, Debug, PartialEq, Eq, Deserialize, Serialize)] pub enum Mode { Normal, @@ -33,6 +35,8 @@ pub struct VimState { pub mode: Mode, pub operator_stack: Vec, pub search: SearchState, + + pub last_find: Option, } pub struct SearchState { diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 82d2e752c3..e31fa4addd 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -14,8 +14,8 @@ use anyhow::Result; use collections::CommandPaletteFilter; use editor::{Bias, Editor, EditorMode, Event}; use gpui::{ - actions, impl_actions,keymap_matcher::MatchResult, keymap_matcher::KeymapContext, AppContext, Subscription, ViewContext, - ViewHandle, WeakViewHandle, WindowContext, + actions, impl_actions, keymap_matcher::KeymapContext, keymap_matcher::MatchResult, AppContext, + Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use language::CursorShape; use motion::Motion; @@ -246,10 +246,14 @@ impl Vim { match Vim::read(cx).active_operator() { Some(Operator::FindForward { before }) => { - motion::motion(Motion::FindForward { before, text }, cx) + let find = Motion::FindForward { before, text }; + Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + motion::motion(find, cx) } Some(Operator::FindBackward { after }) => { - motion::motion(Motion::FindBackward { after, text }, cx) + let find = Motion::FindBackward { after, text }; + Vim::update(cx, |vim, _| vim.state.last_find = Some(find.clone())); + motion::motion(find, cx) } Some(Operator::Replace) => match Vim::read(cx).state.mode { Mode::Normal => normal_replace(text, cx), diff --git a/crates/vim/test_data/test_comma_semicolon.json b/crates/vim/test_data/test_comma_semicolon.json new file mode 100644 index 0000000000..8cde887ed1 --- /dev/null +++ b/crates/vim/test_data/test_comma_semicolon.json @@ -0,0 +1,17 @@ +{"Put":{"state":"ˇone two three four"}} +{"Key":"f"} +{"Key":"o"} +{"Get":{"state":"one twˇo three four","mode":"Normal"}} +{"Key":","} +{"Get":{"state":"ˇone two three four","mode":"Normal"}} +{"Key":"2"} +{"Key":";"} +{"Get":{"state":"one two three fˇour","mode":"Normal"}} +{"Key":"shift-t"} +{"Key":"e"} +{"Get":{"state":"one two threeˇ four","mode":"Normal"}} +{"Key":"3"} +{"Key":";"} +{"Get":{"state":"oneˇ two three four","mode":"Normal"}} +{"Key":","} +{"Get":{"state":"one two thˇree four","mode":"Normal"}} From 243a1a854eb542bf8748838156f1eb97e3a39632 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 21 Jul 2023 14:25:30 -0400 Subject: [PATCH 087/124] Avoid panic from assuming a vim operator exists on the operator stack --- crates/vim/src/visual.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 5e22e77bf0..d87e4ff974 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -58,7 +58,9 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex pub fn visual_object(object: Object, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { - if let Operator::Object { around } = vim.pop_operator(cx) { + if let Some(Operator::Object { around }) = vim.active_operator() { + vim.pop_operator(cx); + vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { From 595bc16749db7384c32f92ec87719428cbb5a186 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 20 Jul 2023 16:01:01 +0300 Subject: [PATCH 088/124] Add search in directory action in the project panel --- assets/keymaps/default.json | 3 +- crates/project_panel/src/project_panel.rs | 110 +++++++++++++++- crates/search/src/project_search.rs | 152 +++++++++++++++++++++- crates/workspace/src/workspace.rs | 6 +- crates/zed/src/zed.rs | 21 +++ 5 files changed, 287 insertions(+), 5 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 92ae4a81ee..d970df1abd 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -529,7 +529,8 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "backspace": "project_panel::Delete", - "alt-cmd-r": "project_panel::RevealInFinder" + "alt-cmd-r": "project_panel::RevealInFinder", + "alt-shift-f": "project_panel::NewSearchInDirectory" } }, { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8097f5ecfd..6f5ae99df9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -125,7 +125,8 @@ actions!( Paste, Delete, Rename, - ToggleFocus + ToggleFocus, + NewSearchInDirectory, ] ); @@ -151,6 +152,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { cx.add_action(ProjectPanel::copy_path); cx.add_action(ProjectPanel::copy_relative_path); cx.add_action(ProjectPanel::reveal_in_finder); + cx.add_action(ProjectPanel::new_search_in_directory); cx.add_action( |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { this.paste(action, cx); @@ -169,6 +171,9 @@ pub enum Event { }, DockPositionChanged, Focus, + NewSearchInDirectory { + dir_entry: Entry, + }, } #[derive(Serialize, Deserialize)] @@ -417,6 +422,12 @@ impl ProjectPanel { CopyRelativePath, )); menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); + if entry.is_dir() { + menu_entries.push(ContextMenuItem::action( + "Search inside", + NewSearchInDirectory, + )); + } if let Some(clipboard_entry) = self.clipboard_entry { if clipboard_entry.worktree_id() == worktree.id() { menu_entries.push(ContextMenuItem::action("Paste", Paste)); @@ -928,6 +939,20 @@ impl ProjectPanel { } } + pub fn new_search_in_directory( + &mut self, + _: &NewSearchInDirectory, + cx: &mut ViewContext, + ) { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + cx.emit(Event::NewSearchInDirectory { + dir_entry: entry.clone(), + }); + } + } + } + fn move_entry( &mut self, entry_to_move: ProjectEntryId, @@ -1677,7 +1702,11 @@ mod tests { use project::FakeFs; use serde_json::json; use settings::SettingsStore; - use std::{collections::HashSet, path::Path}; + use std::{ + collections::HashSet, + path::Path, + sync::atomic::{self, AtomicUsize}, + }; use workspace::{pane, AppState}; #[gpui::test] @@ -2516,6 +2545,83 @@ mod tests { ); } + #[gpui::test] + async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/src", + json!({ + "test": { + "first.rs": "// First Rust file", + "second.rs": "// Second Rust file", + "third.rs": "// Third Rust file", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + let new_search_events_count = Arc::new(AtomicUsize::new(0)); + let _subscription = panel.update(cx, |_, cx| { + let subcription_count = Arc::clone(&new_search_events_count); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::NewSearchInDirectory { .. }) { + subcription_count.fetch_add(1, atomic::Ordering::SeqCst); + } + }) + }); + + toggle_expand_dir(&panel, "src/test", cx); + select_path(&panel, "src/test/first.rs", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test", + " first.rs <== selected", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 0, + "Should not trigger new search in directory when called on a file" + ); + + select_path(&panel, "src/test", cx); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v src", + " v test <== selected", + " first.rs", + " second.rs", + " third.rs" + ] + ); + panel.update(cx, |panel, cx| { + panel.new_search_in_directory(&NewSearchInDirectory, cx) + }); + assert_eq!( + new_search_events_count.load(atomic::Ordering::SeqCst), + 1, + "Should trigger new search in directory when called on a directory" + ); + } + fn toggle_expand_dir( panel: &ViewHandle, path: impl AsRef, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index abebb9a48f..9054d9e121 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -18,7 +18,7 @@ use gpui::{ Task, View, ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, }; use menu::Confirm; -use project::{search::SearchQuery, Project}; +use project::{search::SearchQuery, Entry, Project}; use smallvec::SmallVec; use std::{ any::{Any, TypeId}, @@ -501,6 +501,28 @@ impl ProjectSearchView { this } + pub fn new_search_in_directory( + workspace: &mut Workspace, + dir_entry: &Entry, + cx: &mut ViewContext, + ) { + if !dir_entry.is_dir() { + return; + } + let filter_path = dir_entry.path.join("**"); + let Some(filter_str) = filter_path.to_str() else { return; }; + + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); + workspace.add_item(Box::new(search.clone()), cx); + search.update(cx, |search, cx| { + search + .included_files_editor + .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.focus_query_editor(cx) + }); + } + // Re-activate the most recently activated search or the most recent if it has been closed. // If no search exists in the workspace, create a new one. fn deploy( @@ -1414,6 +1436,134 @@ pub mod tests { }); } + #[gpui::test] + async fn test_new_project_search_in_directory( + deterministic: Arc, + cx: &mut TestAppContext, + ) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "a": { + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + }, + "b": { + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }, + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let worktree_id = project.read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }); + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + + let active_item = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_item.is_none(), + "Expected no search panel to be active, but got: {active_item:?}" + ); + + let one_file_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a/one.rs").into(), cx) + .expect("no entry for /a/one.rs file") + }); + assert!(one_file_entry.is_file()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &one_file_entry, cx) + }); + let active_search_entry = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }); + assert!( + active_search_entry.is_none(), + "Expected no search panel to be active for file entry" + ); + + let a_dir_entry = cx.update(|cx| { + workspace + .read(cx) + .project() + .read(cx) + .entry_for_path(&(worktree_id, "a").into(), cx) + .expect("no entry for /a/ directory") + }); + assert!(a_dir_entry.is_dir()); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::new_search_in_directory(workspace, &a_dir_entry, cx) + }); + + let Some(search_view) = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + }) else { + panic!("Search view expected to appear after new search in directory event trigger") + }; + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert!( + search_view.query_editor.is_focused(cx), + "On new search in directory, focus should be moved into query editor" + ); + search_view.excluded_files_editor.update(cx, |editor, cx| { + assert!( + editor.display_text(cx).is_empty(), + "New search in directory should not have any excluded files" + ); + }); + search_view.included_files_editor.update(cx, |editor, cx| { + assert_eq!( + editor.display_text(cx), + a_dir_entry.path.join("**").display().to_string(), + "New search in directory should have included dir entry path" + ); + }); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("const", cx)); + search_view.search(cx); + }); + deterministic.run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst ONE: usize = 1;\n\n\nconst TWO: usize = one::ONE + one::ONE;", + "New search in directory should have a filter that matches a certain directory" + ); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); let fonts = cx.font_cache(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c64b5189e1..5c1a75e97a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -512,7 +512,7 @@ pub struct Workspace { follower_states_by_leader: FollowerStatesByLeader, last_leaders_by_pane: HashMap, PeerId>, window_edited: bool, - active_call: Option<(ModelHandle, Vec)>, + active_call: Option<(ModelHandle, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, @@ -3009,6 +3009,10 @@ impl Workspace { self.database_id } + pub fn push_subscription(&mut self, subscription: Subscription) { + self.subscriptions.push(subscription) + } + fn location(&self, cx: &AppContext) -> Option { let project = self.project().read(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6bbba0bd02..8a2691da15 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -338,6 +338,27 @@ pub fn initialize_workspace( let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let (project_panel, terminal_panel, assistant_panel) = futures::try_join!(project_panel, terminal_panel, assistant_panel)?; + + cx.update(|cx| { + if let Some(workspace) = workspace_handle.upgrade(cx) { + cx.update_window(project_panel.window_id(), |cx| { + workspace.update(cx, |workspace, cx| { + let project_panel_subscription = + cx.subscribe(&project_panel, move |workspace, _, event, cx| { + if let project_panel::Event::NewSearchInDirectory { dir_entry } = + event + { + search::ProjectSearchView::new_search_in_directory( + workspace, dir_entry, cx, + ) + } + }); + workspace.push_subscription(project_panel_subscription); + }); + }); + } + }); + workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); From 2d8159998d03da2ac96fee1e2469cf4ecf9a6e52 Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 21 Jul 2023 16:13:00 -0400 Subject: [PATCH 089/124] Put our downloaded copy of Node in the env for every NPM action Intelephense (PHP language server) has a dependency on `protobufjs` which invokes `node` in the `postinstall` script and if the user did not have a system Node runtime installed that would fail. Have this use our downloaded installation too --- crates/node_runtime/src/node_runtime.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/node_runtime/src/node_runtime.rs b/crates/node_runtime/src/node_runtime.rs index de9cf501ac..94858df880 100644 --- a/crates/node_runtime/src/node_runtime.rs +++ b/crates/node_runtime/src/node_runtime.rs @@ -62,6 +62,14 @@ impl NodeRuntime { args: &[&str], ) -> Result { let attempt = |installation_path: PathBuf| async move { + let mut env_path = installation_path.join("bin").into_os_string(); + if let Some(existing_path) = std::env::var_os("PATH") { + if !existing_path.is_empty() { + env_path.push(":"); + env_path.push(&existing_path); + } + } + let node_binary = installation_path.join("bin/node"); let npm_file = installation_path.join("bin/npm"); @@ -74,6 +82,7 @@ impl NodeRuntime { } let mut command = Command::new(node_binary); + command.env("PATH", env_path); command.arg(npm_file).arg(subcommand).args(args); if let Some(directory) = directory { From 804da68af756c71bf8e4a7eb2a8588bef1b677a1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 21 Jul 2023 23:22:22 +0300 Subject: [PATCH 090/124] When renaming in project panel, select file names without extensions --- crates/project_panel/src/project_panel.rs | 80 +++++++++++++++++------ 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 8097f5ecfd..896e0a75ec 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -4,7 +4,7 @@ mod project_panel_settings; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use drag_and_drop::{DragAndDrop, Draggable}; -use editor::{Cancel, Editor}; +use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; use file_associations::FileAssociations; use futures::stream::StreamExt; @@ -730,13 +730,20 @@ impl ProjectPanel { is_dir: entry.is_dir(), processing_filename: None, }); - let filename = entry + let file_name = entry .path .file_name() - .map_or(String::new(), |s| s.to_string_lossy().to_string()); + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); self.filename_editor.update(cx, |editor, cx| { - editor.set_text(filename, cx); - editor.select_all(&Default::default(), cx); + editor.set_text(file_name, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([0..selection_end]) + }) }); cx.focus(&self.filename_editor); self.update_visible_entries(None, cx); @@ -1913,7 +1920,7 @@ mod tests { .update(cx, |panel, cx| { panel .filename_editor - .update(cx, |editor, cx| editor.set_text("another-filename", cx)); + .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); panel.confirm(&Confirm, cx).unwrap() }) .await @@ -1927,14 +1934,14 @@ mod tests { " v b", " > 3", " > 4", - " another-filename <== selected", + " another-filename.txt <== selected", " > C", " .dockerignore", " the-new-filename", ] ); - select_path(&panel, "root1/b/another-filename", cx); + select_path(&panel, "root1/b/another-filename.txt", cx); panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -1945,7 +1952,7 @@ mod tests { " v b", " > 3", " > 4", - " [EDITOR: 'another-filename'] <== selected", + " [EDITOR: 'another-filename.txt'] <== selected", " > C", " .dockerignore", " the-new-filename", @@ -1953,9 +1960,15 @@ mod tests { ); let confirm = panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("a-different-filename", cx)); + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + + editor.set_text("a-different-filename.tar.gz", cx) + }); panel.confirm(&Confirm, cx).unwrap() }); assert_eq!( @@ -1967,7 +1980,7 @@ mod tests { " v b", " > 3", " > 4", - " [PROCESSING: 'a-different-filename'] <== selected", + " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", " > C", " .dockerignore", " the-new-filename", @@ -1984,13 +1997,42 @@ mod tests { " v b", " > 3", " > 4", - " a-different-filename <== selected", + " a-different-filename.tar.gz <== selected", " > C", " .dockerignore", " the-new-filename", ] ); + panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v root1", + " > .git", + " > a", + " v b", + " > 3", + " > 4", + " [EDITOR: 'a-different-filename.tar.gz'] <== selected", + " > C", + " .dockerignore", + " the-new-filename", + ] + ); + + panel.update(cx, |panel, cx| { + panel.filename_editor.update(cx, |editor, cx| { + let file_name_selections = editor.selections.all::(cx); + assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); + let file_name_selection = &file_name_selections[0]; + assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); + assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + + }); + panel.cancel(&Cancel, cx) + }); + panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2002,7 +2044,7 @@ mod tests { " > [EDITOR: ''] <== selected", " > 3", " > 4", - " a-different-filename", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -2025,7 +2067,7 @@ mod tests { " > [PROCESSING: 'new-dir']", " > 3 <== selected", " > 4", - " a-different-filename", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -2042,7 +2084,7 @@ mod tests { " > 3 <== selected", " > 4", " > new-dir", - " a-different-filename", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -2059,7 +2101,7 @@ mod tests { " > [EDITOR: '3'] <== selected", " > 4", " > new-dir", - " a-different-filename", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] @@ -2077,7 +2119,7 @@ mod tests { " > 3 <== selected", " > 4", " > new-dir", - " a-different-filename", + " a-different-filename.tar.gz", " > C", " .dockerignore", ] From 4bd415f2b6e1c77bf8a238d864eef1e0db8189cc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 20 Jul 2023 13:22:36 -0700 Subject: [PATCH 091/124] Retrieve git statuses in one batch when scanning dirs --- crates/fs/src/repository.rs | 69 ++++----- crates/project/src/worktree.rs | 259 +++++++++++++++------------------ script/zed-with-local-servers | 2 + 3 files changed, 152 insertions(+), 178 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 3826dae2aa..611427c0a8 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -25,24 +25,13 @@ pub struct Branch { #[async_trait::async_trait] pub trait GitRepository: Send { fn reload_index(&self); - fn load_index_text(&self, relative_file_path: &Path) -> Option; - fn branch_name(&self) -> Option; - - fn statuses(&self) -> Option>; - + fn statuses(&self) -> TreeMap; fn status(&self, path: &RepoPath) -> Result>; - - fn branches(&self) -> Result> { - Ok(vec![]) - } - fn change_branch(&self, _: &str) -> Result<()> { - Ok(()) - } - fn create_branch(&self, _: &str) -> Result<()> { - Ok(()) - } + fn branches(&self) -> Result>; + fn change_branch(&self, _: &str) -> Result<()>; + fn create_branch(&self, _: &str) -> Result<()>; } impl std::fmt::Debug for dyn GitRepository { @@ -89,24 +78,22 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> Option> { - let statuses = self.statuses(None).log_err()?; - + fn statuses(&self) -> TreeMap { let mut map = TreeMap::default(); + if let Some(statuses) = self.statuses(None).log_err() { + for status in statuses + .iter() + .filter(|status| !status.status().contains(git2::Status::IGNORED)) + { + let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); + let Some(status) = read_status(status.status()) else { + continue + }; - for status in statuses - .iter() - .filter(|status| !status.status().contains(git2::Status::IGNORED)) - { - let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - let Some(status) = read_status(status.status()) else { - continue - }; - - map.insert(path, status) + map.insert(path, status) + } } - - Some(map) + map } fn status(&self, path: &RepoPath) -> Result> { @@ -213,19 +200,35 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> Option> { - let state = self.state.lock(); + fn statuses(&self) -> TreeMap { let mut map = TreeMap::default(); + let state = self.state.lock(); for (repo_path, status) in state.worktree_statuses.iter() { map.insert(repo_path.to_owned(), status.to_owned()); } - Some(map) + map } fn status(&self, path: &RepoPath) -> Result> { let state = self.state.lock(); Ok(state.worktree_statuses.get(path).cloned()) } + + fn branches(&self) -> Result> { + Ok(vec![]) + } + + fn change_branch(&self, name: &str) -> Result<()> { + let mut state = self.state.lock(); + state.branch_name = Some(name.to_owned()); + Ok(()) + } + + fn create_branch(&self, name: &str) -> Result<()> { + let mut state = self.state.lock(); + state.branch_name = Some(name.to_owned()); + Ok(()) + } } fn check_path_to_repo_path_errors(relative_file_path: &Path) -> Result<()> { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index a1730fd365..2ce1693459 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2015,37 +2015,6 @@ impl LocalSnapshot { entry } - #[must_use = "Changed paths must be used for diffing later"] - fn scan_statuses( - &mut self, - repo_ptr: &dyn GitRepository, - work_directory: &RepositoryWorkDirectory, - ) -> Vec> { - let mut changes = vec![]; - let mut edits = vec![]; - - let statuses = repo_ptr.statuses(); - - for mut entry in self - .descendent_entries(false, false, &work_directory.0) - .cloned() - { - let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else { - continue; - }; - let repo_path = RepoPath(repo_path.to_path_buf()); - let git_file_status = statuses.as_ref().and_then(|s| s.get(&repo_path).copied()); - if entry.git_status != git_file_status { - entry.git_status = git_file_status; - changes.push(entry.path.clone()); - edits.push(Edit::Insert(entry)); - } - } - - self.entries_by_path.edit(edits, &()); - changes - } - fn ancestor_inodes_for_path(&self, path: &Path) -> TreeSet { let mut inodes = TreeSet::default(); for ancestor in path.ancestors().skip(1) { @@ -2189,6 +2158,30 @@ impl BackgroundScannerState { .any(|p| entry.path.starts_with(p)) } + fn enqueue_scan_dir(&self, abs_path: Arc, entry: &Entry, scan_job_tx: &Sender) { + let path = entry.path.clone(); + let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); + let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); + let containing_repository = self + .snapshot + .local_repo_for_path(&path) + .map(|(path, repo)| (path, repo.repo_ptr.lock().statuses())); + if !ancestor_inodes.contains(&entry.inode) { + ancestor_inodes.insert(entry.inode); + scan_job_tx + .try_send(ScanJob { + abs_path, + path, + ignore_stack, + scan_queue: scan_job_tx.clone(), + ancestor_inodes, + is_external: entry.is_external, + containing_repository, + }) + .unwrap(); + } + } + fn reuse_entry_id(&mut self, entry: &mut Entry) { if let Some(removed_entry_id) = self.removed_entry_ids.remove(&entry.inode) { entry.id = removed_entry_id; @@ -2201,7 +2194,7 @@ impl BackgroundScannerState { self.reuse_entry_id(&mut entry); let entry = self.snapshot.insert_entry(entry, fs); if entry.path.file_name() == Some(&DOT_GIT) { - self.build_repository(entry.path.clone(), fs); + self.build_git_repository(entry.path.clone(), fs); } #[cfg(test)] @@ -2215,7 +2208,6 @@ impl BackgroundScannerState { parent_path: &Arc, entries: impl IntoIterator, ignore: Option>, - fs: &dyn Fs, ) { let mut parent_entry = if let Some(parent_entry) = self .snapshot @@ -2244,16 +2236,12 @@ impl BackgroundScannerState { .insert(abs_parent_path, (ignore, false)); } - self.scanned_dirs.insert(parent_entry.id); + let parent_entry_id = parent_entry.id; + self.scanned_dirs.insert(parent_entry_id); let mut entries_by_path_edits = vec![Edit::Insert(parent_entry)]; let mut entries_by_id_edits = Vec::new(); - let mut dotgit_path = None; for entry in entries { - if entry.path.file_name() == Some(&DOT_GIT) { - dotgit_path = Some(entry.path.clone()); - } - entries_by_id_edits.push(Edit::Insert(PathEntry { id: entry.id, path: entry.path.clone(), @@ -2268,9 +2256,6 @@ impl BackgroundScannerState { .edit(entries_by_path_edits, &()); self.snapshot.entries_by_id.edit(entries_by_id_edits, &()); - if let Some(dotgit_path) = dotgit_path { - self.build_repository(dotgit_path, fs); - } if let Err(ix) = self.changed_paths.binary_search(parent_path) { self.changed_paths.insert(ix, parent_path.clone()); } @@ -2346,7 +2331,7 @@ impl BackgroundScannerState { }); match repository { None => { - self.build_repository(dot_git_dir.into(), fs); + self.build_git_repository(dot_git_dir.into(), fs); } Some((entry_id, repository)) => { if repository.git_dir_scan_id == scan_id { @@ -2370,13 +2355,8 @@ impl BackgroundScannerState { .repository_entries .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - let changed_paths = self.snapshot.scan_statuses(&*repository, &work_dir); - util::extend_sorted( - &mut self.changed_paths, - changed_paths, - usize::MAX, - Ord::cmp, - ) + let statuses = repository.statuses(); + self.update_git_statuses(&work_dir, &statuses); } } } @@ -2397,7 +2377,11 @@ impl BackgroundScannerState { snapshot.repository_entries = repository_entries; } - fn build_repository(&mut self, dot_git_path: Arc, fs: &dyn Fs) -> Option<()> { + fn build_git_repository( + &mut self, + dot_git_path: Arc, + fs: &dyn Fs, + ) -> Option<(RepositoryWorkDirectory, TreeMap)> { log::info!("build git repository {:?}", dot_git_path); let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); @@ -2429,9 +2413,8 @@ impl BackgroundScannerState { }, ); - let changed_paths = self - .snapshot - .scan_statuses(repo_lock.deref(), &work_directory); + let statuses = repo_lock.statuses(); + self.update_git_statuses(&work_directory, &statuses); drop(repo_lock); self.snapshot.git_repositories.insert( @@ -2443,8 +2426,36 @@ impl BackgroundScannerState { }, ); - util::extend_sorted(&mut self.changed_paths, changed_paths, usize::MAX, Ord::cmp); - Some(()) + Some((work_directory, statuses)) + } + + fn update_git_statuses( + &mut self, + work_directory: &RepositoryWorkDirectory, + statuses: &TreeMap, + ) { + let mut changes = vec![]; + let mut edits = vec![]; + + for mut entry in self + .snapshot + .descendent_entries(false, false, &work_directory.0) + .cloned() + { + let Ok(repo_path) = entry.path.strip_prefix(&work_directory.0) else { + continue; + }; + let repo_path = RepoPath(repo_path.to_path_buf()); + let git_file_status = statuses.get(&repo_path).copied(); + if entry.git_status != git_file_status { + entry.git_status = git_file_status; + changes.push(entry.path.clone()); + edits.push(Edit::Insert(entry)); + } + } + + self.snapshot.entries_by_path.edit(edits, &()); + util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp); } } @@ -3031,16 +3042,8 @@ impl BackgroundScanner { ) { use futures::FutureExt as _; - let (root_abs_path, root_inode) = { - let snapshot = &self.state.lock().snapshot; - ( - snapshot.abs_path.clone(), - snapshot.root_entry().map(|e| e.inode), - ) - }; - // Populate ignores above the root. - let ignore_stack; + let root_abs_path = self.state.lock().snapshot.abs_path.clone(); for ancestor in root_abs_path.ancestors().skip(1) { if let Ok(ignore) = build_gitignore(&ancestor.join(&*GITIGNORE), self.fs.as_ref()).await { @@ -3051,31 +3054,24 @@ impl BackgroundScanner { .insert(ancestor.into(), (ignore.into(), false)); } } + + let (scan_job_tx, scan_job_rx) = channel::unbounded(); { let mut state = self.state.lock(); state.snapshot.scan_id += 1; - ignore_stack = state - .snapshot - .ignore_stack_for_abs_path(&root_abs_path, true); - if ignore_stack.is_all() { - if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { + if let Some(mut root_entry) = state.snapshot.root_entry().cloned() { + let ignore_stack = state + .snapshot + .ignore_stack_for_abs_path(&root_abs_path, true); + if ignore_stack.is_all() { root_entry.is_ignored = true; - state.insert_entry(root_entry, self.fs.as_ref()); + state.insert_entry(root_entry.clone(), self.fs.as_ref()); } + state.enqueue_scan_dir(root_abs_path, &root_entry, &scan_job_tx); } }; // Perform an initial scan of the directory. - let (scan_job_tx, scan_job_rx) = channel::unbounded(); - smol::block_on(scan_job_tx.send(ScanJob { - abs_path: root_abs_path, - path: Arc::from(Path::new("")), - ignore_stack, - ancestor_inodes: TreeSet::from_ordered_entries(root_inode), - is_external: false, - scan_queue: scan_job_tx.clone(), - })) - .unwrap(); drop(scan_job_tx); self.scan_dirs(true, scan_job_rx).await; { @@ -3263,20 +3259,7 @@ impl BackgroundScanner { if let Some(entry) = state.snapshot.entry_for_path(ancestor) { if entry.kind == EntryKind::UnloadedDir { let abs_path = root_path.join(ancestor); - let ignore_stack = - state.snapshot.ignore_stack_for_abs_path(&abs_path, true); - let ancestor_inodes = - state.snapshot.ancestor_inodes_for_path(&ancestor); - scan_job_tx - .try_send(ScanJob { - abs_path: abs_path.into(), - path: ancestor.into(), - ignore_stack, - scan_queue: scan_job_tx.clone(), - ancestor_inodes, - is_external: entry.is_external, - }) - .unwrap(); + state.enqueue_scan_dir(abs_path.into(), entry, &scan_job_tx); state.paths_to_scan.insert(path.clone()); break; } @@ -3391,18 +3374,16 @@ impl BackgroundScanner { let mut ignore_stack = job.ignore_stack.clone(); let mut new_ignore = None; - let (root_abs_path, root_char_bag, next_entry_id, repository) = { + let (root_abs_path, root_char_bag, next_entry_id) = { let snapshot = &self.state.lock().snapshot; ( snapshot.abs_path().clone(), snapshot.root_char_bag, self.next_entry_id.clone(), - snapshot - .local_repo_for_path(&job.path) - .map(|(work_dir, repo)| (work_dir, repo.clone())), ) }; + let mut dotgit_path = None; let mut root_canonical_path = None; let mut new_entries: Vec = Vec::new(); let mut new_jobs: Vec> = Vec::new(); @@ -3465,6 +3446,10 @@ impl BackgroundScanner { } } } + // If we find a .git, we'll need to load the repository. + else if child_name == *DOT_GIT { + dotgit_path = Some(child_path.clone()); + } let mut child_entry = Entry::new( child_path.clone(), @@ -3525,22 +3510,17 @@ impl BackgroundScanner { }, ancestor_inodes, scan_queue: job.scan_queue.clone(), + containing_repository: job.containing_repository.clone(), })); } else { new_jobs.push(None); } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - if !child_entry.is_ignored { - if let Some((repo_path, repo)) = &repository { - if let Ok(path) = child_path.strip_prefix(&repo_path.0) { - child_entry.git_status = repo - .repo_ptr - .lock() - .status(&RepoPath(path.into())) - .log_err() - .flatten(); - } + + if let Some((repository_dir, statuses)) = &job.containing_repository { + if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { + child_entry.git_status = statuses.get(&RepoPath(repo_path.into())).copied(); } } } @@ -3549,27 +3529,39 @@ impl BackgroundScanner { } let mut state = self.state.lock(); - let mut new_jobs = new_jobs.into_iter(); + + // Identify any subdirectories that should not be scanned. + let mut job_ix = 0; for entry in &mut new_entries { state.reuse_entry_id(entry); - if entry.is_dir() { - let new_job = new_jobs.next().expect("missing scan job for entry"); if state.should_scan_directory(&entry) { - if let Some(new_job) = new_job { - job.scan_queue - .try_send(new_job) - .expect("channel is unbounded"); - } + job_ix += 1; } else { log::debug!("defer scanning directory {:?}", entry.path); entry.kind = EntryKind::UnloadedDir; + new_jobs.remove(job_ix); } } } - assert!(new_jobs.next().is_none()); - state.populate_dir(&job.path, new_entries, new_ignore, self.fs.as_ref()); + state.populate_dir(&job.path, new_entries, new_ignore); + + let repository = + dotgit_path.and_then(|path| state.build_git_repository(path, self.fs.as_ref())); + + for new_job in new_jobs { + if let Some(mut new_job) = new_job { + if let Some(containing_repository) = &repository { + new_job.containing_repository = Some(containing_repository.clone()); + } + + job.scan_queue + .try_send(new_job) + .expect("channel is unbounded"); + } + } + Ok(()) } @@ -3652,20 +3644,7 @@ impl BackgroundScanner { if let (Some(scan_queue_tx), true) = (&scan_queue_tx, fs_entry.is_dir()) { if state.should_scan_directory(&fs_entry) { - let mut ancestor_inodes = - state.snapshot.ancestor_inodes_for_path(&path); - if !ancestor_inodes.contains(&metadata.inode) { - ancestor_inodes.insert(metadata.inode); - smol::block_on(scan_queue_tx.send(ScanJob { - abs_path, - path: path.clone(), - ignore_stack, - ancestor_inodes, - is_external: fs_entry.is_external, - scan_queue: scan_queue_tx.clone(), - })) - .unwrap(); - } + state.enqueue_scan_dir(abs_path, &fs_entry, scan_queue_tx); } else { fs_entry.kind = EntryKind::UnloadedDir; } @@ -3822,18 +3801,7 @@ impl BackgroundScanner { if was_ignored && !entry.is_ignored && entry.kind.is_unloaded() { let state = self.state.lock(); if state.should_scan_directory(&entry) { - job.scan_queue - .try_send(ScanJob { - abs_path: abs_path.clone(), - path: entry.path.clone(), - ignore_stack: child_ignore_stack.clone(), - scan_queue: job.scan_queue.clone(), - ancestor_inodes: state - .snapshot - .ancestor_inodes_for_path(&entry.path), - is_external: false, - }) - .unwrap(); + state.enqueue_scan_dir(abs_path.clone(), &entry, &job.scan_queue); } } @@ -4022,6 +3990,7 @@ struct ScanJob { scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, + containing_repository: Option<(RepositoryWorkDirectory, TreeMap)>, } struct UpdateIgnoreStatusJob { diff --git a/script/zed-with-local-servers b/script/zed-with-local-servers index ed07862d30..f1de38adcf 100755 --- a/script/zed-with-local-servers +++ b/script/zed-with-local-servers @@ -1 +1,3 @@ +#!/bin/bash + ZED_ADMIN_API_TOKEN=secret ZED_SERVER_URL=http://localhost:3000 cargo run $@ From c538504b9c8805331f8cf03f3b4ba3d589da7f8f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 22 Jul 2023 00:17:02 +0300 Subject: [PATCH 092/124] Do not scroll when selecting all --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 87ba250a88..f1cf5a942b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5298,7 +5298,7 @@ impl Editor { pub fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext) { let end = self.buffer.read(cx).read(cx).len(); - self.change_selections(Some(Autoscroll::fit()), cx, |s| { + self.change_selections(None, cx, |s| { s.select_ranges(vec![0..end]); }); } From 458916409c5455a0b6df6494aa235e1d03e15bde Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 20 Jul 2023 12:32:27 -0600 Subject: [PATCH 093/124] Add a mode indicator for vim This is the second most common remaining complaint (after :w not working). Fixes: zed-industries/community#409 --- Cargo.lock | 1 + crates/theme/src/theme.rs | 1 + crates/vim/Cargo.toml | 2 + crates/vim/src/mode_indicator.rs | 68 +++++++++++++++++++++++++++++ crates/vim/src/vim.rs | 2 + crates/zed/src/zed.rs | 2 + styles/src/style_tree/status_bar.ts | 1 + 7 files changed, 77 insertions(+) create mode 100644 crates/vim/src/mode_indicator.rs diff --git a/Cargo.lock b/Cargo.lock index f0c8917aa2..e805a87230 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8523,6 +8523,7 @@ dependencies = [ "serde_derive", "serde_json", "settings", + "theme", "tokio", "util", "workspace", diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 81ae7a65ca..de0701a343 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -402,6 +402,7 @@ pub struct StatusBar { pub height: f32, pub item_spacing: f32, pub cursor_position: TextStyle, + pub vim_mode: TextStyle, pub active_language: Interactive, pub auto_update_progress_message: TextStyle, pub auto_update_done_message: TextStyle, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 47a85f4ed3..3a5974d6c9 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -32,6 +32,7 @@ language = { path = "../language" } search = { path = "../search" } settings = { path = "../settings" } workspace = { path = "../workspace" } +theme = { path = "../theme" } [dev-dependencies] indoc.workspace = true @@ -44,3 +45,4 @@ project = { path = "../project", features = ["test-support"] } util = { path = "../util", features = ["test-support"] } settings = { path = "../settings" } workspace = { path = "../workspace", features = ["test-support"] } +theme = { path = "../theme", features = ["test-support"] } diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs new file mode 100644 index 0000000000..ef8d159018 --- /dev/null +++ b/crates/vim/src/mode_indicator.rs @@ -0,0 +1,68 @@ +use gpui::{ + elements::{Empty, Label}, + AnyElement, Element, Entity, View, ViewContext, +}; +use workspace::{item::ItemHandle, StatusItemView}; + +use crate::{state::Mode, Vim}; + +pub struct ModeIndicator { + mode: Option, +} + +impl ModeIndicator { + pub fn new(cx: &mut ViewContext) -> Self { + cx.observe_global::(|this, cx| { + let vim = Vim::read(cx); + if vim.enabled { + this.set_mode(Some(Vim::read(cx).state.mode), cx) + } else { + this.set_mode(None, cx) + } + }) + .detach(); + Self { mode: None } + } + + pub fn set_mode(&mut self, mode: Option, cx: &mut ViewContext) { + if mode != self.mode { + self.mode = mode; + cx.notify(); + } + } +} + +impl Entity for ModeIndicator { + type Event = (); +} + +impl View for ModeIndicator { + fn ui_name() -> &'static str { + "ModeIndicator" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(mode) = self.mode { + let theme = &theme::current(cx).workspace.status_bar; + let text = match mode { + Mode::Normal => "", + Mode::Insert => "--- INSERT ---", + Mode::Visual { line: false } => "--- VISUAL ---", + Mode::Visual { line: true } => "--- VISUAL LINE ---", + }; + Label::new(text, theme.vim_mode.clone()).into_any() + } else { + Empty::new().into_any() + } + } +} + +impl StatusItemView for ModeIndicator { + fn set_active_pane_item( + &mut self, + _active_pane_item: Option<&dyn ItemHandle>, + _cx: &mut ViewContext, + ) { + // nothing to do. + } +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 69b94428dd..8ab110ba1e 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -3,6 +3,7 @@ mod test; mod editor_events; mod insert; +mod mode_indicator; mod motion; mod normal; mod object; @@ -18,6 +19,7 @@ use gpui::{ ViewHandle, WeakViewHandle, WindowContext, }; use language::CursorShape; +pub use mode_indicator::ModeIndicator; use motion::Motion; use normal::normal_replace; use serde::Deserialize; diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6bbba0bd02..645371d419 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -312,8 +312,10 @@ pub fn initialize_workspace( feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) }); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + let vim_mode = cx.add_view(|cx| vim::ModeIndicator::new(cx)); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); + status_bar.add_left_item(vim_mode, cx); status_bar.add_left_item(activity_indicator, cx); status_bar.add_right_item(feedback_button, cx); status_bar.add_right_item(copilot, cx); diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 9aeea866f3..b4273cbf99 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -27,6 +27,7 @@ export default function status_bar(): any { }, border: border(layer, { top: true, overlay: true }), cursor_position: text(layer, "sans", "variant"), + vim_mode: text(layer, "sans", "variant"), active_language: interactive({ base: { padding: { left: 6, right: 6 }, From d14a484a20d731511db502a7eacad4dcfbab9383 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 21 Jul 2023 13:19:26 -0600 Subject: [PATCH 094/124] Add support for adding/removing status items --- crates/workspace/src/status_bar.rs | 55 ++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 6fc1467566..6fd3bd5310 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,4 +1,4 @@ -use std::ops::Range; +use std::{any::TypeId, ops::Range}; use crate::{ItemHandle, Pane}; use gpui::{ @@ -27,6 +27,7 @@ trait StatusItemViewHandle { active_pane_item: Option<&dyn ItemHandle>, cx: &mut WindowContext, ); + fn ui_name(&self) -> &'static str; } pub struct StatusBar { @@ -57,7 +58,6 @@ impl View for StatusBar { .with_margin_right(theme.item_spacing) })) .into_any(), - right: Flex::row() .with_children(self.right_items.iter().rev().map(|i| { ChildView::new(i.as_any(), cx) @@ -96,6 +96,53 @@ impl StatusBar { cx.notify(); } + pub fn position_of_item(&mut self) -> Option + where + T: StatusItemView, + { + self.position_of_named_item(T::ui_name()) + } + + pub fn position_of_named_item(&mut self, name: &str) -> Option { + for (index, item) in self.left_items.iter().enumerate() { + if item.as_ref().ui_name() == name { + return Some(index); + } + } + for (index, item) in self.right_items.iter().enumerate() { + if item.as_ref().ui_name() == name { + return Some(index + self.left_items.len()); + } + } + return None; + } + + pub fn insert_item_after( + &mut self, + position: usize, + item: ViewHandle, + cx: &mut ViewContext, + ) where + T: 'static + StatusItemView, + { + if position < self.left_items.len() { + self.left_items.insert(position, Box::new(item)) + } else { + self.right_items + .insert(position - self.left_items.len(), Box::new(item)) + } + cx.notify() + } + + pub fn remove_item_at(&mut self, position: usize, cx: &mut ViewContext) { + if position < self.left_items.len() { + self.left_items.remove(position); + } else { + self.right_items.remove(position - self.left_items.len()); + } + cx.notify(); + } + pub fn add_right_item(&mut self, item: ViewHandle, cx: &mut ViewContext) where T: 'static + StatusItemView, @@ -133,6 +180,10 @@ impl StatusItemViewHandle for ViewHandle { this.set_active_pane_item(active_pane_item, cx) }); } + + fn ui_name(&self) -> &'static str { + T::ui_name() + } } impl From<&dyn StatusItemViewHandle> for AnyViewHandle { From dcaf8a9af895abd8af08fc420d1312136ec14745 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 22 Jul 2023 01:34:25 +0300 Subject: [PATCH 095/124] Open paths starting with ~ from terminal click --- crates/terminal/src/terminal.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index f81af1319e..2785c1a871 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -76,7 +76,7 @@ lazy_static! { // Regex Copied from alacritty's ui_config.rs static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-]+").unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-~]+").unwrap(); } ///Upward flowing events, for changing the title and such diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index cdb1d40efc..bee1107d6d 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -425,6 +425,16 @@ fn possible_open_targets( let maybe_path = path_like.path_like; let potential_abs_paths = if maybe_path.is_absolute() { vec![maybe_path] + } else if maybe_path.starts_with("~") { + if let Some(abs_path) = maybe_path + .strip_prefix("~") + .ok() + .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path))) + { + vec![abs_path] + } else { + Vec::new() + } } else if let Some(workspace) = workspace.upgrade(cx) { workspace.update(cx, |workspace, cx| { workspace From 05b161118c753e72fa61710b85c91f07f8e3c626 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:05:42 -0700 Subject: [PATCH 096/124] Don't call git status when ignored files change --- crates/fs/src/repository.rs | 1 + crates/project/src/worktree.rs | 20 ++++++++++++-------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 611427c0a8..0a43c7ee26 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -109,6 +109,7 @@ impl GitRepository for LibGitRepository { } } } + fn branches(&self) -> Result> { let local_branches = self.branches(Some(BranchType::Local))?; let valid_branches = local_branches diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2ce1693459..880313b6b1 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2162,10 +2162,12 @@ impl BackgroundScannerState { let path = entry.path.clone(); let ignore_stack = self.snapshot.ignore_stack_for_abs_path(&abs_path, true); let mut ancestor_inodes = self.snapshot.ancestor_inodes_for_path(&path); - let containing_repository = self - .snapshot - .local_repo_for_path(&path) - .map(|(path, repo)| (path, repo.repo_ptr.lock().statuses())); + let mut containing_repository = None; + if !ignore_stack.is_all() { + if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { + containing_repository = Some((workdir_path, repo.repo_ptr.lock().statuses())); + } + } if !ancestor_inodes.contains(&entry.inode) { ancestor_inodes.insert(entry.inode); scan_job_tx @@ -3517,10 +3519,12 @@ impl BackgroundScanner { } } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); - - if let Some((repository_dir, statuses)) = &job.containing_repository { - if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { - child_entry.git_status = statuses.get(&RepoPath(repo_path.into())).copied(); + if !child_entry.is_ignored { + if let Some((repository_dir, statuses)) = &job.containing_repository { + if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { + let repo_path = RepoPath(repo_path.into()); + child_entry.git_status = statuses.get(&repo_path).copied(); + } } } } From ff0864026ed9c981ed15252da4d4541e2190bfd9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:08:31 -0700 Subject: [PATCH 097/124] Only fetch statuses for changed paths --- crates/fs/src/repository.rs | 14 +++++++++----- crates/project/src/worktree.rs | 9 ++++++--- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 0a43c7ee26..851d495b01 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -27,7 +27,7 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; - fn statuses(&self) -> TreeMap; + fn statuses(&self, path_prefix: &Path) -> TreeMap; fn status(&self, path: &RepoPath) -> Result>; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -78,9 +78,11 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self) -> TreeMap { + fn statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); - if let Some(statuses) = self.statuses(None).log_err() { + let mut options = git2::StatusOptions::new(); + options.pathspec(path_prefix); + if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() .filter(|status| !status.status().contains(git2::Status::IGNORED)) @@ -201,11 +203,13 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self) -> TreeMap { + fn statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let state = self.state.lock(); for (repo_path, status) in state.worktree_statuses.iter() { - map.insert(repo_path.to_owned(), status.to_owned()); + if repo_path.0.starts_with(path_prefix) { + map.insert(repo_path.to_owned(), status.to_owned()); + } } map } diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 880313b6b1..85cf032464 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2165,7 +2165,10 @@ impl BackgroundScannerState { let mut containing_repository = None; if !ignore_stack.is_all() { if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { - containing_repository = Some((workdir_path, repo.repo_ptr.lock().statuses())); + if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { + containing_repository = + Some((workdir_path, repo.repo_ptr.lock().statuses(repo_path))); + } } } if !ancestor_inodes.contains(&entry.inode) { @@ -2357,7 +2360,7 @@ impl BackgroundScannerState { .repository_entries .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - let statuses = repository.statuses(); + let statuses = repository.statuses(Path::new("")); self.update_git_statuses(&work_dir, &statuses); } } @@ -2415,7 +2418,7 @@ impl BackgroundScannerState { }, ); - let statuses = repo_lock.statuses(); + let statuses = repo_lock.statuses(Path::new("")); self.update_git_statuses(&work_directory, &statuses); drop(repo_lock); From 51d311affd108118f03aef42f2ad4937bfb334fb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 21 Jul 2023 17:51:00 -0700 Subject: [PATCH 098/124] Compute unstaged git status separately, to take advantage of our cached file mtimes --- crates/fs/src/repository.rs | 34 ++++++++++++-- crates/project/src/worktree.rs | 83 +++++++++++++++++++++++++--------- 2 files changed, 92 insertions(+), 25 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 851d495b01..54f80c26a2 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -10,6 +10,7 @@ use std::{ os::unix::prelude::OsStrExt, path::{Component, Path, PathBuf}, sync::Arc, + time::SystemTime, }; use sum_tree::{MapSeekTarget, TreeMap}; use util::ResultExt; @@ -27,7 +28,8 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; - fn statuses(&self, path_prefix: &Path) -> TreeMap; + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap; + fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option; fn status(&self, path: &RepoPath) -> Result>; fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; @@ -78,10 +80,11 @@ impl GitRepository for LibGitRepository { Some(branch.to_string()) } - fn statuses(&self, path_prefix: &Path) -> TreeMap { + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); + options.disable_pathspec_match(true); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() @@ -98,6 +101,27 @@ impl GitRepository for LibGitRepository { map } + fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option { + let index = self.index().log_err()?; + if let Some(entry) = index.get_path(&path, 0) { + let mtime = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err()?; + if entry.mtime.seconds() == mtime.as_secs() as i32 + && entry.mtime.nanoseconds() == mtime.subsec_nanos() + { + None + } else { + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status + } + } else { + Some(GitFileStatus::Added) + } + } + fn status(&self, path: &RepoPath) -> Result> { let status = self.status_file(path); match status { @@ -203,7 +227,7 @@ impl GitRepository for FakeGitRepository { state.branch_name.clone() } - fn statuses(&self, path_prefix: &Path) -> TreeMap { + fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); let state = self.state.lock(); for (repo_path, status) in state.worktree_statuses.iter() { @@ -214,6 +238,10 @@ impl GitRepository for FakeGitRepository { map } + fn unstaged_status(&self, _path: &RepoPath, _mtime: SystemTime) -> Option { + None + } + fn status(&self, path: &RepoPath) -> Result> { let state = self.state.lock(); Ok(state.worktree_statuses.get(path).cloned()) diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 85cf032464..c4b6ed6ca0 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2166,8 +2166,11 @@ impl BackgroundScannerState { if !ignore_stack.is_all() { if let Some((workdir_path, repo)) = self.snapshot.local_repo_for_path(&path) { if let Ok(repo_path) = path.strip_prefix(&workdir_path.0) { - containing_repository = - Some((workdir_path, repo.repo_ptr.lock().statuses(repo_path))); + containing_repository = Some(( + workdir_path, + repo.repo_ptr.clone(), + repo.repo_ptr.lock().staged_statuses(repo_path), + )); } } } @@ -2360,8 +2363,7 @@ impl BackgroundScannerState { .repository_entries .update(&work_dir, |entry| entry.branch = branch.map(Into::into)); - let statuses = repository.statuses(Path::new("")); - self.update_git_statuses(&work_dir, &statuses); + self.update_git_statuses(&work_dir, &*repository); } } } @@ -2386,7 +2388,11 @@ impl BackgroundScannerState { &mut self, dot_git_path: Arc, fs: &dyn Fs, - ) -> Option<(RepositoryWorkDirectory, TreeMap)> { + ) -> Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )> { log::info!("build git repository {:?}", dot_git_path); let work_dir_path: Arc = dot_git_path.parent().unwrap().into(); @@ -2418,27 +2424,28 @@ impl BackgroundScannerState { }, ); - let statuses = repo_lock.statuses(Path::new("")); - self.update_git_statuses(&work_directory, &statuses); + let staged_statuses = self.update_git_statuses(&work_directory, &*repo_lock); drop(repo_lock); self.snapshot.git_repositories.insert( work_dir_id, LocalRepositoryEntry { git_dir_scan_id: 0, - repo_ptr: repository, + repo_ptr: repository.clone(), git_dir_path: dot_git_path.clone(), }, ); - Some((work_directory, statuses)) + Some((work_directory, repository, staged_statuses)) } fn update_git_statuses( &mut self, work_directory: &RepositoryWorkDirectory, - statuses: &TreeMap, - ) { + repo: &dyn GitRepository, + ) -> TreeMap { + let staged_statuses = repo.staged_statuses(Path::new("")); + let mut changes = vec![]; let mut edits = vec![]; @@ -2451,7 +2458,10 @@ impl BackgroundScannerState { continue; }; let repo_path = RepoPath(repo_path.to_path_buf()); - let git_file_status = statuses.get(&repo_path).copied(); + let git_file_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repo.unstaged_status(&repo_path, entry.mtime), + ); if entry.git_status != git_file_status { entry.git_status = git_file_status; changes.push(entry.path.clone()); @@ -2461,6 +2471,7 @@ impl BackgroundScannerState { self.snapshot.entries_by_path.edit(edits, &()); util::extend_sorted(&mut self.changed_paths, changes, usize::MAX, Ord::cmp); + staged_statuses } } @@ -3523,10 +3534,17 @@ impl BackgroundScanner { } else { child_entry.is_ignored = ignore_stack.is_abs_path_ignored(&child_abs_path, false); if !child_entry.is_ignored { - if let Some((repository_dir, statuses)) = &job.containing_repository { + if let Some((repository_dir, repository, staged_statuses)) = + &job.containing_repository + { if let Ok(repo_path) = child_entry.path.strip_prefix(&repository_dir.0) { let repo_path = RepoPath(repo_path.into()); - child_entry.git_status = statuses.get(&repo_path).copied(); + child_entry.git_status = combine_git_statuses( + staged_statuses.get(&repo_path).copied(), + repository + .lock() + .unstaged_status(&repo_path, child_entry.mtime), + ); } } } @@ -3637,13 +3655,11 @@ impl BackgroundScanner { if let Some((work_dir, repo)) = state.snapshot.local_repo_for_path(&path) { - if let Ok(path) = path.strip_prefix(work_dir.0) { - fs_entry.git_status = repo - .repo_ptr - .lock() - .status(&RepoPath(path.into())) - .log_err() - .flatten() + if let Ok(repo_path) = path.strip_prefix(work_dir.0) { + let repo_path = RepoPath(repo_path.into()); + let repo = repo.repo_ptr.lock(); + fs_entry.git_status = + repo.status(&repo_path).log_err().flatten(); } } } @@ -3997,7 +4013,11 @@ struct ScanJob { scan_queue: Sender, ancestor_inodes: TreeSet, is_external: bool, - containing_repository: Option<(RepositoryWorkDirectory, TreeMap)>, + containing_repository: Option<( + RepositoryWorkDirectory, + Arc>, + TreeMap, + )>, } struct UpdateIgnoreStatusJob { @@ -4324,3 +4344,22 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { } } } + +fn combine_git_statuses( + staged: Option, + unstaged: Option, +) -> Option { + if let Some(staged) = staged { + if let Some(unstaged) = unstaged { + if unstaged != staged { + Some(GitFileStatus::Modified) + } else { + Some(staged) + } + } else { + Some(staged) + } + } else { + unstaged + } +} From 6c09782aa2b132216a55d8028721eebe17786151 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 11:53:26 -0700 Subject: [PATCH 099/124] Optimize full file status via passing in known file mtime --- crates/fs/src/repository.rs | 65 +++++++++++++++++++++++++--------- crates/project/src/worktree.rs | 3 +- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 54f80c26a2..e6a41839b6 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -1,6 +1,6 @@ use anyhow::Result; use collections::HashMap; -use git2::{BranchType, ErrorCode}; +use git2::{BranchType, StatusShow}; use parking_lot::Mutex; use rpc::proto; use serde_derive::{Deserialize, Serialize}; @@ -28,9 +28,25 @@ pub trait GitRepository: Send { fn reload_index(&self); fn load_index_text(&self, relative_file_path: &Path) -> Option; fn branch_name(&self) -> Option; + + /// Get the statuses of all of the files in the index that start with the given + /// path and have changes with resepect to the HEAD commit. This is fast because + /// the index stores hashes of trees, so that unchanged directories can be skipped. fn staged_statuses(&self, path_prefix: &Path) -> TreeMap; + + /// Get the status of a given file in the working directory with respect to + /// the index. In the common case, when there are no changes, this only requires + /// an index lookup. The index stores the mtime of each file when it was added, + /// so there's no work to do if the mtime matches. fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option; - fn status(&self, path: &RepoPath) -> Result>; + + /// Get the status of a given file in the working directory with respect to + /// the HEAD commit. In the common case, when there are no changes, this only + /// requires an index lookup and blob comparison between the index and the HEAD + /// commit. The index stores the mtime of each file when it was added, so there's + /// no need to consider the working directory file if the mtime matches. + fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option; + fn branches(&self) -> Result>; fn change_branch(&self, _: &str) -> Result<()>; fn create_branch(&self, _: &str) -> Result<()>; @@ -42,7 +58,6 @@ impl std::fmt::Debug for dyn GitRepository { } } -#[async_trait::async_trait] impl GitRepository for LibGitRepository { fn reload_index(&self) { if let Ok(mut index) = self.index() { @@ -122,18 +137,21 @@ impl GitRepository for LibGitRepository { } } - fn status(&self, path: &RepoPath) -> Result> { - let status = self.status_file(path); - match status { - Ok(status) => Ok(read_status(status)), - Err(e) => { - if e.code() == ErrorCode::NotFound { - Ok(None) - } else { - Err(e.into()) - } - } + fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option { + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + + // If the file has not changed since it was added to the index, then + // there's no need to examine the working directory file: just compare + // the blob in the index to the one in the HEAD commit. + if matches_index(self, path, mtime) { + options.show(StatusShow::Index); } + + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status } fn branches(&self) -> Result> { @@ -178,6 +196,21 @@ impl GitRepository for LibGitRepository { } } +fn matches_index(repo: &LibGitRepository, path: &RepoPath, mtime: SystemTime) -> bool { + if let Some(index) = repo.index().log_err() { + if let Some(entry) = index.get_path(&path, 0) { + if let Some(mtime) = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err() { + if entry.mtime.seconds() == mtime.as_secs() as i32 + && entry.mtime.nanoseconds() == mtime.subsec_nanos() + { + return true; + } + } + } + } + false +} + fn read_status(status: git2::Status) -> Option { if status.contains(git2::Status::CONFLICTED) { Some(GitFileStatus::Conflict) @@ -242,9 +275,9 @@ impl GitRepository for FakeGitRepository { None } - fn status(&self, path: &RepoPath) -> Result> { + fn status(&self, path: &RepoPath, _mtime: SystemTime) -> Option { let state = self.state.lock(); - Ok(state.worktree_statuses.get(path).cloned()) + state.worktree_statuses.get(path).cloned() } fn branches(&self) -> Result> { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c4b6ed6ca0..b0795818b8 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -3658,8 +3658,7 @@ impl BackgroundScanner { if let Ok(repo_path) = path.strip_prefix(work_dir.0) { let repo_path = RepoPath(repo_path.into()); let repo = repo.repo_ptr.lock(); - fs_entry.git_status = - repo.status(&repo_path).log_err().flatten(); + fs_entry.git_status = repo.status(&repo_path, fs_entry.mtime); } } } From f05095a6dd64d16787bf4152f02436e82577d8f9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 22 Jul 2023 02:24:46 +0300 Subject: [PATCH 100/124] Focus project panel on directory select --- crates/project/src/project.rs | 1 + crates/project_panel/src/project_panel.rs | 9 +- crates/terminal/src/terminal.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 71 +++++++++------ crates/workspace/src/workspace.rs | 102 ++++++++++++++-------- crates/zed/src/zed.rs | 92 ++++++++++++------- 6 files changed, 184 insertions(+), 95 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b3255df812..6b905a1faa 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -259,6 +259,7 @@ pub enum Event { LanguageServerLog(LanguageServerId, String), Notification(String), ActiveEntryChanged(Option), + ActivateProjectPanel, WorktreeAdded, WorktreeRemoved(WorktreeId), WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 87b0d21a9f..2a6da7db28 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -174,6 +174,7 @@ pub enum Event { NewSearchInDirectory { dir_entry: Entry, }, + ActivatePanel, } #[derive(Serialize, Deserialize)] @@ -200,6 +201,9 @@ impl ProjectPanel { cx.notify(); } } + project::Event::ActivateProjectPanel => { + cx.emit(Event::ActivatePanel); + } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); this.update_visible_entries(None, cx); @@ -1014,7 +1018,10 @@ impl ProjectPanel { None } - fn selected_entry<'a>(&self, cx: &'a AppContext) -> Option<(&'a Worktree, &'a project::Entry)> { + pub fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a project::Entry)> { let (worktree, entry) = self.selected_entry_handle(cx)?; Some((worktree.read(cx), entry)) } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 2785c1a871..e3109102d1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -73,7 +73,9 @@ const DEBUG_CELL_WIDTH: f32 = 5.; const DEBUG_LINE_HEIGHT: f32 = 5.; lazy_static! { - // Regex Copied from alacritty's ui_config.rs + // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly: + // * avoid Rust-specific escaping. + // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings. static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); static ref WORD_REGEX: RegexSearch = RegexSearch::new("[\\w.:/@-~]+").unwrap(); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index bee1107d6d..e108a05ccc 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -187,37 +187,56 @@ impl TerminalView { } let potential_abs_paths = possible_open_targets(&workspace, maybe_path, cx); if let Some(path) = potential_abs_paths.into_iter().next() { - let visible = path.path_like.is_dir(); + let is_dir = path.path_like.is_dir(); let task_workspace = workspace.clone(); cx.spawn(|_, mut cx| async move { - let opened_item = task_workspace + let opened_items = task_workspace .update(&mut cx, |workspace, cx| { - workspace.open_abs_path(path.path_like, visible, cx) + workspace.open_paths(vec![path.path_like], is_dir, cx) }) .context("workspace update")? - .await - .context("workspace update")?; - if let Some(row) = path.row { - let col = path.column.unwrap_or(0); - if let Some(active_editor) = opened_item.downcast::() { - active_editor - .downgrade() - .update(&mut cx, |editor, cx| { - let snapshot = editor.snapshot(cx).display_snapshot; - let point = snapshot.buffer_snapshot.clip_point( - language::Point::new( - row.saturating_sub(1), - col.saturating_sub(1), - ), - Bias::Left, - ); - editor.change_selections( - Some(Autoscroll::center()), - cx, - |s| s.select_ranges([point..point]), - ); - }) - .log_err(); + .await; + anyhow::ensure!( + opened_items.len() == 1, + "For a single path open, expected single opened item" + ); + let opened_item = opened_items + .into_iter() + .next() + .unwrap() + .transpose() + .context("path open")?; + if is_dir { + task_workspace.update(&mut cx, |workspace, cx| { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActivateProjectPanel); + }) + })?; + } else { + if let Some(row) = path.row { + let col = path.column.unwrap_or(0); + if let Some(active_editor) = + opened_item.and_then(|item| item.downcast::()) + { + active_editor + .downgrade() + .update(&mut cx, |editor, cx| { + let snapshot = editor.snapshot(cx).display_snapshot; + let point = snapshot.buffer_snapshot.clip_point( + language::Point::new( + row.saturating_sub(1), + col.saturating_sub(1), + ), + Bias::Left, + ); + editor.change_selections( + Some(Autoscroll::center()), + cx, + |s| s.select_ranges([point..point]), + ); + }) + .log_err(); + } } } anyhow::Ok(()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5c1a75e97a..1e9e431f9d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -898,6 +898,18 @@ impl Workspace { pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) where T::Event: std::fmt::Debug, + { + self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) + } + + pub fn add_panel_with_extra_event_handler( + &mut self, + panel: ViewHandle, + cx: &mut ViewContext, + handler: F, + ) where + T::Event: std::fmt::Debug, + F: Fn(&mut Self, &ViewHandle, &T::Event, &mut ViewContext) + 'static, { let dock = match panel.position(cx) { DockPosition::Left => &self.left_dock, @@ -965,6 +977,8 @@ impl Workspace { } this.update_active_view_for_followers(cx); cx.notify(); + } else { + handler(this, &panel, event, cx) } } })); @@ -1417,45 +1431,65 @@ impl Workspace { // Sort the paths to ensure we add worktrees for parents before their children. abs_paths.sort_unstable(); cx.spawn(|this, mut cx| async move { - let mut project_paths = Vec::new(); - for path in &abs_paths { - if let Some(project_path) = this + let mut tasks = Vec::with_capacity(abs_paths.len()); + for abs_path in &abs_paths { + let project_path = match this .update(&mut cx, |this, cx| { - Workspace::project_path_for_path(this.project.clone(), path, visible, cx) + Workspace::project_path_for_path( + this.project.clone(), + abs_path, + visible, + cx, + ) }) .log_err() { - project_paths.push(project_path.await.log_err()); - } else { - project_paths.push(None); - } - } + Some(project_path) => project_path.await.log_err(), + None => None, + }; - let tasks = abs_paths - .iter() - .cloned() - .zip(project_paths.into_iter()) - .map(|(abs_path, project_path)| { - let this = this.clone(); - cx.spawn(|mut cx| { - let fs = fs.clone(); - async move { - let (_worktree, project_path) = project_path?; - if fs.is_file(&abs_path).await { - Some( - this.update(&mut cx, |this, cx| { - this.open_path(project_path, None, true, cx) + let this = this.clone(); + let task = cx.spawn(|mut cx| { + let fs = fs.clone(); + let abs_path = abs_path.clone(); + async move { + let (worktree, project_path) = project_path?; + if fs.is_file(&abs_path).await { + Some( + this.update(&mut cx, |this, cx| { + this.open_path(project_path, None, true, cx) + }) + .log_err()? + .await, + ) + } else { + this.update(&mut cx, |workspace, cx| { + let worktree = worktree.read(cx); + let worktree_abs_path = worktree.abs_path(); + let entry_id = if abs_path == worktree_abs_path.as_ref() { + worktree.root_entry() + } else { + abs_path + .strip_prefix(worktree_abs_path.as_ref()) + .ok() + .and_then(|relative_path| { + worktree.entry_for_path(relative_path) + }) + } + .map(|entry| entry.id); + if let Some(entry_id) = entry_id { + workspace.project().update(cx, |_, cx| { + cx.emit(project::Event::ActiveEntryChanged(Some(entry_id))); }) - .log_err()? - .await, - ) - } else { - None - } + } + }) + .log_err()?; + None } - }) - }) - .collect::>(); + } + }); + tasks.push(task); + } futures::future::join_all(tasks).await }) @@ -3009,10 +3043,6 @@ impl Workspace { self.database_id } - pub fn push_subscription(&mut self, subscription: Subscription) { - self.subscriptions.push(subscription) - } - fn location(&self, cx: &AppContext) -> Option { let project = self.project().read(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8a2691da15..db7c57a89c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -339,29 +339,21 @@ pub fn initialize_workspace( let (project_panel, terminal_panel, assistant_panel) = futures::try_join!(project_panel, terminal_panel, assistant_panel)?; - cx.update(|cx| { - if let Some(workspace) = workspace_handle.upgrade(cx) { - cx.update_window(project_panel.window_id(), |cx| { - workspace.update(cx, |workspace, cx| { - let project_panel_subscription = - cx.subscribe(&project_panel, move |workspace, _, event, cx| { - if let project_panel::Event::NewSearchInDirectory { dir_entry } = - event - { - search::ProjectSearchView::new_search_in_directory( - workspace, dir_entry, cx, - ) - } - }); - workspace.push_subscription(project_panel_subscription); - }); - }); - } - }); - workspace_handle.update(&mut cx, |workspace, cx| { let project_panel_position = project_panel.position(cx); - workspace.add_panel(project_panel, cx); + workspace.add_panel_with_extra_event_handler( + project_panel, + cx, + |workspace, _, event, cx| match event { + project_panel::Event::NewSearchInDirectory { dir_entry } => { + search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) + } + project_panel::Event::ActivatePanel => { + workspace.focus_panel::(cx); + } + _ => {} + }, + ); workspace.add_panel(terminal_panel, cx); workspace.add_panel(assistant_panel, cx); @@ -1106,8 +1098,46 @@ mod tests { ) .await; - let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) + .await + .unwrap(); + assert_eq!(cx.window_ids().len(), 1); + let workspace = cx + .read_window(cx.window_ids()[0], |cx| cx.root_view().clone()) + .unwrap() + .downcast::() + .unwrap(); + + #[track_caller] + fn assert_project_panel_selection( + workspace: &Workspace, + expected_worktree_path: &Path, + expected_entry_path: &Path, + cx: &AppContext, + ) { + let project_panel = [ + workspace.left_dock().read(cx).panel::(), + workspace.right_dock().read(cx).panel::(), + workspace.bottom_dock().read(cx).panel::(), + ] + .into_iter() + .find_map(std::convert::identity) + .expect("found no project panels") + .read(cx); + let (selected_worktree, selected_entry) = project_panel + .selected_entry(cx) + .expect("project panel should have a selected entry"); + assert_eq!( + selected_worktree.abs_path().as_ref(), + expected_worktree_path, + "Unexpected project panel selected worktree path" + ); + assert_eq!( + selected_entry.path.as_ref(), + expected_entry_path, + "Unexpected project panel selected entry path" + ); + } // Open a file within an existing worktree. workspace @@ -1116,9 +1146,10 @@ mod tests { }) .await; cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir1"), Path::new("a.txt"), cx); assert_eq!( workspace - .read(cx) .active_pane() .read(cx) .active_item() @@ -1139,8 +1170,9 @@ mod tests { }) .await; cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir2/b.txt"), Path::new(""), cx); let worktree_roots = workspace - .read(cx) .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); @@ -1153,7 +1185,6 @@ mod tests { ); assert_eq!( workspace - .read(cx) .active_pane() .read(cx) .active_item() @@ -1174,8 +1205,9 @@ mod tests { }) .await; cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/dir3"), Path::new("c.txt"), cx); let worktree_roots = workspace - .read(cx) .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); @@ -1188,7 +1220,6 @@ mod tests { ); assert_eq!( workspace - .read(cx) .active_pane() .read(cx) .active_item() @@ -1209,8 +1240,9 @@ mod tests { }) .await; cx.read(|cx| { + let workspace = workspace.read(cx); + assert_project_panel_selection(workspace, Path::new("/d.txt"), Path::new(""), cx); let worktree_roots = workspace - .read(cx) .worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); @@ -1223,7 +1255,6 @@ mod tests { ); let visible_worktree_roots = workspace - .read(cx) .visible_worktrees(cx) .map(|w| w.read(cx).as_local().unwrap().abs_path().as_ref()) .collect::>(); @@ -1237,7 +1268,6 @@ mod tests { assert_eq!( workspace - .read(cx) .active_pane() .read(cx) .active_item() From b338ffe8d818411d0c4a824d7e3f5f92711794da Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 17:47:36 -0700 Subject: [PATCH 101/124] Rely on git status for any paths not matching the git index --- crates/fs/src/repository.rs | 38 ++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index e6a41839b6..47e1bc1aab 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -97,9 +97,11 @@ impl GitRepository for LibGitRepository { fn staged_statuses(&self, path_prefix: &Path) -> TreeMap { let mut map = TreeMap::default(); + let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); options.disable_pathspec_match(true); + if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { for status in statuses .iter() @@ -117,30 +119,32 @@ impl GitRepository for LibGitRepository { } fn unstaged_status(&self, path: &RepoPath, mtime: SystemTime) -> Option { - let index = self.index().log_err()?; - if let Some(entry) = index.get_path(&path, 0) { - let mtime = mtime.duration_since(SystemTime::UNIX_EPOCH).log_err()?; - if entry.mtime.seconds() == mtime.as_secs() as i32 - && entry.mtime.nanoseconds() == mtime.subsec_nanos() - { - None - } else { - let mut options = git2::StatusOptions::new(); - options.pathspec(&path.0); - options.disable_pathspec_match(true); - let statuses = self.statuses(Some(&mut options)).log_err()?; - let status = statuses.get(0).and_then(|s| read_status(s.status())); - status - } - } else { - Some(GitFileStatus::Added) + // If the file has not changed since it was added to the index, then + // there can't be any changes. + if matches_index(self, path, mtime) { + return None; } + + let mut options = git2::StatusOptions::new(); + options.pathspec(&path.0); + options.disable_pathspec_match(true); + options.include_untracked(true); + options.recurse_untracked_dirs(true); + options.include_unmodified(true); + options.show(StatusShow::Workdir); + + let statuses = self.statuses(Some(&mut options)).log_err()?; + let status = statuses.get(0).and_then(|s| read_status(s.status())); + status } fn status(&self, path: &RepoPath, mtime: SystemTime) -> Option { let mut options = git2::StatusOptions::new(); options.pathspec(&path.0); options.disable_pathspec_match(true); + options.include_untracked(true); + options.recurse_untracked_dirs(true); + options.include_unmodified(true); // If the file has not changed since it was added to the index, then // there's no need to examine the working directory file: just compare From a3a9d024ba3323ae4b67e2dea4ecfb88ce6a489b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sat, 22 Jul 2023 17:53:58 -0700 Subject: [PATCH 102/124] Fix filtering of staged statuses --- crates/fs/src/repository.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index 47e1bc1aab..f4678d933f 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -101,18 +101,17 @@ impl GitRepository for LibGitRepository { let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); options.disable_pathspec_match(true); + options.show(StatusShow::Index); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { - for status in statuses - .iter() - .filter(|status| !status.status().contains(git2::Status::IGNORED)) - { + for status in statuses.iter() { let path = RepoPath(PathBuf::from(OsStr::from_bytes(status.path_bytes()))); - let Some(status) = read_status(status.status()) else { - continue - }; - - map.insert(path, status) + let status = status.status(); + if !status.contains(git2::Status::IGNORED) { + if let Some(status) = read_status(status) { + map.insert(path, status) + } + } } } map From 8fff0b0ff8ade2b43f2608824a3cc7b3878afae3 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 23 Jul 2023 21:34:12 -0700 Subject: [PATCH 103/124] Fix pathspec in staged_statuses Enable non-literal matching so that directory paths match all files contained within them. --- crates/fs/src/repository.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/fs/src/repository.rs b/crates/fs/src/repository.rs index f4678d933f..2b2aebe679 100644 --- a/crates/fs/src/repository.rs +++ b/crates/fs/src/repository.rs @@ -100,7 +100,6 @@ impl GitRepository for LibGitRepository { let mut options = git2::StatusOptions::new(); options.pathspec(path_prefix); - options.disable_pathspec_match(true); options.show(StatusShow::Index); if let Some(statuses) = self.statuses(Some(&mut options)).log_err() { From a4914fcf3b0e80505ead8ef3734d572be027aab1 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Sun, 23 Jul 2023 16:32:14 -0700 Subject: [PATCH 104/124] Add tree-sitter-elm --- Cargo.lock | 11 ++++ Cargo.toml | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/elm/config.toml | 11 ++++ crates/zed/src/languages/elm/highlights.scm | 71 +++++++++++++++++++++ crates/zed/src/languages/elm/injections.scm | 2 + 7 files changed, 98 insertions(+) create mode 100644 crates/zed/src/languages/elm/config.toml create mode 100644 crates/zed/src/languages/elm/highlights.scm create mode 100644 crates/zed/src/languages/elm/injections.scm diff --git a/Cargo.lock b/Cargo.lock index f0c8917aa2..eb6c51e47f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7989,6 +7989,16 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-elm" +version = "5.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec83a2e1cfc69d03c8e73636e95662d6c6728539538d341b21251a77039fb94e" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-embedded-template" version = "0.20.0" @@ -9543,6 +9553,7 @@ dependencies = [ "tree-sitter-cpp", "tree-sitter-css", "tree-sitter-elixir", + "tree-sitter-elm", "tree-sitter-embedded-template", "tree-sitter-go", "tree-sitter-heex", diff --git a/Cargo.toml b/Cargo.toml index fa824115cb..03614e61e4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -112,6 +112,7 @@ tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } +tree-sitter-elm = "5.6.4" tree-sitter-embedded-template = "0.20.0" tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index c5bf313701..df7f4bcce9 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -109,6 +109,7 @@ tree-sitter-c.workspace = true tree-sitter-cpp.workspace = true tree-sitter-css.workspace = true tree-sitter-elixir.workspace = true +tree-sitter-elm.workspace = true tree-sitter-embedded-template.workspace = true tree-sitter-go.workspace = true tree-sitter-heex.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index 365e8a3023..c7b5c59b87 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -152,6 +152,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { tree_sitter_php::language(), vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], ); + language("elm", tree_sitter_elm::language(), vec![]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/elm/config.toml b/crates/zed/src/languages/elm/config.toml new file mode 100644 index 0000000000..5051427a93 --- /dev/null +++ b/crates/zed/src/languages/elm/config.toml @@ -0,0 +1,11 @@ +name = "Elm" +path_suffixes = ["elm"] +line_comment = "-- " +block_comment = ["{- ", " -}"] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, + { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, + { start = "'", end = "'", close = true, newline = false, not_in = ["string", "comment"] }, +] diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm new file mode 100644 index 0000000000..f6be193f83 --- /dev/null +++ b/crates/zed/src/languages/elm/highlights.scm @@ -0,0 +1,71 @@ +[ + "if" + "then" + "else" + "let" + "in" + (case) + (of) + (backslash) + (as) + (port) + (exposing) + (alias) + (import) + (module) + (type) + ] @keyword + +[ + (arrow) + (eq) + (operator_identifier) + (colon) +] @operator + +(type_annotation(lower_case_identifier) @function) +(port_annotation(lower_case_identifier) @function) +(function_declaration_left(lower_case_identifier) @function.definition) +(function_call_expr target: (value_expr) @identifier) + +(exposed_value(lower_case_identifier) @function) +(exposed_type(upper_case_identifier) @type) + +(field_access_expr(value_expr(value_qid)) @identifier) +(lower_pattern) @variable +(record_base_identifier) @identifier + +[ + "(" + ")" +] @punctuation.bracket + +[ + "|" + "," +] @punctuation.delimiter + +(number_constant_expr) @constant + +(type_declaration(upper_case_identifier) @type) +(type_ref) @type +(type_alias_declaration name: (upper_case_identifier) @type) + +(union_variant(upper_case_identifier) @variant) +(union_pattern) @variant +(value_expr(upper_case_qid(upper_case_identifier)) @type) + +[ + (line_comment) + (block_comment) +] @comment + +(string_escape) @string.escape + +[ + (open_quote) + (close_quote) + (regular_string_part) + (open_char) + (close_char) +] @string diff --git a/crates/zed/src/languages/elm/injections.scm b/crates/zed/src/languages/elm/injections.scm new file mode 100644 index 0000000000..0567320675 --- /dev/null +++ b/crates/zed/src/languages/elm/injections.scm @@ -0,0 +1,2 @@ +((glsl_content) @content + (#set! "language" "glsl")) From dd504f5965f62d4d3aa4d079b039e68461ae1487 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Sun, 23 Jul 2023 16:43:44 -0700 Subject: [PATCH 105/124] Add tree-sitter-glsl --- Cargo.lock | 10 ++ Cargo.toml | 1 + crates/zed/Cargo.toml | 1 + crates/zed/src/languages.rs | 1 + crates/zed/src/languages/glsl/config.toml | 9 ++ crates/zed/src/languages/glsl/highlights.scm | 118 +++++++++++++++++++ 6 files changed, 140 insertions(+) create mode 100644 crates/zed/src/languages/glsl/config.toml create mode 100644 crates/zed/src/languages/glsl/highlights.scm diff --git a/Cargo.lock b/Cargo.lock index eb6c51e47f..3aa77e2ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8009,6 +8009,15 @@ dependencies = [ "tree-sitter", ] +[[package]] +name = "tree-sitter-glsl" +version = "0.1.4" +source = "git+https://github.com/theHamsta/tree-sitter-glsl?rev=2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3#2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "tree-sitter-go" version = "0.19.1" @@ -9555,6 +9564,7 @@ dependencies = [ "tree-sitter-elixir", "tree-sitter-elm", "tree-sitter-embedded-template", + "tree-sitter-glsl", "tree-sitter-go", "tree-sitter-heex", "tree-sitter-html", diff --git a/Cargo.toml b/Cargo.toml index 03614e61e4..10f2160f45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,6 +114,7 @@ tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev tree-sitter-elixir = { git = "https://github.com/elixir-lang/tree-sitter-elixir", rev = "4ba9dab6e2602960d95b2b625f3386c27e08084e" } tree-sitter-elm = "5.6.4" tree-sitter-embedded-template = "0.20.0" +tree-sitter-glsl = { git = "https://github.com/theHamsta/tree-sitter-glsl", rev = "2a56fb7bc8bb03a1892b4741279dd0a8758b7fb3" } tree-sitter-go = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "aeb2f33b366fd78d5789ff104956ce23508b85db" } tree-sitter-heex = { git = "https://github.com/phoenixframework/tree-sitter-heex", rev = "2e1348c3cf2c9323e87c2744796cf3f3868aa82a" } tree-sitter-json = { git = "https://github.com/tree-sitter/tree-sitter-json", rev = "40a81c01a40ac48744e0c8ccabbaba1920441199" } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index df7f4bcce9..9bd2de0acc 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -111,6 +111,7 @@ tree-sitter-css.workspace = true tree-sitter-elixir.workspace = true tree-sitter-elm.workspace = true tree-sitter-embedded-template.workspace = true +tree-sitter-glsl.workspace = true tree-sitter-go.workspace = true tree-sitter-heex.workspace = true tree-sitter-json.workspace = true diff --git a/crates/zed/src/languages.rs b/crates/zed/src/languages.rs index c7b5c59b87..09f5162c12 100644 --- a/crates/zed/src/languages.rs +++ b/crates/zed/src/languages.rs @@ -153,6 +153,7 @@ pub fn init(languages: Arc, node_runtime: Arc) { vec![Arc::new(php::IntelephenseLspAdapter::new(node_runtime))], ); language("elm", tree_sitter_elm::language(), vec![]); + language("glsl", tree_sitter_glsl::language(), vec![]); } #[cfg(any(test, feature = "test-support"))] diff --git a/crates/zed/src/languages/glsl/config.toml b/crates/zed/src/languages/glsl/config.toml new file mode 100644 index 0000000000..4081a6381f --- /dev/null +++ b/crates/zed/src/languages/glsl/config.toml @@ -0,0 +1,9 @@ +name = "GLSL" +path_suffixes = ["vert", "frag", "tesc", "tese", "geom", "comp"] +line_comment = "// " +block_comment = ["/* ", " */"] +brackets = [ + { start = "{", end = "}", close = true, newline = true }, + { start = "[", end = "]", close = true, newline = true }, + { start = "(", end = ")", close = true, newline = true }, +] diff --git a/crates/zed/src/languages/glsl/highlights.scm b/crates/zed/src/languages/glsl/highlights.scm new file mode 100644 index 0000000000..e4503c6fbb --- /dev/null +++ b/crates/zed/src/languages/glsl/highlights.scm @@ -0,0 +1,118 @@ +"break" @keyword +"case" @keyword +"const" @keyword +"continue" @keyword +"default" @keyword +"do" @keyword +"else" @keyword +"enum" @keyword +"extern" @keyword +"for" @keyword +"if" @keyword +"inline" @keyword +"return" @keyword +"sizeof" @keyword +"static" @keyword +"struct" @keyword +"switch" @keyword +"typedef" @keyword +"union" @keyword +"volatile" @keyword +"while" @keyword + +"#define" @keyword +"#elif" @keyword +"#else" @keyword +"#endif" @keyword +"#if" @keyword +"#ifdef" @keyword +"#ifndef" @keyword +"#include" @keyword +(preproc_directive) @keyword + +"--" @operator +"-" @operator +"-=" @operator +"->" @operator +"=" @operator +"!=" @operator +"*" @operator +"&" @operator +"&&" @operator +"+" @operator +"++" @operator +"+=" @operator +"<" @operator +"==" @operator +">" @operator +"||" @operator + +"." @delimiter +";" @delimiter + +(string_literal) @string +(system_lib_string) @string + +(null) @constant +(number_literal) @number +(char_literal) @number + +(call_expression + function: (identifier) @function) +(call_expression + function: (field_expression + field: (field_identifier) @function)) +(function_declarator + declarator: (identifier) @function) +(preproc_function_def + name: (identifier) @function.special) + +(field_identifier) @property +(statement_identifier) @label +(type_identifier) @type +(primitive_type) @type +(sized_type_specifier) @type + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +(identifier) @variable + +(comment) @comment +; inherits: c + +[ + "in" + "out" + "inout" + "uniform" + "shared" + "layout" + "attribute" + "varying" + "buffer" + "coherent" + "readonly" + "writeonly" + "precision" + "highp" + "mediump" + "lowp" + "centroid" + "sample" + "patch" + "smooth" + "flat" + "noperspective" + "invariant" + "precise" +] @type.qualifier + +"subroutine" @keyword.function + +(extension_storage_class) @storageclass + +( + (identifier) @variable.builtin + (#match? @variable.builtin "^gl_") +) From 6ad0852a70402a482841d7cc3d090553a719c19b Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 24 Jul 2023 00:58:59 -0700 Subject: [PATCH 106/124] Add outline.scm for Elm --- crates/zed/src/languages/elm/outline.scm | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 crates/zed/src/languages/elm/outline.scm diff --git a/crates/zed/src/languages/elm/outline.scm b/crates/zed/src/languages/elm/outline.scm new file mode 100644 index 0000000000..c220676290 --- /dev/null +++ b/crates/zed/src/languages/elm/outline.scm @@ -0,0 +1,22 @@ +(type_declaration + (type) @context + (upper_case_identifier) @name) @item + +(type_alias_declaration + (type) @context + (alias) @context + name: (upper_case_identifier) @name) @item + +(type_alias_declaration + typeExpression: + (type_expression + part: (record_type + (field_type + name: (lower_case_identifier) @name)))) @item + +(union_variant + name: (upper_case_identifier) @name) @item + +(value_declaration + functionDeclarationLeft: + (function_declaration_left(lower_case_identifier) @name)) @item From 7dccb487de2238c67c1b37c41e520e10ba7aeb86 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 24 Jul 2023 15:42:10 +0300 Subject: [PATCH 107/124] Fixes a crash when SelectAllMatches action was called on no matches --- crates/search/src/buffer_search.rs | 81 ++++++++++++++++++++++++++---- 1 file changed, 72 insertions(+), 9 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 7fade13a50..5429305098 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -568,7 +568,7 @@ impl BufferSearchBar { } fn select_all_matches(&mut self, _: &SelectAllMatches, cx: &mut ViewContext) { - if !self.dismissed { + if !self.dismissed && self.active_match_index.is_some() { if let Some(searchable_item) = self.active_searchable_item.as_ref() { if let Some(matches) = self .searchable_items_with_matches @@ -1175,9 +1175,16 @@ mod tests { .await .unwrap(); search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); search_bar.activate_current_match(cx); }); + cx.read_window(window_id, |cx| { + assert!( + !editor.is_focused(cx), + "Initially, the editor should not be focused" + ); + }); let initial_selections = editor.update(cx, |editor, cx| { let initial_selections = editor.selections.display_ranges(cx); assert_eq!( @@ -1191,7 +1198,16 @@ mod tests { }); search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); search_bar.select_all_matches(&SelectAllMatches, cx); + }); + cx.read_window(window_id, |cx| { + assert!( + editor.is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + }); + search_bar.update(cx, |search_bar, cx| { let all_selections = editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); assert_eq!( @@ -1199,8 +1215,6 @@ mod tests { expected_query_matches_count, "Should select all `a` characters in the buffer, but got: {all_selections:?}" ); - }); - search_bar.update(cx, |search_bar, _| { assert_eq!( search_bar.active_match_index, Some(0), @@ -1210,6 +1224,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.select_next_match(&SelectNextMatch, cx); + }); + cx.read_window(window_id, |cx| { + assert!( + editor.is_focused(cx), + "Should still have editor focused after SelectNextMatch" + ); + }); + search_bar.update(cx, |search_bar, cx| { let all_selections = editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); assert_eq!( @@ -1221,8 +1243,6 @@ mod tests { all_selections, initial_selections, "Next match should be different from the first selection" ); - }); - search_bar.update(cx, |search_bar, _| { assert_eq!( search_bar.active_match_index, Some(1), @@ -1231,7 +1251,16 @@ mod tests { }); search_bar.update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); search_bar.select_all_matches(&SelectAllMatches, cx); + }); + cx.read_window(window_id, |cx| { + assert!( + editor.is_focused(cx), + "Should focus editor after successful SelectAllMatches" + ); + }); + search_bar.update(cx, |search_bar, cx| { let all_selections = editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); assert_eq!( @@ -1239,8 +1268,6 @@ mod tests { expected_query_matches_count, "Should select all `a` characters in the buffer, but got: {all_selections:?}" ); - }); - search_bar.update(cx, |search_bar, _| { assert_eq!( search_bar.active_match_index, Some(1), @@ -1250,6 +1277,14 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.select_prev_match(&SelectPrevMatch, cx); + }); + cx.read_window(window_id, |cx| { + assert!( + editor.is_focused(cx), + "Should still have editor focused after SelectPrevMatch" + ); + }); + let last_match_selections = search_bar.update(cx, |search_bar, cx| { let all_selections = editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); assert_eq!( @@ -1261,13 +1296,41 @@ mod tests { all_selections, initial_selections, "Previous match should be the same as the first selection" ); - }); - search_bar.update(cx, |search_bar, _| { assert_eq!( search_bar.active_match_index, Some(0), "Match index should be updated to the previous one" ); + all_selections + }); + + search_bar + .update(cx, |search_bar, cx| { + cx.focus(search_bar.query_editor.as_any()); + search_bar.search("abas_nonexistent_match", None, cx) + }) + .await + .unwrap(); + search_bar.update(cx, |search_bar, cx| { + search_bar.select_all_matches(&SelectAllMatches, cx); + }); + cx.read_window(window_id, |cx| { + assert!( + !editor.is_focused(cx), + "Should not switch focus to editor if SelectAllMatches does not find any matches" + ); + }); + search_bar.update(cx, |search_bar, cx| { + let all_selections = + editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)); + assert_eq!( + all_selections, last_match_selections, + "Should not select anything new if there are no matches" + ); + assert!( + search_bar.active_match_index.is_none(), + "For no matches, there should be no active match index" + ); }); } } From 43d94e37eccd15ac5fad02811989682c387728b6 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 24 Jul 2023 09:37:48 -0600 Subject: [PATCH 108/124] Refactor mode indicator to remove itself One of the problems we had is that the status_bar shows a gap between items, and we want to not add an additional gap for an invisible status indicator. --- crates/theme/src/theme.rs | 2 +- crates/vim/src/mode_indicator.rs | 49 +++++++++--------------- crates/vim/src/test.rs | 58 ++++++++++++++++++++++++++++- crates/vim/src/vim.rs | 52 ++++++++++++++++++++++++++ crates/workspace/src/status_bar.rs | 17 ++++++--- crates/zed/src/zed.rs | 3 +- styles/src/style_tree/status_bar.ts | 2 +- 7 files changed, 142 insertions(+), 41 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index de0701a343..82c3f2a142 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -402,7 +402,7 @@ pub struct StatusBar { pub height: f32, pub item_spacing: f32, pub cursor_position: TextStyle, - pub vim_mode: TextStyle, + pub vim_mode_indicator: TextStyle, pub active_language: Interactive, pub auto_update_progress_message: TextStyle, pub auto_update_done_message: TextStyle, diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index ef8d159018..683024267c 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,30 +1,18 @@ -use gpui::{ - elements::{Empty, Label}, - AnyElement, Element, Entity, View, ViewContext, -}; +use gpui::{elements::Label, AnyElement, Element, Entity, View, ViewContext}; use workspace::{item::ItemHandle, StatusItemView}; -use crate::{state::Mode, Vim}; +use crate::state::Mode; pub struct ModeIndicator { - mode: Option, + pub mode: Mode, } impl ModeIndicator { - pub fn new(cx: &mut ViewContext) -> Self { - cx.observe_global::(|this, cx| { - let vim = Vim::read(cx); - if vim.enabled { - this.set_mode(Some(Vim::read(cx).state.mode), cx) - } else { - this.set_mode(None, cx) - } - }) - .detach(); - Self { mode: None } + pub fn new(mode: Mode) -> Self { + Self { mode } } - pub fn set_mode(&mut self, mode: Option, cx: &mut ViewContext) { + pub fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext) { if mode != self.mode { self.mode = mode; cx.notify(); @@ -38,22 +26,21 @@ impl Entity for ModeIndicator { impl View for ModeIndicator { fn ui_name() -> &'static str { - "ModeIndicator" + "ModeIndicatorView" } fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(mode) = self.mode { - let theme = &theme::current(cx).workspace.status_bar; - let text = match mode { - Mode::Normal => "", - Mode::Insert => "--- INSERT ---", - Mode::Visual { line: false } => "--- VISUAL ---", - Mode::Visual { line: true } => "--- VISUAL LINE ---", - }; - Label::new(text, theme.vim_mode.clone()).into_any() - } else { - Empty::new().into_any() - } + let theme = &theme::current(cx).workspace.status_bar; + // we always choose text to be 12 monospace characters + // so that as the mode indicator changes, the rest of the + // UI stays still. + let text = match self.mode { + Mode::Normal => "-- NORMAL --", + Mode::Insert => "-- INSERT --", + Mode::Visual { line: false } => "-- VISUAL --", + Mode::Visual { line: true } => "VISUAL LINE ", + }; + Label::new(text, theme.vim_mode_indicator.clone()).into_any() } } diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 8ed649e61b..96d6a2b690 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -4,6 +4,8 @@ mod neovim_connection; mod vim_binding_test_context; mod vim_test_context; +use std::sync::Arc; + use command_palette::CommandPalette; use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; @@ -14,7 +16,7 @@ pub use vim_test_context::*; use indoc::indoc; use search::BufferSearchBar; -use crate::state::Mode; +use crate::{state::Mode, ModeIndicator}; #[gpui::test] async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { @@ -195,3 +197,57 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { cx.simulate_keystrokes(["shift-n"]); cx.assert_state(indoc! {"aa\nbb\nˇcc\ncc\ncc\n"}, Mode::Normal); } + +#[gpui::test] +async fn test_status_indicator( + cx: &mut gpui::TestAppContext, + deterministic: Arc, +) { + let mut cx = VimTestContext::new(cx, true).await; + deterministic.run_until_parked(); + + let mode_indicator = cx.workspace(|workspace, cx| { + let status_bar = workspace.status_bar().read(cx); + let mode_indicator = status_bar.item_of_type::(); + assert!(mode_indicator.is_some()); + mode_indicator.unwrap() + }); + + assert_eq!( + cx.workspace(|_, cx| mode_indicator.read(cx).mode), + Mode::Normal + ); + + // shows the correct mode + cx.simulate_keystrokes(["i"]); + deterministic.run_until_parked(); + assert_eq!( + cx.workspace(|_, cx| mode_indicator.read(cx).mode), + Mode::Insert + ); + + // shows even in search + cx.simulate_keystrokes(["escape", "v", "/"]); + deterministic.run_until_parked(); + assert_eq!( + cx.workspace(|_, cx| mode_indicator.read(cx).mode), + Mode::Visual { line: false } + ); + + // hides if vim mode is disabled + cx.disable_vim(); + deterministic.run_until_parked(); + cx.workspace(|workspace, cx| { + let status_bar = workspace.status_bar().read(cx); + let mode_indicator = status_bar.item_of_type::(); + assert!(mode_indicator.is_none()); + }); + + cx.enable_vim(); + deterministic.run_until_parked(); + cx.workspace(|workspace, cx| { + let status_bar = workspace.status_bar().read(cx); + let mode_indicator = status_bar.item_of_type::(); + assert!(mode_indicator.is_some()); + }); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8ab110ba1e..54d18825cd 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -118,6 +118,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) { pub struct Vim { active_editor: Option>, editor_subscription: Option, + mode_indicator: Option>, enabled: bool, state: VimState, @@ -177,6 +178,10 @@ impl Vim { self.state.mode = mode; self.state.operator_stack.clear(); + if let Some(mode_indicator) = &self.mode_indicator { + mode_indicator.update(cx, |mode_indicator, cx| mode_indicator.set_mode(mode, cx)) + } + // Sync editor settings like clip mode self.sync_vim_settings(cx); @@ -259,6 +264,51 @@ impl Vim { } } + fn sync_mode_indicator(cx: &mut AppContext) { + cx.spawn(|mut cx| async move { + let workspace = match cx.update(|cx| { + cx.update_active_window(|cx| { + cx.root_view() + .downcast_ref::() + .map(|workspace| workspace.downgrade()) + }) + }) { + Some(Some(workspace)) => workspace, + _ => { + return Ok(()); + } + }; + + workspace.update(&mut cx, |workspace, cx| { + Vim::update(cx, |vim, cx| { + workspace.status_bar().update(cx, |status_bar, cx| { + let current_position = status_bar.position_of_item::(); + if vim.enabled && current_position.is_none() { + if vim.mode_indicator.is_none() { + vim.mode_indicator = + Some(cx.add_view(|_| ModeIndicator::new(vim.state.mode))); + }; + let mode_indicator = vim.mode_indicator.as_ref().unwrap(); + // TODO: would it be better to depend on the diagnostics crate + // so we can pass the type directly? + let position = status_bar.position_of_named_item("DiagnosticIndicator"); + if let Some(position) = position { + status_bar.insert_item_after(position, mode_indicator.clone(), cx) + } else { + status_bar.add_left_item(mode_indicator.clone(), cx) + } + } else if !vim.enabled { + if let Some(position) = current_position { + status_bar.remove_item_at(position, cx) + } + } + }) + }) + }) + }) + .detach_and_log_err(cx); + } + fn set_enabled(&mut self, enabled: bool, cx: &mut AppContext) { if self.enabled != enabled { self.enabled = enabled; @@ -309,6 +359,8 @@ impl Vim { self.unhook_vim_settings(editor, cx); } }); + + Vim::sync_mode_indicator(cx); } fn unhook_vim_settings(&self, editor: &mut Editor, cx: &mut ViewContext) { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 6fd3bd5310..7b1c11dcf2 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,4 +1,4 @@ -use std::{any::TypeId, ops::Range}; +use std::ops::Range; use crate::{ItemHandle, Pane}; use gpui::{ @@ -96,14 +96,21 @@ impl StatusBar { cx.notify(); } - pub fn position_of_item(&mut self) -> Option + pub fn position_of_item(&self) -> Option where T: StatusItemView, { self.position_of_named_item(T::ui_name()) } - pub fn position_of_named_item(&mut self, name: &str) -> Option { + pub fn item_of_type(&self) -> Option> { + self.left_items + .iter() + .chain(self.right_items.iter()) + .find_map(|item| item.as_any().clone().downcast()) + } + + pub fn position_of_named_item(&self, name: &str) -> Option { for (index, item) in self.left_items.iter().enumerate() { if item.as_ref().ui_name() == name { return Some(index); @@ -126,10 +133,10 @@ impl StatusBar { T: 'static + StatusItemView, { if position < self.left_items.len() { - self.left_items.insert(position, Box::new(item)) + self.left_items.insert(position + 1, Box::new(item)) } else { self.right_items - .insert(position - self.left_items.len(), Box::new(item)) + .insert(position + 1 - self.left_items.len(), Box::new(item)) } cx.notify() } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 645371d419..639e1a3f60 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -312,11 +312,10 @@ pub fn initialize_workspace( feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) }); let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - let vim_mode = cx.add_view(|cx| vim::ModeIndicator::new(cx)); workspace.status_bar().update(cx, |status_bar, cx| { status_bar.add_left_item(diagnostic_summary, cx); - status_bar.add_left_item(vim_mode, cx); status_bar.add_left_item(activity_indicator, cx); + status_bar.add_right_item(feedback_button, cx); status_bar.add_right_item(copilot, cx); status_bar.add_right_item(active_buffer_language, cx); diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index b4273cbf99..74ad7064d1 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -27,7 +27,7 @@ export default function status_bar(): any { }, border: border(layer, { top: true, overlay: true }), cursor_position: text(layer, "sans", "variant"), - vim_mode: text(layer, "sans", "variant"), + vim_mode_indicator: text(layer, "mono", "variant"), active_language: interactive({ base: { padding: { left: 6, right: 6 }, From ea74734b0a8ff445609732fe334a15284a26acee Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 24 Jul 2023 08:51:44 -0700 Subject: [PATCH 109/124] Touch up elm tree sitter integration --- crates/zed/src/languages/elm/highlights.scm | 1 + crates/zed/src/languages/elm/outline.scm | 7 +++---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm index f6be193f83..e20873af1b 100644 --- a/crates/zed/src/languages/elm/highlights.scm +++ b/crates/zed/src/languages/elm/highlights.scm @@ -53,6 +53,7 @@ (union_variant(upper_case_identifier) @variant) (union_pattern) @variant + (value_expr(upper_case_qid(upper_case_identifier)) @type) [ diff --git a/crates/zed/src/languages/elm/outline.scm b/crates/zed/src/languages/elm/outline.scm index c220676290..88ff9a5af2 100644 --- a/crates/zed/src/languages/elm/outline.scm +++ b/crates/zed/src/languages/elm/outline.scm @@ -12,11 +12,10 @@ (type_expression part: (record_type (field_type - name: (lower_case_identifier) @name)))) @item + name: (lower_case_identifier) @name) @item))) (union_variant name: (upper_case_identifier) @name) @item -(value_declaration - functionDeclarationLeft: - (function_declaration_left(lower_case_identifier) @name)) @item +(type_annotation + name: (_) @name) @item From baa16a2fc6d23b208f4824ad8b6130ff5385f1d1 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 24 Jul 2023 09:57:51 -0600 Subject: [PATCH 110/124] Better method ordering --- crates/workspace/src/status_bar.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 7b1c11dcf2..8c3cfe2053 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -96,13 +96,6 @@ impl StatusBar { cx.notify(); } - pub fn position_of_item(&self) -> Option - where - T: StatusItemView, - { - self.position_of_named_item(T::ui_name()) - } - pub fn item_of_type(&self) -> Option> { self.left_items .iter() @@ -110,6 +103,13 @@ impl StatusBar { .find_map(|item| item.as_any().clone().downcast()) } + pub fn position_of_item(&self) -> Option + where + T: StatusItemView, + { + self.position_of_named_item(T::ui_name()) + } + pub fn position_of_named_item(&self, name: &str) -> Option { for (index, item) in self.left_items.iter().enumerate() { if item.as_ref().ui_name() == name { From d95c4fdb2b182c0ae6238614cdc966b829646de4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 24 Jul 2023 09:01:15 -0700 Subject: [PATCH 111/124] Remove unbound highlight queries --- crates/zed/src/languages/elm/highlights.scm | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm index e20873af1b..3ab1028a3a 100644 --- a/crates/zed/src/languages/elm/highlights.scm +++ b/crates/zed/src/languages/elm/highlights.scm @@ -51,9 +51,6 @@ (type_ref) @type (type_alias_declaration name: (upper_case_identifier) @type) -(union_variant(upper_case_identifier) @variant) -(union_pattern) @variant - (value_expr(upper_case_qid(upper_case_identifier)) @type) [ From 41105136a45ff46eca75753fe1361705ba93b1b6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 24 Jul 2023 10:20:10 -0700 Subject: [PATCH 112/124] Add MacOS standard key binding for file renames --- assets/keymaps/default.json | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index d970df1abd..5c1002a5fc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -528,6 +528,7 @@ "cmd-alt-c": "project_panel::CopyPath", "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", + "enter": "project_panel::Rename", "backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" From fbe01089598fee96a5bbf87a44161dba330d7ec8 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 24 Jul 2023 10:44:56 -0700 Subject: [PATCH 113/124] Highlight Elm arrows as keywords --- crates/zed/src/languages/elm/highlights.scm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm index 3ab1028a3a..be66578453 100644 --- a/crates/zed/src/languages/elm/highlights.scm +++ b/crates/zed/src/languages/elm/highlights.scm @@ -14,10 +14,10 @@ (import) (module) (type) + (arrow) ] @keyword [ - (arrow) (eq) (operator_identifier) (colon) From 62ee52a5fc6e98409aa25e9e6dd7b815068f42be Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 24 Jul 2023 10:45:15 -0700 Subject: [PATCH 114/124] Highlight qualified Elm function calls --- crates/zed/src/languages/elm/highlights.scm | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm index be66578453..5838aa4796 100644 --- a/crates/zed/src/languages/elm/highlights.scm +++ b/crates/zed/src/languages/elm/highlights.scm @@ -26,6 +26,11 @@ (type_annotation(lower_case_identifier) @function) (port_annotation(lower_case_identifier) @function) (function_declaration_left(lower_case_identifier) @function.definition) + +(function_call_expr + target: (value_expr + name: (value_qid (dot) (lower_case_identifier) @function))) + (function_call_expr target: (value_expr) @identifier) (exposed_value(lower_case_identifier) @function) From 3cc88904bfa33f2fd84aa75271b4df851c2148dd Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 24 Jul 2023 10:45:46 -0700 Subject: [PATCH 115/124] Add all Elm functions to the outline, including locals --- crates/zed/src/languages/elm/outline.scm | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/zed/src/languages/elm/outline.scm b/crates/zed/src/languages/elm/outline.scm index 88ff9a5af2..1d7d5a70b0 100644 --- a/crates/zed/src/languages/elm/outline.scm +++ b/crates/zed/src/languages/elm/outline.scm @@ -17,5 +17,6 @@ (union_variant name: (upper_case_identifier) @name) @item -(type_annotation - name: (_) @name) @item +(value_declaration + functionDeclarationLeft: + (function_declaration_left(lower_case_identifier) @name)) @item From e199a6a3a1024a4f49ed5bc9dd68c025e3d4ff66 Mon Sep 17 00:00:00 2001 From: Quinn Wilton Date: Mon, 24 Jul 2023 10:55:15 -0700 Subject: [PATCH 116/124] Highlight all Elm function calls --- crates/zed/src/languages/elm/highlights.scm | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/zed/src/languages/elm/highlights.scm b/crates/zed/src/languages/elm/highlights.scm index 5838aa4796..5723c7eecb 100644 --- a/crates/zed/src/languages/elm/highlights.scm +++ b/crates/zed/src/languages/elm/highlights.scm @@ -29,9 +29,7 @@ (function_call_expr target: (value_expr - name: (value_qid (dot) (lower_case_identifier) @function))) - -(function_call_expr target: (value_expr) @identifier) + name: (value_qid (lower_case_identifier) @function))) (exposed_value(lower_case_identifier) @function) (exposed_type(upper_case_identifier) @type) From bdd0b9f3878d3548c03d7f70d219172b655c7443 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 24 Jul 2023 16:35:01 -0400 Subject: [PATCH 117/124] Add open file in project finder via space (#2785) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @mikayla-maki for 👀 [This PR added in the ability to rename a file via `enter`](https://github.com/zed-industries/zed/pull/2784). Previously, `enter` was used to both open a file and confirm a rename, so this PR changes the opening of a file to use `space`, which is what VS Code uses. It also makes a bit more sense because now `enter` is just used to start a rename and confirm the rename, vs being used for 2 different actions. N/A on the release notes, as I adjusted the release note in the previously-tagged PR. Release Notes: - N/A --- assets/keymaps/default.json | 1 + crates/project_panel/src/project_panel.rs | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 5c1002a5fc..7553c19925 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -529,6 +529,7 @@ "alt-cmd-shift-c": "project_panel::CopyRelativePath", "f2": "project_panel::Rename", "enter": "project_panel::Rename", + "space": "project_panel::Open", "backspace": "project_panel::Delete", "alt-cmd-r": "project_panel::RevealInFinder", "alt-shift-f": "project_panel::NewSearchInDirectory" diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 2a6da7db28..3e20c4986e 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -125,6 +125,7 @@ actions!( Paste, Delete, Rename, + Open, ToggleFocus, NewSearchInDirectory, ] @@ -146,6 +147,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { cx.add_action(ProjectPanel::rename); cx.add_async_action(ProjectPanel::delete); cx.add_async_action(ProjectPanel::confirm); + cx.add_async_action(ProjectPanel::open_file); cx.add_action(ProjectPanel::cancel); cx.add_action(ProjectPanel::cut); cx.add_action(ProjectPanel::copy); @@ -560,15 +562,20 @@ impl ProjectPanel { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { if let Some(task) = self.confirm_edit(cx) { - Some(task) - } else if let Some((_, entry)) = self.selected_entry(cx) { + return Some(task); + } + + None + } + + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { self.open_entry(entry.id, true, cx); } - None - } else { - None } + + None } fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { @@ -2382,7 +2389,7 @@ mod tests { toggle_expand_dir(&panel, "src/test", cx); select_path(&panel, "src/test/first.rs", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); cx.foreground().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2410,7 +2417,7 @@ mod tests { ensure_no_open_items_and_panes(window_id, &workspace, cx); select_path(&panel, "src/test/second.rs", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); + panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); cx.foreground().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), From 1a8438288128bdda84dc35e6efc958aa8415b70e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 24 Jul 2023 22:30:51 -0400 Subject: [PATCH 118/124] WIP --- crates/editor/src/editor.rs | 103 ++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 79 +++++++++++++++++++++++ 2 files changed, 182 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f1cf5a942b..fe7667cd2b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,6 +74,8 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use rand::seq::SliceRandom; +use rand::thread_rng; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -226,6 +228,10 @@ actions!( MoveLineUp, MoveLineDown, JoinLines, + SortLinesCaseSensitive, + SortLinesCaseInsensitive, + ReverseLines, + ShuffleLines, Transpose, Cut, Copy, @@ -344,6 +350,10 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); cx.add_action(Editor::join_lines); + cx.add_action(Editor::sort_lines_case_sensitive); + cx.add_action(Editor::sort_lines_case_insensitive); + cx.add_action(Editor::reverse_lines); + // cx.add_action(Editor::shuffle_lines); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -4205,6 +4215,99 @@ impl Editor { }); } + pub fn sort_lines_case_sensitive( + &mut self, + _: &SortLinesCaseSensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, { + |mut lines| { + lines.sort(); + lines + } + }) + } + + pub fn sort_lines_case_insensitive( + &mut self, + _: &SortLinesCaseInsensitive, + cx: &mut ViewContext, + ) { + self.manipulate_lines(cx, { + |mut lines| { + lines.sort_by_key(|line| line.to_lowercase()); + lines + } + }) + } + + pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, { + |mut lines| { + lines.reverse(); + lines + } + }) + } + + // pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { + // self.manipulate_lines(cx, { + // |mut lines| { + // lines.shuffle(&mut thread_rng()); + // lines + // } + // }) + // } + + fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) + where + Fn: FnMut(Vec<&str>) -> Vec<&str>, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut edits = Vec::new(); + + let selections = self.selections.all::(cx); + let mut selections = selections.iter().peekable(); + let mut contiguous_row_selections = Vec::new(); + + while let Some(selection) = selections.next() { + // Find all the selections that span a contiguous row range + let (start_row, end_row) = consume_contiguous_rows( + &mut contiguous_row_selections, + selection, + &display_map, + &mut selections, + ); + let start = Point::new(start_row, 0); + let end = Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let text = buffer.text_for_range(start..end).collect::(); + // TODO SORT LINES: Is there a smarter / more effificent way to obtain lines? + let lines = text.split("\n").collect::>(); + let lines = callback(lines); + edits.push((start..end, lines.join("\n"))); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(contiguous_row_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + + // TODO: + // Write tests + // - Use cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); in tests + // Mikayla check for perf stuff + // Shuffle + } + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 247a7b021d..18028eb8ee 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2500,6 +2500,85 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { }); } +#[gpui::test] +fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(0, 2)]) + }); + editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); + assert_eq!( + buffer.read(cx).text(), + "dddd\nccc\nbb\na\n\n", + "no sorting when single cursor parked on single line" + ); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 2)..Point::new(0, 2)] + ); + + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(0, 2)..Point::new(5, 1)]) + }); + editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); + assert_eq!( + buffer.read(cx).text(), + "a\nbb\nccc\ndddd\n\n", + "single selection is sorted" + ); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 0)..Point::new(5, 1)] + ); + + editor + }); +} + +#[gpui::test] +fn test_sort_lines_with_multi_selection(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + cx.add_window(|cx| { + let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n3\n2\n1\n\n", cx); + let mut editor = build_editor(buffer.clone(), cx); + let buffer = buffer.read(cx).as_singleton().unwrap(); + + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(0, 2)..Point::new(3, 2), + Point::new(5, 0)..Point::new(7, 1), + ]) + }); + + editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); + assert_eq!(buffer.read(cx).text(), "a\nbb\nccc\ndddd\n\n1\n2\n3\n\n"); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + assert_eq!( + editor.selections.ranges::(cx), + &[Point::new(0, 5)..Point::new(2, 2)] + ); + + // assert_eq!( + // editor.selections.ranges::(cx), + // [ + // Point::new(0, 7)..Point::new(0, 7), + // Point::new(1, 3)..Point::new(1, 3) + // ] + // ); + editor + }); +} + #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); From 299818cde0afeb4a0d83311a238f8fea6f77f199 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 25 Jul 2023 11:44:13 -0400 Subject: [PATCH 119/124] Fix rand import and tweak callbacks Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/editor/Cargo.toml | 3 +-- crates/editor/src/editor.rs | 53 ++++++++++--------------------------- 2 files changed, 15 insertions(+), 41 deletions(-) diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 087ce81c26..bc1c904404 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -10,7 +10,6 @@ doctest = false [features] test-support = [ - "rand", "copilot/test-support", "text/test-support", "language/test-support", @@ -62,8 +61,8 @@ serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true +rand.workspace = true -rand = { workspace = true, optional = true } tree-sitter-rust = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true } tree-sitter-typescript = { workspace = true, optional = true } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fe7667cd2b..04821df62e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -74,8 +74,9 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; -use rand::seq::SliceRandom; -use rand::thread_rng; +use rand::{seq::SliceRandom, thread_rng}; +// use rand::seq::SliceRandom; +// use rand::thread_rng; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -353,7 +354,7 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::sort_lines_case_sensitive); cx.add_action(Editor::sort_lines_case_insensitive); cx.add_action(Editor::reverse_lines); - // cx.add_action(Editor::shuffle_lines); + cx.add_action(Editor::shuffle_lines); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -4220,12 +4221,7 @@ impl Editor { _: &SortLinesCaseSensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, { - |mut lines| { - lines.sort(); - lines - } - }) + self.manipulate_lines(cx, |lines| lines.sort()) } pub fn sort_lines_case_insensitive( @@ -4233,35 +4229,20 @@ impl Editor { _: &SortLinesCaseInsensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, { - |mut lines| { - lines.sort_by_key(|line| line.to_lowercase()); - lines - } - }) + self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase())) } pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, { - |mut lines| { - lines.reverse(); - lines - } - }) + self.manipulate_lines(cx, |lines| lines.reverse()) } - // pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { - // self.manipulate_lines(cx, { - // |mut lines| { - // lines.shuffle(&mut thread_rng()); - // lines - // } - // }) - // } + pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { + self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) + } fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) where - Fn: FnMut(Vec<&str>) -> Vec<&str>, + Fn: FnMut(&mut Vec<&str>), { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -4280,12 +4261,12 @@ impl Editor { &display_map, &mut selections, ); + let start = Point::new(start_row, 0); let end = Point::new(end_row - 1, buffer.line_len(end_row - 1)); let text = buffer.text_for_range(start..end).collect::(); - // TODO SORT LINES: Is there a smarter / more effificent way to obtain lines? - let lines = text.split("\n").collect::>(); - let lines = callback(lines); + let mut lines = text.split("\n").collect::>(); + callback(&mut lines); edits.push((start..end, lines.join("\n"))); } @@ -4300,12 +4281,6 @@ impl Editor { this.request_autoscroll(Autoscroll::fit(), cx); }); - - // TODO: - // Write tests - // - Use cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six "); in tests - // Mikayla check for perf stuff - // Shuffle } pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { From 1f65effe57b85d126fcb5689d6d37ec3f768502e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 25 Jul 2023 10:58:44 -0600 Subject: [PATCH 120/124] Update status bar theming Co-Authored-By: Nate Butler --- Cargo.lock | 1 + crates/theme/src/theme.rs | 2 +- crates/vim/Cargo.toml | 1 + crates/vim/src/mode_indicator.rs | 5 ++++- crates/vim/src/vim.rs | 5 ++--- crates/workspace/src/status_bar.rs | 8 ++------ styles/src/style_tree/status_bar.ts | 31 +++++++++++++---------------- 7 files changed, 25 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e805a87230..704eba74b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8514,6 +8514,7 @@ dependencies = [ "indoc", "itertools", "language", + "language_selector", "log", "nvim-rs", "parking_lot 0.11.2", diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 82c3f2a142..4766f636f3 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -402,7 +402,7 @@ pub struct StatusBar { pub height: f32, pub item_spacing: f32, pub cursor_position: TextStyle, - pub vim_mode_indicator: TextStyle, + pub vim_mode_indicator: ContainedText, pub active_language: Interactive, pub auto_update_progress_message: TextStyle, pub auto_update_done_message: TextStyle, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 3a5974d6c9..2d394e3dcf 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -33,6 +33,7 @@ search = { path = "../search" } settings = { path = "../settings" } workspace = { path = "../workspace" } theme = { path = "../theme" } +language_selector = { path = "../language_selector"} [dev-dependencies] indoc.workspace = true diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 683024267c..e0d2b65955 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -40,7 +40,10 @@ impl View for ModeIndicator { Mode::Visual { line: false } => "-- VISUAL --", Mode::Visual { line: true } => "VISUAL LINE ", }; - Label::new(text, theme.vim_mode_indicator.clone()).into_any() + Label::new(text, theme.vim_mode_indicator.text.clone()) + .contained() + .with_style(theme.vim_mode_indicator.container) + .into_any() } } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 54d18825cd..363901d260 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -289,9 +289,8 @@ impl Vim { Some(cx.add_view(|_| ModeIndicator::new(vim.state.mode))); }; let mode_indicator = vim.mode_indicator.as_ref().unwrap(); - // TODO: would it be better to depend on the diagnostics crate - // so we can pass the type directly? - let position = status_bar.position_of_named_item("DiagnosticIndicator"); + let position = status_bar + .position_of_item::(); if let Some(position) = position { status_bar.insert_item_after(position, mode_indicator.clone(), cx) } else { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 8c3cfe2053..8726eaf569 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -107,17 +107,13 @@ impl StatusBar { where T: StatusItemView, { - self.position_of_named_item(T::ui_name()) - } - - pub fn position_of_named_item(&self, name: &str) -> Option { for (index, item) in self.left_items.iter().enumerate() { - if item.as_ref().ui_name() == name { + if item.as_ref().ui_name() == T::ui_name() { return Some(index); } } for (index, item) in self.right_items.iter().enumerate() { - if item.as_ref().ui_name() == name { + if item.as_ref().ui_name() == T::ui_name() { return Some(index + self.left_items.len()); } } diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 74ad7064d1..06afc37823 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -1,6 +1,8 @@ import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../common" +import { text_button } from "../component/text_button" + export default function status_bar(): any { const theme = useTheme() @@ -26,21 +28,16 @@ export default function status_bar(): any { right: 6, }, border: border(layer, { top: true, overlay: true }), - cursor_position: text(layer, "sans", "variant"), - vim_mode_indicator: text(layer, "mono", "variant"), - active_language: interactive({ - base: { - padding: { left: 6, right: 6 }, - ...text(layer, "sans", "variant"), - }, - state: { - hovered: { - ...text(layer, "sans", "on"), - }, - }, + cursor_position: text(layer, "sans", "variant", { size: "xs" }), + vim_mode_indicator: { + margin: { left: 6 }, + ...text(layer, "mono", "variant", { size: "xs" }), + }, + active_language: text_button({ + color: "variant" }), - auto_update_progress_message: text(layer, "sans", "variant"), - auto_update_done_message: text(layer, "sans", "variant"), + auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }), + auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }), lsp_status: interactive({ base: { ...diagnostic_status_container, @@ -60,9 +57,9 @@ export default function status_bar(): any { }), diagnostic_message: interactive({ base: { - ...text(layer, "sans"), + ...text(layer, "sans", { size: "xs" }), }, - state: { hovered: text(layer, "sans", "hovered") }, + state: { hovered: text(layer, "sans", "hovered", { size: "xs" }) }, }), diagnostic_summary: interactive({ base: { @@ -118,7 +115,7 @@ export default function status_bar(): any { icon_color: foreground(layer, "variant"), label: { margin: { left: 6 }, - ...text(layer, "sans", { size: "sm" }), + ...text(layer, "sans", { size: "xs" }), }, }, state: { From 93ec73da2930daf4e4ceeb6b283787e0d2e1581e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 25 Jul 2023 14:04:25 -0400 Subject: [PATCH 121/124] Fix code computing new selections Co-Authored-By: Mikayla Maki --- crates/editor/src/editor.rs | 55 ++++++++++++++++------ crates/editor/src/editor_tests.rs | 12 ++--- crates/editor/src/selections_collection.rs | 2 +- 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 04821df62e..655fb39257 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -75,8 +75,6 @@ pub use multi_buffer::{ use ordered_float::OrderedFloat; use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; -// use rand::seq::SliceRandom; -// use rand::thread_rng; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, }; @@ -4221,7 +4219,7 @@ impl Editor { _: &SortLinesCaseSensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, |lines| lines.sort()) + self.manipulate_lines(cx, |text| text.sort()) } pub fn sort_lines_case_insensitive( @@ -4229,20 +4227,24 @@ impl Editor { _: &SortLinesCaseInsensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase())) + self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase())) } pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| lines.reverse()) + self.manipulate_lines(cx, |lines| { + lines.reverse(); + }) } pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) + self.manipulate_lines(cx, |lines| { + lines.shuffle(&mut thread_rng()); + }) } fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) where - Fn: FnMut(&mut Vec<&str>), + Fn: FnMut(&mut [&str]), { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -4252,9 +4254,9 @@ impl Editor { let selections = self.selections.all::(cx); let mut selections = selections.iter().peekable(); let mut contiguous_row_selections = Vec::new(); + let mut new_selections = Vec::new(); while let Some(selection) = selections.next() { - // Find all the selections that span a contiguous row range let (start_row, end_row) = consume_contiguous_rows( &mut contiguous_row_selections, selection, @@ -4262,12 +4264,35 @@ impl Editor { &mut selections, ); - let start = Point::new(start_row, 0); - let end = Point::new(end_row - 1, buffer.line_len(end_row - 1)); - let text = buffer.text_for_range(start..end).collect::(); - let mut lines = text.split("\n").collect::>(); - callback(&mut lines); - edits.push((start..end, lines.join("\n"))); + let start_point = Point::new(start_row, 0); + let end_point = Point::new(end_row - 1, buffer.line_len(end_row - 1)); + let text = buffer + .text_for_range(start_point..end_point) + .collect::(); + let mut text = text.split("\n").collect_vec(); + + let text_len = text.len(); + callback(&mut text); + + // This is a current limitation with selections. + // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections. + debug_assert!( + text.len() == text_len, + "callback should not change the number of lines" + ); + + edits.push((start_point..end_point, text.join("\n"))); + let start_anchor = buffer.anchor_after(start_point); + let end_anchor = buffer.anchor_before(end_point); + + // Make selection and push + new_selections.push(Selection { + id: selection.id, + start: start_anchor.to_offset(&buffer), + end: end_anchor.to_offset(&buffer), + goal: SelectionGoal::None, + reversed: selection.reversed, + }); } self.transact(cx, |this, cx| { @@ -4276,7 +4301,7 @@ impl Editor { }); this.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select(contiguous_row_selections); + s.select(new_selections); }); this.request_autoscroll(Autoscroll::fit(), cx); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 18028eb8ee..bc814f8c15 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2501,7 +2501,7 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { } #[gpui::test] -fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) { +fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); cx.add_window(|cx| { @@ -2510,7 +2510,7 @@ fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) { let buffer = buffer.read(cx).as_singleton().unwrap(); editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 2)..Point::new(0, 2)]) + s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) }); editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); assert_eq!( @@ -2520,13 +2520,13 @@ fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) { ); assert_eq!( editor.selections.ranges::(cx), - &[Point::new(0, 2)..Point::new(0, 2)] + &[Point::new(0, 1)..Point::new(0, 2)] ); editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 2)..Point::new(5, 1)]) + s.select_ranges([Point::new(0, 2)..Point::new(5, 0)]) }); - editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); + //editor.sort_lines(); assert_eq!( buffer.read(cx).text(), "a\nbb\nccc\ndddd\n\n", @@ -2542,7 +2542,7 @@ fn test_sort_lines_with_single_selection(cx: &mut TestAppContext) { } #[gpui::test] -fn test_sort_lines_with_multi_selection(cx: &mut TestAppContext) { +fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); cx.add_window(|cx| { diff --git a/crates/editor/src/selections_collection.rs b/crates/editor/src/selections_collection.rs index a22506f751..1921bc0738 100644 --- a/crates/editor/src/selections_collection.rs +++ b/crates/editor/src/selections_collection.rs @@ -138,7 +138,7 @@ impl SelectionsCollection { .collect() } - // Returns all of the selections, adjusted to take into account the selection line_mode + /// Returns all of the selections, adjusted to take into account the selection line_mode pub fn all_adjusted(&self, cx: &mut AppContext) -> Vec> { let mut selections = self.all::(cx); if self.line_mode { From bf2ca57f55182ab8568f4d6abe4d69bbc63c9f5b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 25 Jul 2023 14:48:11 -0400 Subject: [PATCH 122/124] Remove { and } from single-line closure --- crates/editor/src/editor.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 655fb39257..e05837740d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4231,15 +4231,11 @@ impl Editor { } pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| { - lines.reverse(); - }) + self.manipulate_lines(cx, |lines| lines.reverse()) } pub fn shuffle_lines(&mut self, _: &ShuffleLines, cx: &mut ViewContext) { - self.manipulate_lines(cx, |lines| { - lines.shuffle(&mut thread_rng()); - }) + self.manipulate_lines(cx, |lines| lines.shuffle(&mut thread_rng())) } fn manipulate_lines(&mut self, cx: &mut ViewContext, mut callback: Fn) From 64b252e81a020f2ccf0895c2afeaf12cff37d182 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Tue, 25 Jul 2023 12:55:01 -0600 Subject: [PATCH 123/124] A little refactor Co-Authored-By: Mikayla Maki --- crates/vim/src/vim.rs | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 363901d260..340da4f896 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -264,25 +264,19 @@ impl Vim { } } - fn sync_mode_indicator(cx: &mut AppContext) { - cx.spawn(|mut cx| async move { - let workspace = match cx.update(|cx| { - cx.update_active_window(|cx| { - cx.root_view() - .downcast_ref::() - .map(|workspace| workspace.downgrade()) - }) - }) { - Some(Some(workspace)) => workspace, - _ => { - return Ok(()); - } + fn sync_mode_indicator(cx: &mut WindowContext) { + let Some(workspace) = cx.root_view() + .downcast_ref::() + .map(|workspace| workspace.downgrade()) else { + return; }; + cx.spawn(|mut cx| async move { workspace.update(&mut cx, |workspace, cx| { Vim::update(cx, |vim, cx| { workspace.status_bar().update(cx, |status_bar, cx| { let current_position = status_bar.position_of_item::(); + if vim.enabled && current_position.is_none() { if vim.mode_indicator.is_none() { vim.mode_indicator = From 4085df5146f015d88c0daca229896ec32e94ee84 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 25 Jul 2023 15:17:16 -0400 Subject: [PATCH 124/124] Add tests for manipulate_lines() --- crates/editor/src/editor_tests.rs | 197 ++++++++++++++++++++---------- 1 file changed, 134 insertions(+), 63 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index bc814f8c15..eb03d2bdc0 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2501,82 +2501,153 @@ fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) { } #[gpui::test] -fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { +async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); - let buffer = buffer.read(cx).as_singleton().unwrap(); + let mut cx = EditorTestContext::new(cx).await; - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 1)..Point::new(0, 1)]) - }); - editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); - assert_eq!( - buffer.read(cx).text(), - "dddd\nccc\nbb\na\n\n", - "no sorting when single cursor parked on single line" - ); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 1)..Point::new(0, 2)] - ); + // Test sort_lines_case_insensitive() + cx.set_state(indoc! {" + «z + y + x + Z + Y + Xˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, cx)); + cx.assert_editor_state(indoc! {" + «x + X + y + Y + z + Zˇ» + "}); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(0, 2)..Point::new(5, 0)]) - }); - //editor.sort_lines(); - assert_eq!( - buffer.read(cx).text(), - "a\nbb\nccc\ndddd\n\n", - "single selection is sorted" - ); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 0)..Point::new(5, 1)] - ); + // Test reverse_lines() + cx.set_state(indoc! {" + «5 + 4 + 3 + 2 + 1ˇ» + "}); + cx.update_editor(|e, cx| e.reverse_lines(&ReverseLines, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + "}); - editor - }); + // Skip testing shuffle_line() + + // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive() + // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines) + + // Don't manipulate when cursor is on single line, but expand the selection + cx.set_state(indoc! {" + ddˇdd + ccc + bb + a + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «ddddˇ» + ccc + bb + a + "}); + + // Basic manipulate case + // Start selection moves to column 0 + // End of selection shrinks to fit shorter line + cx.set_state(indoc! {" + dd«d + ccc + bb + aaaaaˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + dddˇ» + "}); + + // Manipulate case with newlines + cx.set_state(indoc! {" + dd«d + ccc + + bb + aaaaa + + ˇ» + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + « + + aaaaa + bb + ccc + dddˇ» + + "}); } #[gpui::test] -fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { +async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("dddd\nccc\nbb\na\n\n3\n2\n1\n\n", cx); - let mut editor = build_editor(buffer.clone(), cx); - let buffer = buffer.read(cx).as_singleton().unwrap(); + let mut cx = EditorTestContext::new(cx).await; - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(0, 2)..Point::new(3, 2), - Point::new(5, 0)..Point::new(7, 1), - ]) - }); + // Manipulate with multiple selections on a single line + cx.set_state(indoc! {" + dd«dd + cˇ»c«c + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «aaaaa + bb + ccc + ddddˇ» + "}); - editor.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx); - assert_eq!(buffer.read(cx).text(), "a\nbb\nccc\ndddd\n\n1\n2\n3\n\n"); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 5)..Point::new(2, 2)] - ); - assert_eq!( - editor.selections.ranges::(cx), - &[Point::new(0, 5)..Point::new(2, 2)] - ); + // Manipulate with multiple disjoin selections + cx.set_state(indoc! {" + 5« + 4 + 3 + 2 + 1ˇ» - // assert_eq!( - // editor.selections.ranges::(cx), - // [ - // Point::new(0, 7)..Point::new(0, 7), - // Point::new(1, 3)..Point::new(1, 3) - // ] - // ); - editor - }); + dd«dd + ccc + bb + aaaˇ»aa + "}); + cx.update_editor(|e, cx| e.sort_lines_case_sensitive(&SortLinesCaseSensitive, cx)); + cx.assert_editor_state(indoc! {" + «1 + 2 + 3 + 4 + 5ˇ» + + «aaaaa + bb + ccc + ddddˇ» + "}); } #[gpui::test]