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 a80af9ea5f21dfe30e9842ef794580c717bb2dcc..8c6904d1360e63b5024f492985db5e18950cef82 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 1aed9791c41a2c5443bb2924cc12dddd35fb991c..ad8057db4e8421b4729d653d844f4706484b98b8 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 89cf9afba2c9c2c831fc0e280992ded440d4a529..a03e6e54ae555ca964f2d820fd2df2050874e8a8 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 5ee5b36f876e8f1dfbbc018c0bf30ef5612ad5e4..88ccb694d2c31896467837010e944bc8296454b8 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 | 282 +++++------------- 10 files changed, 280 insertions(+), 516 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 1a432f52e20658eacd3141aa141d243324b89299..1db19f7f5e786c43634f0291f15061aa7ee3324e 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 8c6904d1360e63b5024f492985db5e18950cef82..6a298591951dc7c8d12476982225cdae01b58503 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 c9af9f0854cf7beba9254dc02cb76e7053079bfc..b84a095e6b2bfa3ee8356164b7f83c8eb02bc23f 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 - })) - } - - 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 + 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(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 a03e6e54ae555ca964f2d820fd2df2050874e8a8..a479e5fba1c56c5e543e125be3f523c7c61acc84 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 f5af7fc70ea0c0c89ae707ea3b8cde487c686038..b9bb25a5fcdd9f66f46c82ed73d84a0429e44d96 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 1ef8dd34a0da1d0e11d7e50e7c2b45a248ed08cf..4a121c82e4e7109c3e012f9660a07baaa1cfbb2e 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 bfd204671f6994a07b5ab1717afc684b9659c9c8..29808ff7d9df5b428f7299493b88a13cece8e8eb 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 b09c88a4a0ff8b020dbf839684cbbe4c1f3cf88b..56395b107a4b325bd4d1239807bc11456a7becbd 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 c54a1b050b341cb75f1768269579f25c43fa64cb..9f5ee5227431df6a60ae76df9cea66331c18869f 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 6bff12a577bab115c96bf8554cbf5e2a3ca59d44..5a3385a0144938a5658524b616c567e6a20fc48f 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 - where - T: 'static + ItemHandle, - { - self.items - .insert(Reverse(item_handle.id()), item_handle.downgrade()); - pane.update(cx, |pane, cx| pane.open_item(item_handle, self, cx)) + ) -> Box { + pane.update(cx, |pane, cx| pane.open_item(item_view, cx)) } - pub fn activate_pane_for_item( + pub fn open_item_for_project_entry( &mut self, - item: &dyn ItemHandle, + project_entry: ProjectEntry, 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 + 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(); } + + 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 1db19f7f5e786c43634f0291f15061aa7ee3324e..2b10adb202283d0b47c18506aaf3559faba06901 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 6a298591951dc7c8d12476982225cdae01b58503..704a2cf248156821da6f194428018c3b48075f07 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 b84a095e6b2bfa3ee8356164b7f83c8eb02bc23f..a9bac1908b5e5ce90eeec4ad0d9f976999a31227 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 b9bb25a5fcdd9f66f46c82ed73d84a0429e44d96..ae60ab825fac9bce7de85a011f98781bfabae937 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 4a121c82e4e7109c3e012f9660a07baaa1cfbb2e..2bc9c3d234c58ceb33ba66a0c6cf3a3b1fc925c7 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 7f03005b80cc2d8eddab64f0c59a509cf2d6f12b..372f0c169bb06602ebbfc4bbb2f7e576ca4d051e 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 56395b107a4b325bd4d1239807bc11456a7becbd..20df486f486a0fcf58cd6853e55d53262e7a62da 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 9f5ee5227431df6a60ae76df9cea66331c18869f..5d346279f0fa3e425dca6b7b9db1dd2ddef50cdb 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 5a3385a0144938a5658524b616c567e6a20fc48f..464eecd307d21207021e70d0d7c74a7695376e52 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 5d346279f0fa3e425dca6b7b9db1dd2ddef50cdb..7a7b73199e35fa90169ba3b248651c1136410605 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 464eecd307d21207021e70d0d7c74a7695376e52..9692a8c90030b5d3e0b89456f2b465f9ec436468 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 2b10adb202283d0b47c18506aaf3559faba06901..7c69f85acf86eed7874732129fa01cf16eb19b7a 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 704a2cf248156821da6f194428018c3b48075f07..39dad90ece4e572800130bf1b24abf59aa0f1bbf 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 a9bac1908b5e5ce90eeec4ad0d9f976999a31227..6fec3d7a1cd967bd658fe5c18af5bd8ecc95436f 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 ae60ab825fac9bce7de85a011f98781bfabae937..3484796dd9c35da31a29e8894468085dbceb481d 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 20df486f486a0fcf58cd6853e55d53262e7a62da..6208ff4aaa221442d754a539a5c2ab1571664dc6 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 ed45c2d5d6b80d46c9d0a7455ff3621506bf14c1..f891dca71cc12a6d30cfeebba6707a48fda476da 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 7a7b73199e35fa90169ba3b248651c1136410605..73420e213a9337b38b3067842b3b8dff43de82b1 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 open_item( + &mut self, + item_view_to_open: Box, + cx: &mut ViewContext, + ) { + self.add_item(None, item_view_to_open.boxed_clone(), cx); } - pub fn add_item_view( + pub(crate) fn add_item( &mut self, - mut item_view: Box, + project_entry_id: Option, + mut item: Box, cx: &mut ViewContext, ) { - item_view.added_to_pane(cx); + 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 9692a8c90030b5d3e0b89456f2b465f9ec436468..d11e773c3fef0b604c7a67cdd29ae8bfc75818c4 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 7c69f85acf86eed7874732129fa01cf16eb19b7a..dbc8fc3e82c87729757d6de2219d8811a5841231 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 39dad90ece4e572800130bf1b24abf59aa0f1bbf..5a5a7cd5e6daf65cfc55c0ce9a5ec5413a964807 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 6fec3d7a1cd967bd658fe5c18af5bd8ecc95436f..8946328b1a3deb18579ce0bd6e6442c07a1c8f75 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 4340cd5a1b7bb30bf05e40c5e0bf6ab1de88a6b5..0dbb336007d4cd0858140c95b212c193798dc077 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 a479e5fba1c56c5e543e125be3f523c7c61acc84..9831c3aa3473ff36c41ace5cdbf63594475a7978 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 6208ff4aaa221442d754a539a5c2ab1571664dc6..7644b2f55db1c77cde95421e88ea50d2ec7a5f61 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 f891dca71cc12a6d30cfeebba6707a48fda476da..e70957740f72707d3fe6b443dd0cc4ff2e188a15 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 73420e213a9337b38b3067842b3b8dff43de82b1..e8cbd185f6fdc01581d5eff201085e2e05513f63 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 d11e773c3fef0b604c7a67cdd29ae8bfc75818c4..33415e2360bdb281f756cfd6d30effc611b84387 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 85960002469249d94503954f4775d4e4e7a23851..190a27973737cbb0ecd2be1eea1633c8bf90c0c3 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 35610854f30fd272a8763f6ecc8d016381e12baf..f42e57c35d678570ca9ae559e0318d3b8356920e 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 20da4e4800b6547b3b6a26096a3a865b7aca08b3..99f02119ce5da060fe45780cd0cb2a97ccff2e9e 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 dbc8fc3e82c87729757d6de2219d8811a5841231..4eab6984039223c5e7a83df232df4843576e71e4 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 13d90f01190ea0eeddf14585193f22097988b326..1b4d2a8147818a07ef5b9c2a2255a934dee44d1f 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 5a5a7cd5e6daf65cfc55c0ce9a5ec5413a964807..56734dd3001a66fd74af9f0a342edef464b151a7 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 8946328b1a3deb18579ce0bd6e6442c07a1c8f75..9c69078dc34b45934e52f9022cbee08a697c28ed 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 24b438c7e465deef2ff6963fa1faa56af790a6dd..41b3d188bbd0e82d130796ffb319bd9ffe790376 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 7644b2f55db1c77cde95421e88ea50d2ec7a5f61..acf7b2a23405201e94a06a976716649947af89e8 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 db160ab06fed41d4974d7fd83721f58acb415a19..1b8b89f6910c486e2d606a2a30e53a718e31f971 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 e8cbd185f6fdc01581d5eff201085e2e05513f63..552d102729580f04637e00da9167aef1ed62c4bd 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 c387fbe07064ccc136e839f4d8c07a57b1962d84..782e51c706535fbe260f7e827d687449665cdf8f 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 33415e2360bdb281f756cfd6d30effc611b84387..4dac4d22547df0a526cf36b837bd84ea8d61beac 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 99f02119ce5da060fe45780cd0cb2a97ccff2e9e..26b4150e240d9e182bc90a18dcf09a3fe38402f0 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 9c69078dc34b45934e52f9022cbee08a697c28ed..d4e779484b2436507b3379031a3a96485c5b6d07 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 4dac4d22547df0a526cf36b837bd84ea8d61beac..e34cf1bff64d6c6ecbf5321b4c308ecd26a37543 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 56734dd3001a66fd74af9f0a342edef464b151a7..131b16e8b7cd3a0c6f9759549592110e806aa112 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 e34cf1bff64d6c6ecbf5321b4c308ecd26a37543..e10073c04aef840fa52d338f4f335af0b1b4c26f 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 4eab6984039223c5e7a83df232df4843576e71e4..f6c00a1fd5dbe4fe846054ef999f4cc901c9537a 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 131b16e8b7cd3a0c6f9759549592110e806aa112..0b73ebdf63a3bd56e678f6ee75f8f97f4b05b69d 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 3484796dd9c35da31a29e8894468085dbceb481d..ff535afafb06cf952c19cf92d700a388bbcbc3f6 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 e70957740f72707d3fe6b443dd0cc4ff2e188a15..d70bfe2c2161e6dbfe6b68869a47cd6cdd32fe63 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 e10073c04aef840fa52d338f4f335af0b1b4c26f..92fcacaf85f2f72df3da40ec9f5937d171a03362 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 552d102729580f04637e00da9167aef1ed62c4bd..4ee9eb97ae6a3bef77bdc5387dede2db9597098f 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 ceeddc599a0d3d90b0328e2d6d94148b98e42002..bed338d3f43c3ab2e3bbd311b237eed8a015ba34 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 b5740a5ed75bde0fdfd7233133a4afd025c2f80e..b8b5b3a361bfa548372b81d982094ab2e0cf19ad 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 f6c00a1fd5dbe4fe846054ef999f4cc901c9537a..a188761340c518f29a72f068c161c6c36ad21c6c 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 1b4d2a8147818a07ef5b9c2a2255a934dee44d1f..690c5100ecf9ec3a8edf89414a2ef545f868201a 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 0b73ebdf63a3bd56e678f6ee75f8f97f4b05b69d..67d7d7cd90f12fecf97773e4a370d8beb1ec47f9 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 ad8057db4e8421b4729d653d844f4706484b98b8..49d800d619829b4e691095bcc5e588712691c5da 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 d4e779484b2436507b3379031a3a96485c5b6d07..60dd8ae303861f823d63c59e771df96c07b814fd 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 0dbb336007d4cd0858140c95b212c193798dc077..808f7e47031b9c08ba79e89330a990a2c8ed5440 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 50bae39ae9af73b5b8a82596129deec652dfd776..f2dd4e76b1dc414bd6df9113ae4e61da3524e6f1 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 9831c3aa3473ff36c41ace5cdbf63594475a7978..097f9d7199fea07871db8522e0c2e1900bcdbb4c 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 2b4bafa479de3e8b70ac1ded33a7a1f9d4454060..fd4c8ff60b5d660d199e888596cd70649ae478a8 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 372f0c169bb06602ebbfc4bbb2f7e576ca4d051e..0dd6b08bacc68bf888e9348636ec0b66669a509c 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 29808ff7d9df5b428f7299493b88a13cece8e8eb..d79b2dedaeadc0e85a3469ca3e7e03b7e258e589 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 41b3d188bbd0e82d130796ffb319bd9ffe790376..9c4f67aae32a79166e330e9365a46f25e7484cda 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 acf7b2a23405201e94a06a976716649947af89e8..6e402ad4a3ceb6b1e77a5e06baaa73d08dadcf03 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 d70bfe2c2161e6dbfe6b68869a47cd6cdd32fe63..7f1b9f90ad7c0bd710fa79b075e78640ff279da1 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 c3199bc20bf24153de96ef98f24da8b97d6874ed..f879940f219f266a1f3108e0f570948cb9ef584e 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 1b8b89f6910c486e2d606a2a30e53a718e31f971..a12f81857f1d1b2ed4d44aa01c6283da67f2c8c2 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 4ee9eb97ae6a3bef77bdc5387dede2db9597098f..c388a16c2de6e9d100370d5f86ac8c24f5b58a4a 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 782e51c706535fbe260f7e827d687449665cdf8f..4d00591787c9cf88e20ab9a60c705a73250421ee 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 92fcacaf85f2f72df3da40ec9f5937d171a03362..6481e2c8eeabe5e25991eefc2abe26669732823a 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 190a27973737cbb0ecd2be1eea1633c8bf90c0c3..cf149b2469d5c66ebecfaca239decda7cca01e2f 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 f42e57c35d678570ca9ae559e0318d3b8356920e..5b3bb41c1523bf910a523766f6af2c9a631d264d 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 26b4150e240d9e182bc90a18dcf09a3fe38402f0..2b61279a2cf4fa29e85eac14d51817e27da55b24 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 a188761340c518f29a72f068c161c6c36ad21c6c..a1dd7be744ae4c2f79ee78075f620c60595ea640 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 67d7d7cd90f12fecf97773e4a370d8beb1ec47f9..aa2c09519c0d3f7aec1ac68edbe1fb3c8913d4e1 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 9c4f67aae32a79166e330e9365a46f25e7484cda..8eae666c454ea2870301f4238cfd3e137da3fd9a 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 6e402ad4a3ceb6b1e77a5e06baaa73d08dadcf03..ef8ff3611ae675df8f7f86969b3c8adeaa9161cc 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 7f1b9f90ad7c0bd710fa79b075e78640ff279da1..7e9bb38021809685565033e5c007e07f4c2cf97a 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 aa2c09519c0d3f7aec1ac68edbe1fb3c8913d4e1..40d589bea71c6894422fd4cc985a9d3f9d0034d7 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 60dd8ae303861f823d63c59e771df96c07b814fd..6f083f00a91eb1c18504be4e3c3efcd17f8ac866 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 ff535afafb06cf952c19cf92d700a388bbcbc3f6..c999b8ce4eb93228769fa782785af8e6b1b0f571 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 d79b2dedaeadc0e85a3469ca3e7e03b7e258e589..27e125a59205d39c9b3a647103ea7ada1fceb8a8 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 c388a16c2de6e9d100370d5f86ac8c24f5b58a4a..57a74b52ef405bcbe0457536caa852187cfe8c2c 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 6481e2c8eeabe5e25991eefc2abe26669732823a..d220dbc0fac755d2620577246f9b28bc6dcf895a 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 28c433c43f85e7df2de479d96eaf99b3d1a453d0..4cfb831a58cd49ea148afd07382d7fd983795e58 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 40d589bea71c6894422fd4cc985a9d3f9d0034d7..30888d8a408ada78170e6dd6a39979392e085fe4 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 c5300dbcd9f47279aac127315e2d03f354aaae04..b946ea48fbbf58a7196f56949ececfe23fa1fd75 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 808f7e47031b9c08ba79e89330a990a2c8ed5440..ca41eb74a11cb2d08103c22e90eebff3ccef7d53 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 097f9d7199fea07871db8522e0c2e1900bcdbb4c..5488d31416368a19f91f88c1d3bc51e2e7a6c264 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1364,12 +1364,38 @@ 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::(); + 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); + 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::(); @@ -1377,7 +1403,7 @@ impl MutableAppContext { .cx .globals .remove(&type_id) - .expect("no app state has been added for this type"); + .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 c999b8ce4eb93228769fa782785af8e6b1b0f571..317cf1ba028d19b920d04c4a70094adddb932b97 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 d220dbc0fac755d2620577246f9b28bc6dcf895a..33155b5d4f4710338d6c4d4570af129f21e92624 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 317cf1ba028d19b920d04c4a70094adddb932b97..c44364adac96dc0eaa0243b8014d240ac68b5a5f 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 d0cb621ab92457adf9942a5cc1287f17d2572f99..3cc5a91cbeff89d906272b4e445a2eabb84e2749 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 54b26b830ca584f74110c74e5b18d3371d092c18..230db3119c3ff76b6a7f2897d8c0a32712156003 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 7e9bb38021809685565033e5c007e07f4c2cf97a..948901c2a10c3141be12c3885492bdea0520c2fc 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 30888d8a408ada78170e6dd6a39979392e085fe4..1ae9fdce27a9613c92ad6e2e1184e2de1304ef87 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 33155b5d4f4710338d6c4d4570af129f21e92624..df771a700a84e0c63065a35032cf92d4634103c7 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 3cc5a91cbeff89d906272b4e445a2eabb84e2749..d7a928e424e469b7abb63c288d18e7bd8ee2e19b 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 230db3119c3ff76b6a7f2897d8c0a32712156003..39a0d669d5747673e5640f1939d57351ddadf4fe 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 57a74b52ef405bcbe0457536caa852187cfe8c2c..ec27d2400ed0c3b7a86f68cbc6e22c45e36de892 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 df771a700a84e0c63065a35032cf92d4634103c7..80f5b4e7a1e579fa713f706e09833b6986be3e3f 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 a1dd7be744ae4c2f79ee78075f620c60595ea640..7bee815ad92523ef84f768c1686fe110fd0e8564 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 6f083f00a91eb1c18504be4e3c3efcd17f8ac866..d865511a62eb6b06105a3624e333db4d5f8f43c6 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 ef8ff3611ae675df8f7f86969b3c8adeaa9161cc..f027c965c631f0e9f83d5ba49d82eff28366004d 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 ec27d2400ed0c3b7a86f68cbc6e22c45e36de892..c3b6a5fd29b4742097cee6412dffb44c946d866a 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 80f5b4e7a1e579fa713f706e09833b6986be3e3f..85672db11beb3a37d953932c81a1bbaffefb05c6 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 948901c2a10c3141be12c3885492bdea0520c2fc..ce3cfff64661b409d621e22358539cda13c63627 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 85672db11beb3a37d953932c81a1bbaffefb05c6..b80372a98106ffd0a69bcf1236e400664571cad1 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 5488d31416368a19f91f88c1d3bc51e2e7a6c264..a4dcd52f5ad01fea3a4f843b586cb42800d30025 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 88ccb694d2c31896467837010e944bc8296454b8..b4e419107a32dc14ddd80c9bbf5ba2365ec29aa8 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 4cfb831a58cd49ea148afd07382d7fd983795e58..bd93ae3e274cd9b02267e2e2da4e7840e5110281 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 18a0e156db6b881cf0c6a0a8e779c573d1c34f40..ac235dc19e0eb7d4f30d2804b277dcb672fe9a1a 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 59110f73c643cfac93730a0ef5e1900d896397eb..b56017c78924f8828b3a379bceeb616d679cee62 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 609e92af0fdd8531c00a9068521c617c0888a3e6..02069fb6108dc4f0a54254594b2d53e1c2032311 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 d865511a62eb6b06105a3624e333db4d5f8f43c6..522c490cfa74bde9c98c1ab5fabe122ae566083f 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 c44364adac96dc0eaa0243b8014d240ac68b5a5f..4e2abbc2fe0b1b080cbf12243333bcca38898779 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 ce3cfff64661b409d621e22358539cda13c63627..23f7e1c182cd836a85df4bfa0c7f60f7e815dec9 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 b80372a98106ffd0a69bcf1236e400664571cad1..25159fa6896934a8a263a17e79dd91e9a31f3d69 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 cf149b2469d5c66ebecfaca239decda7cca01e2f..05437834942508a63ad067b3bfcbb9e57f4a8f35 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 2b61279a2cf4fa29e85eac14d51817e27da55b24..c33a96a94b8fb0efa030e6d82b38cb32deaa9c54 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 b56017c78924f8828b3a379bceeb616d679cee62..c2527ed94a6a5d5d44a15318fd92a78f9ca1433f 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 1ae9fdce27a9613c92ad6e2e1184e2de1304ef87..00aa8283254a8465b11b99385dbf570fc4ec5715 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 522c490cfa74bde9c98c1ab5fabe122ae566083f..2c454dedb5ccc70cea74d0f80e4d08f3c7a2dd97 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 4a22d6ce5af061ae248e00788d0d4aa5af62a67e..09d4236afe281f9c8983896993689fc777c74465 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 4e2abbc2fe0b1b080cbf12243333bcca38898779..4a6f0dd6cff8f9830d0da0b86c6f0794c8a4d3ba 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 d7a928e424e469b7abb63c288d18e7bd8ee2e19b..f2197055f3229c5faf2a069e7e0ecc3167b376ca 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 23f7e1c182cd836a85df4bfa0c7f60f7e815dec9..785de7ea089d9ae427026057bce1ff0ccd12d952 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 4e7d6f52367094fa5bbf07cd8f55250259bd7601..0c73c7388d0494e75cf2793a975606d3a90d0706 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 c3b6a5fd29b4742097cee6412dffb44c946d866a..997aae3d96379c42f2e1d6412af6305c4d41e372 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 25159fa6896934a8a263a17e79dd91e9a31f3d69..eba7f12981d59bc69aac67130b492b8fa42fe4fa 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 | 139 +++++++++++++++++++++--------- 5 files changed, 145 insertions(+), 44 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 2c454dedb5ccc70cea74d0f80e4d08f3c7a2dd97..98610c4a07fa035d40d8813a8c67606b364c2371 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)), }) } -} -impl Item for Editor { - fn as_followed(&self) -> Option<&dyn FollowedItem> { - Some(self) + 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 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 a4dcd52f5ad01fea3a4f843b586cb42800d30025..f8f505ee783fe274e9f4b182bbbe68c4d34cc760 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 785de7ea089d9ae427026057bce1ff0ccd12d952..7668533a35e75fe60845662e0d73d87795dbfe5e 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 997aae3d96379c42f2e1d6412af6305c4d41e372..43aa1f54a11e4b16b7831c87e51d2f8c2325ef71 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 eba7f12981d59bc69aac67130b492b8fa42fe4fa..6f013d910f3525986b49de5c3b36a7cb8ef7868e 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 43aa1f54a11e4b16b7831c87e51d2f8c2325ef71..8112fa7af41cc67360e2594d2f7048fd5cfa9aff 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 6f013d910f3525986b49de5c3b36a7cb8ef7868e..5955f81e62d41eb7cfd93e554d4649e5f56f2ab8 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 98610c4a07fa035d40d8813a8c67606b364c2371..7d89341c0d339ee35875ed772955330fdfa1747d 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 f2197055f3229c5faf2a069e7e0ecc3167b376ca..1ea340278ac962d7631df5690e70c1f4819d9fee 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 7668533a35e75fe60845662e0d73d87795dbfe5e..25857c53e48cd79250c93e3f80f89c1e3c0b81e5 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 8112fa7af41cc67360e2594d2f7048fd5cfa9aff..a2e49f7f481d65825f93d3ebfad5f2b110416e87 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 5955f81e62d41eb7cfd93e554d4649e5f56f2ab8..3cbd167319d06e364c62b0fe6f31b1d1171adf05 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 00aa8283254a8465b11b99385dbf570fc4ec5715..3f1c9c5fd2aacbf53f6687c280b902cc03120d64 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 7d89341c0d339ee35875ed772955330fdfa1747d..410ffcddca38f4127605023689eec593d5a066d9 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 3cbd167319d06e364c62b0fe6f31b1d1171adf05..b011566c0fbe93a7065b896c0a8c402068967e27 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 b011566c0fbe93a7065b896c0a8c402068967e27..ca1e733fe3ea9a7bb9cbc1353d58fd43a4115a78 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 3f1c9c5fd2aacbf53f6687c280b902cc03120d64..aa4162402943f6ad08c8cba0d4425d279e4a9f31 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 410ffcddca38f4127605023689eec593d5a066d9..5ef739510b6047d212ffe04127a8a945648eeb5b 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 e145488d650cb49d44d6b3de3af9e6304f02609e..12493d6728ac213cd0d10e4364e85b282899ef36 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 09d4236afe281f9c8983896993689fc777c74465..deedf3e88b596209457b5b50087e4655cb968a10 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 1ea340278ac962d7631df5690e70c1f4819d9fee..f0743c5b4fa02ba4952051eb7a76cd8c004dd058 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 ca1e733fe3ea9a7bb9cbc1353d58fd43a4115a78..386eba66eb76290b70a53299eb32834faaff7248 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 c2527ed94a6a5d5d44a15318fd92a78f9ca1433f..d4a5a47b4b509fab8af4eabc9e632a2d952c58e1 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 5ef739510b6047d212ffe04127a8a945648eeb5b..d4163ce353eda5d54afac069d86dc833de9f4d61 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 386eba66eb76290b70a53299eb32834faaff7248..2e84f6c55cb2b5b42f2439257545c55231f4f203 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 | 233 ++++++++++++++++-------------- 3 files changed, 133 insertions(+), 113 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f8f505ee783fe274e9f4b182bbbe68c4d34cc760..80fc36cba3d2d758fb3898218985e4dc3efd6c65 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 25857c53e48cd79250c93e3f80f89c1e3c0b81e5..daa817b670adbd636320b875b3ea2ed484bc7cde 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 2e84f6c55cb2b5b42f2439257545c55231f4f203..7b893c566678db14b51a10120be2509ee4d18f2b 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(); - } + 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"))) - } + 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 | 247 +++++++++++++++++------------- 4 files changed, 151 insertions(+), 110 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index d4163ce353eda5d54afac069d86dc833de9f4d61..f3f00ee01605a6d26acd2e792d687bd6046cabda 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 daa817b670adbd636320b875b3ea2ed484bc7cde..5585b04ad21c07219e8c34cdd63eb8b2c63100a8 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 a2e49f7f481d65825f93d3ebfad5f2b110416e87..367cc967fca85baf5a111341c9e4a914f3139055 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 7b893c566678db14b51a10120be2509ee4d18f2b..c1da70193186874b10d7d7c5f2718248bbc741a7 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_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(), - }, - ); - }); + this.follower_state(leader_id)?.active_view_id = response.active_view_id; + this.leader_updated(leader_id, cx); + Ok::<_, anyhow::Error>(()) + })?; + } + Ok(()) + })) + } - 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 { + 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) { + 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) + 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), } - } - proto::update_followers::Variant::CreateView(_) => todo!(), + 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 aa4162402943f6ad08c8cba0d4425d279e4a9f31..b3d74d03a7fb66821208fffdbf4d7e140590489e 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 f3f00ee01605a6d26acd2e792d687bd6046cabda..1ce59dd93fe8df9ad977883655b08538aef31598 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 33155b5d4f4710338d6c4d4570af129f21e92624..9b9477353b5a93fe148269153695023be50f0b9e 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 b3d74d03a7fb66821208fffdbf4d7e140590489e..0db67e73e79394f950924b81f247231e91883a45 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 1ce59dd93fe8df9ad977883655b08538aef31598..7f28af0c40c7715d9c5599c780a8576b83bbb221 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 c1da70193186874b10d7d7c5f2718248bbc741a7..748b0ced6140b346281ff1ee507a19a0451b42b6 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 | 318 ++++++++++++++++++------------ 3 files changed, 235 insertions(+), 124 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 80fc36cba3d2d758fb3898218985e4dc3efd6c65..41d3bf2cdd18e1fcfe422879ba86bbfee1b1ece5 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 5585b04ad21c07219e8c34cdd63eb8b2c63100a8..044f42368207870bdeaf89d56e87eda49969be35 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 748b0ced6140b346281ff1ee507a19a0451b42b6..5b3a21f7e893ad42a7bf64c9db75ce7835e79c65 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()); + 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); + } } - } - } - 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)); + 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(); } } - } - }); - 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(); + cx.notify(); + } } 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 - .get_mut(&leader_id) - .ok_or_else(|| anyhow!("received follow update for an unfollowed peer")) - } + 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()); - 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 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)); - 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); + 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) + .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 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())); } - 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 044f42368207870bdeaf89d56e87eda49969be35..fe56cea2e299c763fc29b9605ebd5c1015728a3a 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 5b3a21f7e893ad42a7bf64c9db75ce7835e79c65..760ffa8c1a8d84a954e3e9ac597e7af68fc12b15 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 760ffa8c1a8d84a954e3e9ac597e7af68fc12b15..3c5d1dfbf6412c9ad8703622dcf9d1f89077b5b3 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 7f28af0c40c7715d9c5599c780a8576b83bbb221..fee7cc445e91984c0facac81c81c14289890b9dc 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 f0743c5b4fa02ba4952051eb7a76cd8c004dd058..dbf1218ccd0f90e5c537160303561e7717287993 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 fe56cea2e299c763fc29b9605ebd5c1015728a3a..771d82e0173b2f07d27e481380b5a3bdfe5a2e70 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 3c5d1dfbf6412c9ad8703622dcf9d1f89077b5b3..78a59e9d91daaa8d0c4e216e65fe69a69eb0f0fb 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 0db67e73e79394f950924b81f247231e91883a45..80ed9bc51219d7657d640a35ddf989693e0f9186 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 fee7cc445e91984c0facac81c81c14289890b9dc..3fdaad8b8f462369a046a15034c7b3240a830c4a 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 a4fee82805845fc6aed39de37a441d0a66e55c02..fa4208ee5b42d09ae9fb96461ca604635e6e4e7e 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 771d82e0173b2f07d27e481380b5a3bdfe5a2e70..c6627ce7b88b6cc949155d87ddbe6eeaadb597c9 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 78a59e9d91daaa8d0c4e216e65fe69a69eb0f0fb..1314b53e636dd6f15995f29d6bee99bc7000cf3c 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 30888d8a408ada78170e6dd6a39979392e085fe4..c3858b434993820b6458825532d52ec8b09b734c 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 5951e2c20db8b9f675f0d6a25afb7e5590e8dc7b..a090d8dde2420c5073e5786847b446e768ebd149 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 a090d8dde2420c5073e5786847b446e768ebd149..9c87ca301675f5c580df969197681b09328088d4 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 e145488d650cb49d44d6b3de3af9e6304f02609e..715548f8135bc05568873da6b782a3bfdf2d1112 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 a4fee82805845fc6aed39de37a441d0a66e55c02..763d08c05388b9a3986798030e9f027dd8fa40cc 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 9c87ca301675f5c580df969197681b09328088d4..b5bafebe67e21552963fd2b567c1aab35a199258 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 b5bafebe67e21552963fd2b567c1aab35a199258..fe66a23c3dcbfecabfd804ee806ff9ea79667e64 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -132,64 +132,84 @@ 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; - } - - prev_char_kind = char_kind; - column += c.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 let Some(prev_ch) = prev_ch { + if is_boundary(prev_ch, ch) { + *boundary.column_mut() = column; } } - 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); + prev_ch = Some(ch); + column += ch.len_utf8() as u32; } - map.clip_point(point, Bias::Right) + boundary } pub fn is_inside_word(map: &DisplaySnapshot, point: DisplayPoint) -> bool { @@ -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 5e8e799b4c58fd9d073ff078ad19fbb6db0af53d..61d0bf3f67e3799b4a3c8364b9eea9815f210bb2 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 2b56a023fc2a7dbc423b177503c7117e6c02b044..c3f4d2d3a6e8a5086a5c78d183a47fbbfe940551 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 1314b53e636dd6f15995f29d6bee99bc7000cf3c..387bfa9b6eff2b37403c3cf5df26d2476988711c 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 76547967bfb8cc34cec0fae1307250e57c08b6f4..d0368d69338911a66669b9f9f88c0c107dc95ac8 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 fe66a23c3dcbfecabfd804ee806ff9ea79667e64..c8f217b57e0ff12d58174862287955c149c0e1d5 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 c8f217b57e0ff12d58174862287955c149c0e1d5..72567312e1accbdd13f304433ff6ab87073e9a81 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 dbadbb386eb2b5fed57f81d1a5bfc83d264ab04d..17209e5c74420a421d49c5e62a445661c44f8336 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 | 176 ++++++++++++++++++++++++---------- 1 file changed, 125 insertions(+), 51 deletions(-) diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 72567312e1accbdd13f304433ff6ab87073e9a81..5e6dd8c55cfa6a325b010d926454388c5c10dfc8 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; + 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; + } } - } - 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 = None; + let mut prev_ch = if point.is_zero() { None } else { Some('\n') }; + for ch in map.chars_at(point) { + if point >= end { + break; + } - 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 = Some(point); + } + } - if let Some(prev_ch) = prev_ch { - if is_boundary(prev_ch, ch) { - *boundary.column_mut() = column; + if ch == '\n' { + break; } + + prev_ch = Some(ch); + *point.column_mut() += ch.len_utf8() as u32; } - prev_ch = Some(ch); - column += 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; + } } - boundary } +/// 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 start: DisplayPoint, - is_boundary: impl Fn(char, char) -> bool, + mut point: DisplayPoint, + mut is_boundary: impl FnMut(char, char) -> bool, ) -> DisplayPoint { let mut prev_ch = None; - for ch in map.chars_at(start) { + for ch in map.chars_at(point) { 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; + *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 80ed9bc51219d7657d640a35ddf989693e0f9186..8380eea12d9e3c59f821329304c03cc83205087d 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 49d800d619829b4e691095bcc5e588712691c5da..af02a353d4c2baa8038524d115c1fe887c11732a 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 3fdaad8b8f462369a046a15034c7b3240a830c4a..7afc79f8b384a15c8a9111c24e0e11629730970e 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 dbf1218ccd0f90e5c537160303561e7717287993..487c7e01a5a389689becbf7819572ba16566031e 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 c6627ce7b88b6cc949155d87ddbe6eeaadb597c9..e0f4147faf46b9f5f3ee63becdaac2124bf024ff 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 7afc79f8b384a15c8a9111c24e0e11629730970e..d0457eba5adb3b4a18f240bf8141a87c5719276e 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 387bfa9b6eff2b37403c3cf5df26d2476988711c..5ce61824e35a1f2fa13accf7ec430d6515b08d10 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 8380eea12d9e3c59f821329304c03cc83205087d..bffc1a2186ad8ad7517a1fdd0e5feeb9dcff9c93 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 d0457eba5adb3b4a18f240bf8141a87c5719276e..eceaa8815967bb13e35006c840abdcd721c64c78 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 ca41eb74a11cb2d08103c22e90eebff3ccef7d53..4656daa4b3c1adbcf9cda5fd92428240c46ae13a 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 f2dd4e76b1dc414bd6df9113ae4e61da3524e6f1..ce8ba787a827b5daacb400334a91f6ef2967fcce 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 fa4208ee5b42d09ae9fb96461ca604635e6e4e7e..9da9e59e4cd55bd63241d21a412e9711d6a65d90 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 6c9980b334ac3c3c9a8861baf902d47f11bc4788..d36771c44fb302aead93c2eb74df3d0d574ff526 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 fd4c8ff60b5d660d199e888596cd70649ae478a8..968fceb59c5c8eccd05961164fefd6529c617eef 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 4a6f0dd6cff8f9830d0da0b86c6f0794c8a4d3ba..5f9a63c034d88711577aafad022584078504407a 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 27e125a59205d39c9b3a647103ea7ada1fceb8a8..5eb04718d7cf604551fd8668a3fd630f76077b53 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 8eae666c454ea2870301f4238cfd3e137da3fd9a..13c73036f4dcc0a5c6c90557221e6d7b9ee6bdfb 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 f027c965c631f0e9f83d5ba49d82eff28366004d..1302040d19c3c02606db7995d47eac6b147680a2 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 e0f4147faf46b9f5f3ee63becdaac2124bf024ff..761b5737ce86b27c8d1da548ef0fff7bfedba94b 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 f879940f219f266a1f3108e0f570948cb9ef584e..ebdcc492a9f95af0232ef93b8ab934668d634c97 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 5ce61824e35a1f2fa13accf7ec430d6515b08d10..cb9f7e7fed92b6c7b62433d05c11f93dcd05b3bf 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 761b5737ce86b27c8d1da548ef0fff7bfedba94b..047912c0ffd951890bfba11a7e37ed88525aead8 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 367cc967fca85baf5a111341c9e4a914f3139055..ab54b0053e461bdd9111d78ab941add7c40b4b3b 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 cb9f7e7fed92b6c7b62433d05c11f93dcd05b3bf..9c8da079ae1af0817eaebe0548129d7b160be5ad 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 9c8da079ae1af0817eaebe0548129d7b160be5ad..35f3ff9a0cf6519157b16b32fb2dd49cd820df40 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 af02a353d4c2baa8038524d115c1fe887c11732a..0daf8f2fc25d821ed5acb041a9478bc0c15894e2 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 eceaa8815967bb13e35006c840abdcd721c64c78..3f8bb2eab3412719f2efee6da08ee5f946b32d10 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 047912c0ffd951890bfba11a7e37ed88525aead8..3f149390d35e91b751cda9eeab5c706e2ed11109 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 35f3ff9a0cf6519157b16b32fb2dd49cd820df40..1f0a89a083a924d8b25791dd55da849c8e52dfaa 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 5f9a63c034d88711577aafad022584078504407a..c3c2f4e2a036e52c58b76a8a85d985f33180aa94 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 3f149390d35e91b751cda9eeab5c706e2ed11109..3274e70d61fa612e93366611df196e57a91d5939 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 1f0a89a083a924d8b25791dd55da849c8e52dfaa..353c3f251088bdb0697ec991bbf39f05d607564b 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 e937f6daaf2d9b754ee2b3b6457145a9206ecfd7..cfe780d5118c764a829cf047d288bc1e7a0b590e 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 3274e70d61fa612e93366611df196e57a91d5939..9527df8e1f4634331ae301127d62067872d40c04 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 ab54b0053e461bdd9111d78ab941add7c40b4b3b..9903d414e411b03b3f268e3775266a076b4b06b0 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 41d3bf2cdd18e1fcfe422879ba86bbfee1b1ece5..fcf8c05d0c8746909371a66fd1bea62d9f4fc18c 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 fcf8c05d0c8746909371a66fd1bea62d9f4fc18c..0b3dd26f554ef76d2e2e87e3a29d7d60b1b58584 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 0b3dd26f554ef76d2e2e87e3a29d7d60b1b58584..1e4448de98ef5017845c0d552b20778f456c6a89 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 9527df8e1f4634331ae301127d62067872d40c04..53d1315662ec7cd585be71667d3be9ac06f81cce 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 53d1315662ec7cd585be71667d3be9ac06f81cce..79fb49da809f6d0b36fc51a336cdd3c8fb9aa4b4 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 17209e5c74420a421d49c5e62a445661c44f8336..6f3154113760097e3d0768dd20852eccc184c4a6 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 c3858b434993820b6458825532d52ec8b09b734c..2e68d963c96ad5f18f240fa76e5ea315787dd332 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 5e6dd8c55cfa6a325b010d926454388c5c10dfc8..13f66bb24ba82e9d63e90b4a6abccbd66f8a079e 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 67622db83f94539ff40ec728843799a51628e8aa..4908016ef591f6bb1844fa2707b2ff57221e0326 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 39a0d669d5747673e5640f1939d57351ddadf4fe..59d6773451fd2feebc28b17120e0b50a58de1127 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 61d0bf3f67e3799b4a3c8364b9eea9815f210bb2..d10c282e3526056acd397794b184491bd17a43b6 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 c3f4d2d3a6e8a5086a5c78d183a47fbbfe940551..d34613df4ac19633a9659d59b33580f38d4825ad 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 d0368d69338911a66669b9f9f88c0c107dc95ac8..7bd0c59045fa6f99dee87429d5eef38b327f0137 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 b0e5a63137dcbd6b8a0cdde080b99772f87e9447..cbf9389fd4028213aa8c23ad8183e0419f0f6634 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 05437834942508a63ad067b3bfcbb9e57f4a8f35..61967bfcdfb00738c56d52fa228041a30f9e90ce 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 bffc1a2186ad8ad7517a1fdd0e5feeb9dcff9c93..8d1890e04d488f41c70d51b4238fbbb81bc2eb10 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 3f8bb2eab3412719f2efee6da08ee5f946b32d10..295f7f664c1d679c56d1aad0732b20a91365af40 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 2e68d963c96ad5f18f240fa76e5ea315787dd332..d3e3f5ee5cb35e8c9416b879d62c3db8347d77a2 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 4e7d6f52367094fa5bbf07cd8f55250259bd7601..2262919b5da990266aeac27b3642f618d69e8147 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 d3e3f5ee5cb35e8c9416b879d62c3db8347d77a2..06b419dd9ac0363da01df2c7f53ad597fa8bc2a2 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 295f7f664c1d679c56d1aad0732b20a91365af40..3cac47bf5912d8c969b15c9302c46e248aa1c339 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 79fb49da809f6d0b36fc51a336cdd3c8fb9aa4b4..f8ed78a13f58ae9004122aeb8cff15b30215b2f0 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 8296d10f4a8364656d8c80f6654b73cbf10b9a53..55338130dab81644332e2112a24afbab038c38c4 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 1e4448de98ef5017845c0d552b20778f456c6a89..d5a7fcc6e6fc03830c1ed5da925edb3f2da79a33 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 55338130dab81644332e2112a24afbab038c38c4..2d8891b1e05f44abed00c49cc00c39e0d849d343 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 8d1890e04d488f41c70d51b4238fbbb81bc2eb10..3d763478e5f21073cfdb18a76a6c96b049427481 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 3cac47bf5912d8c969b15c9302c46e248aa1c339..ab5359600d980576d8f3ef9fd249a7c2df78478e 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 487c7e01a5a389689becbf7819572ba16566031e..9d25e66190b14bc7d4624885ec7da3dc57acb61b 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 3d763478e5f21073cfdb18a76a6c96b049427481..7ec32e4714346f90cc079c17f7c95feb598d7865 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 7ec32e4714346f90cc079c17f7c95feb598d7865..e1bba12b420dfe1d64d050988906ad30d5b1d960 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 ab5359600d980576d8f3ef9fd249a7c2df78478e..5971cbc07bb5dfbb87cf826418e546551d60637c 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 d5a7fcc6e6fc03830c1ed5da925edb3f2da79a33..43a2c49263f7337128430b451def87876d389613 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 d3e3f5ee5cb35e8c9416b879d62c3db8347d77a2..4867cf726991f9777288f4299d014cb56756227c 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 2d8891b1e05f44abed00c49cc00c39e0d849d343..b24e316b55fdfca8f6200a40d6d5ab621db6c547 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 b24e316b55fdfca8f6200a40d6d5ab621db6c547..ed822a348758ec29c37459776e26f1b4b0c1720b 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 f8ed78a13f58ae9004122aeb8cff15b30215b2f0..1020fd686a2c38fb7ca0f77ffabd81d21d6c5ada 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 9903d414e411b03b3f268e3775266a076b4b06b0..e04af153ba1d76e6ca48855ea98c397da7bc6ebf 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 ed822a348758ec29c37459776e26f1b4b0c1720b..19208294d75630901ec502bbd06ebcf5b3b1ee91 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 e1bba12b420dfe1d64d050988906ad30d5b1d960..6d4785ccade91dbcf4f58135cdfafcdc6d2557c5 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 d34613df4ac19633a9659d59b33580f38d4825ad..afffec507452ee00bcf4d185abb92ef09a92a9d7 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 19208294d75630901ec502bbd06ebcf5b3b1ee91..691f17ca559deee7eb66c8f4c1f3b3bd0d4a6423 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 4867cf726991f9777288f4299d014cb56756227c..81ebf64b0275f1b0f47fc3f3ed11cf830332e40d 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 43a2c49263f7337128430b451def87876d389613..9b7ca35d5fc98f52c01315235ecfacdd27945a0e 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 49cc4abe6999dee3156fc390afbee8cd7a6f325d..2344d97fe273d03feda349ca498d4efceabf276f 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 5971cbc07bb5dfbb87cf826418e546551d60637c..2cea9f0f9a5be5669e8f3345eacf2bb1245de582 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 9b7ca35d5fc98f52c01315235ecfacdd27945a0e..db216124391468e2ef688013da2372bd774a56d4 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 49cc4abe6999dee3156fc390afbee8cd7a6f325d..1db7cc3e69061dac7a66d241079d36fba58a1c13 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 | 71 +++++++++++++++------------ crates/workspace/src/workspace.rs | 10 ++-- 5 files changed, 62 insertions(+), 45 deletions(-) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 7bee815ad92523ef84f768c1686fe110fd0e8564..a0182902f1ca3af18dbcfddae60eaadc3c780d58 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 5971cbc07bb5dfbb87cf826418e546551d60637c..246a462019b53aeca2b6df0111ed7fbaf5025f1f 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 1302040d19c3c02606db7995d47eac6b147680a2..212e1c66a7d9be5c41e657a2e7d97266eba85a3c 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 e04af153ba1d76e6ca48855ea98c397da7bc6ebf..df30d48dbec6010aa85e1d4e9b86461d39fc01a6 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)?; - - // 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); - } + 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 + .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); + } - pane.active_item_index = index; - pane.focus_active_item(cx); - if let Some(data) = entry.data { - 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() + .paths_by_item + .get(&entry.item.id()) + .cloned() + .map(|project_path| (project_path, entry)); } - 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 691f17ca559deee7eb66c8f4c1f3b3bd0d4a6423..65300c5f343c8c9135214887d8eebdcc2870ad25 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 bed338d3f43c3ab2e3bbd311b237eed8a015ba34..452f041c7b040c58879caccc19859c379a152042 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 d4a5a47b4b509fab8af4eabc9e632a2d952c58e1..db4478f489c07a329871491b13f4a580d8006ef7 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 f630d9c0ee0a08fc76753b39817d3665afeae4fc..35a8e85922b153ce3d997ac8901dd70992adfe9d 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 c3c2f4e2a036e52c58b76a8a85d985f33180aa94..fbd3a1f7a6026a0d60361e33e4d801f0d79a6897 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 1020fd686a2c38fb7ca0f77ffabd81d21d6c5ada..68e435dfb1c2a3aa45b8a95131c52146ff4136a9 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 61967bfcdfb00738c56d52fa228041a30f9e90ce..29dfd314273e20f9142e66dfaf847a634e4a8271 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 29dfd314273e20f9142e66dfaf847a634e4a8271..af0c98506d38480003796c6321c5dea7c5765d1d 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 db4478f489c07a329871491b13f4a580d8006ef7..1bae6cd49e5eb9f5e24b86f6165b7a647c6a30e8 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 af0c98506d38480003796c6321c5dea7c5765d1d..b79f7f1bb0966c5df12a1612b8c33015576e3ddd 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 1db7cc3e69061dac7a66d241079d36fba58a1c13..b396337e013e38243bfad077a5c4930b98fe6412 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 c33a96a94b8fb0efa030e6d82b38cb32deaa9c54..1302d54067810653140367b40966e869edcf8372 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 | 53 ++++++++++++++++++++----------------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2344d97fe273d03feda349ca498d4efceabf276f..4ef88a1d486b3a41c044be28dbadbda9e93bf3e8 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) + { + 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 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 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 bd93ae3e274cd9b02267e2e2da4e7840e5110281..01d398b51ceb9e18ae17fa590a80c14d29ce0859 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 cbf9389fd4028213aa8c23ad8183e0419f0f6634..ca0eec353e2bf9cd41189825189a0fc57cccbf98 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 f32028002a4be8aaa48b7fff1aeff5e8169fa051..28f2c0510d2da95759bfeee9a6ac7c7c6a45db00 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 d36771c44fb302aead93c2eb74df3d0d574ff526..19b1a1cf73c18ada121fb4768ef9da5ba819b794 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 6f3154113760097e3d0768dd20852eccc184c4a6..1f825e175c8301f5d12998785c5dfa4ea9e62ce8 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 ac4477263d4cb015d86274ab881b4e673f747f73..7f744afa406ab490444dbff9b08babc043c38cac 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 6722d2b1fb45c99a210fb4b6b317b7eb54095a4f..ba95d33d39ff99f633f840f6d8e42e2199f7d30d 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 4908016ef591f6bb1844fa2707b2ff57221e0326..566ca9b2dbc70bb1ff69b72c4bc6d3755dc4de76 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 | 845 ++++++++++++++++++------------------ 1 file changed, 422 insertions(+), 423 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6722d2b1fb45c99a210fb4b6b317b7eb54095a4f..372d620ed77f68dbb0bf67604d66a74a020fd354 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1744,134 +1744,135 @@ 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; - - 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::(); - - 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, - ) - }); - } + 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 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(); + + 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, - )); + 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); + 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); + } + 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; } - 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; + 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.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 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(); + }); - 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); - } - let anchor = buffer.anchor_after(cursor); - new_selection.start = anchor.clone(); - new_selection.end = anchor; - new_selection - }, - ) - .collect(); + this.request_autoscroll(Autoscroll::Fit, cx); }); - self.request_autoscroll(Autoscroll::Fit, cx); - self.end_transaction(cx); - #[derive(Default)] struct PendingEdit { indent: u32, @@ -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 a selection ends at the beginning of a line, don't indent + // that last line. + if selection.end.column == 0 { + end_row -= 1; } - 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; + } } - last_indent = Some((row, columns_to_next_tab_stop as u32)); + 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; - } + 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); + 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); + } } } - } - }); + }); - self.update_selections( - self.local_selections::(cx), - Some(Autoscroll::Fit), - cx, - ); - self.end_transaction(cx); + this.update_selections( + this.local_selections::(cx), + Some(Autoscroll::Fit), + 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 372d620ed77f68dbb0bf67604d66a74a020fd354..19812e308357a0b5b2590def20e941d1ef896f0d 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 0e77bfa3aa978866135bf9da4b13b8f047b705fd..9d3a1bd0546eb4c5063db917badb1c135522e2ae 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 4656daa4b3c1adbcf9cda5fd92428240c46ae13a..9f0137ef62a25a15eec50ce9d5f84c7c717c3a8b 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 ce8ba787a827b5daacb400334a91f6ef2967fcce..109d33097d69cf5f4d251451aa634887774145de 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 28f2c0510d2da95759bfeee9a6ac7c7c6a45db00..052ed711b3f3d4eceefaa4e6d21a45ce272c5a66 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 19b1a1cf73c18ada121fb4768ef9da5ba819b794..7ce21955e7139118318d35756f94649edbd9b473 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 968fceb59c5c8eccd05961164fefd6529c617eef..a626ff89c86e4715345db38229717325c1c7b92c 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 fbd3a1f7a6026a0d60361e33e4d801f0d79a6897..2529c976ac15d80cad110b40ab6949ef7b6c6c0e 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 5eb04718d7cf604551fd8668a3fd630f76077b53..34d5306d99e78eaf2c056dd1da968e2853ff2c85 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 13c73036f4dcc0a5c6c90557221e6d7b9ee6bdfb..0b1ed2c35b1054d26f184c451e1d42eca83f51b7 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 ebdcc492a9f95af0232ef93b8ab934668d634c97..725319be419b8e196433590bf673f8bc89b936ac 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 19812e308357a0b5b2590def20e941d1ef896f0d..c4808215d75be1458fa380f64033681cb7e23ca9 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 9d3a1bd0546eb4c5063db917badb1c135522e2ae..f10956c125e3d3cc9bc0eab86432a22942a884af 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 c4808215d75be1458fa380f64033681cb7e23ca9..309666d1d548cd5368c969aa12b2812a11d753b2 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 6f3154113760097e3d0768dd20852eccc184c4a6..807049c75c3555cd13f74473cf086b967db67f51 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 ac4477263d4cb015d86274ab881b4e673f747f73..81da94aca7f402ea98f70715c62d152be13609dc 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 6722d2b1fb45c99a210fb4b6b317b7eb54095a4f..7cc203c1849d751272766f398b8803723d414709 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 7cc203c1849d751272766f398b8803723d414709..715f76a07fd793507ab15385b7997b37d1dba5f8 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 715f76a07fd793507ab15385b7997b37d1dba5f8..0aadc3fab9b19bc864563c4647f28f5b866fcdae 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 13c73036f4dcc0a5c6c90557221e6d7b9ee6bdfb..a1a40c9e391c645b0a9798dc94a7fdbf92d895b6 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 212e1c66a7d9be5c41e657a2e7d97266eba85a3c..d78fcb12b72697df3ebeee81d1e4e5de993581ac 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 81da94aca7f402ea98f70715c62d152be13609dc..586f6e0b04f1547c66b4fe514cead3855555dfe4 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 a0182902f1ca3af18dbcfddae60eaadc3c780d58..69a4d1235f69ddc476c37f335358ab107beb423f 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 ba95d33d39ff99f633f840f6d8e42e2199f7d30d..634851bd82d2bf04f670e1c824ecd1176afcc5e4 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 594e3fafb7581b7635d0a9e34e5617941f48cb42..398f7da99e7f41e8ecb44fb124ebeb3f7bb9d2b9 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 33147ce285f371c97870c1281d25a3015a85fb7b..847a38f5001c90527fca82233244c0a020f42e5d 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 0000000000000000000000000000000000000000..0dfa9ecbfca3bd9e81971725c51813f391b006d4 --- /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 e25551ee3a6b81317aa5c00d90cb70d89cc3954e..017f117bbee67a966d250b76878f06556e8feda7 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 d36771c44fb302aead93c2eb74df3d0d574ff526..3dbe8508a5effb94f810bfbbeb6ebbdb5783ca6f 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 28da998d6770f6881ac65fcb439726ae972d139c..c7de6bc882bfe78e0901f910c40439938b188a6a 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 338a5c0ad1425049a811c43fc81b02281ccbe8d2..8cb9629d0729668c5bd62ac3ffda6808683146b7 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 | 26 ++++++++++-------------- crates/text/src/text.rs | 26 +++++++++++------------- 10 files changed, 34 insertions(+), 63 deletions(-) delete mode 100644 crates/editor/src/repro.rs diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 69a4d1235f69ddc476c37f335358ab107beb423f..61a2e6c9c15a94fb723917361f0e2194ad9cc2be 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 1f825e175c8301f5d12998785c5dfa4ea9e62ce8..aee964ec56534053fb2042acae92aa9812c09838 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 634851bd82d2bf04f670e1c824ecd1176afcc5e4..3fbdf7def188377654b63d2bac1fd521f00df209 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 398f7da99e7f41e8ecb44fb124ebeb3f7bb9d2b9..de735dafa72004afba6b7d8ead09c841a88263c3 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 847a38f5001c90527fca82233244c0a020f42e5d..aeaf184c605c05487e80582e9655a800f959625a 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 0dfa9ecbfca3bd9e81971725c51813f391b006d4..0000000000000000000000000000000000000000 --- 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 017f117bbee67a966d250b76878f06556e8feda7..28b03b5aa03f20dd289857a1c4e3cd501dc84b6e 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 3dbe8508a5effb94f810bfbbeb6ebbdb5783ca6f..7eff3b0d977b9391a1eee07aa1cf4701963098c4 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 c7de6bc882bfe78e0901f910c40439938b188a6a..5438ecd51feddfc38c07e73b195b2cda0f4ba96b 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 fn build_max() -> Self { - Self { - timestamp: clock::Local::MAX, - offset: usize::MAX, - bias: Bias::Right, - } - } + pub const MIN: Self = Self { + timestamp: clock::Local::MIN, + offset: usize::MIN, + bias: Bias::Left, + }; + + 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 8cb9629d0729668c5bd62ac3ffda6808683146b7..b811d08c046c58f8c5d6c020c27c45a430258474 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 61a2e6c9c15a94fb723917361f0e2194ad9cc2be..56de434cf49e73c82ac19eefd7be36bdd3f5c71e 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 489381daef3d7156090847e36d48e4c33b94f6b2..9d1b7a75886b234bbcf54d92a31df9898b568469 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 7f744afa406ab490444dbff9b08babc043c38cac..4b07854c3ea5fa432f8bb86e5aae5517bb78e8df 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 3fbdf7def188377654b63d2bac1fd521f00df209..78b7f9a9f973cf495062ecf3232fae9f31f2c4f8 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 de735dafa72004afba6b7d8ead09c841a88263c3..af98f3d5896d955872193a29730696aa9a80ed17 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 aeaf184c605c05487e80582e9655a800f959625a..df080f074cdd5d1295b6a0e4729939819ec71bb0 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 f32028002a4be8aaa48b7fff1aeff5e8169fa051..4c8341cf76046a5cb14fae76c5f1a5a7ca1384fe 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 28b03b5aa03f20dd289857a1c4e3cd501dc84b6e..490789a8c80c3abe320d54bc12b88ec3529832cc 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 a1e335a0354ffdaaf903ba30c3d32f5d24c90093..9fb4cda8e9a8545b1861ccd9b23327563b06a937 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 5438ecd51feddfc38c07e73b195b2cda0f4ba96b..e642aa45d3ff06ffd326dcf633df1985ee5d7ef0 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 05cf0af6ec52e9006816ec056939c1d2be713ba4..7961dccd569c8380c3bb32e57e9057481e4371fd 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 8e68b2545ce2e54833bdac4022c0f25c32593fb9..080e5f131e3aee3d6e1ff83e59ffbde7f6cee6bd 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 080e5f131e3aee3d6e1ff83e59ffbde7f6cee6bd..63e69f45a7d231c74dd7a13f82b57ac60f19eb8d 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 2529c976ac15d80cad110b40ab6949ef7b6c6c0e..278c1271f0a414376131c4a6dfddb6abf8c4c634 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 | 161 ++++++++++++++++-------------- 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, 96 insertions(+), 83 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 278c1271f0a414376131c4a6dfddb6abf8c4c634..0801043a09aec1845f8fc2a0b4eca438a1215b5b 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()); - } - } - _ => {} - } + *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()); } - - 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, _| { - worktree.as_local_mut().unwrap().unshare(); - }); + for open_buffer in self.opened_buffers.values_mut() { + match open_buffer { + OpenBuffer::Strong(buffer) => { + *open_buffer = OpenBuffer::Weak(buffer.downgrade()); + } + _ => {} } - cx.notify() - }); - Ok(()) - }) + } + + 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 2bc9c3d234c58ceb33ba66a0c6cf3a3b1fc925c7..beacc5a8630346aa7e3e99fe029484e3b48774f2 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 4156c0883d9c897ab2dc64a2d9df8c958cfa5a96..8f0b96174ab9f78f22ac2588db668e65a6d0feaf 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 68e435dfb1c2a3aa45b8a95131c52146ff4136a9..748fee3380d4df3f8331887fb71b9430c923c2b6 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 65300c5f343c8c9135214887d8eebdcc2870ad25..17b0c4b51859b9f42acb2a513be6e3c2b94114ae 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 8f0b96174ab9f78f22ac2588db668e65a6d0feaf..726453bea86968c0215e2d70c31884b02f066275 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 748fee3380d4df3f8331887fb71b9430c923c2b6..458712676391fc245878324157426bb9a3da5e98 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 01d398b51ceb9e18ae17fa590a80c14d29ce0859..65d690db2344b32c0def905822d847087d192bf0 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 901699f7c47b9f7764286d8c4020a4d765593870..d16900ebba270956adedad022a4a496f8a5b82e7 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 db216124391468e2ef688013da2372bd774a56d4..a291ad3c4b4b9d1af8dcb63e3aa1af509823147a 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 05fbd5b74b4d4e4c2a0b0595a75d4859b7fa0106..37223d77d1e74cbf1694e643b7c7dfcb6177713c 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 817fde7d66be00e56169123372a35aa414052e87..23b0d2b3b042cbbee44edc52e3e97d8137c6bdf3 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 0000000000000000000000000000000000000000..a223172749d029216ceb3e1c4e6271549223c25d --- /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 0000000000000000000000000000000000000000..388de1a2cafaf4a0c38822c83025222dca58bb30 --- /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 0000000000000000000000000000000000000000..da9c588888b14de5450fcd89c02692e7aa8b655e --- /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 0000000000000000000000000000000000000000..b07449401c4d700fa339932538fbfc8a50c0a972 --- /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 0000000000000000000000000000000000000000..2438a9fa3d4bc3cc180c70a7418abafa1ddb2a03 --- /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 0000000000000000000000000000000000000000..e80d5f0bc790a7f82a52db4b7cc5cb83951b362e --- /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 0000000000000000000000000000000000000000..76ff216a0234fa67d4e1dac12065c5398f369b97 --- /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 0000000000000000000000000000000000000000..f8c6b3d54d83948c3f19afbcf6c7cc8b948bc6c0 --- /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 e97f7f8920f8f126a41485b612820d3b14e57e88..5ccf8056e674b98953a340645677606bae80cfaf 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 ca0eec353e2bf9cd41189825189a0fc57cccbf98..e541dff42d00b9223ff10b2e3acb269053709e85 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 b79f7f1bb0966c5df12a1612b8c33015576e3ddd..63721346c399ca60ea4f701f330ad78fb61d63e0 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 | 110 ++++++++++++++++++++----------- 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, 141 insertions(+), 103 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee964ec56534053fb2042acae92aa9812c09838..c3fef6e0137ea9f9009deab0b8e8b07e46e21bbb 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) { - use Bias::{Left, Right}; + fn assert(text: &str, shift_right: bool, bias: Bias, cx: &mut gpui::MutableAppContext) { + let (unmarked_snapshot, mut markers) = marked_display_snapshot(text, cx); - let text = "\n'a', 'α',\t'✋',\t'❎', '🍐'\n"; - let display_text = "\n'a', 'α', '✋', '❎', '🍐'\n"; - let buffer = MultiBuffer::build_simple(text, cx); + match bias { + Bias::Left => { + if shift_right { + *markers[1].column_mut() += 1; + } - 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!(unmarked_snapshot.clip_point(markers[1], bias), markers[0]) + } + Bias::Right => { + if shift_right { + *markers[0].column_mut() += 1; + } - 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(), - ), - ] { + 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); + } + + #[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 d16900ebba270956adedad022a4a496f8a5b82e7..1d1910e3863e73a491dba5beae37ff1dfaf7e870 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 0daf8f2fc25d821ed5acb041a9478bc0c15894e2..18f780dacc1ad68d28814a92241163f3fcf27748 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 13f66bb24ba82e9d63e90b4a6abccbd66f8a079e..cf2d772b16627bce0ca40409649856157e19405f 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 566ca9b2dbc70bb1ff69b72c4bc6d3755dc4de76..3c3e22ff3b5232f1b73d0a345434b41f9effee3d 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 da9c588888b14de5450fcd89c02692e7aa8b655e..21dc4984362780984a0b799698a611371cba7907 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 b07449401c4d700fa339932538fbfc8a50c0a972..d87dcb24455c532e970b2142d4b59ff41e80f406 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 e80d5f0bc790a7f82a52db4b7cc5cb83951b362e..87218b3f5f221b88c20692b63885317658c14e63 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 76ff216a0234fa67d4e1dac12065c5398f369b97..1aec087d952dbaceed8277931a68e3d294718539 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 f8c6b3d54d83948c3f19afbcf6c7cc8b948bc6c0..f898809138acb36e8b94cf537916ec2963a0e335 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 65d690db2344b32c0def905822d847087d192bf0..74cfa42bdfec115f19bebce982c53ea72d9b6a10 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 c3fef6e0137ea9f9009deab0b8e8b07e46e21bbb..7aa4cdd891db75bb63c7294db37caa5a2f76d5c3 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 1d1910e3863e73a491dba5beae37ff1dfaf7e870..f45ea0fa2eaf7ca3c1d3e79b836640144c63b9ba 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 3c3e22ff3b5232f1b73d0a345434b41f9effee3d..e80547c9dddcd0827d2fa527b20a84d0c6c9df55 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 71b847df692af9e65bf24223c1bdeae35a923d25..b4cf25274eaac3c66ccbe4b9ea602e976b7abc79 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 a223172749d029216ceb3e1c4e6271549223c25d..28ee7de8729f5fc5a8ba4bba2032ffa9d38ee99e 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 388de1a2cafaf4a0c38822c83025222dca58bb30..2f8a60ef33482dda8d5d3d8ffa20fbee5a74b8da 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 21dc4984362780984a0b799698a611371cba7907..0000000000000000000000000000000000000000 --- 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 d87dcb24455c532e970b2142d4b59ff41e80f406..50fd53b37bd7d0dfefb939914b6a661f07594859 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 1aec087d952dbaceed8277931a68e3d294718539..68ecd4e003ee6b9e18678f2519dcebbcaeb37c05 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 f898809138acb36e8b94cf537916ec2963a0e335..e35ed9a122b6df5295b4c5c4fb1424ebbd37fa51 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 | 152 ++++++++++++++++++++++++++++++------ 1 file changed, 127 insertions(+), 25 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index d841024f8880b44579b1aec0df3f1b06b4150675..f8dc1acbf2fa0f707be7fe114aa16e0ea1c45893 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,27 +3588,55 @@ 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() { @@ -3562,6 +3648,22 @@ impl Editor { }); } + 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_subword_end(map, selection.head()); + selection.set_head(cursor, SelectionGoal::None); + } + }); + this.insert("", cx); + }); + } + pub fn move_to_beginning_of_line( &mut self, _: &MoveToBeginningOfLine, @@ -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 74cfa42bdfec115f19bebce982c53ea72d9b6a10..38ffa70955b57bdef1c42144726845686062ffb3 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 e541dff42d00b9223ff10b2e3acb269053709e85..fc9946b778bc8c74254b49b6fd872e402b515f36 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 e35ed9a122b6df5295b4c5c4fb1424ebbd37fa51..051ff21ce76575e480192024ed4b953ba1ca840e 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 a291ad3c4b4b9d1af8dcb63e3aa1af509823147a..7f0c3ffcdeeb1c9696d94dd2279b090eb29bfcdf 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 2f8a60ef33482dda8d5d3d8ffa20fbee5a74b8da..7e49b473f9de17b5969eabfa4b8e014bf4fb7779 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 50fd53b37bd7d0dfefb939914b6a661f07594859..c027ff2c1f3de1e2d63f824e4c58df5362c6e108 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 87218b3f5f221b88c20692b63885317658c14e63..232a76a030ae42f60ee96218a19e9b506f313392 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 68ecd4e003ee6b9e18678f2519dcebbcaeb37c05..c6905a9c7a51fac60f34df7b584ac9a4d5b31b50 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 c6905a9c7a51fac60f34df7b584ac9a4d5b31b50..26f7e24cf29bc7cbb919fc1c65597b7d826beb1a 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 f9327ee2519d0521fd6fb05b812411b81c9abc3a..b269ce3cd5d43d26a03b4177fcffda4f47c9b6f3 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 a8ae9e4cd7071632fb30fb9086c574c4940f53be..6e729fc62bfe2f0bb35307ed460846f08325d85c 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 0801043a09aec1845f8fc2a0b4eca438a1215b5b..404d867069ec383c24dac33f550ac63306468122 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 458712676391fc245878324157426bb9a3da5e98..374aaf6a7ec6820022c1802757cdabf3b51aa946 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::( + |_, _| async move { + Some(vec![lsp::CodeActionOrCommand::CodeAction( + lsp::CodeAction { + title: "the-code-action".to_string(), + ..Default::default() + }, + )]) + }, + ); - fake_server.handle_request::( - |params, _| { + 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 2089b954fb83376237d692993f731e9ab8f2fb31..2c3a8df8a1468a127589bef5ea8f12ec427b8e28 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 38ffa70955b57bdef1c42144726845686062ffb3..9c3692396461c298a0c73160fb177ee8f8c45850 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 02069fb6108dc4f0a54254594b2d53e1c2032311..77e169b91b2e03648402687722b29ea045c7822b 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 b269ce3cd5d43d26a03b4177fcffda4f47c9b6f3..7c74d36ecb038e5a6715c8eade5b756a45d48037 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 f10956c125e3d3cc9bc0eab86432a22942a884af..79b25f8f60fa2ebe9e9832aac99b68c91b775f6b 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 6e729fc62bfe2f0bb35307ed460846f08325d85c..fad49d2424017eea8f66308d2e061b02c9a41ffe 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 | 49 +++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f9327ee2519d0521fd6fb05b812411b81c9abc3a..7cb50167b698d305326264491f2f6f60973e4cf8 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 7cb50167b698d305326264491f2f6f60973e4cf8..6c0b43f39357398e96877efdc2860c1ceb1b4c55 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 07d425af3ebd4235a178ca479a21052d0d71b043..873586b61e35b6e1c5eebd0728213feb0bc1ebc0 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 f9327ee2519d0521fd6fb05b812411b81c9abc3a..60d6bbc699b1a99af465913b34cd1521717bcc6d 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 60d6bbc699b1a99af465913b34cd1521717bcc6d..40cae9576e632c093cf07a8a106c4d9f89f56c86 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 40cae9576e632c093cf07a8a106c4d9f89f56c86..5c3840fcf99703e26bbb2a4a1faf8f71e8008998 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 | 136 +++++++++++++++++++----------------- 1 file changed, 71 insertions(+), 65 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c0b43f39357398e96877efdc2860c1ceb1b4c55..ca94c6923f5d952db183a990e66adbd3cf232495 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; } - - // 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; - } + if row == selection.end.row { + selection.end.column += columns_to_next_tab_stop as u32; } - 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 5c3840fcf99703e26bbb2a4a1faf8f71e8008998..40a27752cdea8cb54f8fe0538e73f02d51be301b 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 ae2c4655a533695f779953fc7787cc6c3dcab6ce..2bea851ec2fa92605aa32b9f2a2c584a649e7305 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 b4cf25274eaac3c66ccbe4b9ea602e976b7abc79..252383b3477704bd56fe8d98427b38e424a903b4 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 40a27752cdea8cb54f8fe0538e73f02d51be301b..6423a7f72fa7b05560529ee05998195fe34e4869 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 7f771f26e8f15a5b8b1a2b40b008a47757051354..f861b6ac60b1f4fe2c1b8ca83df26fed4abf9d6f 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;