diff --git a/Cargo.lock b/Cargo.lock index 30b1408ed5f4aebcbcdc92501211ce3ff388bcad..e8cf0a12f076aff68c4d12fa8b5e9a1f9df5e70f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -867,7 +867,7 @@ dependencies = [ "gpui", "postage", "theme", - "time 0.3.2", + "time 0.3.7", "util", "workspace", ] @@ -988,7 +988,7 @@ dependencies = [ "sum_tree", "surf", "thiserror", - "time 0.3.2", + "time 0.3.7", "tiny_http", "util", ] @@ -1134,7 +1134,7 @@ dependencies = [ "percent-encoding", "rand 0.8.3", "sha2 0.9.5", - "time 0.2.25", + "time 0.2.27", "version_check", ] @@ -1772,23 +1772,6 @@ dependencies = [ "workspace", ] -[[package]] -name = "find" -version = "0.1.0" -dependencies = [ - "aho-corasick", - "anyhow", - "collections", - "editor", - "gpui", - "postage", - "regex", - "smol", - "theme", - "unindent", - "workspace", -] - [[package]] name = "fixedbitset" version = "0.2.0" @@ -2232,7 +2215,7 @@ dependencies = [ "smallvec", "smol", "sum_tree", - "time 0.3.2", + "time 0.3.7", "tiny-skia", "tree-sitter", "usvg", @@ -3089,6 +3072,15 @@ dependencies = [ "libc", ] +[[package]] +name = "num_threads" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +dependencies = [ + "libc", +] + [[package]] name = "oauth2" version = "4.1.0" @@ -3554,6 +3546,7 @@ dependencies = [ name = "project" version = "0.1.0" dependencies = [ + "aho-corasick", "anyhow", "async-trait", "client", @@ -3572,6 +3565,7 @@ dependencies = [ "parking_lot", "postage", "rand 0.8.3", + "regex", "rpc", "serde", "serde_json", @@ -4169,6 +4163,25 @@ version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" +[[package]] +name = "search" +version = "0.1.0" +dependencies = [ + "anyhow", + "collections", + "editor", + "gpui", + "language", + "log", + "postage", + "project", + "serde_json", + "theme", + "unindent", + "util", + "workspace", +] + [[package]] name = "semver" version = "0.9.0" @@ -4678,7 +4691,7 @@ dependencies = [ "sqlx-rt 0.5.5", "stringprep", "thiserror", - "time 0.2.25", + "time 0.2.27", "url", "uuid", "webpki", @@ -5126,9 +5139,9 @@ dependencies = [ [[package]] name = "time" -version = "0.2.25" +version = "0.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1195b046942c221454c2539395f85413b33383a067449d78aab2b7b052a142f7" +checksum = "4752a97f8eebd6854ff91f1c1824cd6160626ac4bd44287f7f4ea2035a02a242" dependencies = [ "const_fn", "libc", @@ -5141,11 +5154,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0a10c9a9fb3a5dce8c2239ed670f1a2569fcf42da035f5face1b19860d52b0" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" dependencies = [ "libc", + "num_threads", ] [[package]] @@ -5160,9 +5174,9 @@ dependencies = [ [[package]] name = "time-macros-impl" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5c3be1edfad6027c69f5491cf4cb310d1a71ecd6af742788c6ff8bced86b8fa" +checksum = "fd3c141a1b43194f3f56a1411225df8646c55781d5f26db825b3d98507eb482f" dependencies = [ "proc-macro-hack", "proc-macro2", @@ -5848,7 +5862,6 @@ dependencies = [ "editor", "env_logger", "file_finder", - "find", "fsevent", "futures", "fuzzy", @@ -5877,6 +5890,7 @@ dependencies = [ "rpc", "rsa", "rust-embed", + "search", "serde", "serde_json", "serde_path_to_error", @@ -5890,7 +5904,6 @@ dependencies = [ "theme", "theme_selector", "thiserror", - "time 0.3.2", "tiny_http", "toml", "tree-sitter", @@ -5942,7 +5955,7 @@ dependencies = [ "surf", "tide", "tide-compress", - "time 0.2.25", + "time 0.2.27", "toml", "util", "zed", diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 198a2be493e0b038fc47fe797c54c19ac583aaf8..bb269fdbab1f1cf883337a7b46a7152a0ad8a76b 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -1,13 +1,11 @@ pub mod items; use anyhow::Result; -use collections::{BTreeSet, HashMap, HashSet}; +use collections::{BTreeSet, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{BlockDisposition, BlockId, BlockProperties, RenderBlock}, - highlight_diagnostic_message, - items::BufferItemHandle, - Autoscroll, Editor, ExcerptId, MultiBuffer, ToOffset, + highlight_diagnostic_message, Editor, ExcerptId, MultiBuffer, ToOffset, }; use gpui::{ action, elements::*, fonts::TextStyle, keymap::Binding, AnyViewHandle, AppContext, Entity, @@ -28,24 +26,15 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemNavHistory, ItemViewHandle as _, Workspace}; +use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Workspace}; action!(Deploy); -action!(OpenExcerpts); const CONTEXT_LINE_COUNT: u32 = 1; pub fn init(cx: &mut MutableAppContext) { - cx.add_bindings([ - Binding::new("alt-shift-D", Deploy, Some("Workspace")), - Binding::new( - "alt-shift-D", - OpenExcerpts, - Some("ProjectDiagnosticsEditor"), - ), - ]); + cx.add_bindings([Binding::new("alt-shift-D", Deploy, Some("Workspace"))]); cx.add_action(ProjectDiagnosticsEditor::deploy); - cx.add_action(ProjectDiagnosticsEditor::open_excerpts); } type Event = editor::Event; @@ -180,47 +169,6 @@ impl ProjectDiagnosticsEditor { } } - fn open_excerpts(&mut self, _: &OpenExcerpts, cx: &mut ViewContext) { - if let Some(workspace) = self.workspace.upgrade(cx) { - let editor = self.editor.read(cx); - let excerpts = self.excerpts.read(cx); - let mut new_selections_by_buffer = HashMap::default(); - - for selection in editor.local_selections::(cx) { - for (buffer, mut range) in - excerpts.range_to_buffer_ranges(selection.start..selection.end, cx) - { - if selection.reversed { - mem::swap(&mut range.start, &mut range.end); - } - new_selections_by_buffer - .entry(buffer) - .or_insert(Vec::new()) - .push(range) - } - } - - // We defer the pane interaction because we ourselves are a workspace item - // and activating a new item causes the pane to call a method on us reentrantly, - // which panics if we're on the stack. - workspace.defer(cx, |workspace, cx| { - for (buffer, ranges) in new_selections_by_buffer { - let buffer = BufferItemHandle(buffer); - if !workspace.activate_pane_for_item(&buffer, cx) { - workspace.activate_next_pane(cx); - } - let editor = workspace - .open_item(buffer, cx) - .downcast::() - .unwrap(); - editor.update(cx, |editor, cx| { - editor.select_ranges(ranges, Some(Autoscroll::Center), cx) - }); - } - }); - } - } - fn update_excerpts(&mut self, cx: &mut ViewContext) { let paths = mem::take(&mut self.paths_to_update); let project = self.model.read(cx).project.clone(); @@ -536,8 +484,8 @@ impl workspace::Item for ProjectDiagnostics { } impl workspace::ItemView for ProjectDiagnosticsEditor { - fn item_id(&self, _: &AppContext) -> usize { - self.model.id() + fn item(&self, _: &AppContext) -> Box { + Box::new(self.model.clone()) } fn tab_content(&self, style: &theme::Tab, _: &AppContext) -> ElementBox { @@ -598,7 +546,11 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option where Self: Sized, { @@ -608,13 +560,8 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { self.settings.clone(), cx, ); - diagnostics.editor.update(cx, |editor, cx| { - let nav_history = self - .editor - .read(cx) - .nav_history() - .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); - editor.set_nav_history(nav_history); + diagnostics.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); }); Some(diagnostics) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 87b3814589165bec4d3fd4129c0172cbab5dcc8d..1dd4a514257e54f8733836533839771663b00199 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -30,14 +30,14 @@ use gpui::{ }; use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; +pub use language::{char_kind, CharKind}; use language::{ AnchorRangeExt as _, BracketPair, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; use multi_buffer::MultiBufferChunks; pub use multi_buffer::{ - char_kind, Anchor, AnchorRangeExt, CharKind, ExcerptId, MultiBuffer, MultiBufferSnapshot, - ToOffset, ToPoint, + Anchor, AnchorRangeExt, ExcerptId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; use postage::watch; @@ -132,6 +132,7 @@ action!(ShowCompletions); action!(ToggleCodeActions, bool); action!(ConfirmCompletion, Option); action!(ConfirmCodeAction, Option); +action!(OpenExcerpts); pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { path_openers.push(Box::new(items::BufferOpener)); @@ -259,6 +260,7 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>, document_highlights_task: Option>, pending_rename: Option, + searchable: bool, } pub struct EditorSnapshot { @@ -834,7 +838,7 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, settings, None, cx) } - pub fn clone(&self, cx: &mut ViewContext) -> Self { + pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { let mut clone = Self::new( self.mode, self.buffer.clone(), @@ -845,10 +849,8 @@ impl Editor { ); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = self - .nav_history - .as_ref() - .map(|nav_history| ItemNavHistory::new(nav_history.history(), &cx.handle())); + clone.nav_history = Some(nav_history); + clone.searchable = self.searchable; clone } @@ -927,6 +929,7 @@ impl Editor { code_actions_task: Default::default(), document_highlights_task: Default::default(), pending_rename: Default::default(), + searchable: true, }; this.end_selection(cx); this @@ -1058,7 +1061,8 @@ impl Editor { first_cursor_top = highlighted_rows.start as f32; last_cursor_bottom = first_cursor_top + 1.; } else if autoscroll == Autoscroll::Newest { - let newest_selection = self.newest_selection::(&display_map.buffer_snapshot); + let newest_selection = + self.newest_selection_with_snapshot::(&display_map.buffer_snapshot); first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; last_cursor_bottom = first_cursor_top + 1.; } else { @@ -1205,7 +1209,7 @@ impl Editor { ) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let tail = self - .newest_selection::(&display_map.buffer_snapshot) + .newest_selection_with_snapshot::(&display_map.buffer_snapshot) .tail(); self.begin_selection(position, false, click_count, cx); @@ -1325,7 +1329,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let tail = self - .newest_selection::(&display_map.buffer_snapshot) + .newest_selection_with_snapshot::(&display_map.buffer_snapshot) .tail(); self.columnar_selection_tail = Some(display_map.buffer_snapshot.anchor_before(tail)); @@ -1511,8 +1515,7 @@ impl Editor { self.set_selections(selections, None, cx); self.request_autoscroll(Autoscroll::Fit, cx); } else { - let buffer = self.buffer.read(cx).snapshot(cx); - let mut oldest_selection = self.oldest_selection::(&buffer); + let mut oldest_selection = self.oldest_selection::(&cx); if self.selection_count() == 1 { if oldest_selection.is_empty() { cx.propagate_action(); @@ -4083,7 +4086,7 @@ impl Editor { pub fn show_next_diagnostic(&mut self, _: &ShowNextDiagnostic, cx: &mut ViewContext) { let buffer = self.buffer.read(cx).snapshot(cx); - let selection = self.newest_selection::(&buffer); + let selection = self.newest_selection_with_snapshot::(&buffer); let mut active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| { active_diagnostics .primary_range @@ -4155,8 +4158,7 @@ impl Editor { }; let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); + let head = editor.newest_selection::(cx).head(); let (buffer, head) = if let Some(text_anchor) = editor.buffer.read(cx).text_anchor_for_position(head, cx) { text_anchor @@ -4170,6 +4172,7 @@ impl Editor { cx.spawn(|workspace, mut cx| async move { let definitions = definitions.await?; workspace.update(&mut cx, |workspace, cx| { + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); for definition in definitions { let range = definition.range.to_offset(definition.buffer.read(cx)); let target_editor_handle = workspace @@ -4180,15 +4183,11 @@ impl Editor { target_editor_handle.update(cx, |target_editor, cx| { // When selecting a definition in a different buffer, disable the nav history // to avoid creating a history entry at the previous cursor location. - let disabled_history = if editor_handle == target_editor_handle { - None - } else { - target_editor.nav_history.take() - }; - target_editor.select_ranges([range], Some(Autoscroll::Center), cx); - if disabled_history.is_some() { - target_editor.nav_history = disabled_history; + if editor_handle != target_editor_handle { + nav_history.borrow_mut().disable(); } + target_editor.select_ranges([range], Some(Autoscroll::Center), cx); + nav_history.borrow_mut().enable(); }); } }); @@ -4207,8 +4206,7 @@ impl Editor { let editor_handle = active_item.act_as::(cx)?; let editor = editor_handle.read(cx); - let buffer = editor.buffer.read(cx); - let head = editor.newest_selection::(&buffer.read(cx)).head(); + let head = editor.newest_selection::(cx).head(); let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; let replica_id = editor.replica_id(cx); @@ -4432,12 +4430,11 @@ impl Editor { self.clear_highlighted_ranges::(cx); let editor = rename.editor.read(cx); - let buffer = editor.buffer.read(cx).snapshot(cx); - let selection = editor.newest_selection::(&buffer); + let snapshot = self.buffer.read(cx).snapshot(cx); + let selection = editor.newest_selection_with_snapshot::(&snapshot); // Update the selection to match the position of the selection inside // the rename editor. - let snapshot = self.buffer.read(cx).snapshot(cx); let rename_range = rename.range.to_offset(&snapshot); let start = snapshot .clip_offset(rename_range.start + selection.start, Bias::Left) @@ -4748,17 +4745,28 @@ impl Editor { pub fn oldest_selection>( &self, - snapshot: &MultiBufferSnapshot, + cx: &AppContext, ) -> Selection { + let snapshot = self.buffer.read(cx).read(cx); self.selections .iter() .min_by_key(|s| s.id) - .map(|selection| self.resolve_selection(selection, snapshot)) - .or_else(|| self.pending_selection(snapshot)) + .map(|selection| self.resolve_selection(selection, &snapshot)) + .or_else(|| self.pending_selection(&snapshot)) .unwrap() } pub fn newest_selection>( + &self, + cx: &AppContext, + ) -> Selection { + self.resolve_selection( + self.newest_anchor_selection(), + &self.buffer.read(cx).read(cx), + ) + } + + pub fn newest_selection_with_snapshot>( &self, snapshot: &MultiBufferSnapshot, ) -> Selection { @@ -5145,6 +5153,14 @@ impl Editor { self.buffer.read(cx).read(cx).text() } + pub fn set_text(&mut self, text: impl Into, cx: &mut ViewContext) { + self.buffer + .read(cx) + .as_singleton() + .expect("you can only call set_text on editors for singleton buffers") + .update(cx, |buffer, cx| buffer.set_text(text, cx)); + } + pub fn display_text(&self, cx: &mut MutableAppContext) -> String { self.display_map .update(cx, |map, cx| map.snapshot(cx)) @@ -5344,6 +5360,78 @@ impl Editor { fn on_display_map_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { cx.notify(); } + + pub fn set_searchable(&mut self, searchable: bool) { + self.searchable = searchable; + } + + pub fn searchable(&self) -> bool { + self.searchable + } + + fn open_excerpts(workspace: &mut Workspace, _: &OpenExcerpts, cx: &mut ViewContext) { + let active_item = workspace.active_item(cx); + let editor_handle = if let Some(editor) = active_item + .as_ref() + .and_then(|item| item.act_as::(cx)) + { + editor + } else { + cx.propagate_action(); + return; + }; + + let editor = editor_handle.read(cx); + let buffer = editor.buffer.read(cx); + if buffer.is_singleton() { + cx.propagate_action(); + return; + } + + let mut new_selections_by_buffer = HashMap::default(); + for selection in editor.local_selections::(cx) { + for (buffer, mut range) in + buffer.range_to_buffer_ranges(selection.start..selection.end, cx) + { + if selection.reversed { + mem::swap(&mut range.start, &mut range.end); + } + new_selections_by_buffer + .entry(buffer) + .or_insert(Vec::new()) + .push(range) + } + } + + editor_handle.update(cx, |editor, cx| { + editor.push_to_nav_history(editor.newest_anchor_selection().head(), None, cx); + }); + let nav_history = workspace.active_pane().read(cx).nav_history().clone(); + nav_history.borrow_mut().disable(); + + // We defer the pane interaction because we ourselves are a workspace item + // and activating a new item causes the pane to call a method on us reentrantly, + // which panics if we're on the stack. + cx.defer(move |workspace, cx| { + for (ix, (buffer, ranges)) in new_selections_by_buffer.into_iter().enumerate() { + let buffer = BufferItemHandle(buffer); + if ix == 0 && !workspace.activate_pane_for_item(&buffer, cx) { + workspace.activate_next_pane(cx); + } + + let editor = workspace + .open_item(buffer, cx) + .downcast::() + .unwrap(); + + editor.update(cx, |editor, cx| { + editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); + }); + } + + nav_history.borrow_mut().enable(); + }); + } } impl EditorSnapshot { @@ -5463,9 +5551,14 @@ fn build_style( get_field_editor_theme: Option, cx: &AppContext, ) -> EditorStyle { - let theme = settings.theme.editor.clone(); + let mut theme = settings.theme.editor.clone(); if let Some(get_field_editor_theme) = get_field_editor_theme { let field_editor_theme = get_field_editor_theme(&settings.theme); + if let Some(background) = field_editor_theme.container.background_color { + theme.background = background; + } + theme.text_color = field_editor_theme.text.color; + theme.selection = field_editor_theme.selection; 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 f4277713b165decd679856e9315488762b72dd7c..dcf716e0bb91b6fc55aba4bf4cc7db55ac81a031 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -951,7 +951,7 @@ impl Element for EditorElement { } let newest_selection_head = view - .newest_selection::(&snapshot.buffer_snapshot) + .newest_selection_with_snapshot::(&snapshot.buffer_snapshot) .head() .to_display_point(&snapshot); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1d89bd567fae24a05dd2b13ec66934cf444f3262..4e25a5a24b0dc3503056c60734319216d7d50970 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -158,11 +158,11 @@ impl WeakItemHandle for WeakMultiBufferItemHandle { } impl ItemView for Editor { - fn item_id(&self, cx: &AppContext) -> usize { + fn item(&self, cx: &AppContext) -> Box { if let Some(buffer) = self.buffer.read(cx).as_singleton() { - buffer.id() + Box::new(BufferItemHandle(buffer)) } else { - self.buffer.id() + Box::new(MultiBufferItemHandle(self.buffer.clone())) } } @@ -194,11 +194,15 @@ impl ItemView for Editor { }) } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option where Self: Sized, { - Some(self.clone(cx)) + Some(self.clone(nav_history, cx)) } fn deactivated(&mut self, cx: &mut ViewContext) { @@ -378,7 +382,9 @@ impl DiagnosticMessage { fn update(&mut self, editor: ViewHandle, cx: &mut ViewContext) { let editor = editor.read(cx); let buffer = editor.buffer().read(cx); - let cursor_position = editor.newest_selection::(&buffer.read(cx)).head(); + let cursor_position = editor + .newest_selection_with_snapshot::(&buffer.read(cx)) + .head(); let new_diagnostic = buffer .read(cx) .diagnostics_in_range::<_, usize>(cursor_position..cursor_position) diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4fc4488af9124e040ed8017e07d5fc6ca862f8df..e05ddc56933bd83d67735a9e877252043f852f23 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -7,8 +7,9 @@ use collections::{Bound, HashMap, HashSet}; use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; pub use language::Completion; use language::{ - Buffer, BufferChunks, BufferSnapshot, Chunk, DiagnosticEntry, Event, File, Language, Outline, - OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, TransactionId, + char_kind, Buffer, BufferChunks, BufferSnapshot, CharKind, Chunk, DiagnosticEntry, Event, File, + Language, Outline, OutlineItem, Selection, ToOffset as _, ToPoint as _, ToPointUtf16 as _, + TransactionId, }; use std::{ cell::{Ref, RefCell}, @@ -42,6 +43,7 @@ pub struct MultiBuffer { title: Option, } +#[derive(Clone)] struct History { next_transaction_id: TransactionId, undo_stack: Vec, @@ -50,14 +52,6 @@ struct History { group_interval: Duration, } -#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] -pub enum CharKind { - Newline, - Punctuation, - Whitespace, - Word, -} - #[derive(Clone)] struct Transaction { id: TransactionId, @@ -102,6 +96,7 @@ pub struct MultiBufferSnapshot { } pub struct ExcerptBoundary { + pub id: ExcerptId, pub row: u32, pub buffer: BufferSnapshot, pub range: Range, @@ -174,6 +169,37 @@ impl MultiBuffer { } } + pub fn clone(&self, new_cx: &mut ModelContext) -> Self { + let mut buffers = HashMap::default(); + for (buffer_id, buffer_state) in self.buffers.borrow().iter() { + buffers.insert( + *buffer_id, + BufferState { + buffer: buffer_state.buffer.clone(), + last_version: buffer_state.last_version.clone(), + last_parse_count: buffer_state.last_parse_count, + last_selections_update_count: buffer_state.last_selections_update_count, + last_diagnostics_update_count: buffer_state.last_diagnostics_update_count, + last_file_update_count: buffer_state.last_file_update_count, + excerpts: buffer_state.excerpts.clone(), + _subscriptions: [ + new_cx.observe(&buffer_state.buffer, |_, _, cx| cx.notify()), + new_cx.subscribe(&buffer_state.buffer, Self::on_buffer_event), + ], + }, + ); + } + Self { + snapshot: RefCell::new(self.snapshot.borrow().clone()), + buffers: RefCell::new(buffers), + subscriptions: Default::default(), + singleton: self.singleton, + replica_id: self.replica_id, + history: self.history.clone(), + title: self.title.clone(), + } + } + pub fn with_title(mut self, title: String) -> Self { self.title = Some(title); self @@ -693,6 +719,11 @@ impl MultiBuffer { O: text::ToOffset, { assert_eq!(self.history.transaction_depth, 0); + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_none() { + return Default::default(); + } + self.sync(cx); let buffer_id = buffer.id(); @@ -733,7 +764,6 @@ impl MultiBuffer { } let mut ids = Vec::new(); - let mut ranges = ranges.into_iter().peekable(); while let Some(range) = ranges.next() { let id = ExcerptId::between(&prev_id, &next_id); if let Err(ix) = buffer_state.excerpts.binary_search(&id) { @@ -773,6 +803,22 @@ impl MultiBuffer { ids } + pub fn clear(&mut self, cx: &mut ModelContext) { + self.sync(cx); + self.buffers.borrow_mut().clear(); + let mut snapshot = self.snapshot.borrow_mut(); + let prev_len = snapshot.len(); + snapshot.excerpts = Default::default(); + snapshot.trailing_excerpt_update_count += 1; + snapshot.is_dirty = false; + snapshot.has_conflict = false; + self.subscriptions.publish_mut([Edit { + old: 0..prev_len, + new: 0..0, + }]); + cx.notify(); + } + pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle) -> Vec { self.buffers .borrow() @@ -840,6 +886,7 @@ impl MultiBuffer { excerpt_ids: impl IntoIterator, cx: &mut ModelContext, ) { + self.sync(cx); let mut buffers = self.buffers.borrow_mut(); let mut snapshot = self.snapshot.borrow_mut(); let mut new_excerpts = SumTree::new(); @@ -1166,6 +1213,12 @@ impl MultiBuffer { let mut buffers = Vec::new(); for _ in 0..mutation_count { + if rng.gen_bool(0.05) { + log::info!("Clearing multi-buffer"); + self.clear(cx); + continue; + } + let excerpt_ids = self .buffers .borrow() @@ -1195,16 +1248,26 @@ impl MultiBuffer { }; let buffer = buffer_handle.read(cx); - let end_ix = buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); - let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + let buffer_text = buffer.text(); + let ranges = (0..rng.gen_range(0..5)) + .map(|_| { + let end_ix = + buffer.clip_offset(rng.gen_range(0..=buffer.len()), Bias::Right); + let start_ix = buffer.clip_offset(rng.gen_range(0..=end_ix), Bias::Left); + start_ix..end_ix + }) + .collect::>(); log::info!( - "Inserting excerpt from buffer {} and range {:?}: {:?}", + "Inserting excerpts from buffer {} and ranges {:?}: {:?}", buffer_handle.id(), - start_ix..end_ix, - &buffer.text()[start_ix..end_ix] + ranges, + ranges + .iter() + .map(|range| &buffer_text[range.clone()]) + .collect::>() ); - let excerpt_id = self.push_excerpts(buffer_handle.clone(), [start_ix..end_ix], cx); + let excerpt_id = self.push_excerpts(buffer_handle.clone(), ranges, cx); log::info!("Inserted with id: {:?}", excerpt_id); } else { let remove_count = rng.gen_range(1..=excerpt_ids.len()); @@ -1342,9 +1405,12 @@ impl MultiBufferSnapshot { (start..end, word_kind) } - fn as_singleton(&self) -> Option<&Excerpt> { + pub fn as_singleton(&self) -> Option<(&ExcerptId, usize, &BufferSnapshot)> { if self.singleton { - self.excerpts.iter().next() + self.excerpts + .iter() + .next() + .map(|e| (&e.id, e.buffer_id, &e.buffer)) } else { None } @@ -1359,8 +1425,8 @@ impl MultiBufferSnapshot { } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_offset(offset, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_offset(offset, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1378,8 +1444,8 @@ impl MultiBufferSnapshot { } pub fn clip_point(&self, point: Point, bias: Bias) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1397,8 +1463,8 @@ impl MultiBufferSnapshot { } pub fn clip_point_utf16(&self, point: PointUtf16, bias: Bias) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.clip_point_utf16(point, bias); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.clip_point_utf16(point, bias); } let mut cursor = self.excerpts.cursor::(); @@ -1466,8 +1532,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point(&self, offset: usize) -> Point { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point(offset); } let mut cursor = self.excerpts.cursor::<(usize, Point)>(); @@ -1487,8 +1553,8 @@ impl MultiBufferSnapshot { } pub fn offset_to_point_utf16(&self, offset: usize) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.offset_to_point_utf16(offset); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.offset_to_point_utf16(offset); } let mut cursor = self.excerpts.cursor::<(usize, PointUtf16)>(); @@ -1508,8 +1574,8 @@ impl MultiBufferSnapshot { } pub fn point_to_point_utf16(&self, point: Point) -> PointUtf16 { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_point_utf16(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_point_utf16(point); } let mut cursor = self.excerpts.cursor::<(Point, PointUtf16)>(); @@ -1529,8 +1595,8 @@ impl MultiBufferSnapshot { } pub fn point_to_offset(&self, point: Point) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_to_offset(point); } let mut cursor = self.excerpts.cursor::<(Point, usize)>(); @@ -1550,8 +1616,8 @@ impl MultiBufferSnapshot { } pub fn point_utf16_to_offset(&self, point: PointUtf16) -> usize { - if let Some(excerpt) = self.as_singleton() { - return excerpt.buffer.point_utf16_to_offset(point); + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer.point_utf16_to_offset(point); } let mut cursor = self.excerpts.cursor::<(PointUtf16, usize)>(); @@ -1711,9 +1777,8 @@ impl MultiBufferSnapshot { D: TextDimension + Ord + Sub, I: 'a + IntoIterator, { - if let Some(excerpt) = self.as_singleton() { - return excerpt - .buffer + if let Some((_, _, buffer)) = self.as_singleton() { + return buffer .summaries_for_anchors(anchors.into_iter().map(|a| &a.text_anchor)) .collect(); } @@ -1878,11 +1943,11 @@ impl MultiBufferSnapshot { pub fn anchor_at(&self, position: T, mut bias: Bias) -> Anchor { let offset = position.to_offset(self); - if let Some(excerpt) = self.as_singleton() { + if let Some((excerpt_id, buffer_id, buffer)) = self.as_singleton() { return Anchor { - buffer_id: Some(excerpt.buffer_id), - excerpt_id: excerpt.id.clone(), - text_anchor: excerpt.buffer.anchor_at(offset, bias), + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: buffer.anchor_at(offset, bias), }; } @@ -1989,6 +2054,7 @@ impl MultiBufferSnapshot { let excerpt = cursor.item()?; let starts_new_buffer = Some(excerpt.buffer_id) != prev_buffer_id; let boundary = ExcerptBoundary { + id: excerpt.id.clone(), row: cursor.start().1.row, buffer: excerpt.buffer.clone(), range: excerpt.range.clone(), @@ -2090,7 +2156,7 @@ impl MultiBufferSnapshot { { self.as_singleton() .into_iter() - .flat_map(move |excerpt| excerpt.buffer.diagnostic_group(group_id)) + .flat_map(move |(_, _, buffer)| buffer.diagnostic_group(group_id)) } pub fn diagnostics_in_range<'a, T, O>( @@ -2101,11 +2167,11 @@ impl MultiBufferSnapshot { T: 'a + ToOffset, O: 'a + text::FromAnchor, { - self.as_singleton().into_iter().flat_map(move |excerpt| { - excerpt - .buffer - .diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) - }) + self.as_singleton() + .into_iter() + .flat_map(move |(_, _, buffer)| { + buffer.diagnostics_in_range(range.start.to_offset(self)..range.end.to_offset(self)) + }) } pub fn range_for_syntax_ancestor(&self, range: Range) -> Option> { @@ -2147,16 +2213,16 @@ impl MultiBufferSnapshot { } pub fn outline(&self, theme: Option<&SyntaxTheme>) -> Option> { - let excerpt = self.as_singleton()?; - let outline = excerpt.buffer.outline(theme)?; + let (excerpt_id, _, buffer) = self.as_singleton()?; + let outline = buffer.outline(theme)?; Some(Outline::new( outline .items .into_iter() .map(|item| OutlineItem { depth: item.depth, - range: self.anchor_in_excerpt(excerpt.id.clone(), item.range.start) - ..self.anchor_in_excerpt(excerpt.id.clone(), item.range.end), + range: self.anchor_in_excerpt(excerpt_id.clone(), item.range.start) + ..self.anchor_in_excerpt(excerpt_id.clone(), item.range.end), text: item.text, highlight_ranges: item.highlight_ranges, name_ranges: item.name_ranges, @@ -2764,18 +2830,6 @@ impl ToPointUtf16 for PointUtf16 { } } -pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { - CharKind::Whitespace - } else if c.is_alphanumeric() || c == '_' { - CharKind::Word - } else { - CharKind::Punctuation - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 0b35ef7dbdbf3889b37b2995902fe18c067dd6b1..c3e9cdcbf257cc12aba8302d3865587bbdd167e6 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -54,7 +54,7 @@ impl GoToLine { let buffer = editor.buffer().read(cx).read(cx); ( Some(scroll_position), - editor.newest_selection(&buffer).head(), + editor.newest_selection_with_snapshot(&buffer).head(), buffer.max_point(), ) }); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 90c25efe5bb838e63194c155365c6d1747b81ac7..dcfb00617d600a2db2feeb94c4aca99d1428b49f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -85,6 +85,8 @@ pub trait UpgradeModelHandle { handle: &WeakModelHandle, ) -> Option>; + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool; + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option; } @@ -608,6 +610,10 @@ impl UpgradeModelHandle for AsyncAppContext { self.0.borrow().upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.0.borrow().model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.0.borrow().upgrade_any_model_handle(handle) } @@ -710,7 +716,7 @@ impl ReadViewWith for TestAppContext { } type ActionCallback = - dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize) -> bool; + dyn FnMut(&mut dyn AnyView, &dyn AnyAction, &mut MutableAppContext, usize, usize); type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; @@ -722,6 +728,7 @@ pub struct MutableAppContext { foreground_platform: Rc, assets: Arc, cx: AppContext, + capture_actions: HashMap>>>, actions: HashMap>>>, global_actions: HashMap>, keystroke_matcher: keymap::Matcher, @@ -741,6 +748,7 @@ pub struct MutableAppContext { pending_flushes: usize, flushing_effects: bool, next_cursor_style_handle_id: Arc, + halt_action_dispatch: bool, } impl MutableAppContext { @@ -761,12 +769,14 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), + app_states: Default::default(), element_states: Default::default(), ref_counts: Arc::new(Mutex::new(RefCounts::default())), background, font_cache, platform, }, + capture_actions: HashMap::new(), actions: HashMap::new(), global_actions: HashMap::new(), keystroke_matcher: keymap::Matcher::default(), @@ -785,6 +795,7 @@ impl MutableAppContext { pending_flushes: 0, flushing_effects: false, next_cursor_style_handle_id: Default::default(), + halt_action_dispatch: false, } } @@ -855,7 +866,25 @@ impl MutableAppContext { .map(|debug_elements| debug_elements(&self.cx)) } - pub fn add_action(&mut self, mut handler: F) + pub fn add_action(&mut self, handler: F) + where + A: Action, + V: View, + F: 'static + FnMut(&mut V, &A, &mut ViewContext), + { + self.add_action_internal(handler, false) + } + + pub fn capture_action(&mut self, handler: F) + where + A: Action, + V: View, + F: 'static + FnMut(&mut V, &A, &mut ViewContext), + { + self.add_action_internal(handler, true) + } + + fn add_action_internal(&mut self, mut handler: F, capture: bool) where A: Action, V: View, @@ -876,11 +905,16 @@ impl MutableAppContext { action, &mut cx, ); - cx.halt_action_dispatch }, ); - self.actions + let actions = if capture { + &mut self.capture_actions + } else { + &mut self.actions + }; + + actions .entry(TypeId::of::()) .or_default() .entry(TypeId::of::()) @@ -1167,45 +1201,59 @@ impl MutableAppContext { action: &dyn AnyAction, ) -> bool { self.update(|this| { - let mut halted_dispatch = false; - for view_id in path.iter().rev() { - if let Some(mut view) = this.cx.views.remove(&(window_id, *view_id)) { + this.halt_action_dispatch = false; + for (capture_phase, view_id) in path + .iter() + .map(|view_id| (true, *view_id)) + .chain(path.iter().rev().map(|view_id| (false, *view_id))) + { + if let Some(mut view) = this.cx.views.remove(&(window_id, view_id)) { let type_id = view.as_any().type_id(); if let Some((name, mut handlers)) = this - .actions + .actions_mut(capture_phase) .get_mut(&type_id) .and_then(|h| h.remove_entry(&action.id())) { for handler in handlers.iter_mut().rev() { - let halt_dispatch = - handler(view.as_mut(), action, this, window_id, *view_id); - if halt_dispatch { - halted_dispatch = true; + this.halt_action_dispatch = true; + handler(view.as_mut(), action, this, window_id, view_id); + if this.halt_action_dispatch { break; } } - this.actions + this.actions_mut(capture_phase) .get_mut(&type_id) .unwrap() .insert(name, handlers); } - this.cx.views.insert((window_id, *view_id), view); + this.cx.views.insert((window_id, view_id), view); - if halted_dispatch { + if this.halt_action_dispatch { break; } } } - if !halted_dispatch { - halted_dispatch = this.dispatch_global_action_any(action); + if !this.halt_action_dispatch { + this.dispatch_global_action_any(action); } - halted_dispatch + this.halt_action_dispatch }) } + fn actions_mut( + &mut self, + capture_phase: bool, + ) -> &mut HashMap>>> { + if capture_phase { + &mut self.capture_actions + } else { + &mut self.actions + } + } + pub fn dispatch_global_action(&mut self, action: A) { self.dispatch_global_action_any(&action); } @@ -1265,6 +1313,27 @@ impl MutableAppContext { Ok(pending) } + pub fn add_app_state(&mut self, state: T) { + self.cx + .app_states + .insert(TypeId::of::(), Box::new(state)); + } + + pub fn update_app_state(&mut self, update: F) -> U + where + F: FnOnce(&mut T, &mut MutableAppContext) -> U, + { + let type_id = TypeId::of::(); + let mut state = self + .cx + .app_states + .remove(&type_id) + .expect("no app state has been added for this type"); + let result = update(state.downcast_mut().unwrap(), self); + self.cx.app_states.insert(type_id, state); + result + } + pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, @@ -1787,6 +1856,10 @@ impl UpgradeModelHandle for MutableAppContext { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -1857,6 +1930,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, + app_states: HashMap>, element_states: HashMap>, background: Arc, ref_counts: Arc>, @@ -1888,6 +1962,14 @@ impl AppContext { pub fn platform(&self) -> &Arc { &self.platform } + + pub fn app_state(&self) -> &T { + self.app_states + .get(&TypeId::of::()) + .expect("no app state has been added for this type") + .downcast_ref() + .unwrap() + } } impl ReadModel for AppContext { @@ -1915,6 +1997,10 @@ impl UpgradeModelHandle for AppContext { } } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.models.contains_key(&handle.model_id) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { if self.models.contains_key(&handle.model_id) { self.ref_counts.lock().inc_model(handle.model_id); @@ -2320,6 +2406,10 @@ impl UpgradeModelHandle for ModelContext<'_, M> { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -2344,7 +2434,6 @@ pub struct ViewContext<'a, T: ?Sized> { window_id: usize, view_id: usize, view_type: PhantomData, - halt_action_dispatch: bool, } impl<'a, T: View> ViewContext<'a, T> { @@ -2354,7 +2443,6 @@ impl<'a, T: View> ViewContext<'a, T> { window_id, view_id, view_type: PhantomData, - halt_action_dispatch: true, } } @@ -2529,7 +2617,7 @@ impl<'a, T: View> ViewContext<'a, T> { } pub fn propagate_action(&mut self) { - self.halt_action_dispatch = false; + self.app.halt_action_dispatch = false; } pub fn spawn(&self, f: F) -> Task @@ -2660,6 +2748,10 @@ impl UpgradeModelHandle for ViewContext<'_, V> { self.cx.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.cx.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.cx.upgrade_any_model_handle(handle) } @@ -2902,6 +2994,12 @@ impl PartialEq for ModelHandle { impl Eq for ModelHandle {} +impl PartialEq> for ModelHandle { + fn eq(&self, other: &WeakModelHandle) -> bool { + self.model_id == other.model_id + } +} + impl Hash for ModelHandle { fn hash(&self, state: &mut H) { self.model_id.hash(state); @@ -2974,6 +3072,10 @@ impl WeakModelHandle { self.model_id } + pub fn is_upgradable(&self, cx: &impl UpgradeModelHandle) -> bool { + cx.model_handle_is_upgradable(self) + } + pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option> { cx.upgrade_model_handle(self) } @@ -4322,37 +4424,58 @@ mod tests { let actions = Rc::new(RefCell::new(Vec::new())); - let actions_clone = actions.clone(); - cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { - actions_clone.borrow_mut().push("global".to_string()); - }); + { + let actions = actions.clone(); + cx.add_global_action(move |_: &Action, _: &mut MutableAppContext| { + actions.borrow_mut().push("global".to_string()); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewA, action: &Action, cx| { - assert_eq!(action.0, "bar"); - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} a", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewA, action: &Action, cx| { + assert_eq!(action.0, "bar"); + cx.propagate_action(); + actions.borrow_mut().push(format!("{} a", view.id)); + }); + } + + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewA, _: &Action, cx| { + if view.id != 1 { + cx.add_view(|cx| { + cx.propagate_action(); // Still works on a nested ViewContext + ViewB { id: 5 } + }); + } + actions.borrow_mut().push(format!("{} b", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewA, _: &Action, cx| { - if view.id != 1 { + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { cx.propagate_action(); - } - actions_clone.borrow_mut().push(format!("{} b", view.id)); - }); + actions.borrow_mut().push(format!("{} c", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} c", view.id)); - }); + { + let actions = actions.clone(); + cx.add_action(move |view: &mut ViewB, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} d", view.id)); + }); + } - let actions_clone = actions.clone(); - cx.add_action(move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions_clone.borrow_mut().push(format!("{} d", view.id)); - }); + { + let actions = actions.clone(); + cx.capture_action(move |view: &mut ViewA, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} capture", view.id)); + }); + } let (window_id, view_1) = cx.add_window(Default::default(), |_| ViewA { id: 1 }); let view_2 = cx.add_view(window_id, |_| ViewB { id: 2 }); @@ -4367,7 +4490,17 @@ mod tests { assert_eq!( *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "1 b"] + vec![ + "1 capture", + "3 capture", + "4 d", + "4 c", + "3 b", + "3 a", + "2 d", + "2 c", + "1 b" + ] ); // Remove view_1, which doesn't propagate the action @@ -4380,7 +4513,16 @@ mod tests { assert_eq!( *actions.borrow(), - vec!["4 d", "4 c", "3 b", "3 a", "2 d", "2 c", "global"] + vec![ + "3 capture", + "4 d", + "4 c", + "3 b", + "3 a", + "2 d", + "2 c", + "global" + ] ); } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 848cb8fe393344710612fe5f382507b196162e06..05fbd5b74b4d4e4c2a0b0595a75d4859b7fa0106 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -76,6 +76,19 @@ pub enum MatchResult { Action(Box), } +impl Debug for MatchResult { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + MatchResult::None => f.debug_struct("MatchResult::None").finish(), + MatchResult::Pending => f.debug_struct("MatchResult::Pending").finish(), + MatchResult::Action(action) => f + .debug_tuple("MatchResult::Action") + .field(&action.name()) + .finish(), + } + } +} + impl Matcher { pub fn new(keymap: Keymap) -> Self { Self { diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index ba59e14a30b38fe4b551b96f3b119020752b30ec..8a41a76e714bc352593c20a87e18edb5758b8e0b 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -281,6 +281,10 @@ impl<'a> UpgradeModelHandle for LayoutContext<'a> { self.app.upgrade_model_handle(handle) } + fn model_handle_is_upgradable(&self, handle: &WeakModelHandle) -> bool { + self.app.model_handle_is_upgradable(handle) + } + fn upgrade_any_model_handle(&self, handle: &AnyWeakModelHandle) -> Option { self.app.upgrade_any_model_handle(handle) } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 336ad737c36da5298e635cd15fd5a045f5e7a129..ddc6fa7c93d6d73d915c1a36a56c925a265a04f0 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -365,6 +365,14 @@ pub(crate) struct DiagnosticEndpoint { severity: DiagnosticSeverity, } +#[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] +pub enum CharKind { + Newline, + Punctuation, + Whitespace, + Word, +} + impl Buffer { pub fn new>>( replica_id: ReplicaId, @@ -1337,6 +1345,13 @@ impl Buffer { let _ = language_server.latest_snapshot.blocking_send(snapshot); } + pub fn set_text(&mut self, text: T, cx: &mut ModelContext) -> Option + where + T: Into, + { + self.edit_internal([0..self.len()], text, false, cx) + } + pub fn edit( &mut self, ranges_iter: I, @@ -2659,3 +2674,15 @@ pub fn contiguous_ranges( } }) } + +pub fn char_kind(c: char) -> CharKind { + if c == '\n' { + CharKind::Newline + } else if c.is_whitespace() { + CharKind::Whitespace + } else if c.is_alphanumeric() || c == '_' { + CharKind::Word + } else { + CharKind::Punctuation + } +} diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index c563c233118fb106299c7d57606abf78e8d3e8ac..2e2efdb28c0da5462184f11851ebeb71537c91c1 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -33,7 +33,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, - outbound_tx: RwLock>>>, + outbound_tx: channel::Sender>, capabilities: watch::Receiver>, notification_handlers: Arc>>, response_handlers: Arc>>, @@ -213,7 +213,7 @@ impl LanguageServer { response_handlers, capabilities: capabilities_rx, next_id: Default::default(), - outbound_tx: RwLock::new(Some(outbound_tx)), + outbound_tx, executor: executor.clone(), io_tasks: Mutex::new(Some((input_task, output_task))), initialized: initialized_rx, @@ -296,37 +296,41 @@ impl LanguageServer { let request = Self::request_internal::( &this.next_id, &this.response_handlers, - this.outbound_tx.read().as_ref(), + &this.outbound_tx, params, ); let response = request.await?; Self::notify_internal::( - this.outbound_tx.read().as_ref(), + &this.outbound_tx, InitializedParams {}, )?; Ok(response.capabilities) } - pub fn shutdown(&self) -> Option>> { + pub fn shutdown(&self) -> Option>> { if let Some(tasks) = self.io_tasks.lock().take() { let response_handlers = self.response_handlers.clone(); - let outbound_tx = self.outbound_tx.write().take(); let next_id = AtomicUsize::new(self.next_id.load(SeqCst)); + let outbound_tx = self.outbound_tx.clone(); let mut output_done = self.output_done_rx.lock().take().unwrap(); - Some(async move { - Self::request_internal::( - &next_id, - &response_handlers, - outbound_tx.as_ref(), - (), - ) - .await?; - Self::notify_internal::(outbound_tx.as_ref(), ())?; - drop(outbound_tx); - output_done.recv().await; - drop(tasks); - Ok(()) - }) + let shutdown_request = Self::request_internal::( + &next_id, + &response_handlers, + &outbound_tx, + (), + ); + let exit = Self::notify_internal::(&outbound_tx, ()); + outbound_tx.close(); + Some( + async move { + shutdown_request.await?; + exit?; + output_done.recv().await; + drop(tasks); + Ok(()) + } + .log_err(), + ) } else { None } @@ -375,7 +379,7 @@ impl LanguageServer { Self::request_internal::( &this.next_id, &this.response_handlers, - this.outbound_tx.read().as_ref(), + &this.outbound_tx, params, ) .await @@ -385,7 +389,7 @@ impl LanguageServer { fn request_internal( next_id: &AtomicUsize, response_handlers: &Mutex>, - outbound_tx: Option<&channel::Sender>>, + outbound_tx: &channel::Sender>, params: T::Params, ) -> impl 'static + Future> where @@ -415,16 +419,8 @@ impl LanguageServer { ); let send = outbound_tx - .as_ref() - .ok_or_else(|| { - anyhow!("tried to send a request to a language server that has been shut down") - }) - .and_then(|outbound_tx| { - outbound_tx - .try_send(message) - .context("failed to write to language server's stdin")?; - Ok(()) - }); + .try_send(message) + .context("failed to write to language server's stdin"); async move { send?; rx.recv().await.unwrap() @@ -438,13 +434,13 @@ impl LanguageServer { let this = self.clone(); async move { this.initialized.clone().recv().await; - Self::notify_internal::(this.outbound_tx.read().as_ref(), params)?; + Self::notify_internal::(&this.outbound_tx, params)?; Ok(()) } } fn notify_internal( - outbound_tx: Option<&channel::Sender>>, + outbound_tx: &channel::Sender>, params: T::Params, ) -> Result<()> { let message = serde_json::to_vec(&Notification { @@ -453,9 +449,6 @@ impl LanguageServer { params, }) .unwrap(); - let outbound_tx = outbound_tx - .as_ref() - .ok_or_else(|| anyhow!("tried to notify a language server that has been shut down"))?; outbound_tx.try_send(message)?; Ok(()) } diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 4d2dc125c633b87e417c9ffb003ab972ec651d71..3607f1af87b4b95fb0222ecaad0c35d4a49c2010 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -259,7 +259,9 @@ impl OutlineView { let editor = self.active_editor.read(cx); let buffer = editor.buffer().read(cx).read(cx); - let cursor_offset = editor.newest_selection::(&buffer).head(); + let cursor_offset = editor + .newest_selection_with_snapshot::(&buffer) + .head(); selected_index = self .outline .items diff --git a/crates/project/Cargo.toml b/crates/project/Cargo.toml index f72ba133c32df4135cbd4e0f68c6eac9e300252d..dea5a10279a8f6e3dfe1c62d9526bbea0e797fac 100644 --- a/crates/project/Cargo.toml +++ b/crates/project/Cargo.toml @@ -26,6 +26,7 @@ lsp = { path = "../lsp" } rpc = { path = "../rpc" } sum_tree = { path = "../sum_tree" } util = { path = "../util" } +aho-corasick = "0.7" anyhow = "1.0.38" async-trait = "0.1" futures = "0.3" @@ -36,6 +37,7 @@ log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" +regex = "1.5" serde = { version = "1", features = ["derive"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } sha2 = "0.10" diff --git a/crates/project/src/fs.rs b/crates/project/src/fs.rs index 7f89c29c8384e33d3817e180c42c288cdbda22a2..578be8cf82cf1a6e11648c6a62ba1c4f083bacf2 100644 --- a/crates/project/src/fs.rs +++ b/crates/project/src/fs.rs @@ -18,6 +18,7 @@ pub trait Fs: Send + Sync { async fn rename(&self, source: &Path, target: &Path, options: RenameOptions) -> Result<()>; async fn remove_dir(&self, path: &Path, options: RemoveOptions) -> Result<()>; async fn remove_file(&self, path: &Path, options: RemoveOptions) -> Result<()>; + async fn open_sync(&self, path: &Path) -> Result>; async fn load(&self, path: &Path) -> Result; async fn save(&self, path: &Path, text: &Rope) -> Result<()>; async fn canonicalize(&self, path: &Path) -> Result; @@ -121,6 +122,10 @@ impl Fs for RealFs { } } + async fn open_sync(&self, path: &Path) -> Result> { + Ok(Box::new(std::fs::File::open(path)?)) + } + async fn load(&self, path: &Path) -> Result { let mut file = smol::fs::File::open(path).await?; let mut text = String::new(); @@ -203,7 +208,6 @@ impl Fs for RealFs { fn is_fake(&self) -> bool { false } - #[cfg(any(test, feature = "test-support"))] fn as_fake(&self) -> &FakeFs { panic!("called `RealFs::as_fake`") @@ -535,6 +539,11 @@ impl Fs for FakeFs { Ok(()) } + async fn open_sync(&self, path: &Path) -> Result> { + let text = self.load(path).await?; + Ok(Box::new(io::Cursor::new(text))) + } + async fn load(&self, path: &Path) -> Result { let path = normalize_path(path); self.executor.simulate_random_delay().await; diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index b091fe0bc390bf95f36d51596e81696790c01fa0..3b502fc8fafc5accfc977eee572c853a68701b48 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,4 +1,4 @@ -use crate::{BufferRequestHandle, DocumentHighlight, Location, Project, ProjectTransaction}; +use crate::{DocumentHighlight, Location, Project, ProjectTransaction}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use client::{proto, PeerId}; @@ -48,7 +48,6 @@ pub(crate) trait LspCommand: 'static + Sized { message: ::Response, project: ModelHandle, buffer: ModelHandle, - request_handle: BufferRequestHandle, cx: AsyncAppContext, ) -> Result; fn buffer_id_from_proto(message: &Self::ProtoRequest) -> u64; @@ -162,7 +161,6 @@ impl LspCommand for PrepareRename { message: proto::PrepareRenameResponse, _: ModelHandle, buffer: ModelHandle, - _: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result>> { if message.can_rename { @@ -279,7 +277,6 @@ impl LspCommand for PerformRename { message: proto::PerformRenameResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result { let message = message @@ -287,12 +284,7 @@ impl LspCommand for PerformRename { .ok_or_else(|| anyhow!("missing transaction"))?; project .update(&mut cx, |project, cx| { - project.deserialize_project_transaction( - message, - self.push_to_history, - request_handle, - cx, - ) + project.deserialize_project_transaction(message, self.push_to_history, cx) }) .await } @@ -435,16 +427,13 @@ impl LspCommand for GetDefinition { message: proto::GetDefinitionResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result> { let mut locations = Vec::new(); for location in message.locations { let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let buffer = project - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let start = location .start @@ -586,16 +575,13 @@ impl LspCommand for GetReferences { message: proto::GetReferencesResponse, project: ModelHandle, _: ModelHandle, - request_handle: BufferRequestHandle, mut cx: AsyncAppContext, ) -> Result> { let mut locations = Vec::new(); for location in message.locations { let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; let target_buffer = project - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let start = location .start @@ -720,7 +706,6 @@ impl LspCommand for GetDocumentHighlights { message: proto::GetDocumentHighlightsResponse, _: ModelHandle, _: ModelHandle, - _: BufferRequestHandle, _: AsyncAppContext, ) -> Result> { Ok(message diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 236b02cd6c97ef0e940fdf88e4963b32806e8174..507870341a1c92a727d8dc18f3e0f19b3d0c8060 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1,31 +1,35 @@ pub mod fs; mod ignore; mod lsp_command; +pub mod search; pub mod worktree; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; use collections::{hash_map, HashMap, HashSet}; -use futures::{future::Shared, Future, FutureExt}; +use futures::{future::Shared, Future, FutureExt, StreamExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ + proto::{deserialize_anchor, serialize_anchor}, range_from_lsp, Anchor, AnchorRangeExt, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, File as _, Language, LanguageRegistry, Operation, PointUtf16, ToLspPosition, ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DocumentHighlightKind, LanguageServer}; use lsp_command::*; -use postage::{broadcast, prelude::Stream, sink::Sink, watch}; +use postage::watch; use rand::prelude::*; +use search::SearchQuery; use sha2::{Digest, Sha256}; use smol::block_on; use std::{ cell::RefCell, + cmp, convert::TryInto, hash::Hash, mem, @@ -54,25 +58,19 @@ pub struct Project { collaborators: HashMap, subscriptions: Vec, language_servers_with_diagnostics_running: isize, - opened_buffer: broadcast::Sender<()>, + opened_buffer: (Rc>>, watch::Receiver<()>), + shared_buffers: HashMap>, loading_buffers: HashMap< ProjectPath, postage::watch::Receiver, Arc>>>, >, - buffers_state: Rc>, - shared_buffers: HashMap>>, + opened_buffers: HashMap, nonce: u128, } -#[derive(Default)] -struct ProjectBuffers { - buffer_request_count: usize, - preserved_buffers: Vec>, - open_buffers: HashMap, -} - enum OpenBuffer { - Loaded(WeakModelHandle), + Strong(ModelHandle), + Weak(WeakModelHandle), Loading(Vec), } @@ -151,8 +149,6 @@ pub struct Symbol { pub signature: [u8; 32], } -pub struct BufferRequestHandle(Rc>); - #[derive(Default)] pub struct ProjectTransaction(pub HashMap, language::Transaction>); @@ -223,6 +219,7 @@ impl Project { client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); client.add_entity_request_handler(Self::handle_lsp_command::); + client.add_entity_request_handler(Self::handle_search_project); client.add_entity_request_handler(Self::handle_get_project_symbols); client.add_entity_request_handler(Self::handle_open_buffer_for_symbol); client.add_entity_request_handler(Self::handle_open_buffer); @@ -243,7 +240,7 @@ impl Project { move |this, mut cx| { async move { let mut status = rpc.status(); - while let Some(status) = status.recv().await { + while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { let remote_id = if let client::Status::Connected { .. } = status { let response = rpc.request(proto::RegisterProject {}).await?; @@ -278,19 +275,20 @@ impl Project { } }); + let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); Self { worktrees: Default::default(), collaborators: Default::default(), - buffers_state: Default::default(), - loading_buffers: Default::default(), + opened_buffers: Default::default(), shared_buffers: Default::default(), + loading_buffers: Default::default(), client_state: ProjectClientState::Local { is_shared: false, remote_id_tx, remote_id_rx, _maintain_remote_id_task, }, - opened_buffer: broadcast::channel(1).0, + opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), subscriptions: Vec::new(), active_entry: None, languages, @@ -331,11 +329,12 @@ impl Project { load_task.detach(); } + let (opened_buffer_tx, opened_buffer_rx) = watch::channel(); let this = cx.add_model(|cx| { let mut this = Self { worktrees: Vec::new(), loading_buffers: Default::default(), - opened_buffer: broadcast::channel(1).0, + opened_buffer: (Rc::new(RefCell::new(opened_buffer_tx)), opened_buffer_rx), shared_buffers: Default::default(), active_entry: None, collaborators: Default::default(), @@ -352,7 +351,7 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), - buffers_state: Default::default(), + opened_buffers: Default::default(), nonce: StdRng::from_entropy().gen(), }; for worktree in worktrees { @@ -392,20 +391,21 @@ impl Project { } #[cfg(any(test, feature = "test-support"))] - pub fn shared_buffer(&self, peer_id: PeerId, remote_id: u64) -> Option> { - self.shared_buffers - .get(&peer_id) - .and_then(|buffers| buffers.get(&remote_id)) - .cloned() + pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { + self.opened_buffers + .get(&remote_id) + .and_then(|buffer| buffer.upgrade(cx)) } #[cfg(any(test, feature = "test-support"))] - pub fn has_buffered_operations(&self) -> bool { - self.buffers_state - .borrow() - .open_buffers - .values() - .any(|buffer| matches!(buffer, OpenBuffer::Loading(_))) + pub fn has_deferred_operations(&self, cx: &AppContext) -> bool { + self.opened_buffers.values().any(|buffer| match buffer { + OpenBuffer::Strong(buffer) => buffer.read(cx).deferred_ops_len() > 0, + OpenBuffer::Weak(buffer) => buffer + .upgrade(cx) + .map_or(false, |buffer| buffer.read(cx).deferred_ops_len() > 0), + OpenBuffer::Loading(_) => false, + }) } #[cfg(any(test, feature = "test-support"))] @@ -454,7 +454,7 @@ impl Project { if let Some(id) = id { return id; } - watch.recv().await; + watch.next().await; } } } @@ -506,7 +506,7 @@ impl Project { pub fn share(&self, cx: &mut ModelContext) -> Task> { let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, _| { + let project_id = this.update(&mut cx, |this, cx| { if let ProjectClientState::Local { is_shared, remote_id_rx, @@ -514,6 +514,17 @@ impl Project { } = &mut this.client_state { *is_shared = true; + for open_buffer in this.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(_) => {} + OpenBuffer::Weak(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + *open_buffer = OpenBuffer::Strong(buffer); + } + } + OpenBuffer::Loading(_) => unreachable!(), + } + } remote_id_rx .borrow() .ok_or_else(|| anyhow!("no project id")) @@ -523,6 +534,7 @@ impl Project { })?; rpc.request(proto::ShareProject { project_id }).await?; + let mut tasks = Vec::new(); this.update(&mut cx, |this, cx| { for worktree in this.worktrees(cx).collect::>() { @@ -551,6 +563,15 @@ impl Project { } = &mut this.client_state { *is_shared = false; + for open_buffer in this.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(buffer) => { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + OpenBuffer::Weak(_) => {} + OpenBuffer::Loading(_) => unreachable!(), + } + } remote_id_rx .borrow() .ok_or_else(|| anyhow!("no project id")) @@ -651,7 +672,7 @@ impl Project { Err(error) => return Err(anyhow!("{}", error)), } } - loading_watch.recv().await; + loading_watch.next().await; } }) } @@ -690,7 +711,6 @@ impl Project { let remote_worktree_id = worktree.read(cx).id(); let path = path.clone(); let path_string = path.to_string_lossy().to_string(); - let request_handle = self.start_buffer_request(cx); cx.spawn(|this, mut cx| async move { let response = rpc .request(proto::OpenBuffer { @@ -700,11 +720,8 @@ impl Project { }) .await?; let buffer = response.buffer.ok_or_else(|| anyhow!("missing buffer"))?; - - this.update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle, cx) - }) - .await + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await }) } @@ -745,10 +762,6 @@ impl Project { }) } - fn start_buffer_request(&self, cx: &AppContext) -> BufferRequestHandle { - BufferRequestHandle::new(self.buffers_state.clone(), cx) - } - pub fn save_buffer_as( &self, buffer: ModelHandle, @@ -777,20 +790,16 @@ impl Project { pub fn has_open_buffer(&self, path: impl Into, cx: &AppContext) -> bool { let path = path.into(); if let Some(worktree) = self.worktree_for_id(path.worktree_id, cx) { - self.buffers_state - .borrow() - .open_buffers - .iter() - .any(|(_, buffer)| { - if let Some(buffer) = buffer.upgrade(cx) { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if file.worktree == worktree && file.path() == &path.path { - return true; - } + self.opened_buffers.iter().any(|(_, buffer)| { + if let Some(buffer) = buffer.upgrade(cx) { + if let Some(file) = File::from_dyn(buffer.read(cx).file()) { + if file.worktree == worktree && file.path() == &path.path { + return true; } } - false - }) + } + false + }) } else { false } @@ -801,24 +810,16 @@ impl Project { path: &ProjectPath, cx: &mut ModelContext, ) -> Option> { - let mut result = None; let worktree = self.worktree_for_id(path.worktree_id, cx)?; - self.buffers_state - .borrow_mut() - .open_buffers - .retain(|_, buffer| { - if let Some(buffer) = buffer.upgrade(cx) { - if let Some(file) = File::from_dyn(buffer.read(cx).file()) { - if file.worktree == worktree && file.path() == &path.path { - result = Some(buffer); - } - } - true - } else { - false - } - }); - result + self.opened_buffers.values().find_map(|buffer| { + let buffer = buffer.upgrade(cx)?; + let file = File::from_dyn(buffer.read(cx).file())?; + if file.worktree == worktree && file.path() == &path.path { + Some(buffer) + } else { + None + } + }) } fn register_buffer( @@ -828,17 +829,18 @@ impl Project { cx: &mut ModelContext, ) -> Result<()> { let remote_id = buffer.read(cx).remote_id(); - match self - .buffers_state - .borrow_mut() - .open_buffers - .insert(remote_id, OpenBuffer::Loaded(buffer.downgrade())) - { + let open_buffer = if self.is_remote() || self.is_shared() { + OpenBuffer::Strong(buffer.clone()) + } else { + OpenBuffer::Weak(buffer.downgrade()) + }; + + match self.opened_buffers.insert(remote_id, open_buffer) { None => {} Some(OpenBuffer::Loading(operations)) => { buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))? } - Some(OpenBuffer::Loaded(existing_handle)) => { + Some(OpenBuffer::Weak(existing_handle)) => { if existing_handle.upgrade(cx).is_some() { Err(anyhow!( "already registered buffer with remote id {}", @@ -846,6 +848,10 @@ impl Project { ))? } } + Some(OpenBuffer::Strong(_)) => Err(anyhow!( + "already registered buffer with remote id {}", + remote_id + ))?, } self.assign_language_to_buffer(&buffer, worktree, cx); Ok(()) @@ -1165,7 +1171,7 @@ impl Project { path: relative_path.into(), }; - for buffer in self.buffers_state.borrow().open_buffers.values() { + for buffer in self.opened_buffers.values() { if let Some(buffer) = buffer.upgrade(cx) { if buffer .read(cx) @@ -1228,7 +1234,6 @@ impl Project { let remote_buffers = self.remote_id().zip(remote_buffers); let client = self.client.clone(); - let request_handle = self.start_buffer_request(cx); cx.spawn(|this, mut cx| async move { let mut project_transaction = ProjectTransaction::default(); @@ -1247,12 +1252,7 @@ impl Project { .ok_or_else(|| anyhow!("missing transaction"))?; project_transaction = this .update(&mut cx, |this, cx| { - this.deserialize_project_transaction( - response, - push_to_history, - request_handle, - cx, - ) + this.deserialize_project_transaction(response, push_to_history, cx) }) .await?; } @@ -1469,7 +1469,6 @@ impl Project { cx, ) } else if let Some(project_id) = self.remote_id() { - let request_handle = self.start_buffer_request(cx); let request = self.client.request(proto::OpenBufferForSymbol { project_id, symbol: Some(serialize_symbol(symbol)), @@ -1477,10 +1476,8 @@ impl Project { cx.spawn(|this, mut cx| async move { let response = request.await?; let buffer = response.buffer.ok_or_else(|| anyhow!("invalid buffer"))?; - this.update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle, cx) - }) - .await + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await }) } else { Task::ready(Err(anyhow!("project does not have a remote id"))) @@ -1859,7 +1856,6 @@ impl Project { }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); - let request_handle = self.start_buffer_request(cx); let request = proto::ApplyCodeAction { project_id, buffer_id: buffer_handle.read(cx).remote_id(), @@ -1872,12 +1868,7 @@ impl Project { .transaction .ok_or_else(|| anyhow!("missing transaction"))?; this.update(&mut cx, |this, cx| { - this.deserialize_project_transaction( - response, - push_to_history, - request_handle, - cx, - ) + this.deserialize_project_transaction(response, push_to_history, cx) }) .await }) @@ -2042,6 +2033,209 @@ impl Project { ) } + pub fn search( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Task, Vec>>>> { + if self.is_local() { + let snapshots = self + .strong_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + return Task::ready(Ok(Default::default())); + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); + cx.background() + .spawn({ + let fs = self.fs.clone(); + let background = cx.background().clone(); + let query = query.clone(); + async move { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let paths_per_worker = (path_count + workers - 1) / workers; + let snapshots = &snapshots; + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = + snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = worker_start_ix + .saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) + - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + let matches = if let Some(file) = + fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + }; + + if matches { + let project_path = + (snapshot.id(), entry.path.clone()); + if matching_paths_tx + .send(project_path) + .await + .is_err() + { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; + } + }) + .detach(); + + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let open_buffers = self + .opened_buffers + .values() + .filter_map(|b| b.upgrade(cx)) + .collect::>(); + cx.spawn(|this, cx| async move { + for buffer in &open_buffers { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer.clone(), snapshot)).await?; + } + + let open_buffers = Rc::new(RefCell::new(open_buffers)); + while let Some(project_path) = matching_paths_rx.next().await { + if buffers_tx.is_closed() { + break; + } + + let this = this.clone(); + let open_buffers = open_buffers.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + if let Some(buffer) = this + .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await + .log_err() + { + if open_buffers.borrow_mut().insert(buffer.clone()) { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx.send((buffer, snapshot)).await?; + } + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach_and_log_err(cx); + + let background = cx.background().clone(); + cx.background().spawn(async move { + let query = &query; + let mut matched_buffers = Vec::new(); + for _ in 0..workers { + matched_buffers.push(HashMap::default()); + } + background + .scoped(|scope| { + for worker_matched_buffers in matched_buffers.iter_mut() { + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((buffer, snapshot)) = buffers_rx.next().await { + let buffer_matches = query + .search(snapshot.as_rope()) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect::>(); + if !buffer_matches.is_empty() { + worker_matched_buffers + .insert(buffer.clone(), buffer_matches); + } + } + }); + } + }) + .await; + Ok(matched_buffers.into_iter().flatten().collect()) + }) + } else if let Some(project_id) = self.remote_id() { + let request = self.client.request(query.to_proto(project_id)); + cx.spawn(|this, mut cx| async move { + let response = request.await?; + let mut result = HashMap::default(); + for location in response.locations { + let buffer = location.buffer.ok_or_else(|| anyhow!("missing buffer"))?; + let target_buffer = this + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await?; + let start = location + .start + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target start"))?; + let end = location + .end + .and_then(deserialize_anchor) + .ok_or_else(|| anyhow!("missing target end"))?; + result + .entry(target_buffer) + .or_insert(Vec::new()) + .push(start..end) + } + Ok(result) + }) + } else { + Task::ready(Ok(Default::default())) + } + } + fn request_lsp( &self, buffer_handle: ModelHandle, @@ -2068,12 +2262,11 @@ impl Project { } } else if let Some(project_id) = self.remote_id() { let rpc = self.client.clone(); - let request_handle = self.start_buffer_request(cx); let message = request.to_proto(project_id, buffer); return cx.spawn(|this, cx| async move { let response = rpc.request(message).await?; request - .response_from_proto(response, this, buffer_handle, request_handle, cx) + .response_from_proto(response, this, buffer_handle, cx) .await }); } @@ -2201,7 +2394,7 @@ impl Project { ) { let snapshot = worktree_handle.read(cx).snapshot(); let mut buffers_to_delete = Vec::new(); - for (buffer_id, buffer) in &self.buffers_state.borrow().open_buffers { + for (buffer_id, buffer) in &self.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| { if let Some(old_file) = File::from_dyn(buffer.file()) { @@ -2258,10 +2451,7 @@ impl Project { } for buffer_id in buffers_to_delete { - self.buffers_state - .borrow_mut() - .open_buffers - .remove(&buffer_id); + self.opened_buffers.remove(&buffer_id); } } @@ -2388,8 +2578,7 @@ impl Project { .remove(&peer_id) .ok_or_else(|| anyhow!("unknown peer {:?}", peer_id))? .replica_id; - this.shared_buffers.remove(&peer_id); - for (_, buffer) in &this.buffers_state.borrow().open_buffers { + for (_, buffer) in &this.opened_buffers { if let Some(buffer) = buffer.upgrade(cx) { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } @@ -2515,24 +2704,16 @@ impl Project { .into_iter() .map(|op| language::proto::deserialize_operation(op)) .collect::, _>>()?; - let is_remote = this.is_remote(); - let mut buffers_state = this.buffers_state.borrow_mut(); - let buffer_request_count = buffers_state.buffer_request_count; - match buffers_state.open_buffers.entry(buffer_id) { + match this.opened_buffers.entry(buffer_id) { hash_map::Entry::Occupied(mut e) => match e.get_mut() { - OpenBuffer::Loaded(buffer) => { - if let Some(buffer) = buffer.upgrade(cx) { - buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; - } else if is_remote && buffer_request_count > 0 { - e.insert(OpenBuffer::Loading(ops)); - } + OpenBuffer::Strong(buffer) => { + buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?; } OpenBuffer::Loading(operations) => operations.extend_from_slice(&ops), + _ => unreachable!(), }, hash_map::Entry::Vacant(e) => { - if is_remote && buffer_request_count > 0 { - e.insert(OpenBuffer::Loading(ops)); - } + e.insert(OpenBuffer::Loading(ops)); } } Ok(()) @@ -2554,9 +2735,7 @@ impl Project { .ok_or_else(|| anyhow!("no such worktree"))?; let file = File::from_proto(file, worktree.clone(), cx)?; let buffer = this - .buffers_state - .borrow_mut() - .open_buffers + .opened_buffers .get_mut(&buffer_id) .and_then(|b| b.upgrade(cx)) .ok_or_else(|| anyhow!("no such buffer"))?; @@ -2574,15 +2753,14 @@ impl Project { mut cx: AsyncAppContext, ) -> Result { let buffer_id = envelope.payload.buffer_id; - let sender_id = envelope.original_sender_id()?; let requested_version = envelope.payload.version.try_into()?; - let (project_id, buffer) = this.update(&mut cx, |this, _| { + let (project_id, buffer) = this.update(&mut cx, |this, cx| { let project_id = this.remote_id().ok_or_else(|| anyhow!("not connected"))?; let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .opened_buffers + .get(&buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; Ok::<_, anyhow::Error>((project_id, buffer)) })?; @@ -2611,16 +2789,12 @@ impl Project { ) -> Result { let sender_id = envelope.original_sender_id()?; let format = this.update(&mut cx, |this, cx| { - let shared_buffers = this - .shared_buffers - .get(&sender_id) - .ok_or_else(|| anyhow!("peer has no buffers"))?; let mut buffers = HashSet::default(); for buffer_id in &envelope.payload.buffer_ids { buffers.insert( - shared_buffers + this.opened_buffers .get(buffer_id) - .cloned() + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?, ); } @@ -2642,17 +2816,16 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let position = envelope .payload .position .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid position"))?; let version = clock::Global::from(envelope.payload.version); - let buffer = this.read_with(&cx, |this, _| { - this.shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + let buffer = this.read_with(&cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; if !buffer @@ -2681,12 +2854,11 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let apply_additional_edits = this.update(&mut cx, |this, cx| { let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; let language = buffer.read(cx).language(); let completion = language::proto::deserialize_completion( @@ -2715,7 +2887,6 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result { - let sender_id = envelope.original_sender_id()?; let start = envelope .payload .start @@ -2726,10 +2897,10 @@ impl Project { .end .and_then(language::proto::deserialize_anchor) .ok_or_else(|| anyhow!("invalid end"))?; - let buffer = this.update(&mut cx, |this, _| { - this.shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; let version = buffer.read_with(&cx, |buffer, _| buffer.version()); @@ -2765,9 +2936,9 @@ impl Project { )?; let apply_code_action = this.update(&mut cx, |this, cx| { let buffer = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&envelope.payload.buffer_id).cloned()) + .opened_buffers + .get(&envelope.payload.buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id))?; Ok::<_, anyhow::Error>(this.apply_code_action(buffer, action, false, cx)) })?; @@ -2794,9 +2965,9 @@ impl Project { let (request, buffer_version) = this.update(&mut cx, |this, cx| { let buffer_id = T::buffer_id_from_proto(&envelope.payload); let buffer_handle = this - .shared_buffers - .get(&sender_id) - .and_then(|shared_buffers| shared_buffers.get(&buffer_id).cloned()) + .opened_buffers + .get(&buffer_id) + .map(|buffer| buffer.upgrade(cx).unwrap()) .ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?; let buffer = buffer_handle.read(cx); let buffer_version = buffer.version(); @@ -2832,6 +3003,36 @@ impl Project { }) } + async fn handle_search_project( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let query = SearchQuery::from_proto(envelope.payload)?; + let result = this + .update(&mut cx, |this, cx| this.search(query, cx)) + .await?; + + this.update(&mut cx, |this, cx| { + let mut locations = Vec::new(); + for (buffer, ranges) in result { + for range in ranges { + let start = serialize_anchor(&range.start); + let end = serialize_anchor(&range.end); + let buffer = this.serialize_buffer_for_peer(&buffer, peer_id, cx); + locations.push(proto::Location { + buffer: Some(buffer), + start: Some(start), + end: Some(end), + }); + } + } + Ok(proto::SearchProjectResponse { locations }) + }) + } + async fn handle_open_buffer_for_symbol( this: ModelHandle, envelope: TypedEnvelope, @@ -2922,16 +3123,13 @@ impl Project { &mut self, message: proto::ProjectTransaction, push_to_history: bool, - request_handle: BufferRequestHandle, cx: &mut ModelContext, ) -> Task> { cx.spawn(|this, mut cx| async move { let mut project_transaction = ProjectTransaction::default(); for (buffer, transaction) in message.buffers.into_iter().zip(message.transactions) { let buffer = this - .update(&mut cx, |this, cx| { - this.deserialize_buffer(buffer, request_handle.clone(), cx) - }) + .update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) .await?; let transaction = language::proto::deserialize_transaction(transaction)?; project_transaction.0.insert(buffer, transaction); @@ -2963,15 +3161,13 @@ impl Project { ) -> proto::Buffer { let buffer_id = buffer.read(cx).remote_id(); let shared_buffers = self.shared_buffers.entry(peer_id).or_default(); - match shared_buffers.entry(buffer_id) { - hash_map::Entry::Occupied(_) => proto::Buffer { + if shared_buffers.insert(buffer_id) { + proto::Buffer { + variant: Some(proto::buffer::Variant::State(buffer.read(cx).to_proto())), + } + } else { + proto::Buffer { variant: Some(proto::buffer::Variant::Id(buffer_id)), - }, - hash_map::Entry::Vacant(entry) => { - entry.insert(buffer.clone()); - proto::Buffer { - variant: Some(proto::buffer::Variant::State(buffer.read(cx).to_proto())), - } } } } @@ -2979,21 +3175,18 @@ impl Project { fn deserialize_buffer( &mut self, buffer: proto::Buffer, - request_handle: BufferRequestHandle, cx: &mut ModelContext, ) -> Task>> { let replica_id = self.replica_id(); - let mut opened_buffer_tx = self.opened_buffer.clone(); - let mut opened_buffer_rx = self.opened_buffer.subscribe(); + let opened_buffer_tx = self.opened_buffer.0.clone(); + let mut opened_buffer_rx = self.opened_buffer.1.clone(); cx.spawn(|this, mut cx| async move { match buffer.variant.ok_or_else(|| anyhow!("missing buffer"))? { proto::buffer::Variant::Id(id) => { let buffer = loop { let buffer = this.read_with(&cx, |this, cx| { - this.buffers_state - .borrow() - .open_buffers + this.opened_buffers .get(&id) .and_then(|buffer| buffer.upgrade(cx)) }); @@ -3001,7 +3194,7 @@ impl Project { break buffer; } opened_buffer_rx - .recv() + .next() .await .ok_or_else(|| anyhow!("project dropped while waiting for buffer"))?; }; @@ -3029,12 +3222,11 @@ impl Project { Buffer::from_proto(replica_id, buffer, buffer_file, cx).unwrap() }); - request_handle.preserve_buffer(buffer.clone()); this.update(&mut cx, |this, cx| { this.register_buffer(&buffer, buffer_worktree.as_ref(), cx) })?; - let _ = opened_buffer_tx.send(()).await; + *opened_buffer_tx.borrow_mut().borrow_mut() = (); Ok(buffer) } } @@ -3071,20 +3263,13 @@ impl Project { } async fn handle_close_buffer( - this: ModelHandle, - envelope: TypedEnvelope, + _: ModelHandle, + _: TypedEnvelope, _: Arc, - mut cx: AsyncAppContext, + _: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - if let Some(shared_buffers) = - this.shared_buffers.get_mut(&envelope.original_sender_id()?) - { - shared_buffers.remove(&envelope.payload.buffer_id); - cx.notify(); - } - Ok(()) - }) + // TODO: use this for following + Ok(()) } async fn handle_buffer_saved( @@ -3102,9 +3287,7 @@ impl Project { this.update(&mut cx, |this, cx| { let buffer = this - .buffers_state - .borrow() - .open_buffers + .opened_buffers .get(&envelope.payload.buffer_id) .and_then(|buffer| buffer.upgrade(cx)); if let Some(buffer) = buffer { @@ -3130,9 +3313,7 @@ impl Project { .into(); this.update(&mut cx, |this, cx| { let buffer = this - .buffers_state - .borrow() - .open_buffers + .opened_buffers .get(&payload.buffer_id) .and_then(|buffer| buffer.upgrade(cx)); if let Some(buffer) = buffer { @@ -3182,48 +3363,6 @@ impl Project { } } -impl BufferRequestHandle { - fn new(state: Rc>, cx: &AppContext) -> Self { - { - let state = &mut *state.borrow_mut(); - state.buffer_request_count += 1; - if state.buffer_request_count == 1 { - state.preserved_buffers.extend( - state - .open_buffers - .values() - .filter_map(|buffer| buffer.upgrade(cx)), - ) - } - } - Self(state) - } - - fn preserve_buffer(&self, buffer: ModelHandle) { - self.0.borrow_mut().preserved_buffers.push(buffer); - } -} - -impl Clone for BufferRequestHandle { - fn clone(&self) -> Self { - self.0.borrow_mut().buffer_request_count += 1; - Self(self.0.clone()) - } -} - -impl Drop for BufferRequestHandle { - fn drop(&mut self) { - let mut state = self.0.borrow_mut(); - state.buffer_request_count -= 1; - if state.buffer_request_count == 0 { - state.preserved_buffers.clear(); - state - .open_buffers - .retain(|_, buffer| matches!(buffer, OpenBuffer::Loaded(_))) - } - } -} - impl WorktreeHandle { pub fn upgrade(&self, cx: &AppContext) -> Option> { match self { @@ -3236,7 +3375,8 @@ impl WorktreeHandle { impl OpenBuffer { pub fn upgrade(&self, cx: &impl UpgradeModelHandle) -> Option> { match self { - OpenBuffer::Loaded(handle) => handle.upgrade(cx), + OpenBuffer::Strong(handle) => Some(handle.clone()), + OpenBuffer::Weak(handle) => handle.upgrade(cx), OpenBuffer::Loading(_) => None, } } @@ -4709,4 +4849,84 @@ mod tests { "const TWO: usize = one::THREE + one::THREE;" ); } + + #[gpui::test] + async fn test_search(mut cx: gpui::TestAppContext) { + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "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(), &mut cx); + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + let worktree_id = tree.read_with(&cx, |tree, _| tree.id()); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + assert_eq!( + search(&project, SearchQuery::text("TWO", false, true), &mut cx) + .await + .unwrap(), + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]) + ]) + ); + + let buffer_4 = project + .update(&mut cx, |project, cx| { + project.open_buffer((worktree_id, "four.rs"), cx) + }) + .await + .unwrap(); + buffer_4.update(&mut cx, |buffer, cx| { + buffer.edit([20..28, 31..43], "two::TWO", cx); + }); + + assert_eq!( + search(&project, SearchQuery::text("TWO", false, true), &mut cx) + .await + .unwrap(), + HashMap::from_iter([ + ("two.rs".to_string(), vec![6..9]), + ("three.rs".to_string(), vec![37..40]), + ("four.rs".to_string(), vec![25..28, 36..39]) + ]) + ); + + async fn search( + project: &ModelHandle, + query: SearchQuery, + cx: &mut gpui::TestAppContext, + ) -> Result>>> { + let results = project + .update(cx, |project, cx| project.search(query, cx)) + .await?; + + Ok(results + .into_iter() + .map(|(buffer, ranges)| { + buffer.read_with(cx, |buffer, _| { + let path = buffer.file().unwrap().path().to_string_lossy().to_string(); + let ranges = ranges + .into_iter() + .map(|range| range.to_offset(buffer)) + .collect::>(); + (path, ranges) + }) + }) + .collect()) + } + } } diff --git a/crates/project/src/search.rs b/crates/project/src/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..58fa04eeb2feabba45ae36c7d23b3630f22ca621 --- /dev/null +++ b/crates/project/src/search.rs @@ -0,0 +1,227 @@ +use aho_corasick::{AhoCorasick, AhoCorasickBuilder}; +use anyhow::Result; +use client::proto; +use language::{char_kind, Rope}; +use regex::{Regex, RegexBuilder}; +use smol::future::yield_now; +use std::{ + io::{BufRead, BufReader, Read}, + ops::Range, + sync::Arc, +}; + +#[derive(Clone)] +pub enum SearchQuery { + Text { + search: Arc>, + query: Arc, + whole_word: bool, + case_sensitive: bool, + }, + Regex { + regex: Regex, + query: Arc, + multiline: bool, + whole_word: bool, + case_sensitive: bool, + }, +} + +impl SearchQuery { + pub fn text(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Self { + let query = query.to_string(); + let search = AhoCorasickBuilder::new() + .auto_configure(&[&query]) + .ascii_case_insensitive(!case_sensitive) + .build(&[&query]); + Self::Text { + search: Arc::new(search), + query: Arc::from(query), + whole_word, + case_sensitive, + } + } + + pub fn regex(query: impl ToString, whole_word: bool, case_sensitive: bool) -> Result { + let mut query = query.to_string(); + let initial_query = Arc::from(query.as_str()); + if whole_word { + let mut word_query = String::new(); + word_query.push_str("\\b"); + word_query.push_str(&query); + word_query.push_str("\\b"); + query = word_query + } + + let multiline = query.contains("\n") || query.contains("\\n"); + let regex = RegexBuilder::new(&query) + .case_insensitive(!case_sensitive) + .multi_line(multiline) + .build()?; + Ok(Self::Regex { + regex, + query: initial_query, + multiline, + whole_word, + case_sensitive, + }) + } + + pub fn from_proto(message: proto::SearchProject) -> Result { + if message.regex { + Self::regex(message.query, message.whole_word, message.case_sensitive) + } else { + Ok(Self::text( + message.query, + message.whole_word, + message.case_sensitive, + )) + } + } + + pub fn to_proto(&self, project_id: u64) -> proto::SearchProject { + proto::SearchProject { + project_id, + query: self.as_str().to_string(), + regex: self.is_regex(), + whole_word: self.whole_word(), + case_sensitive: self.case_sensitive(), + } + } + + pub fn detect(&self, stream: T) -> Result { + if self.as_str().is_empty() { + return Ok(false); + } + + match self { + Self::Text { search, .. } => { + let mat = search.stream_find_iter(stream).next(); + match mat { + Some(Ok(_)) => Ok(true), + Some(Err(err)) => Err(err.into()), + None => Ok(false), + } + } + Self::Regex { + regex, multiline, .. + } => { + let mut reader = BufReader::new(stream); + if *multiline { + let mut text = String::new(); + if let Err(err) = reader.read_to_string(&mut text) { + Err(err.into()) + } else { + Ok(regex.find(&text).is_some()) + } + } else { + for line in reader.lines() { + let line = line?; + if regex.find(&line).is_some() { + return Ok(true); + } + } + Ok(false) + } + } + } + } + + pub async fn search(&self, rope: &Rope) -> Vec> { + const YIELD_INTERVAL: usize = 20000; + + if self.as_str().is_empty() { + return Default::default(); + } + + let mut matches = Vec::new(); + match self { + Self::Text { + search, whole_word, .. + } => { + for (ix, mat) in search + .stream_find_iter(rope.bytes_in_range(0..rope.len())) + .enumerate() + { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + let mat = mat.unwrap(); + if *whole_word { + let prev_kind = rope.reversed_chars_at(mat.start()).next().map(char_kind); + let start_kind = char_kind(rope.chars_at(mat.start()).next().unwrap()); + let end_kind = char_kind(rope.reversed_chars_at(mat.end()).next().unwrap()); + let next_kind = rope.chars_at(mat.end()).next().map(char_kind); + if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { + continue; + } + } + matches.push(mat.start()..mat.end()) + } + } + Self::Regex { + regex, multiline, .. + } => { + if *multiline { + let text = rope.to_string(); + for (ix, mat) in regex.find_iter(&text).enumerate() { + if (ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + matches.push(mat.start()..mat.end()); + } + } else { + let mut line = String::new(); + let mut line_offset = 0; + for (chunk_ix, chunk) in rope.chunks().chain(["\n"]).enumerate() { + if (chunk_ix + 1) % YIELD_INTERVAL == 0 { + yield_now().await; + } + + for (newline_ix, text) in chunk.split('\n').enumerate() { + if newline_ix > 0 { + for mat in regex.find_iter(&line) { + let start = line_offset + mat.start(); + let end = line_offset + mat.end(); + matches.push(start..end); + } + + line_offset += line.len() + 1; + line.clear(); + } + line.push_str(text); + } + } + } + } + } + matches + } + + pub fn as_str(&self) -> &str { + match self { + Self::Text { query, .. } => query.as_ref(), + Self::Regex { query, .. } => query.as_ref(), + } + } + + pub fn whole_word(&self) -> bool { + match self { + Self::Text { whole_word, .. } => *whole_word, + Self::Regex { whole_word, .. } => *whole_word, + } + } + + pub fn case_sensitive(&self) -> bool { + match self { + Self::Text { case_sensitive, .. } => *case_sensitive, + Self::Regex { case_sensitive, .. } => *case_sensitive, + } + } + + pub fn is_regex(&self) -> bool { + matches!(self, Self::Regex { .. }) + } +} diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 795ca23c8f28cd732858b52c82d1ef2ac9615ac0..6455af3e53864caf647df14a3510424b71a7d96c 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -554,10 +554,6 @@ impl LocalWorktree { Ok((tree, scan_states_tx)) } - pub fn abs_path(&self) -> &Arc { - &self.abs_path - } - pub fn contains_abs_path(&self, path: &Path) -> bool { path.starts_with(&self.abs_path) } @@ -1017,6 +1013,10 @@ impl Snapshot { } impl LocalSnapshot { + pub fn abs_path(&self) -> &Arc { + &self.abs_path + } + #[cfg(test)] pub(crate) fn to_proto( &self, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ebdb39942ddfa4968ba5ffcaa4acf8bd8a794140..18e32560268bd3e56087d2ce687dd68ec712bfb7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -21,6 +21,7 @@ message Envelope { LeaveProject leave_project = 15; AddProjectCollaborator add_project_collaborator = 16; RemoveProjectCollaborator remove_project_collaborator = 17; + GetDefinition get_definition = 18; GetDefinitionResponse get_definition_response = 19; GetReferences get_references = 20; @@ -61,22 +62,24 @@ message Envelope { PrepareRenameResponse prepare_rename_response = 54; PerformRename perform_rename = 55; PerformRenameResponse perform_rename_response = 56; - - GetChannels get_channels = 57; - GetChannelsResponse get_channels_response = 58; - JoinChannel join_channel = 59; - JoinChannelResponse join_channel_response = 60; - LeaveChannel leave_channel = 61; - SendChannelMessage send_channel_message = 62; - SendChannelMessageResponse send_channel_message_response = 63; - ChannelMessageSent channel_message_sent = 64; - GetChannelMessages get_channel_messages = 65; - GetChannelMessagesResponse get_channel_messages_response = 66; - - UpdateContacts update_contacts = 67; - - GetUsers get_users = 68; - GetUsersResponse get_users_response = 69; + SearchProject search_project = 57; + SearchProjectResponse search_project_response = 58; + + GetChannels get_channels = 59; + GetChannelsResponse get_channels_response = 60; + JoinChannel join_channel = 61; + JoinChannelResponse join_channel_response = 62; + LeaveChannel leave_channel = 63; + SendChannelMessage send_channel_message = 64; + SendChannelMessageResponse send_channel_message_response = 65; + ChannelMessageSent channel_message_sent = 66; + GetChannelMessages get_channel_messages = 67; + GetChannelMessagesResponse get_channel_messages_response = 68; + + UpdateContacts update_contacts = 69; + + GetUsers get_users = 70; + GetUsersResponse get_users_response = 71; } } @@ -366,6 +369,18 @@ message PerformRenameResponse { ProjectTransaction transaction = 2; } +message SearchProject { + uint64 project_id = 1; + string query = 2; + bool regex = 3; + bool whole_word = 4; + bool case_sensitive = 5; +} + +message SearchProjectResponse { + repeated Location locations = 1; +} + message CodeAction { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 5ec46eb353a59652eff919b9f8fce9fdb11aebb3..1bfb392db0b7c037b8e4c906b8e61b4b80e9b15b 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -190,6 +190,8 @@ messages!( (RegisterWorktree, Foreground), (RemoveProjectCollaborator, Foreground), (SaveBuffer, Foreground), + (SearchProject, Foreground), + (SearchProjectResponse, Foreground), (SendChannelMessage, Foreground), (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), @@ -230,6 +232,7 @@ request_messages!( (RegisterProject, RegisterProjectResponse), (RegisterWorktree, Ack), (SaveBuffer, BufferSaved), + (SearchProject, SearchProjectResponse), (SendChannelMessage, SendChannelMessageResponse), (ShareProject, Ack), (Test, Test), @@ -262,6 +265,7 @@ entity_messages!( PrepareRename, RemoveProjectCollaborator, SaveBuffer, + SearchProject, UnregisterWorktree, UnshareProject, UpdateBuffer, diff --git a/crates/find/Cargo.toml b/crates/search/Cargo.toml similarity index 71% rename from crates/find/Cargo.toml rename to crates/search/Cargo.toml index acab695d12c17c3bd6501ac64adf9a4f2f718daf..cee9f156e040d469ee8e716b953b3181f1d56237 100644 --- a/crates/find/Cargo.toml +++ b/crates/search/Cargo.toml @@ -1,25 +1,27 @@ [package] -name = "find" +name = "search" version = "0.1.0" edition = "2021" [lib] -path = "src/find.rs" +path = "src/search.rs" [dependencies] collections = { path = "../collections" } editor = { path = "../editor" } gpui = { path = "../gpui" } +language = { path = "../language" } +project = { path = "../project" } theme = { path = "../theme" } +util = { path = "../util" } workspace = { path = "../workspace" } -aho-corasick = "0.7" anyhow = "1.0" +log = "0.4" postage = { version = "0.4.1", features = ["futures-traits"] } -regex = "1.5" -smol = { version = "1.2" } [dev-dependencies] editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } +serde_json = { version = "1.0.64", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } unindent = "0.1" diff --git a/crates/find/src/find.rs b/crates/search/src/buffer_search.rs similarity index 53% rename from crates/find/src/find.rs rename to crates/search/src/buffer_search.rs index 60dd4cefb73bcf699094b83e16d5e24cfb5614d3..847345448a1dfad09424a403377e9a2802a0ef51 100644 --- a/crates/find/src/find.rs +++ b/crates/search/src/buffer_search.rs @@ -1,61 +1,45 @@ -use aho_corasick::AhoCorasickBuilder; -use anyhow::Result; +use crate::{active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch}; use collections::HashMap; -use editor::{ - char_kind, display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor, MultiBufferSnapshot, -}; +use editor::{display_map::ToDisplayPoint, Anchor, Autoscroll, Bias, Editor}; use gpui::{ action, elements::*, keymap::Binding, platform::CursorStyle, Entity, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; +use language::AnchorRangeExt; use postage::watch; -use regex::RegexBuilder; -use smol::future::yield_now; -use std::{ - cmp::{self, Ordering}, - ops::Range, -}; +use project::search::SearchQuery; +use std::ops::Range; use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Dismiss); action!(FocusEditor); -action!(ToggleMode, SearchMode); -action!(GoToMatch, Direction); - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum Direction { - Prev, - Next, -} - -#[derive(Clone, Copy)] -pub enum SearchMode { - WholeWord, - CaseSensitive, - Regex, -} +action!(ToggleSearchOption, SearchOption); pub fn init(cx: &mut MutableAppContext) { cx.add_bindings([ Binding::new("cmd-f", Deploy(true), Some("Editor && mode == full")), Binding::new("cmd-e", Deploy(false), Some("Editor && mode == full")), - Binding::new("escape", Dismiss, Some("FindBar")), - Binding::new("cmd-f", FocusEditor, Some("FindBar")), - Binding::new("enter", GoToMatch(Direction::Next), Some("FindBar")), - Binding::new("shift-enter", GoToMatch(Direction::Prev), Some("FindBar")), - Binding::new("cmd-g", GoToMatch(Direction::Next), Some("Pane")), - Binding::new("cmd-shift-G", GoToMatch(Direction::Prev), Some("Pane")), + Binding::new("escape", Dismiss, Some("SearchBar")), + Binding::new("cmd-f", FocusEditor, Some("SearchBar")), + Binding::new("enter", SelectMatch(Direction::Next), Some("SearchBar")), + Binding::new( + "shift-enter", + SelectMatch(Direction::Prev), + Some("SearchBar"), + ), + Binding::new("cmd-g", SelectMatch(Direction::Next), Some("Pane")), + Binding::new("cmd-shift-G", SelectMatch(Direction::Prev), Some("Pane")), ]); - cx.add_action(FindBar::deploy); - cx.add_action(FindBar::dismiss); - cx.add_action(FindBar::focus_editor); - cx.add_action(FindBar::toggle_mode); - cx.add_action(FindBar::go_to_match); - cx.add_action(FindBar::go_to_match_on_pane); + cx.add_action(SearchBar::deploy); + cx.add_action(SearchBar::dismiss); + cx.add_action(SearchBar::focus_editor); + cx.add_action(SearchBar::toggle_search_option); + cx.add_action(SearchBar::select_match); + cx.add_action(SearchBar::select_match_on_pane); } -struct FindBar { +struct SearchBar { settings: watch::Receiver, query_editor: ViewHandle, active_editor: Option>, @@ -63,20 +47,20 @@ struct FindBar { active_editor_subscription: Option, editors_with_matches: HashMap, Vec>>, pending_search: Option>, - case_sensitive_mode: bool, - whole_word_mode: bool, - regex_mode: bool, + case_sensitive: bool, + whole_word: bool, + regex: bool, query_contains_error: bool, dismissed: bool, } -impl Entity for FindBar { +impl Entity for SearchBar { type Event = (); } -impl View for FindBar { +impl View for SearchBar { fn ui_name() -> &'static str { - "FindBar" + "SearchBar" } fn on_focus(&mut self, cx: &mut ViewContext) { @@ -86,9 +70,9 @@ impl View for FindBar { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings.borrow().theme; let editor_container = if self.query_contains_error { - theme.find.invalid_editor + theme.search.invalid_editor } else { - theme.find.editor.input.container + theme.search.editor.input.container }; Flex::row() .with_child( @@ -97,16 +81,16 @@ impl View for FindBar { .with_style(editor_container) .aligned() .constrained() - .with_max_width(theme.find.editor.max_width) + .with_max_width(theme.search.editor.max_width) .boxed(), ) .with_child( Flex::row() - .with_child(self.render_mode_button("Case", SearchMode::CaseSensitive, cx)) - .with_child(self.render_mode_button("Word", SearchMode::WholeWord, cx)) - .with_child(self.render_mode_button("Regex", SearchMode::Regex, cx)) + .with_child(self.render_search_option("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_search_option("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_search_option("Regex", SearchOption::Regex, cx)) .contained() - .with_style(theme.find.mode_button_group) + .with_style(theme.search.option_button_group) .aligned() .boxed(), ) @@ -126,22 +110,22 @@ impl View for FindBar { }; Some( - Label::new(message, theme.find.match_index.text.clone()) + Label::new(message, theme.search.match_index.text.clone()) .contained() - .with_style(theme.find.match_index.container) + .with_style(theme.search.match_index.container) .aligned() .boxed(), ) })) .contained() - .with_style(theme.find.container) + .with_style(theme.search.container) .constrained() .with_height(theme.workspace.toolbar.height) - .named("find bar") + .named("search bar") } } -impl Toolbar for FindBar { +impl Toolbar for SearchBar { fn active_item_changed( &mut self, item: Option>, @@ -152,14 +136,15 @@ impl Toolbar for FindBar { self.pending_search.take(); if let Some(editor) = item.and_then(|item| item.act_as::(cx)) { - self.active_editor_subscription = - Some(cx.subscribe(&editor, Self::on_active_editor_event)); - self.active_editor = Some(editor); - self.update_matches(false, cx); - true - } else { - false + if editor.read(cx).searchable() { + self.active_editor_subscription = + Some(cx.subscribe(&editor, Self::on_active_editor_event)); + self.active_editor = Some(editor); + self.update_matches(false, cx); + return true; + } } + false } fn on_dismiss(&mut self, cx: &mut ViewContext) { @@ -172,13 +157,13 @@ impl Toolbar for FindBar { } } -impl FindBar { +impl SearchBar { fn new(settings: watch::Receiver, cx: &mut ViewContext) -> Self { let query_editor = cx.add_view(|cx| { Editor::auto_height( 2, settings.clone(), - Some(|theme| theme.find.editor.input.clone()), + Some(|theme| theme.search.editor.input.clone()), cx, ) }); @@ -191,9 +176,9 @@ impl FindBar { active_editor_subscription: None, active_match_index: None, editors_with_matches: Default::default(), - case_sensitive_mode: false, - whole_word_mode: false, - regex_mode: false, + case_sensitive: false, + whole_word: false, + regex: false, settings, pending_search: None, query_contains_error: false, @@ -210,27 +195,27 @@ impl FindBar { }); } - fn render_mode_button( + fn render_search_option( &self, icon: &str, - mode: SearchMode, + search_option: SearchOption, cx: &mut RenderContext, ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; - let is_active = self.is_mode_enabled(mode); - MouseEventHandler::new::(mode as usize, cx, |state, _| { + let theme = &self.settings.borrow().theme.search; + let is_active = self.is_search_option_enabled(search_option); + MouseEventHandler::new::(search_option as usize, cx, |state, _| { let style = match (is_active, state.hovered) { - (false, false) => &theme.mode_button, - (false, true) => &theme.hovered_mode_button, - (true, false) => &theme.active_mode_button, - (true, true) => &theme.active_hovered_mode_button, + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, }; Label::new(icon.to_string(), style.text.clone()) .contained() .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(ToggleMode(mode))) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(search_option))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -241,20 +226,20 @@ impl FindBar { direction: Direction, cx: &mut RenderContext, ) -> ElementBox { - let theme = &self.settings.borrow().theme.find; + let theme = &self.settings.borrow().theme.search; enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, _| { let style = if state.hovered { - &theme.hovered_mode_button + &theme.hovered_option_button } else { - &theme.mode_button + &theme.option_button }; Label::new(icon.to_string(), style.text.clone()) .contained() .with_style(style.container) .boxed() }) - .on_click(move |cx| cx.dispatch_action(GoToMatch(direction))) + .on_click(move |cx| cx.dispatch_action(SelectMatch(direction))) .with_cursor_style(CursorStyle::PointingHand) .boxed() } @@ -262,20 +247,20 @@ impl FindBar { fn deploy(workspace: &mut Workspace, Deploy(focus): &Deploy, cx: &mut ViewContext) { let settings = workspace.settings(); workspace.active_pane().update(cx, |pane, cx| { - pane.show_toolbar(cx, |cx| FindBar::new(settings, cx)); + pane.show_toolbar(cx, |cx| SearchBar::new(settings, cx)); - if let Some(find_bar) = pane + if let Some(search_bar) = pane .active_toolbar() .and_then(|toolbar| toolbar.downcast::()) { - find_bar.update(cx, |find_bar, _| find_bar.dismissed = false); + search_bar.update(cx, |search_bar, _| search_bar.dismissed = false); let editor = pane.active_item().unwrap().act_as::(cx).unwrap(); let display_map = editor .update(cx, |editor, cx| editor.snapshot(cx)) .display_snapshot; let selection = editor .read(cx) - .newest_selection::(&display_map.buffer_snapshot); + .newest_selection_with_snapshot::(&display_map.buffer_snapshot); let mut text: String; if selection.start == selection.end { @@ -295,22 +280,24 @@ impl FindBar { } if !text.is_empty() { - find_bar.update(cx, |find_bar, cx| find_bar.set_query(&text, cx)); + search_bar.update(cx, |search_bar, cx| search_bar.set_query(&text, cx)); } if *focus { - let query_editor = find_bar.read(cx).query_editor.clone(); + let query_editor = search_bar.read(cx).query_editor.clone(); query_editor.update(cx, |query_editor, cx| { query_editor.select_all(&editor::SelectAll, cx); }); - cx.focus(&find_bar); + cx.focus(&search_bar); } + } else { + cx.propagate_action(); } }); } fn dismiss(pane: &mut Pane, _: &Dismiss, cx: &mut ViewContext) { - if pane.toolbar::().is_some() { + if pane.toolbar::().is_some() { pane.dismiss_toolbar(cx); } } @@ -321,71 +308,55 @@ impl FindBar { } } - fn is_mode_enabled(&self, mode: SearchMode) -> bool { - match mode { - SearchMode::WholeWord => self.whole_word_mode, - SearchMode::CaseSensitive => self.case_sensitive_mode, - SearchMode::Regex => self.regex_mode, + 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_mode(&mut self, ToggleMode(mode): &ToggleMode, cx: &mut ViewContext) { - let value = match mode { - SearchMode::WholeWord => &mut self.whole_word_mode, - SearchMode::CaseSensitive => &mut self.case_sensitive_mode, - SearchMode::Regex => &mut self.regex_mode, + fn toggle_search_option( + &mut self, + ToggleSearchOption(search_option): &ToggleSearchOption, + 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(true, cx); cx.notify(); } - fn go_to_match(&mut self, GoToMatch(direction): &GoToMatch, cx: &mut ViewContext) { - if let Some(mut index) = self.active_match_index { + fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { if let Some(editor) = self.active_editor.as_ref() { editor.update(cx, |editor, cx| { - let newest_selection = editor.newest_anchor_selection().clone(); if let Some(ranges) = self.editors_with_matches.get(&cx.weak_handle()) { - let position = newest_selection.head(); - let buffer = editor.buffer().read(cx).read(cx); - if ranges[index].start.cmp(&position, &buffer).unwrap().is_gt() { - if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } - } else if ranges[index].end.cmp(&position, &buffer).unwrap().is_lt() { - if *direction == Direction::Next { - index = 0; - } - } else if *direction == Direction::Prev { - if index == 0 { - index = ranges.len() - 1; - } else { - index -= 1; - } - } else if *direction == Direction::Next { - if index == ranges.len() - 1 { - index = 0 - } else { - index += 1; - } - } - - let range_to_select = ranges[index].clone(); - drop(buffer); - editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + let new_index = match_index_for_direction( + ranges, + &editor.newest_anchor_selection().head(), + index, + direction, + &editor.buffer().read(cx).read(cx), + ); + editor.select_ranges( + [ranges[new_index].clone()], + Some(Autoscroll::Fit), + cx, + ); } }); } } } - fn go_to_match_on_pane(pane: &mut Pane, action: &GoToMatch, cx: &mut ViewContext) { - if let Some(find_bar) = pane.toolbar::() { - find_bar.update(cx, |find_bar, cx| find_bar.go_to_match(action, cx)); + fn select_match_on_pane(pane: &mut Pane, action: &SelectMatch, cx: &mut ViewContext) { + if let Some(search_bar) = pane.toolbar::() { + search_bar.update(cx, |search_bar, cx| search_bar.select_match(action, cx)); } } @@ -442,56 +413,81 @@ impl FindBar { editor.update(cx, |editor, cx| editor.clear_highlighted_ranges::(cx)); } else { let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); - let case_sensitive = self.case_sensitive_mode; - let whole_word = self.whole_word_mode; - let ranges = if self.regex_mode { - cx.background() - .spawn(regex_search(buffer, query, case_sensitive, whole_word)) + let query = if self.regex { + match SearchQuery::regex(query, self.whole_word, self.case_sensitive) { + Ok(query) => query, + Err(_) => { + self.query_contains_error = true; + cx.notify(); + return; + } + } } else { - cx.background().spawn(async move { - Ok(search(buffer, query, case_sensitive, whole_word).await) - }) + SearchQuery::text(query, self.whole_word, self.case_sensitive) }; + let ranges = cx.background().spawn(async move { + let mut ranges = Vec::new(); + if let Some((_, _, excerpt_buffer)) = buffer.as_singleton() { + ranges.extend( + query + .search(excerpt_buffer.as_rope()) + .await + .into_iter() + .map(|range| { + buffer.anchor_after(range.start) + ..buffer.anchor_before(range.end) + }), + ); + } else { + for excerpt in buffer.excerpt_boundaries_in_range(0..buffer.len()) { + let excerpt_range = excerpt.range.to_offset(&excerpt.buffer); + let rope = excerpt.buffer.as_rope().slice(excerpt_range.clone()); + ranges.extend(query.search(&rope).await.into_iter().map(|range| { + let start = excerpt + .buffer + .anchor_after(excerpt_range.start + range.start); + let end = excerpt + .buffer + .anchor_before(excerpt_range.start + range.end); + buffer.anchor_in_excerpt(excerpt.id.clone(), start) + ..buffer.anchor_in_excerpt(excerpt.id.clone(), end) + })); + } + } + ranges + }); + let editor = editor.downgrade(); - self.pending_search = Some(cx.spawn(|this, mut cx| async move { - match ranges.await { - Ok(ranges) => { - if let Some(editor) = editor.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.editors_with_matches - .insert(editor.downgrade(), ranges.clone()); - this.update_match_index(cx); - if !this.dismissed { - editor.update(cx, |editor, cx| { - let theme = &this.settings.borrow().theme.find; - - if select_closest_match { - if let Some(match_ix) = this.active_match_index { - editor.select_ranges( - [ranges[match_ix].clone()], - Some(Autoscroll::Fit), - cx, - ); - } - } - - editor.highlight_ranges::( - ranges, - theme.match_background, + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let ranges = ranges.await; + if let Some((this, editor)) = this.upgrade(&cx).zip(editor.upgrade(&cx)) { + this.update(&mut cx, |this, cx| { + this.editors_with_matches + .insert(editor.downgrade(), ranges.clone()); + this.update_match_index(cx); + if !this.dismissed { + editor.update(cx, |editor, cx| { + let theme = &this.settings.borrow().theme.search; + + if select_closest_match { + if let Some(match_ix) = this.active_match_index { + editor.select_ranges( + [ranges[match_ix].clone()], + Some(Autoscroll::Fit), cx, ); - }); + } } + + editor.highlight_ranges::( + ranges, + theme.match_background, + cx, + ); }); } - } - Err(_) => { - this.update(&mut cx, |this, cx| { - this.query_contains_error = true; - cx.notify(); - }); - } + }); } })); } @@ -499,136 +495,20 @@ impl FindBar { } fn update_match_index(&mut self, cx: &mut ViewContext) { - self.active_match_index = self.active_match_index(cx); - cx.notify(); - } - - fn active_match_index(&mut self, cx: &mut ViewContext) -> Option { - let editor = self.active_editor.as_ref()?; - let ranges = self.editors_with_matches.get(&editor.downgrade())?; - let editor = editor.read(cx); - let position = editor.newest_anchor_selection().head(); - if ranges.is_empty() { - None - } else { - let buffer = editor.buffer().read(cx).read(cx); - match ranges.binary_search_by(|probe| { - if probe.end.cmp(&position, &*buffer).unwrap().is_lt() { - Ordering::Less - } else if probe.start.cmp(&position, &*buffer).unwrap().is_gt() { - Ordering::Greater - } else { - Ordering::Equal - } - }) { - Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), - } - } - } -} - -const YIELD_INTERVAL: usize = 20000; - -async fn search( - buffer: MultiBufferSnapshot, - query: String, - case_sensitive: bool, - whole_word: bool, -) -> Vec> { - let mut ranges = Vec::new(); - - let search = AhoCorasickBuilder::new() - .auto_configure(&[&query]) - .ascii_case_insensitive(!case_sensitive) - .build(&[&query]); - for (ix, mat) in search - .stream_find_iter(buffer.bytes_in_range(0..buffer.len())) - .enumerate() - { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - let mat = mat.unwrap(); - - if whole_word { - let prev_kind = buffer.reversed_chars_at(mat.start()).next().map(char_kind); - let start_kind = char_kind(buffer.chars_at(mat.start()).next().unwrap()); - let end_kind = char_kind(buffer.reversed_chars_at(mat.end()).next().unwrap()); - let next_kind = buffer.chars_at(mat.end()).next().map(char_kind); - if Some(start_kind) == prev_kind || Some(end_kind) == next_kind { - continue; - } - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - - ranges -} - -async fn regex_search( - buffer: MultiBufferSnapshot, - mut query: String, - case_sensitive: bool, - whole_word: bool, -) -> Result>> { - if whole_word { - let mut word_query = String::new(); - word_query.push_str("\\b"); - word_query.push_str(&query); - word_query.push_str("\\b"); - query = word_query; - } - - let mut ranges = Vec::new(); - - if query.contains("\n") || query.contains("\\n") { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .multi_line(true) - .build()?; - for (ix, mat) in regex.find_iter(&buffer.text()).enumerate() { - if (ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - ranges.push(buffer.anchor_after(mat.start())..buffer.anchor_before(mat.end())); - } - } else { - let regex = RegexBuilder::new(&query) - .case_insensitive(!case_sensitive) - .build()?; - - let mut line = String::new(); - let mut line_offset = 0; - for (chunk_ix, chunk) in buffer - .chunks(0..buffer.len(), false) - .map(|c| c.text) - .chain(["\n"]) - .enumerate() - { - if (chunk_ix + 1) % YIELD_INTERVAL == 0 { - yield_now().await; - } - - for (newline_ix, text) in chunk.split('\n').enumerate() { - if newline_ix > 0 { - for mat in regex.find_iter(&line) { - let start = line_offset + mat.start(); - let end = line_offset + mat.end(); - ranges.push(buffer.anchor_after(start)..buffer.anchor_before(end)); - } - - line_offset += line.len() + 1; - line.clear(); - } - line.push_str(text); - } + let new_index = self.active_editor.as_ref().and_then(|editor| { + let ranges = self.editors_with_matches.get(&editor.downgrade())?; + let editor = editor.read(cx); + active_match_index( + &ranges, + &editor.newest_anchor_selection().head(), + &editor.buffer().read(cx).read(cx), + ) + }); + if new_index != self.active_match_index { + self.active_match_index = new_index; + cx.notify(); } } - - Ok(ranges) } #[cfg(test)] @@ -640,10 +520,10 @@ mod tests { use unindent::Unindent as _; #[gpui::test] - async fn test_find_simple(mut cx: TestAppContext) { + async fn test_search_simple(mut cx: TestAppContext) { let fonts = cx.font_cache(); let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); - theme.find.match_background = Color::red(); + theme.search.match_background = Color::red(); let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); let settings = watch::channel_with(settings).1; @@ -663,16 +543,16 @@ mod tests { Editor::for_buffer(buffer.clone(), None, settings.clone(), cx) }); - let find_bar = cx.add_view(Default::default(), |cx| { - let mut find_bar = FindBar::new(settings, cx); - find_bar.active_item_changed(Some(Box::new(editor.clone())), cx); - find_bar + let search_bar = cx.add_view(Default::default(), |cx| { + let mut search_bar = SearchBar::new(settings, cx); + search_bar.active_item_changed(Some(Box::new(editor.clone())), cx); + search_bar }); // Search for a string that appears with different casing. // By default, search is case-insensitive. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("us", cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.set_query("us", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -692,8 +572,8 @@ mod tests { }); // Switch to a case sensitive search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::CaseSensitive), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::CaseSensitive), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -708,8 +588,8 @@ mod tests { // Search for a string that appears both as a whole word and // within other words. By default, all results are found. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.set_query("or", cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.set_query("or", cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -749,8 +629,8 @@ mod tests { }); // Switch to a whole word search. - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.toggle_mode(&ToggleMode(SearchMode::WholeWord), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.toggle_search_option(&ToggleSearchOption(SearchOption::WholeWord), cx); }); editor.next_notification(&cx).await; editor.update(&mut cx, |editor, cx| { @@ -776,82 +656,82 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); - find_bar.update(&mut cx, |find_bar, cx| { - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor in between matches and ensure that going to the previous match selects @@ -859,16 +739,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor in between matches and ensure that going to the next match selects the @@ -876,16 +756,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(1)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(1)); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 11)..DisplayPoint::new(3, 13)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(1)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(1)); }); // Park the cursor after the last match and ensure that going to the previous match selects @@ -893,16 +773,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); // Park the cursor after the last match and ensure that going to the next match selects the @@ -910,16 +790,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(3, 60)..DisplayPoint::new(3, 60)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(2)); - find_bar.go_to_match(&GoToMatch(Direction::Next), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(2)); + search_bar.select_match(&SelectMatch(Direction::Next), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(0, 41)..DisplayPoint::new(0, 43)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(0)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(0)); }); // Park the cursor before the first match and ensure that going to the previous match @@ -927,16 +807,16 @@ mod tests { editor.update(&mut cx, |editor, cx| { editor.select_display_ranges(&[DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0)], cx); }); - find_bar.update(&mut cx, |find_bar, cx| { - assert_eq!(find_bar.active_match_index, Some(0)); - find_bar.go_to_match(&GoToMatch(Direction::Prev), cx); + search_bar.update(&mut cx, |search_bar, cx| { + assert_eq!(search_bar.active_match_index, Some(0)); + search_bar.select_match(&SelectMatch(Direction::Prev), cx); assert_eq!( editor.update(cx, |editor, cx| editor.selected_display_ranges(cx)), [DisplayPoint::new(3, 56)..DisplayPoint::new(3, 58)] ); }); - find_bar.read_with(&cx, |find_bar, _| { - assert_eq!(find_bar.active_match_index, Some(2)); + search_bar.read_with(&cx, |search_bar, _| { + assert_eq!(search_bar.active_match_index, Some(2)); }); } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs new file mode 100644 index 0000000000000000000000000000000000000000..78031dd951ccf17b9cd25a12793d546588cf8f74 --- /dev/null +++ b/crates/search/src/project_search.rs @@ -0,0 +1,848 @@ +use crate::{ + active_match_index, match_index_for_direction, Direction, SearchOption, SelectMatch, + ToggleSearchOption, +}; +use collections::HashMap; +use editor::{Anchor, Autoscroll, Editor, MultiBuffer, SelectAll}; +use gpui::{ + action, elements::*, keymap::Binding, platform::CursorStyle, AppContext, ElementBox, Entity, + ModelContext, ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, WeakModelHandle, +}; +use postage::watch; +use project::{search::SearchQuery, Project}; +use std::{ + any::{Any, TypeId}, + ops::Range, + path::PathBuf, +}; +use util::ResultExt as _; +use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; + +action!(Deploy); +action!(Search); +action!(SearchInNew); +action!(ToggleFocus); + +const MAX_TAB_TITLE_LEN: usize = 24; + +#[derive(Default)] +struct ActiveSearches(HashMap, WeakModelHandle>); + +pub fn init(cx: &mut MutableAppContext) { + cx.add_app_state(ActiveSearches::default()); + cx.add_bindings([ + Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), + Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")), + Binding::new("cmd-shift-F", Deploy, Some("Workspace")), + Binding::new("enter", Search, Some("ProjectSearchView")), + Binding::new("cmd-enter", SearchInNew, Some("ProjectSearchView")), + Binding::new( + "cmd-g", + SelectMatch(Direction::Next), + Some("ProjectSearchView"), + ), + Binding::new( + "cmd-shift-G", + SelectMatch(Direction::Prev), + Some("ProjectSearchView"), + ), + ]); + cx.add_action(ProjectSearchView::deploy); + cx.add_action(ProjectSearchView::search); + cx.add_action(ProjectSearchView::search_in_new); + cx.add_action(ProjectSearchView::toggle_search_option); + cx.add_action(ProjectSearchView::select_match); + cx.add_action(ProjectSearchView::toggle_focus); + cx.capture_action(ProjectSearchView::tab); +} + +struct ProjectSearch { + project: ModelHandle, + excerpts: ModelHandle, + pending_search: Option>>, + match_ranges: Vec>, + active_query: Option, +} + +struct ProjectSearchView { + model: ModelHandle, + query_editor: ViewHandle, + results_editor: ViewHandle, + case_sensitive: bool, + whole_word: bool, + regex: bool, + query_contains_error: bool, + active_match_index: Option, + settings: watch::Receiver, +} + +impl Entity for ProjectSearch { + type Event = (); +} + +impl ProjectSearch { + fn new(project: ModelHandle, cx: &mut ModelContext) -> Self { + let replica_id = project.read(cx).replica_id(); + Self { + project, + excerpts: cx.add_model(|_| MultiBuffer::new(replica_id)), + pending_search: Default::default(), + match_ranges: Default::default(), + active_query: None, + } + } + + fn clone(&self, cx: &mut ModelContext) -> ModelHandle { + cx.add_model(|cx| Self { + project: self.project.clone(), + excerpts: self + .excerpts + .update(cx, |excerpts, cx| cx.add_model(|cx| excerpts.clone(cx))), + pending_search: Default::default(), + match_ranges: self.match_ranges.clone(), + active_query: self.active_query.clone(), + }) + } + + fn search(&mut self, query: SearchQuery, cx: &mut ModelContext) { + let search = self + .project + .update(cx, |project, cx| project.search(query.clone(), cx)); + self.active_query = Some(query); + self.match_ranges.clear(); + self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { + let matches = search.await.log_err()?; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.match_ranges.clear(); + let mut matches = matches.into_iter().collect::>(); + matches + .sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.clear(cx); + for (buffer, buffer_matches) in matches { + let ranges_to_highlight = excerpts.push_excerpts_with_context_lines( + buffer, + buffer_matches.clone(), + 1, + cx, + ); + this.match_ranges.extend(ranges_to_highlight); + } + }); + this.pending_search.take(); + cx.notify(); + }); + } + None + })); + cx.notify(); + } +} + +impl Item for ProjectSearch { + type View = ProjectSearchView; + + fn build_view( + model: ModelHandle, + workspace: &Workspace, + nav_history: ItemNavHistory, + cx: &mut gpui::ViewContext, + ) -> Self::View { + ProjectSearchView::new(model, Some(nav_history), workspace.settings(), cx) + } + + fn project_path(&self) -> Option { + None + } +} + +enum ViewEvent { + UpdateTab, +} + +impl Entity for ProjectSearchView { + type Event = ViewEvent; +} + +impl View for ProjectSearchView { + fn ui_name() -> &'static str { + "ProjectSearchView" + } + + fn render(&mut self, cx: &mut RenderContext) -> ElementBox { + let model = &self.model.read(cx); + let results = if model.match_ranges.is_empty() { + let theme = &self.settings.borrow().theme; + let text = if self.query_editor.read(cx).text(cx).is_empty() { + "" + } else if model.pending_search.is_some() { + "Searching..." + } else { + "No results" + }; + Label::new(text.to_string(), theme.search.results_status.clone()) + .aligned() + .contained() + .with_background_color(theme.editor.background) + .flexible(1., true) + .boxed() + } else { + ChildView::new(&self.results_editor) + .flexible(1., true) + .boxed() + }; + + Flex::column() + .with_child(self.render_query_editor(cx)) + .with_child(results) + .boxed() + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.update_app_state(|state: &mut ActiveSearches, cx| { + state.0.insert( + self.model.read(cx).project.downgrade(), + self.model.downgrade(), + ) + }); + + if self.model.read(cx).match_ranges.is_empty() { + cx.focus(&self.query_editor); + } else { + self.focus_results_editor(cx); + } + } +} + +impl ItemView for ProjectSearchView { + fn act_as_type( + &self, + type_id: TypeId, + self_handle: &ViewHandle, + _: &gpui::AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.into()) + } else if type_id == TypeId::of::() { + Some((&self.results_editor).into()) + } else { + None + } + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn item(&self, _: &gpui::AppContext) -> Box { + Box::new(self.model.clone()) + } + + fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { + let settings = self.settings.borrow(); + let search_theme = &settings.theme.search; + Flex::row() + .with_child( + Svg::new("icons/magnifier.svg") + .with_color(tab_theme.label.text.color) + .constrained() + .with_width(search_theme.tab_icon_width) + .aligned() + .boxed(), + ) + .with_children(self.model.read(cx).active_query.as_ref().map(|query| { + let query_text = if query.as_str().len() > MAX_TAB_TITLE_LEN { + query.as_str()[..MAX_TAB_TITLE_LEN].to_string() + "…" + } else { + query.as_str().to_string() + }; + + Label::new(query_text, tab_theme.label.clone()) + .aligned() + .contained() + .with_margin_left(search_theme.tab_icon_spacing) + .boxed() + })) + .boxed() + } + + fn project_path(&self, _: &gpui::AppContext) -> Option { + None + } + + fn can_save(&self, _: &gpui::AppContext) -> bool { + true + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.results_editor.read(cx).has_conflict(cx) + } + + fn save( + &mut self, + project: ModelHandle, + cx: &mut ViewContext, + ) -> Task> { + self.results_editor + .update(cx, |editor, cx| editor.save(project, cx)) + } + + fn can_save_as(&self, _: &gpui::AppContext) -> bool { + false + } + + fn save_as( + &mut self, + _: ModelHandle, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!("save_as should not have been called") + } + + fn clone_on_split( + &self, + nav_history: ItemNavHistory, + cx: &mut ViewContext, + ) -> Option + where + Self: Sized, + { + let model = self.model.update(cx, |model, cx| model.clone(cx)); + Some(Self::new( + model, + Some(nav_history), + self.settings.clone(), + cx, + )) + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + self.results_editor + .update(cx, |editor, cx| editor.navigate(data, cx)); + } + + fn should_update_tab_on_event(event: &ViewEvent) -> bool { + matches!(event, ViewEvent::UpdateTab) + } +} + +impl ProjectSearchView { + fn new( + model: ModelHandle, + nav_history: Option, + settings: watch::Receiver, + cx: &mut ViewContext, + ) -> Self { + 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 model = model.read(cx); + project = model.project.clone(); + 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(); + } + } + cx.observe(&model, |this, _, cx| this.model_changed(true, cx)) + .detach(); + + let query_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + settings.clone(), + Some(|theme| theme.search.editor.input.clone()), + cx, + ); + editor.set_text(query_text, cx); + editor + }); + + let results_editor = cx.add_view(|cx| { + let mut editor = Editor::for_buffer(excerpts, Some(project), settings.clone(), cx); + editor.set_searchable(false); + editor.set_nav_history(nav_history); + editor + }); + cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) + .detach(); + cx.subscribe(&results_editor, |this, _, event, cx| { + if matches!(event, editor::Event::SelectionsChanged) { + this.update_match_index(cx); + } + }) + .detach(); + + let mut this = ProjectSearchView { + model, + query_editor, + results_editor, + case_sensitive, + whole_word, + regex, + query_contains_error: false, + active_match_index: None, + settings, + }; + this.model_changed(false, cx); + this + } + + // 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(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + // Clean up entries for dropped projects + cx.update_app_state(|state: &mut ActiveSearches, cx| { + state.0.retain(|project, _| project.is_upgradable(cx)) + }); + + let active_search = cx + .app_state::() + .0 + .get(&workspace.project().downgrade()); + + let existing = active_search + .and_then(|active_search| { + workspace + .items_of_type::(cx) + .find(|search| search == active_search) + }) + .or_else(|| workspace.item_of_type::(cx)); + + if let Some(existing) = existing { + workspace.activate_item(&existing, cx); + } else { + let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); + workspace.open_item(model, cx); + } + } + + fn search(&mut self, _: &Search, cx: &mut ViewContext) { + if let Some(query) = self.build_search_query(cx) { + self.model.update(cx, |model, cx| model.search(query, cx)); + } + } + + fn search_in_new(workspace: &mut Workspace, _: &SearchInNew, cx: &mut ViewContext) { + if let Some(search_view) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let new_query = search_view.update(cx, |search_view, cx| { + let new_query = search_view.build_search_query(cx); + if new_query.is_some() { + if let Some(old_query) = search_view.model.read(cx).active_query.clone() { + 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(); + } + } + new_query + }); + if let Some(new_query) = new_query { + let model = cx.add_model(|cx| { + let mut model = ProjectSearch::new(workspace.project().clone(), cx); + model.search(new_query, cx); + model + }); + workspace.open_item(model, cx); + } + } + } + + fn build_search_query(&mut self, cx: &mut ViewContext) -> Option { + let text = self.query_editor.read(cx).text(cx); + if self.regex { + match SearchQuery::regex(text, self.whole_word, self.case_sensitive) { + Ok(query) => Some(query), + Err(_) => { + self.query_contains_error = true; + cx.notify(); + None + } + } + } else { + Some(SearchQuery::text( + text, + self.whole_word, + self.case_sensitive, + )) + } + } + + fn toggle_search_option( + &mut self, + ToggleSearchOption(option): &ToggleSearchOption, + cx: &mut ViewContext, + ) { + let value = match option { + SearchOption::WholeWord => &mut self.whole_word, + SearchOption::CaseSensitive => &mut self.case_sensitive, + SearchOption::Regex => &mut self.regex, + }; + *value = !*value; + self.search(&Search, cx); + cx.notify(); + } + + fn select_match(&mut self, &SelectMatch(direction): &SelectMatch, cx: &mut ViewContext) { + if let Some(index) = self.active_match_index { + let model = self.model.read(cx); + let results_editor = self.results_editor.read(cx); + let new_index = match_index_for_direction( + &model.match_ranges, + &results_editor.newest_anchor_selection().head(), + index, + direction, + &results_editor.buffer().read(cx).read(cx), + ); + let range_to_select = model.match_ranges[new_index].clone(); + self.results_editor.update(cx, |editor, cx| { + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); + }); + } + } + + fn toggle_focus(&mut self, _: &ToggleFocus, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + if !self.model.read(cx).match_ranges.is_empty() { + self.focus_results_editor(cx); + } + } else { + self.focus_query_editor(cx); + } + } + + fn tab(&mut self, _: &editor::Tab, cx: &mut ViewContext) { + if self.query_editor.is_focused(cx) { + if !self.model.read(cx).match_ranges.is_empty() { + self.focus_results_editor(cx); + } + } else { + cx.propagate_action() + } + } + + fn focus_query_editor(&self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + query_editor.select_all(&SelectAll, cx); + }); + cx.focus(&self.query_editor); + } + + fn focus_results_editor(&self, cx: &mut ViewContext) { + self.query_editor.update(cx, |query_editor, cx| { + let cursor = query_editor.newest_anchor_selection().head(); + query_editor.select_ranges([cursor.clone()..cursor], None, cx); + }); + cx.focus(&self.results_editor); + } + + fn model_changed(&mut self, reset_selections: bool, cx: &mut ViewContext) { + let match_ranges = self.model.read(cx).match_ranges.clone(); + if match_ranges.is_empty() { + self.active_match_index = None; + } else { + let theme = &self.settings.borrow().theme.search; + self.results_editor.update(cx, |editor, cx| { + if reset_selections { + editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx); + } + editor.highlight_ranges::(match_ranges, theme.match_background, cx); + }); + if self.query_editor.is_focused(cx) { + self.focus_results_editor(cx); + } + } + + cx.emit(ViewEvent::UpdateTab); + cx.notify(); + } + + fn update_match_index(&mut self, cx: &mut ViewContext) { + let results_editor = self.results_editor.read(cx); + let new_index = active_match_index( + &self.model.read(cx).match_ranges, + &results_editor.newest_anchor_selection().head(), + &results_editor.buffer().read(cx).read(cx), + ); + if self.active_match_index != new_index { + self.active_match_index = new_index; + cx.notify(); + } + } + + fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { + let theme = &self.settings.borrow().theme; + let editor_container = if self.query_contains_error { + theme.search.invalid_editor + } else { + theme.search.editor.input.container + }; + Flex::row() + .with_child( + ChildView::new(&self.query_editor) + .contained() + .with_style(editor_container) + .aligned() + .constrained() + .with_max_width(theme.search.editor.max_width) + .boxed(), + ) + .with_child( + Flex::row() + .with_child(self.render_option_button("Case", SearchOption::CaseSensitive, cx)) + .with_child(self.render_option_button("Word", SearchOption::WholeWord, cx)) + .with_child(self.render_option_button("Regex", SearchOption::Regex, cx)) + .contained() + .with_style(theme.search.option_button_group) + .aligned() + .boxed(), + ) + .with_children({ + self.active_match_index.into_iter().flat_map(|match_ix| { + [ + Flex::row() + .with_child(self.render_nav_button("<", Direction::Prev, cx)) + .with_child(self.render_nav_button(">", Direction::Next, cx)) + .aligned() + .boxed(), + Label::new( + format!( + "{}/{}", + match_ix + 1, + self.model.read(cx).match_ranges.len() + ), + theme.search.match_index.text.clone(), + ) + .contained() + .with_style(theme.search.match_index.container) + .aligned() + .boxed(), + ] + }) + }) + .contained() + .with_style(theme.search.container) + .constrained() + .with_height(theme.workspace.toolbar.height) + .named("project search") + } + + fn render_option_button( + &self, + icon: &str, + option: SearchOption, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.search; + let is_active = self.is_option_enabled(option); + MouseEventHandler::new::(option as usize, cx, |state, _| { + let style = match (is_active, state.hovered) { + (false, false) => &theme.option_button, + (false, true) => &theme.hovered_option_button, + (true, false) => &theme.active_option_button, + (true, true) => &theme.active_hovered_option_button, + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(ToggleSearchOption(option))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } + + fn is_option_enabled(&self, option: SearchOption) -> bool { + match option { + SearchOption::WholeWord => self.whole_word, + SearchOption::CaseSensitive => self.case_sensitive, + SearchOption::Regex => self.regex, + } + } + + fn render_nav_button( + &self, + icon: &str, + direction: Direction, + cx: &mut RenderContext, + ) -> ElementBox { + let theme = &self.settings.borrow().theme.search; + enum NavButton {} + MouseEventHandler::new::(direction as usize, cx, |state, _| { + let style = if state.hovered { + &theme.hovered_option_button + } else { + &theme.option_button + }; + Label::new(icon.to_string(), style.text.clone()) + .contained() + .with_style(style.container) + .boxed() + }) + .on_click(move |cx| cx.dispatch_action(SelectMatch(direction))) + .with_cursor_style(CursorStyle::PointingHand) + .boxed() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::DisplayPoint; + use gpui::{color::Color, TestAppContext}; + use project::FakeFs; + use serde_json::json; + use std::sync::Arc; + + #[gpui::test] + async fn test_project_search(mut cx: TestAppContext) { + let fonts = cx.font_cache(); + let mut theme = gpui::fonts::with_font_cache(fonts.clone(), || theme::Theme::default()); + theme.search.match_background = Color::red(); + let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); + let settings = watch::channel_with(settings).1; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "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(), &mut cx); + let (tree, _) = project + .update(&mut cx, |project, cx| { + project.find_or_create_local_worktree("/dir", false, cx) + }) + .await + .unwrap(); + cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete()) + .await; + + let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); + let search_view = cx.add_view(Default::default(), |cx| { + ProjectSearchView::new(search.clone(), None, settings, cx) + }); + + search_view.update(&mut cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(&Search, cx); + }); + search_view.next_notification(&cx).await; + search_view.update(&mut cx, |search_view, cx| { + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.display_text(cx)), + "\n\nconst THREE: usize = one::ONE + two::TWO;\n\n\nconst TWO: usize = one::ONE + one::ONE;" + ); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.all_highlighted_ranges(cx)), + &[ + ( + DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35), + Color::red() + ), + ( + DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40), + Color::red() + ), + ( + DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9), + Color::red() + ) + ] + ); + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(&SelectMatch(Direction::Next), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(0)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 32)..DisplayPoint::new(2, 35)] + ); + search_view.select_match(&SelectMatch(Direction::Prev), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(2)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(5, 6)..DisplayPoint::new(5, 9)] + ); + search_view.select_match(&SelectMatch(Direction::Prev), cx); + }); + + search_view.update(&mut cx, |search_view, cx| { + assert_eq!(search_view.active_match_index, Some(1)); + assert_eq!( + search_view + .results_editor + .update(cx, |editor, cx| editor.selected_display_ranges(cx)), + [DisplayPoint::new(2, 37)..DisplayPoint::new(2, 40)] + ); + }); + } +} diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1e335a0354ffdaaf903ba30c3d32f5d24c90093 --- /dev/null +++ b/crates/search/src/search.rs @@ -0,0 +1,88 @@ +use std::{ + cmp::{self, Ordering}, + ops::Range, +}; + +use editor::{Anchor, MultiBufferSnapshot}; +use gpui::{action, MutableAppContext}; + +mod buffer_search; +mod project_search; + +pub fn init(cx: &mut MutableAppContext) { + buffer_search::init(cx); + project_search::init(cx); +} + +action!(ToggleSearchOption, SearchOption); +action!(SelectMatch, Direction); + +#[derive(Clone, Copy)] +pub enum SearchOption { + WholeWord, + CaseSensitive, + Regex, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum Direction { + Prev, + Next, +} + +pub(crate) fn active_match_index( + ranges: &[Range], + cursor: &Anchor, + buffer: &MultiBufferSnapshot, +) -> Option { + if ranges.is_empty() { + None + } else { + match ranges.binary_search_by(|probe| { + if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { + Ordering::Less + } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { + Ordering::Greater + } else { + Ordering::Equal + } + }) { + Ok(i) | Err(i) => Some(cmp::min(i, ranges.len() - 1)), + } + } +} + +pub(crate) fn match_index_for_direction( + ranges: &[Range], + cursor: &Anchor, + mut index: usize, + direction: Direction, + buffer: &MultiBufferSnapshot, +) -> usize { + if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { + if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } + } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { + if direction == Direction::Next { + index = 0; + } + } else if direction == Direction::Prev { + if index == 0 { + index = ranges.len() - 1; + } else { + index -= 1; + } + } else if direction == Direction::Next { + if index == ranges.len() - 1 { + index = 0 + } else { + index += 1; + } + }; + index +} diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 35617c048f4a4ce80962b96339ac14207ea8e9c6..a9ebdceca09f403a81f8f21cc9a5562faf152329 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -12,7 +12,7 @@ use collections::{HashMap, HashSet}; use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard}; use rpc::{ - proto::{self, AnyTypedEnvelope, EnvelopedMessage, RequestMessage}, + proto::{self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, RequestMessage}, Connection, ConnectionId, Peer, TypedEnvelope, }; use sha1::{Digest as _, Sha1}; @@ -77,25 +77,28 @@ impl Server { .add_message_handler(Server::update_diagnostic_summary) .add_message_handler(Server::disk_based_diagnostics_updating) .add_message_handler(Server::disk_based_diagnostics_updated) - .add_request_handler(Server::get_definition) - .add_request_handler(Server::get_references) - .add_request_handler(Server::get_document_highlights) - .add_request_handler(Server::get_project_symbols) - .add_request_handler(Server::open_buffer_for_symbol) - .add_request_handler(Server::open_buffer) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler( + Server::forward_project_request::, + ) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) + .add_request_handler(Server::forward_project_request::) .add_message_handler(Server::close_buffer) .add_request_handler(Server::update_buffer) .add_message_handler(Server::update_buffer_file) .add_message_handler(Server::buffer_reloaded) .add_message_handler(Server::buffer_saved) .add_request_handler(Server::save_buffer) - .add_request_handler(Server::format_buffers) - .add_request_handler(Server::get_completions) - .add_request_handler(Server::apply_additional_edits_for_completion) - .add_request_handler(Server::get_code_actions) - .add_request_handler(Server::apply_code_action) - .add_request_handler(Server::prepare_rename) - .add_request_handler(Server::perform_rename) .add_request_handler(Server::get_channels) .add_request_handler(Server::get_users) .add_request_handler(Server::join_channel) @@ -542,83 +545,16 @@ impl Server { Ok(()) } - async fn get_definition( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_references( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_document_highlights( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn get_project_symbols( + async fn forward_project_request( self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn open_buffer_for_symbol( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { + request: TypedEnvelope, + ) -> tide::Result + where + T: EntityMessage + RequestMessage, + { let host_connection_id = self .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host_connection_id, request.payload) - .await?) - } - - async fn open_buffer( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host_connection_id = self - .state() - .read_project(request.payload.project_id, request.sender_id)? + .read_project(request.payload.remote_entity_id(), request.sender_id)? .host_connection_id; Ok(self .peer @@ -665,104 +601,6 @@ impl Server { Ok(response) } - async fn format_buffers( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn get_completions( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn apply_additional_edits_for_completion( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn get_code_actions( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn apply_code_action( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn prepare_rename( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - - async fn perform_rename( - self: Arc, - request: TypedEnvelope, - ) -> tide::Result { - let host = self - .state() - .read_project(request.payload.project_id, request.sender_id)? - .host_connection_id; - Ok(self - .peer - .forward_request(request.sender_id, host, request.payload.clone()) - .await?) - } - async fn update_buffer( self: Arc, request: TypedEnvelope, @@ -1186,7 +1024,7 @@ mod tests { LanguageConfig, LanguageRegistry, LanguageServerConfig, Point, ToLspPosition, }, lsp, - project::{DiagnosticSummary, Project, ProjectPath}, + project::{search::SearchQuery, DiagnosticSummary, Project, ProjectPath}, workspace::{Settings, Workspace, WorkspaceParams}, }; @@ -1327,14 +1165,6 @@ mod tests { // .condition(&cx_a, |buffer, _| buffer.selection_sets().count() == 0) // .await; - // Close the buffer as client A, see that the buffer is closed. - cx_a.update(move |_| drop(buffer_a)); - project_a - .condition(&cx_a, |project, cx| { - !project.has_open_buffer((worktree_id, "b.txt"), cx) - }) - .await; - // Dropping the client B's project removes client B from client A's collaborators. cx_b.update(move |_| drop(project_b)); project_a @@ -2697,14 +2527,6 @@ mod tests { ); }); assert_eq!(definitions_1[0].buffer, definitions_2[0].buffer); - - cx_b.update(|_| { - drop(definitions_1); - drop(definitions_2); - }); - project_b - .condition(&cx_b, |proj, cx| proj.worktrees(cx).count() == 1) - .await; } #[gpui::test(iterations = 10)] @@ -2843,6 +2665,118 @@ mod tests { }); } + #[gpui::test(iterations = 10)] + async fn test_project_search(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { + cx_a.foreground().forbid_parking(); + let lang_registry = Arc::new(LanguageRegistry::new()); + let fs = FakeFs::new(cx_a.background()); + fs.insert_tree( + "/root-1", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a": "hello world", + "b": "goodnight moon", + "c": "a world of goo", + "d": "world champion of clown world", + }), + ) + .await; + fs.insert_tree( + "/root-2", + json!({ + "e": "disney world is fun", + }), + ) + .await; + + // Connect to a server as 2 clients. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let client_a = server.create_client(&mut cx_a, "user_a").await; + let client_b = server.create_client(&mut cx_b, "user_b").await; + + // Share a project as client A + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let project_id = project_a.update(&mut cx_a, |p, _| p.next_remote_id()).await; + + let (worktree_1, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/root-1", false, cx) + }) + .await + .unwrap(); + worktree_1 + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let (worktree_2, _) = project_a + .update(&mut cx_a, |p, cx| { + p.find_or_create_local_worktree("/root-2", false, cx) + }) + .await + .unwrap(); + worktree_2 + .read_with(&cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + + eprintln!("sharing"); + + project_a + .update(&mut cx_a, |p, cx| p.share(cx)) + .await + .unwrap(); + + // Join the worktree as client B. + let project_b = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + + let results = project_b + .update(&mut cx_b, |project, cx| { + project.search(SearchQuery::text("world", false, false), cx) + }) + .await + .unwrap(); + + let mut ranges_by_path = results + .into_iter() + .map(|(buffer, ranges)| { + buffer.read_with(&cx_b, |buffer, cx| { + let path = buffer.file().unwrap().full_path(cx); + let offset_ranges = ranges + .into_iter() + .map(|range| range.to_offset(buffer)) + .collect::>(); + (path, offset_ranges) + }) + }) + .collect::>(); + ranges_by_path.sort_by_key(|(path, _)| path.clone()); + + assert_eq!( + ranges_by_path, + &[ + (PathBuf::from("root-1/a"), vec![6..11]), + (PathBuf::from("root-1/c"), vec![2..7]), + (PathBuf::from("root-1/d"), vec![0..5, 24..29]), + (PathBuf::from("root-2/e"), vec![7..12]), + ] + ); + } + #[gpui::test(iterations = 10)] async fn test_document_highlights(mut cx_a: TestAppContext, mut cx_b: TestAppContext) { cx_a.foreground().forbid_parking(); @@ -4418,23 +4352,21 @@ mod tests { .project .as_ref() .unwrap() - .read_with(guest_cx, |project, _| { + .read_with(guest_cx, |project, cx| { assert!( - !project.has_buffered_operations(), - "guest {} has buffered operations ", + !project.has_deferred_operations(cx), + "guest {} has deferred operations", guest_id, ); }); for guest_buffer in &guest_client.buffers { let buffer_id = guest_buffer.read_with(guest_cx, |buffer, _| buffer.remote_id()); - let host_buffer = host_project.read_with(&host_cx, |project, _| { - project - .shared_buffer(guest_client.peer_id, buffer_id) - .expect(&format!( - "host doest not have buffer for guest:{}, peer:{}, id:{}", - guest_id, guest_client.peer_id, buffer_id - )) + let host_buffer = host_project.read_with(&host_cx, |project, cx| { + project.buffer_for_id(buffer_id, cx).expect(&format!( + "host does not have buffer for guest:{}, peer:{}, id:{}", + guest_id, guest_client.peer_id, buffer_id + )) }); assert_eq!( guest_buffer.read_with(guest_cx, |buffer, _| buffer.text()), @@ -4829,8 +4761,9 @@ mod tests { } else { buffer.update(&mut cx, |buffer, cx| { log::info!( - "Host: updating buffer {:?}", - buffer.file().unwrap().full_path(cx) + "Host: updating buffer {:?} ({})", + buffer.file().unwrap().full_path(cx), + buffer.remote_id() ); buffer.randomly_edit(&mut *rng.lock(), 5, cx) }); @@ -4917,9 +4850,19 @@ mod tests { project_path.1 ); let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(project_path, cx)) + .update(&mut cx, |project, cx| { + project.open_buffer(project_path.clone(), cx) + }) .await .unwrap(); + log::info!( + "Guest {}: path in worktree {:?} {:?} {:?} opened with buffer id {:?}", + guest_id, + project_path.0, + worktree_root_name, + project_path.1, + buffer.read_with(&cx, |buffer, _| buffer.remote_id()) + ); self.buffers.insert(buffer.clone()); buffer } else { @@ -5008,7 +4951,7 @@ mod tests { save.await; } } - 40..=45 => { + 40..=44 => { let prepare_rename = project.update(&mut cx, |project, cx| { log::info!( "Guest {}: preparing rename for buffer {:?}", @@ -5028,10 +4971,10 @@ mod tests { prepare_rename.await; } } - 46..=49 => { + 45..=49 => { let definitions = project.update(&mut cx, |project, cx| { log::info!( - "Guest {}: requesting defintions for buffer {:?}", + "Guest {}: requesting definitions for buffer {:?}", guest_id, buffer.read(cx).file().unwrap().full_path(cx) ); @@ -5049,7 +4992,7 @@ mod tests { .extend(definitions.await.into_iter().map(|loc| loc.buffer)); } } - 50..=55 => { + 50..=54 => { let highlights = project.update(&mut cx, |project, cx| { log::info!( "Guest {}: requesting highlights for buffer {:?}", @@ -5069,6 +5012,22 @@ mod tests { highlights.await; } } + 55..=59 => { + let search = project.update(&mut cx, |project, cx| { + let query = rng.lock().gen_range('a'..='z'); + log::info!("Guest {}: project-wide search {:?}", guest_id, query); + project.search(SearchQuery::text(query, false, false), cx) + }); + let search = cx + .background() + .spawn(async move { search.await.expect("search request failed") }); + if rng.lock().gen_bool(0.3) { + log::info!("Guest {}: detaching search request", guest_id); + search.detach(); + } else { + self.buffers.extend(search.await.into_keys()); + } + } _ => { buffer.update(&mut cx, |buffer, cx| { log::info!( diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index cbb6f7f6f5270931d5cba49e754fd79e5f0defe2..fab2aa5251979da3d20a1e00caa7d0df17d75159 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -34,13 +34,13 @@ where stack: ArrayVec::new(), position: D::default(), did_seek: false, - at_end: false, + at_end: tree.is_empty(), } } fn reset(&mut self) { self.did_seek = false; - self.at_end = false; + self.at_end = self.tree.is_empty(); self.stack.truncate(0); self.position = D::default(); } @@ -139,7 +139,7 @@ where if self.at_end { self.position = D::default(); self.descend_to_last_item(self.tree, cx); - self.at_end = false; + self.at_end = self.tree.is_empty(); } else { while let Some(entry) = self.stack.pop() { if entry.index > 0 { @@ -195,13 +195,15 @@ where { let mut descend = false; - if self.stack.is_empty() && !self.at_end { - self.stack.push(StackEntry { - tree: self.tree, - index: 0, - position: D::default(), - }); - descend = true; + if self.stack.is_empty() { + if !self.at_end { + self.stack.push(StackEntry { + tree: self.tree, + index: 0, + position: D::default(), + }); + descend = true; + } self.did_seek = true; } @@ -279,6 +281,10 @@ where cx: &::Context, ) { self.did_seek = true; + if subtree.is_empty() { + return; + } + loop { match subtree.0.as_ref() { Node::Internal { @@ -298,7 +304,7 @@ where subtree = child_trees.last().unwrap(); } Node::Leaf { item_summaries, .. } => { - let last_index = item_summaries.len().saturating_sub(1); + let last_index = item_summaries.len() - 1; for item_summary in &item_summaries[0..last_index] { self.position.add_summary(item_summary, cx); } diff --git a/crates/sum_tree/src/sum_tree.rs b/crates/sum_tree/src/sum_tree.rs index ea21672b10745445c6de403523c6fb3e4fdcb993..c372ffc6b07d532610cf9fc062c6434022c20fe3 100644 --- a/crates/sum_tree/src/sum_tree.rs +++ b/crates/sum_tree/src/sum_tree.rs @@ -821,6 +821,14 @@ mod tests { assert_eq!(cursor.item(), None); assert_eq!(cursor.prev_item(), None); assert_eq!(cursor.start().sum, 0); + cursor.prev(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.start().sum, 0); + cursor.next(&()); + assert_eq!(cursor.item(), None); + assert_eq!(cursor.prev_item(), None); + assert_eq!(cursor.start().sum, 0); // Single-element tree let mut tree = SumTree::::new(); diff --git a/crates/text/src/rope.rs b/crates/text/src/rope.rs index cd474cc4da1b33991659f20d690702395c33c711..3d1cb28eb18bc0605a64cc40aa66b4618a2dcb2b 100644 --- a/crates/text/src/rope.rs +++ b/crates/text/src/rope.rs @@ -48,6 +48,12 @@ impl Rope { *self = new_rope; } + pub fn slice(&self, range: Range) -> Rope { + let mut cursor = self.cursor(0); + cursor.seek_forward(range.start); + cursor.slice(range.end) + } + pub fn push(&mut self, text: &str) { let mut new_chunks = SmallVec::<[_; 16]>::new(); let mut new_chunk = ArrayString::new(); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 34640211f67b58e2f6bb6c3cae720582ef13ac35..1ca2e3a6040393f30ec424cdeef6ed88e7ce818e 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -24,7 +24,7 @@ pub struct Theme { pub project_panel: ProjectPanel, pub selector: Selector, pub editor: Editor, - pub find: Find, + pub search: Search, pub project_diagnostics: ProjectDiagnostics, } @@ -95,18 +95,21 @@ pub struct Toolbar { } #[derive(Clone, Deserialize, Default)] -pub struct Find { +pub struct Search { #[serde(flatten)] pub container: ContainerStyle, pub editor: FindEditor, pub invalid_editor: ContainerStyle, - pub mode_button_group: ContainerStyle, - pub mode_button: ContainedText, - pub active_mode_button: ContainedText, - pub hovered_mode_button: ContainedText, - pub active_hovered_mode_button: ContainedText, + pub option_button_group: ContainerStyle, + pub option_button: ContainedText, + pub active_option_button: ContainedText, + pub hovered_option_button: ContainedText, + pub active_hovered_option_button: ContainedText, pub match_background: Color, pub match_index: ContainedText, + pub results_status: TextStyle, + pub tab_icon_width: f32, + pub tab_icon_spacing: f32, } #[derive(Clone, Deserialize, Default)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c1f34c94c7bd37e030e0ee67d500bb92ad7ebf1e..050cd7d5555af9a2432afbd323c8c3446d1589b8 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -123,6 +123,7 @@ enum NavigationMode { Normal, GoingBack, GoingForward, + Disabled, } impl Default for NavigationMode { @@ -149,6 +150,10 @@ impl Pane { } } + pub fn nav_history(&self) -> &Rc> { + &self.nav_history + } + pub fn activate(&self, cx: &mut ViewContext) { cx.emit(Event::Activate); } @@ -279,7 +284,7 @@ impl Pane { item_view.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); self.item_views - .insert(item_idx, (item_view.item_id(cx), item_view)); + .insert(item_idx, (item_view.item(cx).id(), item_view)); self.activate_item(item_idx, cx); cx.notify(); } @@ -662,6 +667,14 @@ impl ItemNavHistory { } impl NavHistory { + pub fn disable(&mut self) { + self.mode = NavigationMode::Disabled; + } + + pub fn enable(&mut self) { + self.mode = NavigationMode::Normal; + } + pub fn pop_backward(&mut self) -> Option { self.backward_stack.pop_back() } @@ -672,7 +685,7 @@ impl NavHistory { fn pop(&mut self, mode: NavigationMode) -> Option { match mode { - NavigationMode::Normal => None, + NavigationMode::Normal | NavigationMode::Disabled => None, NavigationMode::GoingBack => self.pop_backward(), NavigationMode::GoingForward => self.pop_forward(), } @@ -688,6 +701,7 @@ impl NavHistory { item_view: Rc, ) { match self.mode { + NavigationMode::Disabled => {} NavigationMode::Normal => { if self.backward_stack.len() >= MAX_NAVIGATION_HISTORY_LEN { self.backward_stack.pop_front(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c7368ddbe96da439aea8443536e1dc64ff095d68..0ad3a1e5e35ad0cce16c2f6bf2c40f5cf03293db 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,7 +9,7 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; -use collections::HashSet; +use collections::BTreeMap; use gpui::{ action, color::Color, @@ -36,6 +36,7 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, + cmp::Reverse, future::Future, hash::{Hash, Hasher}, path::{Path, PathBuf}, @@ -153,10 +154,10 @@ pub trait Item: Entity + Sized { pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} - fn item_id(&self, cx: &AppContext) -> usize; + fn item(&self, cx: &AppContext) -> Box; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn clone_on_split(&self, _: &mut ViewContext) -> Option + fn clone_on_split(&self, _: ItemNavHistory, _: &mut ViewContext) -> Option where Self: Sized, { @@ -225,11 +226,15 @@ pub trait WeakItemHandle { } pub trait ItemViewHandle: 'static { - fn item_id(&self, cx: &AppContext) -> usize; + fn item(&self, cx: &AppContext) -> Box; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; - fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; + fn clone_on_split( + &self, + nav_history: Rc>, + cx: &mut MutableAppContext, + ) -> Option>; fn added_to_pane(&mut self, cx: &mut ViewContext); fn deactivated(&self, cx: &mut MutableAppContext); fn navigate(&self, data: Box, cx: &mut MutableAppContext); @@ -357,8 +362,8 @@ impl dyn ItemViewHandle { } impl ItemViewHandle for ViewHandle { - fn item_id(&self, cx: &AppContext) -> usize { - self.read(cx).item_id(cx) + fn item(&self, cx: &AppContext) -> Box { + self.read(cx).item(cx) } fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { @@ -373,9 +378,15 @@ impl ItemViewHandle for ViewHandle { Box::new(self.clone()) } - fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { + fn clone_on_split( + &self, + nav_history: Rc>, + cx: &mut MutableAppContext, + ) -> Option> { self.update(cx, |item, cx| { - cx.add_option_view(|cx| item.clone_on_split(cx)) + cx.add_option_view(|cx| { + item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx) + }) }) .map(|handle| Box::new(handle) as Box) } @@ -559,7 +570,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, path_openers: Arc<[Box]>, - items: HashSet>, + items: BTreeMap, Box>, _observe_current_user: Task<()>, } @@ -805,17 +816,26 @@ impl Workspace { fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { self.items - .iter() + .values() .filter_map(|i| i.upgrade(cx)) .find(|i| i.project_path(cx).as_ref() == Some(path)) } pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items - .iter() + .values() .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) } + pub fn items_of_type<'a, T: Item>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.items + .values() + .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + } + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -955,7 +975,8 @@ impl Workspace { where T: 'static + ItemHandle, { - self.items.insert(item_handle.downgrade()); + self.items + .insert(Reverse(item_handle.id()), item_handle.downgrade()); pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) } @@ -1047,7 +1068,10 @@ impl Workspace { let new_pane = self.add_pane(cx); self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { - if let Some(clone) = item.clone_on_split(cx.as_mut()) { + let nav_history = new_pane.read(cx).nav_history().clone(); + if let Some(clone) = item.clone_on_split(nav_history, cx.as_mut()) { + let item = clone.item(cx).downgrade(); + self.items.insert(Reverse(item.id()), item); new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 9dea5ad1b6ba99fe7bfc44e2f37c810477f6982a..12e83086cdb9344e299f8d97480842daaa384d57 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -36,7 +36,7 @@ contacts_panel = { path = "../contacts_panel" } diagnostics = { path = "../diagnostics" } editor = { path = "../editor" } file_finder = { path = "../file_finder" } -find = { path = "../find" } +search = { path = "../search" } fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { path = "../go_to_line" } @@ -88,7 +88,6 @@ smol = "1.2.5" surf = "2.2" tempdir = { version = "0.3.7", optional = true } thiserror = "1.0.29" -time = "0.3" tiny_http = "0.8" toml = "0.5" tree-sitter = "0.20.4" diff --git a/crates/zed/assets/icons/magnifier.svg b/crates/zed/assets/icons/magnifier.svg new file mode 100644 index 0000000000000000000000000000000000000000..dc27a594ee3d6996e6e5a0fd9922774a05129ada --- /dev/null +++ b/crates/zed/assets/icons/magnifier.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index de1344239a06bdd5e7aa2d95c331a9e11547b743..59a488c896b4791e66514a2f0b76f409e8483342 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -348,11 +348,14 @@ tab_icon_width = 13 tab_icon_spacing = 4 tab_summary_spacing = 10 -[find] +[search] match_background = "$state.highlighted_line" background = "$surface.1" +results_status = { extends = "$text.0", size = 18 } +tab_icon_width = 14 +tab_icon_spacing = 4 -[find.mode_button] +[search.option_button] extends = "$text.1" padding = { left = 6, right = 6, top = 1, bottom = 1 } corner_radius = 6 @@ -361,26 +364,26 @@ border = { width = 1, color = "$border.0" } margin.left = 1 margin.right = 1 -[find.mode_button_group] +[search.option_button_group] padding = { left = 2, right = 2 } -[find.active_mode_button] -extends = "$find.mode_button" +[search.active_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.hovered_mode_button] -extends = "$find.mode_button" +[search.hovered_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.active_hovered_mode_button] -extends = "$find.mode_button" +[search.active_hovered_option_button] +extends = "$search.option_button" background = "$surface.2" -[find.match_index] +[search.match_index] extends = "$text.1" padding = 6 -[find.editor] +[search.editor] max_width = 400 background = "$surface.0" corner_radius = 6 @@ -391,6 +394,6 @@ placeholder_text = "$text.2" selection = "$selection.host" border = { width = 1, color = "$border.0" } -[find.invalid_editor] -extends = "$find.editor" +[search.invalid_editor] +extends = "$search.editor" border = { width = 1, color = "$status.bad" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index a6dda2e27bf928e9557a233e3f0c3986ab5e35df..f855f4fb194a9b4a176c8a860a48fd6ccbfd6220 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -60,7 +60,7 @@ fn main() { project_symbols::init(cx); project_panel::init(cx); diagnostics::init(cx); - find::init(cx); + search::init(cx); cx.spawn({ let client = client.clone(); |cx| async move {