From 76fc9c955e773bd28acfbc780937b77d9843c07b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 15 Mar 2022 13:13:33 -0700 Subject: [PATCH 001/139] Restore underline for warnings about unused code --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 25 ++++++++++++++----------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a80af9ea5f..8c6904d136 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4211,7 +4211,7 @@ impl Editor { }; let group = diagnostics.find_map(|entry| { if entry.diagnostic.is_primary - && !entry.diagnostic.is_unnecessary + && entry.diagnostic.severity <= DiagnosticSeverity::WARNING && !entry.range.is_empty() && Some(entry.range.end) != active_primary_range.as_ref().map(|r| *r.end()) { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 1aed9791c4..ad8057db4e 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -21,7 +21,7 @@ use gpui::{ MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; use json::json; -use language::Bias; +use language::{Bias, DiagnosticSeverity}; use smallvec::SmallVec; use std::{ cmp::{self, Ordering}, @@ -665,19 +665,22 @@ impl EditorElement { } } - let mut diagnostic_highlight = HighlightStyle { - ..Default::default() - }; + let mut diagnostic_highlight = HighlightStyle::default(); if chunk.is_unnecessary { diagnostic_highlight.fade_out = Some(style.unnecessary_code_fade); - } else if let Some(severity) = chunk.diagnostic_severity { - let diagnostic_style = super::diagnostic_style(severity, true, style); - diagnostic_highlight.underline = Some(Underline { - color: Some(diagnostic_style.message.text.color), - thickness: 1.0.into(), - squiggly: true, - }); + } + + if let Some(severity) = chunk.diagnostic_severity { + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { + let diagnostic_style = super::diagnostic_style(severity, true, style); + diagnostic_highlight.underline = Some(Underline { + color: Some(diagnostic_style.message.text.color), + thickness: 1.0.into(), + squiggly: true, + }); + } } if let Some(highlight_style) = highlight_style.as_mut() { From b0afb64a6e38244e7fafc794adf4c3a76520c2d2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 16 Mar 2022 14:51:54 +0100 Subject: [PATCH 002/139] Fix edge cases when calling `refresh_windows` This commit ensures that new views are rendered for the first time. This fixes a panic that could be reproduced by dropping the `ThemeSelector` and opening the file finder during the same update. Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 80 +++++++++++++++++++++++++++++++++--- crates/gpui/src/presenter.rs | 39 ++++++++---------- 2 files changed, 91 insertions(+), 28 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 89cf9afba2..a03e6e54ae 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1413,11 +1413,10 @@ impl MutableAppContext { invalidation: None, }, ); - this.open_platform_window(window_id, window_options); root_view.update(this, |view, cx| { view.on_focus(cx); - cx.notify(); }); + this.open_platform_window(window_id, window_options); (window_id, root_view) }) @@ -1475,6 +1474,11 @@ impl MutableAppContext { })); } + let scene = + presenter + .borrow_mut() + .build_scene(window.size(), window.scale_factor(), false, self); + window.present_scene(scene); self.presenters_and_platform_windows .insert(window_id, (presenter.clone(), window)); } @@ -1666,13 +1670,13 @@ impl MutableAppContext { } } - for (window_id, invalidation) in invalidations { + for (window_id, mut invalidation) in invalidations { if let Some((presenter, mut window)) = self.presenters_and_platform_windows.remove(&window_id) { { let mut presenter = presenter.borrow_mut(); - presenter.invalidate(invalidation, self); + presenter.invalidate(&mut invalidation, self); let scene = presenter.build_scene(window.size(), window.scale_factor(), false, self); window.present_scene(scene); @@ -1695,7 +1699,7 @@ impl MutableAppContext { fn perform_window_refresh(&mut self) { let mut presenters = mem::take(&mut self.presenters_and_platform_windows); for (window_id, (presenter, window)) in &mut presenters { - let invalidation = self + let mut invalidation = self .cx .windows .get_mut(&window_id) @@ -1703,7 +1707,10 @@ impl MutableAppContext { .invalidation .take(); let mut presenter = presenter.borrow_mut(); - presenter.refresh(invalidation, self); + presenter.refresh( + invalidation.as_mut().unwrap_or(&mut Default::default()), + self, + ); let scene = presenter.build_scene(window.size(), window.scale_factor(), true, self); window.present_scene(scene); } @@ -5363,4 +5370,65 @@ mod tests { cx.update(|_| drop(view)); condition.await; } + + #[crate::test(self)] + fn test_refresh_windows(cx: &mut MutableAppContext) { + struct View(usize); + + impl super::Entity for View { + type Event = (); + } + + impl super::View for View { + fn ui_name() -> &'static str { + "test view" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().named(format!("render count: {}", post_inc(&mut self.0))) + } + } + + let (window_id, root_view) = cx.add_window(Default::default(), |_| View(0)); + let presenter = cx.presenters_and_platform_windows[&window_id].0.clone(); + + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 0") + ); + + let view = cx.add_view(window_id, |cx| { + cx.refresh_windows(); + View(0) + }); + + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 1") + ); + assert_eq!( + presenter.borrow().rendered_views[&view.id()].name(), + Some("render count: 0") + ); + + cx.update(|cx| cx.refresh_windows()); + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 2") + ); + assert_eq!( + presenter.borrow().rendered_views[&view.id()].name(), + Some("render count: 1") + ); + + cx.update(|cx| { + cx.refresh_windows(); + drop(view); + }); + assert_eq!( + presenter.borrow().rendered_views[&root_view.id()].name(), + Some("render count: 3") + ); + assert_eq!(presenter.borrow().rendered_views.len(), 1); + } } diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 5ee5b36f87..88ccb694d2 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -20,7 +20,7 @@ use std::{ pub struct Presenter { window_id: usize, - rendered_views: HashMap, + pub(crate) rendered_views: HashMap, parents: HashMap, font_cache: Arc, text_layout_cache: TextLayoutCache, @@ -63,39 +63,34 @@ impl Presenter { path } - pub fn invalidate(&mut self, mut invalidation: WindowInvalidation, cx: &mut MutableAppContext) { + pub fn invalidate( + &mut self, + invalidation: &mut WindowInvalidation, + cx: &mut MutableAppContext, + ) { cx.start_frame(); - for view_id in invalidation.removed { + for view_id in &invalidation.removed { invalidation.updated.remove(&view_id); self.rendered_views.remove(&view_id); self.parents.remove(&view_id); } - for view_id in invalidation.updated { + for view_id in &invalidation.updated { self.rendered_views.insert( - view_id, - cx.render_view(self.window_id, view_id, self.titlebar_height, false) + *view_id, + cx.render_view(self.window_id, *view_id, self.titlebar_height, false) .unwrap(), ); } } - pub fn refresh( - &mut self, - invalidation: Option, - cx: &mut MutableAppContext, - ) { - cx.start_frame(); - if let Some(invalidation) = invalidation { - for view_id in invalidation.removed { - self.rendered_views.remove(&view_id); - self.parents.remove(&view_id); - } - } - + pub fn refresh(&mut self, invalidation: &mut WindowInvalidation, cx: &mut MutableAppContext) { + self.invalidate(invalidation, cx); for (view_id, view) in &mut self.rendered_views { - *view = cx - .render_view(self.window_id, *view_id, self.titlebar_height, true) - .unwrap(); + if !invalidation.updated.contains(view_id) { + *view = cx + .render_view(self.window_id, *view_id, self.titlebar_height, true) + .unwrap(); + } } } From a88320dc5f10a9b09ebbb7bc6d1ec02ec7ad4f76 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 16 Mar 2022 13:34:04 -0700 Subject: [PATCH 003/139] Remove workspace::Item trait Co-Authored-By: Nathan Sobo Co-Authored-By: Keith Simmons Co-Authored-By: Antonio Scandurra --- crates/diagnostics/src/diagnostics.rs | 103 +++---- crates/editor/src/editor.rs | 88 ++++-- crates/editor/src/items.rs | 151 ++-------- crates/gpui/src/app.rs | 14 + crates/project/src/project.rs | 10 + crates/project/src/worktree.rs | 9 + crates/project_symbols/src/project_symbols.rs | 10 +- crates/search/src/project_search.rs | 76 ++--- crates/workspace/src/pane.rs | 53 ++-- crates/workspace/src/workspace.rs | 284 +++++------------- 10 files changed, 281 insertions(+), 517 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1a432f52e2..1db19f7f5e 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -25,7 +25,7 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemHandle, ItemNavHistory, ItemViewHandle as _, Settings, Workspace}; +use workspace::{ItemNavHistory, ItemViewHandle as _, Settings, Workspace}; action!(Deploy); @@ -38,12 +38,8 @@ pub fn init(cx: &mut MutableAppContext) { type Event = editor::Event; -struct ProjectDiagnostics { - project: ModelHandle, -} - struct ProjectDiagnosticsEditor { - model: ModelHandle, + project: ModelHandle, workspace: WeakViewHandle, editor: ViewHandle, summary: DiagnosticSummary, @@ -65,16 +61,6 @@ struct DiagnosticGroupState { block_count: usize, } -impl ProjectDiagnostics { - fn new(project: ModelHandle) -> Self { - Self { project } - } -} - -impl Entity for ProjectDiagnostics { - type Event = (); -} - impl Entity for ProjectDiagnosticsEditor { type Event = Event; } @@ -109,12 +95,11 @@ impl View for ProjectDiagnosticsEditor { impl ProjectDiagnosticsEditor { fn new( - model: ModelHandle, + project_handle: ModelHandle, workspace: WeakViewHandle, cx: &mut ViewContext, ) -> Self { - let project = model.read(cx).project.clone(); - cx.subscribe(&project, |this, _, event, cx| match event { + cx.subscribe(&project_handle, |this, _, event, cx| match event { project::Event::DiskBasedDiagnosticsFinished => { this.update_excerpts(cx); this.update_title(cx); @@ -126,20 +111,21 @@ impl ProjectDiagnosticsEditor { }) .detach(); - let excerpts = cx.add_model(|cx| MultiBuffer::new(project.read(cx).replica_id())); + let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts.clone(), Some(project.clone()), cx); + let mut editor = Editor::for_buffer(excerpts.clone(), Some(project_handle.clone()), cx); editor.set_vertical_scroll_margin(5, cx); editor }); cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event)) .detach(); - let project = project.read(cx); + let project = project_handle.read(cx); let paths_to_update = project.diagnostic_summaries(cx).map(|e| e.0).collect(); + let summary = project.diagnostic_summary(cx); let mut this = Self { - model, - summary: project.diagnostic_summary(cx), + project: project_handle, + summary, workspace, excerpts, editor, @@ -151,18 +137,20 @@ impl ProjectDiagnosticsEditor { } fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace.item_of_type::(cx) { + if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, cx); } else { - let diagnostics = - cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone())); - workspace.open_item(diagnostics, cx); + let workspace_handle = cx.weak_handle(); + let diagnostics = cx.add_view(|cx| { + ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) + }); + workspace.open_item(Box::new(diagnostics), 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(); + let project = self.project.clone(); cx.spawn(|this, mut cx| { async move { for path in paths { @@ -443,37 +431,12 @@ impl ProjectDiagnosticsEditor { } fn update_title(&mut self, cx: &mut ViewContext) { - self.summary = self.model.read(cx).project.read(cx).diagnostic_summary(cx); + self.summary = self.project.read(cx).diagnostic_summary(cx); cx.emit(Event::TitleChanged); } } -impl workspace::Item for ProjectDiagnostics { - type View = ProjectDiagnosticsEditor; - - fn build_view( - handle: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Self::View { - let diagnostics = ProjectDiagnosticsEditor::new(handle, workspace.weak_handle(), cx); - diagnostics - .editor - .update(cx, |editor, _| editor.set_nav_history(Some(nav_history))); - diagnostics - } - - fn project_path(&self) -> Option { - None - } -} - impl workspace::ItemView for ProjectDiagnosticsEditor { - fn item(&self, _: &AppContext) -> Box { - Box::new(self.model.clone()) - } - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { render_summary( &self.summary, @@ -486,6 +449,10 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } + fn project_entry(&self, _: &AppContext) -> Option { + None + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)); @@ -532,20 +499,21 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { matches!(event, Event::Saved | Event::Dirtied | Event::TitleChanged) } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, { - let diagnostics = - ProjectDiagnosticsEditor::new(self.model.clone(), self.workspace.clone(), cx); - diagnostics.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - Some(diagnostics) + Some(ProjectDiagnosticsEditor::new( + self.project.clone(), + self.workspace.clone(), + cx, + )) } fn act_as_type( @@ -829,9 +797,8 @@ mod tests { }); // Open the project diagnostics view while there are already diagnostics. - let model = cx.add_model(|_| ProjectDiagnostics::new(project.clone())); let view = cx.add_view(0, |cx| { - ProjectDiagnosticsEditor::new(model, workspace.downgrade(), cx) + ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) }); view.next_notification(&cx).await; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8c6904d136..6a29859195 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,7 +28,6 @@ use gpui::{ ModelHandle, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use items::{BufferItemHandle, MultiBufferItemHandle}; use itertools::Itertools as _; pub use language::{char_kind, CharKind}; use language::{ @@ -836,7 +835,32 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, None, cx) } - pub fn clone(&self, nav_history: ItemNavHistory, cx: &mut ViewContext) -> Self { + pub fn find_or_create( + workspace: &mut Workspace, + buffer: ModelHandle, + cx: &mut ViewContext, + ) -> ViewHandle { + let project = workspace.project().clone(); + + if let Some(project_entry) = + project::File::from_dyn(buffer.read(cx).file()).and_then(|file| file.project_entry(cx)) + { + return workspace + .open_item_for_project_entry(project_entry, cx, |cx| { + let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Editor::for_buffer(multibuffer, Some(project.clone()), cx) + }) + .downcast::() + .unwrap(); + } + + let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)); + workspace.open_item(Box::new(editor.clone()), cx); + editor + } + + pub fn clone(&self, cx: &mut ViewContext) -> Self { let mut clone = Self::new( self.mode, self.buffer.clone(), @@ -846,7 +870,6 @@ impl Editor { ); clone.scroll_position = self.scroll_position; clone.scroll_top_anchor = self.scroll_top_anchor.clone(); - clone.nav_history = Some(nav_history); clone.searchable = self.searchable; clone } @@ -938,14 +961,20 @@ impl Editor { _: &workspace::OpenNew, cx: &mut ViewContext, ) { - let project = workspace.project(); + let project = workspace.project().clone(); if project.read(cx).is_remote() { cx.propagate_action(); } else if let Some(buffer) = project .update(cx, |project, cx| project.create_buffer(cx)) .log_err() { - workspace.open_item(BufferItemHandle(buffer), cx); + let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + workspace.open_item( + Box::new( + cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)), + ), + cx, + ); } } @@ -2349,7 +2378,11 @@ impl Editor { }); workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); + let project = workspace.project().clone(); + let editor = workspace.open_item( + Box::new(cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx))), + cx, + ); if let Some(editor) = editor.act_as::(cx) { editor.update(cx, |editor, cx| { let color = editor.style(cx).highlighted_line_background; @@ -4280,20 +4313,17 @@ impl Editor { return; }; - let definitions = workspace - .project() - .update(cx, |project, cx| project.definition(&buffer, head, cx)); + let project = workspace.project().clone(); + let definitions = project.update(cx, |project, cx| project.definition(&buffer, head, cx)); 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 - .open_item(BufferItemHandle(definition.buffer), cx) - .downcast::() - .unwrap(); + let target_editor_handle = + Self::find_or_create(workspace, definition.buffer, cx); 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. @@ -4324,9 +4354,8 @@ impl Editor { let (buffer, head) = editor.buffer.read(cx).text_anchor_for_position(head, cx)?; let replica_id = editor.replica_id(cx); - let references = workspace - .project() - .update(cx, |project, cx| project.references(&buffer, head, cx)); + let project = workspace.project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); Some(cx.spawn(|workspace, mut cx| async move { let mut locations = references.await?; if locations.is_empty() { @@ -4370,13 +4399,13 @@ impl Editor { }); workspace.update(&mut cx, |workspace, cx| { - let editor = workspace.open_item(MultiBufferItemHandle(excerpt_buffer), cx); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_background::(ranges_to_highlight, color, cx); - }); - } + let editor = + cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx)); + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_background::(ranges_to_highlight, color, cx); + }); + workspace.open_item(Box::new(editor), cx); }); Ok(()) @@ -5563,17 +5592,10 @@ impl Editor { // 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(); + workspace.activate_next_pane(cx); + for (buffer, ranges) in new_selections_by_buffer.into_iter() { + let editor = Self::find_or_create(workspace, buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index c9af9f0854..b84a095e6b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,20 +1,16 @@ use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; use anyhow::Result; use gpui::{ - elements::*, AppContext, Entity, ModelContext, ModelHandle, MutableAppContext, RenderContext, - Subscription, Task, View, ViewContext, ViewHandle, WeakModelHandle, + elements::*, AppContext, Entity, ModelContext, ModelHandle, RenderContext, Subscription, Task, + View, ViewContext, ViewHandle, WeakModelHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectPath}; +use project::{File, Project, ProjectEntry, ProjectPath}; +use std::fmt::Write; use std::path::PathBuf; -use std::rc::Rc; -use std::{cell::RefCell, fmt::Write}; use text::{Point, Selection}; use util::ResultExt; -use workspace::{ - ItemHandle, ItemNavHistory, ItemView, ItemViewHandle, NavHistory, PathOpener, Settings, - StatusItemView, WeakItemHandle, Workspace, -}; +use workspace::{ItemNavHistory, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView}; pub struct BufferOpener; @@ -35,127 +31,22 @@ impl PathOpener for BufferOpener { &self, project: &mut Project, project_path: ProjectPath, + window_id: usize, cx: &mut ModelContext, - ) -> Option>>> { + ) -> Option>>> { let buffer = project.open_buffer(project_path, cx); - let task = cx.spawn(|_, _| async move { + Some(cx.spawn(|project, mut cx| async move { let buffer = buffer.await?; - Ok(Box::new(BufferItemHandle(buffer)) as Box) - }); - Some(task) - } -} - -impl ItemHandle for BufferItemHandle { - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - let buffer = cx.add_model(|cx| MultiBuffer::singleton(self.0.clone(), cx)); - Box::new(cx.add_view(window_id, |cx| { - let mut editor = Editor::for_buffer(buffer, Some(workspace.project().clone()), cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); - editor + let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let editor = cx.add_view(window_id, |cx| { + Editor::for_buffer(multibuffer, Some(project), cx) + }); + Ok(Box::new(editor) as Box) })) } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn to_any(&self) -> gpui::AnyModelHandle { - self.0.clone().into() - } - - fn downgrade(&self) -> Box { - Box::new(WeakBufferItemHandle(self.0.downgrade())) - } - - fn project_path(&self, cx: &AppContext) -> Option { - File::from_dyn(self.0.read(cx).file()).map(|f| ProjectPath { - worktree_id: f.worktree_id(cx), - path: f.path().clone(), - }) - } - - fn id(&self) -> usize { - self.0.id() - } -} - -impl ItemHandle for MultiBufferItemHandle { - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - Box::new(cx.add_view(window_id, |cx| { - let mut editor = - Editor::for_buffer(self.0.clone(), Some(workspace.project().clone()), cx); - editor.nav_history = Some(ItemNavHistory::new(nav_history, &cx.handle())); - editor - })) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn to_any(&self) -> gpui::AnyModelHandle { - self.0.clone().into() - } - - fn downgrade(&self) -> Box { - Box::new(WeakMultiBufferItemHandle(self.0.downgrade())) - } - - fn project_path(&self, _: &AppContext) -> Option { - None - } - - fn id(&self) -> usize { - self.0.id() - } -} - -impl WeakItemHandle for WeakBufferItemHandle { - fn upgrade(&self, cx: &AppContext) -> Option> { - self.0 - .upgrade(cx) - .map(|buffer| Box::new(BufferItemHandle(buffer)) as Box) - } - - fn id(&self) -> usize { - self.0.id() - } -} - -impl WeakItemHandle for WeakMultiBufferItemHandle { - fn upgrade(&self, cx: &AppContext) -> Option> { - self.0 - .upgrade(cx) - .map(|buffer| Box::new(MultiBufferItemHandle(buffer)) as Box) - } - - fn id(&self) -> usize { - self.0.id() - } } impl ItemView for Editor { - fn item(&self, cx: &AppContext) -> Box { - if let Some(buffer) = self.buffer.read(cx).as_singleton() { - Box::new(BufferItemHandle(buffer)) - } else { - Box::new(MultiBufferItemHandle(self.buffer.clone())) - } - } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); @@ -184,15 +75,19 @@ impl ItemView for Editor { }) } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn project_entry(&self, cx: &AppContext) -> Option { + File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry(cx)) + } + + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, { - Some(self.clone(nav_history, cx)) + Some(self.clone(cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); } fn deactivated(&mut self, cx: &mut ViewContext) { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a03e6e54ae..a479e5fba1 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -595,6 +595,14 @@ impl AsyncAppContext { self.update(|cx| cx.add_model(build_model)) } + pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle + where + T: View, + F: FnOnce(&mut ViewContext) -> T, + { + self.update(|cx| cx.add_view(window_id, build_view)) + } + pub fn platform(&self) -> Arc { self.0.borrow().platform() } @@ -3459,6 +3467,12 @@ impl PartialEq for ViewHandle { } } +impl PartialEq> for ViewHandle { + fn eq(&self, other: &WeakViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl Eq for ViewHandle {} impl Debug for ViewHandle { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index f5af7fc70e..b9bb25a5fc 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -3221,6 +3221,16 @@ impl Project { self.active_entry } + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + self.worktree_for_id(path.worktree_id, cx)? + .read(cx) + .entry_for_path(&path.path) + .map(|entry| ProjectEntry { + worktree_id: path.worktree_id, + entry_id: entry.id, + }) + } + // RPC message handlers async fn handle_unshare_project( diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 1ef8dd34a0..4a121c82e4 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,3 +1,5 @@ +use crate::ProjectEntry; + use super::{ fs::{self, Fs}, ignore::IgnoreStack, @@ -1502,6 +1504,13 @@ impl File { pub fn worktree_id(&self, cx: &AppContext) -> WorktreeId { self.worktree.read(cx).id() } + + pub fn project_entry(&self, cx: &AppContext) -> Option { + self.entry_id.map(|entry_id| ProjectEntry { + worktree_id: self.worktree_id(cx), + entry_id, + }) + } } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index bfd204671f..29808ff7d9 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -1,6 +1,5 @@ use editor::{ - combine_syntax_and_fuzzy_match_highlights, items::BufferItemHandle, styled_runs_for_code_label, - Autoscroll, Bias, Editor, + combine_syntax_and_fuzzy_match_highlights, styled_runs_for_code_label, Autoscroll, Bias, Editor, }; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -346,6 +345,7 @@ impl ProjectSymbolsView { let buffer = workspace .project() .update(cx, |project, cx| project.open_buffer_for_symbol(symbol, cx)); + let symbol = symbol.clone(); cx.spawn(|workspace, mut cx| async move { let buffer = buffer.await?; @@ -353,10 +353,8 @@ impl ProjectSymbolsView { let position = buffer .read(cx) .clip_point_utf16(symbol.range.start, Bias::Left); - let editor = workspace - .open_item(BufferItemHandle(buffer), cx) - .downcast::() - .unwrap(); + + let editor = Editor::find_or_create(workspace, buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges( [position..position], diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b09c88a4a0..56395b107a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -7,7 +7,7 @@ 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, + ViewHandle, WeakModelHandle, WeakViewHandle, }; use project::{search::SearchQuery, Project}; use std::{ @@ -16,7 +16,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{Item, ItemHandle, ItemNavHistory, ItemView, Settings, Workspace}; +use workspace::{ItemNavHistory, ItemView, Settings, Workspace}; action!(Deploy); action!(Search); @@ -26,7 +26,7 @@ action!(ToggleFocus); const MAX_TAB_TITLE_LEN: usize = 24; #[derive(Default)] -struct ActiveSearches(HashMap, WeakModelHandle>); +struct ActiveSearches(HashMap, WeakViewHandle>); pub fn init(cx: &mut MutableAppContext) { cx.add_app_state(ActiveSearches::default()); @@ -139,23 +139,6 @@ impl ProjectSearch { } } -impl Item for ProjectSearch { - type View = ProjectSearchView; - - fn build_view( - model: ModelHandle, - _: &Workspace, - nav_history: ItemNavHistory, - cx: &mut gpui::ViewContext, - ) -> Self::View { - ProjectSearchView::new(model, Some(nav_history), cx) - } - - fn project_path(&self) -> Option { - None - } -} - enum ViewEvent { UpdateTab, } @@ -199,11 +182,11 @@ impl View for ProjectSearchView { } fn on_focus(&mut self, cx: &mut ViewContext) { + let handle = cx.weak_handle(); cx.update_app_state(|state: &mut ActiveSearches, cx| { - state.0.insert( - self.model.read(cx).project.downgrade(), - self.model.downgrade(), - ) + state + .0 + .insert(self.model.read(cx).project.downgrade(), handle) }); if self.model.read(cx).match_ranges.is_empty() { @@ -235,10 +218,6 @@ impl ItemView for ProjectSearchView { .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 = cx.app_state::(); let search_theme = &settings.theme.search; @@ -271,6 +250,10 @@ impl ItemView for ProjectSearchView { None } + fn project_entry(&self, _: &AppContext) -> Option { + None + } + fn can_save(&self, _: &gpui::AppContext) -> bool { true } @@ -305,16 +288,18 @@ impl ItemView for ProjectSearchView { unreachable!("save_as should not have been called") } - fn clone_on_split( - &self, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Option + fn clone_on_split(&self, 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), cx)) + Some(Self::new(model, cx)) + } + + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.results_editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); } fn navigate(&mut self, data: Box, cx: &mut ViewContext) { @@ -328,11 +313,7 @@ impl ItemView for ProjectSearchView { } impl ProjectSearchView { - fn new( - model: ModelHandle, - nav_history: Option, - cx: &mut ViewContext, - ) -> Self { + fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { let project; let excerpts; let mut query_text = String::new(); @@ -364,7 +345,6 @@ impl ProjectSearchView { let results_editor = cx.add_view(|cx| { let mut editor = Editor::for_buffer(excerpts, Some(project), cx); editor.set_searchable(false); - editor.set_nav_history(nav_history); editor }); cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) @@ -406,16 +386,19 @@ impl ProjectSearchView { let existing = active_search .and_then(|active_search| { workspace - .items_of_type::(cx) + .items_of_type::(cx) .find(|search| search == active_search) }) - .or_else(|| workspace.item_of_type::(cx)); + .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); + workspace.open_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), + cx, + ); } } @@ -450,7 +433,10 @@ impl ProjectSearchView { model.search(new_query, cx); model }); - workspace.open_item(model, cx); + workspace.open_item( + Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), + cx, + ); } } } @@ -732,7 +718,7 @@ mod tests { let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search_view = cx.add_view(Default::default(), |cx| { - ProjectSearchView::new(search.clone(), None, cx) + ProjectSearchView::new(search.clone(), cx) }); search_view.update(cx, |search_view, cx| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c54a1b050b..9f5ee52274 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,5 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::{ItemHandle, ItemView, Settings, WeakItemViewHandle, Workspace}; +use crate::{ItemView, Settings, WeakItemViewHandle, Workspace}; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -10,7 +10,7 @@ use gpui::{ AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::ProjectPath; +use project::{ProjectEntry, ProjectPath}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -97,7 +97,7 @@ pub enum Event { } pub struct Pane { - item_views: Vec<(usize, Box)>, + item_views: Vec<(Option, Box)>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -281,27 +281,23 @@ impl Pane { } } - pub fn open_item( + pub fn open_item( &mut self, - item_handle: T, - workspace: &Workspace, + item_view_to_open: Box, cx: &mut ViewContext, - ) -> Box - where - T: 'static + ItemHandle, - { - for (ix, (item_id, item_view)) in self.item_views.iter().enumerate() { - if *item_id == item_handle.id() { + ) -> Box { + // Find an existing view for the same project entry. + for (ix, (entry_id, item_view)) in self.item_views.iter().enumerate() { + if *entry_id == item_view_to_open.project_entry_id(cx) { let item_view = item_view.boxed_clone(); self.activate_item(ix, cx); return item_view; } } - let item_view = - item_handle.add_view(cx.window_id(), workspace, self.nav_history.clone(), cx); - self.add_item_view(item_view.boxed_clone(), cx); - item_view + item_view_to_open.set_nav_history(self.nav_history.clone(), cx); + self.add_item_view(item_view_to_open.boxed_clone(), cx); + item_view_to_open } pub fn add_item_view( @@ -312,18 +308,11 @@ 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(cx).id(), item_view)); + .insert(item_idx, (item_view.project_entry_id(cx), item_view)); self.activate_item(item_idx, cx); cx.notify(); } - pub fn contains_item(&self, item: &dyn ItemHandle) -> bool { - let item_id = item.id(); - self.item_views - .iter() - .any(|(existing_item_id, _)| *existing_item_id == item_id) - } - pub fn item_views(&self) -> impl Iterator> { self.item_views.iter().map(|(_, view)| view) } @@ -334,14 +323,26 @@ impl Pane { .map(|(_, view)| view.clone()) } + pub fn item_for_entry(&self, entry: ProjectEntry) -> Option> { + self.item_views.iter().find_map(|(id, view)| { + if *id == Some(entry.entry_id) { + Some(view.boxed_clone()) + } else { + None + } + }) + } + pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option { self.item_views .iter() .position(|(_, i)| i.id() == item_view.id()) } - pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.item_views.iter().position(|(id, _)| *id == item.id()) + pub fn index_for_item(&self, item: &dyn ItemViewHandle) -> Option { + self.item_views + .iter() + .position(|(_, my_item)| my_item.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6bff12a577..5a3385a014 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,7 +9,6 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; -use collections::BTreeMap; use gpui::{ action, color::Color, @@ -18,16 +17,16 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelContext, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakModelHandle, WeakViewHandle, + AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelContext, ModelHandle, + MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectPath, Worktree}; +use project::{fs, Fs, Project, ProjectEntry, ProjectPath, Worktree}; pub use settings::Settings; use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}; use status_bar::StatusBar; @@ -35,9 +34,7 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, - cmp::Reverse, future::Future, - hash::{Hash, Hasher}, path::{Path, PathBuf}, rc::Rc, sync::Arc, @@ -131,30 +128,19 @@ pub trait PathOpener { &self, project: &mut Project, path: ProjectPath, + window_id: usize, cx: &mut ModelContext, - ) -> Option>>>; -} - -pub trait Item: Entity + Sized { - type View: ItemView; - - fn build_view( - handle: ModelHandle, - workspace: &Workspace, - nav_history: ItemNavHistory, - cx: &mut ViewContext, - ) -> Self::View; - - fn project_path(&self) -> Option; + ) -> Option>>>; } pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} - 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, _: ItemNavHistory, _: &mut ViewContext) -> Option + fn project_entry(&self, cx: &AppContext) -> Option; + fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); + fn clone_on_split(&self, _: &mut ViewContext) -> Option where Self: Sized, { @@ -202,36 +188,13 @@ pub trait ItemView: View { } } -pub trait ItemHandle: Send + Sync { - fn id(&self) -> usize; - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box; - fn boxed_clone(&self) -> Box; - fn downgrade(&self) -> Box; - fn to_any(&self) -> AnyModelHandle; - fn project_path(&self, cx: &AppContext) -> Option; -} - -pub trait WeakItemHandle { - fn id(&self) -> usize; - fn upgrade(&self, cx: &AppContext) -> Option>; -} - pub trait ItemViewHandle: 'static { - fn item(&self, cx: &AppContext) -> Box; fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; - fn clone_on_split( - &self, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Option>; + fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); + fn clone_on_split(&self, 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); @@ -256,97 +219,6 @@ pub trait WeakItemViewHandle { fn upgrade(&self, cx: &AppContext) -> Option>; } -impl ItemHandle for ModelHandle { - fn id(&self) -> usize { - self.id() - } - - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - Box::new(cx.add_view(window_id, |cx| { - let nav_history = ItemNavHistory::new(nav_history, &cx.handle()); - T::build_view(self.clone(), workspace, nav_history, cx) - })) - } - - fn boxed_clone(&self) -> Box { - Box::new(self.clone()) - } - - fn downgrade(&self) -> Box { - Box::new(self.downgrade()) - } - - fn to_any(&self) -> AnyModelHandle { - self.clone().into() - } - - fn project_path(&self, cx: &AppContext) -> Option { - self.read(cx).project_path() - } -} - -impl ItemHandle for Box { - fn id(&self) -> usize { - ItemHandle::id(self.as_ref()) - } - - fn add_view( - &self, - window_id: usize, - workspace: &Workspace, - nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Box { - ItemHandle::add_view(self.as_ref(), window_id, workspace, nav_history, cx) - } - - fn boxed_clone(&self) -> Box { - self.as_ref().boxed_clone() - } - - fn downgrade(&self) -> Box { - self.as_ref().downgrade() - } - - fn to_any(&self) -> AnyModelHandle { - self.as_ref().to_any() - } - - fn project_path(&self, cx: &AppContext) -> Option { - self.as_ref().project_path(cx) - } -} - -impl WeakItemHandle for WeakModelHandle { - fn id(&self) -> usize { - WeakModelHandle::id(self) - } - - fn upgrade(&self, cx: &AppContext) -> Option> { - WeakModelHandle::::upgrade(self, cx).map(|i| Box::new(i) as Box) - } -} - -impl Hash for Box { - fn hash(&self, state: &mut H) { - self.id().hash(state); - } -} - -impl PartialEq for Box { - fn eq(&self, other: &Self) -> bool { - self.id() == other.id() - } -} - -impl Eq for Box {} - impl dyn ItemViewHandle { pub fn downcast(&self) -> Option> { self.to_any().downcast() @@ -359,10 +231,6 @@ impl dyn ItemViewHandle { } impl ItemViewHandle for ViewHandle { - fn item(&self, cx: &AppContext) -> Box { - self.read(cx).item(cx) - } - fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { self.read(cx).tab_content(style, cx) } @@ -371,23 +239,31 @@ impl ItemViewHandle for ViewHandle { self.read(cx).project_path(cx) } + fn project_entry_id(&self, cx: &AppContext) -> Option { + Some(self.read(cx).project_entry(cx)?.entry_id) + } + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } fn clone_on_split( &self, - nav_history: Rc>, + // nav_history: Rc>, cx: &mut MutableAppContext, ) -> Option> { self.update(cx, |item, cx| { - cx.add_option_view(|cx| { - item.clone_on_split(ItemNavHistory::new(nav_history, &cx.handle()), cx) - }) + cx.add_option_view(|cx| item.clone_on_split(cx)) }) .map(|handle| Box::new(handle) as Box) } + fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext) { + self.update(cx, |item, cx| { + item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx); + }) + } + fn added_to_pane(&mut self, cx: &mut ViewContext) { cx.subscribe(self, |pane, item, event, cx| { if T::should_close_item_on_event(event) { @@ -469,12 +345,6 @@ impl Clone for Box { } } -impl Clone for Box { - fn clone(&self) -> Box { - self.boxed_clone() - } -} - impl WeakItemViewHandle for WeakViewHandle { fn id(&self) -> usize { self.id() @@ -563,7 +433,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, path_openers: Arc<[Box]>, - items: BTreeMap, Box>, + // items: BTreeMap, Box>, _observe_current_user: Task<()>, } @@ -627,7 +497,6 @@ impl Workspace { right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), path_openers: params.path_openers.clone(), - items: Default::default(), _observe_current_user, } } @@ -804,16 +673,23 @@ impl Workspace { &mut self, path: ProjectPath, cx: &mut ViewContext, - ) -> Task>> { - if let Some(existing_item) = self.item_for_path(&path, cx) { + ) -> Task>> { + let project_entry = self.project.read(cx).entry_for_path(&path, cx); + + if let Some(existing_item) = project_entry.and_then(|entry| { + self.panes + .iter() + .find_map(|pane| pane.read(cx).item_for_entry(entry)) + }) { return Task::ready(Ok(existing_item)); } let project_path = path.clone(); let path_openers = self.path_openers.clone(); + let window_id = cx.window_id(); self.project.update(cx, |project, cx| { for opener in path_openers.iter() { - if let Some(task) = opener.open(project, project_path.clone(), cx) { + if let Some(task) = opener.open(project, project_path.clone(), window_id, cx) { return task; } } @@ -821,26 +697,19 @@ impl Workspace { }) } - fn item_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option> { - self.items - .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_of_type(cx).max_by_key(|item| item.id()) } - pub fn item_of_type(&self, cx: &AppContext) -> Option> { - self.items - .values() - .find_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) - } - - pub fn items_of_type<'a, T: Item>( + pub fn items_of_type<'a, T: ItemView>( &'a self, cx: &'a AppContext, - ) -> impl 'a + Iterator> { - self.items - .values() - .filter_map(|i| i.upgrade(cx).and_then(|i| i.to_any().downcast())) + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| { + pane.read(cx) + .item_views() + .filter_map(|item| item.to_any().downcast()) + }) } pub fn active_item(&self, cx: &AppContext) -> Option> { @@ -962,52 +831,46 @@ impl Workspace { pane } - pub fn open_item( + pub fn open_item( &mut self, - item_handle: T, + item_view: Box, cx: &mut ViewContext, - ) -> Box - where - T: 'static + ItemHandle, - { - self.open_item_in_pane(item_handle, &self.active_pane().clone(), cx) + ) -> Box { + self.open_item_in_pane(item_view, &self.active_pane().clone(), cx) } - pub fn open_item_in_pane( + pub fn open_item_in_pane( &mut self, - item_handle: T, + item_view: Box, pane: &ViewHandle, cx: &mut ViewContext, + ) -> Box { + pane.update(cx, |pane, cx| pane.open_item(item_view, cx)) + } + + pub fn open_item_for_project_entry( + &mut self, + project_entry: ProjectEntry, + cx: &mut ViewContext, + build_view: F, ) -> Box where - T: 'static + ItemHandle, + T: ItemView, + F: FnOnce(&mut ViewContext) -> T, { - self.items - .insert(Reverse(item_handle.id()), item_handle.downgrade()); - pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) - } - - pub fn activate_pane_for_item( - &mut self, - item: &dyn ItemHandle, - cx: &mut ViewContext, - ) -> bool { - let pane = self.panes.iter().find_map(|pane| { - if pane.read(cx).contains_item(item) { - Some(pane.clone()) - } else { - None - } - }); - if let Some(pane) = pane { - self.activate_pane(pane.clone(), cx); - true - } else { - false + if let Some(existing_item) = self + .panes + .iter() + .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) + { + return existing_item.boxed_clone(); } + + let view = Box::new(cx.add_view(build_view)); + self.open_item(view, cx) } - pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { + pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { if let Some(ix) = pane.read(cx).index_for_item(item) { Some((pane.clone(), ix)) @@ -1078,9 +941,8 @@ impl Workspace { self.activate_pane(new_pane.clone(), cx); if let Some(item) = pane.read(cx).active_item() { 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); + if let Some(clone) = item.clone_on_split(cx.as_mut()) { + clone.set_nav_history(nav_history, cx); new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); } } From 0036e5c86c39c89b1a560a1e2ffc744d38ad0e24 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Mar 2022 15:59:47 -0600 Subject: [PATCH 004/139] Replace ProjectEntry struct with ProjectEntryId Previously, we tracked the worktree_id and entry_id separately, but now that entry ids are unique across all worktrees this is unnecessary. Co-Authored-By: Max Brunsfeld Co-Authored-By: Keith Simmons --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 4 +- crates/editor/src/items.rs | 6 +- crates/project/src/project.rs | 61 +++++++++---- crates/project/src/worktree.rs | 51 +++++------ crates/project_panel/src/project_panel.rs | 106 +++++++++------------- crates/search/src/project_search.rs | 2 +- crates/workspace/src/pane.rs | 8 +- crates/workspace/src/workspace.rs | 12 +-- 9 files changed, 126 insertions(+), 126 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1db19f7f5e..2b10adb202 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -449,7 +449,7 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } - fn project_entry(&self, _: &AppContext) -> Option { + fn project_entry_id(&self, _: &AppContext) -> Option { None } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6a29859195..704a2cf248 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -842,8 +842,8 @@ impl Editor { ) -> ViewHandle { let project = workspace.project().clone(); - if let Some(project_entry) = - project::File::from_dyn(buffer.read(cx).file()).and_then(|file| file.project_entry(cx)) + if let Some(project_entry) = project::File::from_dyn(buffer.read(cx).file()) + .and_then(|file| file.project_entry_id(cx)) { return workspace .open_item_for_project_entry(project_entry, cx, |cx| { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index b84a095e6b..a9bac1908b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,7 +5,7 @@ use gpui::{ View, ViewContext, ViewHandle, WeakModelHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectEntry, ProjectPath}; +use project::{File, Project, ProjectEntryId, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; @@ -75,8 +75,8 @@ impl ItemView for Editor { }) } - fn project_entry(&self, cx: &AppContext) -> Option { - File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry(cx)) + fn project_entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx)) } fn clone_on_split(&self, cx: &mut ViewContext) -> Option diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b9bb25a5fc..ae60ab825f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -39,7 +39,7 @@ use std::{ path::{Component, Path, PathBuf}, rc::Rc, sync::{ - atomic::{AtomicBool, AtomicUsize}, + atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, Arc, }, time::Instant, @@ -51,7 +51,7 @@ pub use worktree::*; pub struct Project { worktrees: Vec, - active_entry: Option, + active_entry: Option, languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, @@ -114,7 +114,7 @@ pub struct Collaborator { #[derive(Clone, Debug, PartialEq)] pub enum Event { - ActiveEntryChanged(Option), + ActiveEntryChanged(Option), WorktreeRemoved(WorktreeId), DiskBasedDiagnosticsStarted, DiskBasedDiagnosticsUpdated, @@ -226,10 +226,25 @@ impl DiagnosticSummary { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct ProjectEntry { - pub worktree_id: WorktreeId, - pub entry_id: usize, +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct ProjectEntryId(usize); + +impl ProjectEntryId { + pub fn new(counter: &AtomicUsize) -> Self { + Self(counter.fetch_add(1, SeqCst)) + } + + pub fn from_proto(id: u64) -> Self { + Self(id as usize) + } + + pub fn to_proto(&self) -> u64 { + self.0 as u64 + } + + pub fn to_usize(&self) -> usize { + self.0 + } } impl Project { @@ -623,6 +638,24 @@ impl Project { .find(|worktree| worktree.read(cx).id() == id) } + pub fn worktree_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.worktrees(cx) + .find(|worktree| worktree.read(cx).contains_entry(entry_id)) + } + + pub fn worktree_id_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option { + self.worktree_for_entry(entry_id, cx) + .map(|worktree| worktree.read(cx).id()) + } + pub fn share(&self, cx: &mut ModelContext) -> Task> { let rpc = self.client.clone(); cx.spawn(|this, mut cx| async move { @@ -3163,10 +3196,7 @@ impl Project { let new_active_entry = entry.and_then(|project_path| { let worktree = self.worktree_for_id(project_path.worktree_id, cx)?; let entry = worktree.read(cx).entry_for_path(project_path.path)?; - Some(ProjectEntry { - worktree_id: project_path.worktree_id, - entry_id: entry.id, - }) + Some(entry.id) }); if new_active_entry != self.active_entry { self.active_entry = new_active_entry; @@ -3217,18 +3247,15 @@ impl Project { } } - pub fn active_entry(&self) -> Option { + pub fn active_entry(&self) -> Option { self.active_entry } - pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { + pub fn entry_for_path(&self, path: &ProjectPath, cx: &AppContext) -> Option { self.worktree_for_id(path.worktree_id, cx)? .read(cx) .entry_for_path(&path.path) - .map(|entry| ProjectEntry { - worktree_id: path.worktree_id, - entry_id: entry.id, - }) + .map(|entry| entry.id) } // RPC message handlers diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 4a121c82e4..2bc9c3d234 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -1,4 +1,4 @@ -use crate::ProjectEntry; +use crate::ProjectEntryId; use super::{ fs::{self, Fs}, @@ -41,10 +41,7 @@ use std::{ future::Future, ops::{Deref, DerefMut}, path::{Path, PathBuf}, - sync::{ - atomic::{AtomicUsize, Ordering::SeqCst}, - Arc, - }, + sync::{atomic::AtomicUsize, Arc}, time::{Duration, SystemTime}, }; use sum_tree::{Bias, Edit, SeekTarget, SumTree, TreeMap}; @@ -103,7 +100,7 @@ pub struct LocalSnapshot { abs_path: Arc, scan_id: usize, ignores: HashMap, (Arc, usize)>, - removed_entry_ids: HashMap, + removed_entry_ids: HashMap, next_entry_id: Arc, snapshot: Snapshot, } @@ -858,13 +855,16 @@ impl Snapshot { self.id } + pub fn contains_entry(&self, entry_id: ProjectEntryId) -> bool { + self.entries_by_id.get(&entry_id, &()).is_some() + } + pub(crate) fn apply_remote_update(&mut self, update: proto::UpdateWorktree) -> Result<()> { let mut entries_by_path_edits = Vec::new(); let mut entries_by_id_edits = Vec::new(); for entry_id in update.removed_entries { - let entry_id = entry_id as usize; let entry = self - .entry_for_id(entry_id) + .entry_for_id(ProjectEntryId::from_proto(entry_id)) .ok_or_else(|| anyhow!("unknown entry"))?; entries_by_path_edits.push(Edit::Remove(PathKey(entry.path.clone()))); entries_by_id_edits.push(Edit::Remove(entry.id)); @@ -987,7 +987,7 @@ impl Snapshot { }) } - pub fn entry_for_id(&self, id: usize) -> Option<&Entry> { + pub fn entry_for_id(&self, id: ProjectEntryId) -> Option<&Entry> { let entry = self.entries_by_id.get(&id, &())?; self.entry_for_path(&entry.path) } @@ -1064,7 +1064,7 @@ impl LocalSnapshot { other_entries.next(); } Ordering::Greater => { - removed_entries.push(other_entry.id as u64); + removed_entries.push(other_entry.id.to_proto()); other_entries.next(); } } @@ -1075,7 +1075,7 @@ impl LocalSnapshot { self_entries.next(); } (None, Some(other_entry)) => { - removed_entries.push(other_entry.id as u64); + removed_entries.push(other_entry.id.to_proto()); other_entries.next(); } (None, None) => break, @@ -1328,7 +1328,7 @@ pub struct File { pub worktree: ModelHandle, pub path: Arc, pub mtime: SystemTime, - pub(crate) entry_id: Option, + pub(crate) entry_id: Option, pub(crate) is_local: bool, } @@ -1425,7 +1425,7 @@ impl language::File for File { fn to_proto(&self) -> rpc::proto::File { rpc::proto::File { worktree_id: self.worktree.id() as u64, - entry_id: self.entry_id.map(|entry_id| entry_id as u64), + entry_id: self.entry_id.map(|entry_id| entry_id.to_proto()), path: self.path.to_string_lossy().into(), mtime: Some(self.mtime.into()), } @@ -1492,7 +1492,7 @@ impl File { worktree, path: Path::new(&proto.path).into(), mtime: proto.mtime.ok_or_else(|| anyhow!("no timestamp"))?.into(), - entry_id: proto.entry_id.map(|entry_id| entry_id as usize), + entry_id: proto.entry_id.map(ProjectEntryId::from_proto), is_local: false, }) } @@ -1505,17 +1505,14 @@ impl File { self.worktree.read(cx).id() } - pub fn project_entry(&self, cx: &AppContext) -> Option { - self.entry_id.map(|entry_id| ProjectEntry { - worktree_id: self.worktree_id(cx), - entry_id, - }) + pub fn project_entry_id(&self, _: &AppContext) -> Option { + self.entry_id } } #[derive(Clone, Debug, PartialEq, Eq)] pub struct Entry { - pub id: usize, + pub id: ProjectEntryId, pub kind: EntryKind, pub path: Arc, pub inode: u64, @@ -1539,7 +1536,7 @@ impl Entry { root_char_bag: CharBag, ) -> Self { Self { - id: next_entry_id.fetch_add(1, SeqCst), + id: ProjectEntryId::new(next_entry_id), kind: if metadata.is_dir { EntryKind::PendingDir } else { @@ -1629,7 +1626,7 @@ impl sum_tree::Summary for EntrySummary { #[derive(Clone, Debug)] struct PathEntry { - id: usize, + id: ProjectEntryId, path: Arc, is_ignored: bool, scan_id: usize, @@ -1644,7 +1641,7 @@ impl sum_tree::Item for PathEntry { } impl sum_tree::KeyedItem for PathEntry { - type Key = usize; + type Key = ProjectEntryId; fn key(&self) -> Self::Key { self.id @@ -1653,7 +1650,7 @@ impl sum_tree::KeyedItem for PathEntry { #[derive(Clone, Debug, Default)] struct PathEntrySummary { - max_id: usize, + max_id: ProjectEntryId, } impl sum_tree::Summary for PathEntrySummary { @@ -1664,7 +1661,7 @@ impl sum_tree::Summary for PathEntrySummary { } } -impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for usize { +impl<'a> sum_tree::Dimension<'a, PathEntrySummary> for ProjectEntryId { fn add_summary(&mut self, summary: &'a PathEntrySummary, _: &()) { *self = summary.max_id; } @@ -2354,7 +2351,7 @@ impl<'a> Iterator for ChildEntriesIter<'a> { impl<'a> From<&'a Entry> for proto::Entry { fn from(entry: &'a Entry) -> Self { Self { - id: entry.id as u64, + id: entry.id.to_proto(), is_dir: entry.is_dir(), path: entry.path.to_string_lossy().to_string(), inode: entry.inode, @@ -2379,7 +2376,7 @@ impl<'a> TryFrom<(&'a CharBag, proto::Entry)> for Entry { }; let path: Arc = Arc::from(Path::new(&entry.path)); Ok(Entry { - id: entry.id as usize, + id: ProjectEntryId::from_proto(entry.id), kind, path: path.clone(), inode: entry.inode, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 7f03005b80..372f0c169b 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -9,7 +9,7 @@ use gpui::{ AppContext, Element, ElementBox, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{Project, ProjectEntry, ProjectPath, Worktree, WorktreeId}; +use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use std::{ collections::{hash_map, HashMap}, ffi::OsStr, @@ -24,7 +24,7 @@ pub struct ProjectPanel { project: ModelHandle, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, - expanded_dir_ids: HashMap>, + expanded_dir_ids: HashMap>, selection: Option, handle: WeakViewHandle, } @@ -32,7 +32,7 @@ pub struct ProjectPanel { #[derive(Copy, Clone)] struct Selection { worktree_id: WorktreeId, - entry_id: usize, + entry_id: ProjectEntryId, index: usize, } @@ -47,8 +47,8 @@ struct EntryDetails { action!(ExpandSelectedEntry); action!(CollapseSelectedEntry); -action!(ToggleExpanded, ProjectEntry); -action!(Open, ProjectEntry); +action!(ToggleExpanded, ProjectEntryId); +action!(Open, ProjectEntryId); pub fn init(cx: &mut MutableAppContext) { cx.add_action(ProjectPanel::expand_selected_entry); @@ -64,10 +64,7 @@ pub fn init(cx: &mut MutableAppContext) { } pub enum Event { - OpenedEntry { - worktree_id: WorktreeId, - entry_id: usize, - }, + OpenedEntry(ProjectEntryId), } impl ProjectPanel { @@ -78,15 +75,15 @@ impl ProjectPanel { cx.notify(); }) .detach(); - cx.subscribe(&project, |this, _, event, cx| match event { - project::Event::ActiveEntryChanged(Some(ProjectEntry { - worktree_id, - entry_id, - })) => { - this.expand_entry(*worktree_id, *entry_id, cx); - this.update_visible_entries(Some((*worktree_id, *entry_id)), cx); - this.autoscroll(); - cx.notify(); + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(); + cx.notify(); + } } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); @@ -109,16 +106,13 @@ impl ProjectPanel { this }); cx.subscribe(&project_panel, move |workspace, _, event, cx| match event { - &Event::OpenedEntry { - worktree_id, - entry_id, - } => { - if let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) { + &Event::OpenedEntry(entry_id) => { + if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { workspace .open_path( ProjectPath { - worktree_id, + worktree_id: worktree.read(cx).id(), path: entry.path.clone(), }, cx, @@ -152,10 +146,7 @@ impl ProjectPanel { } } } else { - let event = Event::OpenedEntry { - worktree_id: worktree.id(), - entry_id: entry.id, - }; + let event = Event::OpenedEntry(entry.id); cx.emit(event); } } @@ -193,22 +184,20 @@ impl ProjectPanel { } fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let ProjectEntry { - worktree_id, - entry_id, - } = action.0; - - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - expanded_dir_ids.insert(ix, entry_id); + let entry_id = action.0; + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + expanded_dir_ids.insert(ix, entry_id); + } } + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus_self(); } - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.focus_self(); } } @@ -229,10 +218,7 @@ impl ProjectPanel { } fn open_entry(&mut self, action: &Open, cx: &mut ViewContext) { - cx.emit(Event::OpenedEntry { - worktree_id: action.0.worktree_id, - entry_id: action.0.entry_id, - }); + cx.emit(Event::OpenedEntry(action.0)); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -313,7 +299,7 @@ impl ProjectPanel { fn update_visible_entries( &mut self, - new_selected_entry: Option<(WorktreeId, usize)>, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, cx: &mut ViewContext, ) { let worktrees = self @@ -379,7 +365,7 @@ impl ProjectPanel { fn expand_entry( &mut self, worktree_id: WorktreeId, - entry_id: usize, + entry_id: ProjectEntryId, cx: &mut ViewContext, ) { let project = self.project.read(cx); @@ -411,7 +397,7 @@ impl ProjectPanel { &self, range: Range, cx: &mut ViewContext, - mut callback: impl FnMut(ProjectEntry, EntryDetails, &mut ViewContext), + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), ) { let mut ix = 0; for (worktree_id, visible_worktree_entries) in &self.visible_entries { @@ -450,11 +436,7 @@ impl ProjectPanel { e.worktree_id == snapshot.id() && e.entry_id == entry.id }), }; - let entry = ProjectEntry { - worktree_id: snapshot.id(), - entry_id: entry.id, - }; - callback(entry, details, cx); + callback(entry.id, details, cx); } } } @@ -463,13 +445,13 @@ impl ProjectPanel { } fn render_entry( - entry: ProjectEntry, + entry_id: ProjectEntryId, details: EntryDetails, theme: &theme::ProjectPanel, cx: &mut ViewContext, ) -> ElementBox { let is_dir = details.is_dir; - MouseEventHandler::new::(entry.entry_id, cx, |state, _| { + MouseEventHandler::new::(entry_id.to_usize(), cx, |state, _| { let style = match (details.is_selected, state.hovered) { (false, false) => &theme.entry, (false, true) => &theme.hovered_entry, @@ -519,9 +501,9 @@ impl ProjectPanel { }) .on_click(move |cx| { if is_dir { - cx.dispatch_action(ToggleExpanded(entry)) + cx.dispatch_action(ToggleExpanded(entry_id)) } else { - cx.dispatch_action(Open(entry)) + cx.dispatch_action(Open(entry_id)) } }) .with_cursor_style(CursorStyle::PointingHand) @@ -830,13 +812,7 @@ mod tests { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.toggle_expanded( - &ToggleExpanded(ProjectEntry { - worktree_id: worktree.id(), - entry_id, - }), - cx, - ); + panel.toggle_expanded(&ToggleExpanded(entry_id), cx); return; } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 56395b107a..20df486f48 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -250,7 +250,7 @@ impl ItemView for ProjectSearchView { None } - fn project_entry(&self, _: &AppContext) -> Option { + fn project_entry_id(&self, _: &AppContext) -> Option { None } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9f5ee52274..5d346279f0 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -10,7 +10,7 @@ use gpui::{ AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::{ProjectEntry, ProjectPath}; +use project::{ProjectEntryId, ProjectPath}; use std::{ any::{Any, TypeId}, cell::RefCell, @@ -97,7 +97,7 @@ pub enum Event { } pub struct Pane { - item_views: Vec<(Option, Box)>, + item_views: Vec<(Option, Box)>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -323,9 +323,9 @@ impl Pane { .map(|(_, view)| view.clone()) } - pub fn item_for_entry(&self, entry: ProjectEntry) -> Option> { + pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { self.item_views.iter().find_map(|(id, view)| { - if *id == Some(entry.entry_id) { + if *id == Some(entry_id) { Some(view.boxed_clone()) } else { None diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5a3385a014..464eecd307 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -26,7 +26,7 @@ use log::error; pub use pane::*; pub use pane_group::*; use postage::prelude::Stream; -use project::{fs, Fs, Project, ProjectEntry, ProjectPath, Worktree}; +use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, Worktree}; pub use settings::Settings; use sidebar::{Side, Sidebar, SidebarItemId, ToggleSidebarItem, ToggleSidebarItemFocus}; use status_bar::StatusBar; @@ -138,7 +138,7 @@ pub trait ItemView: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn project_entry(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -191,7 +191,7 @@ pub trait ItemView: View { pub trait ItemViewHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn project_entry_id(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -239,8 +239,8 @@ impl ItemViewHandle for ViewHandle { self.read(cx).project_path(cx) } - fn project_entry_id(&self, cx: &AppContext) -> Option { - Some(self.read(cx).project_entry(cx)?.entry_id) + fn project_entry_id(&self, cx: &AppContext) -> Option { + self.read(cx).project_entry_id(cx) } fn boxed_clone(&self) -> Box { @@ -850,7 +850,7 @@ impl Workspace { pub fn open_item_for_project_entry( &mut self, - project_entry: ProjectEntry, + project_entry: ProjectEntryId, cx: &mut ViewContext, build_view: F, ) -> Box From 1f9885ec420c952e0a792ff2238bd1c840553b17 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Mar 2022 16:08:13 -0600 Subject: [PATCH 005/139] Remove open_item_in_pane Co-Authored-By: Max Brunsfeld Co-Authored-By: Keith Simmons --- crates/workspace/src/pane.rs | 16 ++++++++-------- crates/workspace/src/workspace.rs | 24 +++++++----------------- 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 5d346279f0..7a7b73199e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -256,16 +256,16 @@ impl Pane { let item = task.await; if let Some(pane) = pane.upgrade(&cx) { if let Some(item) = item.log_err() { - workspace.update(&mut cx, |workspace, cx| { - pane.update(cx, |p, _| p.nav_history.borrow_mut().set_mode(mode)); - let item_view = workspace.open_item_in_pane(item, &pane, cx); - pane.update(cx, |p, _| { - p.nav_history.borrow_mut().set_mode(NavigationMode::Normal) - }); - + pane.update(&mut cx, |pane, cx| { + pane.nav_history.borrow_mut().set_mode(mode); + let item = pane.open_item(item, cx); + pane.nav_history + .borrow_mut() + .set_mode(NavigationMode::Normal); if let Some(data) = entry.data { - item_view.navigate(data, cx); + item.navigate(data, cx); } + item }); } else { workspace diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 464eecd307..9692a8c900 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -658,14 +658,12 @@ impl Workspace { ) -> Task, Arc>> { let load_task = self.load_path(path, cx); let pane = self.active_pane().clone().downgrade(); - cx.spawn(|this, mut cx| async move { + cx.as_mut().spawn(|mut cx| async move { let item = load_task.await?; - this.update(&mut cx, |this, cx| { - let pane = pane - .upgrade(cx) - .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - Ok(this.open_item_in_pane(item, &pane, cx)) - }) + let pane = pane + .upgrade(&cx) + .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; + Ok(pane.update(&mut cx, |pane, cx| pane.open_item(item, cx))) }) } @@ -836,16 +834,8 @@ impl Workspace { item_view: Box, cx: &mut ViewContext, ) -> Box { - self.open_item_in_pane(item_view, &self.active_pane().clone(), cx) - } - - pub fn open_item_in_pane( - &mut self, - item_view: Box, - pane: &ViewHandle, - cx: &mut ViewContext, - ) -> Box { - pane.update(cx, |pane, cx| pane.open_item(item_view, cx)) + self.active_pane() + .update(cx, |pane, cx| pane.open_item(item_view, cx)) } pub fn open_item_for_project_entry( From 728c7081509740ecb02a6e24e625da53715afd67 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 16 Mar 2022 17:40:09 -0600 Subject: [PATCH 006/139] WIP: Massage opening of editors Co-Authored-By: Max Brunsfeld --- crates/diagnostics/src/diagnostics.rs | 8 +- crates/editor/src/editor.rs | 9 +- crates/editor/src/items.rs | 8 +- crates/project/src/project.rs | 103 +++++++++----- crates/search/src/project_search.rs | 4 - crates/server/src/rpc.rs | 102 ++++++++++---- crates/workspace/src/pane.rs | 48 +++++-- crates/workspace/src/workspace.rs | 190 ++++++++++++++++++++------ 8 files changed, 338 insertions(+), 134 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2b10adb202..7c69f85acf 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -155,7 +155,9 @@ impl ProjectDiagnosticsEditor { async move { for path in paths { let buffer = project - .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx)) + .update(&mut cx, |project, cx| { + project.open_buffer_for_path(path.clone(), cx) + }) .await?; this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx)) } @@ -449,10 +451,6 @@ impl workspace::ItemView for ProjectDiagnosticsEditor { None } - fn project_entry_id(&self, _: &AppContext) -> Option { - None - } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 704a2cf248..39dad90ece 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -846,10 +846,7 @@ impl Editor { .and_then(|file| file.project_entry_id(cx)) { return workspace - .open_item_for_project_entry(project_entry, cx, |cx| { - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - Editor::for_buffer(multibuffer, Some(project.clone()), cx) - }) + .open_editor(project_entry, cx) .downcast::() .unwrap(); } @@ -8442,7 +8439,9 @@ mod tests { .0 .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .update(cx, |project, cx| { + project.open_buffer_for_path((worktree_id, ""), cx) + }) .await .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index a9bac1908b..6fec3d7a1c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,7 +5,7 @@ use gpui::{ View, ViewContext, ViewHandle, WeakModelHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectEntryId, ProjectPath}; +use project::{File, Project, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; @@ -34,7 +34,7 @@ impl PathOpener for BufferOpener { window_id: usize, cx: &mut ModelContext, ) -> Option>>> { - let buffer = project.open_buffer(project_path, cx); + let buffer = project.open_buffer_for_path(project_path, cx); Some(cx.spawn(|project, mut cx| async move { let buffer = buffer.await?; let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -75,10 +75,6 @@ impl ItemView for Editor { }) } - fn project_entry_id(&self, cx: &AppContext) -> Option { - File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx)) - } - fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ae60ab825f..3484796dd9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -818,7 +818,19 @@ impl Project { Ok(buffer) } - pub fn open_buffer( + pub fn open_buffer_for_entry( + &mut self, + entry_id: ProjectEntryId, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(project_path) = self.path_for_entry(entry_id, cx) { + self.open_buffer_for_path(project_path, cx) + } else { + Task::ready(Err(anyhow!("entry not found"))) + } + } + + pub fn open_buffer_for_path( &mut self, path: impl Into, cx: &mut ModelContext, @@ -953,8 +965,10 @@ impl Project { worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), path: relative_path.into(), }; - this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await + this.update(&mut cx, |this, cx| { + this.open_buffer_for_path(project_path, cx) + }) + .await }) } @@ -2854,7 +2868,9 @@ impl Project { 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)) + .update(&mut cx, |this, cx| { + this.open_buffer_for_path(project_path, cx) + }) .await .log_err() { @@ -3258,6 +3274,14 @@ impl Project { .map(|entry| entry.id) } + pub fn path_for_entry(&self, entry_id: ProjectEntryId, cx: &AppContext) -> Option { + let worktree = self.worktree_for_entry(entry_id, cx)?; + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + let path = worktree.entry_for_id(entry_id)?.path.clone(); + Some(ProjectPath { worktree_id, path }) + } + // RPC message handlers async fn handle_unshare_project( @@ -3867,7 +3891,7 @@ impl Project { let peer_id = envelope.original_sender_id()?; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let open_buffer = this.update(&mut cx, |this, cx| { - this.open_buffer( + this.open_buffer_for_path( ProjectPath { worktree_id, path: PathBuf::from(envelope.payload.path).into(), @@ -4664,7 +4688,7 @@ mod tests { // Open a buffer without an associated language server. let toml_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "Cargo.toml"), cx) + project.open_buffer_for_path((worktree_id, "Cargo.toml"), cx) }) .await .unwrap(); @@ -4672,7 +4696,7 @@ mod tests { // Open a buffer with an associated language server. let rust_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test.rs"), cx) + project.open_buffer_for_path((worktree_id, "test.rs"), cx) }) .await .unwrap(); @@ -4719,7 +4743,7 @@ mod tests { // Open a third buffer with a different associated language server. let json_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "package.json"), cx) + project.open_buffer_for_path((worktree_id, "package.json"), cx) }) .await .unwrap(); @@ -4750,7 +4774,7 @@ mod tests { // it is also configured based on the existing language server's capabilities. let rust_buffer2 = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "test2.rs"), cx) + project.open_buffer_for_path((worktree_id, "test2.rs"), cx) }) .await .unwrap(); @@ -4861,7 +4885,7 @@ mod tests { // Cause worktree to start the fake language server let _buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, Path::new("b.rs")), cx) + project.open_buffer_for_path((worktree_id, Path::new("b.rs")), cx) }) .await .unwrap(); @@ -4908,7 +4932,9 @@ mod tests { ); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + }) .await .unwrap(); @@ -4975,7 +5001,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) + project.open_buffer_for_path((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5251,7 +5277,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) + project.open_buffer_for_path((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5356,7 +5382,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) + project.open_buffer_for_path((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5514,7 +5540,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "a.rs"), cx) + project.open_buffer_for_path((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5697,7 +5723,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer( + project.open_buffer_for_path( ProjectPath { worktree_id, path: Path::new("").into(), @@ -5793,7 +5819,9 @@ mod tests { .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "file1"), cx) + }) .await .unwrap(); buffer @@ -5831,7 +5859,7 @@ mod tests { .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, ""), cx)) + .update(cx, |p, cx| p.open_buffer_for_path((worktree_id, ""), cx)) .await .unwrap(); buffer @@ -5881,7 +5909,7 @@ mod tests { let opened_buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "file1"), cx) + project.open_buffer_for_path((worktree_id, "file1"), cx) }) .await .unwrap(); @@ -5916,7 +5944,8 @@ mod tests { let worktree_id = tree.read_with(cx, |tree, _| tree.id()); let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { - let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, path), cx)); + let buffer = + project.update(cx, |p, cx| p.open_buffer_for_path((worktree_id, path), cx)); async move { buffer.await.unwrap() } }; let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { @@ -6065,9 +6094,9 @@ mod tests { // Spawn multiple tasks to open paths, repeating some paths. let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { ( - p.open_buffer((worktree_id, "a.txt"), cx), - p.open_buffer((worktree_id, "b.txt"), cx), - p.open_buffer((worktree_id, "a.txt"), cx), + p.open_buffer_for_path((worktree_id, "a.txt"), cx), + p.open_buffer_for_path((worktree_id, "b.txt"), cx), + p.open_buffer_for_path((worktree_id, "a.txt"), cx), ) }); @@ -6084,7 +6113,9 @@ mod tests { // Open the same path again while it is still open. drop(buffer_a_1); let buffer_a_3 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); @@ -6117,7 +6148,9 @@ mod tests { .await; let buffer1 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "file1"), cx) + }) .await .unwrap(); let events = Rc::new(RefCell::new(Vec::new())); @@ -6187,7 +6220,9 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); let buffer2 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file2"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "file2"), cx) + }) .await .unwrap(); buffer2.update(cx, |_, cx| { @@ -6208,7 +6243,9 @@ mod tests { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Rc::new(RefCell::new(Vec::new())); let buffer3 = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "file3"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "file3"), cx) + }) .await .unwrap(); buffer3.update(cx, |_, cx| { @@ -6254,7 +6291,9 @@ mod tests { let abs_path = dir.path().join("the-file"); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "the-file"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "the-file"), cx) + }) .await .unwrap(); @@ -6360,7 +6399,9 @@ mod tests { let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) + .update(cx, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + }) .await .unwrap(); @@ -6633,7 +6674,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, Path::new("one.rs")), cx) + project.open_buffer_for_path((worktree_id, Path::new("one.rs")), cx) }) .await .unwrap(); @@ -6771,7 +6812,7 @@ mod tests { let buffer_4 = project .update(cx, |project, cx| { - project.open_buffer((worktree_id, "four.rs"), cx) + project.open_buffer_for_path((worktree_id, "four.rs"), cx) }) .await .unwrap(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 20df486f48..6208ff4aaa 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -250,10 +250,6 @@ impl ItemView for ProjectSearchView { None } - fn project_entry_id(&self, _: &AppContext) -> Option { - None - } - fn can_save(&self, _: &gpui::AppContext) -> bool { true } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index ed45c2d5d6..f891dca71c 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1137,7 +1137,9 @@ mod tests { // Open the same file as client B and client A. let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "b.txt"), cx) + }) .await .unwrap(); let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); @@ -1148,7 +1150,9 @@ mod tests { assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) }); let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) + .update(cx_a, |p, cx| { + p.open_buffer_for_path((worktree_id, "b.txt"), cx) + }) .await .unwrap(); @@ -1238,7 +1242,9 @@ mod tests { .await .unwrap(); project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); @@ -1273,7 +1279,9 @@ mod tests { .await .unwrap(); project_b2 - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); } @@ -1352,11 +1360,15 @@ mod tests { // Open and edit a buffer as both guests B and C. let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "file1"), cx) + }) .await .unwrap(); let buffer_c = project_c - .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx_c, |p, cx| { + p.open_buffer_for_path((worktree_id, "file1"), cx) + }) .await .unwrap(); buffer_b.update(cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); @@ -1364,7 +1376,9 @@ mod tests { // Open and edit that buffer as the host. let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) + .update(cx_a, |p, cx| { + p.open_buffer_for_path((worktree_id, "file1"), cx) + }) .await .unwrap(); @@ -1514,7 +1528,9 @@ mod tests { // Open a buffer as client B let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); @@ -1597,7 +1613,9 @@ mod tests { // Open a buffer as client B let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); buffer_b.read_with(cx_b, |buf, _| { @@ -1677,14 +1695,16 @@ mod tests { // Open a buffer as client A let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .update(cx_a, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + }) .await .unwrap(); // Start opening the same buffer as client B - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); + let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + })); // Edit the buffer as client A while client B is still opening it. cx_b.background().simulate_random_delay().await; @@ -1760,9 +1780,9 @@ mod tests { .await; // Begin opening a buffer as client B, but leave the project before the open completes. - let buffer_b = cx_b - .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); + let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.txt"), cx) + })); cx_b.update(|_| drop(project_b)); drop(buffer_b); @@ -1932,7 +1952,7 @@ mod tests { let _ = cx_a .background() .spawn(project_a.update(cx_a, |project, cx| { - project.open_buffer( + project.open_buffer_for_path( ProjectPath { worktree_id, path: Path::new("other.rs").into(), @@ -2053,7 +2073,9 @@ mod tests { // Open the file with the errors on client B. They should be present. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + })) .await .unwrap(); @@ -2171,7 +2193,9 @@ mod tests { // Open a file in an editor as the guest. let buffer_b = project_b - .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "main.rs"), cx) + }) .await .unwrap(); let (window_b, _) = cx_b.add_window(|_| EmptyView); @@ -2245,7 +2269,9 @@ mod tests { // Open the buffer on the host. let buffer_a = project_a - .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) + .update(cx_a, |p, cx| { + p.open_buffer_for_path((worktree_id, "main.rs"), cx) + }) .await .unwrap(); buffer_a @@ -2369,7 +2395,9 @@ mod tests { let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + })) .await .unwrap(); @@ -2477,7 +2505,9 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + })) .await .unwrap(); @@ -2616,7 +2646,9 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "one.rs"), cx) + })) .await .unwrap(); @@ -2845,7 +2877,9 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "main.rs"), cx) + })) .await .unwrap(); @@ -2992,7 +3026,9 @@ mod tests { // Cause the language server to start. let _buffer = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "one.rs"), cx) + })) .await .unwrap(); @@ -3123,7 +3159,9 @@ mod tests { let buffer_b1 = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) + .spawn(project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "a.rs"), cx) + })) .await .unwrap(); @@ -3139,9 +3177,13 @@ mod tests { let buffer_b2; if rng.gen() { definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); - buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); + buffer_b2 = project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "b.rs"), cx) + }); } else { - buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); + buffer_b2 = project_b.update(cx_b, |p, cx| { + p.open_buffer_for_path((worktree_id, "b.rs"), cx) + }); definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); } @@ -4762,7 +4804,7 @@ mod tests { ); let buffer = project .update(&mut cx, |project, cx| { - project.open_buffer(project_path, cx) + project.open_buffer_for_path(project_path, cx) }) .await .unwrap(); @@ -4879,7 +4921,7 @@ mod tests { ); let buffer = project .update(&mut cx, |project, cx| { - project.open_buffer(project_path.clone(), cx) + project.open_buffer_for_path(project_path.clone(), cx) }) .await .unwrap(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 7a7b73199e..73420e213a 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -258,14 +258,13 @@ impl Pane { if let Some(item) = item.log_err() { pane.update(&mut cx, |pane, cx| { pane.nav_history.borrow_mut().set_mode(mode); - let item = pane.open_item(item, cx); + pane.open_item(item, cx); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); if let Some(data) = entry.data { item.navigate(data, cx); } - item }); } else { workspace @@ -281,34 +280,43 @@ impl Pane { } } - pub fn open_item( + pub(crate) fn open_editor( &mut self, - item_view_to_open: Box, + project_entry_id: ProjectEntryId, cx: &mut ViewContext, + build_editor: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { - // Find an existing view for the same project entry. - for (ix, (entry_id, item_view)) in self.item_views.iter().enumerate() { - if *entry_id == item_view_to_open.project_entry_id(cx) { + for (ix, (existing_entry_id, item_view)) in self.item_views.iter().enumerate() { + if *existing_entry_id == Some(project_entry_id) { let item_view = item_view.boxed_clone(); self.activate_item(ix, cx); return item_view; } } - item_view_to_open.set_nav_history(self.nav_history.clone(), cx); - self.add_item_view(item_view_to_open.boxed_clone(), cx); - item_view_to_open + let item_view = build_editor(cx); + self.add_item(Some(project_entry_id), item_view.boxed_clone(), cx); + item_view } - pub fn add_item_view( + pub fn open_item( &mut self, - mut item_view: Box, + item_view_to_open: Box, cx: &mut ViewContext, ) { - item_view.added_to_pane(cx); + self.add_item(None, item_view_to_open.boxed_clone(), cx); + } + + pub(crate) fn add_item( + &mut self, + project_entry_id: Option, + mut item: Box, + cx: &mut ViewContext, + ) { + item.set_nav_history(self.nav_history.clone(), cx); + item.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.project_entry_id(cx), item_view)); + self.item_views.insert(item_idx, (project_entry_id, item)); self.activate_item(item_idx, cx); cx.notify(); } @@ -323,6 +331,16 @@ impl Pane { .map(|(_, view)| view.clone()) } + pub fn project_entry_id_for_item(&self, item: &dyn ItemViewHandle) -> Option { + self.item_views.iter().find_map(|(entry_id, existing)| { + if existing.id() == item.id() { + *entry_id + } else { + None + } + }) + } + pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { self.item_views.iter().find_map(|(id, view)| { if *id == Some(entry_id) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9692a8c900..d11e773c3f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,6 +9,7 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; +use futures::TryFutureExt; use gpui::{ action, color::Color, @@ -21,7 +22,7 @@ use gpui::{ MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::LanguageRegistry; +use language::{Buffer, LanguageRegistry}; use log::error; pub use pane::*; pub use pane_group::*; @@ -41,6 +42,15 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; +pub type BuildEditor = Box< + dyn Fn( + usize, + ModelHandle, + ModelHandle, + &mut MutableAppContext, + ) -> Box, +>; + action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); @@ -95,6 +105,16 @@ pub fn init(cx: &mut MutableAppContext) { ]); } +pub fn register_editor_builder(cx: &mut MutableAppContext, build_editor: F) +where + V: ItemView, + F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, +{ + cx.add_app_state::(Box::new(|window_id, project, model, cx| { + Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx))) + })); +} + pub struct AppState { pub languages: Arc, pub themes: Arc, @@ -138,7 +158,6 @@ pub trait ItemView: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn project_entry_id(&self, cx: &AppContext) -> Option; fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -191,7 +210,6 @@ pub trait ItemView: View { pub trait ItemViewHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn project_entry_id(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -239,10 +257,6 @@ impl ItemViewHandle for ViewHandle { self.read(cx).project_path(cx) } - fn project_entry_id(&self, cx: &AppContext) -> Option { - self.read(cx).project_entry_id(cx) - } - fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -656,6 +670,24 @@ impl Workspace { path: ProjectPath, cx: &mut ViewContext, ) -> Task, Arc>> { + let project_entry = self.project.read(cx).entry_for_path(&path, cx); + + let existing_entry = self + .active_pane() + .update(cx, |pane, cx| pane.activate_project_entry(project_entry)); + + cx.spawn(|this, cx| { + if let Some(existing_entry) = existing_entry { + return Ok(existing_entry); + } + + let load_task = this + .update(&mut cx, |this, cx| { + this.load_project_entry(project_entry, cx) + }) + .await; + }); + let load_task = self.load_path(path, cx); let pane = self.active_pane().clone().downgrade(); cx.as_mut().spawn(|mut cx| async move { @@ -663,7 +695,7 @@ impl Workspace { let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - Ok(pane.update(&mut cx, |pane, cx| pane.open_item(item, cx))) + Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx))) }) } @@ -672,13 +704,23 @@ impl Workspace { path: ProjectPath, cx: &mut ViewContext, ) -> Task>> { - let project_entry = self.project.read(cx).entry_for_path(&path, cx); + if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) { + self.load_project_entry(project_entry, cx) + } else { + Task::ready(Err(anyhow!("no such file {:?}", path))) + } + } - if let Some(existing_item) = project_entry.and_then(|entry| { - self.panes - .iter() - .find_map(|pane| pane.read(cx).item_for_entry(entry)) - }) { + pub fn load_project_entry( + &mut self, + project_entry: ProjectEntryId, + cx: &mut ViewContext, + ) -> Task>> { + if let Some(existing_item) = self + .panes + .iter() + .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) + { return Task::ready(Ok(existing_item)); } @@ -829,37 +871,108 @@ impl Workspace { pane } - pub fn open_item( - &mut self, - item_view: Box, - cx: &mut ViewContext, - ) -> Box { + pub fn open_item(&mut self, item_view: Box, cx: &mut ViewContext) { self.active_pane() .update(cx, |pane, cx| pane.open_item(item_view, cx)) } - pub fn open_item_for_project_entry( + pub fn open_editor( &mut self, project_entry: ProjectEntryId, cx: &mut ViewContext, - build_view: F, - ) -> Box - where - T: ItemView, - F: FnOnce(&mut ViewContext) -> T, - { - if let Some(existing_item) = self - .panes - .iter() - .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) - { - return existing_item.boxed_clone(); - } + ) -> Task, Arc>> { + let pane = self.active_pane().clone(); + let project = self.project().clone(); + let buffer = project.update(cx, |project, cx| { + project.open_buffer_for_entry(project_entry, cx) + }); - let view = Box::new(cx.add_view(build_view)); - self.open_item(view, cx) + cx.spawn(|this, cx| async move { + let buffer = buffer.await?; + let editor = this.update(&mut cx, |this, cx| { + let window_id = cx.window_id(); + pane.update(cx, |pane, cx| { + pane.open_editor(project_entry, cx, |cx| { + cx.app_state::()(window_id, project, buffer, cx) + }) + }) + }); + Ok(editor) + }) } + // pub fn open_path( + // &mut self, + // path: ProjectPath, + // cx: &mut ViewContext, + // ) -> Task, Arc>> { + // let project_entry = self.project.read(cx).entry_for_path(&path, cx); + + // let existing_entry = self + // .active_pane() + // .update(cx, |pane, cx| pane.activate_project_entry(project_entry)); + + // cx.spawn(|this, cx| { + // if let Some(existing_entry) = existing_entry { + // return Ok(existing_entry); + // } + + // let load_task = this + // .update(&mut cx, |this, cx| { + // this.load_project_entry(project_entry, cx) + // }) + // .await; + // }); + + // let load_task = self.load_path(path, cx); + // let pane = self.active_pane().clone().downgrade(); + // cx.as_mut().spawn(|mut cx| async move { + // let item = load_task.await?; + // let pane = pane + // .upgrade(&cx) + // .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; + // Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx))) + // }) + // } + + // pub fn load_path( + // &mut self, + // path: ProjectPath, + // cx: &mut ViewContext, + // ) -> Task>> { + // if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) { + // self.load_project_entry(project_entry, cx) + // } else { + // Task::ready(Err(anyhow!("no such file {:?}", path))) + // } + // } + + // pub fn load_project_entry( + // &mut self, + // project_entry: ProjectEntryId, + // cx: &mut ViewContext, + // ) -> Task>> { + // if let Some(existing_item) = self + // .panes + // .iter() + // .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) + // { + // return Task::ready(Ok(existing_item)); + // } + + // let project_path = path.clone(); + // let path_openers = self.path_openers.clone(); + // let window_id = cx.window_id(); + // self.project.update(cx, |project, cx| { + // for opener in path_openers.iter() { + // if let Some(task) = opener.open(project, project_path.clone(), window_id, cx) { + // return task; + // } + // } + // Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) + // }) + // } + pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { if let Some(ix) = pane.read(cx).index_for_item(item) { @@ -930,10 +1043,11 @@ 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() { - let nav_history = new_pane.read(cx).nav_history().clone(); + let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref()); if let Some(clone) = item.clone_on_split(cx.as_mut()) { - clone.set_nav_history(nav_history, cx); - new_pane.update(cx, |new_pane, cx| new_pane.add_item_view(clone, cx)); + new_pane.update(cx, |new_pane, cx| { + new_pane.open_item(project_entry_id, clone, cx); + }); } } self.center.split(&pane, &new_pane, direction).unwrap(); From aced1e2315b8eee5ce2baf2f4769da91f103a647 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 11:29:46 +0100 Subject: [PATCH 007/139] Finish refactoring of how editors are opened --- crates/diagnostics/src/diagnostics.rs | 2 +- crates/editor/src/editor.rs | 41 +++-- crates/editor/src/items.rs | 28 +-- crates/file_finder/src/file_finder.rs | 6 +- crates/gpui/src/app.rs | 6 + crates/search/src/project_search.rs | 4 +- crates/server/src/rpc.rs | 8 +- crates/workspace/src/pane.rs | 16 +- crates/workspace/src/workspace.rs | 236 ++++++-------------------- crates/zed/src/main.rs | 4 +- crates/zed/src/test.rs | 4 +- crates/zed/src/zed.rs | 1 - 12 files changed, 96 insertions(+), 260 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7c69f85acf..dbc8fc3e82 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -144,7 +144,7 @@ impl ProjectDiagnosticsEditor { let diagnostics = cx.add_view(|cx| { ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) }); - workspace.open_item(Box::new(diagnostics), cx); + workspace.add_item(Box::new(diagnostics), cx); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 39dad90ece..5a5a7cd5e6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -57,7 +57,7 @@ pub use sum_tree::Bias; use text::rope::TextDimension; use theme::DiagnosticStyle; use util::{post_inc, ResultExt, TryFutureExt}; -use workspace::{settings, ItemNavHistory, PathOpener, Settings, Workspace}; +use workspace::{settings, ItemNavHistory, Settings, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -141,8 +141,7 @@ pub enum Direction { Next, } -pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec>) { - path_openers.push(Box::new(items::BufferOpener)); +pub fn init(cx: &mut MutableAppContext) { cx.add_bindings(vec![ Binding::new("escape", Cancel, Some("Editor")), Binding::new("backspace", Backspace, Some("Editor")), @@ -340,6 +339,11 @@ pub fn init(cx: &mut MutableAppContext, path_openers: &mut Vec ViewHandle { let project = workspace.project().clone(); - if let Some(project_entry) = project::File::from_dyn(buffer.read(cx).file()) + if let Some(item) = project::File::from_dyn(buffer.read(cx).file()) .and_then(|file| file.project_entry_id(cx)) + .and_then(|entry_id| workspace.item_for_entry(entry_id, cx)) + .and_then(|item| item.downcast()) { - return workspace - .open_editor(project_entry, cx) - .downcast::() - .unwrap(); + return item; } let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)); - workspace.open_item(Box::new(editor.clone()), cx); + workspace.add_item(Box::new(editor.clone()), cx); editor } @@ -966,7 +969,7 @@ impl Editor { .log_err() { let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - workspace.open_item( + workspace.add_item( Box::new( cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)), ), @@ -2376,16 +2379,12 @@ impl Editor { workspace.update(&mut cx, |workspace, cx| { let project = workspace.project().clone(); - let editor = workspace.open_item( - Box::new(cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx))), - cx, - ); - if let Some(editor) = editor.act_as::(cx) { - editor.update(cx, |editor, cx| { - let color = editor.style(cx).highlighted_line_background; - editor.highlight_background::(ranges_to_highlight, color, cx); - }); - } + let editor = cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx)); + workspace.add_item(Box::new(editor.clone()), cx); + editor.update(cx, |editor, cx| { + let color = editor.style(cx).highlighted_line_background; + editor.highlight_background::(ranges_to_highlight, color, cx); + }); }); Ok(()) @@ -4402,7 +4401,7 @@ impl Editor { let color = editor.style(cx).highlighted_line_background; editor.highlight_background::(ranges_to_highlight, color, cx); }); - workspace.open_item(Box::new(editor), cx); + workspace.add_item(Box::new(editor), cx); }); Ok(()) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6fec3d7a1c..8946328b1a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,8 +1,8 @@ use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; use anyhow::Result; use gpui::{ - elements::*, AppContext, Entity, ModelContext, ModelHandle, RenderContext, Subscription, Task, - View, ViewContext, ViewHandle, WeakModelHandle, + elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View, + ViewContext, ViewHandle, WeakModelHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectPath}; @@ -10,9 +10,7 @@ use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; use util::ResultExt; -use workspace::{ItemNavHistory, ItemView, ItemViewHandle, PathOpener, Settings, StatusItemView}; - -pub struct BufferOpener; +use workspace::{ItemNavHistory, ItemView, ItemViewHandle, Settings, StatusItemView}; #[derive(Clone)] pub struct BufferItemHandle(pub ModelHandle); @@ -26,26 +24,6 @@ pub struct MultiBufferItemHandle(pub ModelHandle); #[derive(Clone)] struct WeakMultiBufferItemHandle(WeakModelHandle); -impl PathOpener for BufferOpener { - fn open( - &self, - project: &mut Project, - project_path: ProjectPath, - window_id: usize, - cx: &mut ModelContext, - ) -> Option>>> { - let buffer = project.open_buffer_for_path(project_path, cx); - Some(cx.spawn(|project, mut cx| async move { - let buffer = buffer.await?; - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_view(window_id, |cx| { - Editor::for_buffer(multibuffer, Some(project), cx) - }); - Ok(Box::new(editor) as Box) - })) - } -} - impl ItemView for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 4340cd5a1b..0dbb336007 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -409,14 +409,12 @@ mod tests { #[gpui::test] async fn test_matching_paths(cx: &mut gpui::TestAppContext) { - let mut path_openers = Vec::new(); cx.update(|cx| { super::init(cx); - editor::init(cx, &mut path_openers); + editor::init(cx); }); - let mut params = cx.update(WorkspaceParams::test); - params.path_openers = Arc::from(path_openers); + let params = cx.update(WorkspaceParams::test); params .fs .as_fake() diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a479e5fba1..9831c3aa34 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3473,6 +3473,12 @@ impl PartialEq> for ViewHandle { } } +impl PartialEq> for WeakViewHandle { + fn eq(&self, other: &ViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl Eq for ViewHandle {} impl Debug for ViewHandle { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6208ff4aaa..7644b2f55d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -391,7 +391,7 @@ impl ProjectSearchView { workspace.activate_item(&existing, cx); } else { let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); - workspace.open_item( + workspace.add_item( Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), cx, ); @@ -429,7 +429,7 @@ impl ProjectSearchView { model.search(new_query, cx); model }); - workspace.open_item( + workspace.add_item( Box::new(cx.add_view(|cx| ProjectSearchView::new(model, cx))), cx, ); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f891dca71c..e70957740f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -3201,8 +3201,7 @@ mod tests { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); - let mut path_openers_b = Vec::new(); - cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -3271,7 +3270,6 @@ mod tests { params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); params.project = project_b; - params.path_openers = path_openers_b.into(); let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b @@ -3437,8 +3435,7 @@ mod tests { cx_a.foreground().forbid_parking(); let mut lang_registry = Arc::new(LanguageRegistry::test()); let fs = FakeFs::new(cx_a.background()); - let mut path_openers_b = Vec::new(); - cx_b.update(|cx| editor::init(cx, &mut path_openers_b)); + cx_b.update(|cx| editor::init(cx)); // Set up a fake language server. let (language_server_config, mut fake_language_servers) = LanguageServerConfig::fake(); @@ -3507,7 +3504,6 @@ mod tests { params.client = client_b.client.clone(); params.user_store = client_b.user_store.clone(); params.project = project_b; - params.path_openers = path_openers_b.into(); let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 73420e213a..e8cbd185f6 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -253,12 +253,12 @@ impl Pane { let pane = pane.downgrade(); let task = workspace.load_path(project_path, cx); cx.spawn(|workspace, mut cx| async move { - let item = task.await; + let task = task.await; if let Some(pane) = pane.upgrade(&cx) { - if let Some(item) = item.log_err() { + if let Some((project_entry_id, build_item)) = task.log_err() { pane.update(&mut cx, |pane, cx| { pane.nav_history.borrow_mut().set_mode(mode); - pane.open_item(item, cx); + let item = pane.open_item(project_entry_id, cx, build_item); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -280,7 +280,7 @@ impl Pane { } } - pub(crate) fn open_editor( + pub(crate) fn open_item( &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, @@ -299,14 +299,6 @@ impl Pane { item_view } - pub fn open_item( - &mut self, - item_view_to_open: Box, - cx: &mut ViewContext, - ) { - self.add_item(None, item_view_to_open.boxed_clone(), cx); - } - pub(crate) fn add_item( &mut self, project_entry_id: Option, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d11e773c3f..33415e2360 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,7 +9,6 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; -use futures::TryFutureExt; use gpui::{ action, color::Color, @@ -18,9 +17,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelContext, ModelHandle, - MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, + PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, + WeakViewHandle, }; use language::{Buffer, LanguageRegistry}; use log::error; @@ -42,7 +41,7 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; -pub type BuildEditor = Box< +pub type BuildEditor = Arc< dyn Fn( usize, ModelHandle, @@ -110,7 +109,7 @@ where V: ItemView, F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { - cx.add_app_state::(Box::new(|window_id, project, model, cx| { + cx.add_app_state::(Arc::new(move |window_id, project, model, cx| { Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx))) })); } @@ -122,7 +121,6 @@ pub struct AppState { pub user_store: ModelHandle, pub fs: Arc, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, pub build_window_options: &'static dyn Fn() -> WindowOptions<'static>, pub build_workspace: &'static dyn Fn( ModelHandle, @@ -143,16 +141,6 @@ pub struct JoinProjectParams { pub app_state: Arc, } -pub trait PathOpener { - fn open( - &self, - project: &mut Project, - path: ProjectPath, - window_id: usize, - cx: &mut ModelContext, - ) -> Option>>>; -} - pub trait ItemView: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} @@ -378,7 +366,6 @@ pub struct WorkspaceParams { pub languages: Arc, pub user_store: ModelHandle, pub channel_list: ModelHandle, - pub path_openers: Arc<[Box]>, } impl WorkspaceParams { @@ -409,7 +396,6 @@ impl WorkspaceParams { fs, languages, user_store, - path_openers: Arc::from([]), } } @@ -428,7 +414,6 @@ impl WorkspaceParams { languages: app_state.languages.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - path_openers: app_state.path_openers.clone(), } } } @@ -446,7 +431,6 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, - path_openers: Arc<[Box]>, // items: BTreeMap, Box>, _observe_current_user: Task<()>, } @@ -510,7 +494,6 @@ impl Workspace { left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), - path_openers: params.path_openers.clone(), _observe_current_user, } } @@ -665,76 +648,14 @@ impl Workspace { } } - pub fn open_path( - &mut self, - path: ProjectPath, - cx: &mut ViewContext, - ) -> Task, Arc>> { - let project_entry = self.project.read(cx).entry_for_path(&path, cx); - - let existing_entry = self - .active_pane() - .update(cx, |pane, cx| pane.activate_project_entry(project_entry)); - - cx.spawn(|this, cx| { - if let Some(existing_entry) = existing_entry { - return Ok(existing_entry); - } - - let load_task = this - .update(&mut cx, |this, cx| { - this.load_project_entry(project_entry, cx) - }) - .await; - }); - - let load_task = self.load_path(path, cx); - let pane = self.active_pane().clone().downgrade(); - cx.as_mut().spawn(|mut cx| async move { - let item = load_task.await?; - let pane = pane - .upgrade(&cx) - .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx))) - }) - } - - pub fn load_path( - &mut self, - path: ProjectPath, - cx: &mut ViewContext, - ) -> Task>> { - if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) { - self.load_project_entry(project_entry, cx) - } else { - Task::ready(Err(anyhow!("no such file {:?}", path))) - } - } - - pub fn load_project_entry( - &mut self, - project_entry: ProjectEntryId, - cx: &mut ViewContext, - ) -> Task>> { - if let Some(existing_item) = self - .panes + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.panes() .iter() - .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) - { - return Task::ready(Ok(existing_item)); - } - - let project_path = path.clone(); - let path_openers = self.path_openers.clone(); - let window_id = cx.window_id(); - self.project.update(cx, |project, cx| { - for opener in path_openers.iter() { - if let Some(task) = opener.open(project, project_path.clone(), window_id, cx) { - return task; - } - } - Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) - }) + .find_map(|pane| pane.read(cx).item_for_entry(entry_id)) } pub fn item_of_type(&self, cx: &AppContext) -> Option> { @@ -871,107 +792,58 @@ impl Workspace { pane } - pub fn open_item(&mut self, item_view: Box, cx: &mut ViewContext) { + pub fn add_item(&mut self, item_view: Box, cx: &mut ViewContext) { self.active_pane() - .update(cx, |pane, cx| pane.open_item(item_view, cx)) + .update(cx, |pane, cx| pane.add_item(None, item_view, cx)) } - pub fn open_editor( + pub fn open_path( &mut self, - project_entry: ProjectEntryId, + path: ProjectPath, cx: &mut ViewContext, ) -> Task, Arc>> { - let pane = self.active_pane().clone(); - let project = self.project().clone(); - let buffer = project.update(cx, |project, cx| { - project.open_buffer_for_entry(project_entry, cx) - }); - - cx.spawn(|this, cx| async move { - let buffer = buffer.await?; - let editor = this.update(&mut cx, |this, cx| { - let window_id = cx.window_id(); + let pane = self.active_pane().downgrade(); + let task = self.load_path(path, cx); + cx.spawn(|this, mut cx| async move { + let (project_entry_id, build_editor) = task.await?; + let pane = pane + .upgrade(&cx) + .ok_or_else(|| anyhow!("pane was closed"))?; + this.update(&mut cx, |_, cx| { pane.update(cx, |pane, cx| { - pane.open_editor(project_entry, cx, |cx| { - cx.app_state::()(window_id, project, buffer, cx) - }) + Ok(pane.open_item(project_entry_id, cx, build_editor)) }) - }); - Ok(editor) + }) }) } - // pub fn open_path( - // &mut self, - // path: ProjectPath, - // cx: &mut ViewContext, - // ) -> Task, Arc>> { - // let project_entry = self.project.read(cx).entry_for_path(&path, cx); - - // let existing_entry = self - // .active_pane() - // .update(cx, |pane, cx| pane.activate_project_entry(project_entry)); - - // cx.spawn(|this, cx| { - // if let Some(existing_entry) = existing_entry { - // return Ok(existing_entry); - // } - - // let load_task = this - // .update(&mut cx, |this, cx| { - // this.load_project_entry(project_entry, cx) - // }) - // .await; - // }); - - // let load_task = self.load_path(path, cx); - // let pane = self.active_pane().clone().downgrade(); - // cx.as_mut().spawn(|mut cx| async move { - // let item = load_task.await?; - // let pane = pane - // .upgrade(&cx) - // .ok_or_else(|| anyhow!("could not upgrade pane reference"))?; - // Ok(pane.update(&mut cx, |pane, cx| pane.open_editor(item, cx))) - // }) - // } - - // pub fn load_path( - // &mut self, - // path: ProjectPath, - // cx: &mut ViewContext, - // ) -> Task>> { - // if let Some(project_entry) = self.project.read(cx).entry_for_path(&path, cx) { - // self.load_project_entry(project_entry, cx) - // } else { - // Task::ready(Err(anyhow!("no such file {:?}", path))) - // } - // } - - // pub fn load_project_entry( - // &mut self, - // project_entry: ProjectEntryId, - // cx: &mut ViewContext, - // ) -> Task>> { - // if let Some(existing_item) = self - // .panes - // .iter() - // .find_map(|pane| pane.read(cx).item_for_entry(project_entry)) - // { - // return Task::ready(Ok(existing_item)); - // } - - // let project_path = path.clone(); - // let path_openers = self.path_openers.clone(); - // let window_id = cx.window_id(); - // self.project.update(cx, |project, cx| { - // for opener in path_openers.iter() { - // if let Some(task) = opener.open(project, project_path.clone(), window_id, cx) { - // return task; - // } - // } - // Task::ready(Err(anyhow!("no opener found for path {:?}", project_path))) - // }) - // } + pub(crate) fn load_path( + &mut self, + path: ProjectPath, + cx: &mut ViewContext, + ) -> Task< + Result<( + ProjectEntryId, + impl 'static + FnOnce(&mut MutableAppContext) -> Box, + )>, + > { + let project = self.project().clone(); + let buffer = project.update(cx, |project, cx| project.open_buffer_for_path(path, cx)); + cx.spawn(|this, mut cx| async move { + let buffer = buffer.await?; + let project_entry_id = buffer.read_with(&cx, |buffer, cx| { + project::File::from_dyn(buffer.file()) + .and_then(|file| file.project_entry_id(cx)) + .ok_or_else(|| anyhow!("buffer has no entry")) + })?; + let (window_id, build_editor) = this.update(&mut cx, |_, cx| { + (cx.window_id(), cx.app_state::().clone()) + }); + let build_editor = + move |cx: &mut MutableAppContext| build_editor(window_id, project, buffer, cx); + Ok((project_entry_id, build_editor)) + }) + } pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { @@ -1046,7 +918,7 @@ impl Workspace { let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref()); if let Some(clone) = item.clone_on_split(cx.as_mut()) { new_pane.update(cx, |new_pane, cx| { - new_pane.open_item(project_entry_id, clone, cx); + new_pane.add_item(project_entry_id, clone, cx); }); } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 8596000246..190a279737 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -61,7 +61,6 @@ fn main() { app.run(move |cx| { let http = http::client(); let client = client::Client::new(http.clone()); - let mut path_openers = Vec::new(); let mut languages = language::build_language_registry(login_shell_env_loaded); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http.clone(), cx)); let channel_list = @@ -71,7 +70,7 @@ fn main() { client::Channel::init(&client); client::init(client.clone(), cx); workspace::init(cx); - editor::init(cx, &mut path_openers); + editor::init(cx); go_to_line::init(cx); file_finder::init(cx); chat_panel::init(cx); @@ -120,7 +119,6 @@ fn main() { client, user_store, fs, - path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }); diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index 35610854f3..f42e57c35d 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -17,8 +17,7 @@ fn init_logger() { pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let settings = Settings::test(cx); - let mut path_openers = Vec::new(); - editor::init(cx, &mut path_openers); + editor::init(cx); cx.add_app_state(settings); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let http = FakeHttpClient::with_404_response(); @@ -40,7 +39,6 @@ pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { client, user_store, fs: FakeFs::new(cx.background().clone()), - path_openers: Arc::from(path_openers), build_window_options: &build_window_options, build_workspace: &build_workspace, }) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 20da4e4800..99f02119ce 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -111,7 +111,6 @@ pub fn build_workspace( languages: app_state.languages.clone(), user_store: app_state.user_store.clone(), channel_list: app_state.channel_list.clone(), - path_openers: app_state.path_openers.clone(), }; let mut workspace = Workspace::new(&workspace_params, cx); let project = workspace.project().clone(); From 0efce8f70a8538b95bd4ea5ad17ecef86c88040c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 11:32:46 +0100 Subject: [PATCH 008/139] Rename `ItemView` to `Item` --- crates/diagnostics/src/diagnostics.rs | 4 +- crates/diagnostics/src/items.rs | 2 +- crates/editor/src/editor.rs | 6 +- crates/editor/src/items.rs | 8 +- crates/search/src/buffer_search.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/workspace/src/lsp_status.rs | 4 +- crates/workspace/src/pane.rs | 144 ++++++++++++-------------- crates/workspace/src/status_bar.rs | 8 +- crates/workspace/src/workspace.rs | 63 ++++++----- crates/zed/src/zed.rs | 10 +- 11 files changed, 122 insertions(+), 135 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index dbc8fc3e82..4eab698403 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -25,7 +25,7 @@ use std::{ sync::Arc, }; use util::TryFutureExt; -use workspace::{ItemNavHistory, ItemViewHandle as _, Settings, Workspace}; +use workspace::{ItemHandle as _, ItemNavHistory, Settings, Workspace}; action!(Deploy); @@ -438,7 +438,7 @@ impl ProjectDiagnosticsEditor { } } -impl workspace::ItemView for ProjectDiagnosticsEditor { +impl workspace::Item for ProjectDiagnosticsEditor { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { render_summary( &self.summary, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 13d90f0119..1b4d2a8147 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -71,7 +71,7 @@ impl View for DiagnosticSummary { impl StatusItemView for DiagnosticSummary { fn set_active_pane_item( &mut self, - _: Option<&dyn workspace::ItemViewHandle>, + _: Option<&dyn workspace::ItemHandle>, _: &mut ViewContext, ) { } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5a5a7cd5e6..56734dd300 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6264,7 +6264,7 @@ mod tests { #[gpui::test] fn test_navigation_history(cx: &mut gpui::MutableAppContext) { populate_settings(cx); - use workspace::ItemView; + use workspace::Item; let nav_history = Rc::new(RefCell::new(workspace::NavHistory::default())); let buffer = MultiBuffer::build_simple(&sample_text(30, 5, 'a'), cx); @@ -6283,7 +6283,7 @@ mod tests { editor.select_display_ranges(&[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 3)], cx); let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selected_display_ranges(cx), &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] @@ -6309,7 +6309,7 @@ mod tests { ); let nav_entry = nav_history.borrow_mut().pop_backward().unwrap(); editor.navigate(nav_entry.data.unwrap(), cx); - assert_eq!(nav_entry.item_view.id(), cx.view_id()); + assert_eq!(nav_entry.item.id(), cx.view_id()); assert_eq!( editor.selected_display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 8946328b1a..9c69078dc3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -10,7 +10,7 @@ use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; use util::ResultExt; -use workspace::{ItemNavHistory, ItemView, ItemViewHandle, Settings, StatusItemView}; +use workspace::{Item, ItemHandle, ItemNavHistory, Settings, StatusItemView}; #[derive(Clone)] pub struct BufferItemHandle(pub ModelHandle); @@ -24,7 +24,7 @@ pub struct MultiBufferItemHandle(pub ModelHandle); #[derive(Clone)] struct WeakMultiBufferItemHandle(WeakModelHandle); -impl ItemView for Editor { +impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); @@ -206,7 +206,7 @@ impl View for CursorPosition { impl StatusItemView for CursorPosition { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { @@ -279,7 +279,7 @@ impl View for DiagnosticMessage { impl StatusItemView for DiagnosticMessage { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) { if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 24b438c7e4..41b3d188bb 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -8,7 +8,7 @@ use gpui::{ use language::OffsetRangeExt; use project::search::SearchQuery; use std::ops::Range; -use workspace::{ItemViewHandle, Pane, Settings, Toolbar, Workspace}; +use workspace::{ItemHandle, Pane, Settings, Toolbar, Workspace}; action!(Deploy, bool); action!(Dismiss); @@ -126,7 +126,7 @@ impl View for SearchBar { impl Toolbar for SearchBar { fn active_item_changed( &mut self, - item: Option>, + item: Option>, cx: &mut ViewContext, ) -> bool { self.active_editor_subscription.take(); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7644b2f55d..acf7b2a234 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -16,7 +16,7 @@ use std::{ path::PathBuf, }; use util::ResultExt as _; -use workspace::{ItemNavHistory, ItemView, Settings, Workspace}; +use workspace::{Item, ItemNavHistory, Settings, Workspace}; action!(Deploy); action!(Search); @@ -197,7 +197,7 @@ impl View for ProjectSearchView { } } -impl ItemView for ProjectSearchView { +impl Item for ProjectSearchView { fn act_as_type( &self, type_id: TypeId, diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index db160ab06f..1b8b89f691 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,4 +1,4 @@ -use crate::{ItemViewHandle, Settings, StatusItemView}; +use crate::{ItemHandle, Settings, StatusItemView}; use futures::StreamExt; use gpui::AppContext; use gpui::{ @@ -187,5 +187,5 @@ impl View for LspStatus { } impl StatusItemView for LspStatus { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemViewHandle>, _: &mut ViewContext) {} + fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e8cbd185f6..552d102729 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,5 @@ -use super::{ItemViewHandle, SplitDirection}; -use crate::{ItemView, Settings, WeakItemViewHandle, Workspace}; +use super::{ItemHandle, SplitDirection}; +use crate::{Item, Settings, WeakItemHandle, Workspace}; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -97,7 +97,7 @@ pub enum Event { } pub struct Pane { - item_views: Vec<(Option, Box)>, + items: Vec<(Option, Box)>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -108,7 +108,7 @@ pub struct Pane { pub trait Toolbar: View { fn active_item_changed( &mut self, - item: Option>, + item: Option>, cx: &mut ViewContext, ) -> bool; fn on_dismiss(&mut self, cx: &mut ViewContext); @@ -117,7 +117,7 @@ pub trait Toolbar: View { trait ToolbarHandle { fn active_item_changed( &self, - item: Option>, + item: Option>, cx: &mut MutableAppContext, ) -> bool; fn on_dismiss(&self, cx: &mut MutableAppContext); @@ -126,7 +126,7 @@ trait ToolbarHandle { pub struct ItemNavHistory { history: Rc>, - item_view: Rc, + item: Rc, } #[derive(Default)] @@ -152,14 +152,14 @@ impl Default for NavigationMode { } pub struct NavigationEntry { - pub item_view: Rc, + pub item: Rc, pub data: Option>, } impl Pane { pub fn new() -> Self { Self { - item_views: Vec::new(), + items: Vec::new(), active_item_index: 0, nav_history: Default::default(), toolbars: Default::default(), @@ -216,13 +216,13 @@ impl Pane { // If the item is still present in this pane, then activate it. if let Some(index) = entry - .item_view + .item .upgrade(cx) - .and_then(|v| pane.index_for_item_view(v.as_ref())) + .and_then(|v| pane.index_for_item(v.as_ref())) { - if let Some(item_view) = pane.active_item() { + if let Some(item) = pane.active_item() { pane.nav_history.borrow_mut().set_mode(mode); - item_view.deactivated(cx); + item.deactivated(cx); pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -242,7 +242,7 @@ impl Pane { pane.nav_history .borrow_mut() .paths_by_item - .get(&entry.item_view.id()) + .get(&entry.item.id()) .cloned() .map(|project_path| (project_path, entry)) } @@ -284,47 +284,47 @@ impl Pane { &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, - build_editor: impl FnOnce(&mut MutableAppContext) -> Box, - ) -> Box { - for (ix, (existing_entry_id, item_view)) in self.item_views.iter().enumerate() { + build_editor: impl FnOnce(&mut MutableAppContext) -> Box, + ) -> Box { + for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() { if *existing_entry_id == Some(project_entry_id) { - let item_view = item_view.boxed_clone(); + let item = item.boxed_clone(); self.activate_item(ix, cx); - return item_view; + return item; } } - let item_view = build_editor(cx); - self.add_item(Some(project_entry_id), item_view.boxed_clone(), cx); - item_view + let item = build_editor(cx); + self.add_item(Some(project_entry_id), item.boxed_clone(), cx); + item } pub(crate) fn add_item( &mut self, project_entry_id: Option, - mut item: Box, + mut item: Box, cx: &mut ViewContext, ) { item.set_nav_history(self.nav_history.clone(), cx); item.added_to_pane(cx); - let item_idx = cmp::min(self.active_item_index + 1, self.item_views.len()); - self.item_views.insert(item_idx, (project_entry_id, item)); + let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); + self.items.insert(item_idx, (project_entry_id, item)); self.activate_item(item_idx, cx); cx.notify(); } - pub fn item_views(&self) -> impl Iterator> { - self.item_views.iter().map(|(_, view)| view) + pub fn items(&self) -> impl Iterator> { + self.items.iter().map(|(_, view)| view) } - pub fn active_item(&self) -> Option> { - self.item_views + pub fn active_item(&self) -> Option> { + self.items .get(self.active_item_index) .map(|(_, view)| view.clone()) } - pub fn project_entry_id_for_item(&self, item: &dyn ItemViewHandle) -> Option { - self.item_views.iter().find_map(|(entry_id, existing)| { + pub fn project_entry_id_for_item(&self, item: &dyn ItemHandle) -> Option { + self.items.iter().find_map(|(entry_id, existing)| { if existing.id() == item.id() { *entry_id } else { @@ -333,8 +333,8 @@ impl Pane { }) } - pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { - self.item_views.iter().find_map(|(id, view)| { + pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { + self.items.iter().find_map(|(id, view)| { if *id == Some(entry_id) { Some(view.boxed_clone()) } else { @@ -343,25 +343,17 @@ impl Pane { }) } - pub fn index_for_item_view(&self, item_view: &dyn ItemViewHandle) -> Option { - self.item_views - .iter() - .position(|(_, i)| i.id() == item_view.id()) - } - - pub fn index_for_item(&self, item: &dyn ItemViewHandle) -> Option { - self.item_views - .iter() - .position(|(_, my_item)| my_item.id() == item.id()) + pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { + self.items.iter().position(|(_, i)| i.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { - if index < self.item_views.len() { + if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); if prev_active_item_ix != self.active_item_index - && prev_active_item_ix < self.item_views.len() + && prev_active_item_ix < self.items.len() { - self.item_views[prev_active_item_ix].1.deactivated(cx); + self.items[prev_active_item_ix].1.deactivated(cx); } self.update_active_toolbar(cx); self.focus_active_item(cx); @@ -374,15 +366,15 @@ impl Pane { let mut index = self.active_item_index; if index > 0 { index -= 1; - } else if self.item_views.len() > 0 { - index = self.item_views.len() - 1; + } else if self.items.len() > 0 { + index = self.items.len() - 1; } self.activate_item(index, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { let mut index = self.active_item_index; - if index + 1 < self.item_views.len() { + if index + 1 < self.items.len() { index += 1; } else { index = 0; @@ -391,14 +383,14 @@ impl Pane { } pub fn close_active_item(&mut self, cx: &mut ViewContext) { - if !self.item_views.is_empty() { - self.close_item(self.item_views[self.active_item_index].1.id(), cx) + if !self.items.is_empty() { + self.close_item(self.items[self.active_item_index].1.id(), cx) } } pub fn close_inactive_items(&mut self, cx: &mut ViewContext) { - if !self.item_views.is_empty() { - let active_item_id = self.item_views[self.active_item_index].1.id(); + if !self.items.is_empty() { + let active_item_id = self.items[self.active_item_index].1.id(); self.close_items(cx, |id| id != active_item_id); } } @@ -414,10 +406,10 @@ impl Pane { ) { let mut item_ix = 0; let mut new_active_item_index = self.active_item_index; - self.item_views.retain(|(_, item_view)| { - if should_close(item_view.id()) { + self.items.retain(|(_, item)| { + if should_close(item.id()) { if item_ix == self.active_item_index { - item_view.deactivated(cx); + item.deactivated(cx); } if item_ix < self.active_item_index { @@ -425,10 +417,10 @@ impl Pane { } let mut nav_history = self.nav_history.borrow_mut(); - if let Some(path) = item_view.project_path(cx) { - nav_history.paths_by_item.insert(item_view.id(), path); + if let Some(path) = item.project_path(cx) { + nav_history.paths_by_item.insert(item.id(), path); } else { - nav_history.paths_by_item.remove(&item_view.id()); + nav_history.paths_by_item.remove(&item.id()); } item_ix += 1; @@ -439,10 +431,10 @@ impl Pane { } }); - if self.item_views.is_empty() { + if self.items.is_empty() { cx.emit(Event::Remove); } else { - self.active_item_index = cmp::min(new_active_item_index, self.item_views.len() - 1); + self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1); self.focus_active_item(cx); self.activate(cx); } @@ -511,7 +503,7 @@ impl Pane { } fn update_active_toolbar(&mut self, cx: &mut ViewContext) { - let active_item = self.item_views.get(self.active_item_index); + let active_item = self.items.get(self.active_item_index); for (toolbar_type_id, toolbar) in &self.toolbars { let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx); if Some(*toolbar_type_id) == self.active_toolbar_type { @@ -526,7 +518,7 @@ impl Pane { enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, (_, item_view)) in self.item_views.iter().enumerate() { + for (ix, (_, item)) in self.items.iter().enumerate() { let is_active = ix == self.active_item_index; row.add_child({ @@ -535,7 +527,7 @@ impl Pane { } else { theme.workspace.tab.clone() }; - let title = item_view.tab_content(&tab_style, cx); + let title = item.tab_content(&tab_style, cx); let mut style = if is_active { theme.workspace.active_tab.clone() @@ -552,9 +544,9 @@ impl Pane { .with_child( Align::new({ let diameter = 7.0; - let icon_color = if item_view.has_conflict(cx) { + let icon_color = if item.has_conflict(cx) { Some(style.icon_conflict) - } else if item_view.is_dirty(cx) { + } else if item.is_dirty(cx) { Some(style.icon_dirty) } else { None @@ -598,7 +590,7 @@ impl Pane { .with_child( Align::new( ConstrainedBox::new(if mouse_state.hovered { - let item_id = item_view.id(); + let item_id = item.id(); enum TabCloseButton {} let icon = Svg::new("icons/x.svg"); MouseEventHandler::new::( @@ -702,7 +694,7 @@ impl View for Pane { impl ToolbarHandle for ViewHandle { fn active_item_changed( &self, - item: Option>, + item: Option>, cx: &mut MutableAppContext, ) -> bool { self.update(cx, |this, cx| this.active_item_changed(item, cx)) @@ -718,10 +710,10 @@ impl ToolbarHandle for ViewHandle { } impl ItemNavHistory { - pub fn new(history: Rc>, item_view: &ViewHandle) -> Self { + pub fn new(history: Rc>, item: &ViewHandle) -> Self { Self { history, - item_view: Rc::new(item_view.downgrade()), + item: Rc::new(item.downgrade()), } } @@ -730,7 +722,7 @@ impl ItemNavHistory { } pub fn push(&self, data: Option) { - self.history.borrow_mut().push(data, self.item_view.clone()); + self.history.borrow_mut().push(data, self.item.clone()); } } @@ -763,11 +755,7 @@ impl NavHistory { self.mode = mode; } - pub fn push( - &mut self, - data: Option, - item_view: Rc, - ) { + pub fn push(&mut self, data: Option, item: Rc) { match self.mode { NavigationMode::Disabled => {} NavigationMode::Normal => { @@ -775,7 +763,7 @@ impl NavHistory { self.backward_stack.pop_front(); } self.backward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); self.forward_stack.clear(); @@ -785,7 +773,7 @@ impl NavHistory { self.forward_stack.pop_front(); } self.forward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); } @@ -794,7 +782,7 @@ impl NavHistory { self.backward_stack.pop_front(); } self.backward_stack.push_back(NavigationEntry { - item_view, + item, data: data.map(|data| Box::new(data) as Box), }); } diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index c387fbe070..782e51c706 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -1,4 +1,4 @@ -use crate::{ItemViewHandle, Pane, Settings}; +use crate::{ItemHandle, Pane, Settings}; use gpui::{ elements::*, AnyViewHandle, ElementBox, Entity, MutableAppContext, RenderContext, Subscription, View, ViewContext, ViewHandle, @@ -7,7 +7,7 @@ use gpui::{ pub trait StatusItemView: View { fn set_active_pane_item( &mut self, - active_pane_item: Option<&dyn crate::ItemViewHandle>, + active_pane_item: Option<&dyn crate::ItemHandle>, cx: &mut ViewContext, ); } @@ -16,7 +16,7 @@ trait StatusItemViewHandle { fn to_any(&self) -> AnyViewHandle; fn set_active_pane_item( &self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut MutableAppContext, ); } @@ -114,7 +114,7 @@ impl StatusItemViewHandle for ViewHandle { fn set_active_pane_item( &self, - active_pane_item: Option<&dyn ItemViewHandle>, + active_pane_item: Option<&dyn ItemHandle>, cx: &mut MutableAppContext, ) { self.update(cx, |this, cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33415e2360..4dac4d2254 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -47,7 +47,7 @@ pub type BuildEditor = Arc< ModelHandle, ModelHandle, &mut MutableAppContext, - ) -> Box, + ) -> Box, >; action!(Open, Arc); @@ -106,7 +106,7 @@ pub fn init(cx: &mut MutableAppContext) { pub fn register_editor_builder(cx: &mut MutableAppContext, build_editor: F) where - V: ItemView, + V: Item, F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { cx.add_app_state::(Arc::new(move |window_id, project, model, cx| { @@ -141,7 +141,7 @@ pub struct JoinProjectParams { pub app_state: Arc, } -pub trait ItemView: View { +pub trait Item: View { fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -195,12 +195,12 @@ pub trait ItemView: View { } } -pub trait ItemViewHandle: 'static { +pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; - fn boxed_clone(&self) -> Box; + fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); - fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; + fn clone_on_split(&self, 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); @@ -220,12 +220,12 @@ pub trait ItemViewHandle: 'static { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; } -pub trait WeakItemViewHandle { +pub trait WeakItemHandle { fn id(&self) -> usize; - fn upgrade(&self, cx: &AppContext) -> Option>; + fn upgrade(&self, cx: &AppContext) -> Option>; } -impl dyn ItemViewHandle { +impl dyn ItemHandle { pub fn downcast(&self) -> Option> { self.to_any().downcast() } @@ -236,7 +236,7 @@ impl dyn ItemViewHandle { } } -impl ItemViewHandle for ViewHandle { +impl ItemHandle for ViewHandle { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox { self.read(cx).tab_content(style, cx) } @@ -245,7 +245,7 @@ impl ItemViewHandle for ViewHandle { self.read(cx).project_path(cx) } - fn boxed_clone(&self) -> Box { + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -253,11 +253,11 @@ impl ItemViewHandle for ViewHandle { &self, // nav_history: Rc>, cx: &mut MutableAppContext, - ) -> Option> { + ) -> Option> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) }) - .map(|handle| Box::new(handle) as Box) + .map(|handle| Box::new(handle) as Box) } fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext) { @@ -273,7 +273,7 @@ impl ItemViewHandle for ViewHandle { return; } if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.index_for_item_view(&item) { + if let Some(ix) = pane.index_for_item(&item) { pane.activate_item(ix, cx); pane.activate(cx); } @@ -335,26 +335,25 @@ impl ItemViewHandle for ViewHandle { } } -impl Into for Box { +impl Into for Box { fn into(self) -> AnyViewHandle { self.to_any() } } -impl Clone for Box { - fn clone(&self) -> Box { +impl Clone for Box { + fn clone(&self) -> Box { self.boxed_clone() } } -impl WeakItemViewHandle for WeakViewHandle { +impl WeakItemHandle for WeakViewHandle { fn id(&self) -> usize { self.id() } - fn upgrade(&self, cx: &AppContext) -> Option> { - self.upgrade(cx) - .map(|v| Box::new(v) as Box) + fn upgrade(&self, cx: &AppContext) -> Option> { + self.upgrade(cx).map(|v| Box::new(v) as Box) } } @@ -556,7 +555,7 @@ impl Workspace { &mut self, abs_paths: &[PathBuf], cx: &mut ViewContext, - ) -> Task, Arc>>>> { + ) -> Task, Arc>>>> { let entries = abs_paths .iter() .cloned() @@ -652,28 +651,28 @@ impl Workspace { &self, entry_id: ProjectEntryId, cx: &AppContext, - ) -> Option> { + ) -> Option> { self.panes() .iter() .find_map(|pane| pane.read(cx).item_for_entry(entry_id)) } - pub fn item_of_type(&self, cx: &AppContext) -> Option> { + pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items_of_type(cx).max_by_key(|item| item.id()) } - pub fn items_of_type<'a, T: ItemView>( + pub fn items_of_type<'a, T: Item>( &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator> { self.panes.iter().flat_map(|pane| { pane.read(cx) - .item_views() + .items() .filter_map(|item| item.to_any().downcast()) }) } - pub fn active_item(&self, cx: &AppContext) -> Option> { + pub fn active_item(&self, cx: &AppContext) -> Option> { self.active_pane().read(cx).active_item() } @@ -792,16 +791,16 @@ impl Workspace { pane } - pub fn add_item(&mut self, item_view: Box, cx: &mut ViewContext) { + pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { self.active_pane() - .update(cx, |pane, cx| pane.add_item(None, item_view, cx)) + .update(cx, |pane, cx| pane.add_item(None, item, cx)) } pub fn open_path( &mut self, path: ProjectPath, cx: &mut ViewContext, - ) -> Task, Arc>> { + ) -> Task, Arc>> { let pane = self.active_pane().downgrade(); let task = self.load_path(path, cx); cx.spawn(|this, mut cx| async move { @@ -824,7 +823,7 @@ impl Workspace { ) -> Task< Result<( ProjectEntryId, - impl 'static + FnOnce(&mut MutableAppContext) -> Box, + impl 'static + FnOnce(&mut MutableAppContext) -> Box, )>, > { let project = self.project().clone(); @@ -845,7 +844,7 @@ impl Workspace { }) } - pub fn activate_item(&mut self, item: &dyn ItemViewHandle, cx: &mut ViewContext) -> bool { + pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { if let Some(ix) = pane.read(cx).index_for_item(item) { Some((pane.clone(), ix)) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 99f02119ce..26b4150e24 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -192,7 +192,7 @@ mod tests { use theme::{Theme, ThemeRegistry, DEFAULT_THEME_NAME}; use util::test::temp_tree; use workspace::{ - open_paths, pane, ItemView, ItemViewHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, + open_paths, pane, Item, ItemHandle, OpenNew, Pane, SplitDirection, WorkspaceHandle, }; #[gpui::test] @@ -324,7 +324,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().count(), 1); + assert_eq!(pane.items().count(), 1); }); // Open the second entry @@ -338,7 +338,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file2.clone()) ); - assert_eq!(pane.item_views().count(), 2); + assert_eq!(pane.items().count(), 2); }); // Open the first entry again. The existing pane item is activated. @@ -354,7 +354,7 @@ mod tests { pane.active_item().unwrap().project_path(cx), Some(file1.clone()) ); - assert_eq!(pane.item_views().count(), 2); + assert_eq!(pane.items().count(), 2); }); // Split the pane with the first entry, then open the second entry again. @@ -393,7 +393,7 @@ mod tests { Some(file3.clone()) ); let pane_entries = pane - .item_views() + .items() .map(|i| i.project_path(cx).unwrap()) .collect::>(); assert_eq!(pane_entries, &[file1, file2, file3]); From a691c2fbdb01d6c9f70513c40a59d75b262f05f4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 11:33:58 +0100 Subject: [PATCH 009/139] Delete unused code --- crates/editor/src/items.rs | 18 +++--------------- crates/workspace/src/workspace.rs | 1 - 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 9c69078dc3..d4e779484b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,10 +1,10 @@ -use crate::{Autoscroll, Editor, Event, MultiBuffer, NavigationData, ToOffset, ToPoint as _}; +use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; use anyhow::Result; use gpui::{ elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, WeakModelHandle, + ViewContext, ViewHandle, }; -use language::{Bias, Buffer, Diagnostic, File as _}; +use language::{Bias, Diagnostic, File as _}; use project::{File, Project, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; @@ -12,18 +12,6 @@ use text::{Point, Selection}; use util::ResultExt; use workspace::{Item, ItemHandle, ItemNavHistory, Settings, StatusItemView}; -#[derive(Clone)] -pub struct BufferItemHandle(pub ModelHandle); - -#[derive(Clone)] -struct WeakBufferItemHandle(WeakModelHandle); - -#[derive(Clone)] -pub struct MultiBufferItemHandle(pub ModelHandle); - -#[derive(Clone)] -struct WeakMultiBufferItemHandle(WeakModelHandle); - impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 4dac4d2254..e34cf1bff6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -430,7 +430,6 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, - // items: BTreeMap, Box>, _observe_current_user: Task<()>, } From e6755f4115e67a63ab3430f268e6b065c226c4f2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 11:39:39 +0100 Subject: [PATCH 010/139] Search only in active pane when using `Editor::find_or_create` --- crates/editor/src/editor.rs | 3 ++- crates/workspace/src/workspace.rs | 10 ---------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 56734dd300..131b16e8b7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -848,9 +848,10 @@ impl Editor { if let Some(item) = project::File::from_dyn(buffer.read(cx).file()) .and_then(|file| file.project_entry_id(cx)) - .and_then(|entry_id| workspace.item_for_entry(entry_id, cx)) + .and_then(|entry_id| workspace.active_pane().read(cx).item_for_entry(entry_id)) .and_then(|item| item.downcast()) { + workspace.activate_item(&item, cx); return item; } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e34cf1bff6..e10073c04a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -646,16 +646,6 @@ impl Workspace { } } - pub fn item_for_entry( - &self, - entry_id: ProjectEntryId, - cx: &AppContext, - ) -> Option> { - self.panes() - .iter() - .find_map(|pane| pane.read(cx).item_for_entry(entry_id)) - } - pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items_of_type(cx).max_by_key(|item| item.id()) } From 6446660c88327c27286fc12688994cac2da90f06 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 11:42:13 +0100 Subject: [PATCH 011/139] Rename `open_buffer_for_path` to `open_buffer` --- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/editor.rs | 4 +- crates/project/src/project.rs | 95 ++++++++---------------- crates/server/src/rpc.rs | 102 ++++++++------------------ crates/workspace/src/workspace.rs | 2 +- 5 files changed, 64 insertions(+), 143 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 4eab698403..f6c00a1fd5 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -155,9 +155,7 @@ impl ProjectDiagnosticsEditor { async move { for path in paths { let buffer = project - .update(&mut cx, |project, cx| { - project.open_buffer_for_path(path.clone(), cx) - }) + .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx)) .await?; this.update(&mut cx, |view, cx| view.populate_excerpts(path, buffer, cx)) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 131b16e8b7..0b73ebdf63 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8439,9 +8439,7 @@ mod tests { .0 .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, ""), cx) - }) + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) .await .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 3484796dd9..ff535afafb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -818,19 +818,7 @@ impl Project { Ok(buffer) } - pub fn open_buffer_for_entry( - &mut self, - entry_id: ProjectEntryId, - cx: &mut ModelContext, - ) -> Task>> { - if let Some(project_path) = self.path_for_entry(entry_id, cx) { - self.open_buffer_for_path(project_path, cx) - } else { - Task::ready(Err(anyhow!("entry not found"))) - } - } - - pub fn open_buffer_for_path( + pub fn open_buffer( &mut self, path: impl Into, cx: &mut ModelContext, @@ -965,10 +953,8 @@ impl Project { worktree_id: worktree.read_with(&cx, |worktree, _| worktree.id()), path: relative_path.into(), }; - this.update(&mut cx, |this, cx| { - this.open_buffer_for_path(project_path, cx) - }) - .await + this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) + .await }) } @@ -2868,9 +2854,7 @@ impl Project { 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_for_path(project_path, cx) - }) + .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) .await .log_err() { @@ -3891,7 +3875,7 @@ impl Project { let peer_id = envelope.original_sender_id()?; let worktree_id = WorktreeId::from_proto(envelope.payload.worktree_id); let open_buffer = this.update(&mut cx, |this, cx| { - this.open_buffer_for_path( + this.open_buffer( ProjectPath { worktree_id, path: PathBuf::from(envelope.payload.path).into(), @@ -4688,7 +4672,7 @@ mod tests { // Open a buffer without an associated language server. let toml_buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "Cargo.toml"), cx) + project.open_buffer((worktree_id, "Cargo.toml"), cx) }) .await .unwrap(); @@ -4696,7 +4680,7 @@ mod tests { // Open a buffer with an associated language server. let rust_buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "test.rs"), cx) + project.open_buffer((worktree_id, "test.rs"), cx) }) .await .unwrap(); @@ -4743,7 +4727,7 @@ mod tests { // Open a third buffer with a different associated language server. let json_buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "package.json"), cx) + project.open_buffer((worktree_id, "package.json"), cx) }) .await .unwrap(); @@ -4774,7 +4758,7 @@ mod tests { // it is also configured based on the existing language server's capabilities. let rust_buffer2 = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "test2.rs"), cx) + project.open_buffer((worktree_id, "test2.rs"), cx) }) .await .unwrap(); @@ -4885,7 +4869,7 @@ mod tests { // Cause worktree to start the fake language server let _buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, Path::new("b.rs")), cx) + project.open_buffer((worktree_id, Path::new("b.rs")), cx) }) .await .unwrap(); @@ -4932,9 +4916,7 @@ mod tests { ); let buffer = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); @@ -5001,7 +4983,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "a.rs"), cx) + project.open_buffer((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5277,7 +5259,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "a.rs"), cx) + project.open_buffer((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5382,7 +5364,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "a.rs"), cx) + project.open_buffer((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5540,7 +5522,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "a.rs"), cx) + project.open_buffer((worktree_id, "a.rs"), cx) }) .await .unwrap(); @@ -5723,7 +5705,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path( + project.open_buffer( ProjectPath { worktree_id, path: Path::new("").into(), @@ -5819,9 +5801,7 @@ mod tests { .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "file1"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); buffer @@ -5859,7 +5839,7 @@ mod tests { .read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| p.open_buffer_for_path((worktree_id, ""), cx)) + .update(cx, |p, cx| p.open_buffer((worktree_id, ""), cx)) .await .unwrap(); buffer @@ -5909,7 +5889,7 @@ mod tests { let opened_buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "file1"), cx) + project.open_buffer((worktree_id, "file1"), cx) }) .await .unwrap(); @@ -5944,8 +5924,7 @@ mod tests { let worktree_id = tree.read_with(cx, |tree, _| tree.id()); let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| { - let buffer = - project.update(cx, |p, cx| p.open_buffer_for_path((worktree_id, path), cx)); + let buffer = project.update(cx, |p, cx| p.open_buffer((worktree_id, path), cx)); async move { buffer.await.unwrap() } }; let id_for_path = |path: &'static str, cx: &gpui::TestAppContext| { @@ -6094,9 +6073,9 @@ mod tests { // Spawn multiple tasks to open paths, repeating some paths. let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| { ( - p.open_buffer_for_path((worktree_id, "a.txt"), cx), - p.open_buffer_for_path((worktree_id, "b.txt"), cx), - p.open_buffer_for_path((worktree_id, "a.txt"), cx), + p.open_buffer((worktree_id, "a.txt"), cx), + p.open_buffer((worktree_id, "b.txt"), cx), + p.open_buffer((worktree_id, "a.txt"), cx), ) }); @@ -6113,9 +6092,7 @@ mod tests { // Open the same path again while it is still open. drop(buffer_a_1); let buffer_a_3 = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); @@ -6148,9 +6125,7 @@ mod tests { .await; let buffer1 = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "file1"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); let events = Rc::new(RefCell::new(Vec::new())); @@ -6220,9 +6195,7 @@ mod tests { // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); let buffer2 = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "file2"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "file2"), cx)) .await .unwrap(); buffer2.update(cx, |_, cx| { @@ -6243,9 +6216,7 @@ mod tests { // When a file is already dirty when deleted, we don't emit a Dirtied event. let events = Rc::new(RefCell::new(Vec::new())); let buffer3 = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "file3"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "file3"), cx)) .await .unwrap(); buffer3.update(cx, |_, cx| { @@ -6291,9 +6262,7 @@ mod tests { let abs_path = dir.path().join("the-file"); let buffer = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "the-file"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "the-file"), cx)) .await .unwrap(); @@ -6399,9 +6368,7 @@ mod tests { let worktree_id = worktree.read_with(cx, |tree, _| tree.id()); let buffer = project - .update(cx, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - }) + .update(cx, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)) .await .unwrap(); @@ -6674,7 +6641,7 @@ mod tests { let buffer = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, Path::new("one.rs")), cx) + project.open_buffer((worktree_id, Path::new("one.rs")), cx) }) .await .unwrap(); @@ -6812,7 +6779,7 @@ mod tests { let buffer_4 = project .update(cx, |project, cx| { - project.open_buffer_for_path((worktree_id, "four.rs"), cx) + project.open_buffer((worktree_id, "four.rs"), cx) }) .await .unwrap(); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index e70957740f..d70bfe2c21 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1137,9 +1137,7 @@ mod tests { // Open the same file as client B and client A. let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "b.txt"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) .await .unwrap(); let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); @@ -1150,9 +1148,7 @@ mod tests { assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) }); let buffer_a = project_a - .update(cx_a, |p, cx| { - p.open_buffer_for_path((worktree_id, "b.txt"), cx) - }) + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) .await .unwrap(); @@ -1242,9 +1238,7 @@ mod tests { .await .unwrap(); project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); @@ -1279,9 +1273,7 @@ mod tests { .await .unwrap(); project_b2 - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); } @@ -1360,15 +1352,11 @@ mod tests { // Open and edit a buffer as both guests B and C. let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "file1"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); let buffer_c = project_c - .update(cx_c, |p, cx| { - p.open_buffer_for_path((worktree_id, "file1"), cx) - }) + .update(cx_c, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); buffer_b.update(cx_b, |buf, cx| buf.edit([0..0], "i-am-b, ", cx)); @@ -1376,9 +1364,7 @@ mod tests { // Open and edit that buffer as the host. let buffer_a = project_a - .update(cx_a, |p, cx| { - p.open_buffer_for_path((worktree_id, "file1"), cx) - }) + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "file1"), cx)) .await .unwrap(); @@ -1528,9 +1514,7 @@ mod tests { // Open a buffer as client B let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); @@ -1613,9 +1597,7 @@ mod tests { // Open a buffer as client B let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); buffer_b.read_with(cx_b, |buf, _| { @@ -1695,16 +1677,14 @@ mod tests { // Open a buffer as client A let buffer_a = project_a - .update(cx_a, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - }) + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); // Start opening the same buffer as client B - let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - })); + let buffer_b = cx_b + .background() + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); // Edit the buffer as client A while client B is still opening it. cx_b.background().simulate_random_delay().await; @@ -1780,9 +1760,9 @@ mod tests { .await; // Begin opening a buffer as client B, but leave the project before the open completes. - let buffer_b = cx_b.background().spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.txt"), cx) - })); + let buffer_b = cx_b + .background() + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx))); cx_b.update(|_| drop(project_b)); drop(buffer_b); @@ -1952,7 +1932,7 @@ mod tests { let _ = cx_a .background() .spawn(project_a.update(cx_a, |project, cx| { - project.open_buffer_for_path( + project.open_buffer( ProjectPath { worktree_id, path: Path::new("other.rs").into(), @@ -2073,9 +2053,7 @@ mod tests { // Open the file with the errors on client B. They should be present. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) .await .unwrap(); @@ -2193,9 +2171,7 @@ mod tests { // Open a file in an editor as the guest. let buffer_b = project_b - .update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "main.rs"), cx) - }) + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); let (window_b, _) = cx_b.add_window(|_| EmptyView); @@ -2269,9 +2245,7 @@ mod tests { // Open the buffer on the host. let buffer_a = project_a - .update(cx_a, |p, cx| { - p.open_buffer_for_path((worktree_id, "main.rs"), cx) - }) + .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); buffer_a @@ -2395,9 +2369,7 @@ mod tests { let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) .await .unwrap(); @@ -2505,9 +2477,7 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) .await .unwrap(); @@ -2646,9 +2616,7 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "one.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) .await .unwrap(); @@ -2877,9 +2845,7 @@ mod tests { // Open the file on client B. let buffer_b = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "main.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx))) .await .unwrap(); @@ -3026,9 +2992,7 @@ mod tests { // Cause the language server to start. let _buffer = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "one.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "one.rs"), cx))) .await .unwrap(); @@ -3159,9 +3123,7 @@ mod tests { let buffer_b1 = cx_b .background() - .spawn(project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "a.rs"), cx) - })) + .spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))) .await .unwrap(); @@ -3177,13 +3139,9 @@ mod tests { let buffer_b2; if rng.gen() { definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); - buffer_b2 = project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "b.rs"), cx) - }); + buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); } else { - buffer_b2 = project_b.update(cx_b, |p, cx| { - p.open_buffer_for_path((worktree_id, "b.rs"), cx) - }); + buffer_b2 = project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.rs"), cx)); definitions = project_b.update(cx_b, |p, cx| p.definition(&buffer_b1, 23, cx)); } @@ -4800,7 +4758,7 @@ mod tests { ); let buffer = project .update(&mut cx, |project, cx| { - project.open_buffer_for_path(project_path, cx) + project.open_buffer(project_path, cx) }) .await .unwrap(); @@ -4917,7 +4875,7 @@ mod tests { ); let buffer = project .update(&mut cx, |project, cx| { - project.open_buffer_for_path(project_path.clone(), cx) + project.open_buffer(project_path.clone(), cx) }) .await .unwrap(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index e10073c04a..92fcacaf85 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -816,7 +816,7 @@ impl Workspace { )>, > { let project = self.project().clone(); - let buffer = project.update(cx, |project, cx| project.open_buffer_for_path(path, cx)); + let buffer = project.update(cx, |project, cx| project.open_buffer(path, cx)); cx.spawn(|this, mut cx| async move { let buffer = buffer.await?; let project_entry_id = buffer.read_with(&cx, |buffer, cx| { From 84bacc556f55f88747c08f96ed0a36f1fa4b3dbd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 14:31:05 +0100 Subject: [PATCH 012/139] Rename `build_editor` to `build_item` in `Pane::open_item` Co-Authored-By: Nathan Sobo --- crates/workspace/src/pane.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 552d102729..4ee9eb97ae 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -284,7 +284,7 @@ impl Pane { &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, - build_editor: impl FnOnce(&mut MutableAppContext) -> Box, + build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() { if *existing_entry_id == Some(project_entry_id) { @@ -294,7 +294,7 @@ impl Pane { } } - let item = build_editor(cx); + let item = build_item(cx); self.add_item(Some(project_entry_id), item.boxed_clone(), cx); item } From 44d997c00c875b2202997a185b2b3d4f1b25920c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 14:33:01 +0100 Subject: [PATCH 013/139] Rename `app_state` to `global` in gpui Co-Authored-By: Nathan Sobo --- crates/chat_panel/src/chat_panel.rs | 14 ++++++------ crates/contacts_panel/src/contacts_panel.rs | 4 ++-- crates/diagnostics/src/diagnostics.rs | 6 ++--- crates/diagnostics/src/items.rs | 2 +- crates/editor/src/editor.rs | 14 ++++++------ crates/editor/src/element.rs | 4 ++-- crates/editor/src/items.rs | 4 ++-- crates/file_finder/src/file_finder.rs | 6 ++--- crates/go_to_line/src/go_to_line.rs | 5 +++-- crates/gpui/src/app.rs | 20 ++++++++--------- crates/outline/src/outline.rs | 15 ++++++++----- crates/project_panel/src/project_panel.rs | 4 ++-- crates/project_symbols/src/project_symbols.rs | 6 ++--- crates/search/src/buffer_search.rs | 10 ++++----- crates/search/src/project_search.rs | 22 +++++++++---------- crates/server/src/rpc.rs | 2 +- crates/theme_selector/src/theme_selector.rs | 14 ++++++------ crates/workspace/src/lsp_status.rs | 4 ++-- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/status_bar.rs | 2 +- crates/workspace/src/workspace.rs | 12 +++++----- crates/zed/src/main.rs | 4 ++-- crates/zed/src/test.rs | 2 +- crates/zed/src/zed.rs | 2 +- 24 files changed, 91 insertions(+), 89 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index ceeddc599a..bed338d3f4 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -64,13 +64,13 @@ impl ChatPanel { ix, item_type, is_hovered, - &cx.app_state::().theme.chat_panel.channel_select, + &cx.global::().theme.chat_panel.channel_select, cx, ) } }) .with_style(move |cx| { - let theme = &cx.app_state::().theme.chat_panel.channel_select; + let theme = &cx.global::().theme.chat_panel.channel_select; SelectStyle { header: theme.header.container.clone(), menu: theme.menu.clone(), @@ -200,7 +200,7 @@ impl ChatPanel { } fn render_channel(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Flex::column() .with_child( Container::new(ChildView::new(&self.channel_select).boxed()) @@ -224,7 +224,7 @@ impl ChatPanel { fn render_message(&self, message: &ChannelMessage, cx: &AppContext) -> ElementBox { let now = OffsetDateTime::now_utc(); - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = if message.is_pending() { &settings.theme.chat_panel.pending_message } else { @@ -267,7 +267,7 @@ impl ChatPanel { } fn render_input_box(&self, cx: &AppContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Container::new(ChildView::new(&self.input_editor).boxed()) .with_style(theme.chat_panel.input_editor.container) .boxed() @@ -304,7 +304,7 @@ impl ChatPanel { } fn render_sign_in_prompt(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let rpc = self.rpc.clone(); let this = cx.handle(); @@ -385,7 +385,7 @@ impl View for ChatPanel { } else { self.render_sign_in_prompt(cx) }; - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; ConstrainedBox::new( Container::new(element) .with_style(theme.chat_panel.container) diff --git a/crates/contacts_panel/src/contacts_panel.rs b/crates/contacts_panel/src/contacts_panel.rs index b5740a5ed7..b8b5b3a361 100644 --- a/crates/contacts_panel/src/contacts_panel.rs +++ b/crates/contacts_panel/src/contacts_panel.rs @@ -55,7 +55,7 @@ impl ContactsPanel { app_state: Arc, cx: &mut LayoutContext, ) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let theme = &theme.contacts_panel; let project_count = collaborator.projects.len(); let font_cache = cx.font_cache(); @@ -236,7 +236,7 @@ impl View for ContactsPanel { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.contacts_panel; + let theme = &cx.global::().theme.contacts_panel; Container::new(List::new(self.contacts.clone()).boxed()) .with_style(theme.container) .boxed() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f6c00a1fd5..a188761340 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -72,7 +72,7 @@ impl View for ProjectDiagnosticsEditor { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if self.path_states.is_empty() { - let theme = &cx.app_state::().theme.project_diagnostics; + let theme = &cx.global::().theme.project_diagnostics; Label::new( "No problems in workspace".to_string(), theme.empty_message.clone(), @@ -441,7 +441,7 @@ impl workspace::Item for ProjectDiagnosticsEditor { render_summary( &self.summary, &style.label.text, - &cx.app_state::().theme.project_diagnostics, + &cx.global::().theme.project_diagnostics, ) } @@ -535,7 +535,7 @@ impl workspace::Item for ProjectDiagnosticsEditor { fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, highlights) = highlight_diagnostic_message(&diagnostic.message); Arc::new(move |cx| { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme.editor; let style = &theme.diagnostic_header; let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index 1b4d2a8147..690c5100ec 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -49,7 +49,7 @@ impl View for DiagnosticSummary { let in_progress = self.in_progress; MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.app_state::().theme.project_diagnostics; + let theme = &cx.global::().theme.project_diagnostics; if in_progress { Label::new( "Checking... ".to_string(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0b73ebdf63..67d7d7cd90 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -883,7 +883,7 @@ impl Editor { cx: &mut ViewContext, ) -> Self { let display_map = cx.add_model(|cx| { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = build_style(&*settings, get_field_editor_theme, None, cx); DisplayMap::new( buffer.clone(), @@ -1011,7 +1011,7 @@ impl Editor { fn style(&self, cx: &AppContext) -> EditorStyle { build_style( - cx.app_state::(), + cx.global::(), self.get_field_editor_theme, self.override_text_style.as_deref(), cx, @@ -2729,7 +2729,7 @@ impl Editor { } self.start_transaction(cx); - let tab_size = cx.app_state::().tab_size; + let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); let mut last_indent = None; self.buffer.update(cx, |buffer, cx| { @@ -2806,7 +2806,7 @@ impl Editor { } self.start_transaction(cx); - let tab_size = cx.app_state::().tab_size; + let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut deletion_ranges = Vec::new(); @@ -5324,7 +5324,7 @@ impl Editor { pub fn soft_wrap_mode(&self, cx: &AppContext) -> SoftWrap { let language = self.language(cx); - let settings = cx.app_state::(); + let settings = cx.global::(); let mode = self .soft_wrap_mode_override .unwrap_or_else(|| settings.soft_wrap(language)); @@ -5906,7 +5906,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } Arc::new(move |cx: &BlockContext| { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme.editor; let style = diagnostic_style(diagnostic.severity, is_valid, theme); let font_size = (style.text_scale_factor * settings.buffer_font_size).round(); @@ -9108,7 +9108,7 @@ mod tests { fn populate_settings(cx: &mut gpui::MutableAppContext) { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); } } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index ad8057db4e..49d800d619 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1490,7 +1490,7 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut gpui::MutableAppContext) { - cx.add_app_state(Settings::test(cx)); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) @@ -1512,7 +1512,7 @@ mod tests { #[gpui::test] fn test_layout_with_placeholder_text_and_blocks(cx: &mut gpui::MutableAppContext) { - cx.add_app_state(Settings::test(cx)); + cx.set_global(Settings::test(cx)); let buffer = MultiBuffer::build_simple("", cx); let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, None, cx) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d4e779484b..60dd8ae303 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -179,7 +179,7 @@ impl View for CursorPosition { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if let Some(position) = self.position { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; let mut text = format!("{},{}", position.row + 1, position.column + 1); if self.selected_count > 0 { write!(text, " ({} selected)", self.selected_count).unwrap(); @@ -252,7 +252,7 @@ impl View for DiagnosticMessage { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { if let Some(diagnostic) = &self.diagnostic { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; Label::new( diagnostic.message.split('\n').next().unwrap().to_string(), theme.diagnostic_message.clone(), diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 0dbb336007..808f7e4703 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -67,7 +67,7 @@ impl View for FileFinder { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Align::new( ConstrainedBox::new( Container::new( @@ -106,7 +106,7 @@ impl View for FileFinder { impl FileFinder { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -142,7 +142,7 @@ impl FileFinder { fn render_match(&self, path_match: &PathMatch, index: usize, cx: &AppContext) -> ElementBox { let selected_index = self.selected_index(); - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == selected_index { &settings.theme.selector.active_item } else { diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 50bae39ae9..f2dd4e76b1 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -59,7 +59,8 @@ impl GoToLine { } fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - if let Some(editor) = workspace.active_item(cx) + if let Some(editor) = workspace + .active_item(cx) .and_then(|active_item| active_item.downcast::()) { workspace.toggle_modal(cx, |cx, _| { @@ -148,7 +149,7 @@ impl View for GoToLine { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.selector; + let theme = &cx.global::().theme.selector; let label = format!( "{},{} of {} lines", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9831c3aa34..097f9d7199 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -799,7 +799,7 @@ impl MutableAppContext { models: Default::default(), views: Default::default(), windows: Default::default(), - app_states: Default::default(), + globals: Default::default(), element_states: Default::default(), ref_counts: Arc::new(Mutex::new(ref_counts)), background, @@ -1364,24 +1364,22 @@ 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 set_global(&mut self, state: T) { + self.cx.globals.insert(TypeId::of::(), Box::new(state)); } - pub fn update_app_state(&mut self, update: F) -> U + pub fn update_global(&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 + .globals .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); + self.cx.globals.insert(type_id, state); result } @@ -2054,7 +2052,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, windows: HashMap, - app_states: HashMap>, + globals: HashMap>, element_states: HashMap>, background: Arc, ref_counts: Arc>, @@ -2087,8 +2085,8 @@ impl AppContext { &self.platform } - pub fn app_state(&self) -> &T { - self.app_states + pub fn global(&self) -> &T { + self.globals .get(&TypeId::of::()) .expect("no app state has been added for this type") .downcast_ref() diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 2b4bafa479..fd4c8ff60b 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -69,7 +69,7 @@ impl View for OutlineView { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Flex::new(Axis::Vertical) .with_child( @@ -124,9 +124,12 @@ impl OutlineView { .active_item(cx) .and_then(|item| item.downcast::()) { - let buffer = editor.read(cx).buffer().read(cx).read(cx).outline(Some( - cx.app_state::().theme.editor.syntax.as_ref(), - )); + let buffer = editor + .read(cx) + .buffer() + .read(cx) + .read(cx) + .outline(Some(cx.global::().theme.editor.syntax.as_ref())); if let Some(outline) = buffer { workspace.toggle_modal(cx, |cx, _| { let view = cx.add_view(|cx| OutlineView::new(outline, editor, cx)); @@ -288,7 +291,7 @@ impl OutlineView { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -330,7 +333,7 @@ impl OutlineView { index: usize, cx: &AppContext, ) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == self.selected_match_index { &settings.theme.selector.active_item } else { diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 372f0c169b..0dd6b08bac 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -517,7 +517,7 @@ impl View for ProjectPanel { } fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { - let theme = &cx.app_state::().theme.project_panel; + let theme = &cx.global::().theme.project_panel; let mut container_style = theme.container; let padding = std::mem::take(&mut container_style.padding); let handle = self.handle.clone(); @@ -528,7 +528,7 @@ impl View for ProjectPanel { .map(|(_, worktree_entries)| worktree_entries.len()) .sum(), move |range, items, cx| { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let this = handle.upgrade(cx).unwrap(); this.update(cx.app, |this, cx| { this.for_each_visible_entry(range.clone(), cx, |entry, details, cx| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 29808ff7d9..d79b2dedae 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -69,7 +69,7 @@ impl View for ProjectSymbolsView { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); Flex::new(Axis::Vertical) .with_child( Container::new(ChildView::new(&self.query_editor).boxed()) @@ -233,7 +233,7 @@ impl ProjectSymbolsView { fn render_matches(&self, cx: &AppContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -276,7 +276,7 @@ impl ProjectSymbolsView { show_worktree_root_name: bool, cx: &AppContext, ) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let style = if index == self.selected_match_index { &settings.theme.selector.active_item } else { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 41b3d188bb..9c4f67aae3 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -66,7 +66,7 @@ impl View for SearchBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { @@ -197,7 +197,7 @@ impl SearchBar { ) -> ElementBox { let is_active = self.is_search_option_enabled(search_option); MouseEventHandler::new::(search_option as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = match (is_active, state.hovered) { (false, false) => &theme.option_button, (false, true) => &theme.hovered_option_button, @@ -222,7 +222,7 @@ impl SearchBar { ) -> ElementBox { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = if state.hovered { &theme.hovered_option_button } else { @@ -475,7 +475,7 @@ impl SearchBar { } } - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; editor.highlight_background::( ranges, theme.match_background, @@ -521,7 +521,7 @@ mod tests { 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(); - cx.update(|cx| cx.add_app_state(settings)); + cx.update(|cx| cx.set_global(settings)); let buffer = cx.update(|cx| { MultiBuffer::build_simple( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index acf7b2a234..6e402ad4a3 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -29,7 +29,7 @@ const MAX_TAB_TITLE_LEN: usize = 24; struct ActiveSearches(HashMap, WeakViewHandle>); pub fn init(cx: &mut MutableAppContext) { - cx.add_app_state(ActiveSearches::default()); + cx.set_global(ActiveSearches::default()); cx.add_bindings([ Binding::new("cmd-shift-F", ToggleFocus, Some("ProjectSearchView")), Binding::new("cmd-f", ToggleFocus, Some("ProjectSearchView")), @@ -155,7 +155,7 @@ impl View for 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 = &cx.app_state::().theme; + let theme = &cx.global::().theme; let text = if self.query_editor.read(cx).text(cx).is_empty() { "" } else if model.pending_search.is_some() { @@ -183,7 +183,7 @@ impl View for ProjectSearchView { fn on_focus(&mut self, cx: &mut ViewContext) { let handle = cx.weak_handle(); - cx.update_app_state(|state: &mut ActiveSearches, cx| { + cx.update_global(|state: &mut ActiveSearches, cx| { state .0 .insert(self.model.read(cx).project.downgrade(), handle) @@ -219,7 +219,7 @@ impl Item for ProjectSearchView { } fn tab_content(&self, tab_theme: &theme::Tab, cx: &gpui::AppContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let search_theme = &settings.theme.search; Flex::row() .with_child( @@ -370,12 +370,12 @@ impl ProjectSearchView { // 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| { + cx.update_global(|state: &mut ActiveSearches, cx| { state.0.retain(|project, _| project.is_upgradable(cx)) }); let active_search = cx - .app_state::() + .global::() .0 .get(&workspace.project().downgrade()); @@ -534,7 +534,7 @@ impl ProjectSearchView { if reset_selections { editor.select_ranges(match_ranges.first().cloned(), Some(Autoscroll::Fit), cx); } - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; editor.highlight_background::(match_ranges, theme.match_background, cx); }); if self.query_editor.is_focused(cx) { @@ -560,7 +560,7 @@ impl ProjectSearchView { } fn render_query_editor(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); let editor_container = if self.query_contains_error { theme.search.invalid_editor } else { @@ -624,7 +624,7 @@ impl ProjectSearchView { ) -> ElementBox { let is_active = self.is_option_enabled(option); MouseEventHandler::new::(option as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = match (is_active, state.hovered) { (false, false) => &theme.option_button, (false, true) => &theme.hovered_option_button, @@ -657,7 +657,7 @@ impl ProjectSearchView { ) -> ElementBox { enum NavButton {} MouseEventHandler::new::(direction as usize, cx, |state, cx| { - let theme = &cx.app_state::().theme.search; + let theme = &cx.global::().theme.search; let style = if state.hovered { &theme.hovered_option_button } else { @@ -689,7 +689,7 @@ mod tests { 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(); - cx.update(|cx| cx.add_app_state(settings)); + cx.update(|cx| cx.set_global(settings)); let fs = FakeFs::new(cx.background()); fs.insert_tree( diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index d70bfe2c21..7f1b9f90ad 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4414,7 +4414,7 @@ mod tests { async fn create_client(&mut self, cx: &mut TestAppContext, name: &str) -> TestClient { cx.update(|cx| { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); }); let http = FakeHttpClient::with_404_response(); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index c3199bc20b..f879940f21 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -54,7 +54,7 @@ impl ThemeSelector { cx.subscribe(&query_editor, Self::on_query_editor_event) .detach(); - let original_theme = cx.app_state::().theme.clone(); + let original_theme = cx.global::().theme.clone(); let mut this = Self { themes: registry, @@ -82,7 +82,7 @@ impl ThemeSelector { } fn reload(_: &mut Workspace, action: &Reload, cx: &mut ViewContext) { - let current_theme_name = cx.app_state::().theme.name.clone(); + let current_theme_name = cx.global::().theme.name.clone(); action.0.clear(); match action.0.get(¤t_theme_name) { Ok(theme) => { @@ -206,7 +206,7 @@ impl ThemeSelector { match event { editor::Event::Edited => { self.update_matches(cx); - self.select_if_matching(&cx.app_state::().theme.name); + self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); } editor::Event::Blurred => cx.emit(Event::Dismissed), @@ -216,7 +216,7 @@ impl ThemeSelector { fn render_matches(&self, cx: &mut RenderContext) -> ElementBox { if self.matches.is_empty() { - let settings = cx.app_state::(); + let settings = cx.global::(); return Container::new( Label::new( "No matches".into(), @@ -251,7 +251,7 @@ impl ThemeSelector { } fn render_match(&self, theme_match: &StringMatch, index: usize, cx: &AppContext) -> ElementBox { - let settings = cx.app_state::(); + let settings = cx.global::(); let theme = &settings.theme; let container = Container::new( @@ -276,7 +276,7 @@ impl ThemeSelector { } fn set_theme(theme: Arc, cx: &mut MutableAppContext) { - cx.update_app_state::(|settings, cx| { + cx.update_global::(|settings, cx| { settings.theme = theme; cx.refresh_windows(); }); @@ -299,7 +299,7 @@ impl View for ThemeSelector { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); Align::new( ConstrainedBox::new( Container::new( diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index 1b8b89f691..a12f81857f 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -116,7 +116,7 @@ impl View for LspStatus { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { @@ -166,7 +166,7 @@ impl View for LspStatus { } else if !self.failed.is_empty() { drop(pending_work); MouseEventHandler::new::(0, cx, |_, cx| { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Label::new( format!( "Failed to download {} language server{}. Click to dismiss.", diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 4ee9eb97ae..c388a16c2d 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -513,7 +513,7 @@ impl Pane { } fn render_tabs(&self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 782e51c706..4d00591787 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -38,7 +38,7 @@ impl View for StatusBar { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.app_state::().theme.workspace.status_bar; + let theme = &cx.global::().theme.workspace.status_bar; Flex::row() .with_children(self.left_items.iter().map(|i| { ChildView::new(i.as_ref()) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 92fcacaf85..6481e2c8ee 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -109,7 +109,7 @@ where V: Item, F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { - cx.add_app_state::(Arc::new(move |window_id, project, model, cx| { + cx.set_global::(Arc::new(move |window_id, project, model, cx| { Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx))) })); } @@ -371,7 +371,7 @@ impl WorkspaceParams { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut MutableAppContext) -> Self { let settings = Settings::test(cx); - cx.add_app_state(settings); + cx.set_global(settings); let fs = project::FakeFs::new(cx.background().clone()); let languages = Arc::new(LanguageRegistry::test()); @@ -825,7 +825,7 @@ impl Workspace { .ok_or_else(|| anyhow!("buffer has no entry")) })?; let (window_id, build_editor) = this.update(&mut cx, |_, cx| { - (cx.window_id(), cx.app_state::().clone()) + (cx.window_id(), cx.global::().clone()) }); let build_editor = move |cx: &mut MutableAppContext| build_editor(window_id, project, buffer, cx); @@ -948,7 +948,7 @@ impl Workspace { } fn render_connection_status(&self, cx: &mut RenderContext) -> Option { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; match &*self.client.status().borrow() { client::Status::ConnectionError | client::Status::ConnectionLost @@ -1134,7 +1134,7 @@ impl Workspace { fn render_disconnected_overlay(&self, cx: &AppContext) -> Option { if self.project.read(cx).is_read_only() { - let theme = &cx.app_state::().theme; + let theme = &cx.global::().theme; Some( EventHandler::new( Label::new( @@ -1165,7 +1165,7 @@ impl View for Workspace { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - let theme = cx.app_state::().theme.clone(); + let theme = cx.global::().theme.clone(); Stack::new() .with_child( Flex::column() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 190a279737..cf149b2469 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -101,7 +101,7 @@ fn main() { cx.spawn(|mut cx| async move { while let Some(settings) = settings_rx.next().await { cx.update(|cx| { - cx.update_app_state(|s, _| *s = settings); + cx.update_global(|s, _| *s = settings); cx.refresh_windows(); }); } @@ -110,7 +110,7 @@ fn main() { languages.set_language_server_download_dir(zed::ROOT_PATH.clone()); languages.set_theme(&settings.theme.editor.syntax); - cx.add_app_state(settings); + cx.set_global(settings); let app_state = Arc::new(AppState { languages: Arc::new(languages), diff --git a/crates/zed/src/test.rs b/crates/zed/src/test.rs index f42e57c35d..5b3bb41c15 100644 --- a/crates/zed/src/test.rs +++ b/crates/zed/src/test.rs @@ -18,7 +18,7 @@ fn init_logger() { pub fn test_app_state(cx: &mut MutableAppContext) -> Arc { let settings = Settings::test(cx); editor::init(cx); - cx.add_app_state(settings); + cx.set_global(settings); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); let http = FakeHttpClient::with_404_response(); let client = Client::new(http.clone()); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 26b4150e24..2b61279a2c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -43,7 +43,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { cx.add_global_action(quit); cx.add_global_action({ move |action: &AdjustBufferFontSize, cx| { - cx.update_app_state::(|settings, cx| { + cx.update_global::(|settings, cx| { settings.buffer_font_size = (settings.buffer_font_size + action.0).max(MIN_FONT_SIZE); cx.refresh_windows(); From 6f9c37851ced80b02f88a2bc38d54a72f0b807a3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 14:39:02 +0100 Subject: [PATCH 014/139] Add `Editor::for_multibuffer` and repurpose `Editor::for_buffer` Co-Authored-By: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 3 ++- crates/editor/src/editor.rs | 23 ++++++++++++++--------- crates/search/src/buffer_search.rs | 10 ++++++---- crates/search/src/project_search.rs | 2 +- crates/server/src/rpc.rs | 15 ++++----------- 5 files changed, 27 insertions(+), 26 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a188761340..a1dd7be744 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -113,7 +113,8 @@ impl ProjectDiagnosticsEditor { let excerpts = cx.add_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); let editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts.clone(), Some(project_handle.clone()), cx); + let mut editor = + Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx); editor.set_vertical_scroll_margin(5, cx); editor }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 67d7d7cd90..aa2c09519c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -341,7 +341,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::find_all_references); workspace::register_editor_builder(cx, |project, buffer, cx| { - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Editor::for_buffer(buffer, Some(project), cx) }); } @@ -832,6 +831,15 @@ impl Editor { } pub fn for_buffer( + buffer: ModelHandle, + project: Option>, + cx: &mut ViewContext, + ) -> Self { + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, project, None, cx) + } + + pub fn for_multibuffer( buffer: ModelHandle, project: Option>, cx: &mut ViewContext, @@ -855,8 +863,7 @@ impl Editor { return item; } - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)); workspace.add_item(Box::new(editor.clone()), cx); editor } @@ -969,11 +976,8 @@ impl Editor { .update(cx, |project, cx| project.create_buffer(cx)) .log_err() { - let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); workspace.add_item( - Box::new( - cx.add_view(|cx| Editor::for_buffer(multibuffer, Some(project.clone()), cx)), - ), + Box::new(cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx))), cx, ); } @@ -2380,7 +2384,8 @@ impl Editor { workspace.update(&mut cx, |workspace, cx| { let project = workspace.project().clone(); - let editor = cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx)); + let editor = + cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); workspace.add_item(Box::new(editor.clone()), cx); editor.update(cx, |editor, cx| { let color = editor.style(cx).highlighted_line_background; @@ -4397,7 +4402,7 @@ impl Editor { workspace.update(&mut cx, |workspace, cx| { let editor = - cx.add_view(|cx| Editor::for_buffer(excerpt_buffer, Some(project), cx)); + cx.add_view(|cx| Editor::for_multibuffer(excerpt_buffer, Some(project), cx)); editor.update(cx, |editor, cx| { let color = editor.style(cx).highlighted_line_background; editor.highlight_background::(ranges_to_highlight, color, cx); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 9c4f67aae3..8eae666c45 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -510,8 +510,9 @@ impl SearchBar { #[cfg(test)] mod tests { use super::*; - use editor::{DisplayPoint, Editor, MultiBuffer}; + use editor::{DisplayPoint, Editor}; use gpui::{color::Color, TestAppContext}; + use language::Buffer; use std::sync::Arc; use unindent::Unindent as _; @@ -523,9 +524,10 @@ mod tests { let settings = Settings::new("Courier", &fonts, Arc::new(theme)).unwrap(); cx.update(|cx| cx.set_global(settings)); - let buffer = cx.update(|cx| { - MultiBuffer::build_simple( - &r#" + let buffer = cx.add_model(|cx| { + Buffer::new( + 0, + r#" A regular expression (shortened as regex or regexp;[1] also referred to as rational expression[2][3]) is a sequence of characters that specifies a search pattern in text. Usually such patterns are used by string-searching algorithms diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6e402ad4a3..ef8ff3611a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -339,7 +339,7 @@ impl ProjectSearchView { }); let results_editor = cx.add_view(|cx| { - let mut editor = Editor::for_buffer(excerpts, Some(project), cx); + let mut editor = Editor::for_multibuffer(excerpts, Some(project), cx); editor.set_searchable(false); editor }); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7f1b9f90ad..7e9bb38021 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1013,8 +1013,8 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, MultiBuffer, - Redo, Rename, ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, + ToOffset, ToggleCodeActions, Undo, }; use gpui::{executor, ModelHandle, TestAppContext}; use language::{ @@ -1140,10 +1140,7 @@ mod tests { .update(cx_b, |p, cx| p.open_buffer((worktree_id, "b.txt"), cx)) .await .unwrap(); - let buffer_b = cx_b.add_model(|cx| MultiBuffer::singleton(buffer_b, cx)); - buffer_b.read_with(cx_b, |buf, cx| { - assert_eq!(buf.read(cx).text(), "b-contents") - }); + buffer_b.read_with(cx_b, |buf, _| assert_eq!(buf.text(), "b-contents")); project_a.read_with(cx_a, |project, cx| { assert!(project.has_open_buffer((worktree_id, "b.txt"), cx)) }); @@ -2176,11 +2173,7 @@ mod tests { .unwrap(); let (window_b, _) = cx_b.add_window(|_| EmptyView); let editor_b = cx_b.add_view(window_b, |cx| { - Editor::for_buffer( - cx.add_model(|cx| MultiBuffer::singleton(buffer_b.clone(), cx)), - Some(project_b.clone()), - cx, - ) + Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); let mut fake_language_server = fake_language_servers.next().await.unwrap(); From bff414cfbc48e4cceb6f8dc2930edc90caacbd08 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 15:13:47 +0100 Subject: [PATCH 015/139] Remove `Editor::find_or_create` Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 26 ++------------ crates/editor/src/items.rs | 16 +++++++-- crates/project/src/project.rs | 10 ++++++ crates/project_symbols/src/project_symbols.rs | 2 +- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 35 +++++++++++++++++++ 6 files changed, 63 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aa2c09519c..40d589bea7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -847,27 +847,6 @@ impl Editor { Self::new(EditorMode::Full, buffer, project, None, cx) } - pub fn find_or_create( - workspace: &mut Workspace, - buffer: ModelHandle, - cx: &mut ViewContext, - ) -> ViewHandle { - let project = workspace.project().clone(); - - if let Some(item) = project::File::from_dyn(buffer.read(cx).file()) - .and_then(|file| file.project_entry_id(cx)) - .and_then(|entry_id| workspace.active_pane().read(cx).item_for_entry(entry_id)) - .and_then(|item| item.downcast()) - { - workspace.activate_item(&item, cx); - return item; - } - - let editor = cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)); - workspace.add_item(Box::new(editor.clone()), cx); - editor - } - pub fn clone(&self, cx: &mut ViewContext) -> Self { let mut clone = Self::new( self.mode, @@ -4324,8 +4303,7 @@ impl Editor { for definition in definitions { let range = definition.range.to_offset(definition.buffer.read(cx)); - let target_editor_handle = - Self::find_or_create(workspace, definition.buffer, cx); + let target_editor_handle = workspace.open_project_item(definition.buffer, cx); 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. @@ -5597,7 +5575,7 @@ impl Editor { workspace.activate_next_pane(cx); for (buffer, ranges) in new_selections_by_buffer.into_iter() { - let editor = Self::find_or_create(workspace, buffer, cx); + let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges(ranges, Some(Autoscroll::Newest), cx); }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 60dd8ae303..6f083f00a9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -4,13 +4,13 @@ use gpui::{ elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::{Bias, Diagnostic, File as _}; +use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; use util::ResultExt; -use workspace::{Item, ItemHandle, ItemNavHistory, Settings, StatusItemView}; +use workspace::{Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView}; impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { @@ -132,6 +132,18 @@ impl Item for Editor { } } +impl ProjectItem for Editor { + type Item = Buffer; + + fn for_project_item( + project: ModelHandle, + buffer: ModelHandle, + cx: &mut ViewContext, + ) -> Self { + Self::for_buffer(buffer, Some(project), cx) + } +} + pub struct CursorPosition { position: Option, selected_count: usize, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index ff535afafb..c999b8ce4e 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -49,6 +49,10 @@ use util::{post_inc, ResultExt, TryFutureExt as _}; pub use fs::*; pub use worktree::*; +pub trait Item: Entity { + fn entry_id(&self, cx: &AppContext) -> Option; +} + pub struct Project { worktrees: Vec, active_entry: Option, @@ -4522,6 +4526,12 @@ fn relativize_path(base: &Path, path: &Path) -> PathBuf { components.iter().map(|c| c.as_os_str()).collect() } +impl Item for Buffer { + fn entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.file()).and_then(|file| file.project_entry_id(cx)) + } +} + #[cfg(test)] mod tests { use super::{Event, *}; diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index d79b2dedae..27e125a592 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -354,7 +354,7 @@ impl ProjectSymbolsView { .read(cx) .clip_point_utf16(symbol.range.start, Bias::Left); - let editor = Editor::find_or_create(workspace, buffer, cx); + let editor = workspace.open_project_item::(buffer, cx); editor.update(cx, |editor, cx| { editor.select_ranges( [position..position], diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c388a16c2d..57a74b52ef 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -280,7 +280,7 @@ impl Pane { } } - pub(crate) fn open_item( + pub fn open_item( &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6481e2c8ee..d220dbc0fa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -195,6 +195,16 @@ pub trait Item: View { } } +pub trait ProjectItem: Item { + type Item: project::Item; + + fn for_project_item( + project: ModelHandle, + item: ModelHandle, + cx: &mut ViewContext, + ) -> Self; +} + pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; @@ -833,6 +843,31 @@ impl Workspace { }) } + pub fn open_project_item( + &mut self, + project_item: ModelHandle, + cx: &mut ViewContext, + ) -> ViewHandle + where + T: ProjectItem, + { + use project::Item as _; + + if let Some(item) = project_item + .read(cx) + .entry_id(cx) + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id)) + .and_then(|item| item.downcast()) + { + self.activate_item(&item, cx); + return item; + } + + let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); + self.add_item(Box::new(item.clone()), cx); + item + } + pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { if let Some(ix) = pane.read(cx).index_for_item(item) { From 5d14c9abdf9397aef8605937288dd649132081c1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 15:54:34 +0100 Subject: [PATCH 016/139] Introduce `workspace::register_project_item` This lets downstream crates like `editor` define how project items should be opened. Co-Authored-By: Nathan Sobo --- Cargo.lock | 2 + crates/editor/src/editor.rs | 2 +- crates/file_finder/Cargo.toml | 2 + crates/file_finder/src/file_finder.rs | 7 +++ crates/gpui/src/app.rs | 34 ++++++++++++- crates/project/src/project.rs | 21 +++++++- crates/workspace/src/workspace.rs | 69 +++++++++++++++------------ 7 files changed, 102 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 28c433c43f..4cfb831a58 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1782,7 +1782,9 @@ dependencies = [ name = "file_finder" version = "0.1.0" dependencies = [ + "ctor", "editor", + "env_logger", "fuzzy", "gpui", "postage", diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40d589bea7..30888d8a40 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -340,7 +340,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); - workspace::register_editor_builder(cx, |project, buffer, cx| { + workspace::register_project_item(cx, |project, buffer, cx| { Editor::for_buffer(buffer, Some(project), cx) }); } diff --git a/crates/file_finder/Cargo.toml b/crates/file_finder/Cargo.toml index c5300dbcd9..b946ea48fb 100644 --- a/crates/file_finder/Cargo.toml +++ b/crates/file_finder/Cargo.toml @@ -21,3 +21,5 @@ postage = { version = "0.4.1", features = ["futures-traits"] } gpui = { path = "../gpui", features = ["test-support"] } serde_json = { version = "1.0.64", features = ["preserve_order"] } workspace = { path = "../workspace", features = ["test-support"] } +ctor = "0.1" +env_logger = "0.8" diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 808f7e4703..ca41eb74a1 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -407,6 +407,13 @@ mod tests { use std::path::PathBuf; use workspace::{Workspace, WorkspaceParams}; + #[ctor::ctor] + fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + } + #[gpui::test] async fn test_matching_paths(cx: &mut gpui::TestAppContext) { cx.update(|cx| { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 097f9d7199..5488d31416 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1364,12 +1364,22 @@ impl MutableAppContext { Ok(pending) } + pub fn default_global(&mut self) -> &T { + self.cx + .globals + .entry(TypeId::of::()) + .or_insert_with(|| Box::new(T::default())) + .downcast_ref() + .unwrap() + } + pub fn set_global(&mut self, state: T) { self.cx.globals.insert(TypeId::of::(), Box::new(state)); } - pub fn update_global(&mut self, update: F) -> U + pub fn update_default_global(&mut self, update: F) -> U where + T: 'static + Default, F: FnOnce(&mut T, &mut MutableAppContext) -> U, { let type_id = TypeId::of::(); @@ -1377,7 +1387,23 @@ impl MutableAppContext { .cx .globals .remove(&type_id) - .expect("no app state has been added for this type"); + .unwrap_or_else(|| Box::new(T::default())); + let result = update(state.downcast_mut().unwrap(), self); + self.cx.globals.insert(type_id, state); + result + } + + pub fn update_global(&mut self, update: F) -> U + where + T: 'static, + F: FnOnce(&mut T, &mut MutableAppContext) -> U, + { + let type_id = TypeId::of::(); + let mut state = self + .cx + .globals + .remove(&type_id) + .expect("no global has been added for this type"); let result = update(state.downcast_mut().unwrap(), self); self.cx.globals.insert(type_id, state); result @@ -3715,6 +3741,10 @@ impl AnyModelHandle { pub fn is(&self) -> bool { self.model_type == TypeId::of::() } + + pub fn model_type(&self) -> TypeId { + self.model_type + } } impl From> for AnyModelHandle { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c999b8ce4e..317cf1ba02 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,8 +11,8 @@ use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ - AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task, - UpgradeModelHandle, WeakModelHandle, + AnyModelHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, + MutableAppContext, Task, UpgradeModelHandle, WeakModelHandle, }; use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, @@ -822,6 +822,23 @@ impl Project { Ok(buffer) } + pub fn open_path( + &mut self, + path: impl Into, + cx: &mut ModelContext, + ) -> Task> { + let task = self.open_buffer(path, cx); + cx.spawn_weak(|_, cx| async move { + let buffer = task.await?; + let project_entry_id = buffer + .read_with(&cx, |buffer, cx| { + File::from_dyn(buffer.file()).and_then(|file| file.project_entry_id(cx)) + }) + .ok_or_else(|| anyhow!("no project entry"))?; + Ok((project_entry_id, buffer.into())) + }) + } + pub fn open_buffer( &mut self, path: impl Into, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index d220dbc0fa..33155b5d4f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -9,6 +9,7 @@ mod status_bar; use anyhow::{anyhow, Result}; use client::{Authenticate, ChannelList, Client, User, UserStore}; use clock::ReplicaId; +use collections::HashMap; use gpui::{ action, color::Color, @@ -17,11 +18,11 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, MutableAppContext, - PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, + MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; -use language::{Buffer, LanguageRegistry}; +use language::LanguageRegistry; use log::error; pub use pane::*; pub use pane_group::*; @@ -41,13 +42,16 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; -pub type BuildEditor = Arc< - dyn Fn( - usize, - ModelHandle, - ModelHandle, - &mut MutableAppContext, - ) -> Box, +type ItemBuilders = HashMap< + TypeId, + Arc< + dyn Fn( + usize, + ModelHandle, + AnyModelHandle, + &mut MutableAppContext, + ) -> Box, + >, >; action!(Open, Arc); @@ -104,14 +108,20 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_editor_builder(cx: &mut MutableAppContext, build_editor: F) +pub fn register_project_item(cx: &mut MutableAppContext, build_item: F) where - V: Item, - F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, + V: ProjectItem, + F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { - cx.set_global::(Arc::new(move |window_id, project, model, cx| { - Box::new(cx.add_view(window_id, |cx| build_editor(project, model, cx))) - })); + cx.update_default_global(|builders: &mut ItemBuilders, _| { + builders.insert( + TypeId::of::(), + Arc::new(move |window_id, project, model, cx| { + let model = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| build_item(project, model, cx))) + }), + ); + }); } pub struct AppState { @@ -826,20 +836,19 @@ impl Workspace { )>, > { let project = self.project().clone(); - let buffer = project.update(cx, |project, cx| project.open_buffer(path, cx)); - cx.spawn(|this, mut cx| async move { - let buffer = buffer.await?; - let project_entry_id = buffer.read_with(&cx, |buffer, cx| { - project::File::from_dyn(buffer.file()) - .and_then(|file| file.project_entry_id(cx)) - .ok_or_else(|| anyhow!("buffer has no entry")) + let project_item = project.update(cx, |project, cx| project.open_path(path, cx)); + let window_id = cx.window_id(); + cx.as_mut().spawn(|mut cx| async move { + let (project_entry_id, project_item) = project_item.await?; + let build_item = cx.update(|cx| { + cx.default_global::() + .get(&project_item.model_type()) + .ok_or_else(|| anyhow!("no item builder for project item")) + .cloned() })?; - let (window_id, build_editor) = this.update(&mut cx, |_, cx| { - (cx.window_id(), cx.global::().clone()) - }); - let build_editor = - move |cx: &mut MutableAppContext| build_editor(window_id, project, buffer, cx); - Ok((project_entry_id, build_editor)) + let build_item = + move |cx: &mut MutableAppContext| build_item(window_id, project, project_item, cx); + Ok((project_entry_id, build_item)) }) } From 4bbfd0918eb78e0310b4aad1893d71e1437cb1d5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 16:50:29 +0100 Subject: [PATCH 017/139] Start defining follow protocol Co-Authored-By: Nathan Sobo --- crates/project/src/project.rs | 8 ++-- crates/rpc/proto/zed.proto | 74 +++++++++++++++++++++++++---------- crates/rpc/src/proto.rs | 9 +++-- crates/server/src/rpc.rs | 2 +- 4 files changed, 65 insertions(+), 28 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 317cf1ba02..c44364adac 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -279,7 +279,7 @@ impl Project { 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); + client.add_entity_request_handler(Self::handle_open_buffer_by_path); client.add_entity_request_handler(Self::handle_save_buffer); } @@ -930,7 +930,7 @@ impl Project { let path_string = path.to_string_lossy().to_string(); cx.spawn(|this, mut cx| async move { let response = rpc - .request(proto::OpenBuffer { + .request(proto::OpenBufferByPath { project_id, worktree_id: remote_worktree_id.to_proto(), path: path_string, @@ -3887,9 +3887,9 @@ impl Project { hasher.finalize().as_slice().try_into().unwrap() } - async fn handle_open_buffer( + async fn handle_open_buffer_by_path( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0cb621ab9..3cc5a91cbe 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -40,8 +40,9 @@ message Envelope { StartLanguageServer start_language_server = 33; UpdateLanguageServer update_language_server = 34; - OpenBuffer open_buffer = 35; - OpenBufferResponse open_buffer_response = 36; + OpenBufferById open_buffer_by_id = 35; + OpenBufferByPath open_buffer_by_path = 36; + OpenBufferResponse open_buffer_response = 37; UpdateBuffer update_buffer = 38; UpdateBufferFile update_buffer_file = 39; SaveBuffer save_buffer = 40; @@ -79,6 +80,10 @@ message Envelope { GetUsers get_users = 70; GetUsersResponse get_users_response = 71; + + Follow follow = 72; + FollowResponse follow_response = 73; + UpdateFollower update_follower = 74; } } @@ -241,12 +246,17 @@ message OpenBufferForSymbolResponse { Buffer buffer = 1; } -message OpenBuffer { +message OpenBufferByPath { uint64 project_id = 1; uint64 worktree_id = 2; string path = 3; } +message OpenBufferById { + uint64 project_id = 1; + uint64 id = 2; +} + message OpenBufferResponse { Buffer buffer = 1; } @@ -521,8 +531,49 @@ message UpdateContacts { repeated Contact contacts = 1; } +message UpdateDiagnostics { + uint32 replica_id = 1; + uint32 lamport_timestamp = 2; + repeated Diagnostic diagnostics = 3; +} + +message Follow {} + +message FollowResponse { + uint64 current_view_id = 1; + repeated View views = 2; +} + +message UpdateFollower { + uint64 current_view_id = 1; + repeated ViewUpdate view_updates = 2; +} + // Entities +message View { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + uint64 buffer_id = 1; + Selection newest_selection = 2; + } +} + +message ViewUpdate { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + Selection newest_selection = 1; + } +} + message Collaborator { uint32 peer_id = 1; uint32 replica_id = 2; @@ -578,17 +629,6 @@ message BufferState { repeated string completion_triggers = 8; } -message BufferFragment { - uint32 replica_id = 1; - uint32 local_timestamp = 2; - uint32 lamport_timestamp = 3; - uint32 insertion_offset = 4; - uint32 len = 5; - bool visible = 6; - repeated VectorClockEntry deletions = 7; - repeated VectorClockEntry max_undos = 8; -} - message SelectionSet { uint32 replica_id = 1; repeated Selection selections = 2; @@ -614,12 +654,6 @@ enum Bias { Right = 1; } -message UpdateDiagnostics { - uint32 replica_id = 1; - uint32 lamport_timestamp = 2; - repeated Diagnostic diagnostics = 3; -} - message Diagnostic { Anchor start = 1; Anchor end = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 54b26b830c..230db3119c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -175,7 +175,8 @@ messages!( (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), - (OpenBuffer, Background), + (OpenBufferById, Background), + (OpenBufferByPath, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), (OpenBufferResponse, Background), @@ -223,7 +224,8 @@ request_messages!( (GetUsers, GetUsersResponse), (JoinChannel, JoinChannelResponse), (JoinProject, JoinProjectResponse), - (OpenBuffer, OpenBufferResponse), + (OpenBufferById, OpenBufferResponse), + (OpenBufferByPath, OpenBufferResponse), (OpenBufferForSymbol, OpenBufferForSymbolResponse), (Ping, Ack), (PerformRename, PerformRenameResponse), @@ -255,7 +257,8 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, - OpenBuffer, + OpenBufferById, + OpenBufferByPath, OpenBufferForSymbol, PerformRename, PrepareRename, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7e9bb38021..948901c2a1 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -92,7 +92,7 @@ impl Server { .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::, From 2b4738d82dc4c00827b7fffd3894aa25c592b6e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 17 Mar 2022 17:34:35 +0100 Subject: [PATCH 018/139] Avoid passing a closure to `workspace::register_project_item` Co-Authored-By: Max Brunsfeld --- crates/editor/src/editor.rs | 4 +--- crates/workspace/src/workspace.rs | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30888d8a40..1ae9fdce27 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -340,9 +340,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::confirm_rename); cx.add_async_action(Editor::find_all_references); - workspace::register_project_item(cx, |project, buffer, cx| { - Editor::for_buffer(buffer, Some(project), cx) - }); + workspace::register_project_item::(cx); } trait SelectionExt { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33155b5d4f..df771a700a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -108,17 +108,16 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_project_item(cx: &mut MutableAppContext, build_item: F) +pub fn register_project_item(cx: &mut MutableAppContext) where V: ProjectItem, - F: 'static + Fn(ModelHandle, ModelHandle, &mut ViewContext) -> V, { cx.update_default_global(|builders: &mut ItemBuilders, _| { builders.insert( TypeId::of::(), Arc::new(move |window_id, project, model, cx| { - let model = model.downcast::().unwrap(); - Box::new(cx.add_view(window_id, |cx| build_item(project, model, cx))) + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx))) }), ); }); @@ -813,13 +812,13 @@ impl Workspace { let pane = self.active_pane().downgrade(); let task = self.load_path(path, cx); cx.spawn(|this, mut cx| async move { - let (project_entry_id, build_editor) = task.await?; + let (project_entry_id, build_item) = task.await?; let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("pane was closed"))?; this.update(&mut cx, |_, cx| { pane.update(cx, |pane, cx| { - Ok(pane.open_item(project_entry_id, cx, build_editor)) + Ok(pane.open_item(project_entry_id, cx, build_item)) }) }) }) From 9716ff79648353a13f8109478947e4443c3e112c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 10:46:54 -0700 Subject: [PATCH 019/139] Set up logic for starting following Co-Authored-By: Antonio Scandurra --- crates/rpc/proto/zed.proto | 22 ++++-- crates/rpc/src/proto.rs | 8 +++ crates/workspace/src/pane.rs | 24 +++++++ crates/workspace/src/workspace.rs | 116 ++++++++++++++++++++++++------ 4 files changed, 142 insertions(+), 28 deletions(-) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 3cc5a91cbe..d7a928e424 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -83,7 +83,8 @@ message Envelope { Follow follow = 72; FollowResponse follow_response = 73; - UpdateFollower update_follower = 74; + UpdateFollowers update_followers = 74; + Unfollow unfollow = 75; } } @@ -537,16 +538,27 @@ message UpdateDiagnostics { repeated Diagnostic diagnostics = 3; } -message Follow {} +message Follow { + uint64 project_id = 1; + uint32 leader_id = 2; +} message FollowResponse { uint64 current_view_id = 1; repeated View views = 2; } -message UpdateFollower { - uint64 current_view_id = 1; - repeated ViewUpdate view_updates = 2; +message UpdateFollowers { + uint64 project_id = 1; + uint64 current_view_id = 2; + repeated View created_views = 3; + repeated ViewUpdate updated_views = 4; + repeated uint32 follower_ids = 5; +} + +message Unfollow { + uint64 project_id = 1; + uint32 leader_id = 2; } // Entities diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 230db3119c..39a0d669d5 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -147,6 +147,8 @@ messages!( (BufferSaved, Foreground), (ChannelMessageSent, Foreground), (Error, Foreground), + (Follow, Foreground), + (FollowResponse, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), (GetChannelMessages, Foreground), @@ -196,6 +198,7 @@ messages!( (SendChannelMessageResponse, Foreground), (ShareProject, Foreground), (Test, Foreground), + (Unfollow, Foreground), (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), @@ -203,6 +206,7 @@ messages!( (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), + (UpdateFollowers, Foreground), (UpdateWorktree, Foreground), ); @@ -212,6 +216,7 @@ request_messages!( ApplyCompletionAdditionalEdits, ApplyCompletionAdditionalEditsResponse ), + (Follow, FollowResponse), (FormatBuffers, FormatBuffersResponse), (GetChannelMessages, GetChannelMessagesResponse), (GetChannels, GetChannelsResponse), @@ -248,6 +253,7 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, + Follow, FormatBuffers, GetCodeActions, GetCompletions, @@ -266,11 +272,13 @@ entity_messages!( SaveBuffer, SearchProject, StartLanguageServer, + Unfollow, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateFollowers, UpdateLanguageServer, RegisterWorktree, UpdateWorktree, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 57a74b52ef..ec27d2400e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,5 +1,7 @@ use super::{ItemHandle, SplitDirection}; use crate::{Item, Settings, WeakItemHandle, Workspace}; +use anyhow::{anyhow, Result}; +use client::PeerId; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -105,6 +107,13 @@ pub struct Pane { active_toolbar_visible: bool, } +pub(crate) struct FollowerState { + pub(crate) leader_id: PeerId, + pub(crate) current_view_id: usize, + pub(crate) items_by_leader_view_id: + HashMap, Box)>, +} + pub trait Toolbar: View { fn active_item_changed( &mut self, @@ -313,6 +322,21 @@ impl Pane { cx.notify(); } + pub(crate) fn set_follow_state( + &mut self, + follower_state: FollowerState, + cx: &mut ViewContext, + ) -> Result<()> { + let current_view_id = follower_state.current_view_id as usize; + let (project_entry_id, item) = follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(); + self.add_item(project_entry_id, item, cx); + Ok(()) + } + pub fn items(&self) -> impl Iterator> { self.items.iter().map(|(_, view)| view) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index df771a700a..80f5b4e7a1 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,7 +7,7 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{Authenticate, ChannelList, Client, User, UserStore}; +use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore}; use clock::ReplicaId; use collections::HashMap; use gpui::{ @@ -42,16 +42,18 @@ use std::{ }; use theme::{Theme, ThemeRegistry}; -type ItemBuilders = HashMap< +type ProjectItemBuilders = HashMap< TypeId, - Arc< - dyn Fn( - usize, - ModelHandle, - AnyModelHandle, - &mut MutableAppContext, - ) -> Box, - >, + fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, +>; + +type FollowedItemBuilders = Vec< + fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, + ) -> Option, Box)>>>, >; action!(Open, Arc); @@ -108,18 +110,18 @@ pub fn init(cx: &mut MutableAppContext) { ]); } -pub fn register_project_item(cx: &mut MutableAppContext) -where - V: ProjectItem, -{ - cx.update_default_global(|builders: &mut ItemBuilders, _| { - builders.insert( - TypeId::of::(), - Arc::new(move |window_id, project, model, cx| { - let item = model.downcast::().unwrap(); - Box::new(cx.add_view(window_id, |cx| V::for_project_item(project, item, cx))) - }), - ); +pub fn register_project_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut ProjectItemBuilders, _| { + builders.insert(TypeId::of::(), |window_id, project, model, cx| { + let item = model.downcast::().unwrap(); + Box::new(cx.add_view(window_id, |cx| I::for_project_item(project, item, cx))) + }); + }); +} + +pub fn register_followed_item(cx: &mut MutableAppContext) { + cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { + builders.push(I::for_state_message) }); } @@ -214,6 +216,17 @@ pub trait ProjectItem: Item { ) -> Self; } +pub trait FollowedItem: Item { + type UpdateMessage; + + fn for_state_message( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut MutableAppContext, + ) -> Option, Box)>>>; +} + pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; @@ -840,7 +853,7 @@ impl Workspace { cx.as_mut().spawn(|mut cx| async move { let (project_entry_id, project_item) = project_item.await?; let build_item = cx.update(|cx| { - cx.default_global::() + cx.default_global::() .get(&project_item.model_type()) .ok_or_else(|| anyhow!("no item builder for project item")) .cloned() @@ -990,6 +1003,63 @@ impl Workspace { }); } + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { + if let Some(project_id) = self.project.read(cx).remote_id() { + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + cx.spawn_weak(|this, mut cx| async move { + let mut response = request.await?; + if let Some(this) = this.upgrade(&cx) { + let mut item_tasks = Vec::new(); + let (project, pane) = this.read_with(&cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + for view in &mut response.views { + let variant = view + .variant + .take() + .ok_or_else(|| anyhow!("missing variant"))?; + cx.update(|cx| { + let mut variant = Some(variant); + for build_item in cx.default_global::().clone() { + if let Some(task) = + build_item(pane.clone(), project.clone(), &mut variant, cx) + { + item_tasks.push(task); + break; + } else { + assert!(variant.is_some()); + } + } + }); + } + + let items = futures::future::try_join_all(item_tasks).await?; + let mut items_by_leader_view_id = HashMap::default(); + for (view, item) in response.views.into_iter().zip(items) { + items_by_leader_view_id.insert(view.id as usize, item); + } + + pane.update(&mut cx, |pane, cx| { + pane.set_follow_state( + FollowerState { + leader_id, + current_view_id: response.current_view_id as usize, + items_by_leader_view_id, + }, + cx, + ) + })?; + } + Ok(()) + }) + } else { + Task::ready(Err(anyhow!("project is not remote"))) + } + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { let theme = &cx.global::().theme; match &*self.client.status().borrow() { From 845457e2c46c6d921433930478b873986c008d1d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 10:58:20 -0700 Subject: [PATCH 020/139] Always read project entry id from workspace::Item We cannot store a workspace item's project entry id separately, since buffers' entry ids can change (for example when doing a *save as*). Co-Authored-By: Antonio Scandurra --- crates/diagnostics/src/diagnostics.rs | 4 ++ crates/editor/src/items.rs | 6 ++- crates/search/src/project_search.rs | 4 ++ crates/workspace/src/pane.rs | 70 +++++++++++++-------------- crates/workspace/src/workspace.rs | 17 ++++--- 5 files changed, 59 insertions(+), 42 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a1dd7be744..7bee815ad9 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -450,6 +450,10 @@ impl workspace::Item for ProjectDiagnosticsEditor { None } + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { self.editor .update(cx, |editor, cx| editor.navigate(data, cx)); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 6f083f00a9..d865511a62 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -5,7 +5,7 @@ use gpui::{ ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; -use project::{File, Project, ProjectPath}; +use project::{File, Project, ProjectEntryId, ProjectPath}; use std::fmt::Write; use std::path::PathBuf; use text::{Point, Selection}; @@ -41,6 +41,10 @@ impl Item for Editor { }) } + fn project_entry_id(&self, cx: &AppContext) -> Option { + File::from_dyn(self.buffer().read(cx).file(cx)).and_then(|file| file.project_entry_id(cx)) + } + fn clone_on_split(&self, cx: &mut ViewContext) -> Option where Self: Sized, diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ef8ff3611a..f027c965c6 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -250,6 +250,10 @@ impl Item for ProjectSearchView { None } + fn project_entry_id(&self, _: &AppContext) -> Option { + None + } + fn can_save(&self, _: &gpui::AppContext) -> bool { true } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ec27d2400e..c3b6a5fd29 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -9,8 +9,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::{CursorStyle, NavigationDirection}, - AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyViewHandle, AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use project::{ProjectEntryId, ProjectPath}; use std::{ @@ -99,7 +99,7 @@ pub enum Event { } pub struct Pane { - items: Vec<(Option, Box)>, + items: Vec>, active_item_index: usize, nav_history: Rc>, toolbars: HashMap>, @@ -110,8 +110,7 @@ pub struct Pane { pub(crate) struct FollowerState { pub(crate) leader_id: PeerId, pub(crate) current_view_id: usize, - pub(crate) items_by_leader_view_id: - HashMap, Box)>, + pub(crate) items_by_leader_view_id: HashMap>, } pub trait Toolbar: View { @@ -295,8 +294,8 @@ impl Pane { cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { - for (ix, (existing_entry_id, item)) in self.items.iter().enumerate() { - if *existing_entry_id == Some(project_entry_id) { + for (ix, item) in self.items.iter().enumerate() { + if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); self.activate_item(ix, cx); return item; @@ -304,20 +303,15 @@ impl Pane { } let item = build_item(cx); - self.add_item(Some(project_entry_id), item.boxed_clone(), cx); + self.add_item(item.boxed_clone(), cx); item } - pub(crate) fn add_item( - &mut self, - project_entry_id: Option, - mut item: Box, - cx: &mut ViewContext, - ) { + pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { item.set_nav_history(self.nav_history.clone(), cx); item.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); - self.items.insert(item_idx, (project_entry_id, item)); + self.items.insert(item_idx, item); self.activate_item(item_idx, cx); cx.notify(); } @@ -328,39 +322,45 @@ impl Pane { cx: &mut ViewContext, ) -> Result<()> { let current_view_id = follower_state.current_view_id as usize; - let (project_entry_id, item) = follower_state + let item = follower_state .items_by_leader_view_id .get(¤t_view_id) .ok_or_else(|| anyhow!("invalid current view id"))? .clone(); - self.add_item(project_entry_id, item, cx); + self.add_item(item, cx); Ok(()) } pub fn items(&self) -> impl Iterator> { - self.items.iter().map(|(_, view)| view) + self.items.iter() } pub fn active_item(&self) -> Option> { - self.items - .get(self.active_item_index) - .map(|(_, view)| view.clone()) + self.items.get(self.active_item_index).cloned() } - pub fn project_entry_id_for_item(&self, item: &dyn ItemHandle) -> Option { - self.items.iter().find_map(|(entry_id, existing)| { + pub fn project_entry_id_for_item( + &self, + item: &dyn ItemHandle, + cx: &AppContext, + ) -> Option { + self.items.iter().find_map(|existing| { if existing.id() == item.id() { - *entry_id + existing.project_entry_id(cx) } else { None } }) } - pub fn item_for_entry(&self, entry_id: ProjectEntryId) -> Option> { - self.items.iter().find_map(|(id, view)| { - if *id == Some(entry_id) { - Some(view.boxed_clone()) + pub fn item_for_entry( + &self, + entry_id: ProjectEntryId, + cx: &AppContext, + ) -> Option> { + self.items.iter().find_map(|item| { + if item.project_entry_id(cx) == Some(entry_id) { + Some(item.boxed_clone()) } else { None } @@ -368,7 +368,7 @@ impl Pane { } pub fn index_for_item(&self, item: &dyn ItemHandle) -> Option { - self.items.iter().position(|(_, i)| i.id() == item.id()) + self.items.iter().position(|i| i.id() == item.id()) } pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { @@ -377,7 +377,7 @@ impl Pane { if prev_active_item_ix != self.active_item_index && prev_active_item_ix < self.items.len() { - self.items[prev_active_item_ix].1.deactivated(cx); + self.items[prev_active_item_ix].deactivated(cx); } self.update_active_toolbar(cx); self.focus_active_item(cx); @@ -408,13 +408,13 @@ impl Pane { pub fn close_active_item(&mut self, cx: &mut ViewContext) { if !self.items.is_empty() { - self.close_item(self.items[self.active_item_index].1.id(), cx) + self.close_item(self.items[self.active_item_index].id(), cx) } } pub fn close_inactive_items(&mut self, cx: &mut ViewContext) { if !self.items.is_empty() { - let active_item_id = self.items[self.active_item_index].1.id(); + let active_item_id = self.items[self.active_item_index].id(); self.close_items(cx, |id| id != active_item_id); } } @@ -430,7 +430,7 @@ impl Pane { ) { let mut item_ix = 0; let mut new_active_item_index = self.active_item_index; - self.items.retain(|(_, item)| { + self.items.retain(|item| { if should_close(item.id()) { if item_ix == self.active_item_index { item.deactivated(cx); @@ -529,7 +529,7 @@ impl Pane { fn update_active_toolbar(&mut self, cx: &mut ViewContext) { let active_item = self.items.get(self.active_item_index); for (toolbar_type_id, toolbar) in &self.toolbars { - let visible = toolbar.active_item_changed(active_item.map(|i| i.1.clone()), cx); + let visible = toolbar.active_item_changed(active_item.cloned(), cx); if Some(*toolbar_type_id) == self.active_toolbar_type { self.active_toolbar_visible = visible; } @@ -542,7 +542,7 @@ impl Pane { enum Tabs {} let tabs = MouseEventHandler::new::(0, cx, |mouse_state, cx| { let mut row = Flex::row(); - for (ix, (_, item)) in self.items.iter().enumerate() { + for (ix, item) in self.items.iter().enumerate() { let is_active = ix == self.active_item_index; row.add_child({ diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 80f5b4e7a1..85672db11b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -53,7 +53,7 @@ type FollowedItemBuilders = Vec< ModelHandle, &mut Option, &mut MutableAppContext, - ) -> Option, Box)>>>, + ) -> Option>>>, >; action!(Open, Arc); @@ -157,6 +157,7 @@ pub trait Item: View { fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext); fn clone_on_split(&self, _: &mut ViewContext) -> Option where @@ -224,12 +225,13 @@ pub trait FollowedItem: Item { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option, Box)>>>; + ) -> Option>>>; } pub trait ItemHandle: 'static { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; + fn project_entry_id(&self, cx: &AppContext) -> Option; fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; @@ -277,6 +279,10 @@ impl ItemHandle for ViewHandle { self.read(cx).project_path(cx) } + fn project_entry_id(&self, cx: &AppContext) -> Option { + self.read(cx).project_entry_id(cx) + } + fn boxed_clone(&self) -> Box { Box::new(self.clone()) } @@ -814,7 +820,7 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { self.active_pane() - .update(cx, |pane, cx| pane.add_item(None, item, cx)) + .update(cx, |pane, cx| pane.add_item(item, cx)) } pub fn open_path( @@ -877,7 +883,7 @@ impl Workspace { if let Some(item) = project_item .read(cx) .entry_id(cx) - .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id)) + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id, cx)) .and_then(|item| item.downcast()) { self.activate_item(&item, cx); @@ -959,10 +965,9 @@ 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() { - let project_entry_id = pane.read(cx).project_entry_id_for_item(item.as_ref()); if let Some(clone) = item.clone_on_split(cx.as_mut()) { new_pane.update(cx, |new_pane, cx| { - new_pane.add_item(project_entry_id, clone, cx); + new_pane.add_item(clone, cx); }); } } From 5702737de28013c758fbbad38279c2c7d30f1028 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 13:53:06 -0700 Subject: [PATCH 021/139] Start work on an integration test for following --- crates/server/src/rpc.rs | 180 +++++++++++++++++++++++++++++- crates/workspace/src/workspace.rs | 4 +- 2 files changed, 177 insertions(+), 7 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 948901c2a1..ce3cfff646 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -112,6 +112,7 @@ impl Server { .add_request_handler(Server::join_channel) .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) + .add_request_handler(Server::follow) .add_request_handler(Server::get_channel_messages); Arc::new(server) @@ -669,6 +670,25 @@ impl Server { Ok(()) } + async fn follow( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result { + let leader_id = ConnectionId(request.payload.leader_id); + if !self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)? + .contains(&leader_id) + { + Err(anyhow!("no such peer"))?; + } + let response = self + .peer + .forward_request(request.sender_id, leader_id, request.payload) + .await?; + Ok(response) + } + async fn get_channels( self: Arc, request: TypedEnvelope, @@ -1016,7 +1036,7 @@ mod tests { self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; - use gpui::{executor, ModelHandle, TestAppContext}; + use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, @@ -1028,7 +1048,7 @@ mod tests { fs::{FakeFs, Fs as _}, search::SearchQuery, worktree::WorktreeHandle, - DiagnosticSummary, Project, ProjectPath, + DiagnosticSummary, Project, ProjectPath, WorktreeId, }; use rand::prelude::*; use rpc::PeerId; @@ -1046,7 +1066,7 @@ mod tests { }, time::Duration, }; - use workspace::{Settings, Workspace, WorkspaceParams}; + use workspace::{Settings, SplitDirection, Workspace, WorkspaceParams}; #[cfg(test)] #[ctor::ctor] @@ -3225,7 +3245,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "main.rs").into(), cx) + workspace.open_path((worktree_id, "main.rs"), cx) }) .await .unwrap() @@ -3459,7 +3479,7 @@ mod tests { let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::new(¶ms, cx)); let editor_b = workspace_b .update(cx_b, |workspace, cx| { - workspace.open_path((worktree_id, "one.rs").into(), cx) + workspace.open_path((worktree_id, "one.rs"), cx) }) .await .unwrap() @@ -4148,6 +4168,80 @@ mod tests { } } + #[gpui::test(iterations = 10)] + async fn test_following(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap(); + let editor_a2 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap(); + + // Client B opens an editor. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap(); + + // Client B starts following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.follow(*leader_id, cx) + }) + .await + .unwrap(); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); @@ -4477,6 +4571,7 @@ mod tests { client, peer_id, user_store, + language_registry: Arc::new(LanguageRegistry::test()), project: Default::default(), buffers: Default::default(), }; @@ -4541,6 +4636,7 @@ mod tests { client: Arc, pub peer_id: PeerId, pub user_store: ModelHandle, + language_registry: Arc, project: Option>, buffers: HashSet>, } @@ -4568,6 +4664,80 @@ mod tests { while authed_user.next().await.unwrap().is_none() {} } + async fn build_local_project( + &mut self, + fs: Arc, + root_path: impl AsRef, + cx: &mut TestAppContext, + ) -> (ModelHandle, WorktreeId) { + let project = cx.update(|cx| { + Project::local( + self.client.clone(), + self.user_store.clone(), + self.language_registry.clone(), + fs, + cx, + ) + }); + self.project = Some(project.clone()); + let (worktree, _) = project + .update(cx, |p, cx| { + p.find_or_create_local_worktree(root_path, true, cx) + }) + .await + .unwrap(); + worktree + .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + project + .update(cx, |project, _| project.next_remote_id()) + .await; + (project, worktree.read_with(cx, |tree, _| tree.id())) + } + + async fn build_remote_project( + &mut self, + project_id: u64, + cx: &mut TestAppContext, + ) -> ModelHandle { + let project = Project::remote( + project_id, + self.client.clone(), + self.user_store.clone(), + self.language_registry.clone(), + FakeFs::new(cx.background()), + &mut cx.to_async(), + ) + .await + .unwrap(); + self.project = Some(project.clone()); + project + } + + fn build_workspace( + &self, + project: &ModelHandle, + cx: &mut TestAppContext, + ) -> ViewHandle { + let (window_id, _) = cx.add_window(|cx| EmptyView); + cx.add_view(window_id, |cx| { + let fs = project.read(cx).fs().clone(); + Workspace::new( + &WorkspaceParams { + fs, + project: project.clone(), + user_store: self.user_store.clone(), + languages: self.language_registry.clone(), + channel_list: cx.add_model(|cx| { + ChannelList::new(self.user_store.clone(), self.client.clone(), cx) + }), + client: self.client.clone(), + }, + cx, + ) + }) + } + fn simulate_host( mut self, project: ModelHandle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 85672db11b..b80372a981 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -825,11 +825,11 @@ impl Workspace { pub fn open_path( &mut self, - path: ProjectPath, + path: impl Into, cx: &mut ViewContext, ) -> Task, Arc>> { let pane = self.active_pane().downgrade(); - let task = self.load_path(path, cx); + let task = self.load_path(path.into(), cx); cx.spawn(|this, mut cx| async move { let (project_entry_id, build_item) = task.await?; let pane = pane From eda06ee4086c93d3cc7c3d9bfa71a5e3fae4099e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 17:53:38 -0700 Subject: [PATCH 022/139] Add AnyWeakViewHandle --- crates/gpui/src/app.rs | 57 ++++++++++++++++++++++++++++++++++++ crates/gpui/src/presenter.rs | 4 +++ 2 files changed, 61 insertions(+) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5488d31416..a4dcd52f5a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -93,6 +93,8 @@ pub trait UpgradeModelHandle { pub trait UpgradeViewHandle { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option>; + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option; } pub trait ReadView { @@ -647,6 +649,10 @@ impl UpgradeViewHandle for AsyncAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.0.borrow_mut().upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.0.borrow_mut().upgrade_any_view_handle(handle) + } } impl ReadModelWith for AsyncAppContext { @@ -2017,6 +2023,10 @@ impl UpgradeViewHandle for MutableAppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl ReadView for MutableAppContext { @@ -2174,6 +2184,19 @@ impl UpgradeViewHandle for AppContext { None } } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + if self.ref_counts.lock().is_entity_alive(handle.view_id) { + Some(AnyViewHandle::new( + handle.window_id, + handle.view_id, + handle.view_type, + self.ref_counts.clone(), + )) + } else { + None + } + } } impl ReadView for AppContext { @@ -2931,6 +2954,10 @@ impl UpgradeViewHandle for ViewContext<'_, V> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.cx.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { + self.cx.upgrade_any_view_handle(handle) + } } impl UpdateModel for ViewContext<'_, V> { @@ -3619,6 +3646,14 @@ impl AnyViewHandle { None } } + + pub fn downgrade(&self) -> AnyWeakViewHandle { + AnyWeakViewHandle { + window_id: self.window_id, + view_id: self.view_id, + view_type: self.view_type, + } + } } impl Clone for AnyViewHandle { @@ -3845,6 +3880,28 @@ impl Hash for WeakViewHandle { } } +pub struct AnyWeakViewHandle { + window_id: usize, + view_id: usize, + view_type: TypeId, +} + +impl AnyWeakViewHandle { + pub fn upgrade(&self, cx: &impl UpgradeViewHandle) -> Option { + cx.upgrade_any_view_handle(self) + } +} + +impl From> for AnyWeakViewHandle { + fn from(handle: WeakViewHandle) -> Self { + AnyWeakViewHandle { + window_id: handle.window_id, + view_id: handle.view_id, + view_type: TypeId::of::(), + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct ElementStateId { view_id: usize, diff --git a/crates/gpui/src/presenter.rs b/crates/gpui/src/presenter.rs index 88ccb694d2..b4e419107a 100644 --- a/crates/gpui/src/presenter.rs +++ b/crates/gpui/src/presenter.rs @@ -299,6 +299,10 @@ impl<'a> UpgradeViewHandle for LayoutContext<'a> { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { self.app.upgrade_view_handle(handle) } + + fn upgrade_any_view_handle(&self, handle: &crate::AnyWeakViewHandle) -> Option { + self.app.upgrade_any_view_handle(handle) + } } impl<'a> ElementStateContext for LayoutContext<'a> { From 0fdaa1d7158d2a5754eee48951d50d2e1a9c2c04 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 17 Mar 2022 17:53:49 -0700 Subject: [PATCH 023/139] WIP --- Cargo.lock | 1 + crates/client/src/channel.rs | 2 +- crates/client/src/client.rs | 156 ++++++++++++++++++++++++------ crates/editor/Cargo.toml | 1 + crates/editor/src/items.rs | 27 +++++- crates/project/src/project.rs | 28 +++--- crates/server/src/rpc.rs | 3 + crates/workspace/src/workspace.rs | 81 +++++++++++++--- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 10 files changed, 245 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4cfb831a58..bd93ae3e27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1629,6 +1629,7 @@ dependencies = [ "postage", "project", "rand 0.8.3", + "rpc", "serde", "smallvec", "smol", diff --git a/crates/client/src/channel.rs b/crates/client/src/channel.rs index 18a0e156db..ac235dc19e 100644 --- a/crates/client/src/channel.rs +++ b/crates/client/src/channel.rs @@ -181,7 +181,7 @@ impl Entity for Channel { impl Channel { pub fn init(rpc: &Arc) { - rpc.add_entity_message_handler(Self::handle_message_sent); + rpc.add_model_message_handler(Self::handle_message_sent); } pub fn new( diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 59110f73c6..b56017c789 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -13,8 +13,8 @@ use async_tungstenite::tungstenite::{ }; use futures::{future::LocalBoxFuture, FutureExt, StreamExt}; use gpui::{ - action, AnyModelHandle, AnyWeakModelHandle, AsyncAppContext, Entity, ModelContext, ModelHandle, - MutableAppContext, Task, + action, AnyModelHandle, AnyViewHandle, AnyWeakModelHandle, AnyWeakViewHandle, AsyncAppContext, + Entity, ModelContext, ModelHandle, MutableAppContext, Task, View, ViewContext, ViewHandle, }; use http::HttpClient; use lazy_static::lazy_static; @@ -139,16 +139,16 @@ struct ClientState { entity_id_extractors: HashMap u64>>, _reconnect_task: Option>, reconnect_interval: Duration, - models_by_entity_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakModelHandle>, + entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>, models_by_message_type: HashMap, - model_types_by_message_type: HashMap, + entity_types_by_message_type: HashMap, message_handlers: HashMap< TypeId, Arc< dyn Send + Sync + Fn( - AnyModelHandle, + AnyEntityHandle, Box, AsyncAppContext, ) -> LocalBoxFuture<'static, Result<()>>, @@ -156,6 +156,16 @@ struct ClientState { >, } +enum AnyWeakEntityHandle { + Model(AnyWeakModelHandle), + View(AnyWeakViewHandle), +} + +enum AnyEntityHandle { + Model(AnyModelHandle), + View(AnyViewHandle), +} + #[derive(Clone, Debug)] pub struct Credentials { pub user_id: u64, @@ -171,8 +181,8 @@ impl Default for ClientState { _reconnect_task: None, reconnect_interval: Duration::from_secs(5), models_by_message_type: Default::default(), - models_by_entity_type_and_remote_id: Default::default(), - model_types_by_message_type: Default::default(), + entities_by_type_and_remote_id: Default::default(), + entity_types_by_message_type: Default::default(), message_handlers: Default::default(), } } @@ -195,13 +205,13 @@ impl Drop for Subscription { Subscription::Entity { client, id } => { if let Some(client) = client.upgrade() { let mut state = client.state.write(); - let _ = state.models_by_entity_type_and_remote_id.remove(id); + let _ = state.entities_by_type_and_remote_id.remove(id); } } Subscription::Message { client, id } => { if let Some(client) = client.upgrade() { let mut state = client.state.write(); - let _ = state.model_types_by_message_type.remove(id); + let _ = state.entity_types_by_message_type.remove(id); let _ = state.message_handlers.remove(id); } } @@ -239,7 +249,7 @@ impl Client { state._reconnect_task.take(); state.message_handlers.clear(); state.models_by_message_type.clear(); - state.models_by_entity_type_and_remote_id.clear(); + state.entities_by_type_and_remote_id.clear(); state.entity_id_extractors.clear(); self.peer.reset(); } @@ -313,6 +323,23 @@ impl Client { } } + pub fn add_view_for_remote_entity( + self: &Arc, + remote_id: u64, + cx: &mut ViewContext, + ) -> Subscription { + let handle = AnyViewHandle::from(cx.handle()); + let mut state = self.state.write(); + let id = (TypeId::of::(), remote_id); + state + .entities_by_type_and_remote_id + .insert(id, AnyWeakEntityHandle::View(handle.downgrade())); + Subscription::Entity { + client: Arc::downgrade(self), + id, + } + } + pub fn add_model_for_remote_entity( self: &Arc, remote_id: u64, @@ -322,8 +349,8 @@ impl Client { let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); state - .models_by_entity_type_and_remote_id - .insert(id, handle.downgrade()); + .entities_by_type_and_remote_id + .insert(id, AnyWeakEntityHandle::Model(handle.downgrade())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -355,6 +382,11 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, Arc::new(move |handle, envelope, cx| { + let handle = if let AnyEntityHandle::Model(handle) = handle { + handle + } else { + unreachable!(); + }; let model = handle.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); if let Some(client) = client.upgrade() { @@ -374,7 +406,60 @@ impl Client { } } - pub fn add_entity_message_handler(self: &Arc, handler: H) + pub fn add_view_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + let entity_type_id = TypeId::of::(); + let message_type_id = TypeId::of::(); + + let client = Arc::downgrade(self); + let mut state = self.state.write(); + state + .entity_types_by_message_type + .insert(message_type_id, entity_type_id); + state + .entity_id_extractors + .entry(message_type_id) + .or_insert_with(|| { + Box::new(|envelope| { + let envelope = envelope + .as_any() + .downcast_ref::>() + .unwrap(); + envelope.payload.remote_entity_id() + }) + }); + + let prev_handler = state.message_handlers.insert( + message_type_id, + Arc::new(move |handle, envelope, cx| { + let handle = if let AnyEntityHandle::View(handle) = handle { + handle + } else { + unreachable!(); + }; + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + if let Some(client) = client.upgrade() { + handler(model, *envelope, client.clone(), cx).boxed_local() + } else { + async move { Ok(()) }.boxed_local() + } + }), + ); + if prev_handler.is_some() { + panic!("registered handler for the same message twice"); + } + } + + pub fn add_model_message_handler(self: &Arc, handler: H) where M: EntityMessage, E: Entity, @@ -390,7 +475,7 @@ impl Client { let client = Arc::downgrade(self); let mut state = self.state.write(); state - .model_types_by_message_type + .entity_types_by_message_type .insert(message_type_id, model_type_id); state .entity_id_extractors @@ -408,9 +493,15 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, Arc::new(move |handle, envelope, cx| { - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); if let Some(client) = client.upgrade() { + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); + let handle = if let AnyEntityHandle::Model(handle) = handle { + handle + } else { + unreachable!(); + }; + handler(model, *envelope, client.clone(), cx).boxed_local() } else { async move { Ok(()) }.boxed_local() @@ -432,7 +523,7 @@ impl Client { + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_entity_message_handler(move |model, envelope, client, cx| { + self.add_model_message_handler(move |model, envelope, client, cx| { let receipt = envelope.receipt(); let response = handler(model, envelope, client.clone(), cx); async move { @@ -561,24 +652,26 @@ impl Client { .models_by_message_type .get(&payload_type_id) .and_then(|model| model.upgrade(&cx)) + .map(AnyEntityHandle::Model) .or_else(|| { - let model_type_id = - *state.model_types_by_message_type.get(&payload_type_id)?; + let entity_type_id = + *state.entity_types_by_message_type.get(&payload_type_id)?; let entity_id = state .entity_id_extractors .get(&message.payload_type_id()) .map(|extract_entity_id| { (extract_entity_id)(message.as_ref()) })?; - let model = state - .models_by_entity_type_and_remote_id - .get(&(model_type_id, entity_id))?; - if let Some(model) = model.upgrade(&cx) { - Some(model) + + let entity = state + .entities_by_type_and_remote_id + .get(&(entity_type_id, entity_id))?; + if let Some(entity) = entity.upgrade(&cx) { + Some(entity) } else { state - .models_by_entity_type_and_remote_id - .remove(&(model_type_id, entity_id)); + .entities_by_type_and_remote_id + .remove(&(entity_type_id, entity_id)); None } }); @@ -891,6 +984,15 @@ impl Client { } } +impl AnyWeakEntityHandle { + fn upgrade(&self, cx: &AsyncAppContext) -> Option { + match self { + AnyWeakEntityHandle::Model(handle) => handle.upgrade(cx).map(AnyEntityHandle::Model), + AnyWeakEntityHandle::View(handle) => handle.upgrade(cx).map(AnyEntityHandle::View), + } + } +} + fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { if IMPERSONATE_LOGIN.is_some() { return None; @@ -994,7 +1096,7 @@ mod tests { let (done_tx1, mut done_rx1) = smol::channel::unbounded(); let (done_tx2, mut done_rx2) = smol::channel::unbounded(); - client.add_entity_message_handler( + client.add_model_message_handler( move |model: ModelHandle, _: TypedEnvelope, _, cx| { match model.read_with(&cx, |model, _| model.id) { 1 => done_tx1.try_send(()).unwrap(), diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 609e92af0f..02069fb610 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -27,6 +27,7 @@ gpui = { path = "../gpui" } language = { path = "../language" } lsp = { path = "../lsp" } project = { path = "../project" } +rpc = { path = "../rpc" } snippet = { path = "../snippet" } sum_tree = { path = "../sum_tree" } theme = { path = "../theme" } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d865511a62..522c490cfa 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,13 +6,34 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use std::fmt::Write; -use std::path::PathBuf; +use rpc::proto; +use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; -use workspace::{Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView}; +use workspace::{ + FollowedItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, +}; + +impl FollowedItem for Editor { + fn for_state_message( + pane: ViewHandle, + project: ModelHandle, + state: &mut Option, + cx: &mut gpui::MutableAppContext, + ) -> Option>>> { + todo!() + } + + fn to_state_message(&self, cx: &mut gpui::MutableAppContext) -> proto::view::Variant { + todo!() + } +} impl Item for Editor { + fn as_followed(&self) -> Option<&dyn FollowedItem> { + Some(self) + } + fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c44364adac..4e2abbc2fe 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -124,6 +124,7 @@ pub enum Event { DiskBasedDiagnosticsUpdated, DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), + RemoteIdChanged(Option), } enum LanguageServerEvent { @@ -253,19 +254,19 @@ impl ProjectEntryId { impl Project { pub fn init(client: &Arc) { - client.add_entity_message_handler(Self::handle_add_collaborator); - client.add_entity_message_handler(Self::handle_buffer_reloaded); - client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_start_language_server); - client.add_entity_message_handler(Self::handle_update_language_server); - client.add_entity_message_handler(Self::handle_remove_collaborator); - client.add_entity_message_handler(Self::handle_register_worktree); - client.add_entity_message_handler(Self::handle_unregister_worktree); - client.add_entity_message_handler(Self::handle_unshare_project); - client.add_entity_message_handler(Self::handle_update_buffer_file); - client.add_entity_message_handler(Self::handle_update_buffer); - client.add_entity_message_handler(Self::handle_update_diagnostic_summary); - client.add_entity_message_handler(Self::handle_update_worktree); + client.add_model_message_handler(Self::handle_add_collaborator); + client.add_model_message_handler(Self::handle_buffer_reloaded); + client.add_model_message_handler(Self::handle_buffer_saved); + client.add_model_message_handler(Self::handle_start_language_server); + client.add_model_message_handler(Self::handle_update_language_server); + client.add_model_message_handler(Self::handle_remove_collaborator); + client.add_model_message_handler(Self::handle_register_worktree); + client.add_model_message_handler(Self::handle_unregister_worktree); + client.add_model_message_handler(Self::handle_unshare_project); + client.add_model_message_handler(Self::handle_update_buffer_file); + client.add_model_message_handler(Self::handle_update_buffer); + client.add_model_message_handler(Self::handle_update_diagnostic_summary); + client.add_model_message_handler(Self::handle_update_worktree); client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); client.add_entity_request_handler(Self::handle_apply_code_action); client.add_entity_request_handler(Self::handle_format_buffers); @@ -566,6 +567,7 @@ impl Project { self.subscriptions .push(self.client.add_model_for_remote_entity(remote_id, cx)); } + cx.emit(Event::RemoteIdChanged(remote_id)) } pub fn remote_id(&self) -> Option { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index ce3cfff646..23f7e1c182 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4563,6 +4563,9 @@ mod tests { Channel::init(&client); Project::init(&client); + cx.update(|cx| { + workspace::init(&client, cx); + }); let peer_id = PeerId(connection_id_rx.next().await.unwrap().0); let user_store = cx.add_model(|cx| UserStore::new(client.clone(), http, cx)); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b80372a981..25159fa689 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -7,7 +7,9 @@ pub mod sidebar; mod status_bar; use anyhow::{anyhow, Result}; -use client::{proto, Authenticate, ChannelList, Client, PeerId, User, UserStore}; +use client::{ + proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, +}; use clock::ReplicaId; use collections::HashMap; use gpui::{ @@ -18,9 +20,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, ClipboardItem, Entity, ImageData, ModelHandle, - MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, + ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, + ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -64,7 +66,7 @@ action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); -pub fn init(cx: &mut MutableAppContext) { +pub fn init(client: &Arc, cx: &mut MutableAppContext) { pane::init(cx); menu::init(cx); @@ -108,6 +110,9 @@ pub fn init(cx: &mut MutableAppContext) { None, ), ]); + + client.add_entity_request_handler(Workspace::handle_follow); + client.add_model_message_handler(Workspace::handle_unfollow); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -119,7 +124,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followed_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { builders.push(I::for_state_message) }); @@ -153,6 +158,9 @@ pub struct JoinProjectParams { } pub trait Item: View { + fn as_followed(&self) -> Option<&dyn FollowedItem> { + None + } fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -217,15 +225,17 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem: Item { - type UpdateMessage; - +pub trait FollowedItem { fn for_state_message( pane: ViewHandle, project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>>; + ) -> Option>>> + where + Self: Sized; + + fn to_state_message(&self, cx: &mut MutableAppContext) -> proto::view::Variant; } pub trait ItemHandle: 'static { @@ -459,6 +469,7 @@ pub struct Workspace { weak_self: WeakViewHandle, client: Arc, user_store: ModelHandle, + remote_entity_subscription: Option, fs: Arc, modal: Option, center: PaneGroup, @@ -481,6 +492,17 @@ impl Workspace { }) .detach(); + cx.subscribe(¶ms.project, move |this, project, event, cx| { + if let project::Event::RemoteIdChanged(remote_id) = event { + this.project_remote_id_changed(*remote_id, cx); + } + if project.read(cx).is_read_only() { + cx.blur(); + } + cx.notify() + }) + .detach(); + let pane = cx.add_view(|_| Pane::new()); let pane_id = pane.id(); cx.observe(&pane, move |me, _, cx| { @@ -517,7 +539,7 @@ impl Workspace { cx.emit_global(WorkspaceCreated(weak_self.clone())); - Workspace { + let mut this = Workspace { modal: None, weak_self, center: PaneGroup::new(pane.clone()), @@ -525,13 +547,16 @@ impl Workspace { active_pane: pane.clone(), status_bar, client: params.client.clone(), + remote_entity_subscription: None, user_store: params.user_store.clone(), fs: params.fs.clone(), left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), _observe_current_user, - } + }; + this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); + this } pub fn weak_handle(&self) -> WeakViewHandle { @@ -1008,6 +1033,15 @@ impl Workspace { }); } + fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { + if let Some(remote_id) = remote_id { + self.remote_entity_subscription = + Some(self.client.add_view_for_remote_entity(remote_id, cx)); + } else { + self.remote_entity_subscription.take(); + } + } + pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { if let Some(project_id) = self.project.read(cx).remote_id() { let request = self.client.request(proto::Follow { @@ -1271,6 +1305,29 @@ impl Workspace { None } } + + // RPC handlers + + async fn handle_follow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + cx: AsyncAppContext, + ) -> Result { + Ok(proto::FollowResponse { + current_view_id: 0, + views: Default::default(), + }) + } + + async fn handle_unfollow( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + cx: AsyncAppContext, + ) -> Result<()> { + Ok(()) + } } impl Entity for Workspace { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index cf149b2469..0543783494 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -69,7 +69,7 @@ fn main() { project::Project::init(&client); client::Channel::init(&client); client::init(client.clone(), cx); - workspace::init(cx); + workspace::init(&client, cx); editor::init(cx); go_to_line::init(cx); file_finder::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2b61279a2c..c33a96a94b 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -252,7 +252,7 @@ mod tests { async fn test_new_empty_workspace(cx: &mut TestAppContext) { let app_state = cx.update(test_app_state); cx.update(|cx| { - workspace::init(cx); + workspace::init(&app_state.client, cx); }); cx.dispatch_global_action(workspace::OpenNew(app_state.clone())); let window_id = *cx.window_ids().first().unwrap(); From f0b7bd6e17899a423e859efc5cd1cca5cde13ec5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 10:22:13 +0100 Subject: [PATCH 024/139] Serialize initial follow state in leader and reflect it in follower --- crates/client/src/client.rs | 40 ++++++++++++++-- crates/editor/src/editor.rs | 1 + crates/editor/src/items.rs | 59 ++++++++++++++++++++--- crates/language/src/proto.rs | 19 ++++---- crates/project/src/project.rs | 77 ++++++++++++++++++++++++------- crates/rpc/proto/zed.proto | 2 +- crates/server/src/rpc.rs | 8 ++++ crates/text/src/selection.rs | 6 +++ crates/workspace/src/pane.rs | 28 +++++++---- crates/workspace/src/workspace.rs | 69 +++++++++++++++++++-------- 10 files changed, 246 insertions(+), 63 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index b56017c789..c2527ed94a 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -494,14 +494,13 @@ impl Client { message_type_id, Arc::new(move |handle, envelope, cx| { if let Some(client) = client.upgrade() { - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); let handle = if let AnyEntityHandle::Model(handle) = handle { handle } else { unreachable!(); }; - + let model = handle.downcast::().unwrap(); + let envelope = envelope.into_any().downcast::>().unwrap(); handler(model, *envelope, client.clone(), cx).boxed_local() } else { async move { Ok(()) }.boxed_local() @@ -513,7 +512,7 @@ impl Client { } } - pub fn add_entity_request_handler(self: &Arc, handler: H) + pub fn add_model_request_handler(self: &Arc, handler: H) where M: EntityMessage + RequestMessage, E: Entity, @@ -546,6 +545,39 @@ impl Client { }) } + pub fn add_view_request_handler(self: &Arc, handler: H) + where + M: EntityMessage + RequestMessage, + E: View, + H: 'static + + Send + + Sync + + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, + { + self.add_view_message_handler(move |view, envelope, client, cx| { + let receipt = envelope.receipt(); + let response = handler(view, envelope, client.clone(), cx); + async move { + match response.await { + Ok(response) => { + client.respond(receipt, response)?; + Ok(()) + } + Err(error) => { + client.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + )?; + Err(error) + } + } + } + }) + } + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { read_credentials_from_keychain(cx).is_some() } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1ae9fdce27..00aa828325 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -341,6 +341,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::find_all_references); workspace::register_project_item::(cx); + workspace::register_followed_item::(cx); } trait SelectionExt { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 522c490cfa..2c454dedb5 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,8 +1,8 @@ use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; -use anyhow::Result; +use anyhow::{anyhow, Result}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, RenderContext, Subscription, Task, View, - ViewContext, ViewHandle, + elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, + Task, View, ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; @@ -19,13 +19,58 @@ impl FollowedItem for Editor { pane: ViewHandle, project: ModelHandle, state: &mut Option, - cx: &mut gpui::MutableAppContext, + cx: &mut MutableAppContext, ) -> Option>>> { - todo!() + let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { + if let Some(proto::view::Variant::Editor(state)) = state.take() { + state + } else { + unreachable!() + } + } else { + return None; + }; + + let buffer = project.update(cx, |project, cx| { + project.open_buffer_by_id(state.buffer_id, cx) + }); + Some(cx.spawn(|mut cx| async move { + let buffer = buffer.await?; + let editor = pane + .read_with(&cx, |pane, cx| { + pane.items_of_type::().find(|editor| { + editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) + }) + }) + .unwrap_or_else(|| { + cx.add_view(pane.window_id(), |cx| { + Editor::for_buffer(buffer, Some(project), cx) + }) + }); + Ok(Box::new(editor) as Box<_>) + })) } - fn to_state_message(&self, cx: &mut gpui::MutableAppContext) -> proto::view::Variant { - todo!() + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + let buffer_id = self + .buffer + .read(cx) + .as_singleton() + .unwrap() + .read(cx) + .remote_id(); + let selection = self.newest_anchor_selection(); + let selection = Selection { + id: selection.id, + start: selection.start.text_anchor.clone(), + end: selection.end.text_anchor.clone(), + reversed: selection.reversed, + goal: Default::default(), + }; + proto::view::Variant::Editor(proto::view::Editor { + buffer_id, + newest_selection: Some(language::proto::serialize_selection(&selection)), + }) } } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 4a22d6ce5a..09d4236afe 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -100,15 +100,16 @@ pub fn serialize_undo_map_entry( } pub fn serialize_selections(selections: &Arc<[Selection]>) -> Vec { - selections - .iter() - .map(|selection| proto::Selection { - id: selection.id as u64, - start: Some(serialize_anchor(&selection.start)), - end: Some(serialize_anchor(&selection.end)), - reversed: selection.reversed, - }) - .collect() + selections.iter().map(serialize_selection).collect() +} + +pub fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(serialize_anchor(&selection.start)), + end: Some(serialize_anchor(&selection.end)), + reversed: selection.reversed, + } } pub fn serialize_diagnostics<'a>( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4e2abbc2fe..4a6f0dd6cf 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -267,21 +267,22 @@ impl Project { client.add_model_message_handler(Self::handle_update_buffer); client.add_model_message_handler(Self::handle_update_diagnostic_summary); client.add_model_message_handler(Self::handle_update_worktree); - client.add_entity_request_handler(Self::handle_apply_additional_edits_for_completion); - client.add_entity_request_handler(Self::handle_apply_code_action); - client.add_entity_request_handler(Self::handle_format_buffers); - client.add_entity_request_handler(Self::handle_get_code_actions); - client.add_entity_request_handler(Self::handle_get_completions); - 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_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_by_path); - client.add_entity_request_handler(Self::handle_save_buffer); + client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion); + client.add_model_request_handler(Self::handle_apply_code_action); + client.add_model_request_handler(Self::handle_format_buffers); + client.add_model_request_handler(Self::handle_get_code_actions); + client.add_model_request_handler(Self::handle_get_completions); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_lsp_command::); + client.add_model_request_handler(Self::handle_search_project); + client.add_model_request_handler(Self::handle_get_project_symbols); + client.add_model_request_handler(Self::handle_open_buffer_for_symbol); + client.add_model_request_handler(Self::handle_open_buffer_by_id); + client.add_model_request_handler(Self::handle_open_buffer_by_path); + client.add_model_request_handler(Self::handle_save_buffer); } pub fn local( @@ -488,7 +489,6 @@ impl Project { cx.update(|cx| Project::local(client, user_store, languages, fs, cx)) } - #[cfg(any(test, feature = "test-support"))] pub fn buffer_for_id(&self, remote_id: u64, cx: &AppContext) -> Option> { self.opened_buffers .get(&remote_id) @@ -981,6 +981,32 @@ impl Project { }) } + pub fn open_buffer_by_id( + &mut self, + id: u64, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(buffer) = self.buffer_for_id(id, cx) { + Task::ready(Ok(buffer)) + } else if self.is_local() { + Task::ready(Err(anyhow!("buffer {} does not exist", id))) + } else if let Some(project_id) = self.remote_id() { + let request = self + .client + .request(proto::OpenBufferById { project_id, id }); + cx.spawn(|this, mut cx| async move { + let buffer = request + .await? + .buffer + .ok_or_else(|| anyhow!("invalid buffer"))?; + this.update(&mut cx, |this, cx| this.deserialize_buffer(buffer, cx)) + .await + }) + } else { + Task::ready(Err(anyhow!("cannot open buffer while disconnected"))) + } + } + pub fn save_buffer_as( &mut self, buffer: ModelHandle, @@ -3889,6 +3915,25 @@ impl Project { hasher.finalize().as_slice().try_into().unwrap() } + async fn handle_open_buffer_by_id( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let peer_id = envelope.original_sender_id()?; + let buffer = this + .update(&mut cx, |this, cx| { + this.open_buffer_by_id(envelope.payload.id, cx) + }) + .await?; + this.update(&mut cx, |this, cx| { + Ok(proto::OpenBufferResponse { + buffer: Some(this.serialize_buffer_for_peer(&buffer, peer_id, cx)), + }) + }) + } + async fn handle_open_buffer_by_path( this: ModelHandle, envelope: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d7a928e424..f2197055f3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -544,7 +544,7 @@ message Follow { } message FollowResponse { - uint64 current_view_id = 1; + optional uint64 current_view_id = 1; repeated View views = 2; } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 23f7e1c182..785de7ea08 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -92,6 +92,7 @@ impl Server { .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( @@ -4240,6 +4241,13 @@ mod tests { }) .await .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "2.txt").into()) + ); } #[gpui::test(iterations = 100)] diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 4e7d6f5236..0c73c7388d 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -18,6 +18,12 @@ pub struct Selection { pub goal: SelectionGoal, } +impl Default for SelectionGoal { + fn default() -> Self { + Self::None + } +} + impl Selection { pub fn head(&self) -> T { if self.reversed { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index c3b6a5fd29..997aae3d96 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -109,7 +109,7 @@ pub struct Pane { pub(crate) struct FollowerState { pub(crate) leader_id: PeerId, - pub(crate) current_view_id: usize, + pub(crate) current_view_id: Option, pub(crate) items_by_leader_view_id: HashMap>, } @@ -308,6 +308,11 @@ impl Pane { } pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { + // Prevent adding the same item to the pane more than once. + if self.items.iter().any(|i| i.id() == item.id()) { + return; + } + item.set_nav_history(self.nav_history.clone(), cx); item.added_to_pane(cx); let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); @@ -321,13 +326,14 @@ impl Pane { follower_state: FollowerState, cx: &mut ViewContext, ) -> Result<()> { - let current_view_id = follower_state.current_view_id as usize; - let item = follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(); - self.add_item(item, cx); + if let Some(current_view_id) = follower_state.current_view_id { + let item = follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(); + self.add_item(item, cx); + } Ok(()) } @@ -335,6 +341,12 @@ impl Pane { self.items.iter() } + pub fn items_of_type<'a, T: View>(&'a self) -> impl 'a + Iterator> { + self.items + .iter() + .filter_map(|item| item.to_any().downcast()) + } + pub fn active_item(&self) -> Option> { self.items.get(self.active_item_index).cloned() } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 25159fa689..eba7f12981 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -111,8 +111,8 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { ), ]); - client.add_entity_request_handler(Workspace::handle_follow); - client.add_model_message_handler(Workspace::handle_unfollow); + client.add_view_request_handler(Workspace::handle_follow); + client.add_view_message_handler(Workspace::handle_unfollow); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -235,7 +235,7 @@ pub trait FollowedItem { where Self: Sized; - fn to_state_message(&self, cx: &mut MutableAppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; } pub trait ItemHandle: 'static { @@ -262,6 +262,8 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; + fn can_be_followed(&self, cx: &AppContext) -> bool; + fn to_state_message(&self, cx: &AppContext) -> Option; } pub trait WeakItemHandle { @@ -297,11 +299,7 @@ impl ItemHandle for ViewHandle { Box::new(self.clone()) } - fn clone_on_split( - &self, - // nav_history: Rc>, - cx: &mut MutableAppContext, - ) -> Option> { + fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option> { self.update(cx, |item, cx| { cx.add_option_view(|cx| item.clone_on_split(cx)) }) @@ -381,6 +379,16 @@ impl ItemHandle for ViewHandle { fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option { self.read(cx).act_as_type(type_id, self, cx) } + + fn can_be_followed(&self, cx: &AppContext) -> bool { + self.read(cx).as_followed().is_some() + } + + fn to_state_message(&self, cx: &AppContext) -> Option { + self.read(cx) + .as_followed() + .map(|item| item.to_state_message(cx)) + } } impl Into for Box { @@ -709,6 +717,13 @@ impl Workspace { } } + pub fn items<'a>( + &'a self, + cx: &'a AppContext, + ) -> impl 'a + Iterator> { + self.panes.iter().flat_map(|pane| pane.read(cx).items()) + } + pub fn item_of_type(&self, cx: &AppContext) -> Option> { self.items_of_type(cx).max_by_key(|item| item.id()) } @@ -717,11 +732,9 @@ impl Workspace { &'a self, cx: &'a AppContext, ) -> impl 'a + Iterator> { - self.panes.iter().flat_map(|pane| { - pane.read(cx) - .items() - .filter_map(|item| item.to_any().downcast()) - }) + self.panes + .iter() + .flat_map(|pane| pane.read(cx).items_of_type()) } pub fn active_item(&self, cx: &AppContext) -> Option> { @@ -1085,7 +1098,7 @@ impl Workspace { pane.set_follow_state( FollowerState { leader_id, - current_view_id: response.current_view_id as usize, + current_view_id: response.current_view_id.map(|id| id as usize), items_by_leader_view_id, }, cx, @@ -1310,13 +1323,33 @@ impl Workspace { async fn handle_follow( this: ViewHandle, - envelope: TypedEnvelope, + _: TypedEnvelope, _: Arc, cx: AsyncAppContext, ) -> Result { - Ok(proto::FollowResponse { - current_view_id: 0, - views: Default::default(), + this.read_with(&cx, |this, cx| { + let current_view_id = if let Some(active_item) = this.active_item(cx) { + if active_item.can_be_followed(cx) { + Some(active_item.id() as u64) + } else { + None + } + } else { + None + }; + Ok(proto::FollowResponse { + current_view_id, + views: this + .items(cx) + .filter_map(|item| { + let variant = item.to_state_message(cx)?; + Some(proto::View { + id: item.id() as u64, + variant: Some(variant), + }) + }) + .collect(), + }) }) } From 10e6d82c3ec95cf9a86979ee47794726057df9c9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 14:20:09 +0100 Subject: [PATCH 025/139] WIP: Start on sending view updates to followers --- crates/editor/src/items.rs | 29 +++++- crates/gpui/src/app.rs | 8 ++ crates/server/src/rpc.rs | 11 +++ crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 141 +++++++++++++++++++++--------- 5 files changed, 146 insertions(+), 45 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c454dedb5..98610c4a07 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -72,13 +72,34 @@ impl FollowedItem for Editor { newest_selection: Some(language::proto::serialize_selection(&selection)), }) } + + fn to_update_message( + &self, + event: &Self::Event, + cx: &AppContext, + ) -> Option { + match event { + Event::SelectionsChanged => { + let selection = self.newest_anchor_selection(); + let selection = Selection { + id: selection.id, + start: selection.start.text_anchor.clone(), + end: selection.end.text_anchor.clone(), + reversed: selection.reversed, + goal: Default::default(), + }; + Some(proto::view_update::Variant::Editor( + proto::view_update::Editor { + newest_selection: Some(language::proto::serialize_selection(&selection)), + }, + )) + } + _ => None, + } + } } impl Item for Editor { - fn as_followed(&self) -> Option<&dyn FollowedItem> { - Some(self) - } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a4dcd52f5a..f8f505ee78 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2121,6 +2121,10 @@ impl AppContext { &self.platform } + pub fn has_global(&self) -> bool { + self.globals.contains_key(&TypeId::of::()) + } + pub fn global(&self) -> &T { self.globals .get(&TypeId::of::()) @@ -3654,6 +3658,10 @@ impl AnyViewHandle { view_type: self.view_type, } } + + pub fn view_type(&self) -> TypeId { + self.view_type + } } impl Clone for AnyViewHandle { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 785de7ea08..7668533a35 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4248,6 +4248,17 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); + + // When client A activates a different editor, client B does so as well. + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(editor_a1.as_ref(), cx) + }); + workspace_b + .condition(cx_b, |workspace, cx| { + let active_item = workspace.active_item(cx).unwrap(); + active_item.project_path(cx) == Some((worktree_id, "1.txt").into()) + }) + .await; } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 997aae3d96..43aa1f54a1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -288,7 +288,7 @@ impl Pane { } } - pub fn open_item( + pub(crate) fn open_item( &mut self, project_entry_id: ProjectEntryId, cx: &mut ViewContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index eba7f12981..6f013d910f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11,7 +11,7 @@ use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; use clock::ReplicaId; -use collections::HashMap; +use collections::{HashMap, HashSet}; use gpui::{ action, color::Color, @@ -49,13 +49,18 @@ type ProjectItemBuilders = HashMap< fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, >; -type FollowedItemBuilders = Vec< - fn( - ViewHandle, - ModelHandle, - &mut Option, - &mut MutableAppContext, - ) -> Option>>>, +type FollowedItemBuilder = fn( + ViewHandle, + ModelHandle, + &mut Option, + &mut MutableAppContext, +) -> Option>>>; +type FollowedItemBuilders = HashMap< + TypeId, + ( + FollowedItemBuilder, + fn(AnyViewHandle) -> Box, + ), >; action!(Open, Arc); @@ -124,9 +129,14 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followed_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { - builders.push(I::for_state_message) + builders.insert( + TypeId::of::(), + (I::for_state_message, |this| { + Box::new(this.downcast::().unwrap()) + }), + ); }); } @@ -158,9 +168,6 @@ pub struct JoinProjectParams { } pub trait Item: View { - fn as_followed(&self) -> Option<&dyn FollowedItem> { - None - } fn deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) {} fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; @@ -225,7 +232,7 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem { +pub trait FollowedItem: Item { fn for_state_message( pane: ViewHandle, project: ModelHandle, @@ -234,8 +241,40 @@ pub trait FollowedItem { ) -> Option>>> where Self: Sized; - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_update_message( + &self, + event: &Self::Event, + cx: &AppContext, + ) -> Option; +} + +pub trait FollowedItemHandle { + fn id(&self) -> usize; + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_update_message( + &self, + event: &dyn Any, + cx: &AppContext, + ) -> Option; +} + +impl FollowedItemHandle for ViewHandle { + fn id(&self) -> usize { + self.id() + } + + fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + self.read(cx).to_state_message(cx) + } + + fn to_update_message( + &self, + event: &dyn Any, + cx: &AppContext, + ) -> Option { + self.read(cx).to_update_message(event.downcast_ref()?, cx) + } } pub trait ItemHandle: 'static { @@ -262,8 +301,7 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; - fn can_be_followed(&self, cx: &AppContext) -> bool; - fn to_state_message(&self, cx: &AppContext) -> Option; + fn to_followed_item_handle(&self, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { @@ -318,15 +356,22 @@ impl ItemHandle for ViewHandle { pane.close_item(item.id(), cx); return; } + if T::should_activate_item_on_event(event) { if let Some(ix) = pane.index_for_item(&item) { pane.activate_item(ix, cx); pane.activate(cx); } } + if T::should_update_tab_on_event(event) { cx.notify() } + + if let Some(message) = item + .to_followed_item_handle(cx) + .and_then(|i| i.to_update_message(event, cx)) + {} }) .detach(); } @@ -380,14 +425,14 @@ impl ItemHandle for ViewHandle { self.read(cx).act_as_type(type_id, self, cx) } - fn can_be_followed(&self, cx: &AppContext) -> bool { - self.read(cx).as_followed().is_some() - } - - fn to_state_message(&self, cx: &AppContext) -> Option { - self.read(cx) - .as_followed() - .map(|item| item.to_state_message(cx)) + fn to_followed_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); + let item = self.to_any(); + Some(builders.get(&item.view_type())?.1(item)) + } else { + None + } } } @@ -487,9 +532,16 @@ pub struct Workspace { active_pane: ViewHandle, status_bar: ViewHandle, project: ModelHandle, + leader_state: LeaderState, _observe_current_user: Task<()>, } +#[derive(Default)] +struct LeaderState { + followers: HashSet, + subscriptions: Vec, +} + impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { cx.observe(¶ms.project, |_, project, cx| { @@ -561,6 +613,7 @@ impl Workspace { left_sidebar: Sidebar::new(Side::Left), right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), + leader_state: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -1068,6 +1121,13 @@ impl Workspace { let (project, pane) = this.read_with(&cx, |this, _| { (this.project.clone(), this.active_pane().clone()) }); + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); for view in &mut response.views { let variant = view .variant @@ -1075,7 +1135,7 @@ impl Workspace { .ok_or_else(|| anyhow!("missing variant"))?; cx.update(|cx| { let mut variant = Some(variant); - for build_item in cx.default_global::().clone() { + for build_item in &item_builders { if let Some(task) = build_item(pane.clone(), project.clone(), &mut variant, cx) { @@ -1323,28 +1383,29 @@ impl Workspace { async fn handle_follow( this: ViewHandle, - _: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, - cx: AsyncAppContext, + mut cx: AsyncAppContext, ) -> Result { - this.read_with(&cx, |this, cx| { - let current_view_id = if let Some(active_item) = this.active_item(cx) { - if active_item.can_be_followed(cx) { - Some(active_item.id() as u64) - } else { - None - } - } else { - None - }; + this.update(&mut cx, |this, cx| { + this.leader_state + .followers + .insert(envelope.original_sender_id()?); + + let current_view_id = this + .active_item(cx) + .and_then(|i| i.to_followed_item_handle(cx)) + .map(|i| i.id() as u64); Ok(proto::FollowResponse { current_view_id, views: this .items(cx) .filter_map(|item| { - let variant = item.to_state_message(cx)?; + let id = item.id() as u64; + let item = item.to_followed_item_handle(cx)?; + let variant = item.to_state_message(cx); Some(proto::View { - id: item.id() as u64, + id, variant: Some(variant), }) }) From 3d81eb9ddf53b3baf019e2fd20080d321b2b7554 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 14:59:53 +0100 Subject: [PATCH 026/139] Allow accessing workspace after adding item to pane --- crates/workspace/src/pane.rs | 86 ++++++++++++------------ crates/workspace/src/workspace.rs | 104 +++++++++++++++++++++--------- 2 files changed, 116 insertions(+), 74 deletions(-) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 43aa1f54a1..8112fa7af4 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -107,12 +107,6 @@ pub struct Pane { active_toolbar_visible: bool, } -pub(crate) struct FollowerState { - pub(crate) leader_id: PeerId, - pub(crate) current_view_id: Option, - pub(crate) items_by_leader_view_id: HashMap>, -} - pub trait Toolbar: View { fn active_item_changed( &mut self, @@ -266,7 +260,17 @@ impl Pane { if let Some((project_entry_id, build_item)) = task.log_err() { pane.update(&mut cx, |pane, cx| { pane.nav_history.borrow_mut().set_mode(mode); - let item = pane.open_item(project_entry_id, cx, build_item); + }); + let item = workspace.update(&mut cx, |workspace, cx| { + Self::open_item( + workspace, + pane.clone(), + project_entry_id, + cx, + build_item, + ) + }); + pane.update(&mut cx, |pane, cx| { pane.nav_history .borrow_mut() .set_mode(NavigationMode::Normal); @@ -289,52 +293,50 @@ impl Pane { } pub(crate) fn open_item( - &mut self, + workspace: &mut Workspace, + pane: ViewHandle, project_entry_id: ProjectEntryId, - cx: &mut ViewContext, + cx: &mut ViewContext, build_item: impl FnOnce(&mut MutableAppContext) -> Box, ) -> Box { - for (ix, item) in self.items.iter().enumerate() { - if item.project_entry_id(cx) == Some(project_entry_id) { - let item = item.boxed_clone(); - self.activate_item(ix, cx); - return item; + let existing_item = pane.update(cx, |pane, cx| { + for (ix, item) in pane.items.iter().enumerate() { + if item.project_entry_id(cx) == Some(project_entry_id) { + let item = item.boxed_clone(); + pane.activate_item(ix, cx); + return Some(item); + } } + None + }); + if let Some(existing_item) = existing_item { + existing_item + } else { + let item = build_item(cx); + Self::add_item(workspace, pane, item.boxed_clone(), cx); + item } - - let item = build_item(cx); - self.add_item(item.boxed_clone(), cx); - item } - pub(crate) fn add_item(&mut self, mut item: Box, cx: &mut ViewContext) { + pub(crate) fn add_item( + workspace: &mut Workspace, + pane: ViewHandle, + mut item: Box, + cx: &mut ViewContext, + ) { // Prevent adding the same item to the pane more than once. - if self.items.iter().any(|i| i.id() == item.id()) { + if pane.read(cx).items.iter().any(|i| i.id() == item.id()) { return; } - item.set_nav_history(self.nav_history.clone(), cx); - item.added_to_pane(cx); - let item_idx = cmp::min(self.active_item_index + 1, self.items.len()); - self.items.insert(item_idx, item); - self.activate_item(item_idx, cx); - cx.notify(); - } - - pub(crate) fn set_follow_state( - &mut self, - follower_state: FollowerState, - cx: &mut ViewContext, - ) -> Result<()> { - if let Some(current_view_id) = follower_state.current_view_id { - let item = follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(); - self.add_item(item, cx); - } - Ok(()) + item.set_nav_history(pane.read(cx).nav_history.clone(), cx); + item.added_to_pane(workspace, pane.clone(), cx); + pane.update(cx, |pane, cx| { + let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); + pane.items.insert(item_idx, item); + pane.activate_item(item_idx, cx); + cx.notify(); + }); } pub fn items(&self) -> impl Iterator> { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 6f013d910f..5955f81e62 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -284,7 +284,12 @@ pub trait ItemHandle: 'static { fn boxed_clone(&self) -> Box; fn set_nav_history(&self, nav_history: Rc>, cx: &mut MutableAppContext); fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option>; - fn added_to_pane(&mut self, cx: &mut ViewContext); + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ); fn deactivated(&self, cx: &mut MutableAppContext); fn navigate(&self, data: Box, cx: &mut MutableAppContext); fn id(&self) -> usize; @@ -350,22 +355,37 @@ impl ItemHandle for ViewHandle { }) } - fn added_to_pane(&mut self, cx: &mut ViewContext) { - cx.subscribe(self, |pane, item, event, cx| { + fn added_to_pane( + &self, + workspace: &mut Workspace, + pane: ViewHandle, + cx: &mut ViewContext, + ) { + let pane = pane.downgrade(); + cx.subscribe(self, move |workspace, item, event, cx| { + let pane = if let Some(pane) = pane.upgrade(cx) { + pane + } else { + log::error!("unexpected item event after pane was dropped"); + return; + }; + if T::should_close_item_on_event(event) { - pane.close_item(item.id(), cx); + pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); return; } if T::should_activate_item_on_event(event) { - if let Some(ix) = pane.index_for_item(&item) { - pane.activate_item(ix, cx); - pane.activate(cx); - } + pane.update(cx, |pane, cx| { + if let Some(ix) = pane.index_for_item(&item) { + pane.activate_item(ix, cx); + pane.activate(cx); + } + }); } if T::should_update_tab_on_event(event) { - cx.notify() + pane.update(cx, |_, cx| cx.notify()); } if let Some(message) = item @@ -533,6 +553,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, + follower_states_by_leader: HashMap, _observe_current_user: Task<()>, } @@ -542,6 +563,11 @@ struct LeaderState { subscriptions: Vec, } +struct FollowerState { + current_view_id: Option, + items_by_leader_view_id: HashMap>, +} + impl Workspace { pub fn new(params: &WorkspaceParams, cx: &mut ViewContext) -> Self { cx.observe(¶ms.project, |_, project, cx| { @@ -614,6 +640,7 @@ impl Workspace { right_sidebar: Sidebar::new(Side::Right), project: params.project.clone(), leader_state: Default::default(), + follower_states_by_leader: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -910,8 +937,8 @@ impl Workspace { } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { - self.active_pane() - .update(cx, |pane, cx| pane.add_item(item, cx)) + let pane = self.active_pane().clone(); + Pane::add_item(self, pane, item, cx); } pub fn open_path( @@ -926,10 +953,14 @@ impl Workspace { let pane = pane .upgrade(&cx) .ok_or_else(|| anyhow!("pane was closed"))?; - this.update(&mut cx, |_, cx| { - pane.update(cx, |pane, cx| { - Ok(pane.open_item(project_entry_id, cx, build_item)) - }) + this.update(&mut cx, |this, cx| { + Ok(Pane::open_item( + this, + pane, + project_entry_id, + cx, + build_item, + )) }) }) } @@ -1057,9 +1088,7 @@ impl Workspace { 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()) { - new_pane.update(cx, |new_pane, cx| { - new_pane.add_item(clone, cx); - }); + Pane::add_item(self, new_pane.clone(), clone, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1149,21 +1178,32 @@ impl Workspace { } let items = futures::future::try_join_all(item_tasks).await?; - let mut items_by_leader_view_id = HashMap::default(); - for (view, item) in response.views.into_iter().zip(items) { - items_by_leader_view_id.insert(view.id as usize, item); - } - - pane.update(&mut cx, |pane, cx| { - pane.set_follow_state( - FollowerState { - leader_id, - current_view_id: response.current_view_id.map(|id| id as usize), - items_by_leader_view_id, - }, - cx, + let follower_state = FollowerState { + current_view_id: response.current_view_id.map(|id| id as usize), + items_by_leader_view_id: response + .views + .iter() + .map(|v| v.id as usize) + .zip(items) + .collect(), + }; + let current_item = if let Some(current_view_id) = follower_state.current_view_id + { + Some( + follower_state + .items_by_leader_view_id + .get(¤t_view_id) + .ok_or_else(|| anyhow!("invalid current view id"))? + .clone(), ) - })?; + } else { + None + }; + this.update(&mut cx, |this, cx| { + if let Some(item) = current_item { + Pane::add_item(this, pane, item, cx); + } + }); } Ok(()) }) From 7d7e10598ae9e1109a6e1a5d97b4ced8fc65f98f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 16:00:03 +0100 Subject: [PATCH 027/139] Broadcast active view to followers --- crates/editor/src/items.rs | 6 +- crates/rpc/proto/zed.proto | 38 ++++---- crates/server/src/rpc.rs | 36 ++++++++ crates/workspace/src/pane.rs | 5 +- crates/workspace/src/workspace.rs | 145 ++++++++++++++++++++++++------ 5 files changed, 182 insertions(+), 48 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 98610c4a07..7d89341c0d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -77,7 +77,7 @@ impl FollowedItem for Editor { &self, event: &Self::Event, cx: &AppContext, - ) -> Option { + ) -> Option { match event { Event::SelectionsChanged => { let selection = self.newest_anchor_selection(); @@ -88,8 +88,8 @@ impl FollowedItem for Editor { reversed: selection.reversed, goal: Default::default(), }; - Some(proto::view_update::Variant::Editor( - proto::view_update::Editor { + Some(proto::update_followers::update_view::Variant::Editor( + proto::update_followers::update_view::Editor { newest_selection: Some(language::proto::serialize_selection(&selection)), }, )) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f2197055f3..1ea340278a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -544,16 +544,33 @@ message Follow { } message FollowResponse { - optional uint64 current_view_id = 1; + optional uint64 active_view_id = 1; repeated View views = 2; } message UpdateFollowers { uint64 project_id = 1; - uint64 current_view_id = 2; - repeated View created_views = 3; - repeated ViewUpdate updated_views = 4; - repeated uint32 follower_ids = 5; + repeated uint32 follower_ids = 2; + oneof variant { + UpdateActiveView update_active_view = 3; + View create_view = 4; + UpdateView update_view = 5; + } + + message UpdateActiveView { + optional uint64 id = 1; + } + + message UpdateView { + uint64 id = 1; + oneof variant { + Editor editor = 2; + } + + message Editor { + Selection newest_selection = 1; + } + } } message Unfollow { @@ -575,17 +592,6 @@ message View { } } -message ViewUpdate { - uint64 id = 1; - oneof variant { - Editor editor = 2; - } - - message Editor { - Selection newest_selection = 1; - } -} - message Collaborator { uint32 peer_id = 1; uint32 replica_id = 2; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 7668533a35..25857c53e4 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -114,6 +114,8 @@ impl Server { .add_message_handler(Server::leave_channel) .add_request_handler(Server::send_channel_message) .add_request_handler(Server::follow) + .add_message_handler(Server::unfollow) + .add_message_handler(Server::update_followers) .add_request_handler(Server::get_channel_messages); Arc::new(server) @@ -690,6 +692,40 @@ impl Server { Ok(response) } + async fn unfollow( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let leader_id = ConnectionId(request.payload.leader_id); + if !self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)? + .contains(&leader_id) + { + Err(anyhow!("no such peer"))?; + } + self.peer + .forward_send(request.sender_id, leader_id, request.payload)?; + Ok(()) + } + + async fn update_followers( + self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let connection_ids = self + .state() + .project_connection_ids(request.payload.project_id, request.sender_id)?; + for follower_id in &request.payload.follower_ids { + let follower_id = ConnectionId(*follower_id); + if connection_ids.contains(&follower_id) { + self.peer + .forward_send(request.sender_id, follower_id, request.payload.clone())?; + } + } + Ok(()) + } + async fn get_channels( self: Arc, request: TypedEnvelope, diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 8112fa7af4..a2e49f7f48 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -321,11 +321,12 @@ impl Pane { pub(crate) fn add_item( workspace: &mut Workspace, pane: ViewHandle, - mut item: Box, + item: Box, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. - if pane.read(cx).items.iter().any(|i| i.id() == item.id()) { + if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { + pane.update(cx, |pane, cx| pane.activate_item(item_ix, cx)); return; } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5955f81e62..3cbd167319 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,6 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; +use util::ResultExt; type ProjectItemBuilders = HashMap< TypeId, @@ -118,6 +119,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { client.add_view_request_handler(Workspace::handle_follow); client.add_view_message_handler(Workspace::handle_unfollow); + client.add_view_message_handler(Workspace::handle_update_followers); } pub fn register_project_item(cx: &mut MutableAppContext) { @@ -246,7 +248,7 @@ pub trait FollowedItem: Item { &self, event: &Self::Event, cx: &AppContext, - ) -> Option; + ) -> Option; } pub trait FollowedItemHandle { @@ -256,7 +258,7 @@ pub trait FollowedItemHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option; + ) -> Option; } impl FollowedItemHandle for ViewHandle { @@ -272,7 +274,7 @@ impl FollowedItemHandle for ViewHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option { + ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } } @@ -361,6 +363,16 @@ impl ItemHandle for ViewHandle { pane: ViewHandle, cx: &mut ViewContext, ) { + if let Some(followed_item) = self.to_followed_item_handle(cx) { + workspace.update_followers( + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item.id() as u64, + variant: Some(followed_item.to_state_message(cx)), + }), + cx, + ); + } + let pane = pane.downgrade(); cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = pane.upgrade(cx) { @@ -391,7 +403,17 @@ impl ItemHandle for ViewHandle { if let Some(message) = item .to_followed_item_handle(cx) .and_then(|i| i.to_update_message(event, cx)) - {} + { + workspace.update_followers( + proto::update_followers::Variant::UpdateView( + proto::update_followers::UpdateView { + id: item.id() as u64, + variant: Some(message), + }, + ), + cx, + ); + } }) .detach(); } @@ -553,18 +575,17 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, + follower_states_by_leader: HashMap, FollowerState>>, _observe_current_user: Task<()>, } #[derive(Default)] struct LeaderState { followers: HashSet, - subscriptions: Vec, } struct FollowerState { - current_view_id: Option, + active_view_id: Option, items_by_leader_view_id: HashMap>, } @@ -1053,6 +1074,15 @@ impl Workspace { cx.focus(&self.active_pane); cx.notify(); } + + self.update_followers( + proto::update_followers::Variant::UpdateActiveView( + proto::update_followers::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + }, + ), + cx, + ); } fn handle_pane_event( @@ -1179,7 +1209,7 @@ impl Workspace { let items = futures::future::try_join_all(item_tasks).await?; let follower_state = FollowerState { - current_view_id: response.current_view_id.map(|id| id as usize), + active_view_id: response.active_view_id.map(|id| id as usize), items_by_leader_view_id: response .views .iter() @@ -1187,22 +1217,12 @@ impl Workspace { .zip(items) .collect(), }; - let current_item = if let Some(current_view_id) = follower_state.current_view_id - { - Some( - follower_state - .items_by_leader_view_id - .get(¤t_view_id) - .ok_or_else(|| anyhow!("invalid current view id"))? - .clone(), - ) - } else { - None - }; this.update(&mut cx, |this, cx| { - if let Some(item) = current_item { - Pane::add_item(this, pane, item, cx); - } + this.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert(pane.downgrade(), follower_state); + this.leader_updated(leader_id, cx); }); } Ok(()) @@ -1212,6 +1232,24 @@ impl Workspace { } } + fn update_followers( + &self, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let project_id = self.project.read(cx).remote_id()?; + if !self.leader_state.followers.is_empty() { + self.client + .send(proto::UpdateFollowers { + project_id, + follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), + variant: Some(update), + }) + .log_err(); + } + None + } + fn render_connection_status(&self, cx: &mut RenderContext) -> Option { let theme = &cx.global::().theme; match &*self.client.status().borrow() { @@ -1432,12 +1470,12 @@ impl Workspace { .followers .insert(envelope.original_sender_id()?); - let current_view_id = this + let active_view_id = this .active_item(cx) .and_then(|i| i.to_followed_item_handle(cx)) .map(|i| i.id() as u64); Ok(proto::FollowResponse { - current_view_id, + active_view_id, views: this .items(cx) .filter_map(|item| { @@ -1458,9 +1496,62 @@ impl Workspace { this: ViewHandle, envelope: TypedEnvelope, _: Arc, - cx: AsyncAppContext, + mut cx: AsyncAppContext, ) -> Result<()> { - Ok(()) + this.update(&mut cx, |this, cx| { + this.leader_state + .followers + .remove(&envelope.original_sender_id()?); + Ok(()) + }) + } + + async fn handle_update_followers( + this: ViewHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + let leader_id = envelope.original_sender_id()?; + let follower_states = this + .follower_states_by_leader + .get_mut(&leader_id) + .ok_or_else(|| anyhow!("received follow update for an unfollowed peer"))?; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid update"))? + { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + for (pane, state) in follower_states { + state.active_view_id = update_active_view.id.map(|id| id as usize); + } + } + proto::update_followers::Variant::CreateView(_) => todo!(), + proto::update_followers::Variant::UpdateView(_) => todo!(), + } + + this.leader_updated(leader_id, cx); + Ok(()) + }) + } + + fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { + let mut items_to_add = Vec::new(); + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { + if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { + items_to_add.push((pane, item.clone())); + } + } + } + + for (pane, item) in items_to_add { + Pane::add_item(self, pane, item.clone(), cx); + } + + None } } From f4520d4184251b787db70ccee4403154b308a437 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 18 Mar 2022 18:07:03 +0100 Subject: [PATCH 028/139] WIP --- crates/editor/src/editor.rs | 2 +- crates/editor/src/items.rs | 4 ++-- crates/workspace/src/workspace.rs | 21 +++++++++++++++------ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 00aa828325..3f1c9c5fd2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -341,7 +341,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_async_action(Editor::find_all_references); workspace::register_project_item::(cx); - workspace::register_followed_item::(cx); + workspace::register_followable_item::(cx); } trait SelectionExt { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7d89341c0d..410ffcddca 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -11,10 +11,10 @@ use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; use workspace::{ - FollowedItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, + FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, }; -impl FollowedItem for Editor { +impl FollowableItem for Editor { fn for_state_message( pane: ViewHandle, project: ModelHandle, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3cbd167319..b011566c0f 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -131,7 +131,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { }); } -pub fn register_followed_item(cx: &mut MutableAppContext) { +pub fn register_followable_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { builders.insert( TypeId::of::(), @@ -234,7 +234,7 @@ pub trait ProjectItem: Item { ) -> Self; } -pub trait FollowedItem: Item { +pub trait FollowableItem: Item { fn for_state_message( pane: ViewHandle, project: ModelHandle, @@ -261,7 +261,7 @@ pub trait FollowedItemHandle { ) -> Option; } -impl FollowedItemHandle for ViewHandle { +impl FollowedItemHandle for ViewHandle { fn id(&self) -> usize { self.id() } @@ -586,7 +586,12 @@ struct LeaderState { struct FollowerState { active_view_id: Option, - items_by_leader_view_id: HashMap>, + items_by_leader_view_id: HashMap, +} + +enum FollowerItem { + Loading(Vec), + Loaded(Box), } impl Workspace { @@ -1524,12 +1529,16 @@ impl Workspace { .ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - for (pane, state) in follower_states { + for state in follower_states.values_mut() { state.active_view_id = update_active_view.id.map(|id| id as usize); } } + proto::update_followers::Variant::UpdateView(update_view) => { + for state in follower_states.values_mut() { + state.items_by_leader_view_id.get(k) + } + } proto::update_followers::Variant::CreateView(_) => todo!(), - proto::update_followers::Variant::UpdateView(_) => todo!(), } this.leader_updated(leader_id, cx); From 2c5317556635f9d16a80ff1829f0c51c52a1c6f7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 10:12:02 -0700 Subject: [PATCH 029/139] Rename FollowedItem -> FollowableItem Co-Authored-By: Antonio Scandurra --- crates/workspace/src/workspace.rs | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b011566c0f..ca1e733fe3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -50,17 +50,17 @@ type ProjectItemBuilders = HashMap< fn(usize, ModelHandle, AnyModelHandle, &mut MutableAppContext) -> Box, >; -type FollowedItemBuilder = fn( +type FollowableItemBuilder = fn( ViewHandle, ModelHandle, &mut Option, &mut MutableAppContext, ) -> Option>>>; -type FollowedItemBuilders = HashMap< +type FollowableItemBuilders = HashMap< TypeId, ( - FollowedItemBuilder, - fn(AnyViewHandle) -> Box, + FollowableItemBuilder, + fn(AnyViewHandle) -> Box, ), >; @@ -132,7 +132,7 @@ pub fn register_project_item(cx: &mut MutableAppContext) { } pub fn register_followable_item(cx: &mut MutableAppContext) { - cx.update_default_global(|builders: &mut FollowedItemBuilders, _| { + cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { builders.insert( TypeId::of::(), (I::for_state_message, |this| { @@ -251,7 +251,7 @@ pub trait FollowableItem: Item { ) -> Option; } -pub trait FollowedItemHandle { +pub trait FollowableItemHandle { fn id(&self) -> usize; fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( @@ -261,7 +261,7 @@ pub trait FollowedItemHandle { ) -> Option; } -impl FollowedItemHandle for ViewHandle { +impl FollowableItemHandle for ViewHandle { fn id(&self) -> usize { self.id() } @@ -308,7 +308,7 @@ pub trait ItemHandle: 'static { cx: &mut MutableAppContext, ) -> Task>; fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option; - fn to_followed_item_handle(&self, cx: &AppContext) -> Option>; + fn to_followable_item_handle(&self, cx: &AppContext) -> Option>; } pub trait WeakItemHandle { @@ -363,7 +363,7 @@ impl ItemHandle for ViewHandle { pane: ViewHandle, cx: &mut ViewContext, ) { - if let Some(followed_item) = self.to_followed_item_handle(cx) { + if let Some(followed_item) = self.to_followable_item_handle(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, @@ -401,7 +401,7 @@ impl ItemHandle for ViewHandle { } if let Some(message) = item - .to_followed_item_handle(cx) + .to_followable_item_handle(cx) .and_then(|i| i.to_update_message(event, cx)) { workspace.update_followers( @@ -467,9 +467,9 @@ impl ItemHandle for ViewHandle { self.read(cx).act_as_type(type_id, self, cx) } - fn to_followed_item_handle(&self, cx: &AppContext) -> Option> { - if cx.has_global::() { - let builders = cx.global::(); + fn to_followable_item_handle(&self, cx: &AppContext) -> Option> { + if cx.has_global::() { + let builders = cx.global::(); let item = self.to_any(); Some(builders.get(&item.view_type())?.1(item)) } else { @@ -1186,7 +1186,7 @@ impl Workspace { (this.project.clone(), this.active_pane().clone()) }); let item_builders = cx.update(|cx| { - cx.default_global::() + cx.default_global::() .values() .map(|b| b.0) .collect::>() @@ -1477,7 +1477,7 @@ impl Workspace { let active_view_id = this .active_item(cx) - .and_then(|i| i.to_followed_item_handle(cx)) + .and_then(|i| i.to_followable_item_handle(cx)) .map(|i| i.id() as u64); Ok(proto::FollowResponse { active_view_id, @@ -1485,7 +1485,7 @@ impl Workspace { .items(cx) .filter_map(|item| { let id = item.id() as u64; - let item = item.to_followed_item_handle(cx)?; + let item = item.to_followable_item_handle(cx)?; let variant = item.to_state_message(cx); Some(proto::View { id, From d02ab9bd061e1a11fb065531cdc25aff15f782bc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 12:25:30 -0700 Subject: [PATCH 030/139] Start work on updating editors's scroll positions when following Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 9 +++ crates/editor/src/items.rs | 68 +++++++++++--------- crates/editor/src/multi_buffer.rs | 8 +++ crates/language/src/proto.rs | 20 +++--- crates/rpc/proto/zed.proto | 4 +- crates/workspace/src/workspace.rs | 101 +++++++++++++++++++++--------- 6 files changed, 139 insertions(+), 71 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3f1c9c5fd2..aa41624029 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1033,6 +1033,14 @@ impl Editor { self.scroll_top_anchor = Some(anchor); } + cx.emit(Event::ScrollPositionChanged); + cx.notify(); + } + + fn set_scroll_top_anchor(&mut self, anchor: Anchor, cx: &mut ViewContext) { + self.scroll_position = Vector2F::zero(); + self.scroll_top_anchor = Some(anchor); + cx.emit(Event::ScrollPositionChanged); cx.notify(); } @@ -5634,6 +5642,7 @@ pub enum Event { Saved, TitleChanged, SelectionsChanged, + ScrollPositionChanged, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 410ffcddca..5ef739510b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use rpc::proto; +use rpc::proto::{self, update_followers::update_view}; use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; @@ -20,7 +20,7 @@ impl FollowableItem for Editor { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>> { + ) -> Option>>> { let state = if matches!(state, Some(proto::view::Variant::Editor(_))) { if let Some(proto::view::Variant::Editor(state)) = state.take() { state @@ -36,7 +36,7 @@ impl FollowableItem for Editor { }); Some(cx.spawn(|mut cx| async move { let buffer = buffer.await?; - let editor = pane + Ok(pane .read_with(&cx, |pane, cx| { pane.items_of_type::().find(|editor| { editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) @@ -46,8 +46,7 @@ impl FollowableItem for Editor { cx.add_view(pane.window_id(), |cx| { Editor::for_buffer(buffer, Some(project), cx) }) - }); - Ok(Box::new(editor) as Box<_>) + })) })) } @@ -59,17 +58,12 @@ impl FollowableItem for Editor { .unwrap() .read(cx) .remote_id(); - let selection = self.newest_anchor_selection(); - let selection = Selection { - id: selection.id, - start: selection.start.text_anchor.clone(), - end: selection.end.text_anchor.clone(), - reversed: selection.reversed, - goal: Default::default(), - }; proto::view::Variant::Editor(proto::view::Editor { buffer_id, - newest_selection: Some(language::proto::serialize_selection(&selection)), + scroll_top: self + .scroll_top_anchor + .as_ref() + .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), }) } @@ -77,26 +71,42 @@ impl FollowableItem for Editor { &self, event: &Self::Event, cx: &AppContext, - ) -> Option { + ) -> Option { match event { - Event::SelectionsChanged => { - let selection = self.newest_anchor_selection(); - let selection = Selection { - id: selection.id, - start: selection.start.text_anchor.clone(), - end: selection.end.text_anchor.clone(), - reversed: selection.reversed, - goal: Default::default(), - }; - Some(proto::update_followers::update_view::Variant::Editor( - proto::update_followers::update_view::Editor { - newest_selection: Some(language::proto::serialize_selection(&selection)), - }, - )) + Event::ScrollPositionChanged => { + Some(update_view::Variant::Editor(update_view::Editor { + scroll_top: self + .scroll_top_anchor + .as_ref() + .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + })) } _ => None, } } + + fn apply_update_message( + &mut self, + message: update_view::Variant, + cx: &mut ViewContext, + ) -> Result<()> { + match message { + update_view::Variant::Editor(message) => { + if let Some(anchor) = message.scroll_top { + let anchor = language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?; + let anchor = { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); + buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) + }; + self.set_scroll_top_anchor(anchor, cx); + } + } + } + Ok(()) + } } impl Item for Editor { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e145488d65..12493d6728 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -821,6 +821,14 @@ impl MultiBuffer { .map_or(Vec::new(), |state| state.excerpts.clone()) } + pub fn excerpt_ids(&self) -> Vec { + self.buffers + .borrow() + .values() + .flat_map(|state| state.excerpts.iter().cloned()) + .collect() + } + pub fn excerpt_containing( &self, position: impl ToOffset, diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 09d4236afe..deedf3e88b 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -275,19 +275,21 @@ pub fn deserialize_selections(selections: Vec) -> Arc<[Selecti Arc::from( selections .into_iter() - .filter_map(|selection| { - Some(Selection { - id: selection.id as usize, - start: deserialize_anchor(selection.start?)?, - end: deserialize_anchor(selection.end?)?, - reversed: selection.reversed, - goal: SelectionGoal::None, - }) - }) + .filter_map(deserialize_selection) .collect::>(), ) } +pub fn deserialize_selection(selection: proto::Selection) -> Option> { + Some(Selection { + id: selection.id as usize, + start: deserialize_anchor(selection.start?)?, + end: deserialize_anchor(selection.end?)?, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + pub fn deserialize_diagnostics( diagnostics: Vec, ) -> Arc<[DiagnosticEntry]> { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 1ea340278a..f0743c5b4f 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -568,7 +568,7 @@ message UpdateFollowers { } message Editor { - Selection newest_selection = 1; + Anchor scroll_top = 1; } } } @@ -588,7 +588,7 @@ message View { message Editor { uint64 buffer_id = 1; - Selection newest_selection = 2; + Anchor scroll_top = 2; } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ca1e733fe3..386eba66eb 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -6,7 +6,7 @@ pub mod settings; pub mod sidebar; mod status_bar; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; @@ -43,7 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; -use util::ResultExt; +use util::{ResultExt, TryFutureExt}; type ProjectItemBuilders = HashMap< TypeId, @@ -55,7 +55,7 @@ type FollowableItemBuilder = fn( ModelHandle, &mut Option, &mut MutableAppContext, -) -> Option>>>; +) -> Option>>>; type FollowableItemBuilders = HashMap< TypeId, ( @@ -135,9 +135,15 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { cx.update_default_global(|builders: &mut FollowableItemBuilders, _| { builders.insert( TypeId::of::(), - (I::for_state_message, |this| { - Box::new(this.downcast::().unwrap()) - }), + ( + |pane, project, state, cx| { + I::for_state_message(pane, project, state, cx).map(|task| { + cx.foreground() + .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) + }) + }, + |this| Box::new(this.downcast::().unwrap()), + ), ); }); } @@ -240,32 +246,35 @@ pub trait FollowableItem: Item { project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, - ) -> Option>>> - where - Self: Sized; + ) -> Option>>>; fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( &self, event: &Self::Event, cx: &AppContext, ) -> Option; + fn apply_update_message( + &mut self, + message: proto::update_followers::update_view::Variant, + cx: &mut ViewContext, + ) -> Result<()>; } -pub trait FollowableItemHandle { - fn id(&self) -> usize; +pub trait FollowableItemHandle: ItemHandle { fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; fn to_update_message( &self, event: &dyn Any, cx: &AppContext, ) -> Option; + fn apply_update_message( + &self, + message: proto::update_followers::update_view::Variant, + cx: &mut MutableAppContext, + ) -> Result<()>; } impl FollowableItemHandle for ViewHandle { - fn id(&self) -> usize { - self.id() - } - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { self.read(cx).to_state_message(cx) } @@ -277,6 +286,14 @@ impl FollowableItemHandle for ViewHandle { ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } + + fn apply_update_message( + &self, + message: proto::update_followers::update_view::Variant, + cx: &mut MutableAppContext, + ) -> Result<()> { + self.update(cx, |this, cx| this.apply_update_message(message, cx)) + } } pub trait ItemHandle: 'static { @@ -584,14 +601,15 @@ struct LeaderState { followers: HashSet, } +#[derive(Default)] struct FollowerState { active_view_id: Option, items_by_leader_view_id: HashMap, } enum FollowerItem { - Loading(Vec), - Loaded(Box), + Loading(Vec), + Loaded(Box), } impl Workspace { @@ -1212,21 +1230,40 @@ impl Workspace { }); } - let items = futures::future::try_join_all(item_tasks).await?; - let follower_state = FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: response - .views - .iter() - .map(|v| v.id as usize) - .zip(items) - .collect(), - }; this.update(&mut cx, |this, cx| { this.follower_states_by_leader .entry(leader_id) .or_default() - .insert(pane.downgrade(), follower_state); + .insert( + pane.downgrade(), + FollowerState { + active_view_id: response.active_view_id.map(|id| id as usize), + items_by_leader_view_id: Default::default(), + }, + ); + }); + + let items = futures::future::try_join_all(item_tasks).await?; + this.update(&mut cx, |this, cx| { + let follower_state = this + .follower_states_by_leader + .entry(leader_id) + .or_default() + .entry(pane.downgrade()) + .or_default(); + for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { + let prev_state = follower_state.items_by_leader_view_id.remove(&id); + if let Some(FollowerItem::Loading(updates)) = prev_state { + for update in updates { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + follower_state + .items_by_leader_view_id + .insert(id, FollowerItem::Loaded(item)); + } this.leader_updated(leader_id, cx); }); } @@ -1535,7 +1572,7 @@ impl Workspace { } proto::update_followers::Variant::UpdateView(update_view) => { for state in follower_states.values_mut() { - state.items_by_leader_view_id.get(k) + // state.items_by_leader_view_id.get(k) } } proto::update_followers::Variant::CreateView(_) => todo!(), @@ -1550,8 +1587,10 @@ impl Workspace { let mut items_to_add = Vec::new(); for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { - if let Some(item) = state.items_by_leader_view_id.get(&active_view_id) { - items_to_add.push((pane, item.clone())); + if let Some(FollowerItem::Loaded(item)) = + state.items_by_leader_view_id.get(&active_view_id) + { + items_to_add.push((pane, item.boxed_clone())); } } } From df0632011c802054167b493ac3d04a24291efad9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:03:43 -0700 Subject: [PATCH 031/139] :art: client Forgot to push this yesterday night. --- crates/client/src/client.rs | 198 +++++++++++++++--------------------- 1 file changed, 80 insertions(+), 118 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index c2527ed94a..d4a5a47b4b 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -136,7 +136,7 @@ impl Status { struct ClientState { credentials: Option, status: (watch::Sender, watch::Receiver), - entity_id_extractors: HashMap u64>>, + entity_id_extractors: HashMap u64>, _reconnect_task: Option>, reconnect_interval: Duration, entities_by_type_and_remote_id: HashMap<(TypeId, u64), AnyWeakEntityHandle>, @@ -150,6 +150,7 @@ struct ClientState { + Fn( AnyEntityHandle, Box, + &Arc, AsyncAppContext, ) -> LocalBoxFuture<'static, Result<()>>, >, @@ -328,12 +329,11 @@ impl Client { remote_id: u64, cx: &mut ViewContext, ) -> Subscription { - let handle = AnyViewHandle::from(cx.handle()); - let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); - state + self.state + .write() .entities_by_type_and_remote_id - .insert(id, AnyWeakEntityHandle::View(handle.downgrade())); + .insert(id, AnyWeakEntityHandle::View(cx.weak_handle().into())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -345,12 +345,11 @@ impl Client { remote_id: u64, cx: &mut ModelContext, ) -> Subscription { - let handle = AnyModelHandle::from(cx.handle()); - let mut state = self.state.write(); let id = (TypeId::of::(), remote_id); - state + self.state + .write() .entities_by_type_and_remote_id - .insert(id, AnyWeakEntityHandle::Model(handle.downgrade())); + .insert(id, AnyWeakEntityHandle::Model(cx.weak_handle().into())); Subscription::Entity { client: Arc::downgrade(self), id, @@ -373,7 +372,6 @@ impl Client { { let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state .models_by_message_type @@ -381,7 +379,7 @@ impl Client { let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, cx| { + Arc::new(move |handle, envelope, client, cx| { let handle = if let AnyEntityHandle::Model(handle) = handle { handle } else { @@ -389,11 +387,7 @@ impl Client { }; let model = handle.downcast::().unwrap(); let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + handler(model, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -416,47 +410,13 @@ impl Client { + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - let entity_type_id = TypeId::of::(); - let message_type_id = TypeId::of::(); - - let client = Arc::downgrade(self); - let mut state = self.state.write(); - state - .entity_types_by_message_type - .insert(message_type_id, entity_type_id); - state - .entity_id_extractors - .entry(message_type_id) - .or_insert_with(|| { - Box::new(|envelope| { - let envelope = envelope - .as_any() - .downcast_ref::>() - .unwrap(); - envelope.payload.remote_entity_id() - }) - }); - - let prev_handler = state.message_handlers.insert( - message_type_id, - Arc::new(move |handle, envelope, cx| { - let handle = if let AnyEntityHandle::View(handle) = handle { - handle - } else { - unreachable!(); - }; - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - if let Some(client) = client.upgrade() { - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } - }), - ); - if prev_handler.is_some() { - panic!("registered handler for the same message twice"); - } + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::View(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) } pub fn add_model_message_handler(self: &Arc, handler: H) @@ -468,11 +428,29 @@ impl Client { + Sync + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, + { + self.add_entity_message_handler::(move |handle, message, client, cx| { + if let AnyEntityHandle::Model(handle) = handle { + handler(handle.downcast::().unwrap(), message, client, cx) + } else { + unreachable!(); + } + }) + } + + fn add_entity_message_handler(self: &Arc, handler: H) + where + M: EntityMessage, + E: Entity, + H: 'static + + Send + + Sync + + Fn(AnyEntityHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, + F: 'static + Future>, { let model_type_id = TypeId::of::(); let message_type_id = TypeId::of::(); - let client = Arc::downgrade(self); let mut state = self.state.write(); state .entity_types_by_message_type @@ -481,30 +459,20 @@ impl Client { .entity_id_extractors .entry(message_type_id) .or_insert_with(|| { - Box::new(|envelope| { - let envelope = envelope + |envelope| { + envelope .as_any() .downcast_ref::>() - .unwrap(); - envelope.payload.remote_entity_id() - }) + .unwrap() + .payload + .remote_entity_id() + } }); - let prev_handler = state.message_handlers.insert( message_type_id, - Arc::new(move |handle, envelope, cx| { - if let Some(client) = client.upgrade() { - let handle = if let AnyEntityHandle::Model(handle) = handle { - handle - } else { - unreachable!(); - }; - let model = handle.downcast::().unwrap(); - let envelope = envelope.into_any().downcast::>().unwrap(); - handler(model, *envelope, client.clone(), cx).boxed_local() - } else { - async move { Ok(()) }.boxed_local() - } + Arc::new(move |handle, envelope, client, cx| { + let envelope = envelope.into_any().downcast::>().unwrap(); + handler(handle, *envelope, client.clone(), cx).boxed_local() }), ); if prev_handler.is_some() { @@ -522,26 +490,12 @@ impl Client { + Fn(ModelHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_model_message_handler(move |model, envelope, client, cx| { - let receipt = envelope.receipt(); - let response = handler(model, envelope, client.clone(), cx); - async move { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } + self.add_model_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) }) } @@ -555,29 +509,37 @@ impl Client { + Fn(ViewHandle, TypedEnvelope, Arc, AsyncAppContext) -> F, F: 'static + Future>, { - self.add_view_message_handler(move |view, envelope, client, cx| { - let receipt = envelope.receipt(); - let response = handler(view, envelope, client.clone(), cx); - async move { - match response.await { - Ok(response) => { - client.respond(receipt, response)?; - Ok(()) - } - Err(error) => { - client.respond_with_error( - receipt, - proto::Error { - message: error.to_string(), - }, - )?; - Err(error) - } - } - } + self.add_view_message_handler(move |entity, envelope, client, cx| { + Self::respond_to_request::( + envelope.receipt(), + handler(entity, envelope, client.clone(), cx), + client, + ) }) } + async fn respond_to_request>>( + receipt: Receipt, + response: F, + client: Arc, + ) -> Result<()> { + match response.await { + Ok(response) => { + client.respond(receipt, response)?; + Ok(()) + } + Err(error) => { + client.respond_with_error( + receipt, + proto::Error { + message: error.to_string(), + }, + )?; + Err(error) + } + } + } + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { read_credentials_from_keychain(cx).is_some() } @@ -718,7 +680,7 @@ impl Client { if let Some(handler) = state.message_handlers.get(&payload_type_id).cloned() { drop(state); // Avoid deadlocks if the handler interacts with rpc::Client - let future = handler(model, message, cx.clone()); + let future = handler(model, message, &this, cx.clone()); let client_id = this.id; log::debug!( From d860ed25c18e4691813a7e96223f6cb9e2e455a1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:36:05 -0700 Subject: [PATCH 032/139] Allow FollowableItem::to_state_message to return None This way, we can avoid a panic if we don't handle certain cases, like a non-singleton editor. --- crates/editor/src/items.rs | 14 ++++---------- crates/workspace/src/workspace.rs | 24 +++++++++++++----------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5ef739510b..d4163ce353 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -50,21 +50,15 @@ impl FollowableItem for Editor { })) } - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { - let buffer_id = self - .buffer - .read(cx) - .as_singleton() - .unwrap() - .read(cx) - .remote_id(); - proto::view::Variant::Editor(proto::view::Editor { + fn to_state_message(&self, cx: &AppContext) -> Option { + let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); + Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, scroll_top: self .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), - }) + })) } fn to_update_message( diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 386eba66eb..2e84f6c55c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -247,7 +247,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, event: &Self::Event, @@ -261,7 +261,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant; + fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, event: &dyn Any, @@ -275,7 +275,7 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { - fn to_state_message(&self, cx: &AppContext) -> proto::view::Variant { + fn to_state_message(&self, cx: &AppContext) -> Option { self.read(cx).to_state_message(cx) } @@ -381,13 +381,15 @@ impl ItemHandle for ViewHandle { cx: &mut ViewContext, ) { if let Some(followed_item) = self.to_followable_item_handle(cx) { - workspace.update_followers( - proto::update_followers::Variant::CreateView(proto::View { - id: followed_item.id() as u64, - variant: Some(followed_item.to_state_message(cx)), - }), - cx, - ); + if let Some(message) = followed_item.to_state_message(cx) { + workspace.update_followers( + proto::update_followers::Variant::CreateView(proto::View { + id: followed_item.id() as u64, + variant: Some(message), + }), + cx, + ); + } } let pane = pane.downgrade(); @@ -1523,7 +1525,7 @@ impl Workspace { .filter_map(|item| { let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx); + let variant = item.to_state_message(cx)?; Some(proto::View { id, variant: Some(variant), From e338da0271390333bd1de2877e4abd86158d6d41 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 13:37:07 -0700 Subject: [PATCH 033/139] Allow clicking a titlebar avatar to initiate following --- crates/gpui/src/app.rs | 6 + crates/server/src/rpc.rs | 7 +- crates/workspace/src/workspace.rs | 235 ++++++++++++++++-------------- 3 files changed, 134 insertions(+), 114 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8f505ee78..80fc36cba3 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -184,6 +184,12 @@ macro_rules! action { Box::new(self.clone()) } } + + impl From<$arg> for $name { + fn from(arg: $arg) -> Self { + Self(arg) + } + } }; ($name:ident) => { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 25857c53e4..daa817b670 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4272,8 +4272,8 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - let leader_id = project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(*leader_id, cx) + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace.follow(&leader_id.into(), cx).unwrap() }) .await .unwrap(); @@ -4291,8 +4291,7 @@ mod tests { }); workspace_b .condition(cx_b, |workspace, cx| { - let active_item = workspace.active_item(cx).unwrap(); - active_item.project_path(cx) == Some((worktree_id, "1.txt").into()) + workspace.active_item(cx).unwrap().id() == editor_b1.id() }) .await; } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2e84f6c55c..7b893c5666 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -43,7 +43,7 @@ use std::{ sync::Arc, }; use theme::{Theme, ThemeRegistry}; -use util::{ResultExt, TryFutureExt}; +use util::ResultExt; type ProjectItemBuilders = HashMap< TypeId, @@ -68,6 +68,7 @@ action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); +action!(FollowCollaborator, PeerId); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); @@ -88,6 +89,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); + cx.add_async_action(Workspace::follow); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(cx).detach_and_log_err(cx); @@ -1192,88 +1194,90 @@ impl Workspace { } } - pub fn follow(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Task> { - if let Some(project_id) = self.project.read(cx).remote_id() { - let request = self.client.request(proto::Follow { - project_id, - leader_id: leader_id.0, - }); - cx.spawn_weak(|this, mut cx| async move { - let mut response = request.await?; - if let Some(this) = this.upgrade(&cx) { - let mut item_tasks = Vec::new(); - let (project, pane) = this.read_with(&cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - for view in &mut response.views { - let variant = view - .variant - .take() - .ok_or_else(|| anyhow!("missing variant"))?; - cx.update(|cx| { - let mut variant = Some(variant); - for build_item in &item_builders { - if let Some(task) = - build_item(pane.clone(), project.clone(), &mut variant, cx) - { - item_tasks.push(task); - break; - } else { - assert!(variant.is_some()); - } + pub fn follow( + &mut self, + FollowCollaborator(leader_id): &FollowCollaborator, + cx: &mut ViewContext, + ) -> Option>> { + let leader_id = *leader_id; + let project_id = self.project.read(cx).remote_id()?; + let request = self.client.request(proto::Follow { + project_id, + leader_id: leader_id.0, + }); + Some(cx.spawn_weak(|this, mut cx| async move { + let mut response = request.await?; + if let Some(this) = this.upgrade(&cx) { + let mut item_tasks = Vec::new(); + let (project, pane) = this.read_with(&cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + for view in &mut response.views { + let variant = view + .variant + .take() + .ok_or_else(|| anyhow!("missing variant"))?; + cx.update(|cx| { + let mut variant = Some(variant); + for build_item in &item_builders { + if let Some(task) = + build_item(pane.clone(), project.clone(), &mut variant, cx) + { + item_tasks.push(task); + break; + } else { + assert!(variant.is_some()); } - }); - } - - this.update(&mut cx, |this, cx| { - this.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert( - pane.downgrade(), - FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: Default::default(), - }, - ); - }); - - let items = futures::future::try_join_all(item_tasks).await?; - this.update(&mut cx, |this, cx| { - let follower_state = this - .follower_states_by_leader - .entry(leader_id) - .or_default() - .entry(pane.downgrade()) - .or_default(); - for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { - let prev_state = follower_state.items_by_leader_view_id.remove(&id); - if let Some(FollowerItem::Loading(updates)) = prev_state { - for update in updates { - item.apply_update_message(update, cx) - .context("failed to apply view update") - .log_err(); - } - } - follower_state - .items_by_leader_view_id - .insert(id, FollowerItem::Loaded(item)); } - this.leader_updated(leader_id, cx); }); } - Ok(()) - }) - } else { - Task::ready(Err(anyhow!("project is not remote"))) - } + + this.update(&mut cx, |this, cx| { + this.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert( + pane.downgrade(), + FollowerState { + active_view_id: response.active_view_id.map(|id| id as usize), + items_by_leader_view_id: Default::default(), + }, + ); + }); + + let items = futures::future::try_join_all(item_tasks).await?; + this.update(&mut cx, |this, cx| { + let follower_state = this + .follower_states_by_leader + .entry(leader_id) + .or_default() + .entry(pane.downgrade()) + .or_default(); + for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { + let prev_state = follower_state.items_by_leader_view_id.remove(&id); + if let Some(FollowerItem::Loading(updates)) = prev_state { + for update in updates { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + follower_state + .items_by_leader_view_id + .insert(id, FollowerItem::Loaded(item)); + } + this.leader_updated(leader_id, cx); + }); + } + Ok(()) + })) } fn update_followers( @@ -1383,7 +1387,9 @@ impl Workspace { Some(self.render_avatar( collaborator.user.avatar.clone()?, collaborator.replica_id, + Some(collaborator.peer_id), theme, + cx, )) }) .collect() @@ -1397,7 +1403,7 @@ impl Workspace { cx: &mut RenderContext, ) -> ElementBox { if let Some(avatar) = user.and_then(|user| user.avatar.clone()) { - self.render_avatar(avatar, replica_id, theme) + self.render_avatar(avatar, replica_id, None, theme, cx) } else { MouseEventHandler::new::(0, cx, |state, _| { let style = if state.hovered { @@ -1421,52 +1427,61 @@ impl Workspace { &self, avatar: Arc, replica_id: ReplicaId, + peer_id: Option, theme: &Theme, + cx: &mut RenderContext, ) -> ElementBox { - ConstrainedBox::new( - Stack::new() - .with_child( - ConstrainedBox::new( - Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) - .boxed(), - ) + let content = Stack::new() + .with_child( + Image::new(avatar) + .with_style(theme.workspace.titlebar.avatar) + .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), - ) - .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) - .constrained() - .with_width(theme.workspace.titlebar.avatar_ribbon.width) - .with_height(theme.workspace.titlebar.avatar_ribbon.height) - .aligned() - .bottom() - .boxed(), - ) - .boxed(), - ) - .with_width(theme.workspace.right_sidebar.width) - .boxed() + ) + .with_child( + AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) + .constrained() + .with_width(theme.workspace.titlebar.avatar_ribbon.width) + .with_height(theme.workspace.titlebar.avatar_ribbon.height) + .aligned() + .bottom() + .boxed(), + ) + .constrained() + .with_width(theme.workspace.right_sidebar.width) + .boxed(); + + if let Some(peer_id) = peer_id { + MouseEventHandler::new::( + replica_id.into(), + cx, + move |_, _| content, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| cx.dispatch_action(FollowCollaborator(peer_id))) + .boxed() + } else { + content + } } fn render_share_icon(&self, theme: &Theme, cx: &mut RenderContext) -> Option { if self.project().read(cx).is_local() && self.client.user_id().is_some() { - enum Share {} - let color = if self.project().read(cx).is_shared() { theme.workspace.titlebar.share_icon_active_color } else { theme.workspace.titlebar.share_icon_color }; Some( - MouseEventHandler::new::(0, cx, |_, _| { + MouseEventHandler::new::(0, cx, |_, _| { Align::new( - ConstrainedBox::new( - Svg::new("icons/broadcast-24.svg").with_color(color).boxed(), - ) - .with_width(24.) - .boxed(), + Svg::new("icons/broadcast-24.svg") + .with_color(color) + .constrained() + .with_width(24.) + .boxed(), ) .boxed() }) From 570c98745574de51bacd3792b46263eadd0f18a4 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 18 Mar 2022 15:56:57 -0700 Subject: [PATCH 034/139] Handle view updates when following Basic following now works. Editors' scroll positions are their only replicated view state. --- crates/editor/src/items.rs | 2 +- crates/server/src/rpc.rs | 8 +- crates/workspace/src/pane.rs | 4 +- crates/workspace/src/workspace.rs | 249 +++++++++++++++++------------- 4 files changed, 152 insertions(+), 111 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d4163ce353..f3f00ee016 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -64,7 +64,7 @@ impl FollowableItem for Editor { fn to_update_message( &self, event: &Self::Event, - cx: &AppContext, + _: &AppContext, ) -> Option { match event { Event::ScrollPositionChanged => { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index daa817b670..5585b04ad2 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4252,7 +4252,7 @@ mod tests { }) .await .unwrap(); - let editor_a2 = workspace_a + let _editor_a2 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) }) @@ -4261,6 +4261,9 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + }); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4271,7 +4274,6 @@ mod tests { // Client B starts following client A. workspace_b .update(cx_b, |workspace, cx| { - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); workspace.follow(&leader_id.into(), cx).unwrap() }) @@ -4776,7 +4778,7 @@ mod tests { project: &ModelHandle, cx: &mut TestAppContext, ) -> ViewHandle { - let (window_id, _) = cx.add_window(|cx| EmptyView); + let (window_id, _) = cx.add_window(|_| EmptyView); cx.add_view(window_id, |cx| { let fs = project.read(cx).fs().clone(); Workspace::new( diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a2e49f7f48..367cc967fc 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1,7 +1,5 @@ use super::{ItemHandle, SplitDirection}; use crate::{Item, Settings, WeakItemHandle, Workspace}; -use anyhow::{anyhow, Result}; -use client::PeerId; use collections::{HashMap, VecDeque}; use gpui::{ action, @@ -258,7 +256,7 @@ impl Pane { let task = task.await; if let Some(pane) = pane.upgrade(&cx) { if let Some((project_entry_id, build_item)) = task.log_err() { - pane.update(&mut cx, |pane, cx| { + pane.update(&mut cx, |pane, _| { pane.nav_history.borrow_mut().set_mode(mode); }); let item = workspace.update(&mut cx, |workspace, cx| { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7b893c5666..c1da701931 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -11,7 +11,7 @@ use client::{ proto, Authenticate, ChannelList, Client, PeerId, Subscription, TypedEnvelope, User, UserStore, }; use clock::ReplicaId; -use collections::{HashMap, HashSet}; +use collections::{hash_map, HashMap, HashSet}; use gpui::{ action, color::Color, @@ -37,6 +37,7 @@ pub use status_bar::StatusItemView; use std::{ any::{Any, TypeId}, cell::RefCell, + fmt, future::Future, path::{Path, PathBuf}, rc::Rc, @@ -298,7 +299,7 @@ impl FollowableItemHandle for ViewHandle { } } -pub trait ItemHandle: 'static { +pub trait ItemHandle: 'static + fmt::Debug { fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_id(&self, cx: &AppContext) -> Option; @@ -596,7 +597,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, FollowerState>>, + follower_states_by_leader: HashMap, _observe_current_user: Task<()>, } @@ -607,10 +608,12 @@ struct LeaderState { #[derive(Default)] struct FollowerState { - active_view_id: Option, - items_by_leader_view_id: HashMap, + active_view_id: Option, + items_by_leader_view_id: HashMap, + panes: HashSet>, } +#[derive(Debug)] enum FollowerItem { Loading(Vec), Loaded(Box), @@ -1206,78 +1209,84 @@ impl Workspace { leader_id: leader_id.0, }); Some(cx.spawn_weak(|this, mut cx| async move { - let mut response = request.await?; + let response = request.await?; if let Some(this) = this.upgrade(&cx) { - let mut item_tasks = Vec::new(); - let (project, pane) = this.read_with(&cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - for view in &mut response.views { - let variant = view - .variant - .take() - .ok_or_else(|| anyhow!("missing variant"))?; - cx.update(|cx| { - let mut variant = Some(variant); - for build_item in &item_builders { - if let Some(task) = - build_item(pane.clone(), project.clone(), &mut variant, cx) - { - item_tasks.push(task); - break; - } else { - assert!(variant.is_some()); - } - } - }); + Self::add_views_from_leader(this.clone(), leader_id, response.views, &mut cx) + .await?; + this.update(&mut cx, |this, cx| { + this.follower_state(leader_id)?.active_view_id = response.active_view_id; + this.leader_updated(leader_id, cx); + Ok::<_, anyhow::Error>(()) + })?; + } + Ok(()) + })) + } + + async fn add_views_from_leader( + this: ViewHandle, + leader_id: PeerId, + views: Vec, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let (project, pane) = this.read_with(cx, |this, _| { + (this.project.clone(), this.active_pane().clone()) + }); + + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in views { + let mut variant = view.variant; + if variant.is_none() { + Err(anyhow!("missing variant"))?; + } + for build_item in &item_builders { + let task = + cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(view.id); + break; + } else { + assert!(variant.is_some()); } + } + } - this.update(&mut cx, |this, cx| { - this.follower_states_by_leader - .entry(leader_id) - .or_default() - .insert( - pane.downgrade(), - FollowerState { - active_view_id: response.active_view_id.map(|id| id as usize), - items_by_leader_view_id: Default::default(), - }, - ); - }); - - let items = futures::future::try_join_all(item_tasks).await?; - this.update(&mut cx, |this, cx| { - let follower_state = this - .follower_states_by_leader - .entry(leader_id) - .or_default() - .entry(pane.downgrade()) - .or_default(); - for (id, item) in response.views.iter().map(|v| v.id as usize).zip(items) { - let prev_state = follower_state.items_by_leader_view_id.remove(&id); - if let Some(FollowerItem::Loading(updates)) = prev_state { - for update in updates { + let pane = pane.downgrade(); + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this.follower_states_by_leader.entry(leader_id).or_default(); + state.panes.insert(pane); + for (id, item) in leader_view_ids.into_iter().zip(items) { + match state.items_by_leader_view_id.entry(id) { + hash_map::Entry::Occupied(e) => { + let e = e.into_mut(); + if let FollowerItem::Loading(updates) = e { + for update in updates.drain(..) { item.apply_update_message(update, cx) .context("failed to apply view update") .log_err(); } } - follower_state - .items_by_leader_view_id - .insert(id, FollowerItem::Loaded(item)); + *e = FollowerItem::Loaded(item); } - this.leader_updated(leader_id, cx); - }); + hash_map::Entry::Vacant(e) => { + e.insert(FollowerItem::Loaded(item)); + } + } } - Ok(()) - })) + }); + + Ok(()) } fn update_followers( @@ -1557,7 +1566,7 @@ impl Workspace { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _| { this.leader_state .followers .remove(&envelope.original_sender_id()?); @@ -1571,49 +1580,81 @@ impl Workspace { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| { - let leader_id = envelope.original_sender_id()?; - let follower_states = this - .follower_states_by_leader - .get_mut(&leader_id) - .ok_or_else(|| anyhow!("received follow update for an unfollowed peer"))?; - match envelope - .payload - .variant - .ok_or_else(|| anyhow!("invalid update"))? - { - proto::update_followers::Variant::UpdateActiveView(update_active_view) => { - for state in follower_states.values_mut() { - state.active_view_id = update_active_view.id.map(|id| id as usize); - } - } - proto::update_followers::Variant::UpdateView(update_view) => { - for state in follower_states.values_mut() { - // state.items_by_leader_view_id.get(k) - } - } - proto::update_followers::Variant::CreateView(_) => todo!(), + let leader_id = envelope.original_sender_id()?; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid update"))? + { + proto::update_followers::Variant::UpdateActiveView(update_active_view) => { + this.update(&mut cx, |this, cx| { + this.follower_state(leader_id)?.active_view_id = update_active_view.id; + this.leader_updated(leader_id, cx); + Ok::<_, anyhow::Error>(()) + }) } + proto::update_followers::Variant::UpdateView(update_view) => { + this.update(&mut cx, |this, cx| { + let variant = update_view + .variant + .ok_or_else(|| anyhow!("missing update view variant"))?; + match this + .follower_state(leader_id)? + .items_by_leader_view_id + .entry(update_view.id) + .or_insert(FollowerItem::Loading(Vec::new())) + { + FollowerItem::Loaded(item) => { + item.apply_update_message(variant, cx).log_err(); + } + FollowerItem::Loading(updates) => updates.push(variant), + } + this.leader_updated(leader_id, cx); + Ok(()) + }) + } + proto::update_followers::Variant::CreateView(view) => { + Self::add_views_from_leader(this.clone(), leader_id, vec![view], &mut cx).await?; + this.update(&mut cx, |this, cx| { + this.leader_updated(leader_id, cx); + }); + Ok(()) + } + } + .log_err(); - this.leader_updated(leader_id, cx); - Ok(()) - }) + Ok(()) + } + + fn follower_state(&mut self, leader_id: PeerId) -> Result<&mut FollowerState> { + self.follower_states_by_leader + .get_mut(&leader_id) + .ok_or_else(|| anyhow!("received follow update for an unfollowed peer")) } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - let mut items_to_add = Vec::new(); - for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { - if let Some((pane, active_view_id)) = pane.upgrade(cx).zip(state.active_view_id) { - if let Some(FollowerItem::Loaded(item)) = - state.items_by_leader_view_id.get(&active_view_id) - { - items_to_add.push((pane, item.boxed_clone())); + let state = self.follower_states_by_leader.get_mut(&leader_id)?; + let active_item = state.items_by_leader_view_id.get(&state.active_view_id?)?; + if let FollowerItem::Loaded(item) = active_item { + let mut panes = Vec::new(); + state.panes.retain(|pane| { + if let Some(pane) = pane.upgrade(cx) { + panes.push(pane); + true + } else { + false } - } - } + }); - for (pane, item) in items_to_add { - Pane::add_item(self, pane, item.clone(), cx); + if panes.is_empty() { + self.follower_states_by_leader.remove(&leader_id); + } else { + let item = item.boxed_clone(); + for pane in panes { + Pane::add_item(self, pane, item.clone(), cx); + } + cx.notify(); + } } None From 0e920ad5e97aa1c4638a9027a3ea2f8e7f67051d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Sat, 19 Mar 2022 10:50:23 +0100 Subject: [PATCH 035/139] Unset follower's scroll anchor when editor is scrolled all the way up --- crates/editor/src/editor.rs | 4 ++-- crates/editor/src/items.rs | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index aa41624029..b3d74d03a7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1037,9 +1037,9 @@ impl Editor { cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Anchor, cx: &mut ViewContext) { + fn set_scroll_top_anchor(&mut self, anchor: Option, cx: &mut ViewContext) { self.scroll_position = Vector2F::zero(); - self.scroll_top_anchor = Some(anchor); + self.scroll_top_anchor = anchor; cx.emit(Event::ScrollPositionChanged); cx.notify(); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f3f00ee016..1ce59dd93f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -95,7 +95,9 @@ impl FollowableItem for Editor { let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) }; - self.set_scroll_top_anchor(anchor, cx); + self.set_scroll_top_anchor(Some(anchor), cx); + } else { + self.set_scroll_top_anchor(None, cx); } } } From 7cb8935ff5061379cbbcee1a616ade75f59a1f70 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 20 Mar 2022 08:54:20 -0600 Subject: [PATCH 036/139] Pass project entry id to Pane when opening a project items This fixes an oversight where we were failing to associate project items with their project entry ids, which broke the logic that prevented the same project entry from being opened twice in the same pane. --- crates/workspace/src/workspace.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 33155b5d4f..9b9477353b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -862,10 +862,9 @@ impl Workspace { { use project::Item as _; - if let Some(item) = project_item - .read(cx) - .entry_id(cx) - .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(entry_id)) + let entry_id = project_item.read(cx).entry_id(cx); + if let Some(item) = entry_id + .and_then(|entry_id| self.active_pane().read(cx).item_for_entry(dbg!(entry_id))) .and_then(|item| item.downcast()) { self.activate_item(&item, cx); @@ -873,7 +872,9 @@ impl Workspace { } let item = cx.add_view(|cx| T::for_project_item(self.project().clone(), project_item, cx)); - self.add_item(Box::new(item.clone()), cx); + self.active_pane().update(cx, |pane, cx| { + pane.add_item(entry_id, Box::new(item.clone()), cx) + }); item } From a2dbebd9ba8742266469366857bdbfa0af9ba394 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 15:16:56 +0100 Subject: [PATCH 037/139] Hide cursor both locally and remotely when following --- crates/editor/src/editor.rs | 8 ++++++-- crates/editor/src/items.rs | 18 ++++++++++++++++++ crates/workspace/src/workspace.rs | 7 +++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b3d74d03a7..0db67e73e7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -464,6 +464,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + following: bool, } pub struct EditorSnapshot { @@ -937,6 +938,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + following: false, }; this.end_selection(cx); this @@ -5036,7 +5038,7 @@ impl Editor { self.selections = selections; self.pending_selection = pending_selection; - if self.focused { + if self.focused && !self.following { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); @@ -5671,7 +5673,9 @@ impl View for Editor { self.blink_cursors(self.blink_epoch, cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - buffer.set_active_selections(&self.selections, cx) + if !self.following { + buffer.set_active_selections(&self.selections, cx); + } }); } } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 1ce59dd93f..7f28af0c40 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -50,6 +50,24 @@ impl FollowableItem for Editor { })) } + fn set_following(&mut self, following: bool, cx: &mut ViewContext) { + self.following = following; + if self.following { + self.show_local_selections = false; + self.buffer.update(cx, |buffer, cx| { + buffer.remove_active_selections(cx); + }); + } else { + self.show_local_selections = true; + if self.focused { + self.buffer.update(cx, |buffer, cx| { + buffer.set_active_selections(&self.selections, cx); + }); + } + } + cx.notify(); + } + fn to_state_message(&self, cx: &AppContext) -> Option { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c1da701931..748b0ced61 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -250,6 +250,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; + fn set_following(&mut self, following: bool, cx: &mut ViewContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -264,6 +265,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { + fn set_following(&self, following: bool, cx: &mut MutableAppContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -278,6 +280,10 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { + fn set_following(&self, following: bool, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| this.set_following(following, cx)) + } + fn to_state_message(&self, cx: &AppContext) -> Option { self.read(cx).to_state_message(cx) } @@ -1267,6 +1273,7 @@ impl Workspace { let state = this.follower_states_by_leader.entry(leader_id).or_default(); state.panes.insert(pane); for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_following(true, cx); match state.items_by_leader_view_id.entry(id) { hash_map::Entry::Occupied(e) => { let e = e.into_mut(); From a154e4500bd7f276e9371c0bae474b135bb10977 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 16:46:10 +0100 Subject: [PATCH 038/139] Implement `Workspace::unfollow` This also changes the structure of the follow state back to be per-pane. This is because we can't share the same view state across different panes for a couple of reasons: - Rendering the same view in N different panes is almost always not something that we want due to global state such as focus. - If we allowed it and a user followed the same person in two different panes, there would be no way of unfollowing in one pane without also unfollowing in the other. --- crates/gpui/src/app.rs | 7 + crates/server/src/rpc.rs | 34 +++- crates/workspace/src/workspace.rs | 322 ++++++++++++++++++------------ 3 files changed, 237 insertions(+), 126 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 80fc36cba3..41d3bf2cdd 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3542,6 +3542,13 @@ impl PartialEq> for WeakViewHandle { impl Eq for ViewHandle {} +impl Hash for ViewHandle { + fn hash(&self, state: &mut H) { + self.window_id.hash(state); + self.view_id.hash(state); + } +} + impl Debug for ViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("ViewHandle<{}>", type_name::())) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 5585b04ad2..044f423682 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4251,12 +4251,16 @@ mod tests { workspace.open_path((worktree_id, "1.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); - let _editor_a2 = workspace_a + let editor_a2 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); // Client B opens an editor. @@ -4269,6 +4273,8 @@ mod tests { workspace.open_path((worktree_id, "1.txt"), cx) }) .await + .unwrap() + .downcast::() .unwrap(); // Client B starts following client A. @@ -4286,16 +4292,40 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); + let editor_b2 = workspace_b + .read_with(cx_b, |workspace, cx| workspace.active_item(cx)) + .unwrap() + .downcast::() + .unwrap(); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(editor_a1.as_ref(), cx) + workspace.activate_item(&editor_a1, cx) }); workspace_b .condition(cx_b, |workspace, cx| { workspace.active_item(cx).unwrap().id() == editor_b1.id() }) .await; + + // After unfollowing, client B stops receiving updates from client A. + workspace_b.update(cx_b, |workspace, cx| { + workspace.unfollow(&workspace.active_pane().clone(), cx) + }); + workspace_a.update(cx_a, |workspace, cx| { + workspace.activate_item(&editor_a2, cx); + editor_a2.update(cx, |editor, cx| editor.set_text("ONE", cx)); + }); + editor_b2 + .condition(cx_b, |editor, cx| editor.text(cx) == "ONE") + .await; + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_b1.id() + ); } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 748b0ced61..5b3a21f7e8 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -70,6 +70,7 @@ action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); action!(FollowCollaborator, PeerId); +action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); @@ -91,6 +92,12 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::follow); + cx.add_action( + |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + let pane = workspace.active_pane().clone(); + workspace.unfollow(&pane, cx); + }, + ); cx.add_action( |workspace: &mut Workspace, _: &Save, cx: &mut ViewContext| { workspace.save_active_item(cx).detach_and_log_err(cx); @@ -100,6 +107,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ + Binding::new("cmd-alt-shift-U", Unfollow, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), Binding::new( @@ -603,7 +611,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, + follower_states_by_leader: HashMap, FollowerState>>, _observe_current_user: Task<()>, } @@ -616,7 +624,6 @@ struct LeaderState { struct FollowerState { active_view_id: Option, items_by_leader_view_id: HashMap, - panes: HashSet>, } #[derive(Debug)] @@ -1166,6 +1173,7 @@ impl Workspace { if self.center.remove(&pane).unwrap() { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); + self.unfollow(&pane, cx); cx.notify(); } } @@ -1209,6 +1217,14 @@ impl Workspace { cx: &mut ViewContext, ) -> Option>> { let leader_id = *leader_id; + let pane = self.active_pane().clone(); + + self.unfollow(&pane, cx); + self.follower_states_by_leader + .entry(leader_id) + .or_default() + .insert(pane.clone(), Default::default()); + let project_id = self.project.read(cx).remote_id()?; let request = self.client.request(proto::Follow { project_id, @@ -1217,99 +1233,50 @@ impl Workspace { Some(cx.spawn_weak(|this, mut cx| async move { let response = request.await?; if let Some(this) = this.upgrade(&cx) { - Self::add_views_from_leader(this.clone(), leader_id, response.views, &mut cx) - .await?; - this.update(&mut cx, |this, cx| { - this.follower_state(leader_id)?.active_view_id = response.active_view_id; - this.leader_updated(leader_id, cx); + this.update(&mut cx, |this, _| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id) + .and_then(|states_by_pane| states_by_pane.get_mut(&pane)) + .ok_or_else(|| anyhow!("following interrupted"))?; + state.active_view_id = response.active_view_id; Ok::<_, anyhow::Error>(()) })?; + Self::add_views_from_leader( + this, + leader_id, + vec![pane.clone()], + response.views, + &mut cx, + ) + .await?; } Ok(()) })) } - async fn add_views_from_leader( - this: ViewHandle, - leader_id: PeerId, - views: Vec, - cx: &mut AsyncAppContext, - ) -> Result<()> { - let (project, pane) = this.read_with(cx, |this, _| { - (this.project.clone(), this.active_pane().clone()) - }); - - let item_builders = cx.update(|cx| { - cx.default_global::() - .values() - .map(|b| b.0) - .collect::>() - .clone() - }); - - let mut item_tasks = Vec::new(); - let mut leader_view_ids = Vec::new(); - for view in views { - let mut variant = view.variant; - if variant.is_none() { - Err(anyhow!("missing variant"))?; - } - for build_item in &item_builders { - let task = - cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); - if let Some(task) = task { - item_tasks.push(task); - leader_view_ids.push(view.id); - break; - } else { - assert!(variant.is_some()); - } - } - } - - let pane = pane.downgrade(); - let items = futures::future::try_join_all(item_tasks).await?; - this.update(cx, |this, cx| { - let state = this.follower_states_by_leader.entry(leader_id).or_default(); - state.panes.insert(pane); - for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_following(true, cx); - match state.items_by_leader_view_id.entry(id) { - hash_map::Entry::Occupied(e) => { - let e = e.into_mut(); - if let FollowerItem::Loading(updates) = e { - for update in updates.drain(..) { - item.apply_update_message(update, cx) - .context("failed to apply view update") - .log_err(); - } - } - *e = FollowerItem::Loaded(item); - } - hash_map::Entry::Vacant(e) => { - e.insert(FollowerItem::Loaded(item)); + pub fn unfollow(&mut self, pane: &ViewHandle, cx: &mut ViewContext) -> Option<()> { + for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + if let Some(state) = states_by_pane.remove(&pane) { + for (_, item) in state.items_by_leader_view_id { + if let FollowerItem::Loaded(item) = item { + item.set_following(false, cx); } } + + if states_by_pane.is_empty() { + if let Some(project_id) = self.project.read(cx).remote_id() { + self.client + .send(proto::Unfollow { + project_id, + leader_id: leader_id.0, + }) + .log_err(); + } + } + + cx.notify(); } - }); - - Ok(()) - } - - fn update_followers( - &self, - update: proto::update_followers::Variant, - cx: &AppContext, - ) -> Option<()> { - let project_id = self.project.read(cx).remote_id()?; - if !self.leader_state.followers.is_empty() { - self.client - .send(proto::UpdateFollowers { - project_id, - follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), - variant: Some(update), - }) - .log_err(); } None } @@ -1595,8 +1562,9 @@ impl Workspace { { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(&mut cx, |this, cx| { - this.follower_state(leader_id)?.active_view_id = update_active_view.id; - this.leader_updated(leader_id, cx); + this.update_leader_state(leader_id, cx, |state, _| { + state.active_view_id = update_active_view.id; + }); Ok::<_, anyhow::Error>(()) }) } @@ -1605,26 +1573,33 @@ impl Workspace { let variant = update_view .variant .ok_or_else(|| anyhow!("missing update view variant"))?; - match this - .follower_state(leader_id)? - .items_by_leader_view_id - .entry(update_view.id) - .or_insert(FollowerItem::Loading(Vec::new())) - { - FollowerItem::Loaded(item) => { - item.apply_update_message(variant, cx).log_err(); + this.update_leader_state(leader_id, cx, |state, cx| { + let variant = variant.clone(); + match state + .items_by_leader_view_id + .entry(update_view.id) + .or_insert(FollowerItem::Loading(Vec::new())) + { + FollowerItem::Loaded(item) => { + item.apply_update_message(variant, cx).log_err(); + } + FollowerItem::Loading(updates) => updates.push(variant), } - FollowerItem::Loading(updates) => updates.push(variant), - } - this.leader_updated(leader_id, cx); + }); Ok(()) }) } proto::update_followers::Variant::CreateView(view) => { - Self::add_views_from_leader(this.clone(), leader_id, vec![view], &mut cx).await?; - this.update(&mut cx, |this, cx| { - this.leader_updated(leader_id, cx); + let panes = this.read_with(&cx, |this, _| { + this.follower_states_by_leader + .get(&leader_id) + .into_iter() + .flat_map(|states_by_pane| states_by_pane.keys()) + .cloned() + .collect() }); + Self::add_views_from_leader(this.clone(), leader_id, panes, vec![view], &mut cx) + .await?; Ok(()) } } @@ -1633,37 +1608,136 @@ impl Workspace { Ok(()) } - fn follower_state(&mut self, leader_id: PeerId) -> Result<&mut FollowerState> { - self.follower_states_by_leader + async fn add_views_from_leader( + this: ViewHandle, + leader_id: PeerId, + panes: Vec>, + views: Vec, + cx: &mut AsyncAppContext, + ) -> Result<()> { + let project = this.read_with(cx, |this, _| this.project.clone()); + + let item_builders = cx.update(|cx| { + cx.default_global::() + .values() + .map(|b| b.0) + .collect::>() + .clone() + }); + + let mut item_tasks_by_pane = HashMap::default(); + for pane in panes { + let mut item_tasks = Vec::new(); + let mut leader_view_ids = Vec::new(); + for view in &views { + let mut variant = view.variant.clone(); + if variant.is_none() { + Err(anyhow!("missing variant"))?; + } + for build_item in &item_builders { + let task = + cx.update(|cx| build_item(pane.clone(), project.clone(), &mut variant, cx)); + if let Some(task) = task { + item_tasks.push(task); + leader_view_ids.push(view.id); + break; + } else { + assert!(variant.is_some()); + } + } + } + + item_tasks_by_pane.insert(pane, (item_tasks, leader_view_ids)); + } + + for (pane, (item_tasks, leader_view_ids)) in item_tasks_by_pane { + let items = futures::future::try_join_all(item_tasks).await?; + this.update(cx, |this, cx| { + let state = this + .follower_states_by_leader + .get_mut(&leader_id)? + .get_mut(&pane)?; + + for (id, item) in leader_view_ids.into_iter().zip(items) { + item.set_following(true, cx); + match state.items_by_leader_view_id.entry(id) { + hash_map::Entry::Occupied(e) => { + let e = e.into_mut(); + if let FollowerItem::Loading(updates) = e { + for update in updates.drain(..) { + item.apply_update_message(update, cx) + .context("failed to apply view update") + .log_err(); + } + } + *e = FollowerItem::Loaded(item); + } + hash_map::Entry::Vacant(e) => { + e.insert(FollowerItem::Loaded(item)); + } + } + } + + Some(()) + }); + } + this.update(cx, |this, cx| this.leader_updated(leader_id, cx)); + + Ok(()) + } + + fn update_followers( + &self, + update: proto::update_followers::Variant, + cx: &AppContext, + ) -> Option<()> { + let project_id = self.project.read(cx).remote_id()?; + if !self.leader_state.followers.is_empty() { + self.client + .send(proto::UpdateFollowers { + project_id, + follower_ids: self.leader_state.followers.iter().map(|f| f.0).collect(), + variant: Some(update), + }) + .log_err(); + } + None + } + + fn update_leader_state( + &mut self, + leader_id: PeerId, + cx: &mut ViewContext, + mut update_fn: impl FnMut(&mut FollowerState, &mut ViewContext), + ) { + for (_, state) in self + .follower_states_by_leader .get_mut(&leader_id) - .ok_or_else(|| anyhow!("received follow update for an unfollowed peer")) + .into_iter() + .flatten() + { + update_fn(state, cx); + } + self.leader_updated(leader_id, cx); } fn leader_updated(&mut self, leader_id: PeerId, cx: &mut ViewContext) -> Option<()> { - let state = self.follower_states_by_leader.get_mut(&leader_id)?; - let active_item = state.items_by_leader_view_id.get(&state.active_view_id?)?; - if let FollowerItem::Loaded(item) = active_item { - let mut panes = Vec::new(); - state.panes.retain(|pane| { - if let Some(pane) = pane.upgrade(cx) { - panes.push(pane); - true - } else { - false + let mut items_to_add = Vec::new(); + for (pane, state) in self.follower_states_by_leader.get(&leader_id)? { + if let Some(active_item) = state + .active_view_id + .and_then(|id| state.items_by_leader_view_id.get(&id)) + { + if let FollowerItem::Loaded(item) = active_item { + items_to_add.push((pane.clone(), item.boxed_clone())); } - }); - - if panes.is_empty() { - self.follower_states_by_leader.remove(&leader_id); - } else { - let item = item.boxed_clone(); - for pane in panes { - Pane::add_item(self, pane, item.clone(), cx); - } - cx.notify(); } } + for (pane, item) in items_to_add { + Pane::add_item(self, pane.clone(), item.boxed_clone(), cx); + cx.notify(); + } None } } From 9575796f9ec164aeed22e7a4beb8ab88837746b2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 21 Mar 2022 17:10:23 +0100 Subject: [PATCH 039/139] Allow unfollowing of leaders by clicking on their avatar --- crates/server/src/rpc.rs | 2 +- crates/workspace/src/workspace.rs | 35 ++++++++++++++++++------------- 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 044f423682..fe56cea2e2 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4281,7 +4281,7 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); - workspace.follow(&leader_id.into(), cx).unwrap() + workspace.toggle_follow(&leader_id.into(), cx).unwrap() }) .await .unwrap(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5b3a21f7e8..760ffa8c1a 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -69,7 +69,7 @@ action!(Open, Arc); action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); -action!(FollowCollaborator, PeerId); +action!(ToggleFollow, PeerId); action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); @@ -91,7 +91,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { }); cx.add_action(Workspace::toggle_share); - cx.add_async_action(Workspace::follow); + cx.add_async_action(Workspace::toggle_follow); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -1211,15 +1211,21 @@ impl Workspace { } } - pub fn follow( + pub fn toggle_follow( &mut self, - FollowCollaborator(leader_id): &FollowCollaborator, + ToggleFollow(leader_id): &ToggleFollow, cx: &mut ViewContext, ) -> Option>> { let leader_id = *leader_id; let pane = self.active_pane().clone(); - self.unfollow(&pane, cx); + if let Some(prev_leader_id) = self.unfollow(&pane, cx) { + if leader_id == prev_leader_id { + cx.notify(); + return None; + } + } + self.follower_states_by_leader .entry(leader_id) .or_default() @@ -1255,7 +1261,11 @@ impl Workspace { })) } - pub fn unfollow(&mut self, pane: &ViewHandle, cx: &mut ViewContext) -> Option<()> { + pub fn unfollow( + &mut self, + pane: &ViewHandle, + cx: &mut ViewContext, + ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { @@ -1276,6 +1286,7 @@ impl Workspace { } cx.notify(); + return Some(*leader_id); } } None @@ -1437,14 +1448,10 @@ impl Workspace { .boxed(); if let Some(peer_id) = peer_id { - MouseEventHandler::new::( - replica_id.into(), - cx, - move |_, _| content, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(move |cx| cx.dispatch_action(FollowCollaborator(peer_id))) - .boxed() + MouseEventHandler::new::(replica_id.into(), cx, move |_, _| content) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(move |cx| cx.dispatch_action(ToggleFollow(peer_id))) + .boxed() } else { content } From 1c23a45d02ea16f1a780f2bd68f319cc6d0188ad Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 21 Mar 2022 12:11:31 -0400 Subject: [PATCH 040/139] Update Zed fonts to 1.2.1 --- .../fonts/zed-mono/zed-mono-extended.ttf | Bin 3291724 -> 3291724 bytes .../fonts/zed-mono/zed-mono-extendedbold.ttf | Bin 3275296 -> 3275296 bytes .../zed-mono/zed-mono-extendedbolditalic.ttf | Bin 3459752 -> 3459752 bytes .../zed-mono/zed-mono-extendeditalic.ttf | Bin 3482500 -> 3482500 bytes .../fonts/zed-sans/zed-sans-extended.ttf | Bin 3881072 -> 3881072 bytes .../fonts/zed-sans/zed-sans-extendedbold.ttf | Bin 3861168 -> 3861168 bytes .../zed-sans/zed-sans-extendedbolditalic.ttf | Bin 4133220 -> 4133220 bytes .../zed-sans/zed-sans-extendeditalic.ttf | Bin 4143104 -> 4143104 bytes 8 files changed, 0 insertions(+), 0 deletions(-) diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extended.ttf index 1cab6121aad689ef98ac7f79b408108d15b87e68..05b8c7085e51d4321bf1e73f763980629c8ddfd0 100644 GIT binary patch delta 507 zcmYMxODIH99LMo<&&A9&USqDuxOY77Fve@sJ+(-_qludx)3rD`lqV-}5B z51HV&wJF9YZKSUvl{=y*w}M-W83FE#?-OZJm=MDt0d&ml=?F~YyGP z^mWzeT(v87m<+%-#2}y_pZZQ+LIV3GScq;P_-mr~dw#31fHvom_=8W6Klsm@d4(wY hnp2%Gm3v>GXGgQ!KS&Axf-fU?wV{o_HR@|sSBnhIT-Z zoS`9-E^-r-Ft~$>#6UyJVzCIT`g?ZxL|Z zo0(MyC6lz?JR<50qLH;fzDrtGqodjb|<-&P#C4hV9DGk@ z@{w`UY;y`qJna$^!lTUk27V^~^HBKJV}m@%hXN=BI}|}Nlz4H1ypcC z6;wkF)IuG&pdQ@N0FBTD&Bm+8vu05;&+huZ-Pr#(bcdsw2fWY%t5zf6*ldXw2AjXrUT4GHrjE XD(ru!YVjE>qkHF*{8wYXUQPW3ey6tq diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbold.ttf index 5c027864e01d3346cc84847db83e022d1fcf320c..d5dde1bb14d5809c4c3312f53903adc7bece6b52 100644 GIT binary patch delta 526 zcmYMx&npCB9LMozo>|sdzn1ZH#v0c8{bQ&6Sdm|){5YT_2dxyfDWQ<7*0fjlvKO)H zQCl2bm_NW_aOkP$2XbAIE_(_@fx#fEJb6f8nbIG?UV`j zxI;1DsgFL2RPK(P#RZQPGyOD^9~NoJDj}vxMod3>^=JRx7urkKLLJmY12jUD zzL#v?+;u8+iVVUN#9&1C2EFH{f`RWKY(;XG)%wWjf|@35p{;qyci`212ddG|D@4)h tjha+^hbK!bGud6Qq>O*SbCKJI#j(HrbL`8JGG#j^eV9RmLNM60oy12M67DKyZy)IgC8MG+bZ1tEh&2N!9M ztRYLlhBU$>gXj`yZt3O_+)Q;5N|g*jy9re)LeqazYaaY~@Bi@)?|m=-t1RRp6`zTR zf0s!*JSNX{J{Dblr$#Jeq}`km(QFI_Q|ZlT}usbV@a_N3reBx5{g1qX7expb=>Ed$+T*P4tNNi&;>TILpMAE8G4`> z`rtA2!vH9tf&&I&2%InsBj5rzcwiL9;E9n|$1e|*)|36sn+5Mj5J=`~<1hhUn1rV= zW#nqpn^}99Md%g$gpCHn_tCfSkP0|ABr>+R=FPl*uUVwTU9FG|Z2OGBwpR9wVG0`T tM5{Ri7tgkiR=S>F(U5qK@1h@$YUuxdyZFc27BTG-21ks9MjflU{{;ayu diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendedbolditalic.ttf index 47a3a9bc27d1f4be40eb1795f523a3f184027e8c..bcd8c7e618e0b684e236a728c85860308b5ad386 100644 GIT binary patch delta 505 zcmX}pIY425*+h-EalMi?n!{)`#v_YY>^-nCSj56ez#|CaDjKW;f@?s6*hoM~ zH-(C=Y#~;5mf8q{g(RR>X;e^f{9MrD;pZI&hMze(x)V;|P*oZdg%GjiW=Q+%d3-Up zAn!YdiEz2}v+U){ zd4Ye>b^0Pvc_A~9x3t-^JWA)pNr~Ey3E`Z7@u+ZVbZRSa(YNxYXEA$@+|oYrP@=>4 zzn6$gZiOqOAM}y2GM`}2a<}j*JV`XPX58?h6Gm^OlH+Ai4i!)dRZtBzV1-(cpbqMx z0UDtRY+wf&n!y1r&&FItiNPz`}yC?DesCO zx*-5P&MdYs&X)7 l!gHUx<5USd<=M@9>wMMd8)*@r@nPiAzdHRlGwB;e{sGwsx&r_J delta 505 zcmYk(O(;ZB6bJBg@0-Ch#>aRV<_(51jBn;8UmGh%RY`?h_ehfpmyd`{lY(Z9{a87zYm?_F+9oEE!nogf8fY z9_WQWBe2px_2MEok5D&{FbOpZL1S+^bmO&OqEBb05}^_MD%x-|l?WNBgmy4!BR4sX h`ZI0XrygH_kIt8keo}+Q&A{RKQCrxO4G diff --git a/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf b/crates/zed/assets/fonts/zed-mono/zed-mono-extendeditalic.ttf index 85ff2c3c4ae8e54881c262de18a0f457685481f5..023c5a87cea6c1ba98238367e162d1de1003397c 100644 GIT binary patch delta 503 zcmX}p%PT}t90%}o&Na9m<8{4e?ih^6U>JN@|e`1p~z0TW~0n*N=oXi zuo7-8C>9!avQWx`g|RXvY}D~}i^Zqk?|b?!&adC<)g4`h=v5kn3=S{kXzOuMHSX{ zZw@VzTY75d2cvf=vyR%gg^NDZCxvy-niF2+c{7J2Yg#5`K{i++2XY|~@}U3}D1;&? zh7u?R8`wbw2b6&m%Ao=(!39-N4K+{;Zm5HLXfW0^k34Os*uLM~e)9L1;`={OMRqSd z&v9KjTuog#Z8m delta 503 zcmYMxKS*0q6bA5f?=#lt->NUROwxi+U8V5<;#7SsNb#bVerGj*Letl3Jew_1N&T!AY8^>pC19D5le?Rjb zQg3!CIFe1yz5k$3*G^H)|AvTOCUH=8V%{PnQXsjQUKz{2`n4{FWI}Qw6(OsHbO~84 zg_t*G``IzO(D>-GGck$$$;s)=rdbE|&;X6l1dpK^ zTA&p)a6=nBfv4~cJm3W#e9#UCJckZ=0e%QTCv-tK1R(@H&}(g)VRga#u>CvdmL4K@ z?Yu@jvtJ5BAM`^624K+IFAY_$TFBrgMY+k`s0oNzU%y0;e6Aw;d?8(qj=EB4Co>1- rsC7^t-X2j5@>p+qthz(r+7qjZrm;U{C|Cdf>xb`h=6@|D{W1L>^whA9 diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extended.ttf index d5cc91e27aaee686ea84ed9fcb99634c16c156b5..07a968568039987f2ba5b64de12a5ce1016a0054 100644 GIT binary patch delta 251 zcmXwt$xQ+Q004Jc@K(I=LO?DLK=EF@5)&Gr18T4Yl9|6vXa^KvBffq3*pDw0<6`0@ zlSw8?KEovSc@zzsuOlRsj6{-^oJ`5I%*d?F$-FGcqAW>XmSsg&Wlh$lARDqNTe2-X zQj}dONm(kgCsnCQT^iDqeL0XrIg*yNr6b33B3(I^GdY(Fxs)rpmRNdnBe!Gj#&P`6 keH-TQzk*8i5v06#o}&ML4}MCye)c7JOiv~VCqLz{zooNSn*aa+ delta 251 zcmXwtIZgrr004Jc5HAE3Z&y(8zR&fbAn*Zd8=hfJGSmABpTHCRhNZEv*v86)xSBZ0 zWRgkK&oIq=9z>l=bAW`BlSuMXkTEIBxJ<~TOv$vAWJbzTky)9Od8x{REXtBB%ZjYZ znygDK8&Z?HY|552q$%67BfGLE`*I+MawNxcBBydD=W-#JawXStBZ=I~o!k$37$(Wn k<7>a%`U+~%N09Lrzjyxo-5bS)ZvG``XD1hgzdz%zKR=*Zng9R* diff --git a/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf b/crates/zed/assets/fonts/zed-sans/zed-sans-extendedbold.ttf index 0e1264b1ceedf72417968c21d2dd0c7b2f1176ea..696c3cdd5b8ecde7cf574ec8491c15094b3bb4fa 100644 GIT binary patch delta 249 zcmXwtyDmck06_2UZ7D@5^(bvAeW2d25=-L;npkYU!Qca$oU{85F?8`M221CPNx~!% zm!u~-Imzk0yobHmhi-IVeeEKlBqWlgq+~=!B`sqzE*Y7SNtu$YOv{YqWLD-RFY~e> zi?SpIS(X(kN=eGHDr-`ab=i=r)TAyA*_18WmZt2;uI$Oa9LS*@$+4Wsshmkm&Y!vX pd}}>^TwUL0FTa9vGzen)&AZC5-<_XK>L&RVwBwTq!oMH$-(Sw&StkGh delta 249 zcmXwtIZgrr004Jc5D^6r@Z?bO0z}0Vjg225lyv^VACS!S{=rfU8lPfoXQHvy%7nO> zILTy^8NPgm!`S;l)X%jCNGJ)3Bq=GGkhDxnMy4by(=sDD$;+(F$-FE`L5i{{OR_90 zvMOs*lCo5!D(kW#n^Kc4*_IvIl|89TLz=QLEjf@wIg+*<%ZZ%InVidobma1xtFMpF nWApCeHh=vcRHOGG=C%CR{qIk2luh-MPr*%m5@32{{TViT5$jX delta 240 zcmXxVIZi?W002?&t0*q;T~IdJ-1h}s5dAG@FeIFSf+LX3^mg`i9)Rmu5kq5XXN)vX z@-pM$PkcP}ItY4Ue}KegMgp0YIhmJ)EXblHWl5H0MOGyxX$fUb)@4IBB_mt1Em_G) zUJ6o_9Vtm!DpHl2?8=_(%YhuqksQm3oXVM;%Y|IZm0Zh>)TJS}awqrlAWeCcmORPx ei0D0?@4WnWQ?2h@CBUwi)W5rJZX%piIYqw zna=xH*h%blqldd4HScTx{VaZ$GSBIkpq-pl5dQvzzy1Iu|6Q>F delta 263 zcmXwt$u2_y007_XYb{k;TPdE}ccr#xDH4fKaP}*0GIJG)k7%5n{DZIPQ5;=*s)$Eo zlF1~~{QL@=sgJGbuJqPILdi%ZS;yvTPUTF_A sDOayt-zLe+bML;`=myp3BS`7Q?@#~jmA~WML$)2 Date: Mon, 21 Mar 2022 18:16:06 +0100 Subject: [PATCH 041/139] :lipstick: --- crates/workspace/src/workspace.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 760ffa8c1a..3c5d1dfbf6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1248,14 +1248,8 @@ impl Workspace { state.active_view_id = response.active_view_id; Ok::<_, anyhow::Error>(()) })?; - Self::add_views_from_leader( - this, - leader_id, - vec![pane.clone()], - response.views, - &mut cx, - ) - .await?; + Self::add_views_from_leader(this, leader_id, vec![pane], response.views, &mut cx) + .await?; } Ok(()) })) From 3e0bc979c3bc864b046108bb69155af6815b5787 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 11:47:00 -0700 Subject: [PATCH 042/139] Avoid infinite loop when collaborators follow each other Co-Authored-By: Antonio Scandurra --- crates/editor/src/items.rs | 2 +- crates/rpc/proto/zed.proto | 37 +++++---- crates/server/src/rpc.rs | 124 ++++++++++++++++++++++++++++-- crates/workspace/src/workspace.rs | 73 +++++++++++------- 4 files changed, 186 insertions(+), 50 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7f28af0c40..fee7cc445e 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -6,7 +6,7 @@ use gpui::{ }; use language::{Bias, Buffer, Diagnostic, File as _}; use project::{File, Project, ProjectEntryId, ProjectPath}; -use rpc::proto::{self, update_followers::update_view}; +use rpc::proto::{self, update_view}; use std::{fmt::Write, path::PathBuf}; use text::{Point, Selection}; use util::ResultExt; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f0743c5b4f..dbf1218ccd 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -556,21 +556,6 @@ message UpdateFollowers { View create_view = 4; UpdateView update_view = 5; } - - message UpdateActiveView { - optional uint64 id = 1; - } - - message UpdateView { - uint64 id = 1; - oneof variant { - Editor editor = 2; - } - - message Editor { - Anchor scroll_top = 1; - } - } } message Unfollow { @@ -580,10 +565,30 @@ message Unfollow { // Entities +message UpdateActiveView { + optional uint64 id = 1; + optional uint32 leader_id = 2; +} + +message UpdateView { + uint64 id = 1; + optional uint32 leader_id = 2; + + oneof variant { + Editor editor = 3; + } + + message Editor { + Anchor scroll_top = 1; + } +} + message View { uint64 id = 1; + optional uint32 leader_id = 2; + oneof variant { - Editor editor = 2; + Editor editor = 3; } message Editor { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index fe56cea2e2..771d82e017 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -678,17 +678,21 @@ impl Server { request: TypedEnvelope, ) -> tide::Result { let leader_id = ConnectionId(request.payload.leader_id); + let follower_id = request.sender_id; if !self .state() - .project_connection_ids(request.payload.project_id, request.sender_id)? + .project_connection_ids(request.payload.project_id, follower_id)? .contains(&leader_id) { Err(anyhow!("no such peer"))?; } - let response = self + let mut response = self .peer .forward_request(request.sender_id, leader_id, request.payload) .await?; + response + .views + .retain(|view| view.leader_id != Some(follower_id.0)); Ok(response) } @@ -716,9 +720,18 @@ impl Server { let connection_ids = self .state() .project_connection_ids(request.payload.project_id, request.sender_id)?; + let leader_id = request + .payload + .variant + .as_ref() + .and_then(|variant| match variant { + proto::update_followers::Variant::CreateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateView(payload) => payload.leader_id, + proto::update_followers::Variant::UpdateActiveView(payload) => payload.leader_id, + }); for follower_id in &request.payload.follower_ids { let follower_id = ConnectionId(*follower_id); - if connection_ids.contains(&follower_id) { + if connection_ids.contains(&follower_id) && Some(follower_id.0) != leader_id { self.peer .forward_send(request.sender_id, follower_id, request.payload.clone())?; } @@ -4265,9 +4278,6 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); - workspace_b.update(cx_b, |workspace, cx| { - workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); - }); let editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4328,6 +4338,108 @@ mod tests { ); } + #[gpui::test(iterations = 10)] + async fn test_peers_following_each_other(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B opens an editor. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let _editor_b1 = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Clients A and B follow each other in split panes + workspace_a + .update(cx_a, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); + workspace + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + workspace + .toggle_follow(&workspace::ToggleFollow(leader_id), cx) + .unwrap() + }) + .await + .unwrap(); + + workspace_a + .update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + workspace.open_path((worktree_id, "3.txt"), cx) + }) + .await + .unwrap(); + + // Ensure peers following each other doesn't cause an infinite loop. + cx_a.foreground().run_until_parked(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .project_path(cx)), + Some((worktree_id, "3.txt").into()) + ); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3c5d1dfbf6..78a59e9d91 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -264,10 +264,10 @@ pub trait FollowableItem: Item { &self, event: &Self::Event, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &mut self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; } @@ -279,10 +279,10 @@ pub trait FollowableItemHandle: ItemHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option; + ) -> Option; fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; } @@ -300,13 +300,13 @@ impl FollowableItemHandle for ViewHandle { &self, event: &dyn Any, cx: &AppContext, - ) -> Option { + ) -> Option { self.read(cx).to_update_message(event.downcast_ref()?, cx) } fn apply_update_message( &self, - message: proto::update_followers::update_view::Variant, + message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_message(message, cx)) @@ -403,6 +403,7 @@ impl ItemHandle for ViewHandle { proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), }), cx, ); @@ -441,12 +442,11 @@ impl ItemHandle for ViewHandle { .and_then(|i| i.to_update_message(event, cx)) { workspace.update_followers( - proto::update_followers::Variant::UpdateView( - proto::update_followers::UpdateView { - id: item.id() as u64, - variant: Some(message), - }, - ), + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: Some(message), + leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -628,7 +628,7 @@ struct FollowerState { #[derive(Debug)] enum FollowerItem { - Loading(Vec), + Loading(Vec), Loaded(Box), } @@ -1110,7 +1110,7 @@ impl Workspace { fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { - self.active_pane = pane; + self.active_pane = pane.clone(); self.status_bar.update(cx, |status_bar, cx| { status_bar.set_active_pane(&self.active_pane, cx); }); @@ -1119,11 +1119,10 @@ impl Workspace { } self.update_followers( - proto::update_followers::Variant::UpdateActiveView( - proto::update_followers::UpdateActiveView { - id: self.active_item(cx).map(|item| item.id() as u64), - }, - ), + proto::update_followers::Variant::UpdateActiveView(proto::UpdateActiveView { + id: self.active_item(cx).map(|item| item.id() as u64), + leader_id: self.leader_for_pane(&pane).map(|id| id.0), + }), cx, ); } @@ -1520,14 +1519,22 @@ impl Workspace { Ok(proto::FollowResponse { active_view_id, views: this - .items(cx) - .filter_map(|item| { - let id = item.id() as u64; - let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx)?; - Some(proto::View { - id, - variant: Some(variant), + .panes() + .iter() + .flat_map(|pane| { + let leader_id = this.leader_for_pane(pane).map(|id| id.0); + pane.read(cx).items().filter_map({ + let cx = &cx; + move |item| { + let id = item.id() as u64; + let item = item.to_followable_item_handle(cx)?; + let variant = item.to_state_message(cx)?; + Some(proto::View { + id, + leader_id, + variant: Some(variant), + }) + } }) }) .collect(), @@ -1705,6 +1712,18 @@ impl Workspace { None } + fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + self.follower_states_by_leader + .iter() + .find_map(|(leader_id, state)| { + if state.contains_key(pane) { + Some(*leader_id) + } else { + None + } + }) + } + fn update_leader_state( &mut self, leader_id: PeerId, From 06cd9ac664b53c7e836d0b7262f14ac27d947b44 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 14:04:55 -0700 Subject: [PATCH 043/139] Match the leader's last selection when unfollowing Co-Authored-By: Antonio Scandurra --- crates/editor/src/editor.rs | 8 +++---- crates/editor/src/items.rs | 39 ++++++++++++++++++++++++------- crates/language/src/buffer.rs | 6 ----- crates/server/src/rpc.rs | 35 +++++++++++++++++++++++---- crates/workspace/src/workspace.rs | 22 ++++++++++++----- 5 files changed, 81 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0db67e73e7..80ed9bc512 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -464,7 +464,7 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, - following: bool, + leader_replica_id: Option, } pub struct EditorSnapshot { @@ -938,7 +938,7 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), - following: false, + leader_replica_id: None, }; this.end_selection(cx); this @@ -5038,7 +5038,7 @@ impl Editor { self.selections = selections; self.pending_selection = pending_selection; - if self.focused && !self.following { + if self.focused && self.leader_replica_id.is_none() { self.buffer.update(cx, |buffer, cx| { buffer.set_active_selections(&self.selections, cx) }); @@ -5673,7 +5673,7 @@ impl View for Editor { self.blink_cursors(self.blink_epoch, cx); self.buffer.update(cx, |buffer, cx| { buffer.finalize_last_transaction(cx); - if !self.following { + if self.leader_replica_id.is_none() { buffer.set_active_selections(&self.selections, cx); } }); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index fee7cc445e..3fdaad8b8f 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,4 +1,4 @@ -use crate::{Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; +use crate::{Anchor, Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, @@ -50,20 +50,43 @@ impl FollowableItem for Editor { })) } - fn set_following(&mut self, following: bool, cx: &mut ViewContext) { - self.following = following; - if self.following { + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + let prev_leader_replica_id = self.leader_replica_id; + self.leader_replica_id = leader_replica_id; + if self.leader_replica_id.is_some() { self.show_local_selections = false; self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); } else { self.show_local_selections = true; - if self.focused { - self.buffer.update(cx, |buffer, cx| { - buffer.set_active_selections(&self.selections, cx); - }); + if let Some(leader_replica_id) = prev_leader_replica_id { + let selections = self + .buffer + .read(cx) + .snapshot(cx) + .remote_selections_in_range(&(Anchor::min()..Anchor::max())) + .filter_map(|(replica_id, selections)| { + if replica_id == leader_replica_id { + Some(selections) + } else { + None + } + }) + .collect::>(); + if !selections.is_empty() { + self.set_selections(selections.into(), None, cx); + } } + self.buffer.update(cx, |buffer, cx| { + if self.focused { + buffer.set_active_selections(&self.selections, cx); + } + }); } cx.notify(); } diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a4fee82805..fa4208ee5b 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1801,12 +1801,6 @@ impl BufferSnapshot { .min_by_key(|(open_range, close_range)| close_range.end - open_range.start) } - /* - impl BufferSnapshot - pub fn remote_selections_in_range(&self, Range) -> impl Iterator>)> - pub fn remote_selections_in_range(&self, Range) -> impl Iterator( &'a self, range: Range, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 771d82e017..c6627ce7b8 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1083,8 +1083,8 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, - ToOffset, ToggleCodeActions, Undo, + self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, + Rename, ToOffset, ToggleCodeActions, Undo, }; use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ @@ -4290,7 +4290,13 @@ mod tests { // Client B starts following client A. workspace_b .update(cx_b, |workspace, cx| { - let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); + let leader_id = project_b + .read(cx) + .collaborators() + .values() + .next() + .unwrap() + .peer_id; workspace.toggle_follow(&leader_id.into(), cx).unwrap() }) .await @@ -4318,16 +4324,35 @@ mod tests { }) .await; + editor_a1.update(cx_a, |editor, cx| { + editor.select_ranges([2..2], None, cx); + }); + editor_b1 + .condition(cx_b, |editor, cx| { + let snapshot = editor.buffer().read(cx).snapshot(cx); + let selection = snapshot + .remote_selections_in_range(&(Anchor::min()..Anchor::max())) + .next(); + selection.map_or(false, |selection| { + selection.1.start.to_offset(&snapshot) == 2 + }) + }) + .await; + // After unfollowing, client B stops receiving updates from client A. workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); + editor_b1.update(cx_b, |editor, cx| { + assert_eq!(editor.selected_ranges::(cx), &[2..2]); + }); + workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a2, cx); - editor_a2.update(cx, |editor, cx| editor.set_text("ONE", cx)); + editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); }); editor_b2 - .condition(cx_b, |editor, cx| editor.text(cx) == "ONE") + .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 78a59e9d91..1314b53e63 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -258,7 +258,7 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn set_following(&mut self, following: bool, cx: &mut ViewContext); + fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -273,7 +273,7 @@ pub trait FollowableItem: Item { } pub trait FollowableItemHandle: ItemHandle { - fn set_following(&self, following: bool, cx: &mut MutableAppContext); + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); fn to_state_message(&self, cx: &AppContext) -> Option; fn to_update_message( &self, @@ -288,8 +288,10 @@ pub trait FollowableItemHandle: ItemHandle { } impl FollowableItemHandle for ViewHandle { - fn set_following(&self, following: bool, cx: &mut MutableAppContext) { - self.update(cx, |this, cx| this.set_following(following, cx)) + fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext) { + self.update(cx, |this, cx| { + this.set_leader_replica_id(leader_replica_id, cx) + }) } fn to_state_message(&self, cx: &AppContext) -> Option { @@ -1263,7 +1265,7 @@ impl Workspace { if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { - item.set_following(false, cx); + item.set_leader_replica_id(None, cx); } } @@ -1624,6 +1626,14 @@ impl Workspace { cx: &mut AsyncAppContext, ) -> Result<()> { let project = this.read_with(cx, |this, _| this.project.clone()); + let replica_id = project + .read_with(cx, |project, _| { + project + .collaborators() + .get(&leader_id) + .map(|c| c.replica_id) + }) + .ok_or_else(|| anyhow!("no such collaborator {}", leader_id))?; let item_builders = cx.update(|cx| { cx.default_global::() @@ -1667,7 +1677,7 @@ impl Workspace { .get_mut(&pane)?; for (id, item) in leader_view_ids.into_iter().zip(items) { - item.set_following(true, cx); + item.set_leader_replica_id(Some(replica_id), cx); match state.items_by_leader_view_id.entry(id) { hash_map::Entry::Occupied(e) => { let e = e.into_mut(); From baeb7d27b8c1121fceb9908253103457ea1a7ddc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 13:58:16 -0600 Subject: [PATCH 044/139] Clarify word movement function names and improve test coverage Co-Authored-By: Keith Simmons --- crates/editor/src/editor.rs | 13 ++--- crates/editor/src/movement.rs | 102 ++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 30888d8a40..c3858b4349 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3480,7 +3480,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + let cursor = movement::previous_word_start(&display_map, head).to_point(&display_map); selection.start = cursor.clone(); selection.end = cursor; selection.reversed = false; @@ -3498,7 +3498,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::prev_word_boundary(&display_map, head).to_point(&display_map); + let cursor = movement::previous_word_start(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3517,7 +3517,7 @@ impl Editor { if selection.is_empty() { let head = selection.head().to_display_point(&display_map); let cursor = - movement::prev_word_boundary(&display_map, head).to_point(&display_map); + movement::previous_word_start(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3536,7 +3536,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); selection.start = cursor; selection.end = cursor; selection.reversed = false; @@ -3554,7 +3554,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_boundary(&display_map, head).to_point(&display_map); + let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3572,8 +3572,7 @@ impl Editor { for selection in &mut selections { if selection.is_empty() { let head = selection.head().to_display_point(&display_map); - let cursor = - movement::next_word_boundary(&display_map, head).to_point(&display_map); + let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 5951e2c20d..a090d8dde2 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -132,7 +132,7 @@ pub fn line_end( } } -pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { +pub fn previous_word_start(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let mut line_start = 0; if point.row() > 0 { if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { @@ -171,7 +171,7 @@ pub fn prev_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> Dis boundary } -pub fn next_word_boundary(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { +pub fn next_word_end(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let mut prev_char_kind = None; for c in map.chars_at(point) { let char_kind = char_kind(c); @@ -297,6 +297,84 @@ mod tests { ); } + #[gpui::test] + fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + dbg!(&display_points); + assert_eq!( + previous_word_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + assert("\n| |lorem", cx); + assert(" |lorem|", cx); + assert(" |lor|em", cx); + assert("\nlorem\n| |ipsum", cx); + assert("\n\n|\n|", cx); + assert(" |lorem |ipsum", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|-#$@|ipsum", cx); + assert("|lorem_|ipsum", cx); + } + + #[gpui::test] + fn test_next_word_end(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + next_word_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + assert("\n| lorem|", cx); + assert(" |lorem|", cx); + assert(" lor|em|", cx); + assert(" lorem| |\nipsum\n", cx); + assert("\n|\n|\n\n", cx); + assert("lorem| ipsum| ", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|#$@-|ipsum", cx); + assert("lorem|_ipsum|", cx); + } + + // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. + fn marked_snapshot( + text: &str, + cx: &mut gpui::MutableAppContext, + ) -> (DisplaySnapshot, Vec) { + let mut marked_offsets = Vec::new(); + let chunks = text.split('|'); + let mut text = String::new(); + + for chunk in chunks { + text.push_str(chunk); + marked_offsets.push(text.len()); + } + marked_offsets.pop(); + + let tab_size = 4; + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let buffer = MultiBuffer::build_simple(&text, cx); + let display_map = cx + .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let marked_display_points = marked_offsets + .into_iter() + .map(|offset| offset.to_display_point(&snapshot)) + .collect(); + + (snapshot, marked_display_points) + } + #[gpui::test] fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) { let tab_size = 4; @@ -312,44 +390,44 @@ mod tests { .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 12)), + previous_word_start(&snapshot, DisplayPoint::new(0, 12)), DisplayPoint::new(0, 7) ); assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 7)), + previous_word_start(&snapshot, DisplayPoint::new(0, 7)), DisplayPoint::new(0, 2) ); assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 6)), + previous_word_start(&snapshot, DisplayPoint::new(0, 6)), DisplayPoint::new(0, 2) ); assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 2)), + previous_word_start(&snapshot, DisplayPoint::new(0, 2)), DisplayPoint::new(0, 0) ); assert_eq!( - prev_word_boundary(&snapshot, DisplayPoint::new(0, 1)), + previous_word_start(&snapshot, DisplayPoint::new(0, 1)), DisplayPoint::new(0, 0) ); assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 0)), + next_word_end(&snapshot, DisplayPoint::new(0, 0)), DisplayPoint::new(0, 1) ); assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 1)), + next_word_end(&snapshot, DisplayPoint::new(0, 1)), DisplayPoint::new(0, 6) ); assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 2)), + next_word_end(&snapshot, DisplayPoint::new(0, 2)), DisplayPoint::new(0, 6) ); assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 6)), + next_word_end(&snapshot, DisplayPoint::new(0, 6)), DisplayPoint::new(0, 12) ); assert_eq!( - next_word_boundary(&snapshot, DisplayPoint::new(0, 7)), + next_word_end(&snapshot, DisplayPoint::new(0, 7)), DisplayPoint::new(0, 12) ); } From 210fa4c4431e8d00a4c7470ec44c385bf3a18d34 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 14:14:55 -0600 Subject: [PATCH 045/139] Remove CharKind::Newline This is just a character, and so it seems clearer to refer to it specifically when we want to know if a character is a newline. There was only one case where we relied on Newline being different from Whitespace, and we special-cased that instance. Changing this actually makes us match the behavior of VS Code when double-clicking runs of multiple newlines. /cc @as-cii Co-Authored-By: Keith Simmons --- crates/editor/src/movement.rs | 20 ++++++++++---------- crates/editor/src/multi_buffer.rs | 4 ++-- crates/language/src/buffer.rs | 5 +---- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index a090d8dde2..9c87ca3016 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -151,17 +151,14 @@ pub fn previous_word_start(map: &DisplaySnapshot, mut point: DisplayPoint) -> Di let mut boundary = DisplayPoint::new(point.row(), 0); let mut column = 0; - let mut prev_char_kind = CharKind::Newline; + let mut prev_char_kind = CharKind::Whitespace; for c in map.chars_at(DisplayPoint::new(point.row(), 0)) { if column >= point.column() { break; } let char_kind = char_kind(c); - if char_kind != prev_char_kind - && char_kind != CharKind::Whitespace - && char_kind != CharKind::Newline - { + if char_kind != prev_char_kind && char_kind != CharKind::Whitespace && c != '\n' { *boundary.column_mut() = column; } @@ -179,10 +176,7 @@ pub fn next_word_end(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayP if c == '\n' { break; } - if prev_char_kind != char_kind - && prev_char_kind != CharKind::Whitespace - && prev_char_kind != CharKind::Newline - { + if prev_char_kind != char_kind && prev_char_kind != CharKind::Whitespace { break; } } @@ -441,7 +435,7 @@ mod tests { .select_font(family_id, &Default::default()) .unwrap(); let font_size = 14.0; - let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit", cx); + let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit\n\n\n\n", cx); let display_map = cx .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -502,5 +496,11 @@ mod tests { surrounding_word(&snapshot, DisplayPoint::new(1, 7)), DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), ); + + // Don't consider runs of multiple newlines to be a "word" + assert_eq!( + surrounding_word(&snapshot, DisplayPoint::new(3, 0)), + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + ); } } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e145488d65..715548f813 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1407,7 +1407,7 @@ impl MultiBufferSnapshot { ); for ch in prev_chars { - if Some(char_kind(ch)) == word_kind { + if Some(char_kind(ch)) == word_kind && ch != '\n' { start -= ch.len_utf8(); } else { break; @@ -1415,7 +1415,7 @@ impl MultiBufferSnapshot { } for ch in next_chars { - if Some(char_kind(ch)) == word_kind { + if Some(char_kind(ch)) == word_kind && ch != '\n' { end += ch.len_utf8(); } else { break; diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index a4fee82805..763d08c053 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -271,7 +271,6 @@ pub(crate) struct DiagnosticEndpoint { #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug)] pub enum CharKind { - Newline, Punctuation, Whitespace, Word, @@ -2259,9 +2258,7 @@ pub fn contiguous_ranges( } pub fn char_kind(c: char) -> CharKind { - if c == '\n' { - CharKind::Newline - } else if c.is_whitespace() { + if c.is_whitespace() { CharKind::Whitespace } else if c.is_alphanumeric() || c == '_' { CharKind::Word From 0c89ad3ac02ab27988dd82b474370fcc98e5ebbc Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 15:41:42 -0600 Subject: [PATCH 046/139] Make multi-byte and surrounding_word tests more readable Just merge multi-byte tests into the main word movement tests --- crates/editor/src/movement.rs | 246 ++++++++++------------------------ 1 file changed, 68 insertions(+), 178 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 9c87ca3016..b5bafebe67 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -222,6 +222,74 @@ mod tests { use crate::{Buffer, DisplayMap, MultiBuffer}; use language::Point; + #[gpui::test] + fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + dbg!(&display_points); + assert_eq!( + previous_word_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + assert("\n| |lorem", cx); + assert(" |lorem|", cx); + assert(" |lor|em", cx); + assert("\nlorem\n| |ipsum", cx); + assert("\n\n|\n|", cx); + assert(" |lorem |ipsum", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|-#$@|ipsum", cx); + assert("|lorem_|ipsum", cx); + assert(" |defγ|", cx); + assert(" |bcΔ|", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_next_word_end(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + next_word_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + assert("\n| lorem|", cx); + assert(" |lorem|", cx); + assert(" lor|em|", cx); + assert(" lorem| |\nipsum\n", cx); + assert("\n|\n|\n\n", cx); + assert("lorem| ipsum| ", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|#$@-|ipsum", cx); + assert("lorem|_ipsum|", cx); + assert(" |bcΔ|", cx); + assert(" ab|——|cd", cx); + } + + #[gpui::test] + fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + surrounding_word(&snapshot, display_points[1]), + display_points[0]..display_points[2] + ); + } + + assert("||lorem| ipsum", cx); + assert("|lo|rem| ipsum", cx); + assert("|lorem|| ipsum", cx); + assert("lorem| | |ipsum", cx); + assert("lorem\n|||\nipsum", cx); + assert("lorem\n||ipsum|", cx); + assert("lorem,|| |ipsum", cx); + assert("|lorem||, ipsum", cx); + } + #[gpui::test] fn test_move_up_and_down_with_excerpts(cx: &mut gpui::MutableAppContext) { let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); @@ -291,49 +359,6 @@ mod tests { ); } - #[gpui::test] - fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { - fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); - dbg!(&display_points); - assert_eq!( - previous_word_start(&snapshot, display_points[1]), - display_points[0] - ); - } - - assert("\n| |lorem", cx); - assert(" |lorem|", cx); - assert(" |lor|em", cx); - assert("\nlorem\n| |ipsum", cx); - assert("\n\n|\n|", cx); - assert(" |lorem |ipsum", cx); - assert("lorem|-|ipsum", cx); - assert("lorem|-#$@|ipsum", cx); - assert("|lorem_|ipsum", cx); - } - - #[gpui::test] - fn test_next_word_end(cx: &mut gpui::MutableAppContext) { - fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); - assert_eq!( - next_word_end(&snapshot, display_points[0]), - display_points[1] - ); - } - - assert("\n| lorem|", cx); - assert(" |lorem|", cx); - assert(" lor|em|", cx); - assert(" lorem| |\nipsum\n", cx); - assert("\n|\n|\n\n", cx); - assert("lorem| ipsum| ", cx); - assert("lorem|-|ipsum", cx); - assert("lorem|#$@-|ipsum", cx); - assert("lorem|_ipsum|", cx); - } - // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. fn marked_snapshot( text: &str, @@ -368,139 +393,4 @@ mod tests { (snapshot, marked_display_points) } - - #[gpui::test] - fn test_prev_next_word_boundary_multibyte(cx: &mut gpui::MutableAppContext) { - let tab_size = 4; - let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - - let buffer = MultiBuffer::build_simple("a bcΔ defγ hi—jk", cx); - let display_map = cx - .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - assert_eq!( - previous_word_start(&snapshot, DisplayPoint::new(0, 12)), - DisplayPoint::new(0, 7) - ); - assert_eq!( - previous_word_start(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 2) - ); - assert_eq!( - previous_word_start(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 2) - ); - assert_eq!( - previous_word_start(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 0) - ); - assert_eq!( - previous_word_start(&snapshot, DisplayPoint::new(0, 1)), - DisplayPoint::new(0, 0) - ); - - assert_eq!( - next_word_end(&snapshot, DisplayPoint::new(0, 0)), - DisplayPoint::new(0, 1) - ); - assert_eq!( - next_word_end(&snapshot, DisplayPoint::new(0, 1)), - DisplayPoint::new(0, 6) - ); - assert_eq!( - next_word_end(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 6) - ); - assert_eq!( - next_word_end(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 12) - ); - assert_eq!( - next_word_end(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 12) - ); - } - - #[gpui::test] - fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { - let tab_size = 4; - let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - let buffer = MultiBuffer::build_simple("lorem ipsum dolor\n sit\n\n\n\n", cx); - let display_map = cx - .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 0)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 2)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 5)), - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 5), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 6)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 7)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 11)), - DisplayPoint::new(0, 6)..DisplayPoint::new(0, 11), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 13)), - DisplayPoint::new(0, 11)..DisplayPoint::new(0, 14), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 14)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 17)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(0, 19)), - DisplayPoint::new(0, 14)..DisplayPoint::new(0, 19), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 0)), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 1)), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 4), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 6)), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - ); - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(1, 7)), - DisplayPoint::new(1, 4)..DisplayPoint::new(1, 7), - ); - - // Don't consider runs of multiple newlines to be a "word" - assert_eq!( - surrounding_word(&snapshot, DisplayPoint::new(3, 0)), - DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), - ); - } } From 5b548747050eb1c7a0c645027c35bb5e8a5a775e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 16:01:22 -0600 Subject: [PATCH 047/139] Extract logic for scanning over character boundaries --- crates/editor/src/movement.rs | 101 ++++++++++++++++++++-------------- 1 file changed, 60 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index b5bafebe67..fe66a23c3d 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -132,66 +132,86 @@ pub fn line_end( } } -pub fn previous_word_start(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { +pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary_reversed(map, point, |left, right| { + char_kind(left) != char_kind(right) && !right.is_whitespace() + }) +} + +pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary(map, point, |left, right| { + char_kind(left) != char_kind(right) && !left.is_whitespace() + }) +} + +fn find_boundary( + map: &DisplaySnapshot, + mut start: DisplayPoint, + is_boundary: impl Fn(char, char) -> bool, +) -> DisplayPoint { + let mut prev_ch = None; + for ch in map.chars_at(start) { + if let Some(prev_ch) = prev_ch { + if ch == '\n' { + break; + } + if is_boundary(prev_ch, ch) { + break; + } + } + + if ch == '\n' { + *start.row_mut() += 1; + *start.column_mut() = 0; + } else { + *start.column_mut() += ch.len_utf8() as u32; + } + prev_ch = Some(ch); + } + map.clip_point(start, Bias::Right) +} + +fn find_boundary_reversed( + map: &DisplaySnapshot, + mut start: DisplayPoint, + is_boundary: impl Fn(char, char) -> bool, +) -> DisplayPoint { let mut line_start = 0; - if point.row() > 0 { - if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { + if start.row() > 0 { + if let Some(indent) = map.soft_wrap_indent(start.row() - 1) { line_start = indent; } } - if point.column() == line_start { - if point.row() == 0 { + if start.column() == line_start { + if start.row() == 0 { return DisplayPoint::new(0, 0); } else { - let row = point.row() - 1; - point = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left); + let row = start.row() - 1; + start = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left); } } - let mut boundary = DisplayPoint::new(point.row(), 0); + let mut boundary = DisplayPoint::new(start.row(), 0); let mut column = 0; - let mut prev_char_kind = CharKind::Whitespace; - for c in map.chars_at(DisplayPoint::new(point.row(), 0)) { - if column >= point.column() { + let mut prev_ch = None; + for ch in map.chars_at(DisplayPoint::new(start.row(), 0)) { + if column >= start.column() { break; } - let char_kind = char_kind(c); - if char_kind != prev_char_kind && char_kind != CharKind::Whitespace && c != '\n' { - *boundary.column_mut() = column; + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + *boundary.column_mut() = column; + } } - prev_char_kind = char_kind; - column += c.len_utf8() as u32; + prev_ch = Some(ch); + column += ch.len_utf8() as u32; } boundary } -pub fn next_word_end(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { - let mut prev_char_kind = None; - for c in map.chars_at(point) { - let char_kind = char_kind(c); - if let Some(prev_char_kind) = prev_char_kind { - if c == '\n' { - break; - } - if prev_char_kind != char_kind && prev_char_kind != CharKind::Whitespace { - break; - } - } - - if c == '\n' { - *point.row_mut() += 1; - *point.column_mut() = 0; - } else { - *point.column_mut() += c.len_utf8() as u32; - } - prev_char_kind = Some(char_kind); - } - map.clip_point(point, Bias::Right) -} - pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; @@ -226,7 +246,6 @@ mod tests { fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { let (snapshot, display_points) = marked_snapshot(marked_text, cx); - dbg!(&display_points); assert_eq!( previous_word_start(&snapshot, display_points[1]), display_points[0] From c8f36af82365c5976322d58110a004e87c9858ff Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 15:12:15 -0700 Subject: [PATCH 048/139] Show borders around avatars and panes to indicate following state --- crates/theme/src/theme.rs | 1 + crates/workspace/src/pane_group.rs | 61 ++++++++++++++++++++++++----- crates/workspace/src/workspace.rs | 38 +++++++++++++----- crates/zed/assets/themes/_base.toml | 1 + 4 files changed, 82 insertions(+), 19 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 5e8e799b4c..61d0bf3f67 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -35,6 +35,7 @@ pub struct Workspace { pub tab: Tab, pub active_tab: Tab, pub pane_divider: Border, + pub leader_border_opacity: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 2b56a023fc..c3f4d2d3a6 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -1,9 +1,11 @@ +use crate::{FollowerStatesByLeader, Pane}; use anyhow::{anyhow, Result}; -use gpui::{elements::*, Axis, ViewHandle}; +use client::PeerId; +use collections::HashMap; +use gpui::{elements::*, Axis, Border, ViewHandle}; +use project::Collaborator; use theme::Theme; -use crate::Pane; - #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { root: Member, @@ -47,8 +49,13 @@ impl PaneGroup { } } - pub fn render<'a>(&self, theme: &Theme) -> ElementBox { - self.root.render(theme) + pub(crate) fn render<'a>( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { + self.root.render(theme, follower_states, collaborators) } } @@ -80,10 +87,39 @@ impl Member { Member::Axis(PaneAxis { axis, members }) } - pub fn render(&self, theme: &Theme) -> ElementBox { + pub fn render( + &self, + theme: &Theme, + follower_states: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { match self { - Member::Pane(pane) => ChildView::new(pane).boxed(), - Member::Axis(axis) => axis.render(theme), + Member::Pane(pane) => { + let mut border = Border::default(); + let leader = follower_states + .iter() + .find_map(|(leader_id, follower_states)| { + if follower_states.contains_key(pane) { + Some(leader_id) + } else { + None + } + }) + .and_then(|leader_id| collaborators.get(leader_id)); + if let Some(leader) = leader { + let leader_color = theme + .editor + .replica_selection_style(leader.replica_id) + .cursor; + border = Border::all(1.0, leader_color); + border + .color + .fade_out(1. - theme.workspace.leader_border_opacity); + border.overlay = true; + } + ChildView::new(pane).contained().with_border(border).boxed() + } + Member::Axis(axis) => axis.render(theme, follower_states, collaborators), } } } @@ -172,11 +208,16 @@ impl PaneAxis { } } - fn render<'a>(&self, theme: &Theme) -> ElementBox { + fn render( + &self, + theme: &Theme, + follower_state: &FollowerStatesByLeader, + collaborators: &HashMap, + ) -> ElementBox { let last_member_ix = self.members.len() - 1; Flex::new(self.axis) .with_children(self.members.iter().enumerate().map(|(ix, member)| { - let mut member = member.render(theme); + let mut member = member.render(theme, follower_state, collaborators); if ix < last_member_ix { let mut border = theme.workspace.pane_divider; border.left = false; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1314b53e63..387bfa9b6e 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -20,9 +20,9 @@ use gpui::{ json::{self, to_string_pretty, ToJson}, keymap::Binding, platform::{CursorStyle, WindowOptions}, - AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, ClipboardItem, Entity, ImageData, - ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, View, - ViewContext, ViewHandle, WeakViewHandle, + AnyModelHandle, AnyViewHandle, AppContext, AsyncAppContext, Border, ClipboardItem, Entity, + ImageData, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakViewHandle, }; use language::LanguageRegistry; use log::error; @@ -613,7 +613,7 @@ pub struct Workspace { status_bar: ViewHandle, project: ModelHandle, leader_state: LeaderState, - follower_states_by_leader: HashMap, FollowerState>>, + follower_states_by_leader: FollowerStatesByLeader, _observe_current_user: Task<()>, } @@ -622,6 +622,8 @@ struct LeaderState { followers: HashSet, } +type FollowerStatesByLeader = HashMap, FollowerState>>; + #[derive(Default)] struct FollowerState { active_view_id: Option, @@ -1262,6 +1264,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Option { for (leader_id, states_by_pane) in &mut self.follower_states_by_leader { + let leader_id = *leader_id; if let Some(state) = states_by_pane.remove(&pane) { for (_, item) in state.items_by_leader_view_id { if let FollowerItem::Loaded(item) = item { @@ -1270,6 +1273,7 @@ impl Workspace { } if states_by_pane.is_empty() { + self.follower_states_by_leader.remove(&leader_id); if let Some(project_id) = self.project.read(cx).remote_id() { self.client .send(proto::Unfollow { @@ -1281,7 +1285,7 @@ impl Workspace { } cx.notify(); - return Some(*leader_id); + return Some(leader_id); } } None @@ -1420,17 +1424,25 @@ impl Workspace { theme: &Theme, cx: &mut RenderContext, ) -> ElementBox { + let replica_color = theme.editor.replica_selection_style(replica_id).cursor; + let is_followed = peer_id.map_or(false, |peer_id| { + self.follower_states_by_leader.contains_key(&peer_id) + }); + let mut avatar_style = theme.workspace.titlebar.avatar; + if is_followed { + avatar_style.border = Border::all(1.0, replica_color); + } let content = Stack::new() .with_child( Image::new(avatar) - .with_style(theme.workspace.titlebar.avatar) + .with_style(avatar_style) .constrained() .with_width(theme.workspace.titlebar.avatar_width) .aligned() .boxed(), ) .with_child( - AvatarRibbon::new(theme.editor.replica_selection_style(replica_id).cursor) + AvatarRibbon::new(replica_color) .constrained() .with_width(theme.workspace.titlebar.avatar_ribbon.width) .with_height(theme.workspace.titlebar.avatar_ribbon.height) @@ -1800,8 +1812,16 @@ impl View for Workspace { content.add_child( Flex::column() .with_child( - Flexible::new(1., true, self.center.render(&theme)) - .boxed(), + Flexible::new( + 1., + true, + self.center.render( + &theme, + &self.follower_states_by_leader, + self.project.read(cx).collaborators(), + ), + ) + .boxed(), ) .with_child(ChildView::new(&self.status_bar).boxed()) .flexible(1., true) diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index 76547967bf..d0368d6933 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,6 +4,7 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } +leader_border_opacity = 0.6 [workspace.titlebar] height = 32 From c0d05c82b7bf3ca3355f2cf82a76537f0338a7b4 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 17:37:36 -0600 Subject: [PATCH 049/139] WIP: Start on previous_subword_start Co-Authored-By: Max Brunsfeld Co-Authored-By: Keith Simmons --- crates/editor/src/movement.rs | 98 +++++++++++++++++++++++++---------- 1 file changed, 70 insertions(+), 28 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index fe66a23c3d..c8f217b57e 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -138,40 +138,22 @@ pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> Displa }) } +pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary_reversed(map, point, |left, right| { + (char_kind(left) != char_kind(right) + || left == '_' && right != '_' + || left.is_lowercase() && right.is_uppercase()) + && !right.is_whitespace() + }) +} + pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { find_boundary(map, point, |left, right| { char_kind(left) != char_kind(right) && !left.is_whitespace() }) } -fn find_boundary( - map: &DisplaySnapshot, - mut start: DisplayPoint, - is_boundary: impl Fn(char, char) -> bool, -) -> DisplayPoint { - let mut prev_ch = None; - for ch in map.chars_at(start) { - if let Some(prev_ch) = prev_ch { - if ch == '\n' { - break; - } - if is_boundary(prev_ch, ch) { - break; - } - } - - if ch == '\n' { - *start.row_mut() += 1; - *start.column_mut() = 0; - } else { - *start.column_mut() += ch.len_utf8() as u32; - } - prev_ch = Some(ch); - } - map.clip_point(start, Bias::Right) -} - -fn find_boundary_reversed( +pub fn find_boundary_reversed( map: &DisplaySnapshot, mut start: DisplayPoint, is_boundary: impl Fn(char, char) -> bool, @@ -212,6 +194,33 @@ fn find_boundary_reversed( boundary } +pub fn find_boundary( + map: &DisplaySnapshot, + mut start: DisplayPoint, + is_boundary: impl Fn(char, char) -> bool, +) -> DisplayPoint { + let mut prev_ch = None; + for ch in map.chars_at(start) { + if let Some(prev_ch) = prev_ch { + if ch == '\n' { + break; + } + if is_boundary(prev_ch, ch) { + break; + } + } + + if ch == '\n' { + *start.row_mut() += 1; + *start.column_mut() = 0; + } else { + *start.column_mut() += ch.len_utf8() as u32; + } + prev_ch = Some(ch); + } + map.clip_point(start, Bias::Right) +} + pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { let ix = map.clip_point(point, Bias::Left).to_offset(map, Bias::Left); let text = &map.buffer_snapshot; @@ -266,6 +275,39 @@ mod tests { assert(" ab|——|cd", cx); } + #[gpui::test] + fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + previous_subword_start(&snapshot, display_points[1]), + display_points[0] + ); + } + + // Subword boundaries are respected + assert("lorem_|ip|sum", cx); + assert("lorem_|ipsum|", cx); + assert("|lorem_|ipsum", cx); + assert("lorem_|ipsum_|dolor", cx); + assert("lorem|Ip|sum", cx); + assert("lorem|Ipsum|", cx); + + // Word boundaries are still respected + assert("\n| |lorem", cx); + assert(" |lorem|", cx); + assert(" |lor|em", cx); + assert("\nlorem\n| |ipsum", cx); + assert("\n\n|\n|", cx); + assert(" |lorem |ipsum", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|-#$@|ipsum", cx); + assert(" |defγ|", cx); + assert(" bc|Δ|", cx); + assert(" |bcδ|", cx); + assert(" ab|——|cd", cx); + } + #[gpui::test] fn test_next_word_end(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { From e5a00d72f8a498e77e82b396c3b846b21ad18609 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 20:02:08 -0600 Subject: [PATCH 050/139] Implement next_subword_end --- crates/editor/src/movement.rs | 41 +++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index c8f217b57e..72567312e1 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -153,6 +153,15 @@ pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint }) } +pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { + find_boundary(map, point, |left, right| { + (char_kind(left) != char_kind(right) + || left != '_' && right == '_' + || left.is_lowercase() && right.is_uppercase()) + && !left.is_whitespace() + }) +} + pub fn find_boundary_reversed( map: &DisplaySnapshot, mut start: DisplayPoint, @@ -331,6 +340,38 @@ mod tests { assert(" ab|——|cd", cx); } + #[gpui::test] + fn test_next_subword_end(cx: &mut gpui::MutableAppContext) { + fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + next_subword_end(&snapshot, display_points[0]), + display_points[1] + ); + } + + // Subword boundaries are respected + assert("lo|rem|_ipsum", cx); + assert("|lorem|_ipsum", cx); + assert("lorem|_ipsum|", cx); + assert("lorem|_ipsum|_dolor", cx); + assert("lo|rem|Ipsum", cx); + assert("lorem|Ipsum|Dolor", cx); + + // Word boundaries are still respected + assert("\n| lorem|", cx); + assert(" |lorem|", cx); + assert(" lor|em|", cx); + assert(" lorem| |\nipsum\n", cx); + assert("\n|\n|\n\n", cx); + assert("lorem| ipsum| ", cx); + assert("lorem|-|ipsum", cx); + assert("lorem|#$@-|ipsum", cx); + assert("lorem|_ipsum|", cx); + assert(" |bc|Δ", cx); + assert(" ab|——|cd", cx); + } + #[gpui::test] fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { From f70f4c77293b8cec39ad80b22d97310301244200 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 21:07:56 -0600 Subject: [PATCH 051/139] Improve DisplayPoint Debug impl --- crates/editor/src/display_map.rs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index dbadbb386e..17209e5c74 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -12,7 +12,7 @@ use gpui::{ Entity, ModelContext, ModelHandle, }; use language::{Point, Subscription as BufferSubscription}; -use std::{any::TypeId, ops::Range, sync::Arc}; +use std::{any::TypeId, fmt::Debug, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; use wrap_map::WrapMap; @@ -414,9 +414,19 @@ impl DisplaySnapshot { } } -#[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] +#[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct DisplayPoint(BlockPoint); +impl Debug for DisplayPoint { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!( + "DisplayPoint({}, {})", + self.row(), + self.column() + )) + } +} + impl DisplayPoint { pub fn new(row: u32, column: u32) -> Self { Self(BlockPoint(Point::new(row, column))) @@ -426,7 +436,6 @@ impl DisplayPoint { Self::new(0, 0) } - #[cfg(test)] pub fn is_zero(&self) -> bool { self.0.is_zero() } From ee3e6049a3870108e6562e14186409ea8be3ca94 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 21 Mar 2022 21:24:52 -0600 Subject: [PATCH 052/139] Make boundary-finding methods wrap across newlines This requires word and subword methods to explicitly acknowledge that they want to stop at newlines, which I think actually increases clarity. It makes the boundary finding method more general and useful for external callers such as the forthcoming vim crate. --- crates/editor/src/movement.rs | 192 +++++++++++++++++++++++----------- 1 file changed, 133 insertions(+), 59 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 72567312e1..5e6dd8c55c 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -133,101 +133,111 @@ pub fn line_end( } pub fn previous_word_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - find_boundary_reversed(map, point, |left, right| { - char_kind(left) != char_kind(right) && !right.is_whitespace() + find_preceding_boundary(map, point, |left, right| { + (char_kind(left) != char_kind(right) && !right.is_whitespace()) || left == '\n' }) } pub fn previous_subword_start(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - find_boundary_reversed(map, point, |left, right| { - (char_kind(left) != char_kind(right) - || left == '_' && right != '_' - || left.is_lowercase() && right.is_uppercase()) - && !right.is_whitespace() + find_preceding_boundary(map, point, |left, right| { + let is_word_start = char_kind(left) != char_kind(right) && !right.is_whitespace(); + let is_subword_start = + left == '_' && right != '_' || left.is_lowercase() && right.is_uppercase(); + is_word_start || is_subword_start || left == '\n' }) } pub fn next_word_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { find_boundary(map, point, |left, right| { - char_kind(left) != char_kind(right) && !left.is_whitespace() + (char_kind(left) != char_kind(right) && !left.is_whitespace()) || right == '\n' }) } pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { find_boundary(map, point, |left, right| { - (char_kind(left) != char_kind(right) - || left != '_' && right == '_' - || left.is_lowercase() && right.is_uppercase()) - && !left.is_whitespace() + let is_word_end = (char_kind(left) != char_kind(right)) && !left.is_whitespace(); + let is_subword_end = + left != '_' && right == '_' || left.is_lowercase() && right.is_uppercase(); + is_word_end || is_subword_end || right == '\n' }) } -pub fn find_boundary_reversed( +/// Scans for a boundary from the start of each line preceding the given end point until a boundary +/// is found, indicated by the given predicate returning true. The predicate is called with the +/// character to the left and right of the candidate boundary location, and will be called with `\n` +/// characters indicating the start or end of a line. If the predicate returns true multiple times +/// on a line, the *rightmost* boundary is returned. +pub fn find_preceding_boundary( map: &DisplaySnapshot, - mut start: DisplayPoint, - is_boundary: impl Fn(char, char) -> bool, + end: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { - let mut line_start = 0; - if start.row() > 0 { - if let Some(indent) = map.soft_wrap_indent(start.row() - 1) { - line_start = indent; - } - } - - if start.column() == line_start { - if start.row() == 0 { - return DisplayPoint::new(0, 0); - } else { - let row = start.row() - 1; - start = map.clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left); - } - } - - let mut boundary = DisplayPoint::new(start.row(), 0); - let mut column = 0; - let mut prev_ch = None; - for ch in map.chars_at(DisplayPoint::new(start.row(), 0)) { - if column >= start.column() { - break; - } - - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - *boundary.column_mut() = column; + let mut point = end; + loop { + *point.column_mut() = 0; + if point.row() > 0 { + if let Some(indent) = map.soft_wrap_indent(point.row() - 1) { + *point.column_mut() = indent; } } - prev_ch = Some(ch); - column += ch.len_utf8() as u32; - } - boundary -} + let mut boundary = None; + let mut prev_ch = if point.is_zero() { None } else { Some('\n') }; + for ch in map.chars_at(point) { + if point >= end { + break; + } + + if let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + boundary = Some(point); + } + } -pub fn find_boundary( - map: &DisplaySnapshot, - mut start: DisplayPoint, - is_boundary: impl Fn(char, char) -> bool, -) -> DisplayPoint { - let mut prev_ch = None; - for ch in map.chars_at(start) { - if let Some(prev_ch) = prev_ch { if ch == '\n' { break; } + + prev_ch = Some(ch); + *point.column_mut() += ch.len_utf8() as u32; + } + + if let Some(boundary) = boundary { + return boundary; + } else if point.row() == 0 { + return DisplayPoint::zero(); + } else { + *point.row_mut() -= 1; + } + } +} + +/// Scans for a boundary following the given start point until a boundary is found, indicated by the +/// given predicate returning true. The predicate is called with the character to the left and right +/// of the candidate boundary location, and will be called with `\n` characters indicating the start +/// or end of a line. +pub fn find_boundary( + map: &DisplaySnapshot, + mut point: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, +) -> DisplayPoint { + let mut prev_ch = None; + for ch in map.chars_at(point) { + if let Some(prev_ch) = prev_ch { if is_boundary(prev_ch, ch) { break; } } if ch == '\n' { - *start.row_mut() += 1; - *start.column_mut() = 0; + *point.row_mut() += 1; + *point.column_mut() = 0; } else { - *start.column_mut() += ch.len_utf8() as u32; + *point.column_mut() += ch.len_utf8() as u32; } prev_ch = Some(ch); } - map.clip_point(start, Bias::Right) + map.clip_point(point, Bias::Right) } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { @@ -271,7 +281,9 @@ mod tests { } assert("\n| |lorem", cx); + assert("|\n| lorem", cx); assert(" |lorem|", cx); + assert("| |lorem", cx); assert(" |lor|em", cx); assert("\nlorem\n| |ipsum", cx); assert("\n\n|\n|", cx); @@ -317,6 +329,37 @@ mod tests { assert(" ab|——|cd", cx); } + #[gpui::test] + fn test_find_preceding_boundary(cx: &mut gpui::MutableAppContext) { + fn assert( + marked_text: &str, + cx: &mut gpui::MutableAppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + find_preceding_boundary(&snapshot, display_points[1], is_boundary), + display_points[0] + ); + } + + assert("abc|def\ngh\nij|k", cx, |left, right| { + left == 'c' && right == 'd' + }); + assert("abcdef\n|gh\nij|k", cx, |left, right| { + left == '\n' && right == 'g' + }); + let mut line_count = 0; + assert("abcdef\n|gh\nij|k", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + #[gpui::test] fn test_next_word_end(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { @@ -372,6 +415,37 @@ mod tests { assert(" ab|——|cd", cx); } + #[gpui::test] + fn test_find_boundary(cx: &mut gpui::MutableAppContext) { + fn assert( + marked_text: &str, + cx: &mut gpui::MutableAppContext, + is_boundary: impl FnMut(char, char) -> bool, + ) { + let (snapshot, display_points) = marked_snapshot(marked_text, cx); + assert_eq!( + find_boundary(&snapshot, display_points[0], is_boundary), + display_points[1] + ); + } + + assert("abc|def\ngh\nij|k", cx, |left, right| { + left == 'j' && right == 'k' + }); + assert("ab|cdef\ngh\n|ijk", cx, |left, right| { + left == '\n' && right == 'i' + }); + let mut line_count = 0; + assert("abc|def\ngh\n|ijk", cx, |left, _| { + if left == '\n' { + line_count += 1; + line_count == 2 + } else { + false + } + }); + } + #[gpui::test] fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { From 17285512822c037e09d73a2eb23c0c1cfc046531 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 21:47:24 -0700 Subject: [PATCH 053/139] Always mirror the leader's selections when following --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 11 +++- crates/editor/src/items.rs | 117 +++++++++++++++++++++++++---------- crates/rpc/proto/zed.proto | 6 +- crates/server/src/rpc.rs | 19 ++---- 5 files changed, 103 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 80ed9bc512..8380eea12d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1568,7 +1568,7 @@ impl Editor { #[cfg(any(test, feature = "test-support"))] pub fn selected_ranges>( &self, - cx: &mut MutableAppContext, + cx: &AppContext, ) -> Vec> { self.local_selections::(cx) .iter() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 49d800d619..af02a353d4 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -939,8 +939,12 @@ impl Element for EditorElement { *contains_non_empty_selection |= !is_empty; } } + + // Render the local selections in the leader's color when following. + let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); + selections.insert( - view.replica_id(cx), + local_replica_id, local_selections .into_iter() .map(|selection| crate::Selection { @@ -958,6 +962,11 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + // The local selections match the leader's selections. + if Some(replica_id) == view.leader_replica_id { + continue; + } + selections .entry(replica_id) .or_insert(Vec::new()) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3fdaad8b8f..7afc79f8b3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,10 +1,10 @@ -use crate::{Anchor, Autoscroll, Editor, Event, NavigationData, ToOffset, ToPoint as _}; +use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::{Bias, Buffer, Diagnostic, File as _}; +use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; use std::{fmt::Write, path::PathBuf}; @@ -44,7 +44,23 @@ impl FollowableItem for Editor { }) .unwrap_or_else(|| { cx.add_view(pane.window_id(), |cx| { - Editor::for_buffer(buffer, Some(project), cx) + let mut editor = Editor::for_buffer(buffer, Some(project), cx); + let selections = { + let buffer = editor.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + state + .selections + .into_iter() + .filter_map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + }) + .collect::>() + }; + if !selections.is_empty() { + editor.set_selections(selections.into(), None, cx); + } + editor }) })) })) @@ -55,33 +71,12 @@ impl FollowableItem for Editor { leader_replica_id: Option, cx: &mut ViewContext, ) { - let prev_leader_replica_id = self.leader_replica_id; self.leader_replica_id = leader_replica_id; if self.leader_replica_id.is_some() { - self.show_local_selections = false; self.buffer.update(cx, |buffer, cx| { buffer.remove_active_selections(cx); }); } else { - self.show_local_selections = true; - if let Some(leader_replica_id) = prev_leader_replica_id { - let selections = self - .buffer - .read(cx) - .snapshot(cx) - .remote_selections_in_range(&(Anchor::min()..Anchor::max())) - .filter_map(|(replica_id, selections)| { - if replica_id == leader_replica_id { - Some(selections) - } else { - None - } - }) - .collect::>(); - if !selections.is_empty() { - self.set_selections(selections.into(), None, cx); - } - } self.buffer.update(cx, |buffer, cx| { if self.focused { buffer.set_active_selections(&self.selections, cx); @@ -99,6 +94,7 @@ impl FollowableItem for Editor { .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -108,12 +104,13 @@ impl FollowableItem for Editor { _: &AppContext, ) -> Option { match event { - Event::ScrollPositionChanged => { + Event::ScrollPositionChanged | Event::SelectionsChanged => { Some(update_view::Variant::Editor(update_view::Editor { scroll_top: self .scroll_top_anchor .as_ref() .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + selections: self.selections.iter().map(serialize_selection).collect(), })) } _ => None, @@ -127,25 +124,77 @@ impl FollowableItem for Editor { ) -> Result<()> { match message { update_view::Variant::Editor(message) => { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); + let excerpt_id = excerpt_id.clone(); + drop(buffer); + if let Some(anchor) = message.scroll_top { - let anchor = language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?; - let anchor = { - let buffer = self.buffer.read(cx); - let buffer = buffer.read(cx); - let (excerpt_id, _, _) = buffer.as_singleton().unwrap(); - buffer.anchor_in_excerpt(excerpt_id.clone(), anchor) - }; - self.set_scroll_top_anchor(Some(anchor), cx); + self.set_scroll_top_anchor( + Some(Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }), + cx, + ); } else { self.set_scroll_top_anchor(None, cx); } + + let selections = message + .selections + .into_iter() + .filter_map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + }) + .collect::>(); + if !selections.is_empty() { + self.set_selections(selections.into(), None, cx); + } } } Ok(()) } } +fn serialize_selection(selection: &Selection) -> proto::Selection { + proto::Selection { + id: selection.id as u64, + start: Some(language::proto::serialize_anchor( + &selection.start.text_anchor, + )), + end: Some(language::proto::serialize_anchor( + &selection.end.text_anchor, + )), + reversed: selection.reversed, + } +} + +fn deserialize_selection( + excerpt_id: &ExcerptId, + buffer_id: usize, + selection: proto::Selection, +) -> Option> { + Some(Selection { + id: selection.id as usize, + start: Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(selection.start?)?, + }, + end: Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(selection.end?)?, + }, + reversed: selection.reversed, + goal: SelectionGoal::None, + }) +} + impl Item for Editor { fn navigate(&mut self, data: Box, cx: &mut ViewContext) { if let Some(data) = data.downcast_ref::() { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbf1218ccd..487c7e01a5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -579,7 +579,8 @@ message UpdateView { } message Editor { - Anchor scroll_top = 1; + repeated Selection selections = 1; + Anchor scroll_top = 2; } } @@ -593,7 +594,8 @@ message View { message Editor { uint64 buffer_id = 1; - Anchor scroll_top = 2; + repeated Selection selections = 2; + Anchor scroll_top = 3; } } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index c6627ce7b8..e0f4147faf 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1083,8 +1083,8 @@ mod tests { }; use collections::BTreeMap; use editor::{ - self, Anchor, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, - Rename, ToOffset, ToggleCodeActions, Undo, + self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, + ToOffset, ToggleCodeActions, Undo, }; use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; use language::{ @@ -4324,18 +4324,13 @@ mod tests { }) .await; + // When client A selects something, client B does as well. editor_a1.update(cx_a, |editor, cx| { - editor.select_ranges([2..2], None, cx); + editor.select_ranges([1..1, 2..2], None, cx); }); editor_b1 .condition(cx_b, |editor, cx| { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let selection = snapshot - .remote_selections_in_range(&(Anchor::min()..Anchor::max())) - .next(); - selection.map_or(false, |selection| { - selection.1.start.to_offset(&snapshot) == 2 - }) + editor.selected_ranges(cx) == vec![1..1, 2..2] }) .await; @@ -4343,10 +4338,6 @@ mod tests { workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); - editor_b1.update(cx_b, |editor, cx| { - assert_eq!(editor.selected_ranges::(cx), &[2..2]); - }); - workspace_a.update(cx_a, |workspace, cx| { workspace.activate_item(&editor_a2, cx); editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); From c550fc3f01c9fad44b0358c3949eb41cefda8851 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Mar 2022 21:52:28 -0700 Subject: [PATCH 054/139] WIP - Start work on unfollowing automatically --- crates/editor/src/items.rs | 4 ++++ crates/workspace/src/workspace.rs | 16 ++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7afc79f8b3..d0457eba5a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -158,6 +158,10 @@ impl FollowableItem for Editor { } Ok(()) } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + false + } } fn serialize_selection(selection: &Selection) -> proto::Selection { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 387bfa9b6e..5ce61824e3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -270,6 +270,7 @@ pub trait FollowableItem: Item { message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { @@ -285,6 +286,7 @@ pub trait FollowableItemHandle: ItemHandle { message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()>; + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool; } impl FollowableItemHandle for ViewHandle { @@ -313,6 +315,14 @@ impl FollowableItemHandle for ViewHandle { ) -> Result<()> { self.update(cx, |this, cx| this.apply_update_message(message, cx)) } + + fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { + if let Some(event) = event.downcast_ref() { + T::should_unfollow_on_event(event, cx) + } else { + false + } + } } pub trait ItemHandle: 'static + fmt::Debug { @@ -421,6 +431,12 @@ impl ItemHandle for ViewHandle { return; }; + if let Some(item) = item.to_followable_item_handle(cx) { + if item.should_unfollow_on_event(event, cx) { + workspace.unfollow(&pane, cx); + } + } + if T::should_close_item_on_event(event) { pane.update(cx, |pane, cx| pane.close_item(item.id(), cx)); return; From 31175545685efe094e4b253b3280eae32c02f69d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 09:16:25 +0100 Subject: [PATCH 055/139] Automatically unfollow when editing, scrolling or changing selections --- crates/editor/src/editor.rs | 44 ++++-- crates/editor/src/items.rs | 18 ++- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/language/src/buffer.rs | 19 +-- crates/language/src/tests.rs | 12 +- crates/outline/src/outline.rs | 2 +- crates/project/src/project.rs | 13 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 6 +- crates/search/src/project_search.rs | 2 +- crates/server/src/rpc.rs | 148 ++++++++++++++++-- crates/theme_selector/src/theme_selector.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 14 files changed, 214 insertions(+), 60 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8380eea12d..bffc1a2186 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1035,14 +1035,19 @@ impl Editor { self.scroll_top_anchor = Some(anchor); } - cx.emit(Event::ScrollPositionChanged); + cx.emit(Event::ScrollPositionChanged { local: true }); cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Option, cx: &mut ViewContext) { + fn set_scroll_top_anchor( + &mut self, + anchor: Option, + local: bool, + cx: &mut ViewContext, + ) { self.scroll_position = Vector2F::zero(); self.scroll_top_anchor = anchor; - cx.emit(Event::ScrollPositionChanged); + cx.emit(Event::ScrollPositionChanged { local }); cx.notify(); } @@ -1267,7 +1272,7 @@ impl Editor { _ => {} } - self.set_selections(self.selections.clone(), Some(pending), cx); + self.set_selections(self.selections.clone(), Some(pending), true, cx); } fn begin_selection( @@ -1347,7 +1352,12 @@ impl Editor { } else { selections = Arc::from([]); } - self.set_selections(selections, Some(PendingSelection { selection, mode }), cx); + self.set_selections( + selections, + Some(PendingSelection { selection, mode }), + true, + cx, + ); cx.notify(); } @@ -1461,7 +1471,7 @@ impl Editor { pending.selection.end = buffer.anchor_before(head); pending.selection.reversed = false; } - self.set_selections(self.selections.clone(), Some(pending), cx); + self.set_selections(self.selections.clone(), Some(pending), true, cx); } else { log::error!("update_selection dispatched with no pending selection"); return; @@ -1548,7 +1558,7 @@ impl Editor { if selections.is_empty() { selections = Arc::from([pending.selection]); } - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); self.request_autoscroll(Autoscroll::Fit, cx); } else { let mut oldest_selection = self.oldest_selection::(&cx); @@ -1895,7 +1905,7 @@ impl Editor { } drop(snapshot); - self.set_selections(selections.into(), None, cx); + self.set_selections(selections.into(), None, true, cx); true } } else { @@ -3294,7 +3304,7 @@ impl Editor { pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); } @@ -3303,7 +3313,7 @@ impl Editor { pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { - self.set_selections(selections, None, cx); + self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); } @@ -4967,6 +4977,7 @@ impl Editor { } })), None, + true, cx, ); } @@ -5027,6 +5038,7 @@ impl Editor { &mut self, selections: Arc<[Selection]>, pending_selection: Option, + local: bool, cx: &mut ViewContext, ) { assert!( @@ -5095,7 +5107,7 @@ impl Editor { self.refresh_document_highlights(cx); self.pause_cursor_blinking(cx); - cx.emit(Event::SelectionsChanged); + cx.emit(Event::SelectionsChanged { local }); } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { @@ -5508,10 +5520,10 @@ impl Editor { cx: &mut ViewContext, ) { match event { - language::Event::Edited => { + language::Event::Edited { local } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); - cx.emit(Event::Edited); + cx.emit(Event::Edited { local: *local }); } language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), @@ -5638,13 +5650,13 @@ fn compute_scroll_position( #[derive(Copy, Clone)] pub enum Event { Activate, - Edited, + Edited { local: bool }, Blurred, Dirtied, Saved, TitleChanged, - SelectionsChanged, - ScrollPositionChanged, + SelectionsChanged { local: bool }, + ScrollPositionChanged { local: bool }, Closed, } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d0457eba5a..eceaa88159 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -58,7 +58,7 @@ impl FollowableItem for Editor { .collect::>() }; if !selections.is_empty() { - editor.set_selections(selections.into(), None, cx); + editor.set_selections(selections.into(), None, false, cx); } editor }) @@ -104,7 +104,7 @@ impl FollowableItem for Editor { _: &AppContext, ) -> Option { match event { - Event::ScrollPositionChanged | Event::SelectionsChanged => { + Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { Some(update_view::Variant::Editor(update_view::Editor { scroll_top: self .scroll_top_anchor @@ -138,10 +138,11 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }), + false, cx, ); } else { - self.set_scroll_top_anchor(None, cx); + self.set_scroll_top_anchor(None, false, cx); } let selections = message @@ -152,15 +153,20 @@ impl FollowableItem for Editor { }) .collect::>(); if !selections.is_empty() { - self.set_selections(selections.into(), None, cx); + self.set_selections(selections.into(), None, false, cx); } } } Ok(()) } - fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { - false + fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { + match event { + Event::Edited { local } => *local, + Event::SelectionsChanged { local } => *local, + Event::ScrollPositionChanged { local } => *local, + _ => false, + } } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index ca41eb74a1..4656daa4b3 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -291,7 +291,7 @@ impl FileFinder { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index f2dd4e76b1..ce8ba787a8 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -102,7 +102,7 @@ impl GoToLine { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => { + editor::Event::Edited { .. } => { let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text(); let mut components = line_editor.trim().split(&[',', ':'][..]); let row = components.next().and_then(|row| row.parse::().ok()); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index fa4208ee5b..9da9e59e4c 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -142,7 +142,7 @@ pub enum Operation { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { Operation(Operation), - Edited, + Edited { local: bool }, Dirtied, Saved, FileHandleChanged, @@ -968,7 +968,7 @@ impl Buffer { ) -> Option { if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) { let was_dirty = start_version != self.saved_version; - self.did_edit(&start_version, was_dirty, cx); + self.did_edit(&start_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1161,6 +1161,7 @@ impl Buffer { &mut self, old_version: &clock::Global, was_dirty: bool, + local: bool, cx: &mut ModelContext, ) { if self.edits_since::(old_version).next().is_none() { @@ -1169,7 +1170,7 @@ impl Buffer { self.reparse(cx); - cx.emit(Event::Edited); + cx.emit(Event::Edited { local }); if !was_dirty { cx.emit(Event::Dirtied); } @@ -1206,7 +1207,7 @@ impl Buffer { self.text.apply_ops(buffer_ops)?; self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, false, cx); // Notify independently of whether the buffer was edited as the operations could include a // selection update. cx.notify(); @@ -1321,7 +1322,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.undo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1342,7 +1343,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if undone { - self.did_edit(&old_version, was_dirty, cx) + self.did_edit(&old_version, was_dirty, true, cx) } undone } @@ -1353,7 +1354,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.redo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); Some(transaction_id) } else { None @@ -1374,7 +1375,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if redone { - self.did_edit(&old_version, was_dirty, cx) + self.did_edit(&old_version, was_dirty, true, cx) } redone } @@ -1440,7 +1441,7 @@ impl Buffer { if !ops.is_empty() { for op in ops { self.send_operation(Operation::Buffer(op), cx); - self.did_edit(&old_version, was_dirty, cx); + self.did_edit(&old_version, was_dirty, true, cx); } } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 6c9980b334..d36771c44f 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -122,11 +122,19 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer_1_events = buffer_1_events.borrow(); assert_eq!( *buffer_1_events, - vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] + vec![ + Event::Edited { local: true }, + Event::Dirtied, + Event::Edited { local: true }, + Event::Edited { local: true } + ] ); let buffer_2_events = buffer_2_events.borrow(); - assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); + assert_eq!( + *buffer_2_events, + vec![Event::Edited { local: false }, Event::Dirtied] + ); } #[gpui::test] diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index fd4c8ff60b..968fceb59c 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -224,7 +224,7 @@ impl OutlineView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::Edited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 4a6f0dd6cf..5f9a63c034 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1178,7 +1178,7 @@ impl Project { }); cx.background().spawn(request).detach_and_log_err(cx); } - BufferEvent::Edited => { + BufferEvent::Edited { .. } => { let language_server = self .language_server_for_buffer(buffer.read(cx), cx)? .clone(); @@ -6227,7 +6227,10 @@ mod tests { assert!(buffer.is_dirty()); assert_eq!( *events.borrow(), - &[language::Event::Edited, language::Event::Dirtied] + &[ + language::Event::Edited { local: true }, + language::Event::Dirtied + ] ); events.borrow_mut().clear(); buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx); @@ -6250,9 +6253,9 @@ mod tests { assert_eq!( *events.borrow(), &[ - language::Event::Edited, + language::Event::Edited { local: true }, language::Event::Dirtied, - language::Event::Edited, + language::Event::Edited { local: true }, ], ); events.borrow_mut().clear(); @@ -6264,7 +6267,7 @@ mod tests { assert!(buffer.is_dirty()); }); - assert_eq!(*events.borrow(), &[language::Event::Edited]); + assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]); // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 27e125a592..5eb04718d7 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,7 +328,7 @@ impl ProjectSymbolsView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited => self.update_matches(cx), + editor::Event::Edited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 8eae666c45..13c73036f4 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -360,7 +360,7 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { self.query_contains_error = false; self.clear_matches(cx); self.update_matches(true, cx); @@ -377,8 +377,8 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => self.update_matches(false, cx), - editor::Event::SelectionsChanged => self.update_match_index(cx), + editor::Event::Edited { .. } => self.update_matches(false, cx), + editor::Event::SelectionsChanged { .. } => self.update_match_index(cx), _ => {} } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f027c965c6..1302040d19 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -350,7 +350,7 @@ impl ProjectSearchView { cx.observe(&results_editor, |_, _, cx| cx.emit(ViewEvent::UpdateTab)) .detach(); cx.subscribe(&results_editor, |this, _, event, cx| { - if matches!(event, editor::Event::SelectionsChanged) { + if matches!(event, editor::Event::SelectionsChanged { .. }) { this.update_match_index(cx); } }) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index e0f4147faf..761b5737ce 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1086,7 +1086,7 @@ mod tests { self, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, Input, Redo, Rename, ToOffset, ToggleCodeActions, Undo, }; - use gpui::{executor, ModelHandle, TestAppContext, ViewHandle}; + use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle}; use language::{ tree_sitter_rust, Diagnostic, DiagnosticEntry, Language, LanguageConfig, LanguageRegistry, LanguageServerConfig, OffsetRangeExt, Point, ToLspPosition, @@ -4308,11 +4308,6 @@ mod tests { .project_path(cx)), Some((worktree_id, "2.txt").into()) ); - let editor_b2 = workspace_b - .read_with(cx_b, |workspace, cx| workspace.active_item(cx)) - .unwrap() - .downcast::() - .unwrap(); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { @@ -4324,7 +4319,7 @@ mod tests { }) .await; - // When client A selects something, client B does as well. + // Changes to client A's editor are reflected on client B. editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([1..1, 2..2], None, cx); }); @@ -4334,17 +4329,26 @@ mod tests { }) .await; + editor_a1.update(cx_a, |editor, cx| editor.set_text("TWO", cx)); + editor_b1 + .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") + .await; + + editor_a1.update(cx_a, |editor, cx| { + editor.select_ranges([3..3], None, cx); + }); + editor_b1 + .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) + .await; + // After unfollowing, client B stops receiving updates from client A. workspace_b.update(cx_b, |workspace, cx| { workspace.unfollow(&workspace.active_pane().clone(), cx) }); workspace_a.update(cx_a, |workspace, cx| { - workspace.activate_item(&editor_a2, cx); - editor_a2.update(cx, |editor, cx| editor.set_text("TWO", cx)); + workspace.activate_item(&editor_a2, cx) }); - editor_b2 - .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") - .await; + cx_a.foreground().run_until_parked(); assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace .active_item(cx) @@ -4456,6 +4460,126 @@ mod tests { ); } + #[gpui::test(iterations = 10)] + async fn test_auto_unfollowing(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + cx_a.foreground().forbid_parking(); + let fs = FakeFs::new(cx_a.background()); + + // 2 clients connect to a server. + let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await; + let mut client_a = server.create_client(cx_a, "user_a").await; + let mut client_b = server.create_client(cx_b, "user_b").await; + cx_a.update(editor::init); + cx_b.update(editor::init); + + // Client A shares a project. + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "1.txt": "one", + "2.txt": "two", + "3.txt": "three", + }), + ) + .await; + let (project_a, worktree_id) = client_a.build_local_project(fs.clone(), "/a", cx_a).await; + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + + // Client B joins the project. + let project_b = client_b + .build_remote_project( + project_a + .read_with(cx_a, |project, _| project.remote_id()) + .unwrap(), + cx_b, + ) + .await; + + // Client A opens some editors. + let workspace_a = client_a.build_workspace(&project_a, cx_a); + let _editor_a1 = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id, "1.txt"), cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + // Client B starts following client A. + let workspace_b = client_b.build_workspace(&project_b, cx_b); + let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); + let leader_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace + .active_item(cx) + .unwrap() + .downcast::() + .unwrap() + }); + + // When client B moves, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.move_right(&editor::MoveRight, cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B edits, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| editor.insert("X", cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B scrolls, it automatically stops following client A. + editor_b2.update(cx_b, |editor, cx| { + editor.set_scroll_position(vec2f(0., 3.), cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); + } + #[gpui::test(iterations = 100)] async fn test_random_collaboration(cx: &mut TestAppContext, rng: StdRng) { cx.foreground().forbid_parking(); diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index f879940f21..ebdcc492a9 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -204,7 +204,7 @@ impl ThemeSelector { cx: &mut ViewContext, ) { match event { - editor::Event::Edited => { + editor::Event::Edited { .. } => { self.update_matches(cx); self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5ce61824e3..cb9f7e7fed 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1750,7 +1750,7 @@ impl Workspace { None } - fn leader_for_pane(&self, pane: &ViewHandle) -> Option { + pub fn leader_for_pane(&self, pane: &ViewHandle) -> Option { self.follower_states_by_leader .iter() .find_map(|(leader_id, state)| { From 67dbc3117d8f0673d981a2f63eda612eaa938eb4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 09:42:37 +0100 Subject: [PATCH 056/139] Stop following when activating a different item on the follower pane --- crates/server/src/rpc.rs | 38 +++++++++++++++++++++++++++++++ crates/workspace/src/pane.rs | 19 +++++++++------- crates/workspace/src/workspace.rs | 15 ++++++++---- 3 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 761b5737ce..047912c0ff 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4578,6 +4578,44 @@ mod tests { workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), None ); + + workspace_b + .update(cx_b, |workspace, cx| { + workspace.toggle_follow(&leader_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different pane, it continues following client A in the original pane. + workspace_b.update(cx_b, |workspace, cx| { + workspace.split_pane(pane_b.clone(), SplitDirection::Right, cx) + }); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + workspace_b.update(cx_b, |workspace, cx| workspace.activate_next_pane(cx)); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + Some(leader_id) + ); + + // When client B activates a different item in the original pane, it automatically stops following client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id, "2.txt"), cx) + }) + .await + .unwrap(); + assert_eq!( + workspace_b.read_with(cx_b, |workspace, _| workspace.leader_for_pane(&pane_b)), + None + ); } #[gpui::test(iterations = 100)] diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 367cc967fc..ab54b0053e 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -33,7 +33,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { - pane.activate_item(action.0, cx); + pane.activate_item(action.0, true, cx); }); cx.add_action(|pane: &mut Pane, _: &ActivatePrevItem, cx| { pane.activate_prev_item(cx); @@ -92,6 +92,7 @@ pub fn init(cx: &mut MutableAppContext) { pub enum Event { Activate, + ActivateItem { local: bool }, Remove, Split(SplitDirection), } @@ -301,7 +302,7 @@ impl Pane { for (ix, item) in pane.items.iter().enumerate() { if item.project_entry_id(cx) == Some(project_entry_id) { let item = item.boxed_clone(); - pane.activate_item(ix, cx); + pane.activate_item(ix, true, cx); return Some(item); } } @@ -311,7 +312,7 @@ impl Pane { existing_item } else { let item = build_item(cx); - Self::add_item(workspace, pane, item.boxed_clone(), cx); + Self::add_item(workspace, pane, item.boxed_clone(), true, cx); item } } @@ -320,11 +321,12 @@ impl Pane { workspace: &mut Workspace, pane: ViewHandle, item: Box, + local: bool, cx: &mut ViewContext, ) { // Prevent adding the same item to the pane more than once. if let Some(item_ix) = pane.read(cx).items.iter().position(|i| i.id() == item.id()) { - pane.update(cx, |pane, cx| pane.activate_item(item_ix, cx)); + pane.update(cx, |pane, cx| pane.activate_item(item_ix, local, cx)); return; } @@ -333,7 +335,7 @@ impl Pane { pane.update(cx, |pane, cx| { let item_idx = cmp::min(pane.active_item_index + 1, pane.items.len()); pane.items.insert(item_idx, item); - pane.activate_item(item_idx, cx); + pane.activate_item(item_idx, local, cx); cx.notify(); }); } @@ -384,13 +386,14 @@ impl Pane { self.items.iter().position(|i| i.id() == item.id()) } - pub fn activate_item(&mut self, index: usize, cx: &mut ViewContext) { + pub fn activate_item(&mut self, index: usize, local: bool, cx: &mut ViewContext) { if index < self.items.len() { let prev_active_item_ix = mem::replace(&mut self.active_item_index, index); if prev_active_item_ix != self.active_item_index && prev_active_item_ix < self.items.len() { self.items[prev_active_item_ix].deactivated(cx); + cx.emit(Event::ActivateItem { local }); } self.update_active_toolbar(cx); self.focus_active_item(cx); @@ -406,7 +409,7 @@ impl Pane { } else if self.items.len() > 0 { index = self.items.len() - 1; } - self.activate_item(index, cx); + self.activate_item(index, true, cx); } pub fn activate_next_item(&mut self, cx: &mut ViewContext) { @@ -416,7 +419,7 @@ impl Pane { } else { index = 0; } - self.activate_item(index, cx); + self.activate_item(index, true, cx); } pub fn close_active_item(&mut self, cx: &mut ViewContext) { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index cb9f7e7fed..9c8da079ae 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -445,7 +445,7 @@ impl ItemHandle for ViewHandle { if T::should_activate_item_on_event(event) { pane.update(cx, |pane, cx| { if let Some(ix) = pane.index_for_item(&item) { - pane.activate_item(ix, cx); + pane.activate_item(ix, true, cx); pane.activate(cx); } }); @@ -1022,7 +1022,7 @@ impl Workspace { pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { let pane = self.active_pane().clone(); - Pane::add_item(self, pane, item, cx); + Pane::add_item(self, pane, item, true, cx); } pub fn open_path( @@ -1111,7 +1111,7 @@ impl Workspace { }); if let Some((pane, ix)) = result { self.activate_pane(pane.clone(), cx); - pane.update(cx, |pane, cx| pane.activate_item(ix, cx)); + pane.update(cx, |pane, cx| pane.activate_item(ix, true, cx)); true } else { false @@ -1164,6 +1164,11 @@ impl Workspace { pane::Event::Activate => { self.activate_pane(pane, cx); } + pane::Event::ActivateItem { local } => { + if *local { + self.unfollow(&pane, cx); + } + } } } else { error!("pane {} not found", pane_id); @@ -1180,7 +1185,7 @@ impl Workspace { 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()) { - Pane::add_item(self, new_pane.clone(), clone, cx); + Pane::add_item(self, new_pane.clone(), clone, true, cx); } } self.center.split(&pane, &new_pane, direction).unwrap(); @@ -1793,7 +1798,7 @@ impl Workspace { } for (pane, item) in items_to_add { - Pane::add_item(self, pane.clone(), item.boxed_clone(), cx); + Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); cx.notify(); } None From 7d566ce455a919e123331af5b147af4c1f468a6b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:16:58 +0100 Subject: [PATCH 057/139] Follow last collaborator or the next one via `cmd-alt-shift-f` --- crates/workspace/src/workspace.rs | 40 ++++++++++++++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 9c8da079ae..35f3ff9a0c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -70,6 +70,7 @@ action!(OpenNew, Arc); action!(OpenPaths, OpenParams); action!(ToggleShare); action!(ToggleFollow, PeerId); +action!(FollowNextCollaborator); action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); @@ -92,6 +93,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_share); cx.add_async_action(Workspace::toggle_follow); + cx.add_async_action(Workspace::follow_next_collaborator); cx.add_action( |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { let pane = workspace.active_pane().clone(); @@ -107,6 +109,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ + Binding::new("cmd-alt-shift-F", FollowNextCollaborator, None), Binding::new("cmd-alt-shift-U", Unfollow, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), @@ -630,6 +633,7 @@ pub struct Workspace { project: ModelHandle, leader_state: LeaderState, follower_states_by_leader: FollowerStatesByLeader, + last_leaders_by_pane: HashMap, PeerId>, _observe_current_user: Task<()>, } @@ -725,6 +729,7 @@ impl Workspace { project: params.project.clone(), leader_state: Default::default(), follower_states_by_leader: Default::default(), + last_leaders_by_pane: Default::default(), _observe_current_user, }; this.project_remote_id_changed(this.project.read(cx).remote_id(), cx); @@ -1245,15 +1250,17 @@ impl Workspace { if let Some(prev_leader_id) = self.unfollow(&pane, cx) { if leader_id == prev_leader_id { - cx.notify(); return None; } } + self.last_leaders_by_pane + .insert(pane.downgrade(), leader_id); self.follower_states_by_leader .entry(leader_id) .or_default() .insert(pane.clone(), Default::default()); + cx.notify(); let project_id = self.project.read(cx).remote_id()?; let request = self.client.request(proto::Follow { @@ -1279,6 +1286,37 @@ impl Workspace { })) } + pub fn follow_next_collaborator( + &mut self, + _: &FollowNextCollaborator, + cx: &mut ViewContext, + ) -> Option>> { + let collaborators = self.project.read(cx).collaborators(); + let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + let mut collaborators = collaborators.keys().copied(); + while let Some(peer_id) = collaborators.next() { + if peer_id == leader_id { + break; + } + } + collaborators.next() + } else if let Some(last_leader_id) = + self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + { + if collaborators.contains_key(last_leader_id) { + Some(*last_leader_id) + } else { + None + } + } else { + None + }; + + next_leader_id + .or_else(|| collaborators.keys().copied().next()) + .and_then(|leader_id| self.toggle_follow(&ToggleFollow(leader_id), cx)) + } + pub fn unfollow( &mut self, pane: &ViewHandle, From 34e5a1f6bb20fc0217dccecdec34a63af84fd0fc Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:31:28 +0100 Subject: [PATCH 058/139] Always render local selections on top of remote ones --- crates/editor/src/element.rs | 58 ++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index af02a353d4..0daf8f2fc2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -909,7 +909,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections = HashMap::default(); + let mut selections = Vec::new(); let mut active_rows = BTreeMap::new(); let mut highlighted_rows = None; let mut highlighted_ranges = Vec::new(); @@ -922,11 +922,32 @@ impl Element for EditorElement { &display_map, ); + let mut remote_selections = HashMap::default(); + for (replica_id, selection) in display_map + .buffer_snapshot + .remote_selections_in_range(&(start_anchor.clone()..end_anchor.clone())) + { + // The local selections match the leader's selections. + if Some(replica_id) == view.leader_replica_id { + continue; + } + + remote_selections + .entry(replica_id) + .or_insert(Vec::new()) + .push(crate::Selection { + id: selection.id, + goal: selection.goal, + reversed: selection.reversed, + start: selection.start.to_display_point(&display_map), + end: selection.end.to_display_point(&display_map), + }); + } + selections.extend(remote_selections); + if view.show_local_selections { - let local_selections = view.local_selections_in_range( - start_anchor.clone()..end_anchor.clone(), - &display_map, - ); + let local_selections = + view.local_selections_in_range(start_anchor..end_anchor, &display_map); for selection in &local_selections { let is_empty = selection.start == selection.end; let selection_start = snapshot.prev_line_boundary(selection.start).1; @@ -943,7 +964,7 @@ impl Element for EditorElement { // Render the local selections in the leader's color when following. let local_replica_id = view.leader_replica_id.unwrap_or(view.replica_id(cx)); - selections.insert( + selections.push(( local_replica_id, local_selections .into_iter() @@ -955,28 +976,7 @@ impl Element for EditorElement { end: selection.end.to_display_point(&display_map), }) .collect(), - ); - } - - for (replica_id, selection) in display_map - .buffer_snapshot - .remote_selections_in_range(&(start_anchor..end_anchor)) - { - // The local selections match the leader's selections. - if Some(replica_id) == view.leader_replica_id { - continue; - } - - selections - .entry(replica_id) - .or_insert(Vec::new()) - .push(crate::Selection { - id: selection.id, - goal: selection.goal, - reversed: selection.reversed, - start: selection.start.to_display_point(&display_map), - end: selection.end.to_display_point(&display_map), - }); + )); } }); @@ -1222,7 +1222,7 @@ pub struct LayoutState { em_width: f32, em_advance: f32, highlighted_ranges: Vec<(Range, Color)>, - selections: HashMap>>, + selections: Vec<(ReplicaId, Vec>)>, context_menu: Option<(DisplayPoint, ElementBox)>, code_actions_indicator: Option<(u32, ElementBox)>, } From 5dc36260e511d87b44193f174fe46ac7edcab170 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 10:51:17 +0100 Subject: [PATCH 059/139] Reflect leader's view state when recycling existing local editors --- crates/editor/src/items.rs | 61 ++++++++++++++++++++++++++------------ crates/server/src/rpc.rs | 24 +++++++++++---- 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index eceaa88159..3f8bb2eab3 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -36,7 +36,7 @@ impl FollowableItem for Editor { }); Some(cx.spawn(|mut cx| async move { let buffer = buffer.await?; - Ok(pane + let editor = pane .read_with(&cx, |pane, cx| { pane.items_of_type::().find(|editor| { editor.read(cx).buffer.read(cx).as_singleton().as_ref() == Some(&buffer) @@ -44,25 +44,48 @@ impl FollowableItem for Editor { }) .unwrap_or_else(|| { cx.add_view(pane.window_id(), |cx| { - let mut editor = Editor::for_buffer(buffer, Some(project), cx); - let selections = { - let buffer = editor.buffer.read(cx); - let buffer = buffer.read(cx); - let (excerpt_id, buffer_id, _) = buffer.as_singleton().unwrap(); - state - .selections - .into_iter() - .filter_map(|selection| { - deserialize_selection(&excerpt_id, buffer_id, selection) - }) - .collect::>() - }; - if !selections.is_empty() { - editor.set_selections(selections.into(), None, false, cx); - } - editor + Editor::for_buffer(buffer, Some(project), cx) }) - })) + }); + editor.update(&mut cx, |editor, cx| { + let excerpt_id; + let buffer_id; + { + let buffer = editor.buffer.read(cx).read(cx); + let singleton = buffer.as_singleton().unwrap(); + excerpt_id = singleton.0.clone(); + buffer_id = singleton.1; + } + let selections = state + .selections + .into_iter() + .map(|selection| { + deserialize_selection(&excerpt_id, buffer_id, selection) + .ok_or_else(|| anyhow!("invalid selection")) + }) + .collect::>>()?; + if !selections.is_empty() { + editor.set_selections(selections.into(), None, false, cx); + } + + if let Some(anchor) = state.scroll_top { + editor.set_scroll_top_anchor( + Some(Anchor { + buffer_id: Some(state.buffer_id as usize), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }), + false, + cx, + ); + } else { + editor.set_scroll_top_anchor(None, false, cx); + } + + Ok::<_, anyhow::Error>(()) + })?; + Ok(editor) })) } diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 047912c0ff..3f149390d3 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1116,7 +1116,7 @@ mod tests { }, time::Duration, }; - use workspace::{Settings, SplitDirection, Workspace, WorkspaceParams}; + use workspace::{Item, Settings, SplitDirection, Workspace, WorkspaceParams}; #[cfg(test)] #[ctor::ctor] @@ -4287,7 +4287,9 @@ mod tests { .downcast::() .unwrap(); - // Client B starts following client A. + // When client B starts following client A, all visible view states are replicated to client B. + editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx)); + editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx)); workspace_b .update(cx_b, |workspace, cx| { let leader_id = project_b @@ -4301,13 +4303,25 @@ mod tests { }) .await .unwrap(); - assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace + let editor_b2 = workspace_b.read_with(cx_b, |workspace, cx| { + workspace .active_item(cx) .unwrap() - .project_path(cx)), + .downcast::() + .unwrap() + }); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)), Some((worktree_id, "2.txt").into()) ); + assert_eq!( + editor_b2.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)), + vec![2..3] + ); + assert_eq!( + editor_b1.read_with(cx_b, |editor, cx| editor.selected_ranges(cx)), + vec![0..1] + ); // When client A activates a different editor, client B does so as well. workspace_a.update(cx_a, |workspace, cx| { From ffaf409a3111df1d6d70d4c4bd791f0a890f28da Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 11:06:52 +0100 Subject: [PATCH 060/139] Forget last pane's leader when such pane is removed This is just a memory optimization and doesn't cause any observable change in behavior. --- crates/workspace/src/workspace.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 35f3ff9a0c..1f0a89a083 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1203,6 +1203,7 @@ impl Workspace { self.panes.retain(|p| p != &pane); self.activate_pane(self.panes.last().unwrap().clone(), cx); self.unfollow(&pane, cx); + self.last_leaders_by_pane.remove(&pane.downgrade()); cx.notify(); } } From e5a99cf8cd01e9e977aa2f9442e081574f515d66 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 11:15:39 +0100 Subject: [PATCH 061/139] Stop following when leader disconnects --- crates/project/src/project.rs | 2 ++ crates/server/src/rpc.rs | 44 +++++++++++++++++++++++++------ crates/workspace/src/workspace.rs | 24 +++++++++++++++-- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 5f9a63c034..c3c2f4e2a0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -125,6 +125,7 @@ pub enum Event { DiskBasedDiagnosticsFinished, DiagnosticsUpdated(ProjectPath), RemoteIdChanged(Option), + CollaboratorLeft(PeerId), } enum LanguageServerEvent { @@ -3368,6 +3369,7 @@ impl Project { buffer.update(cx, |buffer, cx| buffer.remove_peer(replica_id, cx)); } } + cx.emit(Event::CollaboratorLeft(peer_id)); cx.notify(); Ok(()) }) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 3f149390d3..3274e70d61 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4259,6 +4259,7 @@ mod tests { // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); + let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4287,19 +4288,19 @@ mod tests { .downcast::() .unwrap(); + let client_a_id = project_b.read_with(cx_b, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + let client_b_id = project_a.read_with(cx_a, |project, _| { + project.collaborators().values().next().unwrap().peer_id + }); + // When client B starts following client A, all visible view states are replicated to client B. editor_a1.update(cx_a, |editor, cx| editor.select_ranges([0..1], None, cx)); editor_a2.update(cx_a, |editor, cx| editor.select_ranges([2..3], None, cx)); workspace_b .update(cx_b, |workspace, cx| { - let leader_id = project_b - .read(cx) - .collaborators() - .values() - .next() - .unwrap() - .peer_id; - workspace.toggle_follow(&leader_id.into(), cx).unwrap() + workspace.toggle_follow(&client_a_id.into(), cx).unwrap() }) .await .unwrap(); @@ -4370,6 +4371,33 @@ mod tests { .id()), editor_b1.id() ); + + // Client A starts following client B. + workspace_a + .update(cx_a, |workspace, cx| { + workspace.toggle_follow(&client_b_id.into(), cx).unwrap() + }) + .await + .unwrap(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + Some(client_b_id) + ); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, cx| workspace + .active_item(cx) + .unwrap() + .id()), + editor_a1.id() + ); + + // Following interrupts when client B disconnects. + client_b.disconnect(&cx_b.to_async()).unwrap(); + cx_a.foreground().run_until_parked(); + assert_eq!( + workspace_a.read_with(cx_a, |workspace, _| workspace.leader_for_pane(&pane_a)), + None + ); } #[gpui::test(iterations = 10)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1f0a89a083..353c3f2510 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -667,8 +667,14 @@ impl Workspace { .detach(); cx.subscribe(¶ms.project, move |this, project, event, cx| { - if let project::Event::RemoteIdChanged(remote_id) = event { - this.project_remote_id_changed(*remote_id, cx); + match event { + project::Event::RemoteIdChanged(remote_id) => { + this.project_remote_id_changed(*remote_id, cx); + } + project::Event::CollaboratorLeft(peer_id) => { + this.collaborator_left(*peer_id, cx); + } + _ => {} } if project.read(cx).is_read_only() { cx.blur(); @@ -1241,6 +1247,20 @@ impl Workspace { } } + fn collaborator_left(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + self.leader_state.followers.remove(&peer_id); + if let Some(states_by_pane) = self.follower_states_by_leader.remove(&peer_id) { + for state in states_by_pane.into_values() { + for item in state.items_by_leader_view_id.into_values() { + if let FollowerItem::Loaded(item) = item { + item.set_leader_replica_id(None, cx); + } + } + } + } + cx.notify(); + } + pub fn toggle_follow( &mut self, ToggleFollow(leader_id): &ToggleFollow, From 381c82714bdf9aaef67d8d55b0471206933b4c70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 13:19:07 +0100 Subject: [PATCH 062/139] Bump protocol version --- crates/rpc/src/rpc.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index e937f6daaf..cfe780d511 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -5,4 +5,4 @@ pub mod proto; pub use conn::Connection; pub use peer::*; -pub const PROTOCOL_VERSION: u32 = 11; +pub const PROTOCOL_VERSION: u32 = 12; From 284a446be7fb0d794913e9517af9315b9e0c5ffa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 13:35:37 +0100 Subject: [PATCH 063/139] WIP --- crates/server/src/rpc.rs | 22 +++++++++++++++++++++- crates/workspace/src/pane.rs | 6 ++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 3274e70d61..9527df8e1f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4420,6 +4420,7 @@ mod tests { "1.txt": "one", "2.txt": "two", "3.txt": "three", + "4.txt": "four", }), ) .await; @@ -4441,6 +4442,7 @@ mod tests { // Client A opens some editors. let workspace_a = client_a.build_workspace(&project_a, cx_a); + let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), cx) @@ -4452,6 +4454,7 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); + let pane_b1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) @@ -4465,6 +4468,7 @@ mod tests { workspace_a .update(cx_a, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + assert_ne!(*workspace.active_pane(), pane_a1); let leader_id = *project_a.read(cx).collaborators().keys().next().unwrap(); workspace .toggle_follow(&workspace::ToggleFollow(leader_id), cx) @@ -4475,6 +4479,7 @@ mod tests { workspace_b .update(cx_b, |workspace, cx| { workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, cx); + assert_ne!(*workspace.active_pane(), pane_b1); let leader_id = *project_b.read(cx).collaborators().keys().next().unwrap(); workspace .toggle_follow(&workspace::ToggleFollow(leader_id), cx) @@ -4490,9 +4495,24 @@ mod tests { }) .await .unwrap(); + workspace_b + .update(cx_a, |workspace, cx| { + workspace.activate_next_pane(cx); + workspace.open_path((worktree_id, "4.txt"), cx) + }) + .await + .unwrap(); + cx_a.foreground().run_until_parked(); + + // Ensure leader updates don't change the active pane of followers + workspace_a.read_with(cx_a, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_a1); + }); + workspace_b.read_with(cx_b, |workspace, cx| { + assert_ne!(*workspace.active_pane(), pane_b1); + }); // Ensure peers following each other doesn't cause an infinite loop. - cx_a.foreground().run_until_parked(); assert_eq!( workspace_b.read_with(cx_b, |workspace, cx| workspace .active_item(cx) diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ab54b0053e..9903d414e4 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -396,8 +396,10 @@ impl Pane { cx.emit(Event::ActivateItem { local }); } self.update_active_toolbar(cx); - self.focus_active_item(cx); - self.activate(cx); + if local { + self.focus_active_item(cx); + self.activate(cx); + } cx.notify(); } } From 73eae287a1bdd0de7fb30ef347923df89caef78b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 15:57:30 +0100 Subject: [PATCH 064/139] Don't trigger subscriptions with events emitted prior to subscribing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 171 +++++++++++++++++++++++++++++++++-------- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 41d3bf2cdd..fcf8c05d0c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -8,6 +8,7 @@ use crate::{ AssetCache, AssetSource, ClipboardItem, FontCache, PathPromptOptions, TextLayoutCache, }; use anyhow::{anyhow, Result}; +use collections::btree_map; use keymap::MatchResult; use lazy_static::lazy_static; use parking_lot::Mutex; @@ -1150,25 +1151,22 @@ impl MutableAppContext { H: Handle, F: 'static + FnMut(H, &E::Event, &mut Self) -> bool, { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); - self.subscriptions - .lock() - .entry(handle.id()) - .or_default() - .insert( - id, - Some(Box::new(move |payload, cx| { - if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(emitter, payload, cx) - } else { - false - } - })), - ); + self.pending_effects.push_back(Effect::Subscribe { + entity_id: handle.id(), + subscription_id, + callback: Box::new(move |payload, cx| { + if let Some(emitter) = H::upgrade_from(&emitter, cx.as_ref()) { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(emitter, payload, cx) + } else { + false + } + }), + }); Subscription::Subscription { - id, + id: subscription_id, entity_id: handle.id(), subscriptions: Some(Arc::downgrade(&self.subscriptions)), } @@ -1655,6 +1653,11 @@ impl MutableAppContext { loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { + Effect::Subscribe { + entity_id, + subscription_id, + callback, + } => self.handle_subscribe_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), Effect::GlobalEvent { payload } => self.emit_global_event(payload), Effect::ModelNotification { model_id } => { @@ -1771,6 +1774,30 @@ impl MutableAppContext { } } + fn handle_subscribe_effect( + &mut self, + entity_id: usize, + subscription_id: usize, + callback: SubscriptionCallback, + ) { + match self + .subscriptions + .lock() + .entry(entity_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Subscription was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn emit_event(&mut self, entity_id: usize, payload: Box) { let callbacks = self.subscriptions.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -1785,10 +1812,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1812,10 +1839,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1839,10 +1866,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -1880,10 +1907,10 @@ impl MutableAppContext { .or_default() .entry(id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(Some(callback)); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -2234,6 +2261,11 @@ pub struct WindowInvalidation { } pub enum Effect { + Subscribe { + entity_id: usize, + subscription_id: usize, + callback: SubscriptionCallback, + }, Event { entity_id: usize, payload: Box, @@ -2270,6 +2302,10 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + Effect::Subscribe { entity_id, .. } => f + .debug_struct("Effect::Subscribe") + .field("entity_id", entity_id) + .finish(), Effect::Event { entity_id, .. } => f .debug_struct("Effect::Event") .field("entity_id", entity_id) @@ -4053,10 +4089,10 @@ impl Drop for Subscription { .or_default() .entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4069,10 +4105,10 @@ impl Drop for Subscription { } => { if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { match subscriptions.lock().entry(*type_id).or_default().entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4090,10 +4126,10 @@ impl Drop for Subscription { .or_default() .entry(*id) { - collections::btree_map::Entry::Vacant(entry) => { + btree_map::Entry::Vacant(entry) => { entry.insert(None); } - collections::btree_map::Entry::Occupied(entry) => { + btree_map::Entry::Occupied(entry) => { entry.remove(); } } @@ -4375,6 +4411,7 @@ mod tests { let handle_1 = cx.add_model(|_| Model::default()); let handle_2 = cx.add_model(|_| Model::default()); + handle_1.update(cx, |_, cx| { cx.subscribe(&handle_2, move |model: &mut Model, emitter, event, cx| { model.events.push(*event); @@ -4394,6 +4431,37 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]); } + #[crate::test(self)] + fn test_model_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct Model; + + impl Entity for Model { + type Event = (); + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_model(|cx| { + drop(cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("before emit") + }) + .detach(); + cx.emit(()); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("after emit") + }) + .detach(); + Model + }); + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_observe_and_notify_from_model(cx: &mut MutableAppContext) { #[derive(Default)] @@ -4814,6 +4882,47 @@ mod tests { observed_model.update(cx, |_, cx| cx.emit(())); } + #[crate::test(self)] + fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_window(Default::default(), |cx| { + drop(cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("before emit") + }) + .detach(); + cx.emit(()); + cx.subscribe(&cx.handle(), { + let events = events.clone(); + move |_, _, _, _| events.borrow_mut().push("after emit") + }) + .detach(); + TestView + }); + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_observe_and_notify_from_view(cx: &mut MutableAppContext) { #[derive(Default)] From 5ecf945e282be9d0801d751cfde760448ad4eca9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:11:55 +0100 Subject: [PATCH 065/139] Don't trigger global subscriptions with events emitted prior to subscribing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 108 +++++++++++++++++++++++++++++++++++------ 1 file changed, 94 insertions(+), 14 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index fcf8c05d0c..0b3dd26f55 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1111,21 +1111,18 @@ impl MutableAppContext { E: Any, F: 'static + FnMut(&E, &mut Self), { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let type_id = TypeId::of::(); - self.global_subscriptions - .lock() - .entry(type_id) - .or_default() - .insert( - id, - Some(Box::new(move |payload, cx| { - let payload = payload.downcast_ref().expect("downcast is type safe"); - callback(payload, cx) - })), - ); + self.pending_effects.push_back(Effect::SubscribeGlobal { + type_id, + subscription_id, + callback: Box::new(move |payload, cx| { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(payload, cx) + }), + }); Subscription::GlobalSubscription { - id, + id: subscription_id, type_id, subscriptions: Some(Arc::downgrade(&self.global_subscriptions)), } @@ -1659,6 +1656,13 @@ impl MutableAppContext { callback, } => self.handle_subscribe_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), + Effect::SubscribeGlobal { + type_id, + subscription_id, + callback, + } => { + self.handle_subscribe_global_effect(type_id, subscription_id, callback) + } Effect::GlobalEvent { payload } => self.emit_global_event(payload), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) @@ -1825,6 +1829,30 @@ impl MutableAppContext { } } + fn handle_subscribe_global_effect( + &mut self, + type_id: TypeId, + subscription_id: usize, + callback: GlobalSubscriptionCallback, + ) { + match self + .global_subscriptions + .lock() + .entry(type_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Subscription was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn emit_global_event(&mut self, payload: Box) { let type_id = (&*payload).type_id(); let callbacks = self.global_subscriptions.lock().remove(&type_id); @@ -2270,6 +2298,11 @@ pub enum Effect { entity_id: usize, payload: Box, }, + SubscribeGlobal { + type_id: TypeId, + subscription_id: usize, + callback: GlobalSubscriptionCallback, + }, GlobalEvent { payload: Box, }, @@ -2302,14 +2335,28 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Effect::Subscribe { entity_id, .. } => f + Effect::Subscribe { + entity_id, + subscription_id, + .. + } => f .debug_struct("Effect::Subscribe") .field("entity_id", entity_id) + .field("subscription_id", subscription_id) .finish(), Effect::Event { entity_id, .. } => f .debug_struct("Effect::Event") .field("entity_id", entity_id) .finish(), + Effect::SubscribeGlobal { + type_id, + subscription_id, + .. + } => f + .debug_struct("Effect::Subscribe") + .field("type_id", type_id) + .field("subscription_id", subscription_id) + .finish(), Effect::GlobalEvent { payload, .. } => f .debug_struct("Effect::GlobalEvent") .field("type_id", &(&*payload).type_id()) @@ -4795,6 +4842,39 @@ mod tests { ); } + #[crate::test(self)] + fn test_global_events_emitted_before_subscription(cx: &mut MutableAppContext) { + let events = Rc::new(RefCell::new(Vec::new())); + cx.update(|cx| { + { + let events = events.clone(); + drop(cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("dropped before emit"); + })); + } + + { + let events = events.clone(); + cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("before emit"); + }) + .detach(); + } + + cx.emit_global(()); + + { + let events = events.clone(); + cx.subscribe_global(move |_: &(), _| { + events.borrow_mut().push("after emit"); + }) + .detach(); + } + }); + + assert_eq!(*events.borrow(), ["before emit"]); + } + #[crate::test(self)] fn test_global_nested_events(cx: &mut MutableAppContext) { #[derive(Clone, Debug, Eq, PartialEq)] From 9885c4f6bafd6a3cac1836b85507b39f0e5cbc53 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:24:48 +0100 Subject: [PATCH 066/139] Don't trigger observations with notifications emitted prior to observing Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 181 ++++++++++++++++++++++++++++++++------- crates/server/src/rpc.rs | 4 +- 2 files changed, 151 insertions(+), 34 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 0b3dd26f55..1e4448de98 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1113,7 +1113,7 @@ impl MutableAppContext { { let subscription_id = post_inc(&mut self.next_subscription_id); let type_id = TypeId::of::(); - self.pending_effects.push_back(Effect::SubscribeGlobal { + self.pending_effects.push_back(Effect::GlobalSubscription { type_id, subscription_id, callback: Box::new(move |payload, cx| { @@ -1150,7 +1150,7 @@ impl MutableAppContext { { let subscription_id = post_inc(&mut self.next_subscription_id); let emitter = handle.downgrade(); - self.pending_effects.push_back(Effect::Subscribe { + self.pending_effects.push_back(Effect::Subscription { entity_id: handle.id(), subscription_id, callback: Box::new(move |payload, cx| { @@ -1176,25 +1176,23 @@ impl MutableAppContext { H: Handle, F: 'static + FnMut(H, &mut Self) -> bool, { - let id = post_inc(&mut self.next_subscription_id); + let subscription_id = post_inc(&mut self.next_subscription_id); let observed = handle.downgrade(); - self.observations - .lock() - .entry(handle.id()) - .or_default() - .insert( - id, - Some(Box::new(move |cx| { - if let Some(observed) = H::upgrade_from(&observed, cx) { - callback(observed, cx) - } else { - false - } - })), - ); + let entity_id = handle.id(); + self.pending_effects.push_back(Effect::Observation { + entity_id, + subscription_id, + callback: Box::new(move |cx| { + if let Some(observed) = H::upgrade_from(&observed, cx) { + callback(observed, cx) + } else { + false + } + }), + }); Subscription::Observation { - id, - entity_id: handle.id(), + id: subscription_id, + entity_id, observations: Some(Arc::downgrade(&self.observations)), } } @@ -1650,20 +1648,27 @@ impl MutableAppContext { loop { if let Some(effect) = self.pending_effects.pop_front() { match effect { - Effect::Subscribe { + Effect::Subscription { entity_id, subscription_id, callback, - } => self.handle_subscribe_effect(entity_id, subscription_id, callback), + } => self.handle_subscription_effect(entity_id, subscription_id, callback), Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), - Effect::SubscribeGlobal { + Effect::GlobalSubscription { type_id, subscription_id, callback, - } => { - self.handle_subscribe_global_effect(type_id, subscription_id, callback) - } + } => self.handle_global_subscription_effect( + type_id, + subscription_id, + callback, + ), Effect::GlobalEvent { payload } => self.emit_global_event(payload), + Effect::Observation { + entity_id, + subscription_id, + callback, + } => self.handle_observation_effect(entity_id, subscription_id, callback), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) } @@ -1778,7 +1783,7 @@ impl MutableAppContext { } } - fn handle_subscribe_effect( + fn handle_subscription_effect( &mut self, entity_id: usize, subscription_id: usize, @@ -1829,7 +1834,7 @@ impl MutableAppContext { } } - fn handle_subscribe_global_effect( + fn handle_global_subscription_effect( &mut self, type_id: TypeId, subscription_id: usize, @@ -1879,6 +1884,30 @@ impl MutableAppContext { } } + fn handle_observation_effect( + &mut self, + entity_id: usize, + subscription_id: usize, + callback: ObservationCallback, + ) { + match self + .observations + .lock() + .entry(entity_id) + .or_default() + .entry(subscription_id) + { + btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + // Observation was dropped before effect was processed + btree_map::Entry::Occupied(entry) => { + debug_assert!(entry.get().is_none()); + entry.remove(); + } + } + } + fn notify_model_observers(&mut self, observed_id: usize) { let callbacks = self.observations.lock().remove(&observed_id); if let Some(callbacks) = callbacks { @@ -2289,7 +2318,7 @@ pub struct WindowInvalidation { } pub enum Effect { - Subscribe { + Subscription { entity_id: usize, subscription_id: usize, callback: SubscriptionCallback, @@ -2298,7 +2327,7 @@ pub enum Effect { entity_id: usize, payload: Box, }, - SubscribeGlobal { + GlobalSubscription { type_id: TypeId, subscription_id: usize, callback: GlobalSubscriptionCallback, @@ -2306,6 +2335,11 @@ pub enum Effect { GlobalEvent { payload: Box, }, + Observation { + entity_id: usize, + subscription_id: usize, + callback: ObservationCallback, + }, ModelNotification { model_id: usize, }, @@ -2335,7 +2369,7 @@ pub enum Effect { impl Debug for Effect { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Effect::Subscribe { + Effect::Subscription { entity_id, subscription_id, .. @@ -2348,7 +2382,7 @@ impl Debug for Effect { .debug_struct("Effect::Event") .field("entity_id", entity_id) .finish(), - Effect::SubscribeGlobal { + Effect::GlobalSubscription { type_id, subscription_id, .. @@ -2361,6 +2395,15 @@ impl Debug for Effect { .debug_struct("Effect::GlobalEvent") .field("type_id", &(&*payload).type_id()) .finish(), + Effect::Observation { + entity_id, + subscription_id, + .. + } => f + .debug_struct("Effect::Observation") + .field("entity_id", entity_id) + .field("subscription_id", subscription_id) + .finish(), Effect::ModelNotification { model_id } => f .debug_struct("Effect::ModelNotification") .field("model_id", model_id) @@ -4548,6 +4591,37 @@ mod tests { assert_eq!(handle_1.read(cx).events, vec![7, 5, 10]) } + #[crate::test(self)] + fn test_model_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct Model; + + impl Entity for Model { + type Event = (); + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_model(|cx| { + drop(cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("before notify") + }) + .detach(); + cx.notify(); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("after notify") + }) + .detach(); + Model + }); + assert_eq!(*events.borrow(), ["before notify"]); + } + #[crate::test(self)] fn test_view_handles(cx: &mut MutableAppContext) { struct View { @@ -4843,7 +4917,9 @@ mod tests { } #[crate::test(self)] - fn test_global_events_emitted_before_subscription(cx: &mut MutableAppContext) { + fn test_global_events_emitted_before_subscription_in_same_update_cycle( + cx: &mut MutableAppContext, + ) { let events = Rc::new(RefCell::new(Vec::new())); cx.update(|cx| { { @@ -5050,6 +5126,47 @@ mod tests { assert_eq!(view.read(cx).events, vec![11]); } + #[crate::test(self)] + fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut MutableAppContext) { + #[derive(Default)] + struct TestView; + + impl Entity for TestView { + type Event = (); + } + + impl View for TestView { + fn ui_name() -> &'static str { + "TestView" + } + + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + Empty::new().boxed() + } + } + + let events = Rc::new(RefCell::new(Vec::new())); + cx.add_window(Default::default(), |cx| { + drop(cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("dropped before flush") + })); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("before notify") + }) + .detach(); + cx.notify(); + cx.observe(&cx.handle(), { + let events = events.clone(); + move |_, _, _| events.borrow_mut().push("after notify") + }) + .detach(); + TestView + }); + assert_eq!(*events.borrow(), ["before notify"]); + } + #[crate::test(self)] fn test_dropping_observers(cx: &mut MutableAppContext) { struct View; diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 9527df8e1f..53d1315662 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4505,10 +4505,10 @@ mod tests { cx_a.foreground().run_until_parked(); // Ensure leader updates don't change the active pane of followers - workspace_a.read_with(cx_a, |workspace, cx| { + workspace_a.read_with(cx_a, |workspace, _| { assert_ne!(*workspace.active_pane(), pane_a1); }); - workspace_b.read_with(cx_b, |workspace, cx| { + workspace_b.read_with(cx_b, |workspace, _| { assert_ne!(*workspace.active_pane(), pane_b1); }); From c78bcf711624881a409d8df67c6fb4c13b8ff3c5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 22 Mar 2022 16:44:59 +0100 Subject: [PATCH 067/139] Ensure leader updates don't change the active pane of followers Co-Authored-By: Nathan Sobo --- crates/server/src/rpc.rs | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 53d1315662..79fb49da80 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4454,7 +4454,7 @@ mod tests { // Client B opens an editor. let workspace_b = client_b.build_workspace(&project_b, cx_b); - let pane_b1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); + let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "2.txt"), cx) @@ -4491,13 +4491,15 @@ mod tests { workspace_a .update(cx_a, |workspace, cx| { workspace.activate_next_pane(cx); + assert_eq!(*workspace.active_pane(), pane_a1); workspace.open_path((worktree_id, "3.txt"), cx) }) .await .unwrap(); workspace_b - .update(cx_a, |workspace, cx| { + .update(cx_b, |workspace, cx| { workspace.activate_next_pane(cx); + assert_eq!(*workspace.active_pane(), pane_b1); workspace.open_path((worktree_id, "4.txt"), cx) }) .await @@ -4506,20 +4508,42 @@ mod tests { // Ensure leader updates don't change the active pane of followers workspace_a.read_with(cx_a, |workspace, _| { - assert_ne!(*workspace.active_pane(), pane_a1); + assert_eq!(*workspace.active_pane(), pane_a1); }); workspace_b.read_with(cx_b, |workspace, _| { - assert_ne!(*workspace.active_pane(), pane_b1); + assert_eq!(*workspace.active_pane(), pane_b1); }); // Ensure peers following each other doesn't cause an infinite loop. assert_eq!( - workspace_b.read_with(cx_b, |workspace, cx| workspace + workspace_a.read_with(cx_a, |workspace, cx| workspace .active_item(cx) .unwrap() .project_path(cx)), Some((worktree_id, "3.txt").into()) ); + workspace_a.update(cx_a, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + workspace.activate_next_pane(cx); + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + }); + workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "4.txt").into()) + ); + workspace.activate_next_pane(cx); + assert_eq!( + workspace.active_item(cx).unwrap().project_path(cx), + Some((worktree_id, "3.txt").into()) + ); + }); } #[gpui::test(iterations = 10)] From df751867a125a0e675ba7f554a6aab0afd1130af Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 22 Mar 2022 10:55:03 -0700 Subject: [PATCH 068/139] Remove results from movement function return values, and move editor test utilities to test file --- crates/editor/src/display_map.rs | 16 ++++----- crates/editor/src/editor.rs | 34 ++++++------------- crates/editor/src/movement.rs | 58 +++++++++++++------------------- crates/editor/src/test.rs | 26 ++++++++++++++ 4 files changed, 68 insertions(+), 66 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 17209e5c74..6f31541137 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -714,7 +714,7 @@ mod tests { log::info!("Moving from point {:?}", point); - let moved_right = movement::right(&snapshot, point).unwrap(); + let moved_right = movement::right(&snapshot, point); log::info!("Right {:?}", moved_right); if point < max_point { assert!(moved_right > point); @@ -728,7 +728,7 @@ mod tests { assert_eq!(moved_right, point); } - let moved_left = movement::left(&snapshot, point).unwrap(); + let moved_left = movement::left(&snapshot, point); log::info!("Left {:?}", moved_left); if point > min_point { assert!(moved_left < point); @@ -786,15 +786,15 @@ mod tests { DisplayPoint::new(1, 0) ); assert_eq!( - movement::right(&snapshot, DisplayPoint::new(0, 7)).unwrap(), + movement::right(&snapshot, DisplayPoint::new(0, 7)), DisplayPoint::new(1, 0) ); assert_eq!( - movement::left(&snapshot, DisplayPoint::new(1, 0)).unwrap(), + movement::left(&snapshot, DisplayPoint::new(1, 0)), DisplayPoint::new(0, 7) ); assert_eq!( - movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None).unwrap(), + movement::up(&snapshot, DisplayPoint::new(1, 10), SelectionGoal::None), (DisplayPoint::new(0, 7), SelectionGoal::Column(10)) ); assert_eq!( @@ -802,8 +802,7 @@ mod tests { &snapshot, DisplayPoint::new(0, 7), SelectionGoal::Column(10) - ) - .unwrap(), + ), (DisplayPoint::new(1, 10), SelectionGoal::Column(10)) ); assert_eq!( @@ -811,8 +810,7 @@ mod tests { &snapshot, DisplayPoint::new(1, 10), SelectionGoal::Column(10) - ) - .unwrap(), + ), (DisplayPoint::new(2, 4), SelectionGoal::Column(10)) ); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c3858b4349..2e68d963c9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2663,7 +2663,6 @@ impl Editor { let old_head = selection.head(); let mut new_head = movement::left(&display_map, old_head.to_display_point(&display_map)) - .unwrap() .to_point(&display_map); if let Some((buffer, line_buffer_range)) = display_map .buffer_snapshot @@ -2695,9 +2694,7 @@ impl Editor { for selection in &mut selections { if selection.is_empty() { let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); + let cursor = movement::right(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3315,9 +3312,7 @@ impl Editor { if start != end { selection.end = selection.start.clone(); } else { - let cursor = movement::left(&display_map, start) - .unwrap() - .to_point(&display_map); + let cursor = movement::left(&display_map, start).to_point(&display_map); selection.start = cursor.clone(); selection.end = cursor; } @@ -3332,9 +3327,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); + let cursor = movement::left(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3351,9 +3344,7 @@ impl Editor { if start != end { selection.start = selection.end.clone(); } else { - let cursor = movement::right(&display_map, end) - .unwrap() - .to_point(&display_map); + let cursor = movement::right(&display_map, end).to_point(&display_map); selection.start = cursor; selection.end = cursor; } @@ -3368,9 +3359,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head) - .unwrap() - .to_point(&display_map); + let cursor = movement::right(&display_map, head).to_point(&display_map); selection.set_head(cursor); selection.goal = SelectionGoal::None; } @@ -3402,7 +3391,7 @@ impl Editor { selection.goal = SelectionGoal::None; } - let (start, goal) = movement::up(&display_map, start, selection.goal).unwrap(); + let (start, goal) = movement::up(&display_map, start, selection.goal); let cursor = start.to_point(&display_map); selection.start = cursor; selection.end = cursor; @@ -3417,7 +3406,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::up(&display_map, head, selection.goal).unwrap(); + let (head, goal) = movement::up(&display_map, head, selection.goal); let cursor = head.to_point(&display_map); selection.set_head(cursor); selection.goal = goal; @@ -3448,7 +3437,7 @@ impl Editor { selection.goal = SelectionGoal::None; } - let (start, goal) = movement::down(&display_map, end, selection.goal).unwrap(); + let (start, goal) = movement::down(&display_map, end, selection.goal); let cursor = start.to_point(&display_map); selection.start = cursor; selection.end = cursor; @@ -3463,7 +3452,7 @@ impl Editor { let mut selections = self.local_selections::(cx); for selection in &mut selections { let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::down(&display_map, head, selection.goal).unwrap(); + let (head, goal) = movement::down(&display_map, head, selection.goal); let cursor = head.to_point(&display_map); selection.set_head(cursor); selection.goal = goal; @@ -6149,13 +6138,12 @@ mod tests { #[gpui::test] fn test_selection_with_mouse(cx: &mut gpui::MutableAppContext) { populate_settings(cx); - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + let (_, editor) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); }); - assert_eq!( editor.update(cx, |view, cx| view.selected_display_ranges(cx)), [DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2)] diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 5e6dd8c55c..13f66bb24b 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -1,20 +1,19 @@ use super::{Bias, DisplayPoint, DisplaySnapshot, SelectionGoal, ToDisplayPoint}; use crate::{char_kind, CharKind, ToPoint}; -use anyhow::Result; use language::Point; use std::ops::Range; -pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result { +pub fn left(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { if point.column() > 0 { *point.column_mut() -= 1; } else if point.row() > 0 { *point.row_mut() -= 1; *point.column_mut() = map.line_len(point.row()); } - Ok(map.clip_point(point, Bias::Left)) + map.clip_point(point, Bias::Left) } -pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result { +pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> DisplayPoint { let max_column = map.line_len(point.row()); if point.column() < max_column { *point.column_mut() += 1; @@ -22,14 +21,14 @@ pub fn right(map: &DisplaySnapshot, mut point: DisplayPoint) -> Result Result<(DisplayPoint, SelectionGoal)> { +) -> (DisplayPoint, SelectionGoal) { let mut goal_column = if let SelectionGoal::Column(column) = goal { column } else { @@ -54,17 +53,17 @@ pub fn up( Bias::Right }; - Ok(( + ( map.clip_point(point, clip_bias), SelectionGoal::Column(goal_column), - )) + ) } pub fn down( map: &DisplaySnapshot, start: DisplayPoint, goal: SelectionGoal, -) -> Result<(DisplayPoint, SelectionGoal)> { +) -> (DisplayPoint, SelectionGoal) { let mut goal_column = if let SelectionGoal::Column(column) = goal { column } else { @@ -86,10 +85,10 @@ pub fn down( Bias::Right }; - Ok(( + ( map.clip_point(point, clip_bias), SelectionGoal::Column(goal_column), - )) + ) } pub fn line_beginning( @@ -267,7 +266,7 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range< #[cfg(test)] mod tests { use super::*; - use crate::{Buffer, DisplayMap, MultiBuffer}; + use crate::{test::marked_text, Buffer, DisplayMap, MultiBuffer}; use language::Point; #[gpui::test] @@ -487,50 +486,49 @@ mod tests { ); multibuffer }); - let display_map = cx.add_model(|cx| DisplayMap::new(multibuffer, 2, font_id, 14.0, None, 2, 2, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + assert_eq!(snapshot.text(), "\n\nabc\ndefg\n\n\nhijkl\nmn"); // Can't move up into the first excerpt's header assert_eq!( - up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)).unwrap(), + up(&snapshot, DisplayPoint::new(2, 2), SelectionGoal::Column(2)), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), ); assert_eq!( - up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None).unwrap(), + up(&snapshot, DisplayPoint::new(2, 0), SelectionGoal::None), (DisplayPoint::new(2, 0), SelectionGoal::Column(0)), ); // Move up and down within first excerpt assert_eq!( - up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)).unwrap(), + up(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(4)), (DisplayPoint::new(2, 3), SelectionGoal::Column(4)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)).unwrap(), + down(&snapshot, DisplayPoint::new(2, 3), SelectionGoal::Column(4)), (DisplayPoint::new(3, 4), SelectionGoal::Column(4)), ); // Move up and down across second excerpt's header assert_eq!( - up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)).unwrap(), + up(&snapshot, DisplayPoint::new(6, 5), SelectionGoal::Column(5)), (DisplayPoint::new(3, 4), SelectionGoal::Column(5)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)).unwrap(), + down(&snapshot, DisplayPoint::new(3, 4), SelectionGoal::Column(5)), (DisplayPoint::new(6, 5), SelectionGoal::Column(5)), ); // Can't move down off the end assert_eq!( - down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)).unwrap(), + down(&snapshot, DisplayPoint::new(7, 0), SelectionGoal::Column(0)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); assert_eq!( - down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)).unwrap(), + down(&snapshot, DisplayPoint::new(7, 2), SelectionGoal::Column(2)), (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); } @@ -540,15 +538,7 @@ mod tests { text: &str, cx: &mut gpui::MutableAppContext, ) -> (DisplaySnapshot, Vec) { - let mut marked_offsets = Vec::new(); - let chunks = text.split('|'); - let mut text = String::new(); - - for chunk in chunks { - text.push_str(chunk); - marked_offsets.push(text.len()); - } - marked_offsets.pop(); + let (unmarked_text, markers) = marked_text(text); let tab_size = 4; let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); @@ -558,15 +548,15 @@ mod tests { .unwrap(); let font_size = 14.0; - let buffer = MultiBuffer::build_simple(&text, cx); + let buffer = MultiBuffer::build_simple(&unmarked_text, cx); let display_map = cx .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - let marked_display_points = marked_offsets + let markers = markers .into_iter() .map(|offset| offset.to_display_point(&snapshot)) .collect(); - (snapshot, marked_display_points) + (snapshot, markers) } } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 67622db83f..4908016ef5 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,3 +1,5 @@ +use collections::HashMap; + #[cfg(test)] #[ctor::ctor] fn init_logger() { @@ -5,3 +7,27 @@ fn init_logger() { env_logger::init(); } } + +pub fn marked_text_by( + marked_text: &str, + markers: Vec, +) -> (String, HashMap>) { + let mut extracted_markers: HashMap> = Default::default(); + let mut unmarked_text = String::new(); + + for char in marked_text.chars() { + if markers.contains(&char) { + let char_offsets = extracted_markers.entry(char).or_insert(Vec::new()); + char_offsets.push(unmarked_text.len()); + } else { + unmarked_text.push(char); + } + } + + (unmarked_text, extracted_markers) +} + +pub fn marked_text(marked_text: &str) -> (String, Vec) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']); + (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) +} From 4ed8f6fbb490db2f8158115cb4c9313ab34f14a8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 09:39:39 -0700 Subject: [PATCH 069/139] Make UpdateBuffer a foreground message --- crates/rpc/src/proto.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 39a0d669d5..59d6773451 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -202,7 +202,7 @@ messages!( (UnregisterProject, Foreground), (UnregisterWorktree, Foreground), (UnshareProject, Foreground), - (UpdateBuffer, Background), + (UpdateBuffer, Foreground), (UpdateBufferFile, Foreground), (UpdateContacts, Foreground), (UpdateDiagnosticSummary, Foreground), From c105802b2d1504f757fec27b401fbee8964263df Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 11:43:30 -0700 Subject: [PATCH 070/139] Allow customizing the pane's following border width in the theme --- crates/theme/src/theme.rs | 1 + crates/workspace/src/pane_group.rs | 2 +- crates/zed/assets/themes/_base.toml | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 61d0bf3f67..d10c282e35 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -36,6 +36,7 @@ pub struct Workspace { pub active_tab: Tab, pub pane_divider: Border, pub leader_border_opacity: f32, + pub leader_border_width: f32, pub left_sidebar: Sidebar, pub right_sidebar: Sidebar, pub status_bar: StatusBar, diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index c3f4d2d3a6..d34613df4a 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -111,7 +111,7 @@ impl Member { .editor .replica_selection_style(leader.replica_id) .cursor; - border = Border::all(1.0, leader_color); + border = Border::all(theme.workspace.leader_border_width, leader_color); border .color .fade_out(1. - theme.workspace.leader_border_opacity); diff --git a/crates/zed/assets/themes/_base.toml b/crates/zed/assets/themes/_base.toml index d0368d6933..7bd0c59045 100644 --- a/crates/zed/assets/themes/_base.toml +++ b/crates/zed/assets/themes/_base.toml @@ -4,7 +4,8 @@ base = { family = "Zed Sans", size = 14 } [workspace] background = "$surface.0" pane_divider = { width = 1, color = "$border.0" } -leader_border_opacity = 0.6 +leader_border_opacity = 0.7 +leader_border_width = 2.0 [workspace.titlebar] height = 32 From 0a3f013e00693d2475589f67d70bd5cd8d94004d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 11:44:54 -0700 Subject: [PATCH 071/139] Use env_logger when running the app in a terminal --- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 6 ++---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index b0e5a63137..cbf9389fd4 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -64,6 +64,7 @@ crossbeam-channel = "0.5.0" ctor = "0.1.20" dirs = "3.0" easy-parallel = "3.1.0" +env_logger = "0.8" futures = "0.3" http-auth-basic = "0.1.3" ignore = "0.4" diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 0543783494..61967bfcdf 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -9,7 +9,6 @@ use gpui::{App, AssetSource, Task}; use log::LevelFilter; use parking_lot::Mutex; use project::Fs; -use simplelog::SimpleLogger; use smol::process::Command; use std::{env, fs, path::PathBuf, sync::Arc}; use theme::{ThemeRegistry, DEFAULT_THEME_NAME}; @@ -142,11 +141,10 @@ fn main() { } fn init_logger() { - let level = LevelFilter::Info; - if stdout_is_a_pty() { - SimpleLogger::init(level, Default::default()).expect("could not initialize logger"); + env_logger::init(); } else { + let level = LevelFilter::Info; let log_dir_path = dirs::home_dir() .expect("could not locate home directory for logging") .join("Library/Logs/"); From fc811e08562a913d2efc42501ecbef17c57935c2 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 13:31:13 -0700 Subject: [PATCH 072/139] Don't represent editor's scroll top anchor as an option Use Anchor::min as the special value representing a scroll top of zero --- crates/editor/src/editor.rs | 23 +++++++++-------------- crates/editor/src/items.rs | 26 ++++++++++---------------- 2 files changed, 19 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index bffc1a2186..8d1890e04d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -438,7 +438,7 @@ pub struct Editor { select_larger_syntax_node_stack: Vec]>>, active_diagnostics: Option, scroll_position: Vector2F, - scroll_top_anchor: Option, + scroll_top_anchor: Anchor, autoscroll_request: Option, soft_wrap_mode_override: Option, get_field_editor_theme: Option, @@ -473,7 +473,7 @@ pub struct EditorSnapshot { pub placeholder_text: Option>, is_focused: bool, scroll_position: Vector2F, - scroll_top_anchor: Option, + scroll_top_anchor: Anchor, } #[derive(Clone)] @@ -915,7 +915,7 @@ impl Editor { get_field_editor_theme, project, scroll_position: Vector2F::zero(), - scroll_top_anchor: None, + scroll_top_anchor: Anchor::min(), autoscroll_request: None, focused: false, show_local_cursors: false, @@ -1020,7 +1020,7 @@ impl Editor { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if scroll_position.y() == 0. { - self.scroll_top_anchor = None; + self.scroll_top_anchor = Anchor::min(); self.scroll_position = scroll_position; } else { let scroll_top_buffer_offset = @@ -1032,19 +1032,14 @@ impl Editor { scroll_position.x(), scroll_position.y() - anchor.to_display_point(&map).row() as f32, ); - self.scroll_top_anchor = Some(anchor); + self.scroll_top_anchor = anchor; } cx.emit(Event::ScrollPositionChanged { local: true }); cx.notify(); } - fn set_scroll_top_anchor( - &mut self, - anchor: Option, - local: bool, - cx: &mut ViewContext, - ) { + fn set_scroll_top_anchor(&mut self, anchor: Anchor, local: bool, cx: &mut ViewContext) { self.scroll_position = Vector2F::zero(); self.scroll_top_anchor = anchor; cx.emit(Event::ScrollPositionChanged { local }); @@ -5636,10 +5631,10 @@ impl Deref for EditorSnapshot { fn compute_scroll_position( snapshot: &DisplaySnapshot, mut scroll_position: Vector2F, - scroll_top_anchor: &Option, + scroll_top_anchor: &Anchor, ) -> Vector2F { - if let Some(anchor) = scroll_top_anchor { - let scroll_top = anchor.to_display_point(snapshot).row() as f32; + if *scroll_top_anchor != Anchor::min() { + let scroll_top = scroll_top_anchor.to_display_point(snapshot).row() as f32; scroll_position.set_y(scroll_top + scroll_position.y()); } else { scroll_position.set_y(0.); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3f8bb2eab3..295f7f664c 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -70,17 +70,15 @@ impl FollowableItem for Editor { if let Some(anchor) = state.scroll_top { editor.set_scroll_top_anchor( - Some(Anchor { + Anchor { buffer_id: Some(state.buffer_id as usize), excerpt_id: excerpt_id.clone(), text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, - }), + }, false, cx, ); - } else { - editor.set_scroll_top_anchor(None, false, cx); } Ok::<_, anyhow::Error>(()) @@ -113,10 +111,9 @@ impl FollowableItem for Editor { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, - scroll_top: self - .scroll_top_anchor - .as_ref() - .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + scroll_top: Some(language::proto::serialize_anchor( + &self.scroll_top_anchor.text_anchor, + )), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -129,10 +126,9 @@ impl FollowableItem for Editor { match event { Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { Some(update_view::Variant::Editor(update_view::Editor { - scroll_top: self - .scroll_top_anchor - .as_ref() - .map(|anchor| language::proto::serialize_anchor(&anchor.text_anchor)), + scroll_top: Some(language::proto::serialize_anchor( + &self.scroll_top_anchor.text_anchor, + )), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -155,17 +151,15 @@ impl FollowableItem for Editor { if let Some(anchor) = message.scroll_top { self.set_scroll_top_anchor( - Some(Anchor { + Anchor { buffer_id: Some(buffer_id), excerpt_id: excerpt_id.clone(), text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, - }), + }, false, cx, ); - } else { - self.set_scroll_top_anchor(None, false, cx); } let selections = message From 6a1c197c1bf7f1f39110ca66e149d8d1cfdf9d8d Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 22 Mar 2022 16:05:01 -0700 Subject: [PATCH 073/139] Add editor selection utilities Also improved test that was failing during above to use marker text approach as a drive by --- crates/editor/src/editor.rs | 696 ++++++++++++++++------------------- crates/text/src/selection.rs | 14 +- 2 files changed, 326 insertions(+), 384 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2e68d963c9..d3e3f5ee5c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -88,8 +88,8 @@ action!(MoveUp); action!(MoveDown); action!(MoveLeft); action!(MoveRight); -action!(MoveToPreviousWordBoundary); -action!(MoveToNextWordBoundary); +action!(MoveToPreviousWordStart); +action!(MoveToNextWordEnd); action!(MoveToBeginningOfLine); action!(MoveToEndOfLine); action!(MoveToBeginning); @@ -200,10 +200,10 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-n", MoveDown, Some("Editor")), Binding::new("ctrl-b", MoveLeft, Some("Editor")), Binding::new("ctrl-f", MoveRight, Some("Editor")), - Binding::new("alt-left", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-b", MoveToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-right", MoveToNextWordBoundary, Some("Editor")), - Binding::new("alt-f", MoveToNextWordBoundary, Some("Editor")), + Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")), + Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")), + Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")), + Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")), Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), @@ -281,8 +281,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::tab); cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); - cx.add_action(Editor::delete_to_previous_word_boundary); - cx.add_action(Editor::delete_to_next_word_boundary); + cx.add_action(Editor::delete_to_previous_word_start); + cx.add_action(Editor::delete_to_next_word_end); cx.add_action(Editor::delete_to_beginning_of_line); cx.add_action(Editor::delete_to_end_of_line); cx.add_action(Editor::cut_to_end_of_line); @@ -298,8 +298,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::move_down); cx.add_action(Editor::move_left); cx.add_action(Editor::move_right); - cx.add_action(Editor::move_to_previous_word_boundary); - cx.add_action(Editor::move_to_next_word_boundary); + cx.add_action(Editor::move_to_previous_word_start); + cx.add_action(Editor::move_to_next_word_end); cx.add_action(Editor::move_to_beginning_of_line); cx.add_action(Editor::move_to_end_of_line); cx.add_action(Editor::move_to_beginning); @@ -308,8 +308,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select_down); cx.add_action(Editor::select_left); cx.add_action(Editor::select_right); - cx.add_action(Editor::select_to_previous_word_boundary); - cx.add_action(Editor::select_to_next_word_boundary); + cx.add_action(Editor::select_to_previous_word_start); + cx.add_action(Editor::select_to_next_word_end); cx.add_action(Editor::select_to_beginning_of_line); cx.add_action(Editor::select_to_end_of_line); cx.add_action(Editor::select_to_beginning); @@ -345,14 +345,6 @@ pub fn init(cx: &mut MutableAppContext) { }); } -trait SelectionExt { - fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; - fn display_range(&self, map: &DisplaySnapshot) -> Range; - fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) - -> Range; -} - trait InvalidationRegion { fn ranges(&self) -> &[Range]; } @@ -1202,6 +1194,66 @@ impl Editor { } } + pub fn move_selections( + &mut self, + cx: &mut ViewContext, + move_selection: impl Fn(&DisplaySnapshot, &mut Selection), + ) { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let selections = self + .local_selections::(cx) + .into_iter() + .map(|selection| { + let mut selection = Selection { + id: selection.id, + start: selection.start.to_display_point(&display_map), + end: selection.end.to_display_point(&display_map), + reversed: selection.reversed, + goal: selection.goal, + }; + move_selection(&display_map, &mut selection); + Selection { + id: selection.id, + start: selection.start.to_point(&display_map), + end: selection.end.to_point(&display_map), + reversed: selection.reversed, + goal: selection.goal, + } + }) + .collect(); + self.update_selections(selections, Some(Autoscroll::Fit), cx); + } + + pub fn move_selection_heads( + &mut self, + cx: &mut ViewContext, + update_head: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_selections(cx, |map, selection| { + let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); + selection.set_head(new_head, new_goal); + }); + } + + pub fn move_cursors( + &mut self, + cx: &mut ViewContext, + update_cursor_position: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_selections(cx, |map, selection| { + let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); + selection.collapse_to(cursor, new_goal) + }); + } + fn select(&mut self, Select(phase): &Select, cx: &mut ViewContext) { self.hide_context_menu(cx); @@ -2678,8 +2730,7 @@ impl Editor { } } - selection.set_head(new_head); - selection.goal = SelectionGoal::None; + selection.set_head(new_head, SelectionGoal::None); } } self.update_selections(selections, Some(Autoscroll::Fit), cx); @@ -2688,20 +2739,15 @@ impl Editor { } pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert(&"", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::right(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert(&"", cx); + }); } pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { @@ -3303,67 +3349,37 @@ impl Editor { } pub fn move_left(&mut self, _: &MoveLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.end = selection.start.clone(); + self.move_selections(cx, |map, selection| { + let cursor = if selection.is_empty() { + movement::left(map, selection.start) } else { - let cursor = movement::left(&display_map, start).to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + selection.start + }; + selection.collapse_to(cursor, SelectionGoal::None); + }); } pub fn select_left(&mut self, _: &SelectLeft, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + (movement::left(map, head), SelectionGoal::None) + }); } pub fn move_right(&mut self, _: &MoveRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - - if start != end { - selection.start = selection.end.clone(); + self.move_selections(cx, |map, selection| { + let cursor = if selection.is_empty() { + movement::right(map, selection.end) } else { - let cursor = movement::right(&display_map, end).to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - } - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + selection.end + }; + selection.collapse_to(cursor, SelectionGoal::None) + }); } pub fn select_right(&mut self, _: &SelectRight, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::right(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + (movement::right(map, head), SelectionGoal::None) + }); } pub fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { @@ -3382,36 +3398,17 @@ impl Editor { return; } - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { + self.move_selections(cx, |map, selection| { + if !selection.is_empty() { selection.goal = SelectionGoal::None; } - - let (start, goal) = movement::up(&display_map, start, selection.goal); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + let (cursor, goal) = movement::up(&map, selection.start, selection.goal); + selection.collapse_to(cursor, goal); + }); } pub fn select_up(&mut self, _: &SelectUp, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::up(&display_map, head, selection.goal); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, movement::up) } pub fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { @@ -3428,147 +3425,91 @@ impl Editor { return; } - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let start = selection.start.to_display_point(&display_map); - let end = selection.end.to_display_point(&display_map); - if start != end { + self.move_selections(cx, |map, selection| { + if !selection.is_empty() { selection.goal = SelectionGoal::None; } - - let (start, goal) = movement::down(&display_map, end, selection.goal); - let cursor = start.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.goal = goal; - selection.reversed = false; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + let (cursor, goal) = movement::down(&map, selection.end, selection.goal); + selection.collapse_to(cursor, goal); + }); } pub fn select_down(&mut self, _: &SelectDown, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let (head, goal) = movement::down(&display_map, head, selection.goal); - let cursor = head.to_point(&display_map); - selection.set_head(cursor); - selection.goal = goal; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, movement::down) } - pub fn move_to_previous_word_boundary( + pub fn move_to_previous_word_start( &mut self, - _: &MoveToPreviousWordBoundary, + _: &MoveToPreviousWordStart, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::previous_word_start(&display_map, head).to_point(&display_map); - selection.start = cursor.clone(); - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn select_to_previous_word_boundary( + pub fn select_to_previous_word_start( &mut self, _: &SelectToPreviousWordBoundary, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::previous_word_start(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::previous_word_start(map, head), + SelectionGoal::None, + ) + }); } - pub fn delete_to_previous_word_boundary( + pub fn delete_to_previous_word_start( &mut self, _: &DeleteToPreviousWordBoundary, cx: &mut ViewContext, ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = - movement::previous_word_start(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::previous_word_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); } - pub fn move_to_next_word_boundary( - &mut self, - _: &MoveToNextWordBoundary, - cx: &mut ViewContext, - ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext) { + self.move_cursors(cx, |map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); } - pub fn select_to_next_word_boundary( + pub fn select_to_next_word_end( &mut self, _: &SelectToNextWordBoundary, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + (movement::next_word_end(map, head), SelectionGoal::None) + }); } - pub fn delete_to_next_word_boundary( + pub fn delete_to_next_word_end( &mut self, _: &DeleteToNextWordBoundary, cx: &mut ViewContext, ) { - self.start_transaction(cx); - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::next_word_end(&display_map, head).to_point(&display_map); - selection.set_head(cursor); - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::next_word_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); } pub fn move_to_beginning_of_line( @@ -3576,18 +3517,12 @@ impl Editor { _: &MoveToBeginningOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, true); - let cursor = new_head.to_point(&display_map); - selection.start = cursor; - selection.end = cursor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + ( + movement::line_beginning(map, head, true), + SelectionGoal::None, + ) + }); } pub fn select_to_beginning_of_line( @@ -3595,15 +3530,12 @@ impl Editor { SelectToBeginningOfLine(stop_at_soft_boundaries): &SelectToBeginningOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_beginning(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::line_beginning(map, head, *stop_at_soft_boundaries), + SelectionGoal::None, + ) + }); } pub fn delete_to_beginning_of_line( @@ -3611,27 +3543,16 @@ impl Editor { _: &DeleteToBeginningOfLine, cx: &mut ViewContext, ) { - self.start_transaction(cx); - self.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); - self.backspace(&Backspace, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_beginning_of_line(&SelectToBeginningOfLine(false), cx); + this.backspace(&Backspace, cx); + }); } pub fn move_to_end_of_line(&mut self, _: &MoveToEndOfLine, cx: &mut ViewContext) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - { - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, true); - let anchor = new_head.to_point(&display_map); - selection.start = anchor.clone(); - selection.end = anchor; - selection.reversed = false; - selection.goal = SelectionGoal::None; - } - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_cursors(cx, |map, head, _| { + (movement::line_end(map, head, true), SelectionGoal::None) + }); } pub fn select_to_end_of_line( @@ -3639,29 +3560,26 @@ impl Editor { SelectToEndOfLine(stop_at_soft_boundaries): &SelectToEndOfLine, cx: &mut ViewContext, ) { - let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let mut selections = self.local_selections::(cx); - for selection in &mut selections { - let head = selection.head().to_display_point(&display_map); - let new_head = movement::line_end(&display_map, head, *stop_at_soft_boundaries); - selection.set_head(new_head.to_point(&display_map)); - selection.goal = SelectionGoal::None; - } - self.update_selections(selections, Some(Autoscroll::Fit), cx); + self.move_selection_heads(cx, |map, head, _| { + ( + movement::line_end(map, head, *stop_at_soft_boundaries), + SelectionGoal::None, + ) + }); } pub fn delete_to_end_of_line(&mut self, _: &DeleteToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.delete(&Delete, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_end_of_line(&SelectToEndOfLine(false), cx); + this.delete(&Delete, cx); + }); } pub fn cut_to_end_of_line(&mut self, _: &CutToEndOfLine, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_to_end_of_line(&SelectToEndOfLine(false), cx); - self.cut(&Cut, cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_to_end_of_line(&SelectToEndOfLine(false), cx); + this.cut(&Cut, cx); + }); } pub fn move_to_beginning(&mut self, _: &MoveToBeginning, cx: &mut ViewContext) { @@ -3682,7 +3600,7 @@ impl Editor { pub fn select_to_beginning(&mut self, _: &SelectToBeginning, cx: &mut ViewContext) { let mut selection = self.local_selections::(cx).last().unwrap().clone(); - selection.set_head(Point::zero()); + selection.set_head(Point::zero(), SelectionGoal::None); self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } @@ -3739,7 +3657,7 @@ impl Editor { pub fn select_to_end(&mut self, _: &SelectToEnd, cx: &mut ViewContext) { let mut selection = self.local_selections::(cx).first().unwrap().clone(); - selection.set_head(self.buffer.read(cx).read(cx).len()); + selection.set_head(self.buffer.read(cx).read(cx).len(), SelectionGoal::None); self.update_selections(vec![selection], Some(Autoscroll::Fit), cx); } @@ -5082,6 +5000,16 @@ impl Editor { cx.notify(); } + fn transact( + &mut self, + cx: &mut ViewContext, + update: impl FnOnce(&mut Self, &mut ViewContext), + ) { + self.start_transaction(cx); + update(self, cx); + self.end_transaction(cx); + } + fn start_transaction(&mut self, cx: &mut ViewContext) { self.start_transaction_at(Instant::now(), cx); } @@ -5748,6 +5676,14 @@ fn build_style( style } +trait SelectionExt { + fn offset_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range; + fn display_range(&self, map: &DisplaySnapshot) -> Range; + fn spanned_rows(&self, include_end_if_at_line_start: bool, map: &DisplaySnapshot) + -> Range; +} + impl SelectionExt for Selection { fn point_range(&self, buffer: &MultiBufferSnapshot) -> Range { let start = self.start.to_point(buffer); @@ -6056,6 +5992,8 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { + use crate::test::marked_text_by; + use super::*; use language::{LanguageConfig, LanguageServerConfig}; use lsp::FakeLanguageServer; @@ -6789,127 +6727,94 @@ mod tests { ], cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use std::<>str::{foo, bar}\n\n {[]baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 2)..DisplayPoint::new(2, 2), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use std<>::str::{foo, bar}\n\n []{baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), - DisplayPoint::new(2, 0)..DisplayPoint::new(2, 0), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "use <>std::str::{foo, bar}\n\n[] {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "<>use std::str::{foo, bar}\n[]\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 0)..DisplayPoint::new(0, 0), - DisplayPoint::new(0, 23)..DisplayPoint::new(0, 23), - ] + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); + assert_selection_ranges( + "<>use std::str::{foo, bar[]}\n\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), - DisplayPoint::new(0, 24)..DisplayPoint::new(0, 24), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use<> std::str::{foo, bar}[]\n\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 7)..DisplayPoint::new(0, 7), - DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use std<>::str::{foo, bar}\n[]\n {baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 9)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 3)..DisplayPoint::new(2, 3), - ] + view.move_to_next_word_end(&MoveToNextWordEnd, cx); + assert_selection_ranges( + "use std::<>str::{foo, bar}\n\n {[]baz.qux()}", + vec![('<', '>'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { view.move_right(&MoveRight, cx); - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] + view.select_to_previous_word_start(&SelectToPreviousWordBoundary, cx); + assert_selection_ranges( + "use std::>s'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.select_to_previous_word_boundary(&SelectToPreviousWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 7), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 2), - ] + view.select_to_previous_word_start(&SelectToPreviousWordBoundary, cx); + assert_selection_ranges( + "use std>::s'), ('[', ']')], + view, + cx, ); - }); - view.update(cx, |view, cx| { - view.select_to_next_word_boundary(&SelectToNextWordBoundary, cx); - assert_eq!( - view.selected_display_ranges(cx), - &[ - DisplayPoint::new(0, 10)..DisplayPoint::new(0, 9), - DisplayPoint::new(2, 4)..DisplayPoint::new(2, 3), - ] + view.select_to_next_word_end(&SelectToNextWordBoundary, cx); + assert_selection_ranges( + "use std::>s'), ('[', ']')], + view, + cx, ); }); } @@ -6929,37 +6834,37 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 7)..DisplayPoint::new(1, 7)], cx); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 9)..DisplayPoint::new(1, 9)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_next_word_boundary(&MoveToNextWordBoundary, cx); + view.move_to_next_word_end(&MoveToNextWordEnd, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 8)..DisplayPoint::new(2, 8)] ); - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4)] ); - view.move_to_previous_word_boundary(&MoveToPreviousWordBoundary, cx); + view.move_to_previous_word_start(&MoveToPreviousWordStart, cx); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 14)..DisplayPoint::new(1, 14)] @@ -6983,7 +6888,7 @@ mod tests { ], cx, ); - view.delete_to_previous_word_boundary(&DeleteToPreviousWordBoundary, cx); + view.delete_to_previous_word_start(&DeleteToPreviousWordBoundary, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); @@ -6998,7 +6903,7 @@ mod tests { ], cx, ); - view.delete_to_next_word_boundary(&DeleteToNextWordBoundary, cx); + view.delete_to_next_word_end(&DeleteToNextWordBoundary, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); @@ -9080,6 +8985,35 @@ mod tests { let settings = Settings::test(cx); cx.set_global(settings); } + + fn assert_selection_ranges( + marked_text: &str, + selection_marker_pairs: Vec<(char, char)>, + view: &mut Editor, + cx: &mut ViewContext, + ) { + let snapshot = view.snapshot(cx).display_snapshot; + let mut marker_chars = Vec::new(); + for (start, end) in selection_marker_pairs.iter() { + marker_chars.push(*start); + marker_chars.push(*end); + } + let (_, markers) = marked_text_by(marked_text, marker_chars); + let asserted_ranges: Vec> = selection_marker_pairs + .iter() + .map(|(start, end)| { + let start = markers.get(start).unwrap()[0].to_display_point(&snapshot); + let end = markers.get(end).unwrap()[0].to_display_point(&snapshot); + start..end + }) + .collect(); + assert_eq!( + view.selected_display_ranges(cx), + &asserted_ranges[..], + "Assert selections are {}", + marked_text + ); + } } trait RangeExt { diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 4e7d6f5236..2262919b5d 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -1,5 +1,5 @@ use crate::Anchor; -use crate::{rope::TextDimension, BufferSnapshot, ToOffset, ToPoint}; +use crate::{rope::TextDimension, BufferSnapshot}; use std::cmp::Ordering; #[derive(Copy, Clone, Debug, Eq, PartialEq)] @@ -36,12 +36,12 @@ impl Selection { } } -impl Selection { +impl Selection { pub fn is_empty(&self) -> bool { self.start == self.end } - pub fn set_head(&mut self, head: T) { + pub fn set_head(&mut self, head: T, new_goal: SelectionGoal) { if head.cmp(&self.tail()) < Ordering::Equal { if !self.reversed { self.end = self.start; @@ -55,6 +55,14 @@ impl Selection { } self.end = head; } + self.goal = new_goal; + } + + pub fn collapse_to(&mut self, point: T, new_goal: SelectionGoal) { + self.start = point; + self.end = point; + self.goal = new_goal; + self.reversed = false; } } From fba6f24ab7b1106df9dd4eb8ed958476d08311c9 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 22 Mar 2022 16:14:32 -0700 Subject: [PATCH 074/139] Add editor lifetime events --- crates/editor/src/editor.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3e3f5ee5c..06b419dd9a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -796,6 +796,8 @@ pub struct NavigationData { offset: usize, } +pub struct EditorCreated(pub ViewHandle); + impl Editor { pub fn single_line( field_editor_style: Option, @@ -932,6 +934,10 @@ impl Editor { cursor_shape: Default::default(), }; this.end_selection(cx); + + let editor_created_event = EditorCreated(cx.handle()); + cx.emit_global(editor_created_event); + this } @@ -5554,8 +5560,16 @@ pub enum Event { Closed, } +pub struct EditorFocused(pub ViewHandle); +pub struct EditorBlurred(pub ViewHandle); +pub struct EditorReleased(pub WeakViewHandle); + impl Entity for Editor { type Event = Event; + + fn release(&mut self, cx: &mut MutableAppContext) { + cx.emit_global(EditorReleased(self.handle.clone())); + } } impl View for Editor { @@ -5572,6 +5586,8 @@ impl View for Editor { } fn on_focus(&mut self, cx: &mut ViewContext) { + let focused_event = EditorFocused(cx.handle()); + cx.emit_global(focused_event); if let Some(rename) = self.pending_rename.as_ref() { cx.focus(&rename.editor); } else { @@ -5585,6 +5601,8 @@ impl View for Editor { } fn on_blur(&mut self, cx: &mut ViewContext) { + let blurred_event = EditorBlurred(cx.handle()); + cx.emit_global(blurred_event); self.focused = false; self.buffer .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); From 4435d9b1060be71f3a050b86fd2c4a65dafa6d88 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 15:27:05 -0700 Subject: [PATCH 075/139] Combine updates from multiple view events when updating followers Co-Authored-By: Nathan Sobo --- crates/editor/src/items.rs | 41 ++++++++---- crates/server/src/rpc.rs | 3 + crates/workspace/src/workspace.rs | 103 +++++++++++++++++++----------- 3 files changed, 95 insertions(+), 52 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 295f7f664c..3cac47bf59 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -15,7 +15,7 @@ use workspace::{ }; impl FollowableItem for Editor { - fn for_state_message( + fn from_state_proto( pane: ViewHandle, project: ModelHandle, state: &mut Option, @@ -107,7 +107,7 @@ impl FollowableItem for Editor { cx.notify(); } - fn to_state_message(&self, cx: &AppContext) -> Option { + fn to_state_proto(&self, cx: &AppContext) -> Option { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, @@ -118,25 +118,38 @@ impl FollowableItem for Editor { })) } - fn to_update_message( + fn add_event_to_update_proto( &self, event: &Self::Event, + update: &mut Option, _: &AppContext, - ) -> Option { - match event { - Event::ScrollPositionChanged { .. } | Event::SelectionsChanged { .. } => { - Some(update_view::Variant::Editor(update_view::Editor { - scroll_top: Some(language::proto::serialize_anchor( + ) -> bool { + let update = + update.get_or_insert_with(|| proto::update_view::Variant::Editor(Default::default())); + + match update { + proto::update_view::Variant::Editor(update) => match event { + Event::ScrollPositionChanged { .. } => { + update.scroll_top = Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, - )), - selections: self.selections.iter().map(serialize_selection).collect(), - })) - } - _ => None, + )); + true + } + Event::SelectionsChanged { .. } => { + update.selections = self + .selections + .iter() + .chain(self.pending_selection.as_ref().map(|p| &p.selection)) + .map(serialize_selection) + .collect(); + true + } + _ => false, + }, } } - fn apply_update_message( + fn apply_update_proto( &mut self, message: update_view::Variant, cx: &mut ViewContext, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 79fb49da80..f8ed78a13f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4349,9 +4349,12 @@ mod tests { .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; + eprintln!("=========================>>>>>>>>"); editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([3..3], None, cx); + editor.set_scroll_position(vec2f(0., 100.), cx); }); + eprintln!("=========================<<<<<<<<<"); editor_b1 .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) .await; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8296d10f4a..55338130da 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -41,7 +41,10 @@ use std::{ future::Future, path::{Path, PathBuf}, rc::Rc, - sync::Arc, + sync::{ + atomic::{AtomicBool, Ordering::SeqCst}, + Arc, + }, }; use theme::{Theme, ThemeRegistry}; use util::ResultExt; @@ -151,7 +154,7 @@ pub fn register_followable_item(cx: &mut MutableAppContext) { TypeId::of::(), ( |pane, project, state, cx| { - I::for_state_message(pane, project, state, cx).map(|task| { + I::from_state_proto(pane, project, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -255,36 +258,39 @@ pub trait ProjectItem: Item { } pub trait FollowableItem: Item { - fn for_state_message( + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn from_state_proto( pane: ViewHandle, project: ModelHandle, state: &mut Option, cx: &mut MutableAppContext, ) -> Option>>>; - fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); - fn to_state_message(&self, cx: &AppContext) -> Option; - fn to_update_message( + fn add_event_to_update_proto( &self, event: &Self::Event, + update: &mut Option, cx: &AppContext, - ) -> Option; - fn apply_update_message( + ) -> bool; + fn apply_update_proto( &mut self, message: proto::update_view::Variant, cx: &mut ViewContext, ) -> Result<()>; + + fn set_leader_replica_id(&mut self, leader_replica_id: Option, cx: &mut ViewContext); fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool; } pub trait FollowableItemHandle: ItemHandle { fn set_leader_replica_id(&self, leader_replica_id: Option, cx: &mut MutableAppContext); - fn to_state_message(&self, cx: &AppContext) -> Option; - fn to_update_message( + fn to_state_proto(&self, cx: &AppContext) -> Option; + fn add_event_to_update_proto( &self, event: &dyn Any, + update: &mut Option, cx: &AppContext, - ) -> Option; - fn apply_update_message( + ) -> bool; + fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, @@ -299,24 +305,29 @@ impl FollowableItemHandle for ViewHandle { }) } - fn to_state_message(&self, cx: &AppContext) -> Option { - self.read(cx).to_state_message(cx) + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.read(cx).to_state_proto(cx) } - fn to_update_message( + fn add_event_to_update_proto( &self, event: &dyn Any, + update: &mut Option, cx: &AppContext, - ) -> Option { - self.read(cx).to_update_message(event.downcast_ref()?, cx) + ) -> bool { + if let Some(event) = event.downcast_ref() { + self.read(cx).add_event_to_update_proto(event, update, cx) + } else { + false + } } - fn apply_update_message( + fn apply_update_proto( &self, message: proto::update_view::Variant, cx: &mut MutableAppContext, ) -> Result<()> { - self.update(cx, |this, cx| this.apply_update_message(message, cx)) + self.update(cx, |this, cx| this.apply_update_proto(message, cx)) } fn should_unfollow_on_event(&self, event: &dyn Any, cx: &AppContext) -> bool { @@ -413,7 +424,7 @@ impl ItemHandle for ViewHandle { cx: &mut ViewContext, ) { if let Some(followed_item) = self.to_followable_item_handle(cx) { - if let Some(message) = followed_item.to_state_message(cx) { + if let Some(message) = followed_item.to_state_proto(cx) { workspace.update_followers( proto::update_followers::Variant::CreateView(proto::View { id: followed_item.id() as u64, @@ -425,6 +436,8 @@ impl ItemHandle for ViewHandle { } } + let pending_update = Rc::new(RefCell::new(None)); + let pending_update_scheduled = Rc::new(AtomicBool::new(false)); let pane = pane.downgrade(); cx.subscribe(self, move |workspace, item, event, cx| { let pane = if let Some(pane) = pane.upgrade(cx) { @@ -435,9 +448,37 @@ impl ItemHandle for ViewHandle { }; if let Some(item) = item.to_followable_item_handle(cx) { - if item.should_unfollow_on_event(event, cx) { + let leader_id = workspace.leader_for_pane(&pane); + + if leader_id.is_some() && item.should_unfollow_on_event(event, cx) { workspace.unfollow(&pane, cx); } + + if item.add_event_to_update_proto(event, &mut *pending_update.borrow_mut(), cx) + && !pending_update_scheduled.load(SeqCst) + { + pending_update_scheduled.store(true, SeqCst); + cx.spawn({ + let pending_update = pending_update.clone(); + let pending_update_scheduled = pending_update_scheduled.clone(); + move |this, mut cx| async move { + this.update(&mut cx, |this, cx| { + pending_update_scheduled.store(false, SeqCst); + this.update_followers( + proto::update_followers::Variant::UpdateView( + proto::UpdateView { + id: item.id() as u64, + variant: pending_update.borrow_mut().take(), + leader_id: leader_id.map(|id| id.0), + }, + ), + cx, + ); + }); + } + }) + .detach(); + } } if T::should_close_item_on_event(event) { @@ -457,20 +498,6 @@ impl ItemHandle for ViewHandle { if T::should_update_tab_on_event(event) { pane.update(cx, |_, cx| cx.notify()); } - - if let Some(message) = item - .to_followable_item_handle(cx) - .and_then(|i| i.to_update_message(event, cx)) - { - workspace.update_followers( - proto::update_followers::Variant::UpdateView(proto::UpdateView { - id: item.id() as u64, - variant: Some(message), - leader_id: workspace.leader_for_pane(&pane).map(|id| id.0), - }), - cx, - ); - } }) .detach(); } @@ -1621,7 +1648,7 @@ impl Workspace { move |item| { let id = item.id() as u64; let item = item.to_followable_item_handle(cx)?; - let variant = item.to_state_message(cx)?; + let variant = item.to_state_proto(cx)?; Some(proto::View { id, leader_id, @@ -1682,7 +1709,7 @@ impl Workspace { .or_insert(FollowerItem::Loading(Vec::new())) { FollowerItem::Loaded(item) => { - item.apply_update_message(variant, cx).log_err(); + item.apply_update_proto(variant, cx).log_err(); } FollowerItem::Loading(updates) => updates.push(variant), } @@ -1774,7 +1801,7 @@ impl Workspace { let e = e.into_mut(); if let FollowerItem::Loading(updates) = e { for update in updates.drain(..) { - item.apply_update_message(update, cx) + item.apply_update_proto(update, cx) .context("failed to apply view update") .log_err(); } From 880eaa268b20cca964c39329b0b83a94890b4449 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:03:24 -0700 Subject: [PATCH 076/139] Coalesce followed view updates only within one frame Co-Authored-By: Nathan Sobo --- crates/gpui/src/app.rs | 113 +++++++++++++++++++++++++++--- crates/workspace/src/workspace.rs | 29 ++++---- 2 files changed, 115 insertions(+), 27 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 1e4448de98..d5a7fcc6e6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1224,7 +1224,17 @@ impl MutableAppContext { } fn defer(&mut self, callback: Box) { - self.pending_effects.push_back(Effect::Deferred(callback)) + self.pending_effects.push_back(Effect::Deferred { + callback, + after_window_update: false, + }) + } + + pub fn after_window_update(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { + self.pending_effects.push_back(Effect::Deferred { + callback: Box::new(callback), + after_window_update: true, + }) } pub(crate) fn notify_model(&mut self, model_id: usize) { @@ -1640,6 +1650,7 @@ impl MutableAppContext { fn flush_effects(&mut self) { self.pending_flushes = self.pending_flushes.saturating_sub(1); + let mut after_window_update_callbacks = Vec::new(); if !self.flushing_effects && self.pending_flushes == 0 { self.flushing_effects = true; @@ -1675,7 +1686,16 @@ impl MutableAppContext { Effect::ViewNotification { window_id, view_id } => { self.notify_view_observers(window_id, view_id) } - Effect::Deferred(callback) => callback(self), + Effect::Deferred { + callback, + after_window_update, + } => { + if after_window_update { + after_window_update_callbacks.push(callback); + } else { + callback(self) + } + } Effect::ModelRelease { model_id, model } => { self.notify_release_observers(model_id, model.as_any()) } @@ -1707,12 +1727,18 @@ impl MutableAppContext { } if self.pending_effects.is_empty() { - self.flushing_effects = false; - self.pending_notifications.clear(); - break; - } else { - refreshing = false; + for callback in after_window_update_callbacks.drain(..) { + callback(self); + } + + if self.pending_effects.is_empty() { + self.flushing_effects = false; + self.pending_notifications.clear(); + break; + } } + + refreshing = false; } } } @@ -2347,7 +2373,10 @@ pub enum Effect { window_id: usize, view_id: usize, }, - Deferred(Box), + Deferred { + callback: Box, + after_window_update: bool, + }, ModelRelease { model_id: usize, model: Box, @@ -2413,7 +2442,7 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), - Effect::Deferred(_) => f.debug_struct("Effect::Deferred").finish(), + Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(), Effect::ModelRelease { model_id, .. } => f .debug_struct("Effect::ModelRelease") .field("model_id", model_id) @@ -2945,6 +2974,18 @@ impl<'a, T: View> ViewContext<'a, T> { })) } + pub fn after_window_update( + &mut self, + callback: impl 'static + FnOnce(&mut T, &mut ViewContext), + ) { + let handle = self.handle(); + self.app.after_window_update(move |cx| { + handle.update(cx, |view, cx| { + callback(view, cx); + }) + }) + } + pub fn propagate_action(&mut self) { self.app.halt_action_dispatch = false; } @@ -4424,7 +4465,7 @@ mod tests { use smol::future::poll_once; use std::{ cell::Cell, - sync::atomic::{AtomicUsize, Ordering::SeqCst}, + sync::atomic::{AtomicBool, AtomicUsize, Ordering::SeqCst}, }; #[crate::test(self)] @@ -4622,6 +4663,58 @@ mod tests { assert_eq!(*events.borrow(), ["before notify"]); } + #[crate::test(self)] + fn test_defer_and_after_window_update(cx: &mut MutableAppContext) { + struct View { + render_count: usize, + } + + impl Entity for View { + type Event = usize; + } + + impl super::View for View { + fn render(&mut self, _: &mut RenderContext) -> ElementBox { + post_inc(&mut self.render_count); + Empty::new().boxed() + } + + fn ui_name() -> &'static str { + "View" + } + } + + let (_, view) = cx.add_window(Default::default(), |_| View { render_count: 0 }); + let called_defer = Rc::new(AtomicBool::new(false)); + let called_after_window_update = Rc::new(AtomicBool::new(false)); + + view.update(cx, |this, cx| { + assert_eq!(this.render_count, 1); + cx.defer({ + let called_defer = called_defer.clone(); + move |this, _| { + assert_eq!(this.render_count, 1); + called_defer.store(true, SeqCst); + } + }); + cx.after_window_update({ + let called_after_window_update = called_after_window_update.clone(); + move |this, cx| { + assert_eq!(this.render_count, 2); + called_after_window_update.store(true, SeqCst); + cx.notify(); + } + }); + assert!(!called_defer.load(SeqCst)); + assert!(!called_after_window_update.load(SeqCst)); + cx.notify(); + }); + + assert!(called_defer.load(SeqCst)); + assert!(called_after_window_update.load(SeqCst)); + assert_eq!(view.read(cx).render_count, 3); + } + #[crate::test(self)] fn test_view_handles(cx: &mut MutableAppContext) { struct View { diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 55338130da..2d8891b1e0 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -458,26 +458,21 @@ impl ItemHandle for ViewHandle { && !pending_update_scheduled.load(SeqCst) { pending_update_scheduled.store(true, SeqCst); - cx.spawn({ + cx.after_window_update({ let pending_update = pending_update.clone(); let pending_update_scheduled = pending_update_scheduled.clone(); - move |this, mut cx| async move { - this.update(&mut cx, |this, cx| { - pending_update_scheduled.store(false, SeqCst); - this.update_followers( - proto::update_followers::Variant::UpdateView( - proto::UpdateView { - id: item.id() as u64, - variant: pending_update.borrow_mut().take(), - leader_id: leader_id.map(|id| id.0), - }, - ), - cx, - ); - }); + move |this, cx| { + pending_update_scheduled.store(false, SeqCst); + this.update_followers( + proto::update_followers::Variant::UpdateView(proto::UpdateView { + id: item.id() as u64, + variant: pending_update.borrow_mut().take(), + leader_id: leader_id.map(|id| id.0), + }), + cx, + ); } - }) - .detach(); + }); } } From 8699dd9c56ee3d0090be8cc158340855515655c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:20:13 -0700 Subject: [PATCH 077/139] Replicate fractional component of leader's scroll position Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 11 ++++++++--- crates/editor/src/items.rs | 20 ++++++++++++-------- crates/rpc/proto/zed.proto | 8 ++++++-- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d1890e04d..3d763478e5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1039,10 +1039,15 @@ impl Editor { cx.notify(); } - fn set_scroll_top_anchor(&mut self, anchor: Anchor, local: bool, cx: &mut ViewContext) { - self.scroll_position = Vector2F::zero(); + fn set_scroll_top_anchor( + &mut self, + anchor: Anchor, + position: Vector2F, + cx: &mut ViewContext, + ) { self.scroll_top_anchor = anchor; - cx.emit(Event::ScrollPositionChanged { local }); + self.scroll_position = position; + cx.emit(Event::ScrollPositionChanged { local: false }); cx.notify(); } diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 3cac47bf59..ab5359600d 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,8 +1,8 @@ use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; use gpui::{ - elements::*, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, - Task, View, ViewContext, ViewHandle, + elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, + RenderContext, Subscription, Task, View, ViewContext, ViewHandle, }; use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; @@ -68,7 +68,7 @@ impl FollowableItem for Editor { editor.set_selections(selections.into(), None, false, cx); } - if let Some(anchor) = state.scroll_top { + if let Some(anchor) = state.scroll_top_anchor { editor.set_scroll_top_anchor( Anchor { buffer_id: Some(state.buffer_id as usize), @@ -76,7 +76,7 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }, - false, + vec2f(state.scroll_x, state.scroll_y), cx, ); } @@ -111,9 +111,11 @@ impl FollowableItem for Editor { let buffer_id = self.buffer.read(cx).as_singleton()?.read(cx).remote_id(); Some(proto::view::Variant::Editor(proto::view::Editor { buffer_id, - scroll_top: Some(language::proto::serialize_anchor( + scroll_top_anchor: Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, )), + scroll_x: self.scroll_position.x(), + scroll_y: self.scroll_position.y(), selections: self.selections.iter().map(serialize_selection).collect(), })) } @@ -130,9 +132,11 @@ impl FollowableItem for Editor { match update { proto::update_view::Variant::Editor(update) => match event { Event::ScrollPositionChanged { .. } => { - update.scroll_top = Some(language::proto::serialize_anchor( + update.scroll_top_anchor = Some(language::proto::serialize_anchor( &self.scroll_top_anchor.text_anchor, )); + update.scroll_x = self.scroll_position.x(); + update.scroll_y = self.scroll_position.y(); true } Event::SelectionsChanged { .. } => { @@ -162,7 +166,7 @@ impl FollowableItem for Editor { let excerpt_id = excerpt_id.clone(); drop(buffer); - if let Some(anchor) = message.scroll_top { + if let Some(anchor) = message.scroll_top_anchor { self.set_scroll_top_anchor( Anchor { buffer_id: Some(buffer_id), @@ -170,7 +174,7 @@ impl FollowableItem for Editor { text_anchor: language::proto::deserialize_anchor(anchor) .ok_or_else(|| anyhow!("invalid scroll top"))?, }, - false, + vec2f(message.scroll_x, message.scroll_y), cx, ); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 487c7e01a5..9d25e66190 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -580,7 +580,9 @@ message UpdateView { message Editor { repeated Selection selections = 1; - Anchor scroll_top = 2; + Anchor scroll_top_anchor = 2; + float scroll_x = 3; + float scroll_y = 4; } } @@ -595,7 +597,9 @@ message View { message Editor { uint64 buffer_id = 1; repeated Selection selections = 2; - Anchor scroll_top = 3; + Anchor scroll_top_anchor = 3; + float scroll_x = 4; + float scroll_y = 5; } } From fad299eb3f71dccfb377ba695254e2af202137a9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 17:39:34 -0700 Subject: [PATCH 078/139] Add unit test for editor's following methods Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3d763478e5..7ec32e4714 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6093,6 +6093,10 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { use super::*; + use gpui::{ + geometry::rect::RectF, + platform::{WindowBounds, WindowOptions}, + }; use language::{LanguageConfig, LanguageServerConfig}; use lsp::FakeLanguageServer; use project::FakeFs; @@ -6101,6 +6105,7 @@ mod tests { use text::Point; use unindent::Unindent; use util::test::sample_text; + use workspace::FollowableItem; #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { @@ -9035,6 +9040,47 @@ mod tests { }); } + #[gpui::test] + fn test_following(cx: &mut gpui::MutableAppContext) { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + populate_settings(cx); + + let (_, leader) = cx.add_window(Default::default(), |cx| build_editor(buffer.clone(), cx)); + let (_, follower) = cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ); + + follower.update(cx, |_, cx| { + cx.subscribe(&leader, |follower, leader, event, cx| { + let mut update = None; + leader + .read(cx) + .add_event_to_update_proto(event, &mut update, cx); + if let Some(update) = update { + follower.apply_update_proto(update, cx).unwrap(); + } + }) + .detach(); + }); + + leader.update(cx, |leader, cx| { + leader.select_ranges([1..1], None, cx); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]); + + leader.update(cx, |leader, cx| { + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + assert_eq!( + follower.update(cx, |follower, cx| follower.scroll_position(cx)), + vec2f(1.5, 3.5) + ); + } + #[test] fn test_combine_syntax_and_fuzzy_match_highlights() { let string = "abcdefghijklmnop"; From fa62fd968f2b82c57582fb5edc8cb4505f73b600 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Mar 2022 18:02:54 -0700 Subject: [PATCH 079/139] Autoscroll when leader moves cursors instead of copying their scroll top. Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 78 ++++++++++++++++++++++++++++--------- crates/editor/src/items.rs | 27 ++++++------- 2 files changed, 74 insertions(+), 31 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7ec32e4714..e1bba12b42 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -439,7 +439,7 @@ pub struct Editor { active_diagnostics: Option, scroll_position: Vector2F, scroll_top_anchor: Anchor, - autoscroll_request: Option, + autoscroll_request: Option<(Autoscroll, bool)>, soft_wrap_mode_override: Option, get_field_editor_theme: Option, override_text_style: Option>, @@ -1017,6 +1017,15 @@ impl Editor { } pub fn set_scroll_position(&mut self, scroll_position: Vector2F, cx: &mut ViewContext) { + self.set_scroll_position_internal(scroll_position, true, cx); + } + + fn set_scroll_position_internal( + &mut self, + scroll_position: Vector2F, + local: bool, + cx: &mut ViewContext, + ) { let map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); if scroll_position.y() == 0. { @@ -1035,7 +1044,7 @@ impl Editor { self.scroll_top_anchor = anchor; } - cx.emit(Event::ScrollPositionChanged { local: true }); + cx.emit(Event::ScrollPositionChanged { local }); cx.notify(); } @@ -1090,7 +1099,7 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let autoscroll = if let Some(autoscroll) = self.autoscroll_request.take() { + let (autoscroll, local) = if let Some(autoscroll) = self.autoscroll_request.take() { autoscroll } else { return false; @@ -1142,15 +1151,15 @@ impl Editor { if target_top < start_row { scroll_position.set_y(target_top); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } else if target_bottom >= end_row { scroll_position.set_y(target_bottom - visible_lines); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } } Autoscroll::Center => { scroll_position.set_y((first_cursor_top - margin).max(0.0)); - self.set_scroll_position(scroll_position, cx); + self.set_scroll_position_internal(scroll_position, local, cx); } } @@ -5111,7 +5120,12 @@ impl Editor { } pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { - self.autoscroll_request = Some(autoscroll); + self.autoscroll_request = Some((autoscroll, true)); + cx.notify(); + } + + fn request_autoscroll_remotely(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { + self.autoscroll_request = Some((autoscroll, false)); cx.notify(); } @@ -9054,31 +9068,59 @@ mod tests { |cx| build_editor(buffer.clone(), cx), ); - follower.update(cx, |_, cx| { - cx.subscribe(&leader, |follower, leader, event, cx| { - let mut update = None; - leader - .read(cx) - .add_event_to_update_proto(event, &mut update, cx); - if let Some(update) = update { - follower.apply_update_proto(update, cx).unwrap(); - } - }) - .detach(); + let pending_update = Rc::new(RefCell::new(None)); + follower.update(cx, { + let update = pending_update.clone(); + |_, cx| { + cx.subscribe(&leader, move |_, leader, event, cx| { + leader + .read(cx) + .add_event_to_update_proto(event, &mut *update.borrow_mut(), cx); + }) + .detach(); + } }); + // Update the selections only leader.update(cx, |leader, cx| { leader.select_ranges([1..1], None, cx); }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); assert_eq!(follower.read(cx).selected_ranges(cx), vec![1..1]); + // Update the scroll position only leader.update(cx, |leader, cx| { leader.set_scroll_position(vec2f(1.5, 3.5), cx); }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); assert_eq!( follower.update(cx, |follower, cx| follower.scroll_position(cx)), vec2f(1.5, 3.5) ); + + // Update the selections and scroll position + leader.update(cx, |leader, cx| { + leader.select_ranges([0..0], None, cx); + leader.request_autoscroll(Autoscroll::Newest, cx); + leader.set_scroll_position(vec2f(1.5, 3.5), cx); + }); + follower.update(cx, |follower, cx| { + let initial_scroll_position = follower.scroll_position(cx); + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + assert_eq!(follower.scroll_position(cx), initial_scroll_position); + assert!(follower.autoscroll_request.is_some()); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0]); } #[test] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index ab5359600d..5971cbc07b 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -166,19 +166,6 @@ impl FollowableItem for Editor { let excerpt_id = excerpt_id.clone(); drop(buffer); - if let Some(anchor) = message.scroll_top_anchor { - self.set_scroll_top_anchor( - Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: language::proto::deserialize_anchor(anchor) - .ok_or_else(|| anyhow!("invalid scroll top"))?, - }, - vec2f(message.scroll_x, message.scroll_y), - cx, - ); - } - let selections = message .selections .into_iter() @@ -188,6 +175,20 @@ impl FollowableItem for Editor { .collect::>(); if !selections.is_empty() { self.set_selections(selections.into(), None, false, cx); + self.request_autoscroll_remotely(Autoscroll::Newest, cx); + } else { + if let Some(anchor) = message.scroll_top_anchor { + self.set_scroll_top_anchor( + Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: language::proto::deserialize_anchor(anchor) + .ok_or_else(|| anyhow!("invalid scroll top"))?, + }, + vec2f(message.scroll_x, message.scroll_y), + cx, + ); + } } } } From 3298529ed1fa2cca864a4b6bc77cf8937257bff4 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 09:14:33 +0100 Subject: [PATCH 080/139] Fix global nested event test after turning subscriptions into effects --- crates/gpui/src/app.rs | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d5a7fcc6e6..43a2c49263 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -5056,11 +5056,13 @@ mod tests { cx.subscribe_global(move |e: &GlobalEvent, cx| { events.borrow_mut().push(("Outer", e.clone())); - let events = events.clone(); - cx.subscribe_global(move |e: &GlobalEvent, _| { - events.borrow_mut().push(("Inner", e.clone())); - }) - .detach(); + if e.0 == 1 { + let events = events.clone(); + cx.subscribe_global(move |e: &GlobalEvent, _| { + events.borrow_mut().push(("Inner", e.clone())); + }) + .detach(); + } }) .detach(); } @@ -5070,16 +5072,18 @@ mod tests { cx.emit_global(GlobalEvent(2)); cx.emit_global(GlobalEvent(3)); }); + cx.update(|cx| { + cx.emit_global(GlobalEvent(4)); + }); assert_eq!( &*events.borrow(), &[ ("Outer", GlobalEvent(1)), ("Outer", GlobalEvent(2)), - ("Inner", GlobalEvent(2)), ("Outer", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), - ("Inner", GlobalEvent(3)), + ("Outer", GlobalEvent(4)), + ("Inner", GlobalEvent(4)), ] ); } From d24bd6f19a229bd42c29447a888f146baa3c5dfa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 11:46:35 +0100 Subject: [PATCH 081/139] Account for pending selections when calling `Editor::refresh_selections` --- crates/editor/src/editor.rs | 112 +++++++++++++++++++++++++++++------- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d3e3f5ee5c..4867cf7269 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4876,26 +4876,40 @@ impl Editor { /// the id of the new excerpt where the head of the selection has been moved. pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { let snapshot = self.buffer.read(cx).read(cx); + let mut selections_with_lost_position = HashMap::default(); + + if let Some(pending) = self.pending_selection.as_mut() { + let anchors = + snapshot.refresh_anchors([&pending.selection.start, &pending.selection.end]); + let (_, start, kept_start) = anchors[0].clone(); + let (_, end, kept_end) = anchors[1].clone(); + let kept_head = if pending.selection.reversed { + kept_start + } else { + kept_end + }; + if !kept_head { + selections_with_lost_position.insert( + pending.selection.id, + pending.selection.head().excerpt_id.clone(), + ); + } + + pending.selection.start = start; + pending.selection.end = end; + } + let anchors_with_status = snapshot.refresh_anchors( self.selections .iter() .flat_map(|selection| [&selection.start, &selection.end]), ); - let offsets = - snapshot.summaries_for_anchors::(anchors_with_status.iter().map(|a| &a.1)); - assert_eq!(anchors_with_status.len(), 2 * self.selections.len()); - assert_eq!(offsets.len(), anchors_with_status.len()); - - let offsets = offsets.chunks(2); - let statuses = anchors_with_status + self.selections = anchors_with_status .chunks(2) - .map(|a| (a[0].0 / 2, a[0].2, a[1].2)); - - let mut selections_with_lost_position = HashMap::default(); - let new_selections = offsets - .zip(statuses) - .map(|(offsets, (selection_ix, kept_start, kept_end))| { - let selection = &self.selections[selection_ix]; + .map(|selection_anchors| { + let (anchor_ix, start, kept_start) = selection_anchors[0].clone(); + let (_, end, kept_end) = selection_anchors[1].clone(); + let selection = &self.selections[anchor_ix / 2]; let kept_head = if selection.reversed { kept_start } else { @@ -4908,14 +4922,16 @@ impl Editor { Selection { id: selection.id, - start: offsets[0], - end: offsets[1], + start, + end, reversed: selection.reversed, goal: selection.goal, } }) .collect(); drop(snapshot); + + let new_selections = self.local_selections::(cx); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); selections_with_lost_position } @@ -8708,13 +8724,15 @@ mod tests { ); let (_, editor) = cx.add_window(Default::default(), |cx| { let mut editor = build_editor(multibuffer.clone(), cx); - editor.select_ranges( + let snapshot = editor.snapshot(cx); + editor.select_ranges([Point::new(1, 3)..Point::new(1, 3)], None, cx); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selected_ranges(cx), [ Point::new(1, 3)..Point::new(1, 3), Point::new(2, 1)..Point::new(2, 1), - ], - None, - cx, + ] ); editor }); @@ -8757,6 +8775,60 @@ mod tests { }); } + #[gpui::test] + fn test_refresh_selections_while_selecting_with_mouse(cx: &mut gpui::MutableAppContext) { + populate_settings(cx); + let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let mut excerpt1_id = None; + let multibuffer = cx.add_model(|cx| { + let mut multibuffer = MultiBuffer::new(0); + excerpt1_id = multibuffer + .push_excerpts( + buffer.clone(), + [ + Point::new(0, 0)..Point::new(1, 4), + Point::new(1, 0)..Point::new(2, 4), + ], + cx, + ) + .into_iter() + .next(); + multibuffer + }); + assert_eq!( + multibuffer.read(cx).read(cx).text(), + "aaaa\nbbbb\nbbbb\ncccc" + ); + let (_, editor) = cx.add_window(Default::default(), |cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selected_ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }); + + multibuffer.update(cx, |multibuffer, cx| { + multibuffer.remove_excerpts([&excerpt1_id.unwrap()], cx); + }); + editor.update(cx, |editor, cx| { + assert_eq!( + editor.selected_ranges(cx), + [Point::new(0, 0)..Point::new(0, 0)] + ); + + // Ensure we don't panic when selections are refreshed and that the pending selection is finalized. + editor.refresh_selections(cx); + assert_eq!( + editor.selected_ranges(cx), + [Point::new(0, 3)..Point::new(0, 3)] + ); + assert!(editor.pending_selection.is_none()); + }); + } + #[gpui::test] async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); From 097bbe3e077275bf3f0bfe441beab0516aee2615 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 23 Mar 2022 06:19:35 -0600 Subject: [PATCH 082/139] Update follow binding, remove unfollow binding The previous binding to follow had ergonomics issues for the frequency that I think we'll want to use it. It would also conflict with the sub-word selection binding. Now that moving the cursor etc unfollows, I don't think we need the follow binding. --- crates/workspace/src/workspace.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2d8891b1e0..b24e316b55 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -112,8 +112,7 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); cx.add_bindings(vec![ - Binding::new("cmd-alt-shift-F", FollowNextCollaborator, None), - Binding::new("cmd-alt-shift-U", Unfollow, None), + Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), Binding::new( From edc038a1cf9c49b9509648d7726fd5a252d2be0f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:26:00 +0100 Subject: [PATCH 083/139] Activate previous pane and next pane via `cmd-k cmd-left` and `cmd-k cmd-right` Co-Authored-By: Nathan Sobo --- crates/workspace/src/workspace.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index b24e316b55..ed822a3487 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -78,6 +78,8 @@ action!(Unfollow); action!(JoinProject, JoinProjectParams); action!(Save); action!(DebugElements); +action!(ActivatePreviousPane); +action!(ActivateNextPane); pub fn init(client: &Arc, cx: &mut MutableAppContext) { pane::init(cx); @@ -111,10 +113,18 @@ pub fn init(client: &Arc, cx: &mut MutableAppContext) { cx.add_action(Workspace::debug_elements); cx.add_action(Workspace::toggle_sidebar_item); cx.add_action(Workspace::toggle_sidebar_item_focus); + cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + workspace.activate_previous_pane(cx) + }); + cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + workspace.activate_next_pane(cx) + }); cx.add_bindings(vec![ Binding::new("ctrl-alt-cmd-f", FollowNextCollaborator, None), Binding::new("cmd-s", Save, None), Binding::new("cmd-alt-i", DebugElements, None), + Binding::new("cmd-k cmd-left", ActivatePreviousPane, None), + Binding::new("cmd-k cmd-right", ActivateNextPane, None), Binding::new( "cmd-shift-!", ToggleSidebarItem(SidebarItemId { @@ -1159,6 +1169,20 @@ impl Workspace { self.activate_pane(self.panes[next_ix].clone(), cx); } + pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { + let ix = self + .panes + .iter() + .position(|pane| pane == &self.active_pane) + .unwrap(); + let prev_ix = if ix == 0 { + self.panes.len() - 1 + } else { + ix - 1 + }; + self.activate_pane(self.panes[prev_ix].clone(), cx); + } + fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { if self.active_pane != pane { self.active_pane = pane.clone(); From 4f27049305491f2403aefde1e3fb1e6dd8e1e717 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:33:22 +0100 Subject: [PATCH 084/139] Focus followed items when they become active if the pane is active Co-Authored-By: Nathan Sobo --- crates/server/src/rpc.rs | 3 +-- crates/workspace/src/pane.rs | 2 +- crates/workspace/src/workspace.rs | 3 +++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index f8ed78a13f..1020fd686a 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -4311,6 +4311,7 @@ mod tests { .downcast::() .unwrap() }); + assert!(cx_b.read(|cx| editor_b2.is_focused(cx))); assert_eq!( editor_b2.read_with(cx_b, |editor, cx| editor.project_path(cx)), Some((worktree_id, "2.txt").into()) @@ -4349,12 +4350,10 @@ mod tests { .condition(cx_b, |editor, cx| editor.text(cx) == "TWO") .await; - eprintln!("=========================>>>>>>>>"); editor_a1.update(cx_a, |editor, cx| { editor.select_ranges([3..3], None, cx); editor.set_scroll_position(vec2f(0., 100.), cx); }); - eprintln!("=========================<<<<<<<<<"); editor_b1 .condition(cx_b, |editor, cx| editor.selected_ranges(cx) == vec![3..3]) .await; diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 9903d414e4..e04af153ba 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -485,7 +485,7 @@ impl Pane { cx.notify(); } - fn focus_active_item(&mut self, cx: &mut ViewContext) { + pub fn focus_active_item(&mut self, cx: &mut ViewContext) { if let Some(active_item) = self.active_item() { cx.focus(active_item); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index ed822a3487..19208294d7 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1902,6 +1902,9 @@ impl Workspace { for (pane, item) in items_to_add { Pane::add_item(self, pane.clone(), item.boxed_clone(), false, cx); + if pane == self.active_pane { + pane.update(cx, |pane, cx| pane.focus_active_item(cx)); + } cx.notify(); } None From 5ac39aa7cd925e131f93585ef3e43ad6067d40b9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 14:46:33 +0100 Subject: [PATCH 085/139] Don't show local cursors when editor is not focused Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e1bba12b42..6d4785ccad 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5520,7 +5520,7 @@ impl Editor { } pub fn show_local_cursors(&self) -> bool { - self.show_local_cursors + self.show_local_cursors && self.focused } fn on_buffer_changed(&mut self, _: ModelHandle, cx: &mut ViewContext) { From 60b6b0b317df55d16c6496239321eca999d5aa15 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 15:06:25 +0100 Subject: [PATCH 086/139] Cycle through panes spatially rather than in the order in which they created Co-Authored-By: Nathan Sobo --- crates/workspace/src/pane_group.rs | 17 ++++++++++++++ crates/workspace/src/workspace.rs | 36 ++++++++++++++++-------------- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index d34613df4a..afffec5074 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -57,6 +57,12 @@ impl PaneGroup { ) -> ElementBox { self.root.render(theme, follower_states, collaborators) } + + pub(crate) fn panes(&self) -> Vec<&ViewHandle> { + let mut panes = Vec::new(); + self.root.collect_panes(&mut panes); + panes + } } #[derive(Clone, Debug, Eq, PartialEq)] @@ -122,6 +128,17 @@ impl Member { Member::Axis(axis) => axis.render(theme, follower_states, collaborators), } } + + fn collect_panes<'a>(&'a self, panes: &mut Vec<&'a ViewHandle>) { + match self { + Member::Axis(axis) => { + for member in &axis.members { + member.collect_panes(panes); + } + } + Member::Pane(pane) => panes.push(pane), + } + } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 19208294d7..691f17ca55 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1160,27 +1160,29 @@ impl Workspace { } pub fn activate_next_pane(&mut self, cx: &mut ViewContext) { - let ix = self - .panes - .iter() - .position(|pane| pane == &self.active_pane) - .unwrap(); - let next_ix = (ix + 1) % self.panes.len(); - self.activate_pane(self.panes[next_ix].clone(), cx); + let next_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let next_ix = (ix + 1) % panes.len(); + panes[next_ix].clone() + }; + self.activate_pane(next_pane, cx); } pub fn activate_previous_pane(&mut self, cx: &mut ViewContext) { - let ix = self - .panes - .iter() - .position(|pane| pane == &self.active_pane) - .unwrap(); - let prev_ix = if ix == 0 { - self.panes.len() - 1 - } else { - ix - 1 + let prev_pane = { + let panes = self.center.panes(); + let ix = panes + .iter() + .position(|pane| **pane == self.active_pane) + .unwrap(); + let prev_ix = if ix == 0 { panes.len() - 1 } else { ix - 1 }; + panes[prev_ix].clone() }; - self.activate_pane(self.panes[prev_ix].clone(), cx); + self.activate_pane(prev_pane, cx); } fn activate_pane(&mut self, pane: ViewHandle, cx: &mut ViewContext) { From b73f57d37a8649c6e462af5f0f641975ec441a26 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 15:21:11 +0100 Subject: [PATCH 087/139] Don't destroy pending selection on `Editor::refresh_selections` Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4867cf7269..81ebf64b02 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4878,7 +4878,8 @@ impl Editor { let snapshot = self.buffer.read(cx).read(cx); let mut selections_with_lost_position = HashMap::default(); - if let Some(pending) = self.pending_selection.as_mut() { + let mut pending_selection = self.pending_selection.take(); + if let Some(pending) = pending_selection.as_mut() { let anchors = snapshot.refresh_anchors([&pending.selection.start, &pending.selection.end]); let (_, start, kept_start) = anchors[0].clone(); @@ -4932,7 +4933,11 @@ impl Editor { drop(snapshot); let new_selections = self.local_selections::(cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + if !new_selections.is_empty() { + self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + } + self.pending_selection = pending_selection; + selections_with_lost_position } @@ -8772,6 +8777,7 @@ mod tests { Point::new(0, 3)..Point::new(0, 3) ] ); + assert!(editor.pending_selection.is_some()); }); } @@ -8825,7 +8831,7 @@ mod tests { editor.selected_ranges(cx), [Point::new(0, 3)..Point::new(0, 3)] ); - assert!(editor.pending_selection.is_none()); + assert!(editor.pending_selection.is_some()); }); } From 454f7a570c00c88ec7ea58bd405cab44c12e3d35 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 22 Mar 2022 16:38:17 -0700 Subject: [PATCH 088/139] Add global change observations --- crates/gpui/src/app.rs | 123 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 121 insertions(+), 2 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 43a2c49263..9b7ca35d5f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -763,6 +763,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; +type GlobalObservationCallback = Box bool>; type ReleaseObservationCallback = Box; pub struct MutableAppContext { @@ -782,12 +783,15 @@ pub struct MutableAppContext { global_subscriptions: Arc>>>>, observations: Arc>>>>, + global_observations: + Arc>>>>, release_observations: Arc>>>, presenters_and_platform_windows: HashMap>, Box)>, foreground: Rc, pending_effects: VecDeque, pending_notifications: HashSet, + pending_global_notifications: HashSet, pending_flushes: usize, flushing_effects: bool, next_cursor_style_handle_id: Arc, @@ -831,10 +835,12 @@ impl MutableAppContext { global_subscriptions: Default::default(), observations: Default::default(), release_observations: Default::default(), + global_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), foreground, pending_effects: VecDeque::new(), pending_notifications: HashSet::new(), + pending_global_notifications: HashSet::new(), pending_flushes: 0, flushing_effects: false, next_cursor_style_handle_id: Default::default(), @@ -1197,6 +1203,27 @@ impl MutableAppContext { } } + pub fn observe_global(&mut self, observe: F) -> Subscription + where + G: Any, + F: 'static + FnMut(&mut MutableAppContext) -> bool, + { + let type_id = TypeId::of::(); + let id = post_inc(&mut self.next_subscription_id); + + self.global_observations + .lock() + .entry(type_id) + .or_default() + .insert(id, Some(Box::new(observe))); + + Subscription::GlobalObservation { + id, + type_id, + observations: Some(Arc::downgrade(&self.global_observations)), + } + } + pub fn observe_release(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -1251,6 +1278,13 @@ impl MutableAppContext { } } + pub(crate) fn notify_global(&mut self, type_id: TypeId) { + if self.pending_global_notifications.insert(type_id) { + self.pending_effects + .push_back(Effect::GlobalNotification { type_id }); + } + } + pub fn dispatch_action( &mut self, window_id: usize, @@ -1380,16 +1414,22 @@ impl MutableAppContext { } pub fn default_global(&mut self) -> &T { + let type_id = TypeId::of::(); + if !self.cx.globals.contains_key(&type_id) { + self.notify_global(type_id); + } self.cx .globals - .entry(TypeId::of::()) + .entry(type_id) .or_insert_with(|| Box::new(T::default())) .downcast_ref() .unwrap() } pub fn set_global(&mut self, state: T) { - self.cx.globals.insert(TypeId::of::(), Box::new(state)); + let type_id = TypeId::of::(); + self.cx.globals.insert(type_id, Box::new(state)); + self.notify_global(type_id); } pub fn update_default_global(&mut self, update: F) -> U @@ -1405,6 +1445,7 @@ impl MutableAppContext { .unwrap_or_else(|| Box::new(T::default())); let result = update(state.downcast_mut().unwrap(), self); self.cx.globals.insert(type_id, state); + self.notify_global(type_id); result } @@ -1421,6 +1462,7 @@ impl MutableAppContext { .expect("no global has been added for this type"); let result = update(state.downcast_mut().unwrap(), self); self.cx.globals.insert(type_id, state); + self.notify_global(type_id); result } @@ -1686,6 +1728,9 @@ impl MutableAppContext { Effect::ViewNotification { window_id, view_id } => { self.notify_view_observers(window_id, view_id) } + Effect::GlobalNotification { type_id } => { + self.notify_global_observers(type_id) + } Effect::Deferred { callback, after_window_update, @@ -1734,6 +1779,7 @@ impl MutableAppContext { if self.pending_effects.is_empty() { self.flushing_effects = false; self.pending_notifications.clear(); + self.pending_global_notifications.clear(); break; } } @@ -2004,6 +2050,35 @@ impl MutableAppContext { } } + fn notify_global_observers(&mut self, observed_type_id: TypeId) { + let callbacks = self.global_observations.lock().remove(&observed_type_id); + if let Some(callbacks) = callbacks { + if self.cx.globals.contains_key(&observed_type_id) { + for (id, callback) in callbacks { + if let Some(mut callback) = callback { + let alive = callback(self); + if alive { + match self + .global_observations + .lock() + .entry(observed_type_id) + .or_default() + .entry(id) + { + collections::btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + collections::btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } + } + } + } + } + fn notify_release_observers(&mut self, entity_id: usize, entity: &dyn Any) { let callbacks = self.release_observations.lock().remove(&entity_id); if let Some(callbacks) = callbacks { @@ -2377,6 +2452,9 @@ pub enum Effect { callback: Box, after_window_update: bool, }, + GlobalNotification { + type_id: TypeId, + }, ModelRelease { model_id: usize, model: Box, @@ -2442,6 +2520,10 @@ impl Debug for Effect { .field("window_id", window_id) .field("view_id", view_id) .finish(), + Effect::GlobalNotification { type_id } => f + .debug_struct("Effect::GlobalNotification") + .field("type_id", type_id) + .finish(), Effect::Deferred { .. } => f.debug_struct("Effect::Deferred").finish(), Effect::ModelRelease { model_id, .. } => f .debug_struct("Effect::ModelRelease") @@ -2621,6 +2703,15 @@ impl<'a, T: Entity> ModelContext<'a, T> { self.app.add_model(build_model) } + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext)) { + let handle = self.handle(); + self.app.defer(Box::new(move |cx| { + handle.update(cx, |model, cx| { + callback(model, cx); + }) + })) + } + pub fn emit(&mut self, payload: T::Event) { self.app.pending_effects.push_back(Effect::Event { entity_id: self.model_id, @@ -4178,6 +4269,13 @@ pub enum Subscription { observations: Option>>>>>, }, + GlobalObservation { + id: usize, + type_id: TypeId, + observations: Option< + Weak>>>>, + >, + }, ReleaseObservation { id: usize, entity_id: usize, @@ -4198,6 +4296,9 @@ impl Subscription { Subscription::Observation { observations, .. } => { observations.take(); } + Subscription::GlobalObservation { observations, .. } => { + observations.take(); + } Subscription::ReleaseObservation { observations, .. } => { observations.take(); } @@ -4266,6 +4367,22 @@ impl Drop for Subscription { } } } + Subscription::GlobalObservation { + id, + type_id, + observations, + } => { + if let Some(observations) = observations.as_ref().and_then(Weak::upgrade) { + match observations.lock().entry(*type_id).or_default().entry(*id) { + collections::btree_map::Entry::Vacant(entry) => { + entry.insert(None); + } + collections::btree_map::Entry::Occupied(entry) => { + entry.remove(); + } + } + } + } Subscription::ReleaseObservation { id, entity_id, @@ -5437,6 +5554,8 @@ mod tests { }); assert_eq!(*observation_count.borrow(), 1); + + // Global Observation } #[crate::test(self)] From 0b3a63b8436474a316ab6b253abed4945b25e127 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Mar 2022 10:24:26 -0700 Subject: [PATCH 089/139] Fix error in follower when leader creates pending selections Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 72 +++++++++++++++++++++++++++++++++++++ crates/editor/src/items.rs | 5 +-- 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49cc4abe69..2344d97fe2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4903,6 +4903,55 @@ impl Editor { ); } + pub fn set_selections_from_remote( + &mut self, + mut selections: Vec>, + cx: &mut ViewContext, + ) { + let buffer = self.buffer.read(cx); + let buffer = buffer.read(cx); + selections.sort_by(|a, b| { + a.start + .cmp(&b.start, &*buffer) + .unwrap() + .then_with(|| b.end.cmp(&a.end, &*buffer).unwrap()) + }); + + // Merge overlapping selections + let mut i = 1; + while i < selections.len() { + if selections[i - 1] + .end + .cmp(&selections[i].start, &*buffer) + .unwrap() + .is_ge() + { + let removed = selections.remove(i); + if removed + .start + .cmp(&selections[i - 1].start, &*buffer) + .unwrap() + .is_lt() + { + selections[i - 1].start = removed.start; + } + if removed + .end + .cmp(&selections[i - 1].end, &*buffer) + .unwrap() + .is_gt() + { + selections[i - 1].end = removed.end; + } + } else { + i += 1; + } + } + + drop(buffer); + self.set_selections(selections.into(), None, false, cx); + } + /// Compute new ranges for any selections that were located in excerpts that have /// since been removed. /// @@ -9109,6 +9158,29 @@ mod tests { assert!(follower.autoscroll_request.is_some()); }); assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0]); + + // Creating a pending selection that precedes another selection + leader.update(cx, |leader, cx| { + leader.select_ranges([1..1], None, cx); + leader.begin_selection(DisplayPoint::new(0, 0), true, 1, cx); + }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..0, 1..1]); + + // Extend the pending selection so that it surrounds another selection + leader.update(cx, |leader, cx| { + leader.extend_selection(DisplayPoint::new(0, 2), 1, cx); + }); + follower.update(cx, |follower, cx| { + follower + .apply_update_proto(pending_update.borrow_mut().take().unwrap(), cx) + .unwrap(); + }); + assert_eq!(follower.read(cx).selected_ranges(cx), vec![0..2]); } #[test] diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5971cbc07b..2cea9f0f9a 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -65,7 +65,7 @@ impl FollowableItem for Editor { }) .collect::>>()?; if !selections.is_empty() { - editor.set_selections(selections.into(), None, false, cx); + editor.set_selections_from_remote(selections.into(), cx); } if let Some(anchor) = state.scroll_top_anchor { @@ -173,8 +173,9 @@ impl FollowableItem for Editor { deserialize_selection(&excerpt_id, buffer_id, selection) }) .collect::>(); + if !selections.is_empty() { - self.set_selections(selections.into(), None, false, cx); + self.set_selections_from_remote(selections, cx); self.request_autoscroll_remotely(Autoscroll::Newest, cx); } else { if let Some(anchor) = message.scroll_top_anchor { From 0a8d543f6654c747511319a29fb4860381af10bb Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 23 Mar 2022 10:27:27 -0700 Subject: [PATCH 090/139] Add global tests and wrap global update functions in update call to flush effects Co-authored-by: Antonio Scandurra --- crates/gpui/src/app.rs | 173 +++++++++++++++++++++++++++++------------ 1 file changed, 124 insertions(+), 49 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9b7ca35d5f..db21612439 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -763,7 +763,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; -type GlobalObservationCallback = Box bool>; +type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; pub struct MutableAppContext { @@ -1206,7 +1206,7 @@ impl MutableAppContext { pub fn observe_global(&mut self, observe: F) -> Subscription where G: Any, - F: 'static + FnMut(&mut MutableAppContext) -> bool, + F: 'static + FnMut(&mut MutableAppContext), { let type_id = TypeId::of::(); let id = post_inc(&mut self.next_subscription_id); @@ -1415,21 +1415,25 @@ impl MutableAppContext { pub fn default_global(&mut self) -> &T { let type_id = TypeId::of::(); - if !self.cx.globals.contains_key(&type_id) { - self.notify_global(type_id); - } - self.cx - .globals - .entry(type_id) - .or_insert_with(|| Box::new(T::default())) - .downcast_ref() - .unwrap() + self.update(|this| { + if !this.globals.contains_key(&type_id) { + this.notify_global(type_id); + } + + this.cx + .globals + .entry(type_id) + .or_insert_with(|| Box::new(T::default())); + }); + self.globals.get(&type_id).unwrap().downcast_ref().unwrap() } pub fn set_global(&mut self, state: T) { - let type_id = TypeId::of::(); - self.cx.globals.insert(type_id, Box::new(state)); - self.notify_global(type_id); + self.update(|this| { + let type_id = TypeId::of::(); + this.cx.globals.insert(type_id, Box::new(state)); + this.notify_global(type_id); + }); } pub fn update_default_global(&mut self, update: F) -> U @@ -1437,16 +1441,18 @@ impl MutableAppContext { T: 'static + Default, F: FnOnce(&mut T, &mut MutableAppContext) -> U, { - let type_id = TypeId::of::(); - let mut state = self - .cx - .globals - .remove(&type_id) - .unwrap_or_else(|| Box::new(T::default())); - let result = update(state.downcast_mut().unwrap(), self); - self.cx.globals.insert(type_id, state); - self.notify_global(type_id); - result + self.update(|this| { + let type_id = TypeId::of::(); + let mut state = this + .cx + .globals + .remove(&type_id) + .unwrap_or_else(|| Box::new(T::default())); + let result = update(state.downcast_mut().unwrap(), this); + this.cx.globals.insert(type_id, state); + this.notify_global(type_id); + result + }) } pub fn update_global(&mut self, update: F) -> U @@ -1454,16 +1460,18 @@ impl MutableAppContext { T: 'static, F: FnOnce(&mut T, &mut MutableAppContext) -> U, { - let type_id = TypeId::of::(); - let mut state = self - .cx - .globals - .remove(&type_id) - .expect("no global has been added for this type"); - let result = update(state.downcast_mut().unwrap(), self); - self.cx.globals.insert(type_id, state); - self.notify_global(type_id); - result + self.update(|this| { + let type_id = TypeId::of::(); + let mut state = this + .cx + .globals + .remove(&type_id) + .expect("no global has been added for this type"); + let result = update(state.downcast_mut().unwrap(), this); + this.cx.globals.insert(type_id, state); + this.notify_global(type_id); + result + }) } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -2056,21 +2064,19 @@ impl MutableAppContext { if self.cx.globals.contains_key(&observed_type_id) { for (id, callback) in callbacks { if let Some(mut callback) = callback { - let alive = callback(self); - if alive { - match self - .global_observations - .lock() - .entry(observed_type_id) - .or_default() - .entry(id) - { - collections::btree_map::Entry::Vacant(entry) => { - entry.insert(Some(callback)); - } - collections::btree_map::Entry::Occupied(entry) => { - entry.remove(); - } + callback(self); + match self + .global_observations + .lock() + .entry(observed_type_id) + .or_default() + .entry(id) + { + collections::btree_map::Entry::Vacant(entry) => { + entry.insert(Some(callback)); + } + collections::btree_map::Entry::Occupied(entry) => { + entry.remove(); } } } @@ -5205,6 +5211,61 @@ mod tests { ); } + #[crate::test(self)] + fn test_global(cx: &mut MutableAppContext) { + type Global = usize; + + let observation_count = Rc::new(RefCell::new(0)); + let subscription = cx.observe_global::({ + let observation_count = observation_count.clone(); + move |_| { + *observation_count.borrow_mut() += 1; + } + }); + + assert!(!cx.has_global::()); + assert_eq!(cx.default_global::(), &0); + assert_eq!(*observation_count.borrow(), 1); + assert!(cx.has_global::()); + assert_eq!( + cx.update_global::(|global, _| { + *global = 1; + "Update Result" + }), + "Update Result" + ); + assert_eq!(*observation_count.borrow(), 2); + assert_eq!(cx.global::(), &1); + + drop(subscription); + cx.update_global::(|global, _| { + *global = 2; + }); + assert_eq!(*observation_count.borrow(), 2); + + type OtherGlobal = f32; + + let observation_count = Rc::new(RefCell::new(0)); + cx.observe_global::({ + let observation_count = observation_count.clone(); + move |_| { + *observation_count.borrow_mut() += 1; + } + }) + .detach(); + + assert_eq!( + cx.update_default_global::(|global, _| { + assert_eq!(global, &0.0); + *global = 2.0; + "Default update result" + }), + "Default update result" + ); + assert_eq!(cx.global::(), &2.0); + assert_eq!(*observation_count.borrow(), 1); + } + #[crate::test(self)] fn test_dropping_subscribers(cx: &mut MutableAppContext) { struct View; @@ -5556,6 +5617,20 @@ mod tests { assert_eq!(*observation_count.borrow(), 1); // Global Observation + let observation_count = Rc::new(RefCell::new(0)); + let subscription = Rc::new(RefCell::new(None)); + *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({ + let observation_count = observation_count.clone(); + let subscription = subscription.clone(); + move |_| { + subscription.borrow_mut().take(); + *observation_count.borrow_mut() += 1; + } + })); + + cx.default_global::<()>(); + cx.set_global(()); + assert_eq!(*observation_count.borrow(), 1); } #[crate::test(self)] From 255a8c5d14609745b113da921d0fe6bb20ab9db9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 18:45:45 +0100 Subject: [PATCH 091/139] Don't push a duplicate nav entry when changing selections via the mouse Co-Authored-By: Keith Simmons --- crates/editor/src/editor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 49cc4abe69..1db7cc3e69 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1393,8 +1393,6 @@ impl Editor { } } - self.push_to_nav_history(newest_selection.head(), Some(end.to_point(&buffer)), cx); - let selection = Selection { id: post_inc(&mut self.next_selection_id), start, @@ -6279,6 +6277,7 @@ mod tests { editor.selected_display_ranges(cx), &[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)] ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); // Move the cursor a small distance via the mouse. // Nothing is added to the navigation history. @@ -6305,6 +6304,7 @@ mod tests { editor.selected_display_ranges(cx), &[DisplayPoint::new(5, 0)..DisplayPoint::new(5, 0)] ); + assert!(nav_history.borrow_mut().pop_backward().is_none()); editor }); From 5cd94b5b92b8835ef5220363b92dee0e0ea2c942 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 23 Mar 2022 19:05:46 +0100 Subject: [PATCH 092/139] WIP --- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/items.rs | 18 +++++-- crates/search/src/project_search.rs | 4 +- crates/workspace/src/pane.rs | 69 +++++++++++++++------------ crates/workspace/src/workspace.rs | 10 ++-- 5 files changed, 61 insertions(+), 44 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7bee815ad9..a0182902f1 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -454,9 +454,9 @@ impl workspace::Item for ProjectDiagnosticsEditor { None } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)); + .update(cx, |editor, cx| editor.navigate(data, cx)) } fn is_dirty(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 5971cbc07b..246a462019 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -241,7 +241,7 @@ fn deserialize_selection( } impl Item for Editor { - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Some(data) = data.downcast_ref::() { let buffer = self.buffer.read(cx).read(cx); let offset = if buffer.can_resolve(&data.anchor) { @@ -249,11 +249,19 @@ impl Item for Editor { } else { buffer.clip_offset(data.offset, Bias::Left) }; - + let newest_selection = self.newest_selection_with_snapshot::(&buffer); drop(buffer); - let nav_history = self.nav_history.take(); - self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); - self.nav_history = nav_history; + + if newest_selection.head() == offset { + false + } else { + let nav_history = self.nav_history.take(); + self.select_ranges([offset..offset], Some(Autoscroll::Fit), cx); + self.nav_history = nav_history; + true + } + } else { + false } } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1302040d19..212e1c66a7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -302,9 +302,9 @@ impl Item for ProjectSearchView { }); } - fn navigate(&mut self, data: Box, cx: &mut ViewContext) { + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { self.results_editor - .update(cx, |editor, cx| editor.navigate(data, cx)); + .update(cx, |editor, cx| editor.navigate(data, cx)) } fn should_update_tab_on_event(event: &ViewEvent) -> bool { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index e04af153ba..df30d48dbe 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -212,40 +212,47 @@ impl Pane { workspace.activate_pane(pane.clone(), cx); let to_load = pane.update(cx, |pane, cx| { - // Retrieve the weak item handle from the history. - let entry = pane.nav_history.borrow_mut().pop(mode)?; + loop { + // Retrieve the weak item handle from the history. + let entry = pane.nav_history.borrow_mut().pop(mode)?; - // If the item is still present in this pane, then activate it. - if let Some(index) = entry - .item - .upgrade(cx) - .and_then(|v| pane.index_for_item(v.as_ref())) - { - if let Some(item) = pane.active_item() { - pane.nav_history.borrow_mut().set_mode(mode); - item.deactivated(cx); - pane.nav_history + // If the item is still present in this pane, then activate it. + if let Some(index) = entry + .item + .upgrade(cx) + .and_then(|v| pane.index_for_item(v.as_ref())) + { + if let Some(item) = pane.active_item() { + pane.nav_history.borrow_mut().set_mode(mode); + item.deactivated(cx); + pane.nav_history + .borrow_mut() + .set_mode(NavigationMode::Normal); + } + + let prev_active_index = mem::replace(&mut pane.active_item_index, index); + pane.focus_active_item(cx); + let mut navigated = prev_active_index != pane.active_item_index; + if let Some(data) = entry.data { + navigated |= pane.active_item()?.navigate(data, cx); + } + + if navigated { + cx.notify(); + break None; + } + } + // If the item is no longer present in this pane, then retrieve its + // project path in order to reopen it. + else { + break pane + .nav_history .borrow_mut() - .set_mode(NavigationMode::Normal); + .paths_by_item + .get(&entry.item.id()) + .cloned() + .map(|project_path| (project_path, entry)); } - - pane.active_item_index = index; - pane.focus_active_item(cx); - if let Some(data) = entry.data { - pane.active_item()?.navigate(data, cx); - } - cx.notify(); - None - } - // If the item is no longer present in this pane, then retrieve its - // project path in order to reopen it. - else { - pane.nav_history - .borrow_mut() - .paths_by_item - .get(&entry.item.id()) - .cloned() - .map(|project_path| (project_path, entry)) } }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 691f17ca55..65300c5f34 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -203,7 +203,9 @@ pub struct JoinProjectParams { pub trait Item: View { fn deactivated(&mut self, _: &mut ViewContext) {} - fn navigate(&mut self, _: Box, _: &mut ViewContext) {} + fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { + false + } fn tab_content(&self, style: &theme::Tab, cx: &AppContext) -> ElementBox; fn project_path(&self, cx: &AppContext) -> Option; fn project_entry_id(&self, cx: &AppContext) -> Option; @@ -362,7 +364,7 @@ pub trait ItemHandle: 'static + fmt::Debug { cx: &mut ViewContext, ); fn deactivated(&self, cx: &mut MutableAppContext); - fn navigate(&self, data: Box, cx: &mut MutableAppContext); + fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool; fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; @@ -510,8 +512,8 @@ impl ItemHandle for ViewHandle { self.update(cx, |this, cx| this.deactivated(cx)); } - fn navigate(&self, data: Box, cx: &mut MutableAppContext) { - self.update(cx, |this, cx| this.navigate(data, cx)); + fn navigate(&self, data: Box, cx: &mut MutableAppContext) -> bool { + self.update(cx, |this, cx| this.navigate(data, cx)) } fn save(&self, project: ModelHandle, cx: &mut MutableAppContext) -> Task> { From ee9ed936e4f7a4ce58fc7b196f5345b9721b752c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 23 Mar 2022 12:15:36 -0600 Subject: [PATCH 093/139] Don't try keychain after authentication fails Previously, we were achieving this by deleting the keychain item, but this can sometimes fail which leads to an infinite loop. Now, we explicitly never try the keychain when reattempting authentication after authentication fails. Co-Authored-By: Max Brunsfeld --- crates/chat_panel/src/chat_panel.rs | 7 +++++- crates/client/src/client.rs | 34 ++++++++++++++--------------- crates/client/src/test.rs | 2 +- crates/project/src/project.rs | 2 +- crates/server/src/rpc.rs | 2 +- crates/zed/src/main.rs | 2 +- 6 files changed, 27 insertions(+), 22 deletions(-) diff --git a/crates/chat_panel/src/chat_panel.rs b/crates/chat_panel/src/chat_panel.rs index bed338d3f4..452f041c7b 100644 --- a/crates/chat_panel/src/chat_panel.rs +++ b/crates/chat_panel/src/chat_panel.rs @@ -327,7 +327,12 @@ impl ChatPanel { let rpc = rpc.clone(); let this = this.clone(); cx.spawn(|mut cx| async move { - if rpc.authenticate_and_connect(&cx).log_err().await.is_some() { + if rpc + .authenticate_and_connect(true, &cx) + .log_err() + .await + .is_some() + { cx.update(|cx| { if let Some(this) = this.upgrade(cx) { if this.is_focused(cx) { diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index d4a5a47b4b..db4478f489 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -55,7 +55,7 @@ action!(Authenticate); pub fn init(rpc: Arc, cx: &mut MutableAppContext) { cx.add_global_action(move |_: &Authenticate, cx| { let rpc = rpc.clone(); - cx.spawn(|cx| async move { rpc.authenticate_and_connect(&cx).log_err().await }) + cx.spawn(|cx| async move { rpc.authenticate_and_connect(true, &cx).log_err().await }) .detach(); }); } @@ -302,7 +302,7 @@ impl Client { state._reconnect_task = Some(cx.spawn(|cx| async move { let mut rng = StdRng::from_entropy(); let mut delay = Duration::from_millis(100); - while let Err(error) = this.authenticate_and_connect(&cx).await { + while let Err(error) = this.authenticate_and_connect(true, &cx).await { log::error!("failed to connect {}", error); this.set_status( Status::ReconnectionError { @@ -547,6 +547,7 @@ impl Client { #[async_recursion(?Send)] pub async fn authenticate_and_connect( self: &Arc, + try_keychain: bool, cx: &AsyncAppContext, ) -> anyhow::Result<()> { let was_disconnected = match *self.status().borrow() { @@ -568,23 +569,22 @@ impl Client { self.set_status(Status::Reauthenticating, cx) } - let mut used_keychain = false; - let credentials = self.state.read().credentials.clone(); - let credentials = if let Some(credentials) = credentials { - credentials - } else if let Some(credentials) = read_credentials_from_keychain(cx) { - used_keychain = true; - credentials - } else { - let credentials = match self.authenticate(&cx).await { + let mut read_from_keychain = false; + let mut credentials = self.state.read().credentials.clone(); + if credentials.is_none() && try_keychain { + credentials = read_credentials_from_keychain(cx); + read_from_keychain = credentials.is_some(); + } + if credentials.is_none() { + credentials = Some(match self.authenticate(&cx).await { Ok(credentials) => credentials, Err(err) => { self.set_status(Status::ConnectionError, cx); return Err(err); } - }; - credentials - }; + }); + } + let credentials = credentials.unwrap(); if was_disconnected { self.set_status(Status::Connecting, cx); @@ -595,7 +595,7 @@ impl Client { match self.establish_connection(&credentials, cx).await { Ok(conn) => { self.state.write().credentials = Some(credentials.clone()); - if !used_keychain && IMPERSONATE_LOGIN.is_none() { + if !read_from_keychain && IMPERSONATE_LOGIN.is_none() { write_credentials_to_keychain(&credentials, cx).log_err(); } self.set_connection(conn, cx).await; @@ -603,10 +603,10 @@ impl Client { } Err(EstablishConnectionError::Unauthorized) => { self.state.write().credentials.take(); - if used_keychain { + if read_from_keychain { cx.platform().delete_credentials(&ZED_SERVER_URL).log_err(); self.set_status(Status::SignedOut, cx); - self.authenticate_and_connect(cx).await + self.authenticate_and_connect(false, cx).await } else { self.set_status(Status::ConnectionError, cx); Err(EstablishConnectionError::Unauthorized)? diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index f630d9c0ee..35a8e85922 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -91,7 +91,7 @@ impl FakeServer { }); client - .authenticate_and_connect(&cx.to_async()) + .authenticate_and_connect(false, &cx.to_async()) .await .unwrap(); server diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c3c2f4e2a0..fbd3a1f7a6 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -377,7 +377,7 @@ impl Project { fs: Arc, cx: &mut AsyncAppContext, ) -> Result> { - client.authenticate_and_connect(&cx).await?; + client.authenticate_and_connect(true, &cx).await?; let response = client .request(proto::JoinProject { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 1020fd686a..68e435dfb1 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -5021,7 +5021,7 @@ mod tests { }); client - .authenticate_and_connect(&cx.to_async()) + .authenticate_and_connect(false, &cx.to_async()) .await .unwrap(); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 61967bfcdf..29dfd31427 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -82,7 +82,7 @@ fn main() { let client = client.clone(); |cx| async move { if client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(&cx).await?; + client.authenticate_and_connect(true, &cx).await?; } Ok::<_, anyhow::Error>(()) } From 657b92b02078688fcdaff715067ddfc260238b01 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 23 Mar 2022 12:18:17 -0600 Subject: [PATCH 094/139] Don't prompt for keychain access when launching from a pty Co-Authored-By: Max Brunsfeld --- crates/zed/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 29dfd31427..af0c98506d 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -81,7 +81,7 @@ fn main() { cx.spawn({ let client = client.clone(); |cx| async move { - if client.has_keychain_credentials(&cx) { + if !stdout_is_a_pty() && client.has_keychain_credentials(&cx) { client.authenticate_and_connect(true, &cx).await?; } Ok::<_, anyhow::Error>(()) From 4a42025c286a987b5f962419ea14576bbdede356 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 23 Mar 2022 12:25:06 -0600 Subject: [PATCH 095/139] Authenticate on startup if ZED_IMPERSONATE is assigned Co-Authored-By: Max Brunsfeld --- crates/client/src/client.rs | 2 +- crates/zed/src/main.rs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index db4478f489..1bae6cd49e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -45,7 +45,7 @@ pub use user::*; lazy_static! { static ref ZED_SERVER_URL: String = std::env::var("ZED_SERVER_URL").unwrap_or("https://zed.dev".to_string()); - static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") + pub static ref IMPERSONATE_LOGIN: Option = std::env::var("ZED_IMPERSONATE") .ok() .and_then(|s| if s.is_empty() { None } else { Some(s) }); } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index af0c98506d..b79f7f1bb0 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -81,8 +81,14 @@ fn main() { cx.spawn({ let client = client.clone(); |cx| async move { - if !stdout_is_a_pty() && client.has_keychain_credentials(&cx) { - client.authenticate_and_connect(true, &cx).await?; + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else { + if client.has_keychain_credentials(&cx) { + client.authenticate_and_connect(true, &cx).await?; + } } Ok::<_, anyhow::Error>(()) } From e36104f30dbc657f3ec68476ce7dd5073d8a7232 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 23 Mar 2022 11:32:18 -0700 Subject: [PATCH 096/139] Add navigation deduping Co-authored-by: Antonio Scandurra --- crates/editor/src/editor.rs | 2 +- crates/zed/src/zed.rs | 46 +++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1db7cc3e69..b396337e01 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5060,7 +5060,7 @@ impl Editor { cx.notify(); } - fn transact( + pub fn transact( &mut self, cx: &mut ViewContext, update: impl FnOnce(&mut Self, &mut ViewContext), diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c33a96a94b..1302d54067 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -893,6 +893,52 @@ mod tests { (file3.clone(), DisplayPoint::new(0, 0)) ); + // Modify file to remove nav history location, and ensure duplicates are skipped + editor1.update(cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(15, 0)..DisplayPoint::new(15, 0)], cx) + }); + + for _ in 0..5 { + editor1.update(cx, |editor, cx| { + editor + .select_display_ranges(&[DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0)], cx); + }); + editor1.update(cx, |editor, cx| { + editor.select_display_ranges( + &[DisplayPoint::new(13, 0)..DisplayPoint::new(13, 0)], + cx, + ) + }); + } + + editor1.update(cx, |editor, cx| { + editor.transact(cx, |editor, cx| { + editor.select_display_ranges( + &[DisplayPoint::new(2, 0)..DisplayPoint::new(14, 0)], + cx, + ); + editor.insert("", cx); + }) + }); + + editor1.update(cx, |editor, cx| { + editor.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx) + }); + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(2, 0)) + ); + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; + assert_eq!( + active_location(&workspace, cx), + (file1.clone(), DisplayPoint::new(3, 0)) + ); + fn active_location( workspace: &ViewHandle, cx: &mut TestAppContext, From f7e7d58f4947834fbeb56ca9b46d3ec7f6bb2d07 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Mar 2022 11:41:20 -0700 Subject: [PATCH 097/139] Avoid making LSP requests when moving cursor due to following --- crates/editor/src/editor.rs | 55 +++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2344d97fe2..4ef88a1d48 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5066,37 +5066,40 @@ impl Editor { cx, ); - let completion_menu = match self.context_menu.as_mut() { - Some(ContextMenu::Completions(menu)) => Some(menu), - _ => { - self.context_menu.take(); - None - } - }; + if local { + let completion_menu = match self.context_menu.as_mut() { + Some(ContextMenu::Completions(menu)) => Some(menu), + _ => { + self.context_menu.take(); + None + } + }; - if let Some(completion_menu) = completion_menu { - let cursor_position = new_cursor_position.to_offset(&buffer); - let (word_range, kind) = - buffer.surrounding_word(completion_menu.initial_position.clone()); - if kind == Some(CharKind::Word) && word_range.to_inclusive().contains(&cursor_position) + if let Some(completion_menu) = completion_menu { + let cursor_position = new_cursor_position.to_offset(&buffer); + let (word_range, kind) = + buffer.surrounding_word(completion_menu.initial_position.clone()); + if kind == Some(CharKind::Word) + && word_range.to_inclusive().contains(&cursor_position) + { + let query = Self::completion_query(&buffer, cursor_position); + cx.background() + .block(completion_menu.filter(query.as_deref(), cx.background().clone())); + self.show_completions(&ShowCompletions, cx); + } else { + self.hide_context_menu(cx); + } + } + + if old_cursor_position.to_display_point(&display_map).row() + != new_cursor_position.to_display_point(&display_map).row() { - let query = Self::completion_query(&buffer, cursor_position); - cx.background() - .block(completion_menu.filter(query.as_deref(), cx.background().clone())); - self.show_completions(&ShowCompletions, cx); - } else { - self.hide_context_menu(cx); + self.available_code_actions.take(); } + self.refresh_code_actions(cx); + self.refresh_document_highlights(cx); } - if old_cursor_position.to_display_point(&display_map).row() - != new_cursor_position.to_display_point(&display_map).row() - { - self.available_code_actions.take(); - } - self.refresh_code_actions(cx); - self.refresh_document_highlights(cx); - self.pause_cursor_blinking(cx); cx.emit(Event::SelectionsChanged { local }); } From 483a84a7f169096a5589f27e29af6df5b12e5538 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Mar 2022 12:09:07 -0700 Subject: [PATCH 098/139] 0.22 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd93ae3e27..01d398b51c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5932,7 +5932,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "zed" -version = "0.21.0" +version = "0.22.0" dependencies = [ "anyhow", "async-compression", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index cbf9389fd4..ca0eec353e 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.21.0" +version = "0.22.0" [lib] name = "zed" From 22148a3639096dc3c004d41370a1a0bd99f865fa Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Mar 2022 15:37:28 -0700 Subject: [PATCH 099/139] Fix extending selections starting at ends of other nodes Fixes #478 --- crates/language/src/buffer.rs | 12 +++++++++-- crates/language/src/tests.rs | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f32028002a..28f2c0510d 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1621,8 +1621,13 @@ impl BufferSnapshot { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut cursor = tree.root_node().walk(); - // Descend to smallest leaf that touches or exceeds the start of the range. - while cursor.goto_first_child_for_byte(range.start).is_some() {} + // Descend to the first leaf that touches the start of the range, + // and if the range is non-empty, extends beyond the start. + while cursor.goto_first_child_for_byte(range.start).is_some() { + if !range.is_empty() && cursor.node().end_byte() == range.start { + cursor.goto_next_sibling(); + } + } // Ascend to the smallest ancestor that strictly contains the range. loop { @@ -1656,6 +1661,9 @@ impl BufferSnapshot { } } + // If there is a candidate node on both sides of the (empty) range, then + // decide between the two by favoring a named node over an anonymous token. + // If both nodes are the same in that regard, favor the right one. if let Some(right_node) = right_node { if right_node.is_named() || !left_node.is_named() { return Some(right_node.byte_range()); diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index d36771c44f..19b1a1cf73 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -458,6 +458,44 @@ fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) { ); } +#[gpui::test] +fn test_range_for_syntax_ancestor(cx: &mut MutableAppContext) { + cx.add_model(|cx| { + let text = "fn a() { b(|c| {}) }"; + let buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let snapshot = buffer.snapshot(); + + assert_eq!( + snapshot.range_for_syntax_ancestor(empty_range_at(text, "|")), + Some(range_of(text, "|")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|")), + Some(range_of(text, "|c|")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|c|")), + Some(range_of(text, "|c| {}")) + ); + assert_eq!( + snapshot.range_for_syntax_ancestor(range_of(text, "|c| {}")), + Some(range_of(text, "(|c| {})")) + ); + + buffer + }); + + fn empty_range_at(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + } + + fn range_of(text: &str, part: &str) -> Range { + let start = text.find(part).unwrap(); + start..start + part.len() + } +} + #[gpui::test] fn test_edit_with_autoindent(cx: &mut MutableAppContext) { cx.add_model(|cx| { From ff4bdb31142171ff373d88eae80fec18acab9e12 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Mar 2022 18:20:55 -0700 Subject: [PATCH 100/139] Fix incorrect highlighting when an empty range is highlighted via the DisplayMap Co-Authored-By: Keith Simmons --- crates/editor/src/display_map.rs | 130 +++++++++++++++++++--- crates/editor/src/display_map/fold_map.rs | 2 +- crates/editor/src/editor.rs | 12 +- crates/editor/src/test.rs | 23 ++++ 4 files changed, 149 insertions(+), 18 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6f31541137..1f825e175c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -489,13 +489,17 @@ impl ToDisplayPoint for Anchor { #[cfg(test)] mod tests { use super::*; - use crate::movement; + use crate::{ + movement, + test::{marked_text, marked_text_ranges}, + }; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; use rand::{prelude::*, Rng}; use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; + use unindent::Unindent; use util::test::sample_text; use Bias::*; @@ -929,7 +933,7 @@ mod tests { let map = cx .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); assert_eq!( - cx.update(|cx| chunks(0..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), vec![ ("fn ".to_string(), None), ("outer".to_string(), Some(Color::blue())), @@ -940,7 +944,7 @@ mod tests { ] ); assert_eq!( - cx.update(|cx| chunks(3..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), vec![ (" fn ".to_string(), Some(Color::red())), ("inner".to_string(), Some(Color::blue())), @@ -952,7 +956,7 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| chunks(0..2, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..2, &map, &theme, cx)), vec![ ("fn ".to_string(), None), ("out".to_string(), Some(Color::blue())), @@ -1018,7 +1022,7 @@ mod tests { DisplayMap::new(buffer, tab_size, font_id, font_size, Some(40.0), 1, 1, cx) }); assert_eq!( - cx.update(|cx| chunks(0..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(0..5, &map, &theme, cx)), [ ("fn \n".to_string(), None), ("oute\nr".to_string(), Some(Color::blue())), @@ -1026,7 +1030,7 @@ mod tests { ] ); assert_eq!( - cx.update(|cx| chunks(3..5, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(3..5, &map, &theme, cx)), [("{}\n\n".to_string(), None)] ); @@ -1034,7 +1038,7 @@ mod tests { map.fold(vec![Point::new(0, 6)..Point::new(3, 2)], cx) }); assert_eq!( - cx.update(|cx| chunks(1..4, &map, &theme, cx)), + cx.update(|cx| syntax_chunks(1..4, &map, &theme, cx)), [ ("out".to_string(), Some(Color::blue())), ("…\n".to_string(), None), @@ -1044,6 +1048,89 @@ mod tests { ); } + #[gpui::test] + async fn test_chunks_with_text_highlights(cx: &mut gpui::TestAppContext) { + cx.foreground().set_block_on_ticks(usize::MAX..=usize::MAX); + + let theme = SyntaxTheme::new(vec![ + ("operator".to_string(), Color::red().into()), + ("string".to_string(), Color::green().into()), + ]); + let language = Arc::new( + Language::new( + LanguageConfig { + name: "Test".into(), + path_suffixes: vec![".test".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_highlights_query( + r#" + ":" @operator + (string_literal) @string + "#, + ) + .unwrap(), + ); + language.set_theme(&theme); + + let (text, highlighted_ranges) = marked_text_ranges( + r#"const{} : B = "c [d]""#, + vec![('{', '}'), ('<', '>'), ('[', ']')], + ); + + let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let buffer_snapshot = buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx)); + + let font_cache = cx.font_cache(); + let tab_size = 4; + let family_id = font_cache.load_family(&["Courier"]).unwrap(); + let font_id = font_cache + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 16.0; + let map = cx + .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); + + enum MyType {} + + let style = HighlightStyle { + color: Some(Color::blue()), + ..Default::default() + }; + + map.update(cx, |map, _cx| { + map.highlight_text( + TypeId::of::(), + highlighted_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_before(range.end) + }) + .collect(), + style, + ); + }); + + assert_eq!( + cx.update(|cx| chunks(0..10, &map, &theme, cx)), + [ + ("const ".to_string(), None, None), + ("a".to_string(), None, Some(Color::blue())), + (":".to_string(), Some(Color::red()), None), + (" B = ".to_string(), None, None), + ("\"c ".to_string(), Some(Color::green()), None), + ("d".to_string(), Some(Color::green()), Some(Color::blue())), + ("\"".to_string(), Some(Color::green()), None), + ] + ); + } + #[gpui::test] fn test_clip_point(cx: &mut gpui::MutableAppContext) { use Bias::{Left, Right}; @@ -1170,27 +1257,38 @@ mod tests { ) } - fn chunks<'a>( + fn syntax_chunks<'a>( rows: Range, map: &ModelHandle, theme: &'a SyntaxTheme, cx: &mut MutableAppContext, ) -> Vec<(String, Option)> { + chunks(rows, map, theme, cx) + .into_iter() + .map(|(text, color, _)| (text, color)) + .collect() + } + + fn chunks<'a>( + rows: Range, + map: &ModelHandle, + theme: &'a SyntaxTheme, + cx: &mut MutableAppContext, + ) -> Vec<(String, Option, Option)> { let snapshot = map.update(cx, |map, cx| map.snapshot(cx)); - let mut chunks: Vec<(String, Option)> = Vec::new(); + let mut chunks: Vec<(String, Option, Option)> = Vec::new(); for chunk in snapshot.chunks(rows, true) { - let color = chunk + let syntax_color = chunk .syntax_highlight_id .and_then(|id| id.style(theme)?.color); - if let Some((last_chunk, last_color)) = chunks.last_mut() { - if color == *last_color { + let highlight_color = chunk.highlight_style.and_then(|style| style.color); + if let Some((last_chunk, last_syntax_color, last_highlight_color)) = chunks.last_mut() { + if syntax_color == *last_syntax_color && highlight_color == *last_highlight_color { last_chunk.push_str(chunk.text); - } else { - chunks.push((chunk.text.to_string(), color)); + continue; } - } else { - chunks.push((chunk.text.to_string(), color)); } + chunks.push((chunk.text.to_string(), syntax_color, highlight_color)); } chunks } diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index ac4477263d..7f744afa40 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1156,7 +1156,7 @@ impl Ord for HighlightEndpoint { fn cmp(&self, other: &Self) -> Ordering { self.offset .cmp(&other.offset) - .then_with(|| self.is_start.cmp(&other.is_start)) + .then_with(|| other.is_start.cmp(&self.is_start)) } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6722d2b1fb..ba95d33d39 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4,6 +4,8 @@ pub mod items; pub mod movement; mod multi_buffer; +pub mod repro; + #[cfg(test)] mod test; @@ -4404,7 +4406,15 @@ impl Editor { .into_iter() .flat_map(|(_, ranges)| ranges), ) - .collect(); + .collect::>(); + + let snapshot = this.buffer.read(cx).snapshot(cx); + let point_ranges = ranges + .iter() + .map(|range| range.to_point(&snapshot)) + .collect::>(); + dbg!(point_ranges); + this.highlight_text::( ranges, HighlightStyle { diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 4908016ef5..566ca9b2db 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,3 +1,5 @@ +use std::ops::Range; + use collections::HashMap; #[cfg(test)] @@ -31,3 +33,24 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']); (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) } + +pub fn marked_text_ranges( + marked_text: &str, + range_markers: Vec<(char, char)>, +) -> (String, Vec>) { + let mut marker_chars = Vec::new(); + for (start, end) in range_markers.iter() { + marker_chars.push(*start); + marker_chars.push(*end); + } + let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars); + let ranges = range_markers + .iter() + .map(|(start_marker, end_marker)| { + let start = markers.get(start_marker).unwrap()[0]; + let end = markers.get(end_marker).unwrap()[0]; + start..end + }) + .collect(); + (unmarked_text, ranges) +} From 864bede8a235122552e71c978a6cb34fed6ed846 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 08:48:06 +0100 Subject: [PATCH 101/139] Use `Editor::transact` everywhere This is in preparation of emitting an edit event every time a transaction ends or it is undone/redone. --- crates/editor/src/editor.rs | 847 ++++++++++++++++++------------------ 1 file changed, 423 insertions(+), 424 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6722d2b1fb..372d620ed7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1744,133 +1744,134 @@ impl Editor { pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { let text = action.0.as_ref(); if !self.skip_autoclose_end(text, cx) { - self.start_transaction(cx); - if !self.surround_with_bracket_pair(text, cx) { - self.insert(text, cx); - self.autoclose_bracket_pairs(cx); - } - self.end_transaction(cx); + self.transact(cx, |this, cx| { + if !this.surround_with_bracket_pair(text, cx) { + this.insert(text, cx); + this.autoclose_bracket_pairs(cx); + } + }); self.trigger_completion_on_input(text, cx); } } pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext) { - self.start_transaction(cx); - let mut old_selections = SmallVec::<[_; 32]>::new(); - { - let selections = self.local_selections::(cx); - let buffer = self.buffer.read(cx).snapshot(cx); - for selection in selections.iter() { - let start_point = selection.start.to_point(&buffer); - let indent = buffer - .indent_column_for_line(start_point.row) - .min(start_point.column); - let start = selection.start; - let end = selection.end; + self.transact(cx, |this, cx| { + let mut old_selections = SmallVec::<[_; 32]>::new(); + { + let selections = this.local_selections::(cx); + let buffer = this.buffer.read(cx).snapshot(cx); + for selection in selections.iter() { + let start_point = selection.start.to_point(&buffer); + let indent = buffer + .indent_column_for_line(start_point.row) + .min(start_point.column); + let start = selection.start; + let end = selection.end; - let mut insert_extra_newline = false; - if let Some(language) = buffer.language() { - let leading_whitespace_len = buffer - .reversed_chars_at(start) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); + let mut insert_extra_newline = false; + if let Some(language) = buffer.language() { + let leading_whitespace_len = buffer + .reversed_chars_at(start) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); - let trailing_whitespace_len = buffer - .chars_at(end) - .take_while(|c| c.is_whitespace() && *c != '\n') - .map(|c| c.len_utf8()) - .sum::(); + let trailing_whitespace_len = buffer + .chars_at(end) + .take_while(|c| c.is_whitespace() && *c != '\n') + .map(|c| c.len_utf8()) + .sum::(); - insert_extra_newline = language.brackets().iter().any(|pair| { - let pair_start = pair.start.trim_end(); - let pair_end = pair.end.trim_start(); + insert_extra_newline = language.brackets().iter().any(|pair| { + let pair_start = pair.start.trim_end(); + let pair_end = pair.end.trim_start(); - pair.newline - && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) - && buffer.contains_str_at( - (start - leading_whitespace_len).saturating_sub(pair_start.len()), - pair_start, - ) - }); - } - - old_selections.push(( - selection.id, - buffer.anchor_after(end), - start..end, - indent, - insert_extra_newline, - )); - } - } - - self.buffer.update(cx, |buffer, cx| { - let mut delta = 0_isize; - let mut pending_edit: Option = None; - for (_, _, range, indent, insert_extra_newline) in &old_selections { - if pending_edit.as_ref().map_or(false, |pending| { - pending.indent != *indent - || pending.insert_extra_newline != *insert_extra_newline - }) { - let pending = pending_edit.take().unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); + pair.newline + && buffer.contains_str_at(end + trailing_whitespace_len, pair_end) + && buffer.contains_str_at( + (start - leading_whitespace_len) + .saturating_sub(pair_start.len()), + pair_start, + ) + }); } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - delta += pending.delta; - } - let start = (range.start as isize + delta) as usize; - let end = (range.end as isize + delta) as usize; - let mut text_len = *indent as usize + 1; - if *insert_extra_newline { - text_len *= 2; + old_selections.push(( + selection.id, + buffer.anchor_after(end), + start..end, + indent, + insert_extra_newline, + )); } - - let pending = pending_edit.get_or_insert_with(Default::default); - pending.delta += text_len as isize - (end - start) as isize; - pending.indent = *indent; - pending.insert_extra_newline = *insert_extra_newline; - pending.ranges.push(start..end); } - let pending = pending_edit.unwrap(); - let mut new_text = String::with_capacity(1 + pending.indent as usize); - new_text.push('\n'); - new_text.extend(iter::repeat(' ').take(pending.indent as usize)); - if pending.insert_extra_newline { - new_text = new_text.repeat(2); - } - buffer.edit_with_autoindent(pending.ranges, new_text, cx); - - let buffer = buffer.read(cx); - self.selections = self - .selections - .iter() - .cloned() - .zip(old_selections) - .map( - |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { - let mut cursor = end_anchor.to_point(&buffer); - if insert_extra_newline { - cursor.row -= 1; - cursor.column = buffer.line_len(cursor.row); + this.buffer.update(cx, |buffer, cx| { + let mut delta = 0_isize; + let mut pending_edit: Option = None; + for (_, _, range, indent, insert_extra_newline) in &old_selections { + if pending_edit.as_ref().map_or(false, |pending| { + pending.indent != *indent + || pending.insert_extra_newline != *insert_extra_newline + }) { + let pending = pending_edit.take().unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); } - let anchor = buffer.anchor_after(cursor); - new_selection.start = anchor.clone(); - new_selection.end = anchor; - new_selection - }, - ) - .collect(); - }); + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + delta += pending.delta; + } - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); + let start = (range.start as isize + delta) as usize; + let end = (range.end as isize + delta) as usize; + let mut text_len = *indent as usize + 1; + if *insert_extra_newline { + text_len *= 2; + } + + let pending = pending_edit.get_or_insert_with(Default::default); + pending.delta += text_len as isize - (end - start) as isize; + pending.indent = *indent; + pending.insert_extra_newline = *insert_extra_newline; + pending.ranges.push(start..end); + } + + let pending = pending_edit.unwrap(); + let mut new_text = String::with_capacity(1 + pending.indent as usize); + new_text.push('\n'); + new_text.extend(iter::repeat(' ').take(pending.indent as usize)); + if pending.insert_extra_newline { + new_text = new_text.repeat(2); + } + buffer.edit_with_autoindent(pending.ranges, new_text, cx); + + let buffer = buffer.read(cx); + this.selections = this + .selections + .iter() + .cloned() + .zip(old_selections) + .map( + |(mut new_selection, (_, end_anchor, _, _, insert_extra_newline))| { + let mut cursor = end_anchor.to_point(&buffer); + if insert_extra_newline { + cursor.row -= 1; + cursor.column = buffer.line_len(cursor.row); + } + let anchor = buffer.anchor_after(cursor); + new_selection.start = anchor.clone(); + new_selection.end = anchor; + new_selection + }, + ) + .collect(); + }); + + this.request_autoscroll(Autoscroll::Fit, cx); + }); #[derive(Default)] struct PendingEdit { @@ -1882,40 +1883,39 @@ impl Editor { } pub fn insert(&mut self, text: &str, cx: &mut ViewContext) { - self.start_transaction(cx); + self.transact(cx, |this, cx| { + let old_selections = this.local_selections::(cx); + let selection_anchors = this.buffer.update(cx, |buffer, cx| { + let anchors = { + let snapshot = buffer.read(cx); + old_selections + .iter() + .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) + .collect::>() + }; + let edit_ranges = old_selections.iter().map(|s| s.start..s.end); + buffer.edit_with_autoindent(edit_ranges, text, cx); + anchors + }); - let old_selections = self.local_selections::(cx); - let selection_anchors = self.buffer.update(cx, |buffer, cx| { - let anchors = { - let snapshot = buffer.read(cx); - old_selections - .iter() - .map(|s| (s.id, s.goal, snapshot.anchor_after(s.end))) - .collect::>() + let selections = { + let snapshot = this.buffer.read(cx).read(cx); + selection_anchors + .into_iter() + .map(|(id, goal, position)| { + let position = position.to_offset(&snapshot); + Selection { + id, + start: position, + end: position, + goal, + reversed: false, + } + }) + .collect() }; - let edit_ranges = old_selections.iter().map(|s| s.start..s.end); - buffer.edit_with_autoindent(edit_ranges, text, cx); - anchors + this.update_selections(selections, Some(Autoscroll::Fit), cx); }); - - let selections = { - let snapshot = self.buffer.read(cx).read(cx); - selection_anchors - .into_iter() - .map(|(id, goal, position)| { - let position = position.to_offset(&snapshot); - Selection { - id, - start: position, - end: position, - goal, - reversed: false, - } - }) - .collect() - }; - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } fn trigger_completion_on_input(&mut self, text: &str, cx: &mut ViewContext) { @@ -2284,21 +2284,21 @@ impl Editor { } let text = &text[common_prefix_len..]; - self.start_transaction(cx); - if let Some(mut snippet) = snippet { - snippet.text = text.to_string(); - for tabstop in snippet.tabstops.iter_mut().flatten() { - tabstop.start -= common_prefix_len as isize; - tabstop.end -= common_prefix_len as isize; - } + self.transact(cx, |this, cx| { + if let Some(mut snippet) = snippet { + snippet.text = text.to_string(); + for tabstop in snippet.tabstops.iter_mut().flatten() { + tabstop.start -= common_prefix_len as isize; + tabstop.end -= common_prefix_len as isize; + } - self.insert_snippet(&ranges, snippet, cx).log_err(); - } else { - self.buffer.update(cx, |buffer, cx| { - buffer.edit_with_autoindent(ranges, text, cx); - }); - } - self.end_transaction(cx); + this.insert_snippet(&ranges, snippet, cx).log_err(); + } else { + this.buffer.update(cx, |buffer, cx| { + buffer.edit_with_autoindent(ranges, text, cx); + }); + } + }); let project = self.project.clone()?; let apply_edits = project.update(cx, |project, cx| { @@ -2732,14 +2732,13 @@ impl Editor { } pub fn clear(&mut self, cx: &mut ViewContext) { - self.start_transaction(cx); - self.select_all(&SelectAll, cx); - self.insert("", cx); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.select_all(&SelectAll, cx); + this.insert("", cx); + }); } pub fn backspace(&mut self, _: &Backspace, cx: &mut ViewContext) { - self.start_transaction(cx); let mut selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in &mut selections { @@ -2765,9 +2764,11 @@ impl Editor { selection.set_head(new_head, SelectionGoal::None); } } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); + + self.transact(cx, |this, cx| { + this.update_selections(selections, Some(Autoscroll::Fit), cx); + this.insert("", cx); + }); } pub fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { @@ -2787,75 +2788,76 @@ impl Editor { return; } - self.start_transaction(cx); let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); - let mut last_indent = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - if selection.is_empty() { - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column % tab_size); - buffer.edit( - [selection.start..selection.start], - " ".repeat(chars_to_next_tab_stop), - cx, - ); - selection.start.column += chars_to_next_tab_stop as u32; - selection.end = selection.start; - } else { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 { - end_row -= 1; - } - - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if let Some((last_indent_row, last_indent_len)) = last_indent { - if last_indent_row == selection.start.row { - selection.start.column += last_indent_len; - start_row += 1; - } - if last_indent_row == selection.end.row { - selection.end.column += last_indent_len; - } - } - - for row in start_row..end_row { - let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; - let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); - let row_start = Point::new(row, 0); + self.transact(cx, |this, cx| { + let mut last_indent = None; + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + if selection.is_empty() { + let char_column = buffer + .read(cx) + .text_for_range(Point::new(selection.start.row, 0)..selection.start) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); buffer.edit( - [row_start..row_start], - " ".repeat(columns_to_next_tab_stop), + [selection.start..selection.start], + " ".repeat(chars_to_next_tab_stop), cx, ); + selection.start.column += chars_to_next_tab_stop as u32; + selection.end = selection.start; + } else { + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += columns_to_next_tab_stop as u32; - } - if row == selection.end.row { - selection.end.column += columns_to_next_tab_stop as u32; + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; } - last_indent = Some((row, columns_to_next_tab_stop as u32)); + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if let Some((last_indent_row, last_indent_len)) = last_indent { + if last_indent_row == selection.start.row { + selection.start.column += last_indent_len; + start_row += 1; + } + if last_indent_row == selection.end.row { + selection.end.column += last_indent_len; + } + } + + for row in start_row..end_row { + let indent_column = + buffer.read(cx).indent_column_for_line(row) as usize; + let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); + let row_start = Point::new(row, 0); + buffer.edit( + [row_start..row_start], + " ".repeat(columns_to_next_tab_stop), + cx, + ); + + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += columns_to_next_tab_stop as u32; + } + if row == selection.end.row { + selection.end.column += columns_to_next_tab_stop as u32; + } + + last_indent = Some((row, columns_to_next_tab_stop as u32)); + } } } - } - }); + }); - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); + this.update_selections(selections, Some(Autoscroll::Fit), cx); + }); } pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { @@ -2864,7 +2866,6 @@ impl Editor { return; } - self.start_transaction(cx); let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -2896,21 +2897,20 @@ impl Editor { } } } - self.buffer.update(cx, |buffer, cx| { - buffer.edit(deletion_ranges, "", cx); - }); - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(deletion_ranges, "", cx); + }); + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); + }); } pub fn delete_line(&mut self, _: &DeleteLine, cx: &mut ViewContext) { - self.start_transaction(cx); - let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = self.buffer.read(cx).snapshot(cx); @@ -2960,30 +2960,29 @@ impl Editor { edit_ranges.push(edit_start..edit_end); } - let buffer = self.buffer.update(cx, |buffer, cx| { - buffer.edit(edit_ranges, "", cx); - buffer.snapshot(cx) + self.transact(cx, |this, cx| { + let buffer = this.buffer.update(cx, |buffer, cx| { + buffer.edit(edit_ranges, "", cx); + buffer.snapshot(cx) + }); + let new_selections = new_cursors + .into_iter() + .map(|(id, cursor)| { + let cursor = cursor.to_point(&buffer); + Selection { + id, + start: cursor, + end: cursor, + reversed: false, + goal: SelectionGoal::None, + } + }) + .collect(); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - let new_selections = new_cursors - .into_iter() - .map(|(id, cursor)| { - let cursor = cursor.to_point(&buffer); - Selection { - id, - start: cursor, - end: cursor, - reversed: false, - goal: SelectionGoal::None, - } - }) - .collect(); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { - self.start_transaction(cx); - let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -3014,14 +3013,15 @@ impl Editor { edits.push((start, text, rows.len() as u32)); } - self.buffer.update(cx, |buffer, cx| { - for (point, text, _) in edits.into_iter().rev() { - buffer.edit(Some(point..point), text, cx); - } - }); + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + for (point, text, _) in edits.into_iter().rev() { + buffer.edit(Some(point..point), text, cx); + } + }); - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); + this.request_autoscroll(Autoscroll::Fit, cx); + }); } pub fn move_line_up(&mut self, _: &MoveLineUp, cx: &mut ViewContext) { @@ -3122,16 +3122,16 @@ impl Editor { new_selections.extend(contiguous_row_selections.drain(..)); } - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + this.fold_ranges(refold_ranges, cx); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn move_line_down(&mut self, _: &MoveLineDown, cx: &mut ViewContext) { @@ -3225,20 +3225,19 @@ impl Editor { new_selections.extend(contiguous_row_selections.drain(..)); } - self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); - self.buffer.update(cx, |buffer, cx| { - for (range, text) in edits { - buffer.edit([range], text, cx); - } + self.transact(cx, |this, cx| { + this.unfold_ranges(unfold_ranges, cx); + this.buffer.update(cx, |buffer, cx| { + for (range, text) in edits { + buffer.edit([range], text, cx); + } + }); + this.fold_ranges(refold_ranges, cx); + this.update_selections(new_selections, Some(Autoscroll::Fit), cx); }); - self.fold_ranges(refold_ranges, cx); - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); - self.end_transaction(cx); } pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - self.start_transaction(cx); let mut text = String::new(); let mut selections = self.local_selections::(cx); let mut clipboard_selections = Vec::with_capacity(selections.len()); @@ -3263,12 +3262,12 @@ impl Editor { }); } } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - self.insert("", cx); - self.end_transaction(cx); - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + self.transact(cx, |this, cx| { + this.update_selections(selections, Some(Autoscroll::Fit), cx); + this.insert("", cx); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + }); } pub fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { @@ -3298,63 +3297,65 @@ impl Editor { } } - cx.as_mut() - .write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); + cx.write_to_clipboard(ClipboardItem::new(text).with_metadata(clipboard_selections)); } pub fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - if let Some(item) = cx.as_mut().read_from_clipboard() { - let clipboard_text = item.text(); - if let Some(mut clipboard_selections) = item.metadata::>() { - let mut selections = self.local_selections::(cx); - let all_selections_were_entire_line = - clipboard_selections.iter().all(|s| s.is_entire_line); - if clipboard_selections.len() != selections.len() { - clipboard_selections.clear(); - } - - let mut delta = 0_isize; - let mut start_offset = 0; - for (i, selection) in selections.iter_mut().enumerate() { - let to_insert; - let entire_line; - if let Some(clipboard_selection) = clipboard_selections.get(i) { - let end_offset = start_offset + clipboard_selection.len; - to_insert = &clipboard_text[start_offset..end_offset]; - entire_line = clipboard_selection.is_entire_line; - start_offset = end_offset - } else { - to_insert = clipboard_text.as_str(); - entire_line = all_selections_were_entire_line; + self.transact(cx, |this, cx| { + if let Some(item) = cx.as_mut().read_from_clipboard() { + let clipboard_text = item.text(); + if let Some(mut clipboard_selections) = item.metadata::>() { + let mut selections = this.local_selections::(cx); + let all_selections_were_entire_line = + clipboard_selections.iter().all(|s| s.is_entire_line); + if clipboard_selections.len() != selections.len() { + clipboard_selections.clear(); } - selection.start = (selection.start as isize + delta) as usize; - selection.end = (selection.end as isize + delta) as usize; - - self.buffer.update(cx, |buffer, cx| { - // If the corresponding selection was empty when this slice of the - // clipboard text was written, then the entire line containing the - // selection was copied. If this selection is also currently empty, - // then paste the line before the current line of the buffer. - let range = if selection.is_empty() && entire_line { - let column = selection.start.to_point(&buffer.read(cx)).column as usize; - let line_start = selection.start - column; - line_start..line_start + let mut delta = 0_isize; + let mut start_offset = 0; + for (i, selection) in selections.iter_mut().enumerate() { + let to_insert; + let entire_line; + if let Some(clipboard_selection) = clipboard_selections.get(i) { + let end_offset = start_offset + clipboard_selection.len; + to_insert = &clipboard_text[start_offset..end_offset]; + entire_line = clipboard_selection.is_entire_line; + start_offset = end_offset } else { - selection.start..selection.end - }; + to_insert = clipboard_text.as_str(); + entire_line = all_selections_were_entire_line; + } - delta += to_insert.len() as isize - range.len() as isize; - buffer.edit([range], to_insert, cx); - selection.start += to_insert.len(); - selection.end = selection.start; - }); + selection.start = (selection.start as isize + delta) as usize; + selection.end = (selection.end as isize + delta) as usize; + + this.buffer.update(cx, |buffer, cx| { + // If the corresponding selection was empty when this slice of the + // clipboard text was written, then the entire line containing the + // selection was copied. If this selection is also currently empty, + // then paste the line before the current line of the buffer. + let range = if selection.is_empty() && entire_line { + let column = + selection.start.to_point(&buffer.read(cx)).column as usize; + let line_start = selection.start - column; + line_start..line_start + } else { + selection.start..selection.end + }; + + delta += to_insert.len() as isize - range.len() as isize; + buffer.edit([range], to_insert, cx); + selection.start += to_insert.len(); + selection.end = selection.start; + }); + } + this.update_selections(selections, Some(Autoscroll::Fit), cx); + } else { + this.insert(clipboard_text, cx); } - self.update_selections(selections, Some(Autoscroll::Fit), cx); - } else { - self.insert(clipboard_text, cx); } - } + }); } pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { @@ -3962,90 +3963,94 @@ impl Editor { let comment_prefix = full_comment_prefix.trim_end_matches(' '); let comment_prefix_whitespace = &full_comment_prefix[comment_prefix.len()..]; - self.start_transaction(cx); - let mut selections = self.local_selections::(cx); - let mut all_selection_lines_are_comments = true; - let mut edit_ranges = Vec::new(); - let mut last_toggled_row = None; - self.buffer.update(cx, |buffer, cx| { - for selection in &mut selections { - edit_ranges.clear(); - let snapshot = buffer.snapshot(cx); + self.transact(cx, |this, cx| { + let mut selections = this.local_selections::(cx); + let mut all_selection_lines_are_comments = true; + let mut edit_ranges = Vec::new(); + let mut last_toggled_row = None; + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + edit_ranges.clear(); + let snapshot = buffer.snapshot(cx); - let end_row = - if selection.end.row > selection.start.row && selection.end.column == 0 { - selection.end.row - } else { - selection.end.row + 1 - }; + let end_row = + if selection.end.row > selection.start.row && selection.end.column == 0 { + selection.end.row + } else { + selection.end.row + 1 + }; - for row in selection.start.row..end_row { - // If multiple selections contain a given row, avoid processing that - // row more than once. - if last_toggled_row == Some(row) { - continue; - } else { - last_toggled_row = Some(row); + for row in selection.start.row..end_row { + // If multiple selections contain a given row, avoid processing that + // row more than once. + if last_toggled_row == Some(row) { + continue; + } else { + last_toggled_row = Some(row); + } + + if snapshot.is_line_blank(row) { + continue; + } + + let start = Point::new(row, snapshot.indent_column_for_line(row)); + let mut line_bytes = snapshot + .bytes_in_range(start..snapshot.max_point()) + .flatten() + .copied(); + + // If this line currently begins with the line comment prefix, then record + // the range containing the prefix. + if all_selection_lines_are_comments + && line_bytes + .by_ref() + .take(comment_prefix.len()) + .eq(comment_prefix.bytes()) + { + // Include any whitespace that matches the comment prefix. + let matching_whitespace_len = line_bytes + .zip(comment_prefix_whitespace.bytes()) + .take_while(|(a, b)| a == b) + .count() + as u32; + let end = Point::new( + row, + start.column + + comment_prefix.len() as u32 + + matching_whitespace_len, + ); + edit_ranges.push(start..end); + } + // If this line does not begin with the line comment prefix, then record + // the position where the prefix should be inserted. + else { + all_selection_lines_are_comments = false; + edit_ranges.push(start..start); + } } - if snapshot.is_line_blank(row) { - continue; - } - - let start = Point::new(row, snapshot.indent_column_for_line(row)); - let mut line_bytes = snapshot - .bytes_in_range(start..snapshot.max_point()) - .flatten() - .copied(); - - // If this line currently begins with the line comment prefix, then record - // the range containing the prefix. - if all_selection_lines_are_comments - && line_bytes - .by_ref() - .take(comment_prefix.len()) - .eq(comment_prefix.bytes()) - { - // Include any whitespace that matches the comment prefix. - let matching_whitespace_len = line_bytes - .zip(comment_prefix_whitespace.bytes()) - .take_while(|(a, b)| a == b) - .count() as u32; - let end = Point::new( - row, - start.column + comment_prefix.len() as u32 + matching_whitespace_len, - ); - edit_ranges.push(start..end); - } - // If this line does not begin with the line comment prefix, then record - // the position where the prefix should be inserted. - else { - all_selection_lines_are_comments = false; - edit_ranges.push(start..start); + if !edit_ranges.is_empty() { + if all_selection_lines_are_comments { + buffer.edit(edit_ranges.iter().cloned(), "", cx); + } else { + let min_column = + edit_ranges.iter().map(|r| r.start.column).min().unwrap(); + let edit_ranges = edit_ranges.iter().map(|range| { + let position = Point::new(range.start.row, min_column); + position..position + }); + buffer.edit(edit_ranges, &full_comment_prefix, cx); + } } } + }); - if !edit_ranges.is_empty() { - if all_selection_lines_are_comments { - buffer.edit(edit_ranges.iter().cloned(), "", cx); - } else { - let min_column = edit_ranges.iter().map(|r| r.start.column).min().unwrap(); - let edit_ranges = edit_ranges.iter().map(|range| { - let position = Point::new(range.start.row, min_column); - position..position - }); - buffer.edit(edit_ranges, &full_comment_prefix, cx); - } - } - } + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + cx, + ); }); - - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); } pub fn select_larger_syntax_node( @@ -5117,13 +5122,9 @@ impl Editor { cx: &mut ViewContext, update: impl FnOnce(&mut Self, &mut ViewContext), ) { - self.start_transaction(cx); - update(self, cx); - self.end_transaction(cx); - } - - fn start_transaction(&mut self, cx: &mut ViewContext) { self.start_transaction_at(Instant::now(), cx); + update(self, cx); + self.end_transaction_at(Instant::now(), cx); } fn start_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { @@ -5137,10 +5138,6 @@ impl Editor { } } - fn end_transaction(&mut self, cx: &mut ViewContext) { - self.end_transaction_at(Instant::now(), cx); - } - fn end_transaction_at(&mut self, now: Instant, cx: &mut ViewContext) { if let Some(tx_id) = self .buffer @@ -5320,11 +5317,13 @@ impl Editor { } 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)); + self.transact(cx, |this, cx| { + this.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 { From a739c362d97380bcf5cebccbce6a187aafe58d95 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 08:55:46 +0100 Subject: [PATCH 102/139] Rename `editor::Event::Edited` to `editor::Event::BufferEdited` This is to distinguish it from a new event we're about to add which represent edits originating from that specific editor. --- crates/editor/src/editor.rs | 6 +++--- crates/editor/src/items.rs | 2 +- crates/file_finder/src/file_finder.rs | 2 +- crates/go_to_line/src/go_to_line.rs | 2 +- crates/language/src/buffer.rs | 19 +++++++++---------- crates/language/src/tests.rs | 12 ++---------- crates/outline/src/outline.rs | 2 +- crates/project/src/project.rs | 11 ++++------- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 4 ++-- crates/theme_selector/src/theme_selector.rs | 2 +- 11 files changed, 26 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 372d620ed7..19812e3083 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5526,10 +5526,10 @@ impl Editor { cx: &mut ViewContext, ) { match event { - language::Event::Edited { local } => { + language::Event::Edited => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); - cx.emit(Event::Edited { local: *local }); + cx.emit(Event::BufferEdited); } language::Event::Dirtied => cx.emit(Event::Dirtied), language::Event::Saved => cx.emit(Event::Saved), @@ -5656,7 +5656,7 @@ fn compute_scroll_position( #[derive(Copy, Clone)] pub enum Event { Activate, - Edited { local: bool }, + BufferEdited, Blurred, Dirtied, Saved, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0e77bfa3aa..9d3a1bd054 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -198,7 +198,7 @@ impl FollowableItem for Editor { fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { match event { - Event::Edited { local } => *local, + // Event::BufferEdited { local } => *local, Event::SelectionsChanged { local } => *local, Event::ScrollPositionChanged { local } => *local, _ => false, diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 4656daa4b3..9f0137ef62 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -291,7 +291,7 @@ impl FileFinder { cx: &mut ViewContext, ) { match event { - editor::Event::Edited { .. } => { + editor::Event::BufferEdited { .. } => { let query = self.query_editor.update(cx, |buffer, cx| buffer.text(cx)); if query.is_empty() { self.latest_search_id = post_inc(&mut self.search_count); diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index ce8ba787a8..109d33097d 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -102,7 +102,7 @@ impl GoToLine { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited { .. } => { + editor::Event::BufferEdited { .. } => { let line_editor = self.line_editor.read(cx).buffer().read(cx).read(cx).text(); let mut components = line_editor.trim().split(&[',', ':'][..]); let row = components.next().and_then(|row| row.parse::().ok()); diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 28f2c0510d..052ed711b3 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -142,7 +142,7 @@ pub enum Operation { #[derive(Clone, Debug, PartialEq, Eq)] pub enum Event { Operation(Operation), - Edited { local: bool }, + Edited, Dirtied, Saved, FileHandleChanged, @@ -967,7 +967,7 @@ impl Buffer { ) -> Option { if let Some((transaction_id, start_version)) = self.text.end_transaction_at(now) { let was_dirty = start_version != self.saved_version; - self.did_edit(&start_version, was_dirty, true, cx); + self.did_edit(&start_version, was_dirty, cx); Some(transaction_id) } else { None @@ -1160,7 +1160,6 @@ impl Buffer { &mut self, old_version: &clock::Global, was_dirty: bool, - local: bool, cx: &mut ModelContext, ) { if self.edits_since::(old_version).next().is_none() { @@ -1169,7 +1168,7 @@ impl Buffer { self.reparse(cx); - cx.emit(Event::Edited { local }); + cx.emit(Event::Edited); if !was_dirty { cx.emit(Event::Dirtied); } @@ -1206,7 +1205,7 @@ impl Buffer { self.text.apply_ops(buffer_ops)?; self.deferred_ops.insert(deferred_ops); self.flush_deferred_ops(cx); - self.did_edit(&old_version, was_dirty, false, cx); + self.did_edit(&old_version, was_dirty, cx); // Notify independently of whether the buffer was edited as the operations could include a // selection update. cx.notify(); @@ -1321,7 +1320,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.undo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, cx); Some(transaction_id) } else { None @@ -1342,7 +1341,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if undone { - self.did_edit(&old_version, was_dirty, true, cx) + self.did_edit(&old_version, was_dirty, cx) } undone } @@ -1353,7 +1352,7 @@ impl Buffer { if let Some((transaction_id, operation)) = self.text.redo() { self.send_operation(Operation::Buffer(operation), cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, cx); Some(transaction_id) } else { None @@ -1374,7 +1373,7 @@ impl Buffer { self.send_operation(Operation::Buffer(operation), cx); } if redone { - self.did_edit(&old_version, was_dirty, true, cx) + self.did_edit(&old_version, was_dirty, cx) } redone } @@ -1440,7 +1439,7 @@ impl Buffer { if !ops.is_empty() { for op in ops { self.send_operation(Operation::Buffer(op), cx); - self.did_edit(&old_version, was_dirty, true, cx); + self.did_edit(&old_version, was_dirty, cx); } } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 19b1a1cf73..7ce21955e7 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -122,19 +122,11 @@ fn test_edit_events(cx: &mut gpui::MutableAppContext) { let buffer_1_events = buffer_1_events.borrow(); assert_eq!( *buffer_1_events, - vec![ - Event::Edited { local: true }, - Event::Dirtied, - Event::Edited { local: true }, - Event::Edited { local: true } - ] + vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited] ); let buffer_2_events = buffer_2_events.borrow(); - assert_eq!( - *buffer_2_events, - vec![Event::Edited { local: false }, Event::Dirtied] - ); + assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]); } #[gpui::test] diff --git a/crates/outline/src/outline.rs b/crates/outline/src/outline.rs index 968fceb59c..a626ff89c8 100644 --- a/crates/outline/src/outline.rs +++ b/crates/outline/src/outline.rs @@ -224,7 +224,7 @@ impl OutlineView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited { .. } => self.update_matches(cx), + editor::Event::BufferEdited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fbd3a1f7a6..2529c976ac 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -6229,10 +6229,7 @@ mod tests { assert!(buffer.is_dirty()); assert_eq!( *events.borrow(), - &[ - language::Event::Edited { local: true }, - language::Event::Dirtied - ] + &[language::Event::Edited, language::Event::Dirtied] ); events.borrow_mut().clear(); buffer.did_save(buffer.version(), buffer.file().unwrap().mtime(), None, cx); @@ -6255,9 +6252,9 @@ mod tests { assert_eq!( *events.borrow(), &[ - language::Event::Edited { local: true }, + language::Event::Edited, language::Event::Dirtied, - language::Event::Edited { local: true }, + language::Event::Edited, ], ); events.borrow_mut().clear(); @@ -6269,7 +6266,7 @@ mod tests { assert!(buffer.is_dirty()); }); - assert_eq!(*events.borrow(), &[language::Event::Edited { local: true }]); + assert_eq!(*events.borrow(), &[language::Event::Edited]); // When a file is deleted, the buffer is considered dirty. let events = Rc::new(RefCell::new(Vec::new())); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 5eb04718d7..34d5306d99 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,7 +328,7 @@ impl ProjectSymbolsView { ) { match event { editor::Event::Blurred => cx.emit(Event::Dismissed), - editor::Event::Edited { .. } => self.update_matches(cx), + editor::Event::BufferEdited { .. } => self.update_matches(cx), _ => {} } } diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 13c73036f4..0b1ed2c35b 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -360,7 +360,7 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited { .. } => { + editor::Event::BufferEdited { .. } => { self.query_contains_error = false; self.clear_matches(cx); self.update_matches(true, cx); @@ -377,7 +377,7 @@ impl SearchBar { cx: &mut ViewContext, ) { match event { - editor::Event::Edited { .. } => self.update_matches(false, cx), + editor::Event::BufferEdited { .. } => self.update_matches(false, cx), editor::Event::SelectionsChanged { .. } => self.update_match_index(cx), _ => {} } diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index ebdcc492a9..725319be41 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -204,7 +204,7 @@ impl ThemeSelector { cx: &mut ViewContext, ) { match event { - editor::Event::Edited { .. } => { + editor::Event::BufferEdited { .. } => { self.update_matches(cx); self.select_if_matching(&cx.global::().theme.name); self.show_selected_theme(cx); From 87981bb2ab8429199a2d1ddcbcc3e2cdba0b5ed2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 08:56:37 +0100 Subject: [PATCH 103/139] Emit an `Edited` event when ending, undoing or redoing a transaction --- crates/editor/src/editor.rs | 5 +++++ crates/editor/src/items.rs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 19812e3083..c4808215d7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3364,6 +3364,7 @@ impl Editor { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); + cx.emit(Event::Edited); } } @@ -3373,6 +3374,7 @@ impl Editor { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); + cx.emit(Event::Edited); } } @@ -5148,6 +5150,8 @@ impl Editor { } else { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); } + + cx.emit(Event::Edited); } } @@ -5657,6 +5661,7 @@ fn compute_scroll_position( pub enum Event { Activate, BufferEdited, + Edited, Blurred, Dirtied, Saved, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 9d3a1bd054..f10956c125 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -198,7 +198,7 @@ impl FollowableItem for Editor { fn should_unfollow_on_event(event: &Self::Event, _: &AppContext) -> bool { match event { - // Event::BufferEdited { local } => *local, + Event::Edited => true, Event::SelectionsChanged { local } => *local, Event::ScrollPositionChanged { local } => *local, _ => false, From 9421ad04f70f3ea87d6384fa2494347ed80e2ee5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 09:17:56 +0100 Subject: [PATCH 104/139] Add a unit test for editor edit events --- crates/editor/src/editor.rs | 110 +++++++++++++++++++++++++++++++++++- 1 file changed, 109 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c4808215d7..309666d1d5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5657,7 +5657,7 @@ fn compute_scroll_position( scroll_position } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Event { Activate, BufferEdited, @@ -6140,6 +6140,114 @@ mod tests { use util::test::sample_text; use workspace::FollowableItem; + #[gpui::test] + fn test_edit_events(cx: &mut MutableAppContext) { + populate_settings(cx); + let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + + let events = Rc::new(RefCell::new(Vec::new())); + let (_, editor1) = cx.add_window(Default::default(), { + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited | Event::Dirtied) { + events.borrow_mut().push(("editor1", *event)); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + let (_, editor2) = cx.add_window(Default::default(), { + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::Edited | Event::BufferEdited | Event::Dirtied) { + events.borrow_mut().push(("editor2", *event)); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + + // Mutating editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.insert("X", cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ("editor1", Event::Dirtied), + ("editor2", Event::Dirtied) + ] + ); + + // Mutating editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.delete(&Delete, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Redoing on editor 1 will emit an `Edited` event only for that editor. + editor1.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor1", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Undoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.undo(&Undo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // Redoing on editor 2 will emit an `Edited` event only for that editor. + editor2.update(cx, |editor, cx| editor.redo(&Redo, cx)); + assert_eq!( + mem::take(&mut *events.borrow_mut()), + [ + ("editor2", Event::Edited), + ("editor1", Event::BufferEdited), + ("editor2", Event::BufferEdited), + ] + ); + + // No event is emitted when the mutation is a no-op. + editor2.update(cx, |editor, cx| { + editor.select_ranges([0..0], None, cx); + editor.backspace(&Backspace, cx); + }); + assert_eq!(mem::take(&mut *events.borrow_mut()), []); + } + #[gpui::test] fn test_undo_redo_with_selection_restoration(cx: &mut MutableAppContext) { populate_settings(cx); From 4f50da759a57c7da0d92f536911d296839c77975 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 17:14:41 +0100 Subject: [PATCH 105/139] Allow customizing whether `FoldMap::unfold` treats ranges as inclusive --- crates/editor/src/display_map.rs | 5 +++-- crates/editor/src/display_map/fold_map.rs | 5 +++-- crates/editor/src/editor.rs | 22 ++++++++++++++-------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 6f31541137..807049c75c 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -114,6 +114,7 @@ impl DisplayMap { pub fn unfold( &mut self, ranges: impl IntoIterator>, + inclusive: bool, cx: &mut ModelContext, ) { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -124,7 +125,7 @@ impl DisplayMap { .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); self.block_map.read(snapshot, edits); - let (snapshot, edits) = fold_map.unfold(ranges); + let (snapshot, edits) = fold_map.unfold(ranges, inclusive); let (snapshot, edits) = self.tab_map.sync(snapshot, edits); let (snapshot, edits) = self .wrap_map @@ -629,7 +630,7 @@ mod tests { if rng.gen() && fold_count > 0 { log::info!("unfolding ranges: {:?}", ranges); map.update(cx, |map, cx| { - map.unfold(ranges, cx); + map.unfold(ranges, true, cx); }); } else { log::info!("folding ranges: {:?}", ranges); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index ac4477263d..81da94aca7 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -140,13 +140,14 @@ impl<'a> FoldMapWriter<'a> { pub fn unfold( &mut self, ranges: impl IntoIterator>, + inclusive: bool, ) -> (FoldSnapshot, Vec) { let mut edits = Vec::new(); let mut fold_ixs_to_delete = Vec::new(); let buffer = self.0.buffer.lock().clone(); for range in ranges.into_iter() { // Remove intersecting folds and add their ranges to edits that are passed to sync. - let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, true); + let mut folds_cursor = intersecting_folds(&buffer, &self.0.folds, range, inclusive); while let Some(fold) = folds_cursor.item() { let offset_range = fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer); if offset_range.end > offset_range.start { @@ -1282,7 +1283,7 @@ mod tests { assert_eq!(snapshot4.text(), "123a…c123456eee"); let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); - writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5))); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), true); let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]); assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee"); } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6722d2b1fb..7cc203c184 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3123,7 +3123,7 @@ impl Editor { } self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); + self.unfold_ranges(unfold_ranges, true, cx); self.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([range], text, cx); @@ -3226,7 +3226,7 @@ impl Editor { } self.start_transaction(cx); - self.unfold_ranges(unfold_ranges, cx); + self.unfold_ranges(unfold_ranges, true, cx); self.buffer.update(cx, |buffer, cx| { for (range, text) in edits { buffer.edit([range], text, cx); @@ -3748,7 +3748,7 @@ impl Editor { to_unfold.push(selection.start..selection.end); } } - self.unfold_ranges(to_unfold, cx); + self.unfold_ranges(to_unfold, true, cx); self.update_selections(new_selections, Some(Autoscroll::Fit), cx); } @@ -5202,7 +5202,7 @@ impl Editor { start..end }) .collect::>(); - self.unfold_ranges(ranges, cx); + self.unfold_ranges(ranges, true, cx); } fn is_line_foldable(&self, display_map: &DisplaySnapshot, display_row: u32) -> bool { @@ -5253,7 +5253,7 @@ impl Editor { self.fold_ranges(ranges, cx); } - fn fold_ranges( + pub fn fold_ranges( &mut self, ranges: impl IntoIterator>, cx: &mut ViewContext, @@ -5266,10 +5266,16 @@ impl Editor { } } - fn unfold_ranges(&mut self, ranges: Vec>, cx: &mut ViewContext) { - if !ranges.is_empty() { + pub fn unfold_ranges( + &mut self, + ranges: impl IntoIterator>, + inclusive: bool, + cx: &mut ViewContext, + ) { + let mut ranges = ranges.into_iter().peekable(); + if ranges.peek().is_some() { self.display_map - .update(cx, |map, cx| map.unfold(ranges, cx)); + .update(cx, |map, cx| map.unfold(ranges, inclusive, cx)); self.request_autoscroll(Autoscroll::Fit, cx); cx.notify(); } From 78b52168fa234b30e5133ff0dd379ce057cfddc3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 17:15:40 +0100 Subject: [PATCH 106/139] Rename `Editor::unfold` to `Editor::unfold_lines` --- crates/editor/src/editor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7cc203c184..715f76a07f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -122,7 +122,7 @@ action!(ConfirmRename); action!(PageUp); action!(PageDown); action!(Fold); -action!(Unfold); +action!(UnfoldLines); action!(FoldSelectedRanges); action!(Scroll, Vector2F); action!(Select, SelectPhase); @@ -263,7 +263,7 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("pageup", PageUp, Some("Editor")), Binding::new("pagedown", PageDown, Some("Editor")), Binding::new("alt-cmd-[", Fold, Some("Editor")), - Binding::new("alt-cmd-]", Unfold, Some("Editor")), + Binding::new("alt-cmd-]", UnfoldLines, Some("Editor")), Binding::new("alt-cmd-f", FoldSelectedRanges, Some("Editor")), Binding::new("ctrl-space", ShowCompletions, Some("Editor")), Binding::new("cmd-.", ToggleCodeActions(false), Some("Editor")), @@ -329,7 +329,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::page_up); cx.add_action(Editor::page_down); cx.add_action(Editor::fold); - cx.add_action(Editor::unfold); + cx.add_action(Editor::unfold_lines); cx.add_action(Editor::fold_selected_ranges); cx.add_action(Editor::show_completions); cx.add_action(Editor::toggle_code_actions); @@ -5187,7 +5187,7 @@ impl Editor { self.fold_ranges(fold_ranges, cx); } - pub fn unfold(&mut self, _: &Unfold, cx: &mut ViewContext) { + pub fn unfold_lines(&mut self, _: &UnfoldLines, cx: &mut ViewContext) { let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -6467,7 +6467,7 @@ mod tests { .unindent(), ); - view.unfold(&Unfold, cx); + view.unfold_lines(&UnfoldLines, cx); assert_eq!( view.display_text(cx), " @@ -6488,7 +6488,7 @@ mod tests { .unindent(), ); - view.unfold(&Unfold, cx); + view.unfold_lines(&UnfoldLines, cx); assert_eq!(view.display_text(cx), buffer.read(cx).read(cx).text()); }); } From 7fa7b7e507f1c648ded3e4e1a0253f2750e6f74b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 17:16:21 +0100 Subject: [PATCH 107/139] Unfold range when selecting the next match Selected matches are unfolded when in project search, buffer search and when hitting `cmd-d` to select the next match. --- crates/editor/src/editor.rs | 2 ++ crates/search/src/buffer_search.rs | 8 +++----- crates/search/src/project_search.rs | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 715f76a07f..0aadc3fab9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3907,6 +3907,7 @@ impl Editor { reversed: false, goal: SelectionGoal::None, }); + self.unfold_ranges([next_selected_range], false, cx); self.update_selections(selections, Some(Autoscroll::Newest), cx); } else { select_next_state.done = true; @@ -3934,6 +3935,7 @@ impl Editor { wordwise: true, done: false, }; + self.unfold_ranges([selection.start..selection.end], false, cx); self.update_selections(selections, Some(Autoscroll::Newest), cx); self.select_next_state = Some(select_state); } else { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 13c73036f4..a1a40c9e39 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -336,11 +336,9 @@ impl SearchBar { direction, &editor.buffer().read(cx).read(cx), ); - editor.select_ranges( - [ranges[new_index].clone()], - Some(Autoscroll::Fit), - cx, - ); + let range_to_select = ranges[new_index].clone(); + editor.unfold_ranges([range_to_select.clone()], false, cx); + editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); } }); } diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 212e1c66a7..d78fcb12b7 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -489,6 +489,7 @@ impl ProjectSearchView { ); let range_to_select = model.match_ranges[new_index].clone(); self.results_editor.update(cx, |editor, cx| { + editor.unfold_ranges([range_to_select.clone()], false, cx); editor.select_ranges([range_to_select], Some(Autoscroll::Fit), cx); }); } From 1c25b3d150457ddc648cf601898dd857cfa06e7b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 24 Mar 2022 17:20:28 +0100 Subject: [PATCH 108/139] Test the new `inclusive` parameter when unfolding --- crates/editor/src/display_map/fold_map.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 81da94aca7..586f6e0b04 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1283,9 +1283,14 @@ mod tests { assert_eq!(snapshot4.text(), "123a…c123456eee"); let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); - writer.unfold(Some(Point::new(0, 4)..Point::new(0, 5)), true); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), false); let (snapshot5, _) = map.read(buffer_snapshot.clone(), vec![]); - assert_eq!(snapshot5.text(), "123aaaaa\nbbbbbb\nccc123456eee"); + assert_eq!(snapshot5.text(), "123a…c123456eee"); + + let (mut writer, _, _) = map.write(buffer_snapshot.clone(), vec![]); + writer.unfold(Some(Point::new(0, 4)..Point::new(0, 4)), true); + let (snapshot6, _) = map.read(buffer_snapshot.clone(), vec![]); + assert_eq!(snapshot6.text(), "123aaaaa\nbbbbbb\nccc123456eee"); } #[gpui::test] From f6805eb80255c9dbd7af057291ff923f32282e27 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 24 Mar 2022 10:22:47 -0700 Subject: [PATCH 109/139] Make rename highlights work across multibuffer excerpts Co-authored-by: Antonio Scandurra Co-authored-by: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 3 +- crates/editor/src/editor.rs | 47 ++++++++++++++++-------- crates/editor/src/multi_buffer.rs | 36 +++++++++++++++--- crates/editor/src/multi_buffer/anchor.rs | 4 +- crates/editor/src/repro.rs | 9 +++++ crates/language/src/diagnostic_set.rs | 8 ++-- crates/language/src/tests.rs | 2 +- crates/text/src/anchor.rs | 20 +++++++++- crates/text/src/text.rs | 26 ++++++------- 9 files changed, 110 insertions(+), 45 deletions(-) create mode 100644 crates/editor/src/repro.rs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index a0182902f1..69a4d1235f 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -278,7 +278,8 @@ impl ProjectDiagnosticsEditor { prev_excerpt_id = excerpt_id.clone(); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); group_state.excerpts.push(excerpt_id.clone()); - let header_position = (excerpt_id.clone(), language::Anchor::min()); + let header_position = + (excerpt_id.clone(), language::Anchor::build_min()); if is_first_excerpt_for_group { is_first_excerpt_for_group = false; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ba95d33d39..634851bd82 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2399,7 +2399,7 @@ impl Editor { ) -> Result<()> { let replica_id = this.read_with(&cx, |this, cx| this.replica_id(cx)); - // If the code action's edits are all contained within this editor, then + // If the project transaction's edits are all contained within this editor, then // avoid opening a new editor to display them. let mut entries = transaction.0.iter(); if let Some((buffer, transaction)) = entries.next() { @@ -2521,7 +2521,6 @@ impl Editor { } let buffer_id = cursor_position.buffer_id; - let excerpt_id = cursor_position.excerpt_id.clone(); let style = this.style(cx); let read_background = style.document_highlight_read_background; let write_background = style.document_highlight_write_background; @@ -2533,22 +2532,39 @@ impl Editor { return; } + let cursor_buffer_snapshot = cursor_buffer.read(cx); let mut write_ranges = Vec::new(); let mut read_ranges = Vec::new(); for highlight in highlights { - let range = Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.start, - }..Anchor { - buffer_id, - excerpt_id: excerpt_id.clone(), - text_anchor: highlight.range.end, - }; - if highlight.kind == lsp::DocumentHighlightKind::WRITE { - write_ranges.push(range); - } else { - read_ranges.push(range); + for (excerpt_id, excerpt_range) in + buffer.excerpts_for_buffer(&cursor_buffer, cx) + { + let start = highlight + .range + .start + .max(&excerpt_range.start, cursor_buffer_snapshot); + let end = highlight + .range + .end + .min(&excerpt_range.end, cursor_buffer_snapshot); + if start.cmp(&end, cursor_buffer_snapshot).unwrap().is_ge() { + continue; + } + + let range = Anchor { + buffer_id, + excerpt_id: excerpt_id.clone(), + text_anchor: start, + }..Anchor { + buffer_id, + excerpt_id, + text_anchor: end, + }; + if highlight.kind == lsp::DocumentHighlightKind::WRITE { + write_ranges.push(range); + } else { + read_ranges.push(range); + } } } @@ -4413,7 +4429,6 @@ impl Editor { .iter() .map(|range| range.to_point(&snapshot)) .collect::>(); - dbg!(point_ranges); this.highlight_text::( ranges, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 594e3fafb7..398f7da99e 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -211,7 +211,11 @@ impl MultiBuffer { pub fn singleton(buffer: ModelHandle, cx: &mut ModelContext) -> Self { let mut this = Self::new(buffer.read(cx).replica_id()); this.singleton = true; - this.push_excerpts(buffer, [text::Anchor::min()..text::Anchor::max()], cx); + this.push_excerpts( + buffer, + [text::Anchor::build_min()..text::Anchor::build_max()], + cx, + ); this.snapshot.borrow_mut().singleton = true; this } @@ -814,11 +818,30 @@ impl MultiBuffer { cx.notify(); } - pub fn excerpt_ids_for_buffer(&self, buffer: &ModelHandle) -> Vec { - self.buffers - .borrow() + pub fn excerpts_for_buffer( + &self, + buffer: &ModelHandle, + cx: &AppContext, + ) -> Vec<(ExcerptId, Range)> { + let mut excerpts = Vec::new(); + let snapshot = self.read(cx); + let buffers = self.buffers.borrow(); + let mut cursor = snapshot.excerpts.cursor::>(); + for excerpt_id in buffers .get(&buffer.id()) - .map_or(Vec::new(), |state| state.excerpts.clone()) + .map(|state| &state.excerpts) + .into_iter() + .flatten() + { + cursor.seek_forward(&Some(excerpt_id), Bias::Left, &()); + if let Some(excerpt) = cursor.item() { + if excerpt.id == *excerpt_id { + excerpts.push((excerpt.id.clone(), excerpt.range.clone())); + } + } + } + + excerpts } pub fn excerpt_ids(&self) -> Vec { @@ -3070,7 +3093,8 @@ mod tests { ); let snapshot = multibuffer.update(cx, |multibuffer, cx| { - let buffer_2_excerpt_id = multibuffer.excerpt_ids_for_buffer(&buffer_2)[0].clone(); + let (buffer_2_excerpt_id, _) = + multibuffer.excerpts_for_buffer(&buffer_2, cx)[0].clone(); multibuffer.remove_excerpts(&[buffer_2_excerpt_id], cx); multibuffer.snapshot(cx) }); diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 33147ce285..847a38f500 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -19,7 +19,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::min(), + text_anchor: text::Anchor::build_min(), } } @@ -27,7 +27,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::max(), + text_anchor: text::Anchor::build_max(), } } diff --git a/crates/editor/src/repro.rs b/crates/editor/src/repro.rs new file mode 100644 index 0000000000..0dfa9ecbfc --- /dev/null +++ b/crates/editor/src/repro.rs @@ -0,0 +1,9 @@ +fn test(complicated: i32, test: i32) { + complicated; + test; + // 1 + // 2 + // 3 + complicated; + test; +} diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index e25551ee3a..017f117bbe 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -187,10 +187,10 @@ impl DiagnosticEntry { impl Default for Summary { fn default() -> Self { Self { - start: Anchor::min(), - end: Anchor::max(), - min_start: Anchor::max(), - max_end: Anchor::min(), + start: Anchor::build_min(), + end: Anchor::build_max(), + min_start: Anchor::build_max(), + max_end: Anchor::build_min(), count: 0, } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index d36771c44f..3dbe8508a5 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -789,7 +789,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { for buffer in &buffers { let buffer = buffer.read(cx).snapshot(); let actual_remote_selections = buffer - .remote_selections_in_range(Anchor::min()..Anchor::max()) + .remote_selections_in_range(Anchor::build_min()..Anchor::build_max()) .map(|(replica_id, selections)| (replica_id, selections.collect::>())) .collect::>(); let expected_remote_selections = active_selections diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 28da998d67..c7de6bc882 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -12,7 +12,7 @@ pub struct Anchor { } impl Anchor { - pub fn min() -> Self { + pub fn build_min() -> Self { Self { timestamp: clock::Local::MIN, offset: usize::MIN, @@ -20,7 +20,7 @@ impl Anchor { } } - pub fn max() -> Self { + pub fn build_max() -> Self { Self { timestamp: clock::Local::MAX, offset: usize::MAX, @@ -42,6 +42,22 @@ impl Anchor { .then_with(|| self.bias.cmp(&other.bias))) } + pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).unwrap().is_le() { + self.clone() + } else { + other.clone() + } + } + + pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self { + if self.cmp(other, buffer).unwrap().is_ge() { + self.clone() + } else { + other.clone() + } + } + pub fn bias(&self, bias: Bias, buffer: &BufferSnapshot) -> Anchor { if bias == Bias::Left { self.bias_left(buffer) diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 338a5c0ad1..8cb9629d07 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1318,8 +1318,8 @@ impl Buffer { let mut futures = Vec::new(); for anchor in anchors { if !self.version.observed(anchor.timestamp) - && *anchor != Anchor::max() - && *anchor != Anchor::min() + && *anchor != Anchor::build_max() + && *anchor != Anchor::build_min() { let (tx, rx) = oneshot::channel(); self.edit_id_resolvers @@ -1638,9 +1638,9 @@ impl BufferSnapshot { let mut position = D::default(); anchors.map(move |anchor| { - if *anchor == Anchor::min() { + if *anchor == Anchor::build_min() { return D::default(); - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::build_max() { return D::from_text_summary(&self.visible_text.summary()); } @@ -1680,9 +1680,9 @@ impl BufferSnapshot { where D: TextDimension, { - if *anchor == Anchor::min() { + if *anchor == Anchor::build_min() { D::default() - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::build_max() { D::from_text_summary(&self.visible_text.summary()) } else { let anchor_key = InsertionFragmentKey { @@ -1718,9 +1718,9 @@ impl BufferSnapshot { } fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { - if *anchor == Anchor::min() { + if *anchor == Anchor::build_min() { &locator::MIN - } else if *anchor == Anchor::max() { + } else if *anchor == Anchor::build_max() { &locator::MAX } else { let anchor_key = InsertionFragmentKey { @@ -1758,9 +1758,9 @@ impl BufferSnapshot { pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { let offset = position.to_offset(self); if bias == Bias::Left && offset == 0 { - Anchor::min() + Anchor::build_min() } else if bias == Bias::Right && offset == self.len() { - Anchor::max() + Anchor::build_max() } else { let mut fragment_cursor = self.fragments.cursor::(); fragment_cursor.seek(&offset, bias, &None); @@ -1775,8 +1775,8 @@ impl BufferSnapshot { } pub fn can_resolve(&self, anchor: &Anchor) -> bool { - *anchor == Anchor::min() - || *anchor == Anchor::max() + *anchor == Anchor::build_min() + || *anchor == Anchor::build_max() || self.version.observed(anchor.timestamp) } @@ -1799,7 +1799,7 @@ impl BufferSnapshot { where D: TextDimension + Ord, { - self.edits_since_in_range(since, Anchor::min()..Anchor::max()) + self.edits_since_in_range(since, Anchor::build_min()..Anchor::build_max()) } pub fn edited_ranges_for_transaction<'a, D>( From ab631cf6c3fd5a4cfb0de996072eaa299c44b0e3 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 24 Mar 2022 10:35:30 -0700 Subject: [PATCH 110/139] Change language::anchor::min() to a constant Co-authored-by: Antonio Scandurra Co-authored-by: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 3 +-- crates/editor/src/display_map.rs | 3 +-- crates/editor/src/editor.rs | 10 +-------- crates/editor/src/multi_buffer.rs | 6 +----- crates/editor/src/multi_buffer/anchor.rs | 4 ++-- crates/editor/src/repro.rs | 9 -------- crates/language/src/diagnostic_set.rs | 8 ++++---- crates/language/src/tests.rs | 2 +- crates/text/src/anchor.rs | 24 +++++++++------------- crates/text/src/text.rs | 26 +++++++++++------------- 10 files changed, 33 insertions(+), 62 deletions(-) delete mode 100644 crates/editor/src/repro.rs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 69a4d1235f..61a2e6c9c1 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -278,8 +278,7 @@ impl ProjectDiagnosticsEditor { prev_excerpt_id = excerpt_id.clone(); first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); group_state.excerpts.push(excerpt_id.clone()); - let header_position = - (excerpt_id.clone(), language::Anchor::build_min()); + let header_position = (excerpt_id.clone(), language::Anchor::MIN); if is_first_excerpt_for_group { is_first_excerpt_for_group = false; diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 1f825e175c..aee964ec56 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -491,7 +491,7 @@ mod tests { use super::*; use crate::{ movement, - test::{marked_text, marked_text_ranges}, + test::{marked_text_ranges}, }; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; @@ -499,7 +499,6 @@ mod tests { use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; - use unindent::Unindent; use util::test::sample_text; use Bias::*; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 634851bd82..3fbdf7def1 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4,8 +4,6 @@ pub mod items; pub mod movement; mod multi_buffer; -pub mod repro; - #[cfg(test)] mod test; @@ -4422,13 +4420,7 @@ impl Editor { .into_iter() .flat_map(|(_, ranges)| ranges), ) - .collect::>(); - - let snapshot = this.buffer.read(cx).snapshot(cx); - let point_ranges = ranges - .iter() - .map(|range| range.to_point(&snapshot)) - .collect::>(); + .collect(); this.highlight_text::( ranges, diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 398f7da99e..de735dafa7 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -211,11 +211,7 @@ impl MultiBuffer { pub fn singleton(buffer: ModelHandle, cx: &mut ModelContext) -> Self { let mut this = Self::new(buffer.read(cx).replica_id()); this.singleton = true; - this.push_excerpts( - buffer, - [text::Anchor::build_min()..text::Anchor::build_max()], - cx, - ); + this.push_excerpts(buffer, [text::Anchor::MIN..text::Anchor::MAX], cx); this.snapshot.borrow_mut().singleton = true; this } diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index 847a38f500..aeaf184c60 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -19,7 +19,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::min(), - text_anchor: text::Anchor::build_min(), + text_anchor: text::Anchor::MIN, } } @@ -27,7 +27,7 @@ impl Anchor { Self { buffer_id: None, excerpt_id: ExcerptId::max(), - text_anchor: text::Anchor::build_max(), + text_anchor: text::Anchor::MAX, } } diff --git a/crates/editor/src/repro.rs b/crates/editor/src/repro.rs deleted file mode 100644 index 0dfa9ecbfc..0000000000 --- a/crates/editor/src/repro.rs +++ /dev/null @@ -1,9 +0,0 @@ -fn test(complicated: i32, test: i32) { - complicated; - test; - // 1 - // 2 - // 3 - complicated; - test; -} diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 017f117bbe..28b03b5aa0 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -187,10 +187,10 @@ impl DiagnosticEntry { impl Default for Summary { fn default() -> Self { Self { - start: Anchor::build_min(), - end: Anchor::build_max(), - min_start: Anchor::build_max(), - max_end: Anchor::build_min(), + start: Anchor::MIN, + end: Anchor::MAX, + min_start: Anchor::MAX, + max_end: Anchor::MIN, count: 0, } } diff --git a/crates/language/src/tests.rs b/crates/language/src/tests.rs index 3dbe8508a5..7eff3b0d97 100644 --- a/crates/language/src/tests.rs +++ b/crates/language/src/tests.rs @@ -789,7 +789,7 @@ fn test_random_collaboration(cx: &mut MutableAppContext, mut rng: StdRng) { for buffer in &buffers { let buffer = buffer.read(cx).snapshot(); let actual_remote_selections = buffer - .remote_selections_in_range(Anchor::build_min()..Anchor::build_max()) + .remote_selections_in_range(Anchor::MIN..Anchor::MAX) .map(|(replica_id, selections)| (replica_id, selections.collect::>())) .collect::>(); let expected_remote_selections = active_selections diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index c7de6bc882..5438ecd51f 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -12,21 +12,17 @@ pub struct Anchor { } impl Anchor { - pub fn build_min() -> Self { - Self { - timestamp: clock::Local::MIN, - offset: usize::MIN, - bias: Bias::Left, - } - } + pub const MIN: Self = Self { + timestamp: clock::Local::MIN, + offset: usize::MIN, + bias: Bias::Left, + }; - pub fn build_max() -> Self { - Self { - timestamp: clock::Local::MAX, - offset: usize::MAX, - bias: Bias::Right, - } - } + pub const MAX: Self = Self { + timestamp: clock::Local::MAX, + offset: usize::MAX, + bias: Bias::Right, + }; pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result { let fragment_id_comparison = if self.timestamp == other.timestamp { diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 8cb9629d07..b811d08c04 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -1318,8 +1318,8 @@ impl Buffer { let mut futures = Vec::new(); for anchor in anchors { if !self.version.observed(anchor.timestamp) - && *anchor != Anchor::build_max() - && *anchor != Anchor::build_min() + && *anchor != Anchor::MAX + && *anchor != Anchor::MIN { let (tx, rx) = oneshot::channel(); self.edit_id_resolvers @@ -1638,9 +1638,9 @@ impl BufferSnapshot { let mut position = D::default(); anchors.map(move |anchor| { - if *anchor == Anchor::build_min() { + if *anchor == Anchor::MIN { return D::default(); - } else if *anchor == Anchor::build_max() { + } else if *anchor == Anchor::MAX { return D::from_text_summary(&self.visible_text.summary()); } @@ -1680,9 +1680,9 @@ impl BufferSnapshot { where D: TextDimension, { - if *anchor == Anchor::build_min() { + if *anchor == Anchor::MIN { D::default() - } else if *anchor == Anchor::build_max() { + } else if *anchor == Anchor::MAX { D::from_text_summary(&self.visible_text.summary()) } else { let anchor_key = InsertionFragmentKey { @@ -1718,9 +1718,9 @@ impl BufferSnapshot { } fn fragment_id_for_anchor(&self, anchor: &Anchor) -> &Locator { - if *anchor == Anchor::build_min() { + if *anchor == Anchor::MIN { &locator::MIN - } else if *anchor == Anchor::build_max() { + } else if *anchor == Anchor::MAX { &locator::MAX } else { let anchor_key = InsertionFragmentKey { @@ -1758,9 +1758,9 @@ impl BufferSnapshot { pub fn anchor_at(&self, position: T, bias: Bias) -> Anchor { let offset = position.to_offset(self); if bias == Bias::Left && offset == 0 { - Anchor::build_min() + Anchor::MIN } else if bias == Bias::Right && offset == self.len() { - Anchor::build_max() + Anchor::MAX } else { let mut fragment_cursor = self.fragments.cursor::(); fragment_cursor.seek(&offset, bias, &None); @@ -1775,9 +1775,7 @@ impl BufferSnapshot { } pub fn can_resolve(&self, anchor: &Anchor) -> bool { - *anchor == Anchor::build_min() - || *anchor == Anchor::build_max() - || self.version.observed(anchor.timestamp) + *anchor == Anchor::MIN || *anchor == Anchor::MAX || self.version.observed(anchor.timestamp) } pub fn clip_offset(&self, offset: usize, bias: Bias) -> usize { @@ -1799,7 +1797,7 @@ impl BufferSnapshot { where D: TextDimension + Ord, { - self.edits_since_in_range(since, Anchor::build_min()..Anchor::build_max()) + self.edits_since_in_range(since, Anchor::MIN..Anchor::MAX) } pub fn edited_ranges_for_transaction<'a, D>( From 92c7b5d6ef576151afc3964c8cde872daceed8d9 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 24 Mar 2022 11:48:31 -0700 Subject: [PATCH 111/139] Remove result from anchor cmp functions Co-authored-by: Nathan Sobo --- crates/diagnostics/src/diagnostics.rs | 3 +- crates/editor/src/display_map/block_map.rs | 2 +- crates/editor/src/display_map/fold_map.rs | 29 ++++++-------- crates/editor/src/editor.rs | 25 ++++-------- crates/editor/src/multi_buffer.rs | 45 +++++----------------- crates/editor/src/multi_buffer/anchor.rs | 19 +++++---- crates/language/src/buffer.rs | 12 +----- crates/language/src/diagnostic_set.rs | 16 +++----- crates/search/src/search.rs | 8 ++-- crates/text/src/anchor.rs | 14 +++---- crates/text/src/tests.rs | 36 +++++------------ 11 files changed, 68 insertions(+), 141 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 61a2e6c9c1..56de434cf4 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -367,8 +367,7 @@ impl ProjectDiagnosticsEditor { range_a .start .cmp(&range_b.start, &snapshot) - .unwrap() - .then_with(|| range_a.end.cmp(&range_b.end, &snapshot).unwrap()) + .then_with(|| range_a.end.cmp(&range_b.end, &snapshot)) }); if path_state.diagnostic_groups.is_empty() { diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 489381daef..9d1b7a7588 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -499,7 +499,7 @@ impl<'a> BlockMapWriter<'a> { let block_ix = match self .0 .blocks - .binary_search_by(|probe| probe.position.cmp(&position, &buffer).unwrap()) + .binary_search_by(|probe| probe.position.cmp(&position, &buffer)) { Ok(ix) | Err(ix) => ix, }; diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 7f744afa40..4b07854c3e 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -256,7 +256,7 @@ impl FoldMap { let mut folds = self.folds.iter().peekable(); while let Some(fold) = folds.next() { if let Some(next_fold) = folds.peek() { - let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()).unwrap(); + let comparison = fold.0.cmp(&next_fold.0, &self.buffer.lock()); assert!(comparison.is_le()); } } @@ -699,10 +699,7 @@ impl FoldSnapshot { let ranges = &highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe - .end - .cmp(&transform_start, &self.buffer_snapshot()) - .unwrap(); + let cmp = probe.end.cmp(&transform_start, &self.buffer_snapshot()); if cmp.is_gt() { Ordering::Greater } else { @@ -715,7 +712,6 @@ impl FoldSnapshot { if range .start .cmp(&transform_end, &self.buffer_snapshot) - .unwrap() .is_ge() { break; @@ -820,8 +816,8 @@ where let start = buffer.anchor_before(range.start.to_offset(buffer)); let end = buffer.anchor_after(range.end.to_offset(buffer)); let mut cursor = folds.filter::<_, usize>(move |summary| { - let start_cmp = start.cmp(&summary.max_end, buffer).unwrap(); - let end_cmp = end.cmp(&summary.min_start, buffer).unwrap(); + let start_cmp = start.cmp(&summary.max_end, buffer); + let end_cmp = end.cmp(&summary.min_start, buffer); if inclusive { start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal @@ -962,19 +958,19 @@ impl sum_tree::Summary for FoldSummary { type Context = MultiBufferSnapshot; fn add_summary(&mut self, other: &Self, buffer: &MultiBufferSnapshot) { - if other.min_start.cmp(&self.min_start, buffer).unwrap() == Ordering::Less { + if other.min_start.cmp(&self.min_start, buffer) == Ordering::Less { self.min_start = other.min_start.clone(); } - if other.max_end.cmp(&self.max_end, buffer).unwrap() == Ordering::Greater { + if other.max_end.cmp(&self.max_end, buffer) == Ordering::Greater { self.max_end = other.max_end.clone(); } #[cfg(debug_assertions)] { - let start_comparison = self.start.cmp(&other.start, buffer).unwrap(); + let start_comparison = self.start.cmp(&other.start, buffer); assert!(start_comparison <= Ordering::Equal); if start_comparison == Ordering::Equal { - assert!(self.end.cmp(&other.end, buffer).unwrap() >= Ordering::Equal); + assert!(self.end.cmp(&other.end, buffer) >= Ordering::Equal); } } @@ -993,7 +989,7 @@ impl<'a> sum_tree::Dimension<'a, FoldSummary> for Fold { impl<'a> sum_tree::SeekTarget<'a, FoldSummary, Fold> for Fold { fn cmp(&self, other: &Self, buffer: &MultiBufferSnapshot) -> Ordering { - self.0.cmp(&other.0, buffer).unwrap() + self.0.cmp(&other.0, buffer) } } @@ -1600,9 +1596,8 @@ mod tests { .filter(|fold| { let start = buffer_snapshot.anchor_before(start); let end = buffer_snapshot.anchor_after(end); - start.cmp(&fold.0.end, &buffer_snapshot).unwrap() == Ordering::Less - && end.cmp(&fold.0.start, &buffer_snapshot).unwrap() - == Ordering::Greater + start.cmp(&fold.0.end, &buffer_snapshot) == Ordering::Less + && end.cmp(&fold.0.start, &buffer_snapshot) == Ordering::Greater }) .map(|fold| fold.0) .collect::>(); @@ -1680,7 +1675,7 @@ mod tests { let buffer = self.buffer.lock().clone(); let mut folds = self.folds.items(&buffer); // Ensure sorting doesn't change how folds get merged and displayed. - folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer).unwrap()); + folds.sort_by(|a, b| a.0.cmp(&b.0, &buffer)); let mut fold_ranges = folds .iter() .map(|fold| fold.0.start.to_offset(&buffer)..fold.0.end.to_offset(&buffer)) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 3fbdf7def1..78b7f9a9f9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2545,7 +2545,7 @@ impl Editor { .range .end .min(&excerpt_range.end, cursor_buffer_snapshot); - if start.cmp(&end, cursor_buffer_snapshot).unwrap().is_ge() { + if start.cmp(&end, cursor_buffer_snapshot).is_ge() { continue; } @@ -2672,8 +2672,7 @@ impl Editor { }) }) .collect::>(); - tabstop_ranges - .sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot).unwrap()); + tabstop_ranges.sort_unstable_by(|a, b| a.start.cmp(&b.start, snapshot)); tabstop_ranges }) .collect::>() @@ -4700,13 +4699,13 @@ impl Editor { let start_ix = match self .selections - .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer).unwrap()) + .binary_search_by(|probe| probe.end.cmp(&range.start, &buffer)) { Ok(ix) | Err(ix) => ix, }; let end_ix = match self .selections - .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer).unwrap()) + .binary_search_by(|probe| probe.start.cmp(&range.end, &buffer)) { Ok(ix) => ix + 1, Err(ix) => ix, @@ -4928,8 +4927,7 @@ impl Editor { selections.sort_by(|a, b| { a.start .cmp(&b.start, &*buffer) - .unwrap() - .then_with(|| b.end.cmp(&a.end, &*buffer).unwrap()) + .then_with(|| b.end.cmp(&a.end, &*buffer)) }); // Merge overlapping selections @@ -4938,24 +4936,17 @@ impl Editor { if selections[i - 1] .end .cmp(&selections[i].start, &*buffer) - .unwrap() .is_ge() { let removed = selections.remove(i); if removed .start .cmp(&selections[i - 1].start, &*buffer) - .unwrap() .is_lt() { selections[i - 1].start = removed.start; } - if removed - .end - .cmp(&selections[i - 1].end, &*buffer) - .unwrap() - .is_gt() - { + if removed.end.cmp(&selections[i - 1].end, &*buffer).is_gt() { selections[i - 1].end = removed.end; } } else { @@ -5429,7 +5420,7 @@ impl Editor { let buffer = &display_snapshot.buffer_snapshot; for (color, ranges) in self.background_highlights.values() { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, &buffer).unwrap(); + let cmp = probe.end.cmp(&search_range.start, &buffer); if cmp.is_gt() { Ordering::Greater } else { @@ -5439,7 +5430,7 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, &buffer).unwrap().is_ge() { + if range.start.cmp(&search_range.end, &buffer).is_ge() { break; } let start = range diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index de735dafa7..af98f3d589 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -522,24 +522,14 @@ impl MultiBuffer { self.buffers.borrow()[&buffer_id] .buffer .update(cx, |buffer, cx| { - selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer).unwrap()); + selections.sort_unstable_by(|a, b| a.start.cmp(&b.start, buffer)); let mut selections = selections.into_iter().peekable(); let merged_selections = Arc::from_iter(iter::from_fn(|| { let mut selection = selections.next()?; while let Some(next_selection) = selections.peek() { - if selection - .end - .cmp(&next_selection.start, buffer) - .unwrap() - .is_ge() - { + if selection.end.cmp(&next_selection.start, buffer).is_ge() { let next_selection = selections.next().unwrap(); - if next_selection - .end - .cmp(&selection.end, buffer) - .unwrap() - .is_ge() - { + if next_selection.end.cmp(&selection.end, buffer).is_ge() { selection.end = next_selection.end; } } else { @@ -1936,11 +1926,7 @@ impl MultiBufferSnapshot { .range .start .bias(anchor.text_anchor.bias, &excerpt.buffer); - if text_anchor - .cmp(&excerpt.range.end, &excerpt.buffer) - .unwrap() - .is_gt() - { + if text_anchor.cmp(&excerpt.range.end, &excerpt.buffer).is_gt() { text_anchor = excerpt.range.end.clone(); } Anchor { @@ -1955,7 +1941,6 @@ impl MultiBufferSnapshot { .bias(anchor.text_anchor.bias, &excerpt.buffer); if text_anchor .cmp(&excerpt.range.start, &excerpt.buffer) - .unwrap() .is_lt() { text_anchor = excerpt.range.start.clone(); @@ -1975,7 +1960,7 @@ impl MultiBufferSnapshot { result.push((anchor_ix, anchor, kept_position)); } } - result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self).unwrap()); + result.sort_unstable_by(|a, b| a.1.cmp(&b.1, self)); result } @@ -2322,10 +2307,10 @@ impl MultiBufferSnapshot { excerpt_id: excerpt.id.clone(), text_anchor: selection.end.clone(), }; - if range.start.cmp(&start, self).unwrap().is_gt() { + if range.start.cmp(&start, self).is_gt() { start = range.start.clone(); } - if range.end.cmp(&end, self).unwrap().is_lt() { + if range.end.cmp(&end, self).is_lt() { end = range.end.clone(); } @@ -2549,17 +2534,9 @@ impl Excerpt { } fn clip_anchor(&self, text_anchor: text::Anchor) -> text::Anchor { - if text_anchor - .cmp(&self.range.start, &self.buffer) - .unwrap() - .is_lt() - { + if text_anchor.cmp(&self.range.start, &self.buffer).is_lt() { self.range.start.clone() - } else if text_anchor - .cmp(&self.range.end, &self.buffer) - .unwrap() - .is_gt() - { + } else if text_anchor.cmp(&self.range.end, &self.buffer).is_gt() { self.range.end.clone() } else { text_anchor @@ -2572,13 +2549,11 @@ impl Excerpt { .range .start .cmp(&anchor.text_anchor, &self.buffer) - .unwrap() .is_le() && self .range .end .cmp(&anchor.text_anchor, &self.buffer) - .unwrap() .is_ge() } } @@ -3385,7 +3360,7 @@ mod tests { let bias = if rng.gen() { Bias::Left } else { Bias::Right }; log::info!("Creating anchor at {} with bias {:?}", offset, bias); anchors.push(multibuffer.anchor_at(offset, bias)); - anchors.sort_by(|a, b| a.cmp(&b, &multibuffer).unwrap()); + anchors.sort_by(|a, b| a.cmp(&b, &multibuffer)); } 40..=44 if !anchors.is_empty() => { let multibuffer = multibuffer.read(cx).read(cx); diff --git a/crates/editor/src/multi_buffer/anchor.rs b/crates/editor/src/multi_buffer/anchor.rs index aeaf184c60..df080f074c 100644 --- a/crates/editor/src/multi_buffer/anchor.rs +++ b/crates/editor/src/multi_buffer/anchor.rs @@ -1,5 +1,4 @@ use super::{ExcerptId, MultiBufferSnapshot, ToOffset, ToPoint}; -use anyhow::Result; use std::{ cmp::Ordering, ops::{Range, Sub}, @@ -35,18 +34,18 @@ impl Anchor { &self.excerpt_id } - pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Result { + pub fn cmp<'a>(&self, other: &Anchor, snapshot: &MultiBufferSnapshot) -> Ordering { let excerpt_id_cmp = self.excerpt_id.cmp(&other.excerpt_id); if excerpt_id_cmp.is_eq() { if self.excerpt_id == ExcerptId::min() || self.excerpt_id == ExcerptId::max() { - Ok(Ordering::Equal) + Ordering::Equal } else if let Some(excerpt) = snapshot.excerpt(&self.excerpt_id) { self.text_anchor.cmp(&other.text_anchor, &excerpt.buffer) } else { - Ok(Ordering::Equal) + Ordering::Equal } } else { - Ok(excerpt_id_cmp) + excerpt_id_cmp } } @@ -97,17 +96,17 @@ impl ToPoint for Anchor { } pub trait AnchorRangeExt { - fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Result; + fn cmp(&self, b: &Range, buffer: &MultiBufferSnapshot) -> Ordering; fn to_offset(&self, content: &MultiBufferSnapshot) -> Range; fn to_point(&self, content: &MultiBufferSnapshot) -> Range; } impl AnchorRangeExt for Range { - fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Result { - Ok(match self.start.cmp(&other.start, buffer)? { - Ordering::Equal => other.end.cmp(&self.end, buffer)?, + fn cmp(&self, other: &Range, buffer: &MultiBufferSnapshot) -> Ordering { + match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), ord @ _ => ord, - }) + } } fn to_offset(&self, content: &MultiBufferSnapshot) -> Range { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index f32028002a..4c8341cf76 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -1813,20 +1813,12 @@ impl BufferSnapshot { }) .map(move |(replica_id, set)| { let start_ix = match set.selections.binary_search_by(|probe| { - probe - .end - .cmp(&range.start, self) - .unwrap() - .then(Ordering::Greater) + probe.end.cmp(&range.start, self).then(Ordering::Greater) }) { Ok(ix) | Err(ix) => ix, }; let end_ix = match set.selections.binary_search_by(|probe| { - probe - .start - .cmp(&range.end, self) - .unwrap() - .then(Ordering::Less) + probe.start.cmp(&range.end, self).then(Ordering::Less) }) { Ok(ix) | Err(ix) => ix, }; diff --git a/crates/language/src/diagnostic_set.rs b/crates/language/src/diagnostic_set.rs index 28b03b5aa0..490789a8c8 100644 --- a/crates/language/src/diagnostic_set.rs +++ b/crates/language/src/diagnostic_set.rs @@ -81,8 +81,8 @@ impl DiagnosticSet { let range = buffer.anchor_before(range.start)..buffer.anchor_at(range.end, end_bias); let mut cursor = self.diagnostics.filter::<_, ()>({ move |summary: &Summary| { - let start_cmp = range.start.cmp(&summary.max_end, buffer).unwrap(); - let end_cmp = range.end.cmp(&summary.min_start, buffer).unwrap(); + let start_cmp = range.start.cmp(&summary.max_end, buffer); + let end_cmp = range.end.cmp(&summary.min_start, buffer); if inclusive { start_cmp <= Ordering::Equal && end_cmp >= Ordering::Equal } else { @@ -123,7 +123,7 @@ impl DiagnosticSet { let start_ix = output.len(); output.extend(groups.into_values().filter_map(|mut entries| { - entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer).unwrap()); + entries.sort_unstable_by(|a, b| a.range.start.cmp(&b.range.start, buffer)); entries .iter() .position(|entry| entry.diagnostic.is_primary) @@ -137,7 +137,6 @@ impl DiagnosticSet { .range .start .cmp(&b.entries[b.primary_ix].range.start, buffer) - .unwrap() }); } @@ -200,15 +199,10 @@ impl sum_tree::Summary for Summary { type Context = text::BufferSnapshot; fn add_summary(&mut self, other: &Self, buffer: &Self::Context) { - if other - .min_start - .cmp(&self.min_start, buffer) - .unwrap() - .is_lt() - { + if other.min_start.cmp(&self.min_start, buffer).is_lt() { self.min_start = other.min_start.clone(); } - if other.max_end.cmp(&self.max_end, buffer).unwrap().is_gt() { + if other.max_end.cmp(&self.max_end, buffer).is_gt() { self.max_end = other.max_end.clone(); } self.start = other.start.clone(); diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index a1e335a035..9fb4cda8e9 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -39,9 +39,9 @@ pub(crate) fn active_match_index( None } else { match ranges.binary_search_by(|probe| { - if probe.end.cmp(&cursor, &*buffer).unwrap().is_lt() { + if probe.end.cmp(&cursor, &*buffer).is_lt() { Ordering::Less - } else if probe.start.cmp(&cursor, &*buffer).unwrap().is_gt() { + } else if probe.start.cmp(&cursor, &*buffer).is_gt() { Ordering::Greater } else { Ordering::Equal @@ -59,7 +59,7 @@ pub(crate) fn match_index_for_direction( direction: Direction, buffer: &MultiBufferSnapshot, ) -> usize { - if ranges[index].start.cmp(&cursor, &buffer).unwrap().is_gt() { + if ranges[index].start.cmp(&cursor, &buffer).is_gt() { if direction == Direction::Prev { if index == 0 { index = ranges.len() - 1; @@ -67,7 +67,7 @@ pub(crate) fn match_index_for_direction( index -= 1; } } - } else if ranges[index].end.cmp(&cursor, &buffer).unwrap().is_lt() { + } else if ranges[index].end.cmp(&cursor, &buffer).is_lt() { if direction == Direction::Next { index = 0; } diff --git a/crates/text/src/anchor.rs b/crates/text/src/anchor.rs index 5438ecd51f..e642aa45d3 100644 --- a/crates/text/src/anchor.rs +++ b/crates/text/src/anchor.rs @@ -24,7 +24,7 @@ impl Anchor { bias: Bias::Right, }; - pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Result { + pub fn cmp(&self, other: &Anchor, buffer: &BufferSnapshot) -> Ordering { let fragment_id_comparison = if self.timestamp == other.timestamp { Ordering::Equal } else { @@ -33,13 +33,13 @@ impl Anchor { .cmp(&buffer.fragment_id_for_anchor(other)) }; - Ok(fragment_id_comparison + fragment_id_comparison .then_with(|| self.offset.cmp(&other.offset)) - .then_with(|| self.bias.cmp(&other.bias))) + .then_with(|| self.bias.cmp(&other.bias)) } pub fn min(&self, other: &Self, buffer: &BufferSnapshot) -> Self { - if self.cmp(other, buffer).unwrap().is_le() { + if self.cmp(other, buffer).is_le() { self.clone() } else { other.clone() @@ -47,7 +47,7 @@ impl Anchor { } pub fn max(&self, other: &Self, buffer: &BufferSnapshot) -> Self { - if self.cmp(other, buffer).unwrap().is_ge() { + if self.cmp(other, buffer).is_ge() { self.clone() } else { other.clone() @@ -117,8 +117,8 @@ pub trait AnchorRangeExt { impl AnchorRangeExt for Range { fn cmp(&self, other: &Range, buffer: &BufferSnapshot) -> Result { - Ok(match self.start.cmp(&other.start, buffer)? { - Ordering::Equal => other.end.cmp(&self.end, buffer)?, + Ok(match self.start.cmp(&other.start, buffer) { + Ordering::Equal => other.end.cmp(&self.end, buffer), ord @ _ => ord, }) } diff --git a/crates/text/src/tests.rs b/crates/text/src/tests.rs index 05cf0af6ec..7961dccd56 100644 --- a/crates/text/src/tests.rs +++ b/crates/text/src/tests.rs @@ -340,59 +340,41 @@ fn test_anchors() { let anchor_at_offset_2 = buffer.anchor_before(2); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_0, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_1, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_2, &buffer), Ordering::Equal ); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_1, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_2, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_0 - .cmp(&anchor_at_offset_2, &buffer) - .unwrap(), + anchor_at_offset_0.cmp(&anchor_at_offset_2, &buffer), Ordering::Less ); assert_eq!( - anchor_at_offset_1 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_1.cmp(&anchor_at_offset_0, &buffer), Ordering::Greater ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_1, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_1, &buffer), Ordering::Greater ); assert_eq!( - anchor_at_offset_2 - .cmp(&anchor_at_offset_0, &buffer) - .unwrap(), + anchor_at_offset_2.cmp(&anchor_at_offset_0, &buffer), Ordering::Greater ); } From eefaa867502859e7e0ea16926c478a14eedd286b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2022 14:28:38 +0100 Subject: [PATCH 112/139] Introduce `Patch::transform_old` --- crates/text/src/patch.rs | 66 ++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 20 deletions(-) diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index 8e68b2545c..080e5f131e 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -199,6 +199,28 @@ where self.0.push(edit); } } + + pub fn transform_old(&self, old: T) -> T { + let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { + Ok(ix) => ix, + Err(ix) => { + if ix == 0 { + return old; + } else { + ix - 1 + } + } + }; + if let Some(edit) = self.0.get(ix) { + if old >= edit.old.end { + edit.new.end + (old - edit.old.end) + } else { + edit.new.start + } + } else { + old + } + } } impl IntoIterator for Patch { @@ -399,26 +421,6 @@ mod tests { ); } - // #[test] - // fn test_compose_edits() { - // assert_eq!( - // compose_edits( - // &Edit { - // old: 3..3, - // new: 3..6, - // }, - // &Edit { - // old: 2..7, - // new: 2..4, - // }, - // ), - // Edit { - // old: 2..4, - // new: 2..4 - // } - // ); - // } - #[gpui::test] fn test_two_new_edits_touching_one_old_edit() { assert_patch_composition( @@ -455,6 +457,30 @@ mod tests { ); } + #[gpui::test] + fn test_transform_old() { + let patch = Patch(vec![ + Edit { + old: 2..4, + new: 2..4, + }, + Edit { + old: 7..8, + new: 7..11, + }, + ]); + assert_eq!(patch.transform_old(0), 0); + assert_eq!(patch.transform_old(1), 1); + assert_eq!(patch.transform_old(2), 2); + assert_eq!(patch.transform_old(3), 2); + assert_eq!(patch.transform_old(4), 4); + assert_eq!(patch.transform_old(5), 5); + assert_eq!(patch.transform_old(6), 6); + assert_eq!(patch.transform_old(7), 7); + assert_eq!(patch.transform_old(8), 11); + assert_eq!(patch.transform_old(9), 12); + } + #[gpui::test(iterations = 100)] fn test_random_patch_compositions(mut rng: StdRng) { let operations = env::var("OPERATIONS") From 7a6fe734404462ad93b3b5062b60d703785c3bda Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2022 14:57:40 +0100 Subject: [PATCH 113/139] Rename `Patch::transform_old` to `Patch::old_to_new` Co-Authored-By: Nathan Sobo --- crates/text/src/patch.rs | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/crates/text/src/patch.rs b/crates/text/src/patch.rs index 080e5f131e..63e69f45a7 100644 --- a/crates/text/src/patch.rs +++ b/crates/text/src/patch.rs @@ -200,7 +200,7 @@ where } } - pub fn transform_old(&self, old: T) -> T { + pub fn old_to_new(&self, old: T) -> T { let ix = match self.0.binary_search_by(|probe| probe.old.start.cmp(&old)) { Ok(ix) => ix, Err(ix) => { @@ -458,7 +458,7 @@ mod tests { } #[gpui::test] - fn test_transform_old() { + fn test_old_to_new() { let patch = Patch(vec![ Edit { old: 2..4, @@ -469,16 +469,16 @@ mod tests { new: 7..11, }, ]); - assert_eq!(patch.transform_old(0), 0); - assert_eq!(patch.transform_old(1), 1); - assert_eq!(patch.transform_old(2), 2); - assert_eq!(patch.transform_old(3), 2); - assert_eq!(patch.transform_old(4), 4); - assert_eq!(patch.transform_old(5), 5); - assert_eq!(patch.transform_old(6), 6); - assert_eq!(patch.transform_old(7), 7); - assert_eq!(patch.transform_old(8), 11); - assert_eq!(patch.transform_old(9), 12); + assert_eq!(patch.old_to_new(0), 0); + assert_eq!(patch.old_to_new(1), 1); + assert_eq!(patch.old_to_new(2), 2); + assert_eq!(patch.old_to_new(3), 2); + assert_eq!(patch.old_to_new(4), 4); + assert_eq!(patch.old_to_new(5), 5); + assert_eq!(patch.old_to_new(6), 6); + assert_eq!(patch.old_to_new(7), 7); + assert_eq!(patch.old_to_new(8), 11); + assert_eq!(patch.old_to_new(9), 12); } #[gpui::test(iterations = 100)] From 865cd1960fa5c5ea54244e69948c96ef357effc9 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 25 Mar 2022 15:35:31 +0100 Subject: [PATCH 114/139] Preserve disk-based diagnostics whose ranges intersect with an edit since save Co-Authored-By: Nathan Sobo --- crates/project/src/project.rs | 65 ++++++++++++++--------------------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 2529c976ac..278c1271f0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -18,8 +18,8 @@ use language::{ proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, Anchor, Bias, Buffer, CodeAction, CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Event as BufferEvent, File as _, Language, LanguageRegistry, - LocalFile, OffsetRangeExt, Operation, PointUtf16, TextBufferSnapshot, ToLspPosition, ToOffset, - ToPointUtf16, Transaction, + LocalFile, OffsetRangeExt, Operation, Patch, PointUtf16, TextBufferSnapshot, ToLspPosition, + ToOffset, ToPointUtf16, Transaction, }; use lsp::{DiagnosticSeverity, DiagnosticTag, DocumentHighlightKind, LanguageServer}; use lsp_command::*; @@ -1866,38 +1866,23 @@ impl Project { }); let mut sanitized_diagnostics = Vec::new(); - let mut edits_since_save = snapshot - .edits_since::(buffer.read(cx).saved_version()) - .peekable(); - let mut last_edit_old_end = PointUtf16::zero(); - let mut last_edit_new_end = PointUtf16::zero(); - 'outer: for entry in diagnostics { - let mut start = entry.range.start; - let mut end = entry.range.end; - - // Some diagnostics are based on files on disk instead of buffers' - // current contents. Adjust these diagnostics' ranges to reflect - // any unsaved edits. + let edits_since_save = Patch::new( + snapshot + .edits_since::(buffer.read(cx).saved_version()) + .collect(), + ); + for entry in diagnostics { + let start; + let end; if entry.diagnostic.is_disk_based { - while let Some(edit) = edits_since_save.peek() { - if edit.old.end <= start { - last_edit_old_end = edit.old.end; - last_edit_new_end = edit.new.end; - edits_since_save.next(); - } else if edit.old.start <= end && edit.old.end >= start { - continue 'outer; - } else { - break; - } - } - - let start_overshoot = start - last_edit_old_end; - start = last_edit_new_end; - start += start_overshoot; - - let end_overshoot = end - last_edit_old_end; - end = last_edit_new_end; - end += end_overshoot; + // Some diagnostics are based on files on disk instead of buffers' + // current contents. Adjust these diagnostics' ranges to reflect + // any unsaved edits. + start = edits_since_save.old_to_new(entry.range.start); + end = edits_since_save.old_to_new(entry.range.end); + } else { + start = entry.range.start; + end = entry.range.end; } let mut range = snapshot.clip_point_utf16(start, Bias::Left) @@ -5018,7 +5003,7 @@ mod tests { } #[gpui::test] - async fn test_transforming_disk_based_diagnostics(cx: &mut gpui::TestAppContext) { + async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) { cx.foreground().forbid_parking(); let (mut lsp_config, mut fake_servers) = LanguageServerConfig::fake(); @@ -5243,11 +5228,13 @@ mod tests { buffer.update(cx, |buffer, cx| { buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx); buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx); + buffer.edit(Some(Point::new(3, 10)..Point::new(3, 10)), "xxx", cx); }); - let change_notification_2 = - fake_server.receive_notification::(); + let change_notification_2 = fake_server + .receive_notification::() + .await; assert!( - change_notification_2.await.text_document.version + change_notification_2.text_document.version > change_notification_1.text_document.version ); @@ -5255,7 +5242,7 @@ mod tests { fake_server.notify::( lsp::PublishDiagnosticsParams { uri: lsp::Url::from_file_path("/dir/a.rs").unwrap(), - version: Some(open_notification.text_document.version), + version: Some(change_notification_2.text_document.version), diagnostics: vec![ lsp::Diagnostic { range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)), @@ -5295,7 +5282,7 @@ mod tests { } }, DiagnosticEntry { - range: Point::new(3, 9)..Point::new(3, 11), + range: Point::new(3, 9)..Point::new(3, 14), diagnostic: Diagnostic { severity: DiagnosticSeverity::ERROR, message: "undefined variable 'BB'".to_string(), From a3e9a3afbf42438673572bca1c672cc8b57d1739 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Mar 2022 10:15:08 -0600 Subject: [PATCH 115/139] Clear out project registration and sharing state on disconnect Previously, we weren't fully clearing the state associated with projects and worktrees when losing connection. This caused us to not see guest avatars disappear and not be able to re-share upon reconnect. Co-Authored-By: Antonio Scandurra --- crates/project/src/project.rs | 163 ++++++++++++++++-------------- crates/project/src/worktree.rs | 9 +- crates/rpc/src/peer.rs | 2 +- crates/server/src/rpc.rs | 5 +- crates/workspace/src/workspace.rs | 2 +- 5 files changed, 97 insertions(+), 84 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 278c1271f0..0801043a09 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -302,31 +302,11 @@ impl Project { let mut status = rpc.status(); while let Some(status) = status.next().await { if let Some(this) = this.upgrade(&cx) { - let remote_id = if status.is_connected() { - let response = rpc.request(proto::RegisterProject {}).await?; - Some(response.project_id) + if status.is_connected() { + this.update(&mut cx, |this, cx| this.register(cx)).await?; } else { - None - }; - - if let Some(project_id) = remote_id { - let mut registrations = Vec::new(); - this.update(&mut cx, |this, cx| { - for worktree in this.worktrees(cx).collect::>() { - registrations.push(worktree.update( - cx, - |worktree, cx| { - let worktree = worktree.as_local_mut().unwrap(); - worktree.register(project_id, cx) - }, - )); - } - }); - for registration in registrations { - registration.await?; - } + this.update(&mut cx, |this, cx| this.unregister(cx)); } - this.update(&mut cx, |this, cx| this.set_remote_id(remote_id, cx)); } } Ok(()) @@ -558,17 +538,54 @@ impl Project { &self.fs } - fn set_remote_id(&mut self, remote_id: Option, cx: &mut ModelContext) { + fn unregister(&mut self, cx: &mut ModelContext) { + self.unshare(cx); + for worktree in &self.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + worktree.update(cx, |worktree, _| { + worktree.as_local_mut().unwrap().unregister(); + }); + } + } + if let ProjectClientState::Local { remote_id_tx, .. } = &mut self.client_state { - *remote_id_tx.borrow_mut() = remote_id; + *remote_id_tx.borrow_mut() = None; } self.subscriptions.clear(); - if let Some(remote_id) = remote_id { - self.subscriptions - .push(self.client.add_model_for_remote_entity(remote_id, cx)); - } - cx.emit(Event::RemoteIdChanged(remote_id)) + } + + fn register(&mut self, cx: &mut ModelContext) -> Task> { + self.unregister(cx); + + let response = self.client.request(proto::RegisterProject {}); + cx.spawn(|this, mut cx| async move { + let remote_id = response.await?.project_id; + + let mut registrations = Vec::new(); + this.update(&mut cx, |this, cx| { + if let ProjectClientState::Local { remote_id_tx, .. } = &mut this.client_state { + *remote_id_tx.borrow_mut() = Some(remote_id); + } + + cx.emit(Event::RemoteIdChanged(Some(remote_id))); + + this.subscriptions + .push(this.client.add_model_for_remote_entity(remote_id, cx)); + + for worktree in &this.worktrees { + if let Some(worktree) = worktree.upgrade(cx) { + registrations.push(worktree.update(cx, |worktree, cx| { + let worktree = worktree.as_local_mut().unwrap(); + worktree.register(remote_id, cx) + })); + } + } + }); + + futures::future::try_join_all(registrations).await?; + Ok(()) + }) } pub fn remote_id(&self) -> Option { @@ -725,59 +742,51 @@ impl Project { }) } - pub fn unshare(&self, cx: &mut ModelContext) -> Task> { + pub fn unshare(&mut self, cx: &mut ModelContext) { let rpc = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let project_id = this.update(&mut cx, |this, cx| { - if let ProjectClientState::Local { - is_shared, - remote_id_rx, - .. - } = &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()); - } - _ => {} - } - } + if let ProjectClientState::Local { + is_shared, + remote_id_rx, + .. + } = &mut self.client_state + { + if !*is_shared { + return; + } - for worktree_handle in this.worktrees.iter_mut() { - match worktree_handle { - WorktreeHandle::Strong(worktree) => { - if !worktree.read(cx).is_visible() { - *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); - } - } - _ => {} - } - } - - remote_id_rx - .borrow() - .ok_or_else(|| anyhow!("no project id")) - } else { - Err(anyhow!("can't share a remote project")) - } - })?; - - rpc.send(proto::UnshareProject { project_id })?; - this.update(&mut cx, |this, cx| { - this.collaborators.clear(); - this.shared_buffers.clear(); - for worktree in this.worktrees(cx).collect::>() { - worktree.update(cx, |worktree, _| { + *is_shared = false; + self.collaborators.clear(); + self.shared_buffers.clear(); + for worktree_handle in self.worktrees.iter_mut() { + if let WorktreeHandle::Strong(worktree) = worktree_handle { + let is_visible = worktree.update(cx, |worktree, _| { worktree.as_local_mut().unwrap().unshare(); + worktree.is_visible() }); + if !is_visible { + *worktree_handle = WorktreeHandle::Weak(worktree.downgrade()); + } } - cx.notify() - }); - Ok(()) - }) + } + + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(buffer) => { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + _ => {} + } + } + + if let Some(project_id) = *remote_id_rx.borrow() { + rpc.send(proto::UnshareProject { project_id }).log_err(); + } + + cx.notify(); + } else { + log::error!("attempted to unshare a remote project"); + } } fn project_unshared(&mut self, cx: &mut ModelContext) { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 2bc9c3d234..beacc5a863 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -711,7 +711,9 @@ impl LocalWorktree { let worktree = this.as_local_mut().unwrap(); match response { Ok(_) => { - worktree.registration = Registration::Done { project_id }; + if worktree.registration == Registration::Pending { + worktree.registration = Registration::Done { project_id }; + } Ok(()) } Err(error) => { @@ -808,6 +810,11 @@ impl LocalWorktree { }) } + pub fn unregister(&mut self) { + self.unshare(); + self.registration = Registration::None; + } + pub fn unshare(&mut self) { self.share.take(); } diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 4156c0883d..8f0b96174a 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -96,7 +96,7 @@ pub struct ConnectionState { const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -const RECEIVE_TIMEOUT: Duration = Duration::from_secs(30); +const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); impl Peer { pub fn new() -> Arc { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 68e435dfb1..748fee3380 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1310,10 +1310,7 @@ mod tests { .unwrap(); // Unshare the project as client A - project_a - .update(cx_a, |project, cx| project.unshare(cx)) - .await - .unwrap(); + project_a.update(cx_a, |project, cx| project.unshare(cx)); project_b .condition(cx_b, |project, _| project.is_read_only()) .await; diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 65300c5f34..17b0c4b518 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1278,7 +1278,7 @@ impl Workspace { self.project.update(cx, |project, cx| { if project.is_local() { if project.is_shared() { - project.unshare(cx).detach(); + project.unshare(cx); } else { project.share(cx).detach(); } From 62b4eb5efcf0ffcae91ba0f0bb635e379d9206ee Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 25 Mar 2022 10:32:31 -0600 Subject: [PATCH 116/139] Add integration test for dropping host connections while sharing Co-Authored-By: Antonio Scandurra --- crates/rpc/src/peer.rs | 2 +- crates/server/src/rpc.rs | 101 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 8f0b96174a..726453bea8 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -96,7 +96,7 @@ pub struct ConnectionState { const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(1); const WRITE_TIMEOUT: Duration = Duration::from_secs(2); -const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); +pub const RECEIVE_TIMEOUT: Duration = Duration::from_secs(5); impl Peer { pub fn new() -> Arc { diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 748fee3380..4587126763 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -1342,6 +1342,107 @@ mod tests { .unwrap(); } + #[gpui::test(iterations = 10)] + async fn test_host_disconnect(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) { + let lang_registry = Arc::new(LanguageRegistry::test()); + let fs = FakeFs::new(cx_a.background()); + cx_a.foreground().forbid_parking(); + + // 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(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + // Share a project as client A + fs.insert_tree( + "/a", + json!({ + ".zed.toml": r#"collaborators = ["user_b"]"#, + "a.txt": "a-contents", + "b.txt": "b-contents", + }), + ) + .await; + let project_a = cx_a.update(|cx| { + Project::local( + client_a.clone(), + client_a.user_store.clone(), + lang_registry.clone(), + fs.clone(), + cx, + ) + }); + let (worktree_a, _) = project_a + .update(cx_a, |p, cx| { + p.find_or_create_local_worktree("/a", true, cx) + }) + .await + .unwrap(); + worktree_a + .read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete()) + .await; + let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id()); + project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + // Join that project 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(); + project_b + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + + // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. + server.disconnect_client(client_a.current_user_id(cx_a)); + cx_a.foreground().advance_clock(rpc::RECEIVE_TIMEOUT); + project_a + .condition(cx_a, |project, _| project.collaborators().is_empty()) + .await; + project_a.read_with(cx_a, |project, _| assert!(!project.is_shared())); + project_b + .condition(cx_b, |project, _| project.is_read_only()) + .await; + assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); + cx_b.update(|_| { + drop(project_b); + }); + + // Await reconnection + let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await; + + // Share the project again and ensure guests can still join. + project_a + .update(cx_a, |project, cx| project.share(cx)) + .await + .unwrap(); + assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); + + let project_b2 = Project::remote( + project_id, + client_b.clone(), + client_b.user_store.clone(), + lang_registry.clone(), + fs.clone(), + &mut cx_b.to_async(), + ) + .await + .unwrap(); + project_b2 + .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) + .await + .unwrap(); + } + #[gpui::test(iterations = 10)] async fn test_propagate_saves_and_fs_changes( cx_a: &mut TestAppContext, From bb9b36dccda4fbb1869d5a590d1db44717b1ccdf Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 24 Mar 2022 19:24:36 -0700 Subject: [PATCH 117/139] Add initial vim mode mode switching Co-authored-by: Nathan Sobo --- Cargo.lock | 14 +++ crates/editor/src/editor.rs | 43 +++++++-- crates/gpui/src/app.rs | 88 ++++++++++-------- crates/gpui/src/keymap.rs | 14 ++- crates/text/src/selection.rs | 13 +++ crates/vim/Cargo.toml | 23 +++++ crates/vim/src/editor_events.rs | 57 ++++++++++++ crates/vim/src/editor_utils.rs | 100 ++++++++++++++++++++ crates/vim/src/insert.rs | 28 ++++++ crates/vim/src/mode.rs | 36 ++++++++ crates/vim/src/normal.rs | 58 ++++++++++++ crates/vim/src/vim.rs | 98 ++++++++++++++++++++ crates/vim/src/vim_tests.rs | 152 +++++++++++++++++++++++++++++++ crates/workspace/src/settings.rs | 6 ++ crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + 16 files changed, 683 insertions(+), 49 deletions(-) create mode 100644 crates/vim/Cargo.toml create mode 100644 crates/vim/src/editor_events.rs create mode 100644 crates/vim/src/editor_utils.rs create mode 100644 crates/vim/src/insert.rs create mode 100644 crates/vim/src/mode.rs create mode 100644 crates/vim/src/normal.rs create mode 100644 crates/vim/src/vim.rs create mode 100644 crates/vim/src/vim_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 01d398b51c..65d690db23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5673,6 +5673,19 @@ version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +[[package]] +name = "vim" +version = "0.1.0" +dependencies = [ + "collections", + "editor", + "gpui", + "language", + "log", + "project", + "workspace", +] + [[package]] name = "waker-fn" version = "1.1.0" @@ -6003,6 +6016,7 @@ dependencies = [ "unindent", "url", "util", + "vim", "workspace", ] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 901699f7c4..d16900ebba 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -456,6 +456,8 @@ pub struct Editor { pending_rename: Option, searchable: bool, cursor_shape: CursorShape, + keymap_context_layers: BTreeMap, + input_enabled: bool, leader_replica_id: Option, } @@ -932,6 +934,8 @@ impl Editor { searchable: true, override_text_style: None, cursor_shape: Default::default(), + keymap_context_layers: Default::default(), + input_enabled: true, leader_replica_id: None, }; this.end_selection(cx); @@ -1000,6 +1004,10 @@ impl Editor { ) } + pub fn mode(&self) -> EditorMode { + self.mode + } + pub fn set_placeholder_text( &mut self, placeholder_text: impl Into>, @@ -1063,6 +1071,19 @@ impl Editor { cx.notify(); } + pub fn set_keymap_context_layer(&mut self, context: gpui::keymap::Context) { + self.keymap_context_layers + .insert(TypeId::of::(), context); + } + + pub fn remove_keymap_context_layer(&mut self) { + self.keymap_context_layers.remove(&TypeId::of::()); + } + + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) @@ -1742,6 +1763,11 @@ impl Editor { } pub fn handle_input(&mut self, action: &Input, cx: &mut ViewContext) { + if !self.input_enabled { + cx.propagate_action(); + return; + } + let text = action.0.as_ref(); if !self.skip_autoclose_end(text, cx) { self.transact(cx, |this, cx| { @@ -5733,26 +5759,31 @@ impl View for Editor { } fn keymap_context(&self, _: &AppContext) -> gpui::keymap::Context { - let mut cx = Self::default_keymap_context(); + let mut context = Self::default_keymap_context(); let mode = match self.mode { EditorMode::SingleLine => "single_line", EditorMode::AutoHeight { .. } => "auto_height", EditorMode::Full => "full", }; - cx.map.insert("mode".into(), mode.into()); + context.map.insert("mode".into(), mode.into()); if self.pending_rename.is_some() { - cx.set.insert("renaming".into()); + context.set.insert("renaming".into()); } match self.context_menu.as_ref() { Some(ContextMenu::Completions(_)) => { - cx.set.insert("showing_completions".into()); + context.set.insert("showing_completions".into()); } Some(ContextMenu::CodeActions(_)) => { - cx.set.insert("showing_code_actions".into()); + context.set.insert("showing_code_actions".into()); } None => {} } - cx + + for layer in self.keymap_context_layers.values() { + context.extend(layer); + } + + context } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index db21612439..a291ad3c4b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -442,13 +442,32 @@ impl TestAppContext { } pub fn dispatch_keystroke( - &self, + &mut self, window_id: usize, - responder_chain: Vec, - keystroke: &Keystroke, - ) -> Result { - let mut state = self.cx.borrow_mut(); - state.dispatch_keystroke(window_id, responder_chain, keystroke) + keystroke: Keystroke, + input: Option, + is_held: bool, + ) { + self.cx.borrow_mut().update(|cx| { + let presenter = cx + .presenters_and_platform_windows + .get(&window_id) + .unwrap() + .0 + .clone(); + let responder_chain = presenter.borrow().dispatch_path(cx.as_ref()); + + if !cx.dispatch_keystroke(window_id, responder_chain, &keystroke) { + presenter.borrow_mut().dispatch_event( + Event::KeyDown { + keystroke, + input, + is_held, + }, + cx, + ); + } + }); } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -503,7 +522,7 @@ impl TestAppContext { pub fn update T>(&mut self, callback: F) -> T { let mut state = self.cx.borrow_mut(); - // Don't increment pending flushes in order to effects to be flushed before the callback + // Don't increment pending flushes in order for effects to be flushed before the callback // completes, which is helpful in tests. let result = callback(&mut *state); // Flush effects after the callback just in case there are any. This can happen in edge @@ -1250,9 +1269,9 @@ impl MutableAppContext { } } - fn defer(&mut self, callback: Box) { + pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut MutableAppContext)) { self.pending_effects.push_back(Effect::Deferred { - callback, + callback: Box::new(callback), after_window_update: false, }) } @@ -1379,17 +1398,15 @@ impl MutableAppContext { window_id: usize, responder_chain: Vec, keystroke: &Keystroke, - ) -> Result { + ) -> bool { let mut context_chain = Vec::new(); for view_id in &responder_chain { - if let Some(view) = self.cx.views.get(&(window_id, *view_id)) { - context_chain.push(view.keymap_context(self.as_ref())); - } else { - return Err(anyhow!( - "View {} in responder chain does not exist", - view_id - )); - } + let view = self + .cx + .views + .get(&(window_id, *view_id)) + .expect("view in responder chain does not exist"); + context_chain.push(view.keymap_context(self.as_ref())); } let mut pending = false; @@ -1404,13 +1421,13 @@ impl MutableAppContext { if self.dispatch_action_any(window_id, &responder_chain[0..=i], action.as_ref()) { self.keystroke_matcher.clear_pending(); - return Ok(true); + return true; } } } } - Ok(pending) + pending } pub fn default_global(&mut self) -> &T { @@ -1540,14 +1557,11 @@ impl MutableAppContext { window.on_event(Box::new(move |event| { app.update(|cx| { if let Event::KeyDown { keystroke, .. } = &event { - if cx - .dispatch_keystroke( - window_id, - presenter.borrow().dispatch_path(cx.as_ref()), - keystroke, - ) - .unwrap() - { + if cx.dispatch_keystroke( + window_id, + presenter.borrow().dispatch_path(cx.as_ref()), + keystroke, + ) { return; } } @@ -2711,11 +2725,11 @@ impl<'a, T: Entity> ModelContext<'a, T> { pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ModelContext)) { let handle = self.handle(); - self.app.defer(Box::new(move |cx| { + self.app.defer(move |cx| { handle.update(cx, |model, cx| { callback(model, cx); }) - })) + }) } pub fn emit(&mut self, payload: T::Event) { @@ -3064,11 +3078,11 @@ impl<'a, T: View> ViewContext<'a, T> { pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut T, &mut ViewContext)) { let handle = self.handle(); - self.app.defer(Box::new(move |cx| { + self.app.defer(move |cx| { handle.update(cx, |view, cx| { callback(view, cx); }) - })) + }) } pub fn after_window_update( @@ -3678,9 +3692,9 @@ impl ViewHandle { F: 'static + FnOnce(&mut T, &mut ViewContext), { let this = self.clone(); - cx.as_mut().defer(Box::new(move |cx| { + cx.as_mut().defer(move |cx| { this.update(cx, |view, cx| update(view, cx)); - })); + }); } pub fn is_focused(&self, cx: &AppContext) -> bool { @@ -5921,8 +5935,7 @@ mod tests { window_id, vec![view_1.id(), view_2.id(), view_3.id()], &Keystroke::parse("a").unwrap(), - ) - .unwrap(); + ); assert_eq!(&*actions.borrow(), &["2 a"]); @@ -5931,8 +5944,7 @@ mod tests { window_id, vec![view_1.id(), view_2.id(), view_3.id()], &Keystroke::parse("b").unwrap(), - ) - .unwrap(); + ); assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]); } diff --git a/crates/gpui/src/keymap.rs b/crates/gpui/src/keymap.rs index 05fbd5b74b..37223d77d1 100644 --- a/crates/gpui/src/keymap.rs +++ b/crates/gpui/src/keymap.rs @@ -224,15 +224,19 @@ impl Keystroke { key: key.unwrap(), }) } + + pub fn modified(&self) -> bool { + self.ctrl || self.alt || self.shift || self.cmd + } } impl Context { - pub fn extend(&mut self, other: Context) { - for v in other.set { - self.set.insert(v); + pub fn extend(&mut self, other: &Context) { + for v in &other.set { + self.set.insert(v.clone()); } - for (k, v) in other.map { - self.map.insert(k, v); + for (k, v) in &other.map { + self.map.insert(k.clone(), v.clone()); } } } diff --git a/crates/text/src/selection.rs b/crates/text/src/selection.rs index 817fde7d66..23b0d2b3b0 100644 --- a/crates/text/src/selection.rs +++ b/crates/text/src/selection.rs @@ -40,6 +40,19 @@ impl Selection { self.start.clone() } } + + pub fn map(&self, f: F) -> Selection + where + F: Fn(T) -> S, + { + Selection:: { + id: self.id, + start: f(self.start.clone()), + end: f(self.end.clone()), + reversed: self.reversed, + goal: self.goal, + } + } } impl Selection { diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml new file mode 100644 index 0000000000..a223172749 --- /dev/null +++ b/crates/vim/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "vim" +version = "0.1.0" +edition = "2021" + +[lib] +path = "src/vim.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { path = "../editor" } +gpui = { path = "../gpui" } +language = { path = "../language" } +workspace = { path = "../workspace" } +log = "0.4" + +[dev-dependencies] +editor = { path = "../editor", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +project = { path = "../project", features = ["test-support"] } +language = { path = "../language", features = ["test-support"] } +workspace = { path = "../workspace", features = ["test-support"] } \ No newline at end of file diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs new file mode 100644 index 0000000000..388de1a2ca --- /dev/null +++ b/crates/vim/src/editor_events.rs @@ -0,0 +1,57 @@ +use editor::{EditorBlurred, EditorCreated, EditorFocused, EditorMode, EditorReleased}; +use gpui::MutableAppContext; + +use crate::{mode::Mode, SwitchMode, VimState}; + +pub fn init(cx: &mut MutableAppContext) { + cx.subscribe_global(editor_created).detach(); + cx.subscribe_global(editor_focused).detach(); + cx.subscribe_global(editor_blurred).detach(); + cx.subscribe_global(editor_released).detach(); +} + +fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { + cx.update_default_global(|vim_state: &mut VimState, cx| { + vim_state.editors.insert(editor.id(), editor.downgrade()); + if vim_state.enabled { + VimState::update_cursor_shapes(cx); + } + }) +} + +fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppContext) { + let mode = if matches!(editor.read(cx).mode(), EditorMode::SingleLine) { + Mode::Insert + } else { + Mode::Normal + }; + + cx.update_default_global(|vim_state: &mut VimState, _| { + vim_state.active_editor = Some(editor.downgrade()); + }); + VimState::switch_mode(&SwitchMode(mode), cx); +} + +fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { + cx.update_default_global(|vim_state: &mut VimState, _| { + if let Some(previous_editor) = vim_state.active_editor.clone() { + if previous_editor == editor.clone() { + vim_state.active_editor = None; + } + } + }); + editor.update(cx, |editor, _| { + editor.remove_keymap_context_layer::(); + }) +} + +fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { + cx.update_default_global(|vim_state: &mut VimState, _| { + vim_state.editors.remove(&editor.id()); + if let Some(previous_editor) = vim_state.active_editor.clone() { + if previous_editor == editor.clone() { + vim_state.active_editor = None; + } + } + }); +} diff --git a/crates/vim/src/editor_utils.rs b/crates/vim/src/editor_utils.rs new file mode 100644 index 0000000000..da9c588888 --- /dev/null +++ b/crates/vim/src/editor_utils.rs @@ -0,0 +1,100 @@ +use editor::{display_map::DisplaySnapshot, Bias, DisplayPoint, Editor}; +use gpui::ViewContext; +use language::{Selection, SelectionGoal}; + +pub trait VimEditorExt { + fn adjust_selections(self: &mut Self, cx: &mut ViewContext); + fn adjusted_move_selections( + self: &mut Self, + cx: &mut ViewContext, + move_selection: impl Fn(&DisplaySnapshot, &mut Selection), + ); + fn adjusted_move_selection_heads( + &mut self, + cx: &mut ViewContext, + update_head: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ); + fn adjusted_move_cursors( + self: &mut Self, + cx: &mut ViewContext, + update_cursor_position: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ); +} + +pub fn adjust_display_point( + map: &DisplaySnapshot, + mut display_point: DisplayPoint, +) -> DisplayPoint { + let next_char = map.chars_at(display_point).next(); + if next_char == Some('\n') || next_char == None { + *display_point.column_mut() = display_point.column().saturating_sub(1); + display_point = map.clip_point(display_point, Bias::Left); + } + display_point +} + +impl VimEditorExt for Editor { + fn adjust_selections(self: &mut Self, cx: &mut ViewContext) { + self.move_selections(cx, |map, selection| { + if selection.is_empty() { + let adjusted_cursor = adjust_display_point(map, selection.start); + selection.collapse_to(adjusted_cursor, selection.goal); + } else { + let adjusted_head = adjust_display_point(map, selection.head()); + selection.set_head(adjusted_head, selection.goal); + } + }) + } + + fn adjusted_move_selections( + self: &mut Self, + cx: &mut ViewContext, + move_selection: impl Fn(&DisplaySnapshot, &mut Selection), + ) { + self.move_selections(cx, |map, selection| { + move_selection(map, selection); + let adjusted_head = adjust_display_point(map, selection.head()); + selection.set_head(adjusted_head, selection.goal); + }) + } + + fn adjusted_move_selection_heads( + &mut self, + cx: &mut ViewContext, + update_head: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.adjusted_move_selections(cx, |map, selection| { + let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); + let adjusted_head = adjust_display_point(map, new_head); + selection.set_head(adjusted_head, new_goal); + }); + } + + fn adjusted_move_cursors( + self: &mut Self, + cx: &mut ViewContext, + update_cursor_position: impl Fn( + &DisplaySnapshot, + DisplayPoint, + SelectionGoal, + ) -> (DisplayPoint, SelectionGoal), + ) { + self.move_selections(cx, |map, selection| { + let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); + let adjusted_cursor = adjust_display_point(map, cursor); + selection.collapse_to(adjusted_cursor, new_goal); + }); + } +} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs new file mode 100644 index 0000000000..b07449401c --- /dev/null +++ b/crates/vim/src/insert.rs @@ -0,0 +1,28 @@ +use editor::Bias; +use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use language::SelectionGoal; +use workspace::Workspace; + +use crate::{editor_utils::VimEditorExt, mode::Mode, SwitchMode, VimState}; + +action!(NormalBefore); + +pub fn init(cx: &mut MutableAppContext) { + let context = Some("Editor && vim_mode == insert"); + cx.add_bindings(vec![ + Binding::new("escape", NormalBefore, context), + Binding::new("ctrl-c", NormalBefore, context), + ]); + + cx.add_action(normal_before); +} + +fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { + VimState::switch_mode(&SwitchMode(Mode::Normal), cx); + VimState::update_active_editor(cx, |editor, cx| { + editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); +} diff --git a/crates/vim/src/mode.rs b/crates/vim/src/mode.rs new file mode 100644 index 0000000000..2438a9fa3d --- /dev/null +++ b/crates/vim/src/mode.rs @@ -0,0 +1,36 @@ +use editor::CursorShape; +use gpui::keymap::Context; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Mode { + Normal, + Insert, +} + +impl Mode { + pub fn cursor_shape(&self) -> CursorShape { + match self { + Mode::Normal => CursorShape::Block, + Mode::Insert => CursorShape::Bar, + } + } + + pub fn keymap_context_layer(&self) -> Context { + let mut context = Context::default(); + context.map.insert( + "vim_mode".to_string(), + match self { + Self::Normal => "normal", + Self::Insert => "insert", + } + .to_string(), + ); + context + } +} + +impl Default for Mode { + fn default() -> Self { + Self::Normal + } +} diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs new file mode 100644 index 0000000000..e80d5f0bc7 --- /dev/null +++ b/crates/vim/src/normal.rs @@ -0,0 +1,58 @@ +use editor::{movement, Bias}; +use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; +use language::SelectionGoal; +use workspace::Workspace; + +use crate::{editor_utils::VimEditorExt, Mode, SwitchMode, VimState}; + +action!(InsertBefore); +action!(MoveLeft); +action!(MoveDown); +action!(MoveUp); +action!(MoveRight); + +pub fn init(cx: &mut MutableAppContext) { + let context = Some("Editor && vim_mode == normal"); + cx.add_bindings(vec![ + Binding::new("i", SwitchMode(Mode::Insert), context), + Binding::new("h", MoveLeft, context), + Binding::new("j", MoveDown, context), + Binding::new("k", MoveUp, context), + Binding::new("l", MoveRight, context), + ]); + + cx.add_action(move_left); + cx.add_action(move_down); + cx.add_action(move_up); + cx.add_action(move_right); +} + +fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { + VimState::update_active_editor(cx, |editor, cx| { + editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); + }); +} + +fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext) { + VimState::update_active_editor(cx, |editor, cx| { + editor.adjusted_move_cursors(cx, movement::down); + }); +} + +fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext) { + VimState::update_active_editor(cx, |editor, cx| { + editor.adjusted_move_cursors(cx, movement::up); + }); +} + +fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) { + VimState::update_active_editor(cx, |editor, cx| { + editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() += 1; + (map.clip_point(cursor, Bias::Right), SelectionGoal::None) + }); + }); +} diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs new file mode 100644 index 0000000000..76ff216a02 --- /dev/null +++ b/crates/vim/src/vim.rs @@ -0,0 +1,98 @@ +mod editor_events; +mod editor_utils; +mod insert; +mod mode; +mod normal; +#[cfg(test)] +mod vim_tests; + +use collections::HashMap; +use editor::Editor; +use editor_utils::VimEditorExt; +use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; + +use mode::Mode; +use workspace::{self, Settings, Workspace}; + +action!(SwitchMode, Mode); + +pub fn init(cx: &mut MutableAppContext) { + editor_events::init(cx); + insert::init(cx); + normal::init(cx); + + cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| VimState::switch_mode(action, cx)); + + cx.observe_global::(VimState::settings_changed) + .detach(); +} + +#[derive(Default)] +pub struct VimState { + editors: HashMap>, + active_editor: Option>, + + enabled: bool, + mode: Mode, +} + +impl VimState { + fn update_active_editor( + cx: &mut MutableAppContext, + update: impl FnOnce(&mut Editor, &mut ViewContext) -> S, + ) -> Option { + cx.global::() + .active_editor + .clone() + .and_then(|ae| ae.upgrade(cx)) + .map(|ae| ae.update(cx, update)) + } + + fn switch_mode(SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) { + let active_editor = cx.update_default_global(|this: &mut Self, _| { + this.mode = *mode; + this.active_editor.clone() + }); + + if let Some(active_editor) = active_editor.and_then(|e| e.upgrade(cx)) { + active_editor.update(cx, |active_editor, cx| { + active_editor.set_keymap_context_layer::(mode.keymap_context_layer()); + active_editor.set_input_enabled(*mode == Mode::Insert); + if *mode != Mode::Insert { + active_editor.adjust_selections(cx); + } + }); + } + VimState::update_cursor_shapes(cx); + } + + fn settings_changed(cx: &mut MutableAppContext) { + cx.update_default_global(|this: &mut Self, cx| { + let settings = cx.global::(); + if this.enabled != settings.vim_mode { + this.enabled = settings.vim_mode; + this.mode = if settings.vim_mode { + Mode::Normal + } else { + Mode::Insert + }; + Self::update_cursor_shapes(cx); + } + }); + } + + fn update_cursor_shapes(cx: &mut MutableAppContext) { + cx.defer(move |cx| { + cx.update_default_global(|this: &mut VimState, cx| { + let cursor_shape = this.mode.cursor_shape(); + for editor in this.editors.values() { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + editor.set_cursor_shape(cursor_shape, cx); + }); + } + } + }); + }); + } +} diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs new file mode 100644 index 0000000000..f8c6b3d54d --- /dev/null +++ b/crates/vim/src/vim_tests.rs @@ -0,0 +1,152 @@ +use std::ops::Deref; + +use editor::{display_map::ToDisplayPoint, DisplayPoint}; +use gpui::{json::json, keymap::Keystroke, AppContext, ViewHandle}; +use language::{Point, Selection}; +use workspace::{WorkspaceHandle, WorkspaceParams}; + +use crate::*; + +#[gpui::test] +async fn test_insert_mode(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, "").await; + assert_eq!(cx.mode(), Mode::Normal); + cx.simulate_keystroke("i"); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes(&["T", "e", "s", "t"]); + assert_eq!(cx.editor_text(), "Test".to_owned()); + cx.simulate_keystroke("escape"); + assert_eq!(cx.mode(), Mode::Normal); +} + +#[gpui::test] +async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, "Test\nTestTest\nTest").await; + assert_eq!(cx.mode(), Mode::Normal); + cx.simulate_keystroke("l"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 1)); + cx.simulate_keystroke("h"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 0)); + cx.simulate_keystroke("j"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + cx.simulate_keystroke("k"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 0)); + + cx.simulate_keystroke("j"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + + // When moving left, cursor does not wrap to the previous line + cx.simulate_keystroke("h"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + + // When moving right, cursor does not reach the line end or wrap to the next line + for _ in 0..9 { + cx.simulate_keystroke("l"); + } + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 7)); + + // Goal column respects the inability to reach the end of the line + cx.simulate_keystroke("k"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 3)); + cx.simulate_keystroke("j"); + assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 7)); +} + +struct VimTestAppContext<'a> { + cx: &'a mut gpui::TestAppContext, + window_id: usize, + editor: ViewHandle, +} + +impl<'a> VimTestAppContext<'a> { + async fn new( + cx: &'a mut gpui::TestAppContext, + initial_editor_text: &str, + ) -> VimTestAppContext<'a> { + cx.update(|cx| { + editor::init(cx); + crate::init(cx); + }); + let params = cx.update(WorkspaceParams::test); + params + .fs + .as_fake() + .insert_tree( + "/root", + json!({ "dir": { "test.txt": initial_editor_text } }), + ) + .await; + + let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx)); + params + .project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/root", true, cx) + }) + .await + .unwrap(); + cx.read(|cx| workspace.read(cx).worktree_scans_complete(cx)) + .await; + + let file = cx.read(|cx| workspace.file_project_paths(cx)[0].clone()); + let item = workspace + .update(cx, |workspace, cx| workspace.open_path(file, cx)) + .await + .expect("Could not open test file"); + + let editor = cx.update(|cx| { + item.act_as::(cx) + .expect("Opened test file wasn't an editor") + }); + editor.update(cx, |_, cx| cx.focus_self()); + + Self { + cx, + window_id, + editor, + } + } + + fn newest_selection(&mut self) -> Selection { + self.editor.update(self.cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + editor + .newest_selection::(cx) + .map(|point| point.to_display_point(&snapshot.display_snapshot)) + }) + } + + fn mode(&mut self) -> Mode { + self.cx.update(|cx| cx.global::().mode) + } + + fn editor_text(&mut self) -> String { + self.editor + .update(self.cx, |editor, cx| editor.snapshot(cx).text()) + } + + fn simulate_keystroke(&mut self, keystroke_text: &str) { + let keystroke = Keystroke::parse(keystroke_text).unwrap(); + let input = if keystroke.modified() { + None + } else { + Some(keystroke.key.clone()) + }; + self.cx + .dispatch_keystroke(self.window_id, keystroke, input, false); + } + + fn simulate_keystrokes(&mut self, keystroke_texts: &[&str]) { + for keystroke_text in keystroke_texts.into_iter() { + self.simulate_keystroke(keystroke_text); + } + } +} + +impl<'a> Deref for VimTestAppContext<'a> { + type Target = gpui::TestAppContext; + + fn deref(&self) -> &Self::Target { + self.cx + } +} diff --git a/crates/workspace/src/settings.rs b/crates/workspace/src/settings.rs index e97f7f8920..5ccf8056e6 100644 --- a/crates/workspace/src/settings.rs +++ b/crates/workspace/src/settings.rs @@ -17,6 +17,7 @@ use util::ResultExt; pub struct Settings { pub buffer_font_family: FamilyId, pub buffer_font_size: f32, + pub vim_mode: bool, pub tab_size: usize, pub soft_wrap: SoftWrap, pub preferred_line_length: u32, @@ -48,6 +49,8 @@ struct SettingsFileContent { buffer_font_family: Option, #[serde(default)] buffer_font_size: Option, + #[serde(default)] + vim_mode: Option, #[serde(flatten)] editor: LanguageOverride, #[serde(default)] @@ -130,6 +133,7 @@ impl Settings { Ok(Self { buffer_font_family: font_cache.load_family(&[buffer_font_family])?, buffer_font_size: 15., + vim_mode: false, tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, @@ -174,6 +178,7 @@ impl Settings { Settings { buffer_font_family: cx.font_cache().load_family(&["Monaco"]).unwrap(), buffer_font_size: 14., + vim_mode: false, tab_size: 4, soft_wrap: SoftWrap::None, preferred_line_length: 80, @@ -200,6 +205,7 @@ impl Settings { } merge(&mut self.buffer_font_size, data.buffer_font_size); + merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.soft_wrap, data.editor.soft_wrap); merge(&mut self.tab_size, data.editor.tab_size); merge( diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index ca0eec353e..e541dff42d 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -55,6 +55,7 @@ text = { path = "../text" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } util = { path = "../util" } +vim = { path = "../vim" } workspace = { path = "../workspace" } anyhow = "1.0.38" async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b79f7f1bb0..63721346c3 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -78,6 +78,7 @@ fn main() { project_panel::init(cx); diagnostics::init(cx); search::init(cx); + vim::init(cx); cx.spawn({ let client = client.clone(); |cx| async move { From 0aaf2706507f212dfe13e9e1c54d2fa49ec57325 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 25 Mar 2022 18:09:37 -0700 Subject: [PATCH 118/139] Add clip_to_line_end to display_map/snapshot and set it to ensure vim positioning in normal mode Co-authored-by: Nathan Sobo --- crates/editor/src/display_map.rs | 112 ++++++++++++++++++++----------- crates/editor/src/editor.rs | 5 ++ crates/editor/src/element.rs | 2 +- crates/editor/src/movement.rs | 43 +++--------- crates/editor/src/test.rs | 32 +++++++++ crates/vim/src/editor_utils.rs | 33 +++++---- crates/vim/src/insert.rs | 2 +- crates/vim/src/normal.rs | 10 +-- crates/vim/src/vim.rs | 5 +- crates/vim/src/vim_tests.rs | 2 +- 10 files changed, 142 insertions(+), 104 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee964ec56..c3fef6e013 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -36,6 +36,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + pub clip_at_line_ends: bool, } impl Entity for DisplayMap { @@ -67,6 +68,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + clip_at_line_ends: false, } } @@ -87,6 +89,7 @@ impl DisplayMap { wraps_snapshot, blocks_snapshot, text_highlights: self.text_highlights.clone(), + clip_at_line_ends: self.clip_at_line_ends, } } @@ -204,6 +207,7 @@ pub struct DisplaySnapshot { wraps_snapshot: wrap_map::WrapSnapshot, blocks_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + clip_at_line_ends: bool, } impl DisplaySnapshot { @@ -331,7 +335,12 @@ impl DisplaySnapshot { } pub fn clip_point(&self, point: DisplayPoint, bias: Bias) -> DisplayPoint { - DisplayPoint(self.blocks_snapshot.clip_point(point.0, bias)) + let mut clipped = self.blocks_snapshot.clip_point(point.0, bias); + if self.clip_at_line_ends && clipped.column == self.line_len(clipped.row) { + clipped.column = clipped.column.saturating_sub(1); + clipped = self.blocks_snapshot.clip_point(clipped, Bias::Left); + } + DisplayPoint(clipped) } pub fn folds_in_range<'a, T>( @@ -487,11 +496,11 @@ impl ToDisplayPoint for Anchor { } #[cfg(test)] -mod tests { +pub mod tests { use super::*; use crate::{ movement, - test::{marked_text_ranges}, + test::{marked_display_snapshot, marked_text_ranges}, }; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; @@ -1132,49 +1141,70 @@ mod tests { #[gpui::test] fn test_clip_point(cx: &mut gpui::MutableAppContext) { + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) { + let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); + + match bias { + Bias::Left => { + if shift_right { + *markers[1].column_mut() += 1; + } + + assert_eq!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) + } + Bias::Right => { + if shift_right { + *markers[0].column_mut() += 1; + } + + assert_eq!( + unmarked_snapshot.clip_point(dbg!(markers[0]), bias), + markers[1] + ) + } + }; + } + use Bias::{Left, Right}; + assert("||α", false, Left, cx); + assert("||α", true, Left, cx); + assert("||α", false, Right, cx); + assert("|α|", true, Right, cx); + assert("||✋", false, Left, cx); + assert("||✋", true, Left, cx); + assert("||✋", false, Right, cx); + assert("|✋|", true, Right, cx); + assert("||🍐", false, Left, cx); + assert("||🍐", true, Left, cx); + assert("||🍐", false, Right, cx); + assert("|🍐|", true, Right, cx); + assert("||\t", false, Left, cx); + assert("||\t", true, Left, cx); + assert("||\t", false, Right, cx); + assert("|\t|", true, Right, cx); + assert(" ||\t", false, Left, cx); + assert(" ||\t", true, Left, cx); + assert(" ||\t", false, Right, cx); + assert(" |\t|", true, Right, cx); + assert(" ||\t", false, Left, cx); + assert(" ||\t", false, Right, cx); + } - let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n"; - let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n"; - let buffer = MultiBuffer::build_simple(text, cx); - - let tab_size = 4; - let font_cache = cx.font_cache(); - let family_id = font_cache.load_family(&["Helvetica"]).unwrap(); - let font_id = font_cache - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - let map = cx.add_model(|cx| { - DisplayMap::new(buffer.clone(), tab_size, font_id, font_size, None, 1, 1, cx) - }); - let map = map.update(cx, |map, cx| map.snapshot(cx)); - - assert_eq!(map.text(), display_text); - for (input_column, bias, output_column) in vec![ - ("'a', '".len(), Left, "'a', '".len()), - ("'a', '".len() + 1, Left, "'a', '".len()), - ("'a', '".len() + 1, Right, "'a', 'α".len()), - ("'a', 'α', ".len(), Left, "'a', 'α',".len()), - ("'a', 'α', ".len(), Right, "'a', 'α', ".len()), - ("'a', 'α', '".len() + 1, Left, "'a', 'α', '".len()), - ("'a', 'α', '".len() + 1, Right, "'a', 'α', '✋".len()), - ("'a', 'α', '✋',".len(), Right, "'a', 'α', '✋',".len()), - ("'a', 'α', '✋', ".len(), Left, "'a', 'α', '✋',".len()), - ( - "'a', 'α', '✋', ".len(), - Right, - "'a', 'α', '✋', ".len(), - ), - ] { + #[gpui::test] + fn test_clip_at_line_ends(cx: &mut gpui::MutableAppContext) { + fn assert(text: &str, cx: &mut gpui::MutableAppContext) { + let (mut unmarked_snapshot, markers) = marked_display_snapshot(text, cx); + unmarked_snapshot.clip_at_line_ends = true; assert_eq!( - map.clip_point(DisplayPoint::new(1, input_column as u32), bias), - DisplayPoint::new(1, output_column as u32), - "clip_point(({}, {}))", - 1, - input_column, + unmarked_snapshot.clip_point(markers[1], Bias::Left), + markers[0] ); } + + assert("||", cx); + assert("|a|", cx); + assert("a|b|", cx); + assert("a|α|", cx); } #[gpui::test] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d16900ebba..1d1910e386 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1071,6 +1071,11 @@ impl Editor { cx.notify(); } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + pub fn set_keymap_context_layer(&mut self, context: gpui::keymap::Context) { self.keymap_context_layers .insert(TypeId::of::(), context); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0daf8f2fc2..18f780dacc 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1292,7 +1292,7 @@ impl PaintState { } } -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub enum CursorShape { Bar, Block, diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 13f66bb24b..cf2d772b16 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -266,13 +266,13 @@ pub fn surrounding_word(map: &DisplaySnapshot, position: DisplayPoint) -> Range< #[cfg(test)] mod tests { use super::*; - use crate::{test::marked_text, Buffer, DisplayMap, MultiBuffer}; + use crate::{test::marked_display_snapshot, Buffer, DisplayMap, MultiBuffer}; use language::Point; #[gpui::test] fn test_previous_word_start(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( previous_word_start(&snapshot, display_points[1]), display_points[0] @@ -298,7 +298,7 @@ mod tests { #[gpui::test] fn test_previous_subword_start(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( previous_subword_start(&snapshot, display_points[1]), display_points[0] @@ -335,7 +335,7 @@ mod tests { cx: &mut gpui::MutableAppContext, is_boundary: impl FnMut(char, char) -> bool, ) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( find_preceding_boundary(&snapshot, display_points[1], is_boundary), display_points[0] @@ -362,7 +362,7 @@ mod tests { #[gpui::test] fn test_next_word_end(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( next_word_end(&snapshot, display_points[0]), display_points[1] @@ -385,7 +385,7 @@ mod tests { #[gpui::test] fn test_next_subword_end(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( next_subword_end(&snapshot, display_points[0]), display_points[1] @@ -421,7 +421,7 @@ mod tests { cx: &mut gpui::MutableAppContext, is_boundary: impl FnMut(char, char) -> bool, ) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( find_boundary(&snapshot, display_points[0], is_boundary), display_points[1] @@ -448,7 +448,7 @@ mod tests { #[gpui::test] fn test_surrounding_word(cx: &mut gpui::MutableAppContext) { fn assert(marked_text: &str, cx: &mut gpui::MutableAppContext) { - let (snapshot, display_points) = marked_snapshot(marked_text, cx); + let (snapshot, display_points) = marked_display_snapshot(marked_text, cx); assert_eq!( surrounding_word(&snapshot, display_points[1]), display_points[0]..display_points[2] @@ -532,31 +532,4 @@ mod tests { (DisplayPoint::new(7, 2), SelectionGoal::Column(2)), ); } - - // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. - fn marked_snapshot( - text: &str, - cx: &mut gpui::MutableAppContext, - ) -> (DisplaySnapshot, Vec) { - let (unmarked_text, markers) = marked_text(text); - - let tab_size = 4; - let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); - let font_id = cx - .font_cache() - .select_font(family_id, &Default::default()) - .unwrap(); - let font_size = 14.0; - - let buffer = MultiBuffer::build_simple(&unmarked_text, cx); - let display_map = cx - .add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); - let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); - let markers = markers - .into_iter() - .map(|offset| offset.to_display_point(&snapshot)) - .collect(); - - (snapshot, markers) - } } diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 566ca9b2db..3c3e22ff3b 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -2,6 +2,11 @@ use std::ops::Range; use collections::HashMap; +use crate::{ + display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, + DisplayPoint, MultiBuffer, +}; + #[cfg(test)] #[ctor::ctor] fn init_logger() { @@ -54,3 +59,30 @@ pub fn marked_text_ranges( .collect(); (unmarked_text, ranges) } + +// Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. +pub fn marked_display_snapshot( + text: &str, + cx: &mut gpui::MutableAppContext, +) -> (DisplaySnapshot, Vec) { + let (unmarked_text, markers) = marked_text(text); + + let tab_size = 4; + let family_id = cx.font_cache().load_family(&["Helvetica"]).unwrap(); + let font_id = cx + .font_cache() + .select_font(family_id, &Default::default()) + .unwrap(); + let font_size = 14.0; + + let buffer = MultiBuffer::build_simple(&unmarked_text, cx); + let display_map = + cx.add_model(|cx| DisplayMap::new(buffer, tab_size, font_id, font_size, None, 1, 1, cx)); + let snapshot = display_map.update(cx, |map, cx| map.snapshot(cx)); + let markers = markers + .into_iter() + .map(|offset| offset.to_display_point(&snapshot)) + .collect(); + + (snapshot, markers) +} diff --git a/crates/vim/src/editor_utils.rs b/crates/vim/src/editor_utils.rs index da9c588888..21dc498436 100644 --- a/crates/vim/src/editor_utils.rs +++ b/crates/vim/src/editor_utils.rs @@ -3,13 +3,13 @@ use gpui::ViewContext; use language::{Selection, SelectionGoal}; pub trait VimEditorExt { - fn adjust_selections(self: &mut Self, cx: &mut ViewContext); - fn adjusted_move_selections( + fn clip_selections(self: &mut Self, cx: &mut ViewContext); + fn clipped_move_selections( self: &mut Self, cx: &mut ViewContext, move_selection: impl Fn(&DisplaySnapshot, &mut Selection), ); - fn adjusted_move_selection_heads( + fn clipped_move_selection_heads( &mut self, cx: &mut ViewContext, update_head: impl Fn( @@ -18,7 +18,7 @@ pub trait VimEditorExt { SelectionGoal, ) -> (DisplayPoint, SelectionGoal), ); - fn adjusted_move_cursors( + fn clipped_move_cursors( self: &mut Self, cx: &mut ViewContext, update_cursor_position: impl Fn( @@ -29,10 +29,7 @@ pub trait VimEditorExt { ); } -pub fn adjust_display_point( - map: &DisplaySnapshot, - mut display_point: DisplayPoint, -) -> DisplayPoint { +pub fn clip_display_point(map: &DisplaySnapshot, mut display_point: DisplayPoint) -> DisplayPoint { let next_char = map.chars_at(display_point).next(); if next_char == Some('\n') || next_char == None { *display_point.column_mut() = display_point.column().saturating_sub(1); @@ -42,31 +39,31 @@ pub fn adjust_display_point( } impl VimEditorExt for Editor { - fn adjust_selections(self: &mut Self, cx: &mut ViewContext) { + fn clip_selections(self: &mut Self, cx: &mut ViewContext) { self.move_selections(cx, |map, selection| { if selection.is_empty() { - let adjusted_cursor = adjust_display_point(map, selection.start); + let adjusted_cursor = clip_display_point(map, selection.start); selection.collapse_to(adjusted_cursor, selection.goal); } else { - let adjusted_head = adjust_display_point(map, selection.head()); + let adjusted_head = clip_display_point(map, selection.head()); selection.set_head(adjusted_head, selection.goal); } }) } - fn adjusted_move_selections( + fn clipped_move_selections( self: &mut Self, cx: &mut ViewContext, move_selection: impl Fn(&DisplaySnapshot, &mut Selection), ) { self.move_selections(cx, |map, selection| { move_selection(map, selection); - let adjusted_head = adjust_display_point(map, selection.head()); + let adjusted_head = clip_display_point(map, selection.head()); selection.set_head(adjusted_head, selection.goal); }) } - fn adjusted_move_selection_heads( + fn clipped_move_selection_heads( &mut self, cx: &mut ViewContext, update_head: impl Fn( @@ -75,14 +72,14 @@ impl VimEditorExt for Editor { SelectionGoal, ) -> (DisplayPoint, SelectionGoal), ) { - self.adjusted_move_selections(cx, |map, selection| { + self.clipped_move_selections(cx, |map, selection| { let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); - let adjusted_head = adjust_display_point(map, new_head); + let adjusted_head = clip_display_point(map, new_head); selection.set_head(adjusted_head, new_goal); }); } - fn adjusted_move_cursors( + fn clipped_move_cursors( self: &mut Self, cx: &mut ViewContext, update_cursor_position: impl Fn( @@ -93,7 +90,7 @@ impl VimEditorExt for Editor { ) { self.move_selections(cx, |map, selection| { let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); - let adjusted_cursor = adjust_display_point(map, cursor); + let adjusted_cursor = clip_display_point(map, cursor); selection.collapse_to(adjusted_cursor, new_goal); }); } diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index b07449401c..d87dcb2445 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -20,7 +20,7 @@ pub fn init(cx: &mut MutableAppContext) { fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { VimState::switch_mode(&SwitchMode(Mode::Normal), cx); VimState::update_active_editor(cx, |editor, cx| { - editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + editor.clipped_move_cursors(cx, |map, mut cursor, _| { *cursor.column_mut() = cursor.column().saturating_sub(1); (map.clip_point(cursor, Bias::Left), SelectionGoal::None) }); diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index e80d5f0bc7..87218b3f5f 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -3,7 +3,7 @@ use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; -use crate::{editor_utils::VimEditorExt, Mode, SwitchMode, VimState}; +use crate::{Mode, SwitchMode, VimState}; action!(InsertBefore); action!(MoveLeft); @@ -29,7 +29,7 @@ pub fn init(cx: &mut MutableAppContext) { fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { VimState::update_active_editor(cx, |editor, cx| { - editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + editor.move_cursors(cx, |map, mut cursor, _| { *cursor.column_mut() = cursor.column().saturating_sub(1); (map.clip_point(cursor, Bias::Left), SelectionGoal::None) }); @@ -38,19 +38,19 @@ fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext) { VimState::update_active_editor(cx, |editor, cx| { - editor.adjusted_move_cursors(cx, movement::down); + editor.move_cursors(cx, movement::down); }); } fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext) { VimState::update_active_editor(cx, |editor, cx| { - editor.adjusted_move_cursors(cx, movement::up); + editor.move_cursors(cx, movement::up); }); } fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) { VimState::update_active_editor(cx, |editor, cx| { - editor.adjusted_move_cursors(cx, |map, mut cursor, _| { + editor.move_cursors(cx, |map, mut cursor, _| { *cursor.column_mut() += 1; (map.clip_point(cursor, Bias::Right), SelectionGoal::None) }); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 76ff216a02..1aec087d95 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -7,7 +7,7 @@ mod normal; mod vim_tests; use collections::HashMap; -use editor::Editor; +use editor::{CursorShape, Editor}; use editor_utils::VimEditorExt; use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; @@ -59,7 +59,7 @@ impl VimState { active_editor.set_keymap_context_layer::(mode.keymap_context_layer()); active_editor.set_input_enabled(*mode == Mode::Insert); if *mode != Mode::Insert { - active_editor.adjust_selections(cx); + active_editor.clip_selections(cx); } }); } @@ -89,6 +89,7 @@ impl VimState { if let Some(editor) = editor.upgrade(cx) { editor.update(cx, |editor, cx| { editor.set_cursor_shape(cursor_shape, cx); + editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); }); } } diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs index f8c6b3d54d..f898809138 100644 --- a/crates/vim/src/vim_tests.rs +++ b/crates/vim/src/vim_tests.rs @@ -1,7 +1,7 @@ use std::ops::Deref; use editor::{display_map::ToDisplayPoint, DisplayPoint}; -use gpui::{json::json, keymap::Keystroke, AppContext, ViewHandle}; +use gpui::{json::json, keymap::Keystroke, ViewHandle}; use language::{Point, Selection}; use workspace::{WorkspaceHandle, WorkspaceParams}; From 1a29180185466e37e17e752d8c2ac7e83138094e Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 25 Mar 2022 20:05:46 -0700 Subject: [PATCH 119/139] Fixed issue with enabling and disabling vim mode dynamically Also added indoc and marked text utility to vim tests to improve readability --- Cargo.lock | 15 +++- crates/editor/src/display_map.rs | 7 +- crates/editor/src/editor.rs | 3 +- crates/editor/src/test.rs | 49 +------------ crates/util/src/test.rs | 51 +++++++++++++- crates/vim/Cargo.toml | 2 + crates/vim/src/editor_events.rs | 8 +-- crates/vim/src/editor_utils.rs | 97 ------------------------- crates/vim/src/insert.rs | 6 +- crates/vim/src/vim.rs | 31 ++++---- crates/vim/src/vim_tests.rs | 117 +++++++++++++++++++++++++++---- 11 files changed, 192 insertions(+), 194 deletions(-) delete mode 100644 crates/vim/src/editor_utils.rs diff --git a/Cargo.lock b/Cargo.lock index 65d690db23..74cfa42bdf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2527,6 +2527,15 @@ dependencies = [ "hashbrown 0.9.1", ] +[[package]] +name = "indoc" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e" +dependencies = [ + "unindent", +] + [[package]] name = "infer" version = "0.2.3" @@ -5553,9 +5562,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "unindent" -version = "0.1.7" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f14ee04d9415b52b3aeab06258a3f07093182b88ba0f9b8d203f211a7a7d41c7" +checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" [[package]] name = "universal-hash" @@ -5680,9 +5689,11 @@ dependencies = [ "collections", "editor", "gpui", + "indoc", "language", "log", "project", + "util", "workspace", ] diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index c3fef6e013..7aa4cdd891 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -498,17 +498,14 @@ impl ToDisplayPoint for Anchor { #[cfg(test)] pub mod tests { use super::*; - use crate::{ - movement, - test::{marked_display_snapshot, marked_text_ranges}, - }; + use crate::{movement, test::marked_display_snapshot}; use gpui::{color::Color, elements::*, test::observe, MutableAppContext}; use language::{Buffer, Language, LanguageConfig, RandomCharIter, SelectionGoal}; use rand::{prelude::*, Rng}; use smol::stream::StreamExt; use std::{env, sync::Arc}; use theme::SyntaxTheme; - use util::test::sample_text; + use util::test::{marked_text_ranges, sample_text}; use Bias::*; #[gpui::test(iterations = 100)] diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 1d1910e386..f45ea0fa2e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6167,7 +6167,6 @@ pub fn styled_runs_for_code_label<'a>( #[cfg(test)] mod tests { - use crate::test::marked_text_by; use super::*; use gpui::{ @@ -6181,7 +6180,7 @@ mod tests { use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; - use util::test::sample_text; + use util::test::{marked_text_by, sample_text}; use workspace::FollowableItem; #[gpui::test] diff --git a/crates/editor/src/test.rs b/crates/editor/src/test.rs index 3c3e22ff3b..e80547c9dd 100644 --- a/crates/editor/src/test.rs +++ b/crates/editor/src/test.rs @@ -1,6 +1,4 @@ -use std::ops::Range; - -use collections::HashMap; +use util::test::marked_text; use crate::{ display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint}, @@ -15,51 +13,6 @@ fn init_logger() { } } -pub fn marked_text_by( - marked_text: &str, - markers: Vec, -) -> (String, HashMap>) { - let mut extracted_markers: HashMap> = Default::default(); - let mut unmarked_text = String::new(); - - for char in marked_text.chars() { - if markers.contains(&char) { - let char_offsets = extracted_markers.entry(char).or_insert(Vec::new()); - char_offsets.push(unmarked_text.len()); - } else { - unmarked_text.push(char); - } - } - - (unmarked_text, extracted_markers) -} - -pub fn marked_text(marked_text: &str) -> (String, Vec) { - let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']); - (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) -} - -pub fn marked_text_ranges( - marked_text: &str, - range_markers: Vec<(char, char)>, -) -> (String, Vec>) { - let mut marker_chars = Vec::new(); - for (start, end) in range_markers.iter() { - marker_chars.push(*start); - marker_chars.push(*end); - } - let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars); - let ranges = range_markers - .iter() - .map(|(start_marker, end_marker)| { - let start = markers.get(start_marker).unwrap()[0]; - let end = markers.get(end_marker).unwrap()[0]; - start..end - }) - .collect(); - (unmarked_text, ranges) -} - // Returns a snapshot from text containing '|' character markers with the markers removed, and DisplayPoints for each one. pub fn marked_display_snapshot( text: &str, diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index 71b847df69..b4cf25274e 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -1,4 +1,8 @@ -use std::path::{Path, PathBuf}; +use std::{ + collections::HashMap, + ops::Range, + path::{Path, PathBuf}, +}; use tempdir::TempDir; pub fn temp_tree(tree: serde_json::Value) -> TempDir { @@ -48,3 +52,48 @@ pub fn sample_text(rows: usize, cols: usize, start_char: char) -> String { } text } + +pub fn marked_text_by( + marked_text: &str, + markers: Vec, +) -> (String, HashMap>) { + let mut extracted_markers: HashMap> = Default::default(); + let mut unmarked_text = String::new(); + + for char in marked_text.chars() { + if markers.contains(&char) { + let char_offsets = extracted_markers.entry(char).or_insert(Vec::new()); + char_offsets.push(unmarked_text.len()); + } else { + unmarked_text.push(char); + } + } + + (unmarked_text, extracted_markers) +} + +pub fn marked_text(marked_text: &str) -> (String, Vec) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['|']); + (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) +} + +pub fn marked_text_ranges( + marked_text: &str, + range_markers: Vec<(char, char)>, +) -> (String, Vec>) { + let mut marker_chars = Vec::new(); + for (start, end) in range_markers.iter() { + marker_chars.push(*start); + marker_chars.push(*end); + } + let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars); + let ranges = range_markers + .iter() + .map(|(start_marker, end_marker)| { + let start = markers.get(start_marker).unwrap()[0]; + let end = markers.get(end_marker).unwrap()[0]; + start..end + }) + .collect(); + (unmarked_text, ranges) +} diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index a223172749..28ee7de872 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -16,8 +16,10 @@ workspace = { path = "../workspace" } log = "0.4" [dev-dependencies] +indoc = "1.0.4" editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } project = { path = "../project", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } workspace = { path = "../workspace", features = ["test-support"] } \ No newline at end of file diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 388de1a2ca..2f8a60ef33 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -13,9 +13,7 @@ pub fn init(cx: &mut MutableAppContext) { fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { cx.update_default_global(|vim_state: &mut VimState, cx| { vim_state.editors.insert(editor.id(), editor.downgrade()); - if vim_state.enabled { - VimState::update_cursor_shapes(cx); - } + VimState::sync_editor_options(cx); }) } @@ -40,9 +38,7 @@ fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppCont } } }); - editor.update(cx, |editor, _| { - editor.remove_keymap_context_layer::(); - }) + VimState::sync_editor_options(cx); } fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { diff --git a/crates/vim/src/editor_utils.rs b/crates/vim/src/editor_utils.rs deleted file mode 100644 index 21dc498436..0000000000 --- a/crates/vim/src/editor_utils.rs +++ /dev/null @@ -1,97 +0,0 @@ -use editor::{display_map::DisplaySnapshot, Bias, DisplayPoint, Editor}; -use gpui::ViewContext; -use language::{Selection, SelectionGoal}; - -pub trait VimEditorExt { - fn clip_selections(self: &mut Self, cx: &mut ViewContext); - fn clipped_move_selections( - self: &mut Self, - cx: &mut ViewContext, - move_selection: impl Fn(&DisplaySnapshot, &mut Selection), - ); - fn clipped_move_selection_heads( - &mut self, - cx: &mut ViewContext, - update_head: impl Fn( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> (DisplayPoint, SelectionGoal), - ); - fn clipped_move_cursors( - self: &mut Self, - cx: &mut ViewContext, - update_cursor_position: impl Fn( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> (DisplayPoint, SelectionGoal), - ); -} - -pub fn clip_display_point(map: &DisplaySnapshot, mut display_point: DisplayPoint) -> DisplayPoint { - let next_char = map.chars_at(display_point).next(); - if next_char == Some('\n') || next_char == None { - *display_point.column_mut() = display_point.column().saturating_sub(1); - display_point = map.clip_point(display_point, Bias::Left); - } - display_point -} - -impl VimEditorExt for Editor { - fn clip_selections(self: &mut Self, cx: &mut ViewContext) { - self.move_selections(cx, |map, selection| { - if selection.is_empty() { - let adjusted_cursor = clip_display_point(map, selection.start); - selection.collapse_to(adjusted_cursor, selection.goal); - } else { - let adjusted_head = clip_display_point(map, selection.head()); - selection.set_head(adjusted_head, selection.goal); - } - }) - } - - fn clipped_move_selections( - self: &mut Self, - cx: &mut ViewContext, - move_selection: impl Fn(&DisplaySnapshot, &mut Selection), - ) { - self.move_selections(cx, |map, selection| { - move_selection(map, selection); - let adjusted_head = clip_display_point(map, selection.head()); - selection.set_head(adjusted_head, selection.goal); - }) - } - - fn clipped_move_selection_heads( - &mut self, - cx: &mut ViewContext, - update_head: impl Fn( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> (DisplayPoint, SelectionGoal), - ) { - self.clipped_move_selections(cx, |map, selection| { - let (new_head, new_goal) = update_head(map, selection.head(), selection.goal); - let adjusted_head = clip_display_point(map, new_head); - selection.set_head(adjusted_head, new_goal); - }); - } - - fn clipped_move_cursors( - self: &mut Self, - cx: &mut ViewContext, - update_cursor_position: impl Fn( - &DisplaySnapshot, - DisplayPoint, - SelectionGoal, - ) -> (DisplayPoint, SelectionGoal), - ) { - self.move_selections(cx, |map, selection| { - let (cursor, new_goal) = update_cursor_position(map, selection.head(), selection.goal); - let adjusted_cursor = clip_display_point(map, cursor); - selection.collapse_to(adjusted_cursor, new_goal); - }); - } -} diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index d87dcb2445..50fd53b37b 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -3,7 +3,7 @@ use gpui::{action, keymap::Binding, MutableAppContext, ViewContext}; use language::SelectionGoal; use workspace::Workspace; -use crate::{editor_utils::VimEditorExt, mode::Mode, SwitchMode, VimState}; +use crate::{mode::Mode, SwitchMode, VimState}; action!(NormalBefore); @@ -18,11 +18,11 @@ pub fn init(cx: &mut MutableAppContext) { } fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { - VimState::switch_mode(&SwitchMode(Mode::Normal), cx); VimState::update_active_editor(cx, |editor, cx| { - editor.clipped_move_cursors(cx, |map, mut cursor, _| { + editor.move_cursors(cx, |map, mut cursor, _| { *cursor.column_mut() = cursor.column().saturating_sub(1); (map.clip_point(cursor, Bias::Left), SelectionGoal::None) }); }); + VimState::switch_mode(&SwitchMode(Mode::Normal), cx); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 1aec087d95..68ecd4e003 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1,5 +1,4 @@ mod editor_events; -mod editor_utils; mod insert; mod mode; mod normal; @@ -8,7 +7,6 @@ mod vim_tests; use collections::HashMap; use editor::{CursorShape, Editor}; -use editor_utils::VimEditorExt; use gpui::{action, MutableAppContext, ViewContext, WeakViewHandle}; use mode::Mode; @@ -49,21 +47,11 @@ impl VimState { } fn switch_mode(SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) { - let active_editor = cx.update_default_global(|this: &mut Self, _| { + cx.update_default_global(|this: &mut Self, _| { this.mode = *mode; - this.active_editor.clone() }); - if let Some(active_editor) = active_editor.and_then(|e| e.upgrade(cx)) { - active_editor.update(cx, |active_editor, cx| { - active_editor.set_keymap_context_layer::(mode.keymap_context_layer()); - active_editor.set_input_enabled(*mode == Mode::Insert); - if *mode != Mode::Insert { - active_editor.clip_selections(cx); - } - }); - } - VimState::update_cursor_shapes(cx); + VimState::sync_editor_options(cx); } fn settings_changed(cx: &mut MutableAppContext) { @@ -76,20 +64,29 @@ impl VimState { } else { Mode::Insert }; - Self::update_cursor_shapes(cx); + Self::sync_editor_options(cx); } }); } - fn update_cursor_shapes(cx: &mut MutableAppContext) { + fn sync_editor_options(cx: &mut MutableAppContext) { cx.defer(move |cx| { cx.update_default_global(|this: &mut VimState, cx| { - let cursor_shape = this.mode.cursor_shape(); + let mode = this.mode; + let cursor_shape = mode.cursor_shape(); + let keymap_layer_active = this.enabled; for editor in this.editors.values() { if let Some(editor) = editor.upgrade(cx) { editor.update(cx, |editor, cx| { editor.set_cursor_shape(cursor_shape, cx); editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); + editor.set_input_enabled(mode == Mode::Insert); + if keymap_layer_active { + let context_layer = mode.keymap_context_layer(); + editor.set_keymap_context_layer::(context_layer); + } else { + editor.remove_keymap_context_layer::(); + } }); } } diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs index f898809138..e35ed9a122 100644 --- a/crates/vim/src/vim_tests.rs +++ b/crates/vim/src/vim_tests.rs @@ -1,8 +1,10 @@ +use indoc::indoc; use std::ops::Deref; use editor::{display_map::ToDisplayPoint, DisplayPoint}; use gpui::{json::json, keymap::Keystroke, ViewHandle}; use language::{Point, Selection}; +use util::test::marked_text; use workspace::{WorkspaceHandle, WorkspaceParams}; use crate::*; @@ -10,46 +12,98 @@ use crate::*; #[gpui::test] async fn test_insert_mode(cx: &mut gpui::TestAppContext) { let mut cx = VimTestAppContext::new(cx, "").await; - assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystroke("i"); assert_eq!(cx.mode(), Mode::Insert); cx.simulate_keystrokes(&["T", "e", "s", "t"]); - assert_eq!(cx.editor_text(), "Test".to_owned()); + cx.assert_newest_selection_head("Test|"); cx.simulate_keystroke("escape"); assert_eq!(cx.mode(), Mode::Normal); + cx.assert_newest_selection_head("Tes|t"); } #[gpui::test] async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { let mut cx = VimTestAppContext::new(cx, "Test\nTestTest\nTest").await; - assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystroke("l"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 1)); + cx.assert_newest_selection_head(indoc! {" + T|est + TestTest + Test"}); cx.simulate_keystroke("h"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 0)); + cx.assert_newest_selection_head(indoc! {" + |Test + TestTest + Test"}); cx.simulate_keystroke("j"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); cx.simulate_keystroke("k"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 0)); - + cx.assert_newest_selection_head(indoc! {" + |Test + TestTest + Test"}); cx.simulate_keystroke("j"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); // When moving left, cursor does not wrap to the previous line cx.simulate_keystroke("h"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 0)); + cx.assert_newest_selection_head(indoc! {" + Test + |TestTest + Test"}); // When moving right, cursor does not reach the line end or wrap to the next line for _ in 0..9 { cx.simulate_keystroke("l"); } - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 7)); + cx.assert_newest_selection_head(indoc! {" + Test + TestTes|t + Test"}); // Goal column respects the inability to reach the end of the line cx.simulate_keystroke("k"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(0, 3)); + cx.assert_newest_selection_head(indoc! {" + Tes|t + TestTest + Test"}); cx.simulate_keystroke("j"); - assert_eq!(cx.newest_selection().head(), DisplayPoint::new(1, 7)); + cx.assert_newest_selection_head(indoc! {" + Test + TestTes|t + Test"}); +} + +#[gpui::test] +async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, "").await; + + // Editor acts as though vim is disabled + cx.disable_vim(); + assert_eq!(cx.mode(), Mode::Insert); + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_newest_selection_head("hjkl|"); + + // Enabling dynamically sets vim mode again + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::Normal); + cx.simulate_keystrokes(&["h", "h", "h", "l"]); + assert_eq!(cx.editor_text(), "hjkl".to_owned()); + cx.assert_newest_selection_head("hj|kl"); + cx.simulate_keystrokes(&["i", "T", "e", "s", "t"]); + cx.assert_newest_selection_head("hjTest|kl"); + + // Disabling and enabling resets to normal mode + assert_eq!(cx.mode(), Mode::Insert); + cx.disable_vim(); + assert_eq!(cx.mode(), Mode::Insert); + cx.enable_vim(); + assert_eq!(cx.mode(), Mode::Normal); } struct VimTestAppContext<'a> { @@ -68,6 +122,13 @@ impl<'a> VimTestAppContext<'a> { crate::init(cx); }); let params = cx.update(WorkspaceParams::test); + + cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = true; + }); + }); + params .fs .as_fake() @@ -107,6 +168,22 @@ impl<'a> VimTestAppContext<'a> { } } + fn enable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = true; + }); + }) + } + + fn disable_vim(&mut self) { + self.cx.update(|cx| { + cx.update_global(|settings: &mut Settings, _| { + settings.vim_mode = false; + }); + }) + } + fn newest_selection(&mut self) -> Selection { self.editor.update(self.cx, |editor, cx| { let snapshot = editor.snapshot(cx); @@ -141,6 +218,20 @@ impl<'a> VimTestAppContext<'a> { self.simulate_keystroke(keystroke_text); } } + + fn assert_newest_selection_head(&mut self, text: &str) { + let (unmarked_text, markers) = marked_text(&text); + assert_eq!( + self.editor_text(), + unmarked_text, + "Unmarked text doesn't match editor text" + ); + let newest_selection = self.newest_selection(); + let expected_head = self.editor.update(self.cx, |editor, cx| { + markers[0].to_display_point(&editor.snapshot(cx)) + }); + assert_eq!(newest_selection.head(), expected_head) + } } impl<'a> Deref for VimTestAppContext<'a> { From 2dc76a2b58b0b48442bfa0373c3bdd677d473fbb Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Mar 2022 07:10:16 -0600 Subject: [PATCH 120/139] Add bindings for subword movement and deletion --- crates/editor/src/editor.rs | 154 ++++++++++++++++++++++++++++++------ 1 file changed, 128 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d841024f88..f8dc1acbf2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -71,8 +71,10 @@ action!(Newline); action!(Tab); action!(Outdent); action!(DeleteLine); -action!(DeleteToPreviousWordBoundary); -action!(DeleteToNextWordBoundary); +action!(DeleteToPreviousWordStart); +action!(DeleteToPreviousSubwordStart); +action!(DeleteToNextWordEnd); +action!(DeleteToNextSubwordEnd); action!(DeleteToBeginningOfLine); action!(DeleteToEndOfLine); action!(CutToEndOfLine); @@ -89,7 +91,9 @@ action!(MoveDown); action!(MoveLeft); action!(MoveRight); action!(MoveToPreviousWordStart); +action!(MoveToPreviousSubwordStart); action!(MoveToNextWordEnd); +action!(MoveToNextSubwordEnd); action!(MoveToBeginningOfLine); action!(MoveToEndOfLine); action!(MoveToBeginning); @@ -98,8 +102,10 @@ action!(SelectUp); action!(SelectDown); action!(SelectLeft); action!(SelectRight); -action!(SelectToPreviousWordBoundary); -action!(SelectToNextWordBoundary); +action!(SelectToPreviousWordStart); +action!(SelectToPreviousSubwordStart); +action!(SelectToNextWordEnd); +action!(SelectToNextSubwordEnd); action!(SelectToBeginningOfLine, bool); action!(SelectToEndOfLine, bool); action!(SelectToBeginning); @@ -173,14 +179,18 @@ pub fn init(cx: &mut MutableAppContext) { ), Binding::new("shift-tab", Outdent, Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), + Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")), + Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")), Binding::new( - "alt-backspace", - DeleteToPreviousWordBoundary, + "ctrl-alt-backspace", + DeleteToPreviousSubwordStart, Some("Editor"), ), - Binding::new("alt-h", DeleteToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-delete", DeleteToNextWordBoundary, Some("Editor")), - Binding::new("alt-d", DeleteToNextWordBoundary, Some("Editor")), + Binding::new("ctrl-alt-h", DeleteToPreviousSubwordStart, Some("Editor")), + Binding::new("alt-delete", DeleteToNextWordEnd, Some("Editor")), + Binding::new("alt-d", DeleteToNextWordEnd, Some("Editor")), + Binding::new("ctrl-alt-delete", DeleteToNextSubwordEnd, Some("Editor")), + Binding::new("ctrl-alt-d", DeleteToNextSubwordEnd, Some("Editor")), Binding::new("cmd-backspace", DeleteToBeginningOfLine, Some("Editor")), Binding::new("cmd-delete", DeleteToEndOfLine, Some("Editor")), Binding::new("ctrl-k", CutToEndOfLine, Some("Editor")), @@ -202,8 +212,12 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-f", MoveRight, Some("Editor")), Binding::new("alt-left", MoveToPreviousWordStart, Some("Editor")), Binding::new("alt-b", MoveToPreviousWordStart, Some("Editor")), + Binding::new("ctrl-alt-left", MoveToPreviousSubwordStart, Some("Editor")), + Binding::new("ctrl-alt-b", MoveToPreviousSubwordStart, Some("Editor")), Binding::new("alt-right", MoveToNextWordEnd, Some("Editor")), Binding::new("alt-f", MoveToNextWordEnd, Some("Editor")), + Binding::new("ctrl-alt-right", MoveToNextSubwordEnd, Some("Editor")), + Binding::new("ctrl-alt-f", MoveToNextSubwordEnd, Some("Editor")), Binding::new("cmd-left", MoveToBeginningOfLine, Some("Editor")), Binding::new("ctrl-a", MoveToBeginningOfLine, Some("Editor")), Binding::new("cmd-right", MoveToEndOfLine, Some("Editor")), @@ -218,19 +232,31 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-shift-B", SelectLeft, Some("Editor")), Binding::new("shift-right", SelectRight, Some("Editor")), Binding::new("ctrl-shift-F", SelectRight, Some("Editor")), + Binding::new("alt-shift-left", SelectToPreviousWordStart, Some("Editor")), + Binding::new("alt-shift-B", SelectToPreviousWordStart, Some("Editor")), Binding::new( - "alt-shift-left", - SelectToPreviousWordBoundary, + "ctrl-alt-shift-left", + SelectToPreviousSubwordStart, Some("Editor"), ), - Binding::new("alt-shift-B", SelectToPreviousWordBoundary, Some("Editor")), - Binding::new("alt-shift-right", SelectToNextWordBoundary, Some("Editor")), - Binding::new("alt-shift-F", SelectToNextWordBoundary, Some("Editor")), + Binding::new( + "ctrl-alt-shift-B", + SelectToPreviousSubwordStart, + Some("Editor"), + ), + Binding::new("alt-shift-right", SelectToNextWordEnd, Some("Editor")), + Binding::new("alt-shift-F", SelectToNextWordEnd, Some("Editor")), Binding::new( "cmd-shift-left", SelectToBeginningOfLine(true), Some("Editor"), ), + Binding::new( + "ctrl-alt-shift-right", + SelectToNextSubwordEnd, + Some("Editor"), + ), + Binding::new("ctrl-alt-shift-F", SelectToNextSubwordEnd, Some("Editor")), Binding::new( "ctrl-shift-A", SelectToBeginningOfLine(true), @@ -282,7 +308,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); cx.add_action(Editor::delete_to_previous_word_start); + cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); + cx.add_action(Editor::delete_to_next_subword_end); cx.add_action(Editor::delete_to_beginning_of_line); cx.add_action(Editor::delete_to_end_of_line); cx.add_action(Editor::cut_to_end_of_line); @@ -299,7 +327,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::move_left); cx.add_action(Editor::move_right); cx.add_action(Editor::move_to_previous_word_start); + cx.add_action(Editor::move_to_previous_subword_start); cx.add_action(Editor::move_to_next_word_end); + cx.add_action(Editor::move_to_next_subword_end); cx.add_action(Editor::move_to_beginning_of_line); cx.add_action(Editor::move_to_end_of_line); cx.add_action(Editor::move_to_beginning); @@ -309,7 +339,9 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select_left); cx.add_action(Editor::select_right); cx.add_action(Editor::select_to_previous_word_start); + cx.add_action(Editor::select_to_previous_subword_start); cx.add_action(Editor::select_to_next_word_end); + cx.add_action(Editor::select_to_next_subword_end); cx.add_action(Editor::select_to_beginning_of_line); cx.add_action(Editor::select_to_end_of_line); cx.add_action(Editor::select_to_beginning); @@ -3501,9 +3533,22 @@ impl Editor { }); } + pub fn move_to_previous_subword_start( + &mut self, + _: &MoveToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.move_cursors(cx, |map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + } + pub fn select_to_previous_word_start( &mut self, - _: &SelectToPreviousWordBoundary, + _: &SelectToPreviousWordStart, cx: &mut ViewContext, ) { self.move_selection_heads(cx, |map, head, _| { @@ -3514,9 +3559,22 @@ impl Editor { }); } + pub fn select_to_previous_subword_start( + &mut self, + _: &SelectToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.move_selection_heads(cx, |map, head, _| { + ( + movement::previous_subword_start(map, head), + SelectionGoal::None, + ) + }); + } + pub fn delete_to_previous_word_start( &mut self, - _: &DeleteToPreviousWordBoundary, + _: &DeleteToPreviousWordStart, cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { @@ -3530,31 +3588,75 @@ impl Editor { }); } + pub fn delete_to_previous_subword_start( + &mut self, + _: &DeleteToPreviousSubwordStart, + cx: &mut ViewContext, + ) { + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::previous_subword_start(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); + } + pub fn move_to_next_word_end(&mut self, _: &MoveToNextWordEnd, cx: &mut ViewContext) { self.move_cursors(cx, |map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); } - pub fn select_to_next_word_end( + pub fn move_to_next_subword_end( &mut self, - _: &SelectToNextWordBoundary, + _: &MoveToNextSubwordEnd, cx: &mut ViewContext, ) { + self.move_cursors(cx, |map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + } + + pub fn select_to_next_word_end(&mut self, _: &SelectToNextWordEnd, cx: &mut ViewContext) { self.move_selection_heads(cx, |map, head, _| { (movement::next_word_end(map, head), SelectionGoal::None) }); } - pub fn delete_to_next_word_end( + pub fn select_to_next_subword_end( &mut self, - _: &DeleteToNextWordBoundary, + _: &SelectToNextSubwordEnd, + cx: &mut ViewContext, + ) { + self.move_selection_heads(cx, |map, head, _| { + (movement::next_subword_end(map, head), SelectionGoal::None) + }); + } + + pub fn delete_to_next_word_end(&mut self, _: &DeleteToNextWordEnd, cx: &mut ViewContext) { + self.transact(cx, |this, cx| { + this.move_selections(cx, |map, selection| { + if selection.is_empty() { + let cursor = movement::next_word_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); + } + + pub fn delete_to_next_subword_end( + &mut self, + _: &DeleteToNextSubwordEnd, cx: &mut ViewContext, ) { self.transact(cx, |this, cx| { this.move_selections(cx, |map, selection| { if selection.is_empty() { - let cursor = movement::next_word_end(map, selection.head()); + let cursor = movement::next_subword_end(map, selection.head()); selection.set_head(cursor, SelectionGoal::None); } }); @@ -7055,7 +7157,7 @@ mod tests { ); view.move_right(&MoveRight, cx); - view.select_to_previous_word_start(&SelectToPreviousWordBoundary, cx); + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); assert_selection_ranges( "use std::>s'), ('[', ']')], @@ -7063,7 +7165,7 @@ mod tests { cx, ); - view.select_to_previous_word_start(&SelectToPreviousWordBoundary, cx); + view.select_to_previous_word_start(&SelectToPreviousWordStart, cx); assert_selection_ranges( "use std>::s'), ('[', ']')], @@ -7071,7 +7173,7 @@ mod tests { cx, ); - view.select_to_next_word_end(&SelectToNextWordBoundary, cx); + view.select_to_next_word_end(&SelectToNextWordEnd, cx); assert_selection_ranges( "use std::>s'), ('[', ']')], @@ -7150,7 +7252,7 @@ mod tests { ], cx, ); - view.delete_to_previous_word_start(&DeleteToPreviousWordBoundary, cx); + view.delete_to_previous_word_start(&DeleteToPreviousWordStart, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e two te four"); @@ -7165,7 +7267,7 @@ mod tests { ], cx, ); - view.delete_to_next_word_end(&DeleteToNextWordBoundary, cx); + view.delete_to_next_word_end(&DeleteToNextWordEnd, cx); }); assert_eq!(buffer.read(cx).read(cx).text(), "e t te our"); From 28371250987221e36ef5737a9cdb9f5e9a67697d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Mar 2022 07:22:01 -0600 Subject: [PATCH 121/139] 0.23.0 --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74cfa42bdf..38ffa70955 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5956,7 +5956,7 @@ checksum = "ec7a2a501ed189703dba8b08142f057e887dfc4b2cc4db2d343ac6376ba3e0b9" [[package]] name = "zed" -version = "0.22.0" +version = "0.23.0" dependencies = [ "anyhow", "async-compression", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index e541dff42d..fc9946b778 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.22.0" +version = "0.23.0" [lib] name = "zed" From 30e31f6561bd49f634d1aa38584dc1be425a3af5 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Mar 2022 19:18:48 -0600 Subject: [PATCH 122/139] Test that vim mode can be disabled on startup --- crates/vim/src/vim_tests.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/vim/src/vim_tests.rs b/crates/vim/src/vim_tests.rs index e35ed9a122..051ff21ce7 100644 --- a/crates/vim/src/vim_tests.rs +++ b/crates/vim/src/vim_tests.rs @@ -11,7 +11,7 @@ use crate::*; #[gpui::test] async fn test_insert_mode(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, "").await; + let mut cx = VimTestAppContext::new(cx, true, "").await; cx.simulate_keystroke("i"); assert_eq!(cx.mode(), Mode::Insert); cx.simulate_keystrokes(&["T", "e", "s", "t"]); @@ -23,7 +23,7 @@ async fn test_insert_mode(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, "Test\nTestTest\nTest").await; + let mut cx = VimTestAppContext::new(cx, true, "Test\nTestTest\nTest").await; cx.simulate_keystroke("l"); cx.assert_newest_selection_head(indoc! {" T|est @@ -81,15 +81,17 @@ async fn test_normal_hjkl(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestAppContext::new(cx, "").await; + let mut cx = VimTestAppContext::new(cx, true, "").await; + + cx.simulate_keystroke("i"); + assert_eq!(cx.mode(), Mode::Insert); // Editor acts as though vim is disabled cx.disable_vim(); - assert_eq!(cx.mode(), Mode::Insert); cx.simulate_keystrokes(&["h", "j", "k", "l"]); cx.assert_newest_selection_head("hjkl|"); - // Enabling dynamically sets vim mode again + // Enabling dynamically sets vim mode again and restores normal mode cx.enable_vim(); assert_eq!(cx.mode(), Mode::Normal); cx.simulate_keystrokes(&["h", "h", "h", "l"]); @@ -106,6 +108,13 @@ async fn test_toggle_through_settings(cx: &mut gpui::TestAppContext) { assert_eq!(cx.mode(), Mode::Normal); } +#[gpui::test] +async fn test_initially_disabled(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestAppContext::new(cx, false, "").await; + cx.simulate_keystrokes(&["h", "j", "k", "l"]); + cx.assert_newest_selection_head("hjkl|"); +} + struct VimTestAppContext<'a> { cx: &'a mut gpui::TestAppContext, window_id: usize, @@ -115,6 +124,7 @@ struct VimTestAppContext<'a> { impl<'a> VimTestAppContext<'a> { async fn new( cx: &'a mut gpui::TestAppContext, + enabled: bool, initial_editor_text: &str, ) -> VimTestAppContext<'a> { cx.update(|cx| { @@ -125,7 +135,7 @@ impl<'a> VimTestAppContext<'a> { cx.update(|cx| { cx.update_global(|settings: &mut Settings, _| { - settings.vim_mode = true; + settings.vim_mode = enabled; }); }); From daf999c3bec39718e4986373410b41192aad20c6 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Mar 2022 14:30:55 -0600 Subject: [PATCH 123/139] Fully disable vim mode on start unless it's enabled Also: Make some structural adjustments to remove the need for defer. Instead of accessing the global in associated VimState functions, have a single method that allows us to call update instance methods. --- crates/gpui/src/app.rs | 26 ++++++---- crates/vim/src/editor_events.rs | 18 +++---- crates/vim/src/insert.rs | 14 ++--- crates/vim/src/normal.rs | 34 ++++++++----- crates/vim/src/vim.rs | 90 ++++++++++++++++----------------- 5 files changed, 99 insertions(+), 83 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index a291ad3c4b..7f0c3ffcde 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -782,7 +782,7 @@ type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; -type GlobalObservationCallback = Box; +type GlobalObservationCallback = Box; type ReleaseObservationCallback = Box; pub struct MutableAppContext { @@ -1222,10 +1222,10 @@ impl MutableAppContext { } } - pub fn observe_global(&mut self, observe: F) -> Subscription + pub fn observe_global(&mut self, mut observe: F) -> Subscription where G: Any, - F: 'static + FnMut(&mut MutableAppContext), + F: 'static + FnMut(&G, &mut MutableAppContext), { let type_id = TypeId::of::(); let id = post_inc(&mut self.next_subscription_id); @@ -1234,7 +1234,14 @@ impl MutableAppContext { .lock() .entry(type_id) .or_default() - .insert(id, Some(Box::new(observe))); + .insert( + id, + Some( + Box::new(move |global: &dyn Any, cx: &mut MutableAppContext| { + observe(global.downcast_ref().unwrap(), cx) + }) as GlobalObservationCallback, + ), + ); Subscription::GlobalObservation { id, @@ -2075,10 +2082,10 @@ impl MutableAppContext { fn notify_global_observers(&mut self, observed_type_id: TypeId) { let callbacks = self.global_observations.lock().remove(&observed_type_id); if let Some(callbacks) = callbacks { - if self.cx.globals.contains_key(&observed_type_id) { + if let Some(global) = self.cx.globals.remove(&observed_type_id) { for (id, callback) in callbacks { if let Some(mut callback) = callback { - callback(self); + callback(global.as_ref(), self); match self .global_observations .lock() @@ -2095,6 +2102,7 @@ impl MutableAppContext { } } } + self.cx.globals.insert(observed_type_id, global); } } } @@ -5232,7 +5240,7 @@ mod tests { let observation_count = Rc::new(RefCell::new(0)); let subscription = cx.observe_global::({ let observation_count = observation_count.clone(); - move |_| { + move |_, _| { *observation_count.borrow_mut() += 1; } }); @@ -5262,7 +5270,7 @@ mod tests { let observation_count = Rc::new(RefCell::new(0)); cx.observe_global::({ let observation_count = observation_count.clone(); - move |_| { + move |_, _| { *observation_count.borrow_mut() += 1; } }) @@ -5636,7 +5644,7 @@ mod tests { *subscription.borrow_mut() = Some(cx.observe_global::<(), _>({ let observation_count = observation_count.clone(); let subscription = subscription.clone(); - move |_| { + move |_, _| { subscription.borrow_mut().take(); *observation_count.borrow_mut() += 1; } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 2f8a60ef33..7e49b473f9 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -13,7 +13,7 @@ pub fn init(cx: &mut MutableAppContext) { fn editor_created(EditorCreated(editor): &EditorCreated, cx: &mut MutableAppContext) { cx.update_default_global(|vim_state: &mut VimState, cx| { vim_state.editors.insert(editor.id(), editor.downgrade()); - VimState::sync_editor_options(cx); + vim_state.sync_editor_options(cx); }) } @@ -24,21 +24,21 @@ fn editor_focused(EditorFocused(editor): &EditorFocused, cx: &mut MutableAppCont Mode::Normal }; - cx.update_default_global(|vim_state: &mut VimState, _| { - vim_state.active_editor = Some(editor.downgrade()); + VimState::update_global(cx, |state, cx| { + state.active_editor = Some(editor.downgrade()); + state.switch_mode(&SwitchMode(mode), cx); }); - VimState::switch_mode(&SwitchMode(mode), cx); } fn editor_blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut MutableAppContext) { - cx.update_default_global(|vim_state: &mut VimState, _| { - if let Some(previous_editor) = vim_state.active_editor.clone() { + VimState::update_global(cx, |state, cx| { + if let Some(previous_editor) = state.active_editor.clone() { if previous_editor == editor.clone() { - vim_state.active_editor = None; + state.active_editor = None; } } - }); - VimState::sync_editor_options(cx); + state.sync_editor_options(cx); + }) } fn editor_released(EditorReleased(editor): &EditorReleased, cx: &mut MutableAppContext) { diff --git a/crates/vim/src/insert.rs b/crates/vim/src/insert.rs index 50fd53b37b..c027ff2c1f 100644 --- a/crates/vim/src/insert.rs +++ b/crates/vim/src/insert.rs @@ -18,11 +18,13 @@ pub fn init(cx: &mut MutableAppContext) { } fn normal_before(_: &mut Workspace, _: &NormalBefore, cx: &mut ViewContext) { - VimState::update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, |map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); }); - }); - VimState::switch_mode(&SwitchMode(Mode::Normal), cx); + state.switch_mode(&SwitchMode(Mode::Normal), cx); + }) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 87218b3f5f..232a76a030 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -28,31 +28,39 @@ pub fn init(cx: &mut MutableAppContext) { } fn move_left(_: &mut Workspace, _: &MoveLeft, cx: &mut ViewContext) { - VimState::update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, |map, mut cursor, _| { - *cursor.column_mut() = cursor.column().saturating_sub(1); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() = cursor.column().saturating_sub(1); + (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + }); }); - }); + }) } fn move_down(_: &mut Workspace, _: &MoveDown, cx: &mut ViewContext) { - VimState::update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, movement::down); + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, movement::down); + }); }); } fn move_up(_: &mut Workspace, _: &MoveUp, cx: &mut ViewContext) { - VimState::update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, movement::up); + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, movement::up); + }); }); } fn move_right(_: &mut Workspace, _: &MoveRight, cx: &mut ViewContext) { - VimState::update_active_editor(cx, |editor, cx| { - editor.move_cursors(cx, |map, mut cursor, _| { - *cursor.column_mut() += 1; - (map.clip_point(cursor, Bias::Right), SelectionGoal::None) + VimState::update_global(cx, |state, cx| { + state.update_active_editor(cx, |editor, cx| { + editor.move_cursors(cx, |map, mut cursor, _| { + *cursor.column_mut() += 1; + (map.clip_point(cursor, Bias::Right), SelectionGoal::None) + }); }); }); } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 68ecd4e003..c6905a9c7a 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -19,10 +19,14 @@ pub fn init(cx: &mut MutableAppContext) { insert::init(cx); normal::init(cx); - cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| VimState::switch_mode(action, cx)); + cx.add_action(|_: &mut Workspace, action: &SwitchMode, cx| { + VimState::update_global(cx, |state, cx| state.switch_mode(action, cx)) + }); - cx.observe_global::(VimState::settings_changed) - .detach(); + cx.observe_global::(|settings, cx| { + VimState::update_global(cx, |state, cx| state.set_enabled(settings.vim_mode, cx)) + }) + .detach(); } #[derive(Default)] @@ -35,62 +39,56 @@ pub struct VimState { } impl VimState { + fn update_global(cx: &mut MutableAppContext, update: F) -> S + where + F: FnOnce(&mut Self, &mut MutableAppContext) -> S, + { + cx.update_default_global(update) + } + fn update_active_editor( + &self, cx: &mut MutableAppContext, update: impl FnOnce(&mut Editor, &mut ViewContext) -> S, ) -> Option { - cx.global::() - .active_editor + self.active_editor .clone() .and_then(|ae| ae.upgrade(cx)) .map(|ae| ae.update(cx, update)) } - fn switch_mode(SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) { - cx.update_default_global(|this: &mut Self, _| { - this.mode = *mode; - }); - - VimState::sync_editor_options(cx); + fn switch_mode(&mut self, SwitchMode(mode): &SwitchMode, cx: &mut MutableAppContext) { + self.mode = *mode; + self.sync_editor_options(cx); } - fn settings_changed(cx: &mut MutableAppContext) { - cx.update_default_global(|this: &mut Self, cx| { - let settings = cx.global::(); - if this.enabled != settings.vim_mode { - this.enabled = settings.vim_mode; - this.mode = if settings.vim_mode { - Mode::Normal - } else { - Mode::Insert - }; - Self::sync_editor_options(cx); - } - }); + fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) { + if self.enabled != enabled { + self.enabled = enabled; + self.sync_editor_options(cx); + } } - fn sync_editor_options(cx: &mut MutableAppContext) { - cx.defer(move |cx| { - cx.update_default_global(|this: &mut VimState, cx| { - let mode = this.mode; - let cursor_shape = mode.cursor_shape(); - let keymap_layer_active = this.enabled; - for editor in this.editors.values() { - if let Some(editor) = editor.upgrade(cx) { - editor.update(cx, |editor, cx| { - editor.set_cursor_shape(cursor_shape, cx); - editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); - editor.set_input_enabled(mode == Mode::Insert); - if keymap_layer_active { - let context_layer = mode.keymap_context_layer(); - editor.set_keymap_context_layer::(context_layer); - } else { - editor.remove_keymap_context_layer::(); - } - }); + fn sync_editor_options(&self, cx: &mut MutableAppContext) { + let mode = self.mode; + let cursor_shape = mode.cursor_shape(); + for editor in self.editors.values() { + if let Some(editor) = editor.upgrade(cx) { + editor.update(cx, |editor, cx| { + if self.enabled { + editor.set_cursor_shape(cursor_shape, cx); + editor.set_clip_at_line_ends(cursor_shape == CursorShape::Block, cx); + editor.set_input_enabled(mode == Mode::Insert); + let context_layer = mode.keymap_context_layer(); + editor.set_keymap_context_layer::(context_layer); + } else { + editor.set_cursor_shape(CursorShape::Bar, cx); + editor.set_clip_at_line_ends(false, cx); + editor.set_input_enabled(true); + editor.remove_keymap_context_layer::(); } - } - }); - }); + }); + } + } } } From c6ad667d492ce8a17ed7580e8abcb6963b37262a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 26 Mar 2022 18:38:00 -0600 Subject: [PATCH 124/139] Assign normal mode when re-enabling --- crates/vim/src/vim.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index c6905a9c7a..26f7e24cf2 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -65,6 +65,9 @@ impl VimState { fn set_enabled(&mut self, enabled: bool, cx: &mut MutableAppContext) { if self.enabled != enabled { self.enabled = enabled; + if enabled { + self.mode = Mode::Normal; + } self.sync_editor_options(cx); } } From cbd266052dee2455ef1252a1a2f1eeafdf664e2d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 10:44:32 +0200 Subject: [PATCH 125/139] Allow returning futures in fake language server request handlers --- crates/editor/src/editor.rs | 80 +++++---- crates/lsp/src/lsp.rs | 55 +++--- crates/project/src/project.rs | 6 +- crates/server/src/rpc.rs | 306 +++++++++++++++++++--------------- 4 files changed, 250 insertions(+), 197 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9327ee251..b269ce3cd5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8850,31 +8850,34 @@ mod tests { position: Point, completions: Vec<(Range, &'static str)>, ) { - fake.handle_request::(move |params, _| { - assert_eq!( - params.text_document_position.text_document.uri, - lsp::Url::from_file_path(path).unwrap() - ); - assert_eq!( - params.text_document_position.position, - lsp::Position::new(position.row, position.column) - ); - Some(lsp::CompletionResponse::Array( - completions - .iter() - .map(|(range, new_text)| lsp::CompletionItem { - label: new_text.to_string(), - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.start.row, range.start.column), - ), - new_text: new_text.to_string(), - })), - ..Default::default() - }) - .collect(), - )) + fake.handle_request::(move |params, _| { + let completions = completions.clone(); + async move { + assert_eq!( + params.text_document_position.text_document.uri, + lsp::Url::from_file_path(path).unwrap() + ); + assert_eq!( + params.text_document_position.position, + lsp::Position::new(position.row, position.column) + ); + Some(lsp::CompletionResponse::Array( + completions + .iter() + .map(|(range, new_text)| lsp::CompletionItem { + label: new_text.to_string(), + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.start.row, range.start.column), + ), + new_text: new_text.to_string(), + })), + ..Default::default() + }) + .collect(), + )) + } }) .next() .await; @@ -8884,18 +8887,21 @@ mod tests { fake: &mut FakeLanguageServer, edit: Option<(Range, &'static str)>, ) { - fake.handle_request::(move |_, _| { - lsp::CompletionItem { - additional_text_edits: edit.clone().map(|(range, new_text)| { - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(range.start.row, range.start.column), - lsp::Position::new(range.end.row, range.end.column), - ), - new_text.to_string(), - )] - }), - ..Default::default() + fake.handle_request::(move |_, _| { + let edit = edit.clone(); + async move { + lsp::CompletionItem { + additional_text_edits: edit.map(|(range, new_text)| { + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(range.start.row, range.start.column), + lsp::Position::new(range.end.row, range.end.column), + ), + new_text.to_string(), + )] + }), + ..Default::default() + } } }) .next() diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index a8ae9e4cd7..6e729fc62b 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,6 +1,8 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; +use futures::{ + channel::oneshot, future::BoxFuture, io::BufWriter, AsyncRead, AsyncWrite, FutureExt, +}; use gpui::{executor, Task}; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; @@ -556,7 +558,10 @@ type FakeLanguageServerHandlers = Arc< Mutex< HashMap< &'static str, - Box Vec>, + Box< + dyn Send + + FnMut(usize, &[u8], gpui::AsyncAppContext) -> BoxFuture<'static, Vec>, + >, >, >, >; @@ -585,11 +590,16 @@ impl LanguageServer { let (stdout_writer, stdout_reader) = async_pipe::pipe(); let mut fake = FakeLanguageServer::new(stdin_reader, stdout_writer, cx); - fake.handle_request::({ + fake.handle_request::({ let capabilities = capabilities.clone(); - move |_, _| InitializeResult { - capabilities: capabilities.clone(), - ..Default::default() + move |_, _| { + let capabilities = capabilities.clone(); + async move { + InitializeResult { + capabilities, + ..Default::default() + } + } } }); @@ -628,7 +638,8 @@ impl FakeLanguageServer { let response; if let Some(handler) = handlers.lock().get_mut(request.method) { response = - handler(request.id, request.params.get().as_bytes(), cx.clone()); + handler(request.id, request.params.get().as_bytes(), cx.clone()) + .await; log::debug!("handled lsp request. method:{}", request.method); } else { response = serde_json::to_vec(&AnyResponse { @@ -704,28 +715,34 @@ impl FakeLanguageServer { } } - pub fn handle_request( + pub fn handle_request( &mut self, mut handler: F, ) -> futures::channel::mpsc::UnboundedReceiver<()> where T: 'static + request::Request, - F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> T::Result, + F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, + Fut: 'static + Send + Future, { let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); self.handlers.lock().insert( T::METHOD, Box::new(move |id, params, cx| { let result = handler(serde_json::from_slice::(params).unwrap(), cx); - let result = serde_json::to_string(&result).unwrap(); - let result = serde_json::from_str::<&RawValue>(&result).unwrap(); - let response = AnyResponse { - id, - error: None, - result: Some(result), - }; - responded_tx.unbounded_send(()).ok(); - serde_json::to_vec(&response).unwrap() + let responded_tx = responded_tx.clone(); + async move { + let result = result.await; + let result = serde_json::to_string(&result).unwrap(); + let result = serde_json::from_str::<&RawValue>(&result).unwrap(); + let response = AnyResponse { + id, + error: None, + result: Some(result), + }; + responded_tx.unbounded_send(()).ok(); + serde_json::to_vec(&response).unwrap() + } + .boxed() }), ); responded_rx @@ -844,7 +861,7 @@ mod tests { "file://b/c" ); - fake.handle_request::(|_, _| ()); + fake.handle_request::(|_, _| async move {}); drop(server); fake.receive_notification::().await; diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0801043a09..404d867069 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5789,7 +5789,7 @@ mod tests { .unwrap(); let mut fake_server = fake_servers.next().await.unwrap(); - fake_server.handle_request::(move |params, _| { + fake_server.handle_request::(|params, _| async move { let params = params.text_document_position_params; assert_eq!( params.text_document.uri.to_file_path().unwrap(), @@ -6724,7 +6724,7 @@ mod tests { project.prepare_rename(buffer.clone(), 7, cx) }); fake_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( @@ -6743,7 +6743,7 @@ mod tests { project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx) }); fake_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///dir/one.rs" diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 4587126763..374aaf6a7e 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -2360,7 +2360,7 @@ mod tests { // Return some completions from the host's language server. cx_a.foreground().start_waiting(); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -2424,8 +2424,8 @@ mod tests { // Return a resolved completion from the host's language server. // The resolved completion has an additional text edit. - fake_language_server.handle_request::( - |params, _| { + fake_language_server.handle_request::( + |params, _| async move { assert_eq!(params.label, "first_method(…)"); lsp::CompletionItem { label: "first_method(…)".into(), @@ -2535,7 +2535,7 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { + fake_language_server.handle_request::(|_, _| async move { Some(vec![ lsp::TextEdit { range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 4)), @@ -2644,12 +2644,14 @@ mod tests { // Request the definition of a symbol as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }, + ); let definitions_1 = project_b .update(cx_b, |p, cx| p.definition(&buffer_b, 23, cx)) @@ -2671,12 +2673,14 @@ mod tests { // Try getting more definitions for the same buffer, ensuring the buffer gets reused from // the previous call to `definition`. - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root-2/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root-2/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(1, 6), lsp::Position::new(1, 11)), + ))) + }, + ); let definitions_2 = project_b .update(cx_b, |p, cx| p.definition(&buffer_b, 33, cx)) @@ -2783,26 +2787,37 @@ mod tests { // Request references to a symbol as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|params, _| { - assert_eq!( - params.text_document_position.text_document.uri.as_str(), - "file:///root-1/one.rs" - ); - Some(vec![ - lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 24), lsp::Position::new(0, 27)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 35), lsp::Position::new(0, 38)), - }, - lsp::Location { - uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 37), lsp::Position::new(0, 40)), - }, - ]) - }); + fake_language_server.handle_request::( + |params, _| async move { + assert_eq!( + params.text_document_position.text_document.uri.as_str(), + "file:///root-1/one.rs" + ); + Some(vec![ + lsp::Location { + uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 24), + lsp::Position::new(0, 27), + ), + }, + lsp::Location { + uri: lsp::Url::from_file_path("/root-1/two.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 35), + lsp::Position::new(0, 38), + ), + }, + lsp::Location { + uri: lsp::Url::from_file_path("/root-2/three.rs").unwrap(), + range: lsp::Range::new( + lsp::Position::new(0, 37), + lsp::Position::new(0, 40), + ), + }, + ]) + }, + ); let references = project_b .update(cx_b, |p, cx| p.references(&buffer_b, 7, cx)) @@ -3012,8 +3027,8 @@ mod tests { // Request document highlights as the guest. let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::( - |params, _| { + fake_language_server.handle_request::( + |params, _| async move { assert_eq!( params .text_document_position_params @@ -3158,20 +3173,22 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - #[allow(deprecated)] - Some(vec![lsp::SymbolInformation { - name: "TWO".into(), - location: lsp::Location { - uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), - range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - }, - kind: lsp::SymbolKind::CONSTANT, - tags: None, - container_name: None, - deprecated: None, - }]) - }); + fake_language_server.handle_request::( + |_, _| async move { + #[allow(deprecated)] + Some(vec![lsp::SymbolInformation { + name: "TWO".into(), + location: lsp::Location { + uri: lsp::Url::from_file_path("/code/crate-2/two.rs").unwrap(), + range: lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + }, + kind: lsp::SymbolKind::CONSTANT, + tags: None, + container_name: None, + deprecated: None, + }]) + }, + ); // Request the definition of a symbol as the guest. let symbols = project_b @@ -3289,12 +3306,14 @@ mod tests { .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server.handle_request::(|_, _| { - Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( - lsp::Url::from_file_path("/root/b.rs").unwrap(), - lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), - ))) - }); + fake_language_server.handle_request::( + |_, _| async move { + Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location::new( + lsp::Url::from_file_path("/root/b.rs").unwrap(), + lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)), + ))) + }, + ); let definitions; let buffer_b2; @@ -3402,7 +3421,7 @@ mod tests { let mut fake_language_server = fake_language_servers.next().await.unwrap(); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -3421,7 +3440,7 @@ mod tests { }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), @@ -3492,41 +3511,43 @@ mod tests { Editor::confirm_code_action(workspace, &ConfirmCodeAction(Some(0)), cx) }) .unwrap(); - fake_language_server.handle_request::(|_, _| { - lsp::CodeAction { - title: "Inline into all callers".to_string(), - edit: Some(lsp::WorkspaceEdit { - changes: Some( - [ - ( - lsp::Url::from_file_path("/a/main.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(1, 22), - lsp::Position::new(1, 34), - ), - "4".to_string(), - )], - ), - ( - lsp::Url::from_file_path("/a/other.rs").unwrap(), - vec![lsp::TextEdit::new( - lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 27), - ), - "".to_string(), - )], - ), - ] - .into_iter() - .collect(), - ), + fake_language_server.handle_request::( + |_, _| async move { + lsp::CodeAction { + title: "Inline into all callers".to_string(), + edit: Some(lsp::WorkspaceEdit { + changes: Some( + [ + ( + lsp::Url::from_file_path("/a/main.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(1, 22), + lsp::Position::new(1, 34), + ), + "4".to_string(), + )], + ), + ( + lsp::Url::from_file_path("/a/other.rs").unwrap(), + vec![lsp::TextEdit::new( + lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 27), + ), + "".to_string(), + )], + ), + ] + .into_iter() + .collect(), + ), + ..Default::default() + }), ..Default::default() - }), - ..Default::default() - } - }); + } + }, + ); // After the action is confirmed, an editor containing both modified files is opened. confirm_action.await.unwrap(); @@ -3642,7 +3663,7 @@ mod tests { }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs"); assert_eq!(params.position, lsp::Position::new(0, 7)); Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( @@ -3672,7 +3693,7 @@ mod tests { Editor::confirm_rename(workspace, &ConfirmRename, cx).unwrap() }); fake_language_server - .handle_request::(|params, _| { + .handle_request::(|params, _| async move { assert_eq!( params.text_document_position.text_document.uri.as_str(), "file:///dir/one.rs" @@ -5320,30 +5341,34 @@ mod tests { let files = files.clone(); let project = project.downgrade(); move |fake_server| { - fake_server.handle_request::(|_, _| { - Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { - text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { - range: lsp::Range::new( - lsp::Position::new(0, 0), - lsp::Position::new(0, 0), - ), - new_text: "the-new-text".to_string(), - })), - ..Default::default() - }])) - }); - - fake_server.handle_request::(|_, _| { - Some(vec![lsp::CodeActionOrCommand::CodeAction( - lsp::CodeAction { - title: "the-code-action".to_string(), + fake_server.handle_request::( + |_, _| async move { + Some(lsp::CompletionResponse::Array(vec![lsp::CompletionItem { + text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit { + range: lsp::Range::new( + lsp::Position::new(0, 0), + lsp::Position::new(0, 0), + ), + new_text: "the-new-text".to_string(), + })), ..Default::default() - }, - )]) - }); + }])) + }, + ); - fake_server.handle_request::( - |params, _| { + fake_server.handle_request::( + |_, _| async move { + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )]) + }, + ); + + fake_server.handle_request::( + |params, _| async move { Some(lsp::PrepareRenameResponse::Range(lsp::Range::new( params.position, params.position, @@ -5351,34 +5376,38 @@ mod tests { }, ); - fake_server.handle_request::({ + fake_server.handle_request::({ let files = files.clone(); let rng = rng.clone(); move |_, _| { - let files = files.lock(); - let mut rng = rng.lock(); - let count = rng.gen_range::(1..3); - let files = (0..count) - .map(|_| files.choose(&mut *rng).unwrap()) - .collect::>(); - log::info!("LSP: Returning definitions in files {:?}", &files); - Some(lsp::GotoDefinitionResponse::Array( - files - .into_iter() - .map(|file| lsp::Location { - uri: lsp::Url::from_file_path(file).unwrap(), - range: Default::default(), - }) - .collect(), - )) + let files = files.clone(); + let rng = rng.clone(); + async move { + let files = files.lock(); + let mut rng = rng.lock(); + let count = rng.gen_range::(1..3); + let files = (0..count) + .map(|_| files.choose(&mut *rng).unwrap()) + .collect::>(); + log::info!("LSP: Returning definitions in files {:?}", &files); + Some(lsp::GotoDefinitionResponse::Array( + files + .into_iter() + .map(|file| lsp::Location { + uri: lsp::Url::from_file_path(file).unwrap(), + range: Default::default(), + }) + .collect(), + )) + } } }); - fake_server.handle_request::({ + fake_server.handle_request::({ let rng = rng.clone(); let project = project.clone(); move |params, mut cx| { - if let Some(project) = project.upgrade(&cx) { + let highlights = if let Some(project) = project.upgrade(&cx) { project.update(&mut cx, |project, cx| { let path = params .text_document_position_params @@ -5415,7 +5444,8 @@ mod tests { }) } else { None - } + }; + async move { highlights } } }); } From 26aa13842972db719b01fb7b29d2357eeeb1a9f2 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 10:57:17 +0200 Subject: [PATCH 126/139] Fire fake timers waking up at the same time as the current clock --- crates/gpui/src/executor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/gpui/src/executor.rs b/crates/gpui/src/executor.rs index 2089b954fb..2c3a8df8a1 100644 --- a/crates/gpui/src/executor.rs +++ b/crates/gpui/src/executor.rs @@ -337,7 +337,7 @@ impl Deterministic { if let Some((_, wakeup_time, _)) = state.pending_timers.first() { let wakeup_time = *wakeup_time; - if wakeup_time < new_now { + if wakeup_time <= new_now { let timer_count = state .pending_timers .iter() From 2c78c830eb6a7834f0c98e57732589cc92bd6cc3 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 10:58:40 +0200 Subject: [PATCH 127/139] Skip formatting during save if it takes too long --- Cargo.lock | 1 + crates/editor/Cargo.toml | 1 + crates/editor/src/editor.rs | 90 ++++++++++++++++++++++++++++++++++++- crates/editor/src/items.rs | 19 ++++++-- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 38ffa70955..9c36923964 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1617,6 +1617,7 @@ dependencies = [ "collections", "ctor", "env_logger", + "futures", "fuzzy", "gpui", "itertools", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 02069fb610..77e169b91b 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -35,6 +35,7 @@ util = { path = "../util" } workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow = "1.0" +futures = "0.3" itertools = "0.10" lazy_static = "1.4" log = "0.4" diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b269ce3cd5..7c74d36ecb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6291,7 +6291,7 @@ mod tests { use text::Point; use unindent::Unindent; use util::test::{marked_text_by, sample_text}; - use workspace::FollowableItem; + use workspace::{FollowableItem, ItemHandle}; #[gpui::test] fn test_edit_events(cx: &mut MutableAppContext) { @@ -8669,6 +8669,94 @@ mod tests { }); } + #[gpui::test] + async fn test_format_during_save(cx: &mut gpui::TestAppContext) { + cx.foreground().forbid_parking(); + cx.update(populate_settings); + + let (mut language_server_config, mut fake_servers) = LanguageServerConfig::fake(); + language_server_config.set_fake_capabilities(lsp::ServerCapabilities { + document_formatting_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }); + let language = Arc::new(Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + language_server: Some(language_server_config), + ..Default::default() + }, + Some(tree_sitter_rust::language()), + )); + + let fs = FakeFs::new(cx.background().clone()); + fs.insert_file("/file.rs", Default::default()).await; + + let project = Project::test(fs, cx); + project.update(cx, |project, _| project.languages().add(language)); + + let worktree_id = project + .update(cx, |project, cx| { + project.find_or_create_local_worktree("/file.rs", true, cx) + }) + .await + .unwrap() + .0 + .read_with(cx, |tree, _| tree.id()); + let buffer = project + .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx)) + .await + .unwrap(); + let mut fake_server = fake_servers.next().await.unwrap(); + + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + let save = cx.update(|cx| editor.save(project.clone(), cx)); + fake_server + .handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + Some(vec![lsp::TextEdit::new( + lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)), + ", ".to_string(), + )]) + }) + .next() + .await; + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one, two\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + + editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); + assert!(cx.read(|cx| editor.is_dirty(cx))); + + // Ensure we can still save even if formatting hangs. + fake_server.handle_request::(move |params, _| async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/file.rs").unwrap() + ); + futures::future::pending::<()>().await; + unreachable!() + }); + let save = cx.update(|cx| editor.save(project.clone(), cx)); + cx.foreground().advance_clock(items::FORMAT_TIMEOUT); + save.await.unwrap(); + assert_eq!( + editor.read_with(cx, |editor, cx| editor.text(cx)), + "one\ntwo\nthree\n" + ); + assert!(!cx.read(|cx| editor.is_dirty(cx))); + } + #[gpui::test] async fn test_completion(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index f10956c125..79b25f8f60 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1,5 +1,6 @@ use crate::{Anchor, Autoscroll, Editor, Event, ExcerptId, NavigationData, ToOffset, ToPoint as _}; use anyhow::{anyhow, Result}; +use futures::FutureExt; use gpui::{ elements::*, geometry::vector::vec2f, AppContext, Entity, ModelHandle, MutableAppContext, RenderContext, Subscription, Task, View, ViewContext, ViewHandle, @@ -7,13 +8,15 @@ use gpui::{ use language::{Bias, Buffer, Diagnostic, File as _, SelectionGoal}; use project::{File, Project, ProjectEntryId, ProjectPath}; use rpc::proto::{self, update_view}; -use std::{fmt::Write, path::PathBuf}; +use std::{fmt::Write, path::PathBuf, time::Duration}; use text::{Point, Selection}; -use util::ResultExt; +use util::TryFutureExt; use workspace::{ FollowableItem, Item, ItemHandle, ItemNavHistory, ProjectItem, Settings, StatusItemView, }; +pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); + impl FollowableItem for Editor { fn from_state_proto( pane: ViewHandle, @@ -317,9 +320,17 @@ impl Item for Editor { ) -> Task> { let buffer = self.buffer().clone(); let buffers = buffer.read(cx).all_buffers(); - let transaction = project.update(cx, |project, cx| project.format(buffers, true, cx)); + let mut timeout = cx.background().timer(FORMAT_TIMEOUT).fuse(); + let format = project.update(cx, |project, cx| project.format(buffers, true, cx)); cx.spawn(|this, mut cx| async move { - let transaction = transaction.await.log_err(); + let transaction = futures::select_biased! { + _ = timeout => { + log::warn!("timed out waiting for formatting"); + None + } + transaction = format.log_err().fuse() => transaction, + }; + this.update(&mut cx, |editor, cx| { editor.request_autoscroll(Autoscroll::Fit, cx) }); From 03752f913d194a4154c23682b125a34811f96c81 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 11:05:55 +0200 Subject: [PATCH 128/139] Fix warnings --- crates/lsp/src/lsp.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 6e729fc62b..fad49d2424 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -1,8 +1,6 @@ use anyhow::{anyhow, Context, Result}; use collections::HashMap; -use futures::{ - channel::oneshot, future::BoxFuture, io::BufWriter, AsyncRead, AsyncWrite, FutureExt, -}; +use futures::{channel::oneshot, io::BufWriter, AsyncRead, AsyncWrite}; use gpui::{executor, Task}; use parking_lot::{Mutex, RwLock}; use postage::{barrier, prelude::Stream}; @@ -560,7 +558,11 @@ type FakeLanguageServerHandlers = Arc< &'static str, Box< dyn Send - + FnMut(usize, &[u8], gpui::AsyncAppContext) -> BoxFuture<'static, Vec>, + + FnMut( + usize, + &[u8], + gpui::AsyncAppContext, + ) -> futures::future::BoxFuture<'static, Vec>, >, >, >, @@ -724,6 +726,8 @@ impl FakeLanguageServer { F: 'static + Send + FnMut(T::Params, gpui::AsyncAppContext) -> Fut, Fut: 'static + Send + Future, { + use futures::FutureExt as _; + let (responded_tx, responded_rx) = futures::channel::mpsc::unbounded(); self.handlers.lock().insert( T::METHOD, From a2c4205c5c3640e6f3ebdb889460b3064bf64bde Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 11:17:19 +0200 Subject: [PATCH 129/139] Make indent and outdent explicit actions and unify `tab`bing logic --- crates/editor/src/editor.rs | 51 ++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9327ee251..7cb50167b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -68,7 +68,8 @@ action!(Backspace); action!(Delete); action!(Input, String); action!(Newline); -action!(Tab); +action!(Tab, Direction); +action!(Indent); action!(Outdent); action!(DeleteLine); action!(DeleteToPreviousWordStart); @@ -171,13 +172,13 @@ pub fn init(cx: &mut MutableAppContext) { Some("Editor && showing_code_actions"), ), Binding::new("enter", ConfirmRename, Some("Editor && renaming")), - Binding::new("tab", Tab, Some("Editor")), + Binding::new("tab", Tab(Direction::Next), Some("Editor")), + Binding::new("shift-tab", Tab(Direction::Prev), Some("Editor")), Binding::new( "tab", ConfirmCompletion(None), Some("Editor && showing_completions"), ), - Binding::new("shift-tab", Outdent, Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")), Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")), @@ -304,7 +305,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); - cx.add_action(Editor::tab); + cx.add_action(Editor::handle_tab); + cx.add_action(Editor::indent); cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); cx.add_action(Editor::delete_to_previous_word_start); @@ -2861,18 +2863,34 @@ impl Editor { }); } - pub fn tab(&mut self, _: &Tab, cx: &mut ViewContext) { - if self.move_to_next_snippet_tabstop(cx) { - return; - } + pub fn handle_tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext) { + match direction { + Direction::Prev => { + if !self.snippet_stack.is_empty() { + self.move_to_prev_snippet_tabstop(cx); + return; + } + self.outdent(&Outdent, cx); + } + Direction::Next => { + if self.move_to_next_snippet_tabstop(cx) { + return; + } + + self.tab(false, cx); + } + } + } + + pub fn tab(&mut self, force_indentation: bool, cx: &mut ViewContext) { let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); self.transact(cx, |this, cx| { let mut last_indent = None; this.buffer.update(cx, |buffer, cx| { for selection in &mut selections { - if selection.is_empty() { + if selection.is_empty() && !force_indentation { let char_column = buffer .read(cx) .text_for_range(Point::new(selection.start.row, 0)..selection.start) @@ -2938,12 +2956,11 @@ impl Editor { }); } - pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { - if !self.snippet_stack.is_empty() { - self.move_to_prev_snippet_tabstop(cx); - return; - } + pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { + self.tab(true, cx); + } + pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); @@ -7458,7 +7475,7 @@ mod tests { ); // indent from mid-tabstop to full tabstop - view.tab(&Tab, cx); + view.tab(false, cx); assert_eq!(view.text(cx), " one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7483,7 +7500,7 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); // indent and outdent affect only the preceding line - view.tab(&Tab, cx); + view.tab(false, cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7498,7 +7515,7 @@ mod tests { // Ensure that indenting/outdenting works when the cursor is at column 0. view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(&Tab, cx); + view.tab(false, cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), From ac88003c19db7b90636990982f7748e62236fecd Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 11:34:46 +0200 Subject: [PATCH 130/139] Bind `Outdent` and `Indent` respectively to `cmd-[` and `cmd-]` --- crates/editor/src/editor.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7cb50167b6..6c0b43f393 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -179,6 +179,8 @@ pub fn init(cx: &mut MutableAppContext) { ConfirmCompletion(None), Some("Editor && showing_completions"), ), + Binding::new("cmd-[", Outdent, Some("Editor")), + Binding::new("cmd-]", Indent, Some("Editor")), Binding::new("ctrl-shift-K", DeleteLine, Some("Editor")), Binding::new("alt-backspace", DeleteToPreviousWordStart, Some("Editor")), Binding::new("alt-h", DeleteToPreviousWordStart, Some("Editor")), From f69bd0e327d785d7bab8e723660c4c739e465798 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 11:52:13 +0200 Subject: [PATCH 131/139] Snap icon sprites to pixel grid This should resolve some rendering artifacts potentially caused by floating point errors when sampling the texture. It should also lead to crisper images when icons are rendered midway through a pixel. --- crates/gpui/src/platform/mac/renderer.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 07d425af3e..873586b61e 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -561,9 +561,10 @@ impl Renderer { } for icon in icons { - let origin = icon.bounds.origin() * scale_factor; - let target_size = icon.bounds.size() * scale_factor; - let source_size = (target_size * 2.).ceil().to_i32(); + // Snap sprite to pixel grid. + let origin = (icon.bounds.origin() * scale_factor).floor(); + let target_size = (icon.bounds.size() * scale_factor).ceil(); + let source_size = (target_size * 2.).to_i32(); let sprite = self.sprite_cache From 3cfa2c65b360adcf36679b3223832085b86f3e1f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 14:38:51 +0200 Subject: [PATCH 132/139] Autoscroll to newest cursor on cmd-d instead of fitting all selections --- crates/editor/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9327ee251..60d6bbc699 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -3998,7 +3998,7 @@ impl Editor { state.stack.pop(); } - self.update_selections(new_selections, Some(Autoscroll::Fit), cx); + self.update_selections(new_selections, Some(Autoscroll::Newest), cx); if state.stack.len() > 1 { self.add_selections_state = Some(state); } From 4ed0607e1e66e2dd112a7a74a7affbc8daa694e0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 14:52:54 +0200 Subject: [PATCH 133/139] Extract `SelectionHistory` in preparation to store an undo/redo stack --- crates/editor/src/editor.rs | 43 +++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 60d6bbc699..40cae9576e 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -455,8 +455,7 @@ pub struct Editor { columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, - selection_history: - HashMap]>, Option]>>)>, + selection_history: SelectionHistory, autoclose_stack: InvalidationStack, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, @@ -508,6 +507,37 @@ pub struct PendingSelection { mode: SelectMode, } +#[derive(Default)] +struct SelectionHistory { + selections_by_transaction: + HashMap]>, Option]>>)>, +} + +impl SelectionHistory { + fn insert_transaction( + &mut self, + transaction_id: TransactionId, + selections: Arc<[Selection]>, + ) { + self.selections_by_transaction + .insert(transaction_id, (selections, None)); + } + + fn transaction( + &self, + transaction_id: TransactionId, + ) -> Option<&(Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get(&transaction_id) + } + + fn transaction_mut( + &mut self, + transaction_id: TransactionId, + ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { + self.selections_by_transaction.get_mut(&transaction_id) + } +} + struct AddSelectionsState { above: bool, stack: Vec, @@ -3438,7 +3468,7 @@ impl Editor { pub fn undo(&mut self, _: &Undo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.undo(cx)) { - if let Some((selections, _)) = self.selection_history.get(&tx_id).cloned() { + if let Some((selections, _)) = self.selection_history.transaction(tx_id).cloned() { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); @@ -3448,7 +3478,8 @@ impl Editor { pub fn redo(&mut self, _: &Redo, cx: &mut ViewContext) { if let Some(tx_id) = self.buffer.update(cx, |buffer, cx| buffer.redo(cx)) { - if let Some((_, Some(selections))) = self.selection_history.get(&tx_id).cloned() { + if let Some((_, Some(selections))) = self.selection_history.transaction(tx_id).cloned() + { self.set_selections(selections, None, true, cx); } self.request_autoscroll(Autoscroll::Fit, cx); @@ -5279,7 +5310,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.start_transaction_at(now, cx)) { self.selection_history - .insert(tx_id, (self.selections.clone(), None)); + .insert_transaction(tx_id, self.selections.clone()); } } @@ -5288,7 +5319,7 @@ impl Editor { .buffer .update(cx, |buffer, cx| buffer.end_transaction_at(now, cx)) { - if let Some((_, end_selections)) = self.selection_history.get_mut(&tx_id) { + if let Some((_, end_selections)) = self.selection_history.transaction_mut(tx_id) { *end_selections = Some(self.selections.clone()); } else { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); From 73c2f52158907efbc7812f97bbfdbbb64fa15222 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 16:05:44 +0200 Subject: [PATCH 134/139] Implement `cmd-u` and `cmd-shift-u` to undo and redo selections --- crates/editor/src/editor.rs | 98 +++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40cae9576e..5c3840fcf9 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -120,6 +120,8 @@ action!(ToggleComments); action!(SelectLargerSyntaxNode); action!(SelectSmallerSyntaxNode); action!(MoveToEnclosingBracket); +action!(UndoSelection); +action!(RedoSelection); action!(GoToDiagnostic, Direction); action!(GoToDefinition); action!(FindAllReferences); @@ -280,6 +282,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("ctrl-w", SelectLargerSyntaxNode, Some("Editor")), Binding::new("alt-down", SelectSmallerSyntaxNode, Some("Editor")), Binding::new("ctrl-shift-W", SelectSmallerSyntaxNode, Some("Editor")), + Binding::new("cmd-u", UndoSelection, Some("Editor")), + Binding::new("cmd-shift-U", RedoSelection, Some("Editor")), Binding::new("f8", GoToDiagnostic(Direction::Next), Some("Editor")), Binding::new("shift-f8", GoToDiagnostic(Direction::Prev), Some("Editor")), Binding::new("f2", Rename, Some("Editor")), @@ -356,6 +360,8 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::select_larger_syntax_node); cx.add_action(Editor::select_smaller_syntax_node); cx.add_action(Editor::move_to_enclosing_bracket); + cx.add_action(Editor::undo_selection); + cx.add_action(Editor::redo_selection); cx.add_action(Editor::go_to_diagnostic); cx.add_action(Editor::go_to_definition); cx.add_action(Editor::page_up); @@ -507,10 +513,32 @@ pub struct PendingSelection { mode: SelectMode, } +#[derive(Clone)] +struct SelectionHistoryEntry { + selections: Arc<[Selection]>, + select_next_state: Option, + add_selections_state: Option, +} + +enum SelectionHistoryMode { + Normal, + Undoing, + Redoing, +} + +impl Default for SelectionHistoryMode { + fn default() -> Self { + Self::Normal + } +} + #[derive(Default)] struct SelectionHistory { selections_by_transaction: HashMap]>, Option]>>)>, + mode: SelectionHistoryMode, + undo_stack: Vec, + redo_stack: Vec, } impl SelectionHistory { @@ -536,13 +564,48 @@ impl SelectionHistory { ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { self.selections_by_transaction.get_mut(&transaction_id) } + + fn push(&mut self, entry: SelectionHistoryEntry) { + if !entry.selections.is_empty() { + match self.mode { + SelectionHistoryMode::Normal => { + self.push_undo(entry); + self.redo_stack.clear(); + } + SelectionHistoryMode::Undoing => self.push_redo(entry), + SelectionHistoryMode::Redoing => self.push_undo(entry), + } + } + } + + fn push_undo(&mut self, entry: SelectionHistoryEntry) { + if self + .undo_stack + .last() + .map_or(true, |e| e.selections != entry.selections) + { + self.undo_stack.push(entry) + } + } + + fn push_redo(&mut self, entry: SelectionHistoryEntry) { + if self + .redo_stack + .last() + .map_or(true, |e| e.selections != entry.selections) + { + self.redo_stack.push(entry) + } + } } +#[derive(Clone)] struct AddSelectionsState { above: bool, stack: Vec, } +#[derive(Clone)] struct SelectNextState { query: AhoCorasick, wordwise: bool, @@ -3943,6 +4006,7 @@ impl Editor { } fn add_selection(&mut self, above: bool, cx: &mut ViewContext) { + self.push_to_selection_history(); let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let mut selections = self.local_selections::(cx); let mut state = self.add_selections_state.take().unwrap_or_else(|| { @@ -4036,6 +4100,7 @@ impl Editor { } pub fn select_next(&mut self, action: &SelectNext, cx: &mut ViewContext) { + self.push_to_selection_history(); let replace_newest = action.0; let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; @@ -4320,6 +4385,30 @@ impl Editor { self.update_selections(selections, Some(Autoscroll::Fit), cx); } + pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Undoing; + if let Some(entry) = self.selection_history.undo_stack.pop() { + self.set_selections(entry.selections.clone(), None, true, cx); + self.select_next_state = entry.select_next_state.clone(); + self.add_selections_state = entry.add_selections_state.clone(); + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + + pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { + self.end_selection(cx); + self.selection_history.mode = SelectionHistoryMode::Redoing; + if let Some(entry) = self.selection_history.redo_stack.pop() { + self.set_selections(entry.selections.clone(), None, true, cx); + self.select_next_state = entry.select_next_state.clone(); + self.add_selections_state = entry.add_selections_state.clone(); + self.request_autoscroll(Autoscroll::Newest, cx); + } + self.selection_history.mode = SelectionHistoryMode::Normal; + } + pub fn go_to_diagnostic( &mut self, &GoToDiagnostic(direction): &GoToDiagnostic, @@ -5218,6 +5307,7 @@ impl Editor { let old_cursor_position = self.newest_anchor_selection().head(); + self.push_to_selection_history(); self.selections = selections; self.pending_selection = pending_selection; if self.focused && self.leader_replica_id.is_none() { @@ -5283,6 +5373,14 @@ impl Editor { cx.emit(Event::SelectionsChanged { local }); } + fn push_to_selection_history(&mut self) { + self.selection_history.push(SelectionHistoryEntry { + selections: self.selections.clone(), + select_next_state: self.select_next_state.clone(), + add_selections_state: self.add_selections_state.clone(), + }); + } + pub fn request_autoscroll(&mut self, autoscroll: Autoscroll, cx: &mut ViewContext) { self.autoscroll_request = Some((autoscroll, true)); cx.notify(); From 2a1fed1387d1c675a78cff5cd86a1293cfa1c420 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 16:36:12 +0200 Subject: [PATCH 135/139] Insert tabs instead of indenting only when all selections are empty Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 138 +++++++++++++++++++----------------- 1 file changed, 72 insertions(+), 66 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c0b43f393..ca94c6923f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -307,7 +307,7 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(Editor::newline); cx.add_action(Editor::backspace); cx.add_action(Editor::delete); - cx.add_action(Editor::handle_tab); + cx.add_action(Editor::tab); cx.add_action(Editor::indent); cx.add_action(Editor::outdent); cx.add_action(Editor::delete_line); @@ -2865,7 +2865,7 @@ impl Editor { }); } - pub fn handle_tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext) { + pub fn tab(&mut self, &Tab(direction): &Tab, cx: &mut ViewContext) { match direction { Direction::Prev => { if !self.snippet_stack.is_empty() { @@ -2880,76 +2880,86 @@ impl Editor { return; } - self.tab(false, cx); + let tab_size = cx.global::().tab_size; + let mut selections = self.local_selections::(cx); + if selections.iter().all(|s| s.is_empty()) { + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + for selection in &mut selections { + let char_column = buffer + .read(cx) + .text_for_range( + Point::new(selection.start.row, 0)..selection.start, + ) + .flat_map(str::chars) + .count(); + let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + buffer.edit( + [selection.start..selection.start], + " ".repeat(chars_to_next_tab_stop), + cx, + ); + selection.start.column += chars_to_next_tab_stop as u32; + selection.end = selection.start; + } + }); + this.update_selections(selections, Some(Autoscroll::Fit), cx); + }); + } else { + self.indent(&Indent, cx); + } } } } - pub fn tab(&mut self, force_indentation: bool, cx: &mut ViewContext) { + pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { let tab_size = cx.global::().tab_size; let mut selections = self.local_selections::(cx); self.transact(cx, |this, cx| { let mut last_indent = None; this.buffer.update(cx, |buffer, cx| { for selection in &mut selections { - if selection.is_empty() && !force_indentation { - let char_column = buffer - .read(cx) - .text_for_range(Point::new(selection.start.row, 0)..selection.start) - .flat_map(str::chars) - .count(); - let chars_to_next_tab_stop = tab_size - (char_column % tab_size); + let mut start_row = selection.start.row; + let mut end_row = selection.end.row + 1; + + // If a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; + } + + // Avoid re-indenting a row that has already been indented by a + // previous selection, but still update this selection's column + // to reflect that indentation. + if let Some((last_indent_row, last_indent_len)) = last_indent { + if last_indent_row == selection.start.row { + selection.start.column += last_indent_len; + start_row += 1; + } + if last_indent_row == selection.end.row { + selection.end.column += last_indent_len; + } + } + + for row in start_row..end_row { + let indent_column = buffer.read(cx).indent_column_for_line(row) as usize; + let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); + let row_start = Point::new(row, 0); buffer.edit( - [selection.start..selection.start], - " ".repeat(chars_to_next_tab_stop), + [row_start..row_start], + " ".repeat(columns_to_next_tab_stop), cx, ); - selection.start.column += chars_to_next_tab_stop as u32; - selection.end = selection.start; - } else { - let mut start_row = selection.start.row; - let mut end_row = selection.end.row + 1; - // If a selection ends at the beginning of a line, don't indent - // that last line. - if selection.end.column == 0 { - end_row -= 1; + // Update this selection's endpoints to reflect the indentation. + if row == selection.start.row { + selection.start.column += columns_to_next_tab_stop as u32; + } + if row == selection.end.row { + selection.end.column += columns_to_next_tab_stop as u32; } - // Avoid re-indenting a row that has already been indented by a - // previous selection, but still update this selection's column - // to reflect that indentation. - if let Some((last_indent_row, last_indent_len)) = last_indent { - if last_indent_row == selection.start.row { - selection.start.column += last_indent_len; - start_row += 1; - } - if last_indent_row == selection.end.row { - selection.end.column += last_indent_len; - } - } - - for row in start_row..end_row { - let indent_column = - buffer.read(cx).indent_column_for_line(row) as usize; - let columns_to_next_tab_stop = tab_size - (indent_column % tab_size); - let row_start = Point::new(row, 0); - buffer.edit( - [row_start..row_start], - " ".repeat(columns_to_next_tab_stop), - cx, - ); - - // Update this selection's endpoints to reflect the indentation. - if row == selection.start.row { - selection.start.column += columns_to_next_tab_stop as u32; - } - if row == selection.end.row { - selection.end.column += columns_to_next_tab_stop as u32; - } - - last_indent = Some((row, columns_to_next_tab_stop as u32)); - } + last_indent = Some((row, columns_to_next_tab_stop as u32)); } } }); @@ -2958,10 +2968,6 @@ impl Editor { }); } - pub fn indent(&mut self, _: &Indent, cx: &mut ViewContext) { - self.tab(true, cx); - } - pub fn outdent(&mut self, _: &Outdent, cx: &mut ViewContext) { let tab_size = cx.global::().tab_size; let selections = self.local_selections::(cx); @@ -7477,7 +7483,7 @@ mod tests { ); // indent from mid-tabstop to full tabstop - view.tab(false, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), " one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7488,7 +7494,7 @@ mod tests { ); // outdent from 1 tabstop to 0 tabstops - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7502,13 +7508,13 @@ mod tests { view.select_display_ranges(&[DisplayPoint::new(1, 1)..DisplayPoint::new(2, 0)], cx); // indent and outdent affect only the preceding line - view.tab(false, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), &[DisplayPoint::new(1, 5)..DisplayPoint::new(2, 0)] ); - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7517,7 +7523,7 @@ mod tests { // Ensure that indenting/outdenting works when the cursor is at column 0. view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.tab(false, cx); + view.tab(&Tab(Direction::Next), cx); assert_eq!(view.text(cx), "one two\n three\n four"); assert_eq!( view.selected_display_ranges(cx), @@ -7525,7 +7531,7 @@ mod tests { ); view.select_display_ranges(&[DisplayPoint::new(1, 0)..DisplayPoint::new(1, 0)], cx); - view.outdent(&Outdent, cx); + view.tab(&Tab(Direction::Prev), cx); assert_eq!(view.text(cx), "one two\nthree\n four"); assert_eq!( view.selected_display_ranges(cx), From bbfb63ff892c0f1010b8d41efea0fe62c7b6a321 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 16:12:34 +0200 Subject: [PATCH 136/139] Cap selection history to 1024 entries --- crates/editor/src/editor.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5c3840fcf9..40a27752cd 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -10,7 +10,7 @@ mod test; use aho_corasick::AhoCorasick; use anyhow::Result; use clock::ReplicaId; -use collections::{BTreeMap, Bound, HashMap, HashSet}; +use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; pub use display_map::DisplayPoint; use display_map::*; pub use element::*; @@ -62,6 +62,7 @@ use workspace::{settings, ItemNavHistory, Settings, Workspace}; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; +const MAX_SELECTION_HISTORY_LEN: usize = 1024; action!(Cancel); action!(Backspace); @@ -537,8 +538,8 @@ struct SelectionHistory { selections_by_transaction: HashMap]>, Option]>>)>, mode: SelectionHistoryMode, - undo_stack: Vec, - redo_stack: Vec, + undo_stack: VecDeque, + redo_stack: VecDeque, } impl SelectionHistory { @@ -581,20 +582,26 @@ impl SelectionHistory { fn push_undo(&mut self, entry: SelectionHistoryEntry) { if self .undo_stack - .last() + .back() .map_or(true, |e| e.selections != entry.selections) { - self.undo_stack.push(entry) + self.undo_stack.push_back(entry); + if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.undo_stack.pop_front(); + } } } fn push_redo(&mut self, entry: SelectionHistoryEntry) { if self .redo_stack - .last() + .back() .map_or(true, |e| e.selections != entry.selections) { - self.redo_stack.push(entry) + self.redo_stack.push_back(entry); + if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { + self.redo_stack.pop_front(); + } } } } @@ -4388,7 +4395,7 @@ impl Editor { pub fn undo_selection(&mut self, _: &UndoSelection, cx: &mut ViewContext) { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Undoing; - if let Some(entry) = self.selection_history.undo_stack.pop() { + if let Some(entry) = self.selection_history.undo_stack.pop_back() { self.set_selections(entry.selections.clone(), None, true, cx); self.select_next_state = entry.select_next_state.clone(); self.add_selections_state = entry.add_selections_state.clone(); @@ -4400,7 +4407,7 @@ impl Editor { pub fn redo_selection(&mut self, _: &RedoSelection, cx: &mut ViewContext) { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Redoing; - if let Some(entry) = self.selection_history.redo_stack.pop() { + if let Some(entry) = self.selection_history.redo_stack.pop_back() { self.set_selections(entry.selections.clone(), None, true, cx); self.select_next_state = entry.select_next_state.clone(); self.add_selections_state = entry.add_selections_state.clone(); From 45ecd8e0a61005da19893bd013fa2d35b45a6c70 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:11:35 +0200 Subject: [PATCH 137/139] Always use square brackets in `marked_text_ranges` Co-Authored-By: Nathan Sobo --- crates/editor/src/display_map.rs | 5 +---- crates/util/src/test.rs | 28 ++++++++++++---------------- 2 files changed, 13 insertions(+), 20 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index ae2c4655a5..2bea851ec2 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1081,10 +1081,7 @@ pub mod tests { ); language.set_theme(&theme); - let (text, highlighted_ranges) = marked_text_ranges( - r#"const{} : B = "c [d]""#, - vec![('{', '}'), ('<', '>'), ('[', ']')], - ); + let (text, highlighted_ranges) = marked_text_ranges(r#"const[] [a]: B = "c [d]""#); let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); buffer.condition(&cx, |buf, _| !buf.is_parsing()).await; diff --git a/crates/util/src/test.rs b/crates/util/src/test.rs index b4cf25274e..252383b347 100644 --- a/crates/util/src/test.rs +++ b/crates/util/src/test.rs @@ -77,22 +77,18 @@ pub fn marked_text(marked_text: &str) -> (String, Vec) { (unmarked_text, markers.remove(&'|').unwrap_or_else(Vec::new)) } -pub fn marked_text_ranges( - marked_text: &str, - range_markers: Vec<(char, char)>, -) -> (String, Vec>) { - let mut marker_chars = Vec::new(); - for (start, end) in range_markers.iter() { - marker_chars.push(*start); - marker_chars.push(*end); - } - let (unmarked_text, markers) = marked_text_by(marked_text, marker_chars); - let ranges = range_markers - .iter() - .map(|(start_marker, end_marker)| { - let start = markers.get(start_marker).unwrap()[0]; - let end = markers.get(end_marker).unwrap()[0]; - start..end +pub fn marked_text_ranges(marked_text: &str) -> (String, Vec>) { + let (unmarked_text, mut markers) = marked_text_by(marked_text, vec!['[', ']']); + let opens = markers.remove(&'[').unwrap_or_default(); + let closes = markers.remove(&']').unwrap_or_default(); + assert_eq!(opens.len(), closes.len(), "marked ranges are unbalanced"); + + let ranges = opens + .into_iter() + .zip(closes) + .map(|(open, close)| { + assert!(close >= open, "marked ranges must be disjoint"); + open..close }) .collect(); (unmarked_text, ranges) From aec82ef71e35989721e1cd16388392c1c1ba18aa Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:20:52 +0200 Subject: [PATCH 138/139] Test selection history Co-Authored-By: Nathan Sobo --- crates/editor/src/editor.rs | 47 ++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 40a27752cd..6423a7f72f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6426,7 +6426,7 @@ mod tests { use std::{cell::RefCell, rc::Rc, time::Instant}; use text::Point; use unindent::Unindent; - use util::test::{marked_text_by, sample_text}; + use util::test::{marked_text_by, marked_text_ranges, sample_text}; use workspace::FollowableItem; #[gpui::test] @@ -8221,6 +8221,21 @@ mod tests { view.selected_display_ranges(cx), vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] ); + + view.undo_selection(&UndoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![ + DisplayPoint::new(0, 3)..DisplayPoint::new(0, 3), + DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3) + ] + ); + + view.redo_selection(&RedoSelection, cx); + assert_eq!( + view.selected_display_ranges(cx), + vec![DisplayPoint::new(1, 3)..DisplayPoint::new(1, 3)] + ); }); view.update(cx, |view, cx| { @@ -8353,6 +8368,36 @@ mod tests { }); } + #[gpui::test] + fn test_select_next(cx: &mut gpui::MutableAppContext) { + populate_settings(cx); + + let (text, ranges) = marked_text_ranges("[abc]\n[abc] [abc]\ndefabc\n[abc]"); + let buffer = MultiBuffer::build_simple(&text, cx); + let (_, view) = cx.add_window(Default::default(), |cx| build_editor(buffer, cx)); + + view.update(cx, |view, cx| { + view.select_ranges([ranges[1].start + 1..ranges[1].start + 1], None, cx); + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.undo_selection(&UndoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..2]); + + view.redo_selection(&RedoSelection, cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..3]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[1..4]); + + view.select_next(&SelectNext(false), cx); + assert_eq!(view.selected_ranges(cx), &ranges[0..4]); + }); + } + #[gpui::test] async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { cx.update(populate_settings); From f274a6ab4f981e28e8167a727fc064d646cd7c18 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 28 Mar 2022 17:47:14 +0200 Subject: [PATCH 139/139] Avoid unnecessary clones when undoing/redoing selections --- crates/editor/src/editor.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 7f771f26e8..f861b6ac60 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4421,9 +4421,9 @@ impl Editor { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Undoing; if let Some(entry) = self.selection_history.undo_stack.pop_back() { - self.set_selections(entry.selections.clone(), None, true, cx); - self.select_next_state = entry.select_next_state.clone(); - self.add_selections_state = entry.add_selections_state.clone(); + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::Newest, cx); } self.selection_history.mode = SelectionHistoryMode::Normal; @@ -4433,9 +4433,9 @@ impl Editor { self.end_selection(cx); self.selection_history.mode = SelectionHistoryMode::Redoing; if let Some(entry) = self.selection_history.redo_stack.pop_back() { - self.set_selections(entry.selections.clone(), None, true, cx); - self.select_next_state = entry.select_next_state.clone(); - self.add_selections_state = entry.add_selections_state.clone(); + self.set_selections(entry.selections, None, true, cx); + self.select_next_state = entry.select_next_state; + self.add_selections_state = entry.add_selections_state; self.request_autoscroll(Autoscroll::Newest, cx); } self.selection_history.mode = SelectionHistoryMode::Normal;