From 3178adefdeab488fb3068c198d8670030e6cf433 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 14:54:05 -0700 Subject: [PATCH 001/142] WIP: Add disclosable channels --- crates/collab_ui/src/collab_panel.rs | 91 ++++++++++++++++++++++++++- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 7 +++ crates/search/src/search.rs | 21 +++---- crates/theme/src/components.rs | 46 +++++++------- 5 files changed, 136 insertions(+), 36 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index c49011b86b..16de60e735 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,6 +8,7 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; +use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -16,7 +17,7 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, }, geometry::{ @@ -1615,6 +1616,10 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) + .component() + .styleable() + .disclosable() + .into_element() .into_any() } @@ -2522,3 +2527,87 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } + +mod components { + + use gpui::{ + elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + Action, Element, + }; + use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, + }; + + #[derive(Clone)] + struct DisclosureStyle { + disclosure: ToggleIconButtonStyle, + spacing: f32, + content: S, + } + + struct Disclosable { + disclosed: bool, + action: Box, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + fn new(disclosed: bool, content: C, action: Box) -> Disclosable { + Disclosable { + disclosed, + content, + action, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + style, + } + } + } + + impl GeneralComponent for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child( + ActionButton::new_dynamic(self.action) + .with_contents(Svg::new("path")) + .toggleable(self.disclosed) + .with_style(self.style.disclosure) + .element(), + ) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child(self.content.with_style(self.style.content).render(v, cx)) + .align_children_center() + .into_any() + } + } + + pub trait DisclosureExt { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable + where + Self: Sized; + } + + impl DisclosureExt for C { + fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) + } + } +} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 03caae8dd9..f7697d6fc1 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -229,6 +229,13 @@ pub trait Element: 'static { { MouseEventHandler::for_child::(self.into_any(), region_id) } + + fn component(self) -> ElementAdapter + where + Self: Sized, + { + ElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e2770c0148..ee4702a6fa 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -50,6 +50,13 @@ pub trait Component { { ComponentAdapter::new(self) } + + fn styleable(self) -> StylableComponentAdapter + where + Self: Sized, + { + StylableComponentAdapter::new(self) + } } impl Component for C { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 8d8c02c8d7..2dc45e3973 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -8,7 +8,9 @@ use gpui::{ pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{action_button::ActionButton, ComponentExt, ToggleIconButtonStyle}; +use theme::components::{ + action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, +}; pub mod buffer_search; mod history; @@ -89,15 +91,12 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic( - self.to_toggle_action(), - format!("Toggle {}", self.label()), - tooltip_style, - ) - .with_contents(theme::components::svg::Svg::new(self.icon())) - .toggleable(active) - .with_style(button_style) - .element() - .into_any() + ActionButton::new_dynamic(self.to_toggle_action()) + .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) + .with_contents(Svg::new(self.icon())) + .toggleable(active) + .with_style(button_style) + .element() + .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fce7ad825c..1e395405cb 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -81,8 +81,7 @@ pub mod action_button { pub struct ActionButton { action: Box, - tooltip: Cow<'static, str>, - tooltip_style: TooltipStyle, + tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, contents: C, style: Interactive, @@ -99,27 +98,27 @@ pub mod action_button { } impl ActionButton<(), ()> { - pub fn new_dynamic( - action: Box, - tooltip: impl Into>, - tooltip_style: TooltipStyle, - ) -> Self { + pub fn new_dynamic(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), style: Interactive::new_blank(), - tooltip: tooltip.into(), - tooltip_style, + tooltip: None, action, } } - pub fn new( - action: A, + pub fn new(action: A) -> Self { + Self::new_dynamic(Box::new(action)) + } + + pub fn with_tooltip( + mut self, tooltip: impl Into>, tooltip_style: TooltipStyle, ) -> Self { - Self::new_dynamic(Box::new(action), tooltip, tooltip_style) + self.tooltip = Some((tooltip.into(), tooltip_style)); + self } pub fn with_contents(self, contents: C) -> ActionButton { @@ -128,7 +127,6 @@ pub mod action_button { tag: self.tag, style: self.style, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, contents, } } @@ -144,7 +142,7 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - tooltip_style: self.tooltip_style, + style, } } @@ -152,7 +150,7 @@ pub mod action_button { impl GeneralComponent for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -180,15 +178,15 @@ pub mod action_button { } }) .with_cursor_style(CursorStyle::PointingHand) - .with_dynamic_tooltip( - self.tag, - 0, - self.tooltip, - Some(self.action), - self.tooltip_style, - cx, - ) - .into_any() + .into_any(); + + if let Some((tooltip, style)) = self.tooltip { + button = button + .with_dynamic_tooltip(self.tag, 0, tooltip, Some(self.action), style, cx) + .into_any() + } + + button } } } From 2d37128693b40b3793d4ca6238e93b5e24c2ec88 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 18 Aug 2023 19:02:27 -0700 Subject: [PATCH 002/142] Actually get it compiling, omg --- crates/collab_ui/src/collab_panel.rs | 38 +++++++----- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 7 +++ crates/gpui/src/elements/component.rs | 85 ++++++++++++++++++++++++--- crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 34 ++++++----- 6 files changed, 131 insertions(+), 41 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 16de60e735..f35c07d5aa 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -8,7 +8,6 @@ use client::{ proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, }; -use components::DisclosureExt; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; @@ -17,8 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, Svg, + Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, + List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, + ParentElement, Stack, Svg, }, geometry::{ rect::RectF, @@ -44,7 +44,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + collab_panel::components::{DisclosureExt, DisclosureStyle}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -1616,10 +1619,17 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .component() - .styleable() - .disclosable() - .into_element() + .dynamic_component() + .stylable() + .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) + .with_style({ + fn style() -> DisclosureStyle<()> { + todo!() + } + + style() + }) + .element() .into_any() } @@ -2531,7 +2541,7 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen mod components { use gpui::{ - elements::{Empty, Flex, GeneralComponent, ParentElement, StyleableComponent}, + elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, Action, Element, }; use theme::components::{ @@ -2539,13 +2549,13 @@ mod components { }; #[derive(Clone)] - struct DisclosureStyle { + pub struct DisclosureStyle { disclosure: ToggleIconButtonStyle, spacing: f32, content: S, } - struct Disclosable { + pub struct Disclosable { disclosed: bool, action: Box, content: C, @@ -2563,7 +2573,7 @@ mod components { } } - impl StyleableComponent for Disclosable { + impl GeneralStyleableComponent for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -2578,7 +2588,7 @@ mod components { } } - impl GeneralComponent for Disclosable> { + impl GeneralComponent for Disclosable> { fn render( self, v: &mut V, @@ -2605,7 +2615,7 @@ mod components { Self: Sized; } - impl DisclosureExt for C { + impl DisclosureExt for C { fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { Disclosable::new(disclosed, self, action) } diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index ad38b5893c..aeeab8101d 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .element(), + .c_element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .element(), + .c_element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index f7697d6fc1..c0484ef939 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -236,6 +236,13 @@ pub trait Element: 'static { { ElementAdapter::new(self.into_any()) } + + fn dynamic_component(self) -> DynamicElementAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()) + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index ee4702a6fa..f5a4180d7f 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -1,4 +1,4 @@ -use std::marker::PhantomData; +use std::{any::Any, marker::PhantomData}; use pathfinder_geometry::{rect::RectF, vector::Vector2F}; @@ -11,15 +11,43 @@ use super::Empty; pub trait GeneralComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } + + fn stylable(self) -> GeneralStylableComponentAdapter + where + Self: Sized, + { + GeneralStylableComponentAdapter::new(self) + } } -pub trait StyleableComponent { +pub struct GeneralStylableComponentAdapter { + component: C, +} + +impl GeneralStylableComponentAdapter { + pub fn new(component: C) -> Self { + Self { component } + } +} + +impl GeneralStyleableComponent for GeneralStylableComponentAdapter { + type Style = (); + + type Output = C; + + fn with_style(self, _: Self::Style) -> Self::Output { + self.component + } +} + +pub trait GeneralStyleableComponent { type Style: Clone; type Output: GeneralComponent; @@ -32,7 +60,7 @@ impl GeneralComponent for () { } } -impl StyleableComponent for () { +impl GeneralStyleableComponent for () { type Style = (); type Output = (); @@ -41,17 +69,34 @@ impl StyleableComponent for () { } } +pub trait StyleableComponent { + type Style: Clone; + type Output: Component; + + fn c_with_style(self, style: Self::Style) -> Self::Output; +} + +impl StyleableComponent for C { + type Style = C::Style; + + type Output = C::Output; + + fn c_with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn c_element(self) -> ComponentAdapter where Self: Sized, { ComponentAdapter::new(self) } - fn styleable(self) -> StylableComponentAdapter + fn c_styleable(self) -> StylableComponentAdapter where Self: Sized, { @@ -65,7 +110,7 @@ impl Component for C { } } -// StylableComponent -> GeneralComponent +// StylableComponent -> Component pub struct StylableComponentAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -80,16 +125,40 @@ impl, V: View> StylableComponentAdapter { } } -impl StyleableComponent for StylableComponentAdapter { +impl, V: View> StyleableComponent for StylableComponentAdapter { type Style = (); type Output = C; - fn with_style(self, _: Self::Style) -> Self::Output { + fn c_with_style(self, _: Self::Style) -> Self::Output { self.component } } +// Element -> GeneralComponent + +pub struct DynamicElementAdapter { + element: Box, +} + +impl DynamicElementAdapter { + pub fn new(element: AnyElement) -> Self { + DynamicElementAdapter { + element: Box::new(element) as Box, + } + } +} + +impl GeneralComponent for DynamicElementAdapter { + fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { + let element = self + .element + .downcast::>() + .expect("Don't move elements out of their view :("); + *element + } +} + // Element -> Component pub struct ElementAdapter { element: AnyElement, diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e3973..b3167efe50 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, GeneralStyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .element() + .c_element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 1e395405cb..3679b1fa4f 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::elements::StyleableComponent; +use gpui::elements::GeneralStyleableComponent; use crate::{Interactive, Toggleable}; @@ -6,18 +6,18 @@ use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } } pub mod toggle { - use gpui::elements::{GeneralComponent, StyleableComponent}; + use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; use crate::Toggleable; @@ -27,7 +27,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +37,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl GeneralStyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +51,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl GeneralComponent for Toggle> { fn render( self, v: &mut V, @@ -69,7 +69,8 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, MouseEventHandler, StyleableComponent, TooltipStyle, + ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, + TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -121,7 +122,10 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { + pub fn with_contents( + self, + contents: C, + ) -> ActionButton { ActionButton { action: self.action, tag: self.tag, @@ -132,7 +136,7 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl GeneralStyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -148,7 +152,7 @@ pub mod action_button { } } - impl GeneralComponent for ActionButton> { + impl GeneralComponent for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { let style = self.style.style_for(state); @@ -195,7 +199,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent}, Element, }; use schemars::JsonSchema; @@ -261,7 +265,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl GeneralStyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -294,7 +298,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, LabelStyle, StyleableComponent}, + elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, Element, }; @@ -312,7 +316,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl GeneralStyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; From bd3ab82dac7d59f19a104416b29976d070946004 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 05:18:53 -0700 Subject: [PATCH 003/142] Add disclosable components into channels Rename components to more closely match their purpose --- crates/client/src/channel_store.rs | 10 ++ crates/collab_ui/src/collab_panel.rs | 170 +++++++----------- crates/gpui/examples/components.rs | 14 +- crates/gpui/src/elements.rs | 11 +- crates/gpui/src/elements/component.rs | 66 +++---- crates/gpui/src/elements/container.rs | 8 + crates/search/src/search.rs | 4 +- crates/theme/src/components.rs | 245 +++++++++++++++++++++----- crates/theme/src/theme.rs | 21 ++- styles/src/style_tree/collab_panel.ts | 4 + 10 files changed, 359 insertions(+), 194 deletions(-) diff --git a/crates/client/src/channel_store.rs b/crates/client/src/channel_store.rs index 03d334a9de..6352ac791e 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/client/src/channel_store.rs @@ -114,6 +114,16 @@ impl ChannelStore { } } + pub fn has_children(&self, channel_id: ChannelId) -> bool { + self.channel_paths.iter().any(|path| { + if let Some(ix) = path.iter().position(|id| *id == channel_id) { + path.len() > ix + 1 + } else { + false + } + }) + } + pub fn channel_count(&self) -> usize { self.channel_paths.len() } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index f35c07d5aa..41c87094d2 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -16,9 +16,9 @@ use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, elements::{ - Canvas, ChildView, Empty, Flex, GeneralComponent, GeneralStyleableComponent, Image, Label, - List, ListOffset, ListState, MouseEventHandler, Orientation, OverlayPositionMode, Padding, - ParentElement, Stack, Svg, + Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, + StyleableComponent, Svg, }, geometry::{ rect::RectF, @@ -36,7 +36,7 @@ use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; -use theme::IconButton; +use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, @@ -44,10 +44,7 @@ use workspace::{ Workspace, }; -use crate::{ - collab_panel::components::{DisclosureExt, DisclosureStyle}, - face_pile::FacePile, -}; +use crate::face_pile::FacePile; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -57,6 +54,11 @@ struct RemoveChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct ToggleCollapsed { + channel_id: u64, +} + #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] struct NewChannel { channel_id: u64, @@ -86,7 +88,8 @@ impl_actions!( NewChannel, InviteMembers, ManageMembers, - RenameChannel + RenameChannel, + ToggleCollapsed ] ); @@ -109,6 +112,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::manage_members); cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); + cx.add_action(CollabPanel::toggle_channel_collapsed); } #[derive(Debug)] @@ -151,6 +155,7 @@ pub struct CollabPanel { list_state: ListState, subscriptions: Vec, collapsed_sections: Vec
, + collapsed_channels: Vec, workspace: WeakViewHandle, context_menu_on_selected: bool, } @@ -402,6 +407,7 @@ impl CollabPanel { subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], + collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), client: workspace.app_state().client.clone(), context_menu_on_selected: true, @@ -661,10 +667,24 @@ impl CollabPanel { self.entries.push(ListEntry::ChannelEditor { depth: 0 }); } } + let mut collapse_depth = None; for mat in matches { let (depth, channel) = channel_store.channel_at_index(mat.candidate_id).unwrap(); + if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else if let Some(collapsed_depth) = collapse_depth { + if depth > collapsed_depth { + continue; + } + if self.is_channel_collapsed(channel.id) { + collapse_depth = Some(depth); + } else { + collapse_depth = None; + } + } + match &self.channel_editing_state { Some(ChannelEditingState::Create { parent_id, .. }) if *parent_id == Some(channel.id) => @@ -1483,6 +1503,11 @@ impl CollabPanel { cx: &AppContext, ) -> AnyElement { Flex::row() + .with_child( + Empty::new() + .constrained() + .with_width(theme.collab_panel.disclosure.button_space()), + ) .with_child( Svg::new("icons/hash.svg") .with_color(theme.collab_panel.channel_hash.color) @@ -1541,6 +1566,10 @@ impl CollabPanel { cx: &mut ViewContext, ) -> AnyElement { let channel_id = channel.id; + let has_children = self.channel_store.read(cx).has_children(channel_id); + let disclosed = + has_children.then(|| !self.collapsed_channels.binary_search(&channel_id).is_ok()); + let is_active = iife!({ let call_channel = ActiveCall::global(cx) .read(cx) @@ -1554,7 +1583,7 @@ impl CollabPanel { const FACEPILE_LIMIT: usize = 3; MouseEventHandler::new::(channel.id as usize, cx, |state, cx| { - Flex::row() + Flex::::row() .with_child( Svg::new("icons/hash.svg") .with_color(theme.channel_hash.color) @@ -1603,6 +1632,14 @@ impl CollabPanel { } }) .align_children_center() + .styleable_component() + .disclosable( + disclosed, + Box::new(ToggleCollapsed { channel_id }), + channel_id as usize, + ) + .with_style(theme.disclosure.clone()) + .element() .constrained() .with_height(theme.row_height) .contained() @@ -1619,17 +1656,6 @@ impl CollabPanel { this.deploy_channel_context_menu(Some(e.position), channel_id, cx); }) .with_cursor_style(CursorStyle::PointingHand) - .dynamic_component() - .stylable() - .disclosable(true, Box::new(RemoveChannel { channel_id: 0 })) - .with_style({ - fn style() -> DisclosureStyle<()> { - todo!() - } - - style() - }) - .element() .into_any() } @@ -2024,6 +2050,24 @@ impl CollabPanel { self.update_entries(false, cx); } + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { + Ok(ix) => { + self.collapsed_channels.remove(ix); + } + Err(ix) => { + self.collapsed_channels.insert(ix, channel_id); + } + }; + self.update_entries(false, cx); + cx.notify(); + } + + fn is_channel_collapsed(&self, channel: ChannelId) -> bool { + self.collapsed_channels.binary_search(&channel).is_ok() + } + fn leave_call(cx: &mut ViewContext) { ActiveCall::global(cx) .update(cx, |call, cx| call.hang_up(cx)) @@ -2537,87 +2581,3 @@ fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Elemen .contained() .with_style(style.container) } - -mod components { - - use gpui::{ - elements::{Empty, Flex, GeneralComponent, GeneralStyleableComponent, ParentElement}, - Action, Element, - }; - use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, - }; - - #[derive(Clone)] - pub struct DisclosureStyle { - disclosure: ToggleIconButtonStyle, - spacing: f32, - content: S, - } - - pub struct Disclosable { - disclosed: bool, - action: Box, - content: C, - style: S, - } - - impl Disclosable<(), ()> { - fn new(disclosed: bool, content: C, action: Box) -> Disclosable { - Disclosable { - disclosed, - content, - action, - style: (), - } - } - } - - impl GeneralStyleableComponent for Disclosable { - type Style = DisclosureStyle; - - type Output = Disclosable; - - fn with_style(self, style: Self::Style) -> Self::Output { - Disclosable { - disclosed: self.disclosed, - action: self.action, - content: self.content, - style, - } - } - } - - impl GeneralComponent for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { - Flex::row() - .with_child( - ActionButton::new_dynamic(self.action) - .with_contents(Svg::new("path")) - .toggleable(self.disclosed) - .with_style(self.style.disclosure) - .element(), - ) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) - .with_child(self.content.with_style(self.style.content).render(v, cx)) - .align_children_center() - .into_any() - } - } - - pub trait DisclosureExt { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable - where - Self: Sized; - } - - impl DisclosureExt for C { - fn disclosable(self, disclosed: bool, action: Box) -> Disclosable { - Disclosable::new(disclosed, self, action) - } - } -} diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index aeeab8101d..5aa648bd65 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -2,7 +2,7 @@ use button_component::Button; use gpui::{ color::Color, - elements::{Component, ContainerStyle, Flex, Label, ParentElement}, + elements::{ContainerStyle, Flex, Label, ParentElement, StatefulComponent}, fonts::{self, TextStyle}, platform::WindowOptions, AnyElement, App, Element, Entity, View, ViewContext, @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .c_element(), + .stateful_element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .c_element(), + .stateful_element(), ) .expanded() .contained() @@ -114,7 +114,7 @@ mod theme { // Component creation: mod toggleable_button { use gpui::{ - elements::{Component, ContainerStyle, LabelStyle}, + elements::{ContainerStyle, LabelStyle, StatefulComponent}, scene::MouseClick, EventContext, View, }; @@ -156,7 +156,7 @@ mod toggleable_button { } } - impl Component for ToggleableButton { + impl StatefulComponent for ToggleableButton { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let button = if let Some(style) = self.style { self.button.with_style(*style.style_for(self.active)) @@ -171,7 +171,7 @@ mod toggleable_button { mod button_component { use gpui::{ - elements::{Component, ContainerStyle, Label, LabelStyle, MouseEventHandler}, + elements::{ContainerStyle, Label, LabelStyle, MouseEventHandler, StatefulComponent}, platform::MouseButton, scene::MouseClick, AnyElement, Element, EventContext, TypeTag, View, ViewContext, @@ -212,7 +212,7 @@ mod button_component { } } - impl Component for Button { + impl StatefulComponent for Button { fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { let click_handler = self.click_handler; diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index c0484ef939..4fcf3815f5 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,19 +230,26 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn component(self) -> ElementAdapter + fn stateful_component(self) -> ElementAdapter where Self: Sized, { ElementAdapter::new(self.into_any()) } - fn dynamic_component(self) -> DynamicElementAdapter + fn component(self) -> DynamicElementAdapter where Self: Sized, { DynamicElementAdapter::new(self.into_any()) } + + fn styleable_component(self) -> StylableAdapter + where + Self: Sized, + { + DynamicElementAdapter::new(self.into_any()).stylable() + } } pub trait RenderElement { diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index f5a4180d7f..703d6eca07 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,35 +9,35 @@ use crate::{ use super::Empty; -pub trait GeneralComponent { +pub trait Component { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> ComponentAdapter + fn element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn stylable(self) -> GeneralStylableComponentAdapter + fn stylable(self) -> StylableAdapter where Self: Sized, { - GeneralStylableComponentAdapter::new(self) + StylableAdapter::new(self) } } -pub struct GeneralStylableComponentAdapter { +pub struct StylableAdapter { component: C, } -impl GeneralStylableComponentAdapter { +impl StylableAdapter { pub fn new(component: C) -> Self { Self { component } } } -impl GeneralStyleableComponent for GeneralStylableComponentAdapter { +impl StyleableComponent for StylableAdapter { type Style = (); type Output = C; @@ -47,20 +47,20 @@ impl GeneralStyleableComponent for GeneralStylableComponent } } -pub trait GeneralStyleableComponent { +pub trait StyleableComponent { type Style: Clone; - type Output: GeneralComponent; + type Output: Component; fn with_style(self, style: Self::Style) -> Self::Output; } -impl GeneralComponent for () { +impl Component for () { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { Empty::new().into_any() } } -impl GeneralStyleableComponent for () { +impl StyleableComponent for () { type Style = (); type Output = (); @@ -69,54 +69,54 @@ impl GeneralStyleableComponent for () { } } -pub trait StyleableComponent { +pub trait StatefulStyleableComponent { type Style: Clone; - type Output: Component; + type Output: StatefulComponent; - fn c_with_style(self, style: Self::Style) -> Self::Output; + fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StyleableComponent for C { +impl StatefulStyleableComponent for C { type Style = C::Style; type Output = C::Output; - fn c_with_style(self, style: Self::Style) -> Self::Output { + fn stateful_with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } -pub trait Component { +pub trait StatefulComponent { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; - fn c_element(self) -> ComponentAdapter + fn stateful_element(self) -> StatefulAdapter where Self: Sized, { - ComponentAdapter::new(self) + StatefulAdapter::new(self) } - fn c_styleable(self) -> StylableComponentAdapter + fn stateful_styleable(self) -> StatefulStylableAdapter where Self: Sized, { - StylableComponentAdapter::new(self) + StatefulStylableAdapter::new(self) } } -impl Component for C { +impl StatefulComponent for C { fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { self.render(v, cx) } } // StylableComponent -> Component -pub struct StylableComponentAdapter, V: View> { +pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, } -impl, V: View> StylableComponentAdapter { +impl, V: View> StatefulStylableAdapter { pub fn new(component: C) -> Self { Self { component, @@ -125,12 +125,14 @@ impl, V: View> StylableComponentAdapter { } } -impl, V: View> StyleableComponent for StylableComponentAdapter { +impl, V: View> StatefulStyleableComponent + for StatefulStylableAdapter +{ type Style = (); type Output = C; - fn c_with_style(self, _: Self::Style) -> Self::Output { + fn stateful_with_style(self, _: Self::Style) -> Self::Output { self.component } } @@ -149,7 +151,7 @@ impl DynamicElementAdapter { } } -impl GeneralComponent for DynamicElementAdapter { +impl Component for DynamicElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { let element = self .element @@ -174,20 +176,20 @@ impl ElementAdapter { } } -impl Component for ElementAdapter { +impl StatefulComponent for ElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } // Component -> Element -pub struct ComponentAdapter { +pub struct StatefulAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl ComponentAdapter { +impl StatefulAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -197,7 +199,7 @@ impl ComponentAdapter { } } -impl + 'static> Element for ComponentAdapter { +impl + 'static> Element for StatefulAdapter { type LayoutState = (); type PaintState = (); diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index bb1366b4e7..68d3e57610 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -45,6 +45,14 @@ impl ContainerStyle { ..Default::default() } } + + pub fn additional_length(&self) -> f32 { + self.padding.left + + self.padding.right + + self.border.width * 2. + + self.margin.left + + self.margin.right + } } pub struct Container { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index b3167efe50..2dc45e3973 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,7 +2,7 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, GeneralStyleableComponent, TooltipStyle}, + elements::{Component, StyleableComponent, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; @@ -96,7 +96,7 @@ impl SearchOptions { .with_contents(Svg::new(self.icon())) .toggleable(active) .with_style(button_style) - .c_element() + .element() .into_any() } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 3679b1fa4f..6f9242199f 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,23 +1,147 @@ -use gpui::elements::GeneralStyleableComponent; +use gpui::{elements::StyleableComponent, Action}; use crate::{Interactive, Toggleable}; -use self::{action_button::ButtonStyle, svg::SvgStyle, toggle::Toggle}; +use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; + fn disclosable( + self, + disclosed: Option, + action: Box, + id: usize, + ) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } + + /// Some(True) => disclosed => content is visible + /// Some(false) => closed => content is hidden + /// None => No disclosure button, but reserve spacing + fn disclosable( + self, + disclosed: Option, + action: Box, + id: usize, + ) -> Disclosable { + Disclosable::new(disclosed, self, action, id) + } +} + +pub mod disclosure { + + use gpui::{ + elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + Action, Element, + }; + use schemars::JsonSchema; + use serde_derive::Deserialize; + + use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + + #[derive(Clone, Default, Deserialize, JsonSchema)] + pub struct DisclosureStyle { + pub button: ToggleIconButtonStyle, + pub spacing: f32, + #[serde(flatten)] + content: S, + } + + impl DisclosureStyle { + pub fn button_space(&self) -> f32 { + self.spacing + self.button.button_width.unwrap() + } + } + + pub struct Disclosable { + disclosed: Option, + action: Box, + id: usize, + content: C, + style: S, + } + + impl Disclosable<(), ()> { + pub fn new( + disclosed: Option, + content: C, + action: Box, + id: usize, + ) -> Disclosable { + Disclosable { + disclosed, + content, + action, + id, + style: (), + } + } + } + + impl StyleableComponent for Disclosable { + type Style = DisclosureStyle; + + type Output = Disclosable; + + fn with_style(self, style: Self::Style) -> Self::Output { + Disclosable { + disclosed: self.disclosed, + action: self.action, + content: self.content, + id: self.id, + style, + } + } + } + + impl Component for Disclosable> { + fn render( + self, + v: &mut V, + cx: &mut gpui::ViewContext, + ) -> gpui::AnyElement { + Flex::row() + .with_child(if let Some(disclosed) = self.disclosed { + ActionButton::new_dynamic(self.action) + .with_id(self.id) + .with_contents(Svg::new(if disclosed { + "icons/file_icons/chevron_down.svg" + } else { + "icons/file_icons/chevron_right.svg" + })) + .toggleable(disclosed) + .with_style(self.style.button) + .element() + .into_any() + } else { + Empty::new() + .into_any() + .constrained() + // TODO: Why is this optional at all? + .with_width(self.style.button.button_width.unwrap()) + .into_any() + }) + .with_child(Empty::new().constrained().with_width(self.style.spacing)) + .with_child( + self.content + .with_style(self.style.content) + .render(v, cx) + .flex(1., true), + ) + .align_children_center() + .into_any() + } + } } pub mod toggle { - use gpui::elements::{GeneralComponent, GeneralStyleableComponent}; + use gpui::elements::{Component, StyleableComponent}; use crate::Toggleable; @@ -27,7 +151,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -37,7 +161,7 @@ pub mod toggle { } } - impl GeneralStyleableComponent for Toggle { + impl StyleableComponent for Toggle { type Style = Toggleable; type Output = Toggle; @@ -51,7 +175,7 @@ pub mod toggle { } } - impl GeneralComponent for Toggle> { + impl Component for Toggle> { fn render( self, v: &mut V, @@ -69,8 +193,7 @@ pub mod action_button { use gpui::{ elements::{ - ContainerStyle, GeneralComponent, GeneralStyleableComponent, MouseEventHandler, - TooltipStyle, + Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, }, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, @@ -80,24 +203,28 @@ pub mod action_button { use crate::Interactive; + #[derive(Clone, Deserialize, Default, JsonSchema)] + pub struct ButtonStyle { + #[serde(flatten)] + pub container: ContainerStyle, + // TODO: These are incorrect for the intended usage of the buttons. + // The size should be constant, but putting them here duplicates them + // across the states the buttons can be in + pub button_width: Option, + pub button_height: Option, + #[serde(flatten)] + contents: C, + } + pub struct ActionButton { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, + id: usize, contents: C, style: Interactive, } - #[derive(Clone, Deserialize, Default, JsonSchema)] - pub struct ButtonStyle { - #[serde(flatten)] - container: ContainerStyle, - button_width: Option, - button_height: Option, - #[serde(flatten)] - contents: C, - } - impl ActionButton<(), ()> { pub fn new_dynamic(action: Box) -> Self { Self { @@ -105,6 +232,7 @@ pub mod action_button { tag: action.type_tag(), style: Interactive::new_blank(), tooltip: None, + id: 0, action, } } @@ -122,21 +250,24 @@ pub mod action_button { self } - pub fn with_contents( - self, - contents: C, - ) -> ActionButton { + pub fn with_id(mut self, id: usize) -> Self { + self.id = id; + self + } + + pub fn with_contents(self, contents: C) -> ActionButton { ActionButton { action: self.action, tag: self.tag, style: self.style, tooltip: self.tooltip, + id: self.id, contents, } } } - impl GeneralStyleableComponent for ActionButton { + impl StyleableComponent for ActionButton { type Style = Interactive>; type Output = ActionButton>; @@ -146,15 +277,15 @@ pub mod action_button { tag: self.tag, contents: self.contents, tooltip: self.tooltip, - + id: self.id, style, } } } - impl GeneralComponent for ActionButton> { + impl Component for ActionButton> { fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - let mut button = MouseEventHandler::new_dynamic(self.tag, 0, cx, |state, cx| { + let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents @@ -177,8 +308,13 @@ pub mod action_button { .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); move |_, _, cx| { - cx.window() - .dispatch_action(cx.view_id(), action.as_ref(), cx); + let window = cx.window(); + let view = cx.view_id(); + let action = action.boxed_clone(); + cx.spawn(|_, mut cx| async move { + window.dispatch_action(view, action.as_ref(), &mut cx) + }) + .detach(); } }) .with_cursor_style(CursorStyle::PointingHand) @@ -199,7 +335,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent}, + elements::{Component, Empty, StyleableComponent}, Element, }; use schemars::JsonSchema; @@ -222,6 +358,7 @@ pub mod svg { pub enum IconSize { IconSize { icon_size: f32 }, Dimensions { width: f32, height: f32 }, + IconDimensions { icon_width: f32, icon_height: f32 }, } #[derive(Deserialize)] @@ -245,6 +382,14 @@ pub mod svg { icon_height: height, color, }, + IconSize::IconDimensions { + icon_width, + icon_height, + } => SvgStyle { + icon_width, + icon_height, + color, + }, }; Ok(result) @@ -252,20 +397,27 @@ pub mod svg { } pub struct Svg { - path: Cow<'static, str>, + path: Option>, style: S, } impl Svg<()> { pub fn new(path: impl Into>) -> Self { Self { - path: path.into(), + path: Some(path.into()), + style: (), + } + } + + pub fn optional(path: Option>>) -> Self { + Self { + path: path.map(Into::into), style: (), } } } - impl GeneralStyleableComponent for Svg<()> { + impl StyleableComponent for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -278,18 +430,23 @@ pub mod svg { } } - impl GeneralComponent for Svg { + impl Component for Svg { fn render( self, _: &mut V, _: &mut gpui::ViewContext, ) -> gpui::AnyElement { - gpui::elements::Svg::new(self.path) - .with_color(self.style.color) - .constrained() - .with_width(self.style.icon_width) - .with_height(self.style.icon_height) - .into_any() + if let Some(path) = self.path { + gpui::elements::Svg::new(path) + .with_color(self.style.color) + .constrained() + } else { + Empty::new().constrained() + } + .constrained() + .with_width(self.style.icon_width) + .with_height(self.style.icon_height) + .into_any() } } } @@ -298,7 +455,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{GeneralComponent, GeneralStyleableComponent, LabelStyle}, + elements::{Component, LabelStyle, StyleableComponent}, Element, }; @@ -316,7 +473,7 @@ pub mod label { } } - impl GeneralStyleableComponent for Label<()> { + impl StyleableComponent for Label<()> { type Style = LabelStyle; type Output = Label; @@ -329,7 +486,7 @@ pub mod label { } } - impl GeneralComponent for Label { + impl Component for Label { fn render( self, _: &mut V, diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 80e823632a..08da9af299 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::ToggleIconButtonStyle; +use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -14,7 +14,7 @@ use schemars::JsonSchema; use serde::{de::DeserializeOwned, Deserialize}; use serde_json::Value; use settings::SettingsStore; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, ops::Deref, sync::Arc}; use ui::{CheckboxStyle, CopilotCTAButton, IconStyle, ModalStyle}; pub use theme_registry::*; @@ -221,6 +221,7 @@ pub struct CopilotAuthAuthorized { pub struct CollabPanel { #[serde(flatten)] pub container: ContainerStyle, + pub disclosure: DisclosureStyle<()>, pub list_empty_state: Toggleable>, pub list_empty_icon: Icon, pub list_empty_label_container: ContainerStyle, @@ -890,6 +891,14 @@ pub struct Interactive { pub disabled: Option, } +impl Deref for Interactive { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.default + } +} + impl Interactive<()> { pub fn new_blank() -> Self { Self { @@ -907,6 +916,14 @@ pub struct Toggleable { inactive: T, } +impl Deref for Toggleable { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.inactive + } +} + impl Toggleable<()> { pub fn new_blank() -> Self { Self { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index a102ee7691..5242f90c8d 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -152,6 +152,10 @@ export default function contacts_panel(): any { return { ...collab_modals(), + disclosure: { + button: toggleable_icon_button(theme, {}), + spacing: 4, + }, log_in_button: interactive({ base: { background: background(theme.middle), From e946b0a2ece63ba160804b5260a4b00433e3130e Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 14:40:05 -0700 Subject: [PATCH 004/142] Finish building out adapters and names Document core traits Add start for a component storybook --- Cargo.lock | 11 ++ Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 11 +- crates/gpui/examples/components.rs | 4 +- crates/gpui/src/elements.rs | 12 +- crates/gpui/src/elements/component.rs | 202 ++++++++++++++++++-------- crates/search/src/search.rs | 8 +- crates/theme/src/components.rs | 117 ++++++--------- 8 files changed, 213 insertions(+), 153 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aab5504a86..01c74fb4ce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1562,6 +1562,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "component_test" +version = "0.1.0" +dependencies = [ + "gpui", + "settings", + "theme", + "util", + "workspace", +] + [[package]] name = "concurrent-queue" version = "2.2.0" diff --git a/Cargo.toml b/Cargo.toml index 7ea79138c0..d434f34773 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/collab_ui", "crates/collections", "crates/command_palette", + "crates/component_test", "crates/context_menu", "crates/copilot", "crates/copilot_button", diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 41c87094d2..52711281c7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -17,8 +17,8 @@ use gpui::{ actions, elements::{ Canvas, ChildView, Component, Empty, Flex, Image, Label, List, ListOffset, ListState, - MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, Stack, - StyleableComponent, Svg, + MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, + Stack, Svg, }, geometry::{ rect::RectF, @@ -1633,11 +1633,8 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable( - disclosed, - Box::new(ToggleCollapsed { channel_id }), - channel_id as usize, - ) + .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() .constrained() diff --git a/crates/gpui/examples/components.rs b/crates/gpui/examples/components.rs index 5aa648bd65..d3ca0d1ecc 100644 --- a/crates/gpui/examples/components.rs +++ b/crates/gpui/examples/components.rs @@ -72,7 +72,7 @@ impl View for TestView { TextStyle::for_color(Color::blue()), ) .with_style(ButtonStyle::fill(Color::yellow())) - .stateful_element(), + .element(), ) .with_child( ToggleableButton::new(self.is_doubling, move |_, v: &mut Self, cx| { @@ -84,7 +84,7 @@ impl View for TestView { inactive: ButtonStyle::fill(Color::red()), active: ButtonStyle::fill(Color::green()), }) - .stateful_element(), + .element(), ) .expanded() .contained() diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 4fcf3815f5..3c80ef73fe 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -230,25 +230,25 @@ pub trait Element: 'static { MouseEventHandler::for_child::(self.into_any(), region_id) } - fn stateful_component(self) -> ElementAdapter + fn component(self) -> StatelessElementAdapter where Self: Sized, { - ElementAdapter::new(self.into_any()) + StatelessElementAdapter::new(self.into_any()) } - fn component(self) -> DynamicElementAdapter + fn stateful_component(self) -> StatefulElementAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()) + StatefulElementAdapter::new(self.into_any()) } - fn styleable_component(self) -> StylableAdapter + fn styleable_component(self) -> StylableAdapter where Self: Sized, { - DynamicElementAdapter::new(self.into_any()).stylable() + StatelessElementAdapter::new(self.into_any()).stylable() } } diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index 703d6eca07..e391ff9b05 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -9,14 +9,15 @@ use crate::{ use super::Empty; +/// The core stateless component trait, simply rendering an element tree pub trait Component { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + fn render(self, cx: &mut ViewContext) -> AnyElement; - fn element(self) -> StatefulAdapter + fn element(self) -> ComponentAdapter where Self: Sized, { - StatefulAdapter::new(self) + ComponentAdapter::new(self) } fn stylable(self) -> StylableAdapter @@ -25,8 +26,46 @@ pub trait Component { { StylableAdapter::new(self) } + + fn stateful(self) -> StatefulAdapter + where + Self: Sized, + { + StatefulAdapter::new(self) + } } +/// Allows a a component's styles to be rebound in a simple way. +pub trait Stylable: Component { + type Style: Clone; + + fn with_style(self, style: Self::Style) -> Self; +} + +/// This trait models the typestate pattern for a component's style, +/// enforcing at compile time that a component is only usable after +/// it has been styled while still allowing for late binding of the +/// styling information +pub trait SafeStylable { + type Style: Clone; + type Output: Component; + + fn with_style(self, style: Self::Style) -> Self::Output; +} + +/// All stylable components can trivially implement SafeStylable +impl SafeStylable for C { + type Style = C::Style; + + type Output = C; + + fn with_style(self, style: Self::Style) -> Self::Output { + self.with_style(style) + } +} + +/// Allows converting an unstylable component into a stylable one +/// by using `()` as the style type pub struct StylableAdapter { component: C, } @@ -37,7 +76,7 @@ impl StylableAdapter { } } -impl StyleableComponent for StylableAdapter { +impl SafeStylable for StylableAdapter { type Style = (); type Output = C; @@ -47,36 +86,61 @@ impl StyleableComponent for StylableAdapter { } } -pub trait StyleableComponent { +/// This is a secondary trait for components that can be styled +/// which rely on their view's state. This is useful for components that, for example, +/// want to take click handler callbacks Unfortunately, the generic bound on the +/// Component trait makes it incompatible with the stateless components above. +// So let's just replicate them for now +pub trait StatefulComponent { + fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; + + fn element(self) -> ComponentAdapter + where + Self: Sized, + { + ComponentAdapter::new(self) + } + + fn styleable(self) -> StatefulStylableAdapter + where + Self: Sized, + { + StatefulStylableAdapter::new(self) + } + + fn stateless(self) -> StatelessElementAdapter + where + Self: Sized + 'static, + { + StatelessElementAdapter::new(self.element().into_any()) + } +} + +/// It is trivial to convert stateless components to stateful components, so lets +/// do so en masse. Note that the reverse is impossible without a helper. +impl StatefulComponent for C { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.render(cx) + } +} + +/// Same as stylable, but generic over a view type +pub trait StatefulStylable: StatefulComponent { type Style: Clone; - type Output: Component; - fn with_style(self, style: Self::Style) -> Self::Output; + fn stateful_with_style(self, style: Self::Style) -> Self; } -impl Component for () { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - Empty::new().into_any() - } -} - -impl StyleableComponent for () { - type Style = (); - type Output = (); - - fn with_style(self, _: Self::Style) -> Self::Output { - () - } -} - -pub trait StatefulStyleableComponent { +/// Same as SafeStylable, but generic over a view type +pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; fn stateful_with_style(self, style: Self::Style) -> Self::Output; } -impl StatefulStyleableComponent for C { +/// Converting from stateless to stateful +impl StatefulSafeStylable for C { type Style = C::Style; type Output = C::Output; @@ -86,31 +150,29 @@ impl StatefulStyleableComponent for C { } } -pub trait StatefulComponent { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement; +// A helper for converting stateless components into stateful ones +pub struct StatefulAdapter { + component: C, + phantom: std::marker::PhantomData, +} - fn stateful_element(self) -> StatefulAdapter - where - Self: Sized, - { - StatefulAdapter::new(self) - } - - fn stateful_styleable(self) -> StatefulStylableAdapter - where - Self: Sized, - { - StatefulStylableAdapter::new(self) +impl StatefulAdapter { + pub fn new(component: C) -> Self { + Self { + component, + phantom: std::marker::PhantomData, + } } } -impl StatefulComponent for C { - fn render(self, v: &mut V, cx: &mut ViewContext) -> AnyElement { - self.render(v, cx) +impl StatefulComponent for StatefulAdapter { + fn render(self, _: &mut V, cx: &mut ViewContext) -> AnyElement { + self.component.render(cx) } } -// StylableComponent -> Component +// A helper for converting stateful but style-less components into stylable ones +// by using `()` as the style type pub struct StatefulStylableAdapter, V: View> { component: C, phantom: std::marker::PhantomData, @@ -125,9 +187,7 @@ impl, V: View> StatefulStylableAdapter { } } -impl, V: View> StatefulStyleableComponent - for StatefulStylableAdapter -{ +impl, V: View> StatefulSafeStylable for StatefulStylableAdapter { type Style = (); type Output = C; @@ -137,37 +197,37 @@ impl, V: View> StatefulStyleableComponent } } -// Element -> GeneralComponent - -pub struct DynamicElementAdapter { +/// A way of erasing the view generic from an element, useful +/// for wrapping up an explicit element tree into stateless +/// components +pub struct StatelessElementAdapter { element: Box, } -impl DynamicElementAdapter { +impl StatelessElementAdapter { pub fn new(element: AnyElement) -> Self { - DynamicElementAdapter { + StatelessElementAdapter { element: Box::new(element) as Box, } } } -impl Component for DynamicElementAdapter { - fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { - let element = self +impl Component for StatelessElementAdapter { + fn render(self, _: &mut ViewContext) -> AnyElement { + *self .element .downcast::>() - .expect("Don't move elements out of their view :("); - *element + .expect("Don't move elements out of their view :(") } } -// Element -> Component -pub struct ElementAdapter { +// For converting elements into stateful components +pub struct StatefulElementAdapter { element: AnyElement, _phantom: std::marker::PhantomData, } -impl ElementAdapter { +impl StatefulElementAdapter { pub fn new(element: AnyElement) -> Self { Self { element, @@ -176,20 +236,35 @@ impl ElementAdapter { } } -impl StatefulComponent for ElementAdapter { +impl StatefulComponent for StatefulElementAdapter { fn render(self, _: &mut V, _: &mut ViewContext) -> AnyElement { self.element } } -// Component -> Element -pub struct StatefulAdapter { +/// A convenient shorthand for creating an empty component. +impl Component for () { + fn render(self, _: &mut ViewContext) -> AnyElement { + Empty::new().into_any() + } +} + +impl Stylable for () { + type Style = (); + + fn with_style(self, _: Self::Style) -> Self { + () + } +} + +// For converting components back into Elements +pub struct ComponentAdapter { component: Option, element: Option>, phantom: PhantomData, } -impl StatefulAdapter { +impl ComponentAdapter { pub fn new(e: E) -> Self { Self { component: Some(e), @@ -199,7 +274,7 @@ impl StatefulAdapter { } } -impl + 'static> Element for StatefulAdapter { +impl + 'static> Element for ComponentAdapter { type LayoutState = (); type PaintState = (); @@ -262,6 +337,7 @@ impl + 'static> Element for StatefulAdapter< ) -> serde_json::Value { serde_json::json!({ "type": "ComponentAdapter", + "component": std::any::type_name::(), "child": self.element.as_ref().map(|el| el.debug(view, cx)), }) } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 2dc45e3973..47f7f485c4 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -2,15 +2,13 @@ use bitflags::bitflags; pub use buffer_search::BufferSearchBar; use gpui::{ actions, - elements::{Component, StyleableComponent, TooltipStyle}, + elements::{Component, SafeStylable, TooltipStyle}, Action, AnyElement, AppContext, Element, View, }; pub use mode::SearchMode; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; -use theme::components::{ - action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle, -}; +use theme::components::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; pub mod buffer_search; mod history; @@ -91,7 +89,7 @@ impl SearchOptions { tooltip_style: TooltipStyle, button_style: ToggleIconButtonStyle, ) -> AnyElement { - ActionButton::new_dynamic(self.to_toggle_action()) + Button::dynamic_action(self.to_toggle_action()) .with_tooltip(format!("Toggle {}", self.label()), tooltip_style) .with_contents(Svg::new(self.icon())) .toggleable(active) diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index 6f9242199f..d9a857915c 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -1,4 +1,4 @@ -use gpui::{elements::StyleableComponent, Action}; +use gpui::{elements::SafeStylable, Action}; use crate::{Interactive, Toggleable}; @@ -6,44 +6,34 @@ use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, t pub type ToggleIconButtonStyle = Toggleable>>; -pub trait ComponentExt { +pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable; + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable; } -impl ComponentExt for C { +impl ComponentExt for C { fn toggleable(self, active: bool) -> Toggle { Toggle::new(self, active) } /// Some(True) => disclosed => content is visible /// Some(false) => closed => content is hidden - /// None => No disclosure button, but reserve spacing - fn disclosable( - self, - disclosed: Option, - action: Box, - id: usize, - ) -> Disclosable { - Disclosable::new(disclosed, self, action, id) + /// None => No disclosure button, but reserve disclosure spacing + fn disclosable(self, disclosed: Option, action: Box) -> Disclosable { + Disclosable::new(disclosed, self, action) } } pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, StyleableComponent}, + elements::{Component, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::ActionButton, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { @@ -72,19 +62,24 @@ pub mod disclosure { disclosed: Option, content: C, action: Box, - id: usize, ) -> Disclosable { Disclosable { disclosed, content, action, - id, + id: 0, style: (), } } } - impl StyleableComponent for Disclosable { + impl Disclosable { + pub fn with_id(self, id: usize) -> Disclosable { + Disclosable { id, ..self } + } + } + + impl SafeStylable for Disclosable { type Style = DisclosureStyle; type Output = Disclosable; @@ -100,15 +95,11 @@ pub mod disclosure { } } - impl Component for Disclosable> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Disclosable> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() .with_child(if let Some(disclosed) = self.disclosed { - ActionButton::new_dynamic(self.action) + Button::dynamic_action(self.action) .with_id(self.id) .with_contents(Svg::new(if disclosed { "icons/file_icons/chevron_down.svg" @@ -131,7 +122,7 @@ pub mod disclosure { .with_child( self.content .with_style(self.style.content) - .render(v, cx) + .render(cx) .flex(1., true), ) .align_children_center() @@ -141,7 +132,7 @@ pub mod disclosure { } pub mod toggle { - use gpui::elements::{Component, StyleableComponent}; + use gpui::elements::{Component, SafeStylable}; use crate::Toggleable; @@ -151,7 +142,7 @@ pub mod toggle { component: C, } - impl Toggle { + impl Toggle { pub fn new(component: C, active: bool) -> Self { Toggle { active, @@ -161,7 +152,7 @@ pub mod toggle { } } - impl StyleableComponent for Toggle { + impl SafeStylable for Toggle { type Style = Toggleable; type Output = Toggle; @@ -175,15 +166,11 @@ pub mod toggle { } } - impl Component for Toggle> { - fn render( - self, - v: &mut V, - cx: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + impl Component for Toggle> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { self.component .with_style(self.style.in_state(self.active).clone()) - .render(v, cx) + .render(cx) } } } @@ -192,9 +179,7 @@ pub mod action_button { use std::borrow::Cow; use gpui::{ - elements::{ - Component, ContainerStyle, MouseEventHandler, StyleableComponent, TooltipStyle, - }, + elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, Action, Element, TypeTag, View, }; @@ -216,7 +201,7 @@ pub mod action_button { contents: C, } - pub struct ActionButton { + pub struct Button { action: Box, tooltip: Option<(Cow<'static, str>, TooltipStyle)>, tag: TypeTag, @@ -225,8 +210,8 @@ pub mod action_button { style: Interactive, } - impl ActionButton<(), ()> { - pub fn new_dynamic(action: Box) -> Self { + impl Button<(), ()> { + pub fn dynamic_action(action: Box) -> Self { Self { contents: (), tag: action.type_tag(), @@ -237,8 +222,8 @@ pub mod action_button { } } - pub fn new(action: A) -> Self { - Self::new_dynamic(Box::new(action)) + pub fn action(action: A) -> Self { + Self::dynamic_action(Box::new(action)) } pub fn with_tooltip( @@ -255,8 +240,8 @@ pub mod action_button { self } - pub fn with_contents(self, contents: C) -> ActionButton { - ActionButton { + pub fn with_contents(self, contents: C) -> Button { + Button { action: self.action, tag: self.tag, style: self.style, @@ -267,12 +252,12 @@ pub mod action_button { } } - impl StyleableComponent for ActionButton { + impl SafeStylable for Button { type Style = Interactive>; - type Output = ActionButton>; + type Output = Button>; fn with_style(self, style: Self::Style) -> Self::Output { - ActionButton { + Button { action: self.action, tag: self.tag, contents: self.contents, @@ -283,14 +268,14 @@ pub mod action_button { } } - impl Component for ActionButton> { - fn render(self, v: &mut V, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + impl Component for Button> { + fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { let mut button = MouseEventHandler::new_dynamic(self.tag, self.id, cx, |state, cx| { let style = self.style.style_for(state); let mut contents = self .contents .with_style(style.contents.to_owned()) - .render(v, cx) + .render(cx) .contained() .with_style(style.container) .constrained(); @@ -335,7 +320,7 @@ pub mod svg { use std::borrow::Cow; use gpui::{ - elements::{Component, Empty, StyleableComponent}, + elements::{Component, Empty, SafeStylable}, Element, }; use schemars::JsonSchema; @@ -417,7 +402,7 @@ pub mod svg { } } - impl StyleableComponent for Svg<()> { + impl SafeStylable for Svg<()> { type Style = SvgStyle; type Output = Svg; @@ -431,11 +416,7 @@ pub mod svg { } impl Component for Svg { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { if let Some(path) = self.path { gpui::elements::Svg::new(path) .with_color(self.style.color) @@ -455,7 +436,7 @@ pub mod label { use std::borrow::Cow; use gpui::{ - elements::{Component, LabelStyle, StyleableComponent}, + elements::{Component, LabelStyle, SafeStylable}, Element, }; @@ -473,7 +454,7 @@ pub mod label { } } - impl StyleableComponent for Label<()> { + impl SafeStylable for Label<()> { type Style = LabelStyle; type Output = Label; @@ -487,11 +468,7 @@ pub mod label { } impl Component for Label { - fn render( - self, - _: &mut V, - _: &mut gpui::ViewContext, - ) -> gpui::AnyElement { + fn render(self, _: &mut gpui::ViewContext) -> gpui::AnyElement { gpui::elements::Label::new(self.text, self.style).into_any() } } From bfd3e53dcd0912f0534a5fff9c18beafba42cca0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 19 Aug 2023 16:29:24 -0700 Subject: [PATCH 005/142] Implement component test page --- Cargo.lock | 3 + crates/component_test/Cargo.toml | 18 +++ crates/component_test/src/component_test.rs | 122 ++++++++++++++++++++ crates/gpui/src/elements/component.rs | 8 +- crates/gpui/src/elements/flex.rs | 33 +++++- crates/theme/src/components.rs | 18 +-- crates/theme/src/theme.rs | 10 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 1 + styles/src/style_tree/app.ts | 5 +- styles/src/style_tree/component_test.ts | 19 +++ 11 files changed, 217 insertions(+), 21 deletions(-) create mode 100644 crates/component_test/Cargo.toml create mode 100644 crates/component_test/src/component_test.rs create mode 100644 styles/src/style_tree/component_test.ts diff --git a/Cargo.lock b/Cargo.lock index 01c74fb4ce..0081626cab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1566,7 +1566,9 @@ dependencies = [ name = "component_test" version = "0.1.0" dependencies = [ + "anyhow", "gpui", + "project", "settings", "theme", "util", @@ -9668,6 +9670,7 @@ dependencies = [ "collab_ui", "collections", "command_palette", + "component_test", "context_menu", "copilot", "copilot_button", diff --git a/crates/component_test/Cargo.toml b/crates/component_test/Cargo.toml new file mode 100644 index 0000000000..d714f6f72f --- /dev/null +++ b/crates/component_test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "component_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/component_test.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +gpui = { path = "../gpui" } +settings = { path = "../settings" } +util = { path = "../util" } +theme = { path = "../theme" } +workspace = { path = "../workspace" } +project = { path = "../project" } diff --git a/crates/component_test/src/component_test.rs b/crates/component_test/src/component_test.rs new file mode 100644 index 0000000000..30fd431290 --- /dev/null +++ b/crates/component_test/src/component_test.rs @@ -0,0 +1,122 @@ +use gpui::{ + actions, + color::Color, + elements::{Component, Flex, ParentElement, SafeStylable}, + AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, +}; +use project::Project; +use theme::components::{action_button::Button, label::Label, ComponentExt}; +use workspace::{ + item::Item, register_deserializable_item, ItemId, Pane, PaneBackdrop, Workspace, WorkspaceId, +}; + +pub fn init(cx: &mut AppContext) { + cx.add_action(ComponentTest::toggle_disclosure); + cx.add_action(ComponentTest::toggle_toggle); + cx.add_action(ComponentTest::deploy); + register_deserializable_item::(cx); +} + +actions!( + test, + [NoAction, ToggleDisclosure, ToggleToggle, NewComponentTest] +); + +struct ComponentTest { + disclosed: bool, + toggled: bool, +} + +impl ComponentTest { + fn new() -> Self { + Self { + disclosed: false, + toggled: false, + } + } + + fn deploy(workspace: &mut Workspace, _: &NewComponentTest, cx: &mut ViewContext) { + workspace.add_item(Box::new(cx.add_view(|_| ComponentTest::new())), cx); + } + + fn toggle_disclosure(&mut self, _: &ToggleDisclosure, cx: &mut ViewContext) { + self.disclosed = !self.disclosed; + cx.notify(); + } + + fn toggle_toggle(&mut self, _: &ToggleToggle, cx: &mut ViewContext) { + self.toggled = !self.toggled; + cx.notify(); + } +} + +impl Entity for ComponentTest { + type Event = (); +} + +impl View for ComponentTest { + fn ui_name() -> &'static str { + "Component Test" + } + + fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { + let theme = theme::current(cx); + + PaneBackdrop::new( + cx.view_id(), + Flex::column() + .with_spacing(10.) + .with_child( + Button::action(NoAction) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Click me!")) + .with_style(theme.component_test.button.clone()) + .element(), + ) + .with_child( + Button::action(ToggleToggle) + .with_tooltip("Here's what a tooltip looks like", theme.tooltip.clone()) + .with_contents(Label::new("Toggle me!")) + .toggleable(self.toggled) + .with_style(theme.component_test.toggle.clone()) + .element(), + ) + .with_child( + Label::new("A disclosure") + .disclosable(Some(self.disclosed), Box::new(ToggleDisclosure)) + .with_style(theme.component_test.disclosure.clone()) + .element(), + ) + .constrained() + .with_width(200.) + .aligned() + .into_any(), + ) + .into_any() + } +} + +impl Item for ComponentTest { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + gpui::elements::Label::new("Component test", style.label.clone()).into_any() + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("ComponentTest") + } + + fn deserialize( + _project: ModelHandle, + _workspace: WeakViewHandle, + _workspace_id: WorkspaceId, + _item_id: ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.add_view(|_| Self::new()))) + } +} diff --git a/crates/gpui/src/elements/component.rs b/crates/gpui/src/elements/component.rs index e391ff9b05..3776abcaa0 100644 --- a/crates/gpui/src/elements/component.rs +++ b/crates/gpui/src/elements/component.rs @@ -128,7 +128,7 @@ impl StatefulComponent for C { pub trait StatefulStylable: StatefulComponent { type Style: Clone; - fn stateful_with_style(self, style: Self::Style) -> Self; + fn with_style(self, style: Self::Style) -> Self; } /// Same as SafeStylable, but generic over a view type @@ -136,7 +136,7 @@ pub trait StatefulSafeStylable { type Style: Clone; type Output: StatefulComponent; - fn stateful_with_style(self, style: Self::Style) -> Self::Output; + fn with_style(self, style: Self::Style) -> Self::Output; } /// Converting from stateless to stateful @@ -145,7 +145,7 @@ impl StatefulSafeStylable for C { type Output = C::Output; - fn stateful_with_style(self, style: Self::Style) -> Self::Output { + fn with_style(self, style: Self::Style) -> Self::Output { self.with_style(style) } } @@ -192,7 +192,7 @@ impl, V: View> StatefulSafeStylable for StatefulStyla type Output = C; - fn stateful_with_style(self, _: Self::Style) -> Self::Output { + fn with_style(self, _: Self::Style) -> Self::Output { self.component } } diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 3000b9575d..9d175afc03 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -22,6 +22,7 @@ pub struct Flex { children: Vec>, scroll_state: Option<(ElementStateHandle>, usize)>, child_alignment: f32, + spacing: f32, } impl Flex { @@ -31,6 +32,7 @@ impl Flex { children: Default::default(), scroll_state: None, child_alignment: -1., + spacing: 0., } } @@ -51,6 +53,11 @@ impl Flex { self } + pub fn with_spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + pub fn scrollable( mut self, element_id: usize, @@ -81,7 +88,8 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - for child in &mut self.children { + let last = self.children.len() - 1; + for (ix, child) in &mut self.children.iter_mut().enumerate() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -93,6 +101,10 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex + } - if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -137,7 +149,8 @@ impl Element for Flex { let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -155,7 +168,12 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis); + fixed_space += size.along(self.axis) + + if ix == 0 || ix == last { + self.spacing / 2. + } else { + self.spacing + }; cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -315,7 +333,8 @@ impl Element for Flex { } } - for child in &mut self.children { + let last = self.children.len().saturating_sub(1); + for (ix, child) in &mut self.children.iter_mut().enumerate() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -353,9 +372,11 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); + let spacing = if ix == last { 0. } else { self.spacing }; + match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x(), 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y()), + Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), } } diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index d9a857915c..fc208954cf 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -74,8 +74,9 @@ pub mod disclosure { } impl Disclosable { - pub fn with_id(self, id: usize) -> Disclosable { - Disclosable { id, ..self } + pub fn with_id(mut self, id: usize) -> Disclosable { + self.id = id; + self } } @@ -181,7 +182,7 @@ pub mod action_button { use gpui::{ elements::{Component, ContainerStyle, MouseEventHandler, SafeStylable, TooltipStyle}, platform::{CursorStyle, MouseButton}, - Action, Element, TypeTag, View, + Action, Element, EventContext, TypeTag, View, }; use schemars::JsonSchema; use serde_derive::Deserialize; @@ -211,14 +212,14 @@ pub mod action_button { } impl Button<(), ()> { - pub fn dynamic_action(action: Box) -> Self { + pub fn dynamic_action(action: Box) -> Button<(), ()> { Self { contents: (), tag: action.type_tag(), + action, style: Interactive::new_blank(), tooltip: None, id: 0, - action, } } @@ -292,7 +293,7 @@ pub mod action_button { }) .on_click(MouseButton::Left, { let action = self.action.boxed_clone(); - move |_, _, cx| { + move |_, _, cx: &mut EventContext| { let window = cx.window(); let view = cx.view_id(); let action = action.boxed_clone(); @@ -437,6 +438,7 @@ pub mod label { use gpui::{ elements::{Component, LabelStyle, SafeStylable}, + fonts::TextStyle, Element, }; @@ -455,14 +457,14 @@ pub mod label { } impl SafeStylable for Label<()> { - type Style = LabelStyle; + type Style = TextStyle; type Output = Label; fn with_style(self, style: Self::Style) -> Self::Output { Label { text: self.text, - style, + style: style.into(), } } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 08da9af299..0f34963708 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -3,7 +3,7 @@ mod theme_registry; mod theme_settings; pub mod ui; -use components::{disclosure::DisclosureStyle, ToggleIconButtonStyle}; +use components::{action_button::ButtonStyle, disclosure::DisclosureStyle, ToggleIconButtonStyle}; use gpui::{ color::Color, elements::{ContainerStyle, ImageStyle, LabelStyle, Shadow, SvgStyle, TooltipStyle}, @@ -66,6 +66,7 @@ pub struct Theme { pub feedback: FeedbackStyle, pub welcome: WelcomeStyle, pub titlebar: Titlebar, + pub component_test: ComponentTest, } #[derive(Deserialize, Default, Clone, JsonSchema)] @@ -260,6 +261,13 @@ pub struct CollabPanel { pub face_overlap: f32, } +#[derive(Deserialize, Default, JsonSchema)] +pub struct ComponentTest { + pub button: Interactive>, + pub toggle: Toggleable>>, + pub disclosure: DisclosureStyle, +} + #[derive(Deserialize, Default, JsonSchema)] pub struct TabbedModal { pub tab_button: Toggleable>, diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 988648d4b1..cb66a6b587 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -25,6 +25,7 @@ cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } command_palette = { path = "../command_palette" } +component_test = { path = "../component_test" } context_menu = { path = "../context_menu" } client = { path = "../client" } clock = { path = "../clock" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index deef3b85eb..caeaeceded 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -166,6 +166,7 @@ fn main() { terminal_view::init(cx); copilot::init(http.clone(), node_runtime, cx); ai::init(cx); + component_test::init(cx); cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee5e19e111..cf7e0538a7 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -12,7 +12,6 @@ import simple_message_notification from "./simple_message_notification" import project_shared_notification from "./project_shared_notification" import tooltip from "./tooltip" import terminal from "./terminal" -import contact_finder from "./contact_finder" import collab_panel from "./collab_panel" import toolbar_dropdown_menu from "./toolbar_dropdown_menu" import incoming_call_notification from "./incoming_call_notification" @@ -22,6 +21,7 @@ import assistant from "./assistant" import { titlebar } from "./titlebar" import editor from "./editor" import feedback from "./feedback" +import component_test from "./component_test" import { useTheme } from "../common" export default function app(): any { @@ -54,6 +54,7 @@ export default function app(): any { tooltip: tooltip(), terminal: terminal(), assistant: assistant(), - feedback: feedback() + feedback: feedback(), + component_test: component_test(), } } diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts new file mode 100644 index 0000000000..eadbb5c2f1 --- /dev/null +++ b/styles/src/style_tree/component_test.ts @@ -0,0 +1,19 @@ +import { toggle_label_button_style } from "../component/label_button" +import { useTheme } from "../common" +import { text_button } from "../component/text_button" +import { toggleable_icon_button } from "../component/icon_button" +import { text } from "./components" + +export default function contacts_panel(): any { + const theme = useTheme() + + return { + button: text_button({}), + toggle: toggle_label_button_style({ active_color: "accent" }), + disclosure: { + ...text(theme.lowest, "sans", "base"), + button: toggleable_icon_button(theme, {}), + spacing: 4, + } + } +} From 3d89cd10a4c544bb4973861023f1da53954ecbf0 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 21 Aug 2023 16:35:57 +0200 Subject: [PATCH 006/142] added sha1 encoding for each document --- Cargo.lock | 411 ++++++++++---------- crates/semantic_index/Cargo.toml | 1 + crates/semantic_index/src/db.rs | 21 +- crates/semantic_index/src/embedding.rs | 2 +- crates/semantic_index/src/parsing.rs | 14 + crates/semantic_index/src/semantic_index.rs | 3 +- 6 files changed, 245 insertions(+), 207 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3edf9acab3..8048398ef8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -88,9 +88,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" dependencies = [ "memchr", ] @@ -140,7 +140,7 @@ source = "git+https://github.com/zed-industries/alacritty?rev=33306142195b354ef3 dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -151,7 +151,7 @@ dependencies = [ "alacritty_config", "alacritty_config_derive", "base64 0.13.1", - "bitflags 2.3.3", + "bitflags 2.4.0", "home", "libc", "log", @@ -268,9 +268,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "c677ab05e09154296dd37acecd46420c17b9713e8366facafa8fc0885167cf4c" dependencies = [ "anstyle", "windows-sys", @@ -278,9 +278,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.72" +version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b13c32d80ecc7ab747b80c3784bce54ee8a7a0cc4fbda9bf4cda2cf6fe90854" +checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" [[package]] name = "arrayref" @@ -337,7 +337,7 @@ dependencies = [ "futures-core", "futures-io", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -351,7 +351,7 @@ dependencies = [ "futures-core", "futures-io", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -411,15 +411,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -482,7 +482,7 @@ checksum = "0e97ce7de6cf12de5d7226c73f5ba9811622f4db3a5b91b55c53e987e5f91cba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -505,7 +505,7 @@ dependencies = [ "log", "memchr", "once_cell", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "wasm-bindgen-futures", @@ -519,7 +519,7 @@ checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -530,7 +530,7 @@ checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -567,13 +567,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.72" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6dde6e4ed435a4c1ee4e73592f5ba9da2151af10076cc04858746af9352d09" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -586,7 +586,7 @@ dependencies = [ "futures-io", "futures-util", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tungstenite 0.16.0", ] @@ -681,12 +681,12 @@ dependencies = [ "http", "http-body", "hyper", - "itoa 1.0.9", + "itoa", "matchit", "memchr", "mime", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -727,7 +727,7 @@ dependencies = [ "futures-util", "http", "mime", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "tokio", @@ -831,7 +831,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn 2.0.28", + "syn 2.0.29", "which", ] @@ -858,9 +858,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -996,7 +996,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" dependencies = [ "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "serde", ] @@ -1156,11 +1156,12 @@ checksum = "a2698f953def977c68f935bb0dfa959375ad4638570e969e2f1e9f433cbf1af6" [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -1251,9 +1252,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.3.19" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd304a20bff958a57f04c4e96a2e7594cc4490a0e809cbd48bb6437edaa452d" +checksum = "03aef18ddf7d879c15ce20f04826ef8418101c7e528014c3eeea13321047dca3" dependencies = [ "clap_builder", "clap_derive 4.3.12", @@ -1262,9 +1263,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.3.19" +version = "4.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01c6a3f08f1fe5662a35cfe393aec09c4df95f60ee93b7556505260f75eee9e1" +checksum = "f8ce6fffb678c9b80a70b6b6de0aad31df727623a70fd9a842c30cd573e2fa98" dependencies = [ "anstream", "anstyle", @@ -1294,7 +1295,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -1355,7 +1356,7 @@ dependencies = [ "sum_tree", "tempfile", "thiserror", - "time 0.3.24", + "time 0.3.25", "tiny_http", "url", "util", @@ -1457,7 +1458,7 @@ dependencies = [ "sha-1 0.9.8", "sqlx", "theme", - "time 0.3.24", + "time 0.3.25", "tokio", "tokio-tungstenite", "toml 0.5.11", @@ -1984,7 +1985,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "socket2", + "socket2 0.4.9", "winapi 0.3.9", ] @@ -2065,9 +2066,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8810e7e2cf385b1e9b50d68264908ec367ba642c96d02edfe61c39e88e2a3c01" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" dependencies = [ "serde", ] @@ -2246,9 +2247,9 @@ dependencies = [ [[package]] name = "dyn-clone" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "304e6508efa593091e97a9abbc10f90aa7ca635b6d2784feff3c89d41dd12272" +checksum = "bbfc4744c1b8f2a09adc0e55242f60b1af195d88596bd8700be74418c056c555" [[package]] name = "editor" @@ -2361,9 +2362,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "erased-serde" -version = "0.3.28" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da96524cc884f6558f1769b6c46686af2fe8e8b4cd253bd5a3cdba8181b8e070" +checksum = "fc978899517288e3ebbd1a3bfc1d9537dbb87eeab149e53ea490e63bcdff561a" dependencies = [ "serde", ] @@ -2526,13 +2527,13 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if 1.0.0", "libc", - "redox_syscall 0.2.16", + "redox_syscall 0.3.5", "windows-sys", ] @@ -2544,9 +2545,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide 0.7.1", @@ -2687,7 +2688,7 @@ dependencies = [ "smol", "sum_tree", "tempfile", - "time 0.3.24", + "time 0.3.25", "util", ] @@ -2825,7 +2826,7 @@ dependencies = [ "futures-io", "memchr", "parking", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "waker-fn", ] @@ -2837,7 +2838,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -2866,7 +2867,7 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "pin-utils", "slab", "tokio-io", @@ -2989,11 +2990,11 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" +checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "bstr", "fnv", "log", @@ -3078,7 +3079,7 @@ dependencies = [ "smol", "sqlez", "sum_tree", - "time 0.3.24", + "time 0.3.25", "tiny-skia", "usvg", "util", @@ -3293,7 +3294,7 @@ checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" dependencies = [ "bytes 1.4.0", "fnv", - "itoa 1.0.9", + "itoa", ] [[package]] @@ -3304,7 +3305,7 @@ checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ "bytes 1.4.0", "http", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", ] [[package]] @@ -3321,9 +3322,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "human_bytes" @@ -3352,9 +3353,9 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa 1.0.9", - "pin-project-lite 0.2.10", - "socket2", + "itoa", + "pin-project-lite 0.2.12", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -3368,7 +3369,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" dependencies = [ "hyper", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tokio-io-timeout", ] @@ -3586,7 +3587,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi 0.3.2", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -3626,12 +3627,6 @@ dependencies = [ "either", ] -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - [[package]] name = "itoa" version = "1.0.9" @@ -4058,9 +4053,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "serde", "value-bag", @@ -4091,9 +4086,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.94.0" +version = "0.94.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b63735a13a1f9cd4f4835223d828ed9c2e35c8c5e61837774399f558b6a1237" +checksum = "c66bfd44a06ae10647fe3f8214762e9369fd4248df1350924b4ef9e770a85ea1" dependencies = [ "bitflags 1.3.2", "serde", @@ -4751,9 +4746,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "729b745ad4a5575dd06a3e1af1414bd330ee561c01b3899eb584baeaa8def17e" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -4772,7 +4767,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -4783,9 +4778,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "866b5f16f90776b9bb8dc1e1802ac6f0513de3a7a7465867bfbc563dc737faac" dependencies = [ "cc", "libc", @@ -4920,7 +4915,7 @@ dependencies = [ "libc", "redox_syscall 0.3.5", "smallvec", - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -5012,12 +5007,12 @@ dependencies = [ [[package]] name = "petgraph" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dd7d28ee937e54fe3080c91faa1c3a46c06de6252988a7f4592ba2310ef22a4" +checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 1.9.3", + "indexmap 2.0.0", ] [[package]] @@ -5045,22 +5040,22 @@ checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5071,9 +5066,9 @@ checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" [[package]] name = "pin-utils" @@ -5098,7 +5093,7 @@ dependencies = [ "line-wrap", "quick-xml", "serde", - "time 0.3.24", + "time 0.3.25", ] [[package]] @@ -5163,7 +5158,7 @@ dependencies = [ "concurrent-queue", "libc", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "windows-sys", ] @@ -5213,7 +5208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64d9ba0963cdcea2e1b2230fbae2bab30eb25a174be395c41e764bfb65dd62" dependencies = [ "proc-macro2", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -5553,9 +5548,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.32" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -5778,13 +5773,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", - "regex-automata 0.3.4", + "regex-automata 0.3.6", "regex-syntax 0.7.4", ] @@ -5799,11 +5794,11 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7b6d6190b7594385f61bd3911cd1be99dfddcfc365a4160cc2ab5bff4aed294" +checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" dependencies = [ - "aho-corasick 1.0.2", + "aho-corasick 1.0.4", "memchr", "regex-syntax 0.7.4", ] @@ -5873,7 +5868,7 @@ dependencies = [ "native-tls", "once_cell", "percent-encoding", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "serde", "serde_json", "serde_urlencoded", @@ -6093,7 +6088,7 @@ dependencies = [ "proc-macro2", "quote", "rust-embed-utils", - "syn 2.0.28", + "syn 2.0.29", "walkdir", ] @@ -6164,7 +6159,7 @@ dependencies = [ "bitflags 1.3.2", "errno 0.2.8", "io-lifetimes 0.5.3", - "itoa 1.0.9", + "itoa", "libc", "linux-raw-sys 0.0.42", "once_cell", @@ -6187,11 +6182,11 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno 0.3.2", "libc", "linux-raw-sys 0.4.5", @@ -6393,7 +6388,7 @@ dependencies = [ "serde_json", "sqlx", "thiserror", - "time 0.3.24", + "time 0.3.25", "tracing", "url", "uuid 1.4.1", @@ -6421,7 +6416,7 @@ dependencies = [ "rust_decimal", "sea-query-derive", "serde_json", - "time 0.3.24", + "time 0.3.25", "uuid 1.4.1", ] @@ -6436,7 +6431,7 @@ dependencies = [ "sea-query", "serde_json", "sqlx", - "time 0.3.24", + "time 0.3.25", "uuid 1.4.1", ] @@ -6564,10 +6559,11 @@ dependencies = [ "serde", "serde_json", "settings", + "sha1", "smol", "tempdir", "theme", - "tiktoken-rs 0.5.0", + "tiktoken-rs 0.5.1", "tree-sitter", "tree-sitter-cpp", "tree-sitter-elixir", @@ -6615,22 +6611,22 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" +checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.180" +version = "1.0.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" +checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6655,24 +6651,24 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.104" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "indexmap 2.0.0", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] [[package]] name = "serde_json_lenient" -version = "0.1.4" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d7b9ce5b0a63c6269b9623ed828b39259545a6ec0d8a35d6135ad6af6232add" +checksum = "29591aaa3a13f5ad0f2dd1a8a21bcddab11eaae7c3522b20ade2e85e9df52206" dependencies = [ - "indexmap 1.9.3", - "itoa 0.4.8", + "indexmap 2.0.0", + "itoa", "ryu", "serde", ] @@ -6685,7 +6681,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -6704,7 +6700,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa 1.0.9", + "itoa", "ryu", "serde", ] @@ -6991,6 +6987,16 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -7090,7 +7096,7 @@ dependencies = [ "hkdf", "hmac 0.12.1", "indexmap 1.9.3", - "itoa 1.0.9", + "itoa", "libc", "libsqlite3-sys", "log", @@ -7113,7 +7119,7 @@ dependencies = [ "sqlx-rt", "stringprep", "thiserror", - "time 0.3.24", + "time 0.3.25", "tokio-stream", "url", "uuid 1.4.1", @@ -7236,7 +7242,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dc09e9364c2045ab5fa38f7b04d077b3359d30c4c2b3ec4bae67a358bd64326" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7247,7 +7253,7 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ada6f627e38cbb8860283649509d87bc4a5771141daa41c78fd31f2b9485888d" dependencies = [ - "itoa 1.0.9", + "itoa", "ryu", "sval", ] @@ -7312,9 +7318,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.28" +version = "2.0.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" +checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" dependencies = [ "proc-macro2", "quote", @@ -7398,14 +7404,14 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.7.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486094ee78b2e5038a6382ed7645bc084dc2ec433426ca4c3cb61e2007b8998" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ "cfg-if 1.0.0", "fastrand 2.0.0", "redox_syscall 0.3.5", - "rustix 0.38.4", + "rustix 0.38.8", "windows-sys", ] @@ -7552,22 +7558,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "611040a08a0439f8248d1990b111c95baa9c704c805fa1f62104b39655fd7f90" +checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.44" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" +checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7614,9 +7620,9 @@ dependencies = [ [[package]] name = "tiktoken-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a99d843674a3468b4a9200a565bbe909a0152f95e82a52feae71e6bf2d4b49d" +checksum = "2bf14cb08d8fda6e484c75ec2bfb6bcef48347d47abcd011fa9d56ee995a3da0" dependencies = [ "anyhow", "base64 0.21.2", @@ -7640,12 +7646,12 @@ dependencies = [ [[package]] name = "time" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79eabcd964882a646b3584543ccabeae7869e9ac32a46f6f22b7a5bd405308b" +checksum = "b0fdd63d58b18d663fbdf70e049f00a22c8e42be082203be7f26589213cd75ea" dependencies = [ "deranged", - "itoa 1.0.9", + "itoa", "serde", "time-core", "time-macros", @@ -7710,20 +7716,19 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes 1.4.0", "libc", "mio 0.8.8", "num_cpus", "parking_lot 0.12.1", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", "windows-sys", ] @@ -7745,7 +7750,7 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" dependencies = [ - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7757,7 +7762,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -7788,7 +7793,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" dependencies = [ "futures-core", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7814,7 +7819,7 @@ dependencies = [ "futures-core", "futures-sink", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", ] @@ -7828,7 +7833,7 @@ dependencies = [ "futures-core", "futures-io", "futures-sink", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tokio", "tracing", ] @@ -7917,7 +7922,7 @@ dependencies = [ "futures-util", "indexmap 1.9.3", "pin-project", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "rand 0.8.5", "slab", "tokio", @@ -7940,7 +7945,7 @@ dependencies = [ "http", "http-body", "http-range-header", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tower", "tower-layer", "tower-service", @@ -7966,7 +7971,7 @@ checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if 1.0.0", "log", - "pin-project-lite 0.2.10", + "pin-project-lite 0.2.12", "tracing-attributes", "tracing-core", ] @@ -7979,7 +7984,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] @@ -8064,9 +8069,9 @@ dependencies = [ [[package]] name = "tree-sitter-c" -version = "0.20.4" +version = "0.20.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa1bb73a4101c88775e4fefcd0543ee25e192034484a5bd45cb99eefb997dca9" +checksum = "30b03bdf218020057abee831581a74bff8c298323d6c6cd1a70556430ded9f4b" dependencies = [ "cc", "tree-sitter", @@ -8213,9 +8218,9 @@ dependencies = [ [[package]] name = "tree-sitter-python" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f47ebd9cac632764b2f4389b08517bf2ef895431dd163eb562e3d2062cc23a14" +checksum = "e6c93b1b1fbd0d399db3445f51fd3058e43d0b4dcff62ddbdb46e66550978aa5" dependencies = [ "cc", "tree-sitter", @@ -8242,9 +8247,9 @@ dependencies = [ [[package]] name = "tree-sitter-rust" -version = "0.20.3" +version = "0.20.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797842733e252dc11ae5d403a18060bf337b822fc2ae5ddfaa6ff4d9cc20bda6" +checksum = "b0832309b0b2b6d33760ce5c0e818cb47e1d72b468516bfe4134408926fa7594" dependencies = [ "cc", "tree-sitter", @@ -8773,7 +8778,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-shared", ] @@ -8807,7 +8812,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -9044,9 +9049,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.1" +version = "63.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" +checksum = "2560471f60a48b77fccefaf40796fda61c97ce1e790b59dfcec9dc3995c9f63a" dependencies = [ "leb128", "memchr", @@ -9056,11 +9061,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.69" +version = "1.0.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" +checksum = "3bdc306c2c4c2f2bf2ba69e083731d0d2a77437fc6a350a19db139636e7e416c" dependencies = [ - "wast 62.0.1", + "wast 63.0.0", ] [[package]] @@ -9262,7 +9267,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9271,7 +9276,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets 0.48.5", ] [[package]] @@ -9291,17 +9296,17 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.48.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -9312,9 +9317,9 @@ checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" @@ -9324,9 +9329,9 @@ checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_aarch64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" @@ -9336,9 +9341,9 @@ checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" @@ -9348,9 +9353,9 @@ checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_i686_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" @@ -9360,9 +9365,9 @@ checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnu" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" @@ -9372,9 +9377,9 @@ checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_gnullvm" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" @@ -9384,15 +9389,15 @@ checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" [[package]] name = "windows_x86_64_msvc" -version = "0.48.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.2" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bd122eb777186e60c3fdf765a58ac76e41c582f1f535fbf3314434c6b58f3f7" +checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" dependencies = [ "memchr", ] @@ -9522,7 +9527,7 @@ name = "xtask" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.3.19", + "clap 4.3.23", "schemars", "serde_json", "theme", @@ -9703,7 +9708,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.28", + "syn 2.0.29", ] [[package]] diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 3c7a6ff5df..4e817fcbe2 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -38,6 +38,7 @@ parking_lot.workspace = true rand.workspace = true schemars.workspace = true globset.workspace = true +sha1 = "0.10.5" [dev-dependencies] gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/semantic_index/src/db.rs b/crates/semantic_index/src/db.rs index e57a5d733f..60ecf3b45f 100644 --- a/crates/semantic_index/src/db.rs +++ b/crates/semantic_index/src/db.rs @@ -26,6 +26,9 @@ pub struct FileRecord { #[derive(Debug)] struct Embedding(pub Vec); +#[derive(Debug)] +struct Sha1(pub Vec); + impl FromSql for Embedding { fn column_result(value: ValueRef) -> FromSqlResult { let bytes = value.as_blob()?; @@ -37,6 +40,17 @@ impl FromSql for Embedding { } } +impl FromSql for Sha1 { + fn column_result(value: ValueRef) -> FromSqlResult { + let bytes = value.as_blob()?; + let sha1: Result, Box> = bincode::deserialize(bytes); + if sha1.is_err() { + return Err(rusqlite::types::FromSqlError::Other(sha1.unwrap_err())); + } + return Ok(Sha1(sha1.unwrap())); + } +} + pub struct VectorDatabase { db: rusqlite::Connection, } @@ -132,6 +146,7 @@ impl VectorDatabase { end_byte INTEGER NOT NULL, name VARCHAR NOT NULL, embedding BLOB NOT NULL, + sha1 BLOB NOT NULL, FOREIGN KEY(file_id) REFERENCES files(id) ON DELETE CASCADE )", [], @@ -182,15 +197,17 @@ impl VectorDatabase { // I imagine we can speed this up with a bulk insert of some kind. for document in documents { let embedding_blob = bincode::serialize(&document.embedding)?; + let sha_blob = bincode::serialize(&document.sha1)?; self.db.execute( - "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding) VALUES (?1, ?2, ?3, ?4, $5)", + "INSERT INTO documents (file_id, start_byte, end_byte, name, embedding, sha1) VALUES (?1, ?2, ?3, ?4, ?5, ?6)", params![ file_id, document.range.start.to_string(), document.range.end.to_string(), document.name, - embedding_blob + embedding_blob, + sha_blob ], )?; } diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 77457ec7f6..4fc247bfcc 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -39,7 +39,7 @@ struct OpenAIEmbeddingResponse { #[derive(Debug, Deserialize)] struct OpenAIEmbedding { - embedding: Vec, + embedding: Vec, index: usize, object: String, } diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index cef23862c5..4aefb0b00d 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -1,5 +1,6 @@ use anyhow::{anyhow, Ok, Result}; use language::{Grammar, Language}; +use sha1::{Digest, Sha1}; use std::{ cmp::{self, Reverse}, collections::HashSet, @@ -15,6 +16,7 @@ pub struct Document { pub range: Range, pub content: String, pub embedding: Vec, + pub sha1: [u8; 20], } const CODE_CONTEXT_TEMPLATE: &str = @@ -63,11 +65,15 @@ impl CodeContextRetriever { .replace("", language_name.as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: language_name.to_string(), + sha1: sha1.finalize().into(), }]) } @@ -76,11 +82,15 @@ impl CodeContextRetriever { .replace("", relative_path.to_string_lossy().as_ref()) .replace("", &content); + let mut sha1 = Sha1::new(); + sha1.update(&document_span); + Ok(vec![Document { range: 0..content.len(), content: document_span, embedding: Vec::new(), name: "Markdown".to_string(), + sha1: sha1.finalize().into(), }]) } @@ -253,11 +263,15 @@ impl CodeContextRetriever { ); } + let mut sha1 = Sha1::new(); + sha1.update(&document_content); + documents.push(Document { name, content: document_content, range: item_range.clone(), embedding: vec![], + sha1: sha1.finalize().into(), }) } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 5aaecac733..f567ca8770 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -34,7 +34,7 @@ use util::{ ResultExt, }; -const SEMANTIC_INDEX_VERSION: usize = 6; +const SEMANTIC_INDEX_VERSION: usize = 7; const EMBEDDINGS_BATCH_SIZE: usize = 80; pub fn init( @@ -92,6 +92,7 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, + file_mtimes: HashMap, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, } From ced2b2aec3e9fee2e598e0a0125f843e05c4e906 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 11:58:48 +0200 Subject: [PATCH 007/142] reworked ProjectState to include additional context --- crates/search/src/project_search.rs | 12 +- crates/semantic_index/src/embedding.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 138 ++++++++++++++++-- .../src/semantic_index_tests.rs | 7 + 4 files changed, 146 insertions(+), 13 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 196d5589f4..7e3585656a 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -640,6 +640,7 @@ impl ProjectSearchView { self.search_options = SearchOptions::none(); let project = self.model.read(cx).project.clone(); + let index_task = semantic_index.update(cx, |semantic_index, cx| { semantic_index.index_project(project, cx) }); @@ -759,7 +760,7 @@ impl ProjectSearchView { } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { - let project; + let mut project; let excerpts; let mut query_text = String::new(); let mut options = SearchOptions::NONE; @@ -843,6 +844,15 @@ impl ProjectSearchView { .detach(); let filters_enabled = false; + // Initialize Semantic Index if Needed + if SemanticIndex::enabled(cx) { + let model = model.read(cx); + project = model.project.clone(); + SemanticIndex::global(cx).map(|semantic| { + semantic.update(cx, |this, cx| this.initialize_project(project, cx)) + }); + } + // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 4fc247bfcc..77457ec7f6 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -39,7 +39,7 @@ struct OpenAIEmbeddingResponse { #[derive(Debug, Deserialize)] struct OpenAIEmbedding { - embedding: Vec, + embedding: Vec, index: usize, object: String, } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index f567ca8770..2b803e36ac 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -92,7 +92,8 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - file_mtimes: HashMap, + worktree_file_mtimes: HashMap>, + subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, } @@ -113,6 +114,25 @@ impl JobHandle { } } impl ProjectState { + fn new( + subscription: gpui::Subscription, + worktree_db_ids: Vec<(WorktreeId, i64)>, + worktree_file_mtimes: HashMap>, + outstanding_job_count_rx: watch::Receiver, + _outstanding_job_count_tx: Arc>>, + ) -> Self { + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + + Self { + worktree_db_ids, + worktree_file_mtimes, + outstanding_job_count_rx, + _outstanding_job_count_tx, + subscription, + } + } + fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids .iter() @@ -577,6 +597,84 @@ impl SemanticIndex { }) } + pub fn initialize_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) { + let worktree_scans_complete = project + .read(cx) + .worktrees(cx) + .map(|worktree| { + let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); + async move { + scan_complete.await; + } + }) + .collect::>(); + + let worktree_db_ids = project + .read(cx) + .worktrees(cx) + .map(|worktree| { + self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf()) + }) + .collect::>(); + + let _subscription = cx.subscribe(&project, |this, project, event, cx| { + if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { + todo!(); + // this.project_entries_changed(project, changes, cx, worktree_id); + } + }); + + cx.spawn(|this, mut cx| async move { + futures::future::join_all(worktree_scans_complete).await; + + let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; + let worktrees = project.read_with(&cx, |project, cx| { + project + .worktrees(cx) + .map(|worktree| worktree.read(cx).snapshot()) + .collect::>() + }); + + let mut worktree_file_mtimes = HashMap::new(); + let mut db_ids_by_worktree_id = HashMap::new(); + + for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { + let db_id = db_id?; + db_ids_by_worktree_id.insert(worktree.id(), db_id); + worktree_file_mtimes.insert( + worktree.id(), + this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) + .await?, + ); + } + + let worktree_db_ids = db_ids_by_worktree_id + .iter() + .map(|(a, b)| (*a, *b)) + .collect(); + + let (job_count_tx, job_count_rx) = watch::channel_with(0); + let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + this.update(&mut cx, |this, _| { + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes.clone(), + job_count_rx, + job_count_tx, + ); + this.projects.insert(project.downgrade(), project_state); + }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx) + } + pub fn index_project( &mut self, project: ModelHandle, @@ -605,6 +703,22 @@ impl SemanticIndex { let db_update_tx = self.db_update_tx.clone(); let parsing_files_tx = self.parsing_files_tx.clone(); + let state = self.projects.get(&project.downgrade()); + let state = if state.is_none() { + return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); + } else { + state.unwrap() + }; + + let state = state.clone().to_owned(); + + let _subscription = cx.subscribe(&project, |this, project, event, _cx| { + if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { + todo!(); + // this.project_entries_changed(project, changes, cx, worktree_id); + } + }); + cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -629,20 +743,22 @@ impl SemanticIndex { ); } + let worktree_db_ids = db_ids_by_worktree_id + .iter() + .map(|(a, b)| (*a, *b)) + .collect(); + let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); this.update(&mut cx, |this, _| { - this.projects.insert( - project.downgrade(), - ProjectState { - worktree_db_ids: db_ids_by_worktree_id - .iter() - .map(|(a, b)| (*a, *b)) - .collect(), - outstanding_job_count_rx: job_count_rx.clone(), - _outstanding_job_count_tx: job_count_tx.clone(), - }, + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes.clone(), + job_count_rx.clone(), + job_count_tx.clone(), ); + this.projects.insert(project.downgrade(), project_state); }); cx.background() diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 07ddce4d37..0ac5953f0b 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -86,6 +86,13 @@ async fn test_semantic_index(cx: &mut TestAppContext) { .unwrap(); let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + + store + .update(cx, |store, cx| { + store.initialize_project(project.clone(), cx) + }) + .await; + let (file_count, outstanding_file_count) = store .update(cx, |store, cx| store.index_project(project.clone(), cx)) .await From aabdfa210f3afbaea6929f2fae319a93b7ab8c67 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 14:45:27 +0200 Subject: [PATCH 008/142] working on initialization + index breakup --- crates/search/src/project_search.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 308 +++++++++++++------- 2 files changed, 197 insertions(+), 113 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 7e3585656a..ca317c0ded 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -849,7 +849,7 @@ impl ProjectSearchView { let model = model.read(cx); project = model.project.clone(); SemanticIndex::global(cx).map(|semantic| { - semantic.update(cx, |this, cx| this.initialize_project(project, cx)) + semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); }); } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 2b803e36ac..8849b643c5 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -96,6 +96,7 @@ struct ProjectState { subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, + queue: HashMap>, } #[derive(Clone)] @@ -130,9 +131,25 @@ impl ProjectState { outstanding_job_count_rx, _outstanding_job_count_tx, subscription, + queue: HashMap::new(), } } + fn add_to_queue(&mut self, worktree_id: WorktreeId, operation: IndexOperation) { + if let Some(worktree_queue) = self.queue.get_mut(&worktree_id) { + worktree_queue.push(operation); + } else { + self.queue.insert(worktree_id, vec![operation]); + } + } + + fn pop(&mut self) -> Option { + self.queue + .iter_mut() + .next() + .and_then(|(_, mut entry)| entry.pop()) + } + fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { self.worktree_db_ids .iter() @@ -158,6 +175,7 @@ impl ProjectState { } } +#[derive(Clone)] pub struct PendingFile { worktree_db_id: i64, relative_path: PathBuf, @@ -167,6 +185,12 @@ pub struct PendingFile { job_handle: JobHandle, } +#[derive(Clone)] +enum IndexOperation { + IndexFile { file: PendingFile }, + DeleteFile { file: PendingFile }, +} + pub struct SearchResult { pub buffer: ModelHandle, pub range: Range, @@ -628,102 +652,12 @@ impl SemanticIndex { } }); - cx.spawn(|this, mut cx| async move { - futures::future::join_all(worktree_scans_complete).await; - - let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - let worktrees = project.read_with(&cx, |project, cx| { - project - .worktrees(cx) - .map(|worktree| worktree.read(cx).snapshot()) - .collect::>() - }); - - let mut worktree_file_mtimes = HashMap::new(); - let mut db_ids_by_worktree_id = HashMap::new(); - - for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { - let db_id = db_id?; - db_ids_by_worktree_id.insert(worktree.id(), db_id); - worktree_file_mtimes.insert( - worktree.id(), - this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) - .await?, - ); - } - - let worktree_db_ids = db_ids_by_worktree_id - .iter() - .map(|(a, b)| (*a, *b)) - .collect(); - - let (job_count_tx, job_count_rx) = watch::channel_with(0); - let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - this.update(&mut cx, |this, _| { - let project_state = ProjectState::new( - _subscription, - worktree_db_ids, - worktree_file_mtimes.clone(), - job_count_rx, - job_count_tx, - ); - this.projects.insert(project.downgrade(), project_state); - }); - - anyhow::Ok(()) - }) - .detach_and_log_err(cx) - } - - pub fn index_project( - &mut self, - project: ModelHandle, - cx: &mut ModelContext, - ) -> Task)>> { - let t0 = Instant::now(); - let worktree_scans_complete = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - let scan_complete = worktree.read(cx).as_local().unwrap().scan_complete(); - async move { - scan_complete.await; - } - }) - .collect::>(); - let worktree_db_ids = project - .read(cx) - .worktrees(cx) - .map(|worktree| { - self.find_or_create_worktree(worktree.read(cx).abs_path().to_path_buf()) - }) - .collect::>(); - let language_registry = self.language_registry.clone(); - let db_update_tx = self.db_update_tx.clone(); - let parsing_files_tx = self.parsing_files_tx.clone(); - - let state = self.projects.get(&project.downgrade()); - let state = if state.is_none() { - return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); - } else { - state.unwrap() - }; - - let state = state.clone().to_owned(); - - let _subscription = cx.subscribe(&project, |this, project, event, _cx| { - if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - todo!(); - // this.project_entries_changed(project, changes, cx, worktree_id); - } - }); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - let worktrees = project.read_with(&cx, |project, cx| { project .worktrees(cx) @@ -733,6 +667,7 @@ impl SemanticIndex { let mut worktree_file_mtimes = HashMap::new(); let mut db_ids_by_worktree_id = HashMap::new(); + for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { let db_id = db_id?; db_ids_by_worktree_id.insert(worktree.id(), db_id); @@ -761,10 +696,12 @@ impl SemanticIndex { this.projects.insert(project.downgrade(), project_state); }); - cx.background() + let worktree_files = cx + .background() .spawn(async move { - let mut count = 0; + let mut worktree_files = HashMap::new(); for worktree in worktrees.into_iter() { + let mut candidate_files = Vec::new(); let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -773,6 +710,7 @@ impl SemanticIndex { .language_for_file(&absolute_path, None) .await { + // Test if file is valid parseable file if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) && &language.name().as_ref() != &"Markdown" && language @@ -789,40 +727,186 @@ impl SemanticIndex { .map_or(false, |existing_mtime| existing_mtime == file.mtime); if !already_stored { - count += 1; - let job_handle = JobHandle::new(&job_count_tx); - parsing_files_tx - .try_send(PendingFile { + candidate_files.push(IndexOperation::IndexFile { + file: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, absolute_path, language, job_handle, modified_time: file.mtime, - }) - .unwrap(); + }, + }); } } } - for file in file_mtimes.keys() { - db_update_tx - .try_send(DbOperation::Delete { - worktree_id: db_ids_by_worktree_id[&worktree.id()], - path: file.to_owned(), - }) - .unwrap(); - } + + worktree_files.insert(worktree.id(), candidate_files); } - log::trace!( - "walking worktree took {:?} milliseconds", - t0.elapsed().as_millis() - ); - anyhow::Ok((count, job_count_rx)) + anyhow::Ok(worktree_files) }) - .await + .await?; + + this.update(&mut cx, |this, cx| { + if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { + for (worktree_id, index_operations) in &worktree_files { + for op in index_operations { + project_state.add_to_queue(*worktree_id, op.clone()); + } + } + } + }); + + cx.background().spawn(async move { anyhow::Ok(()) }).await }) + .detach_and_log_err(cx) + } + + pub fn index_project( + &mut self, + project: ModelHandle, + cx: &mut ModelContext, + ) -> Task)>> { + let state = self.projects.get_mut(&project.downgrade()); + let state = if state.is_none() { + return Task::Ready(Some(Err(anyhow!("Project not yet initialized")))); + } else { + state.unwrap() + }; + + let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); + let job_count_rx = state.outstanding_job_count_rx.clone(); + let count = state.queue.values().map(Vec::len).sum(); + cx.spawn(|this, mut cx| async move { + this.update(&mut cx, |this, cx| { + let Some(mut state) = this.projects.get_mut(&project.downgrade()) else { + return; + }; + let Some(mut index_operation) = state.pop() else { return;}; + let _ = match index_operation { + IndexOperation::IndexFile { file } => { + parsing_files_tx.try_send(file); + } + IndexOperation::DeleteFile { file } => { + db_update_tx.try_send(DbOperation::Delete { + worktree_id: file.worktree_db_id, + path: file.relative_path, + }); + } + }; + }); + }) + .detach(); + + Task::Ready(Some(Ok((count, job_count_rx)))) + + // cx.spawn(|this, mut cx| async move { + // futures::future::join_all(worktree_scans_complete).await; + + // let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; + + // let worktrees = project.read_with(&cx, |project, cx| { + // project + // .worktrees(cx) + // .map(|worktree| worktree.read(cx).snapshot()) + // .collect::>() + // }); + + // let mut worktree_file_mtimes = HashMap::new(); + // let mut db_ids_by_worktree_id = HashMap::new(); + // for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { + // let db_id = db_id?; + // db_ids_by_worktree_id.insert(worktree.id(), db_id); + // worktree_file_mtimes.insert( + // worktree.id(), + // this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) + // .await?, + // ); + // } + + // let worktree_db_ids = db_ids_by_worktree_id + // .iter() + // .map(|(a, b)| (*a, *b)) + // .collect(); + + // let (job_count_tx, job_count_rx) = watch::channel_with(0); + // let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + // this.update(&mut cx, |this, _| { + // let project_state = ProjectState::new( + // _subscription, + // worktree_db_ids, + // worktree_file_mtimes.clone(), + // job_count_rx.clone(), + // job_count_tx.clone(), + // ); + // this.projects.insert(project.downgrade(), project_state); + // }); + + // cx.background() + // .spawn(async move { + // let mut count = 0; + // for worktree in worktrees.into_iter() { + // let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); + // for file in worktree.files(false, 0) { + // let absolute_path = worktree.absolutize(&file.path); + + // if let Ok(language) = language_registry + // .language_for_file(&absolute_path, None) + // .await + // { + // if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + // && &language.name().as_ref() != &"Markdown" + // && language + // .grammar() + // .and_then(|grammar| grammar.embedding_config.as_ref()) + // .is_none() + // { + // continue; + // } + + // let path_buf = file.path.to_path_buf(); + // let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); + // let already_stored = stored_mtime + // .map_or(false, |existing_mtime| existing_mtime == file.mtime); + + // if !already_stored { + // count += 1; + + // let job_handle = JobHandle::new(&job_count_tx); + // parsing_files_tx + // .try_send(PendingFile { + // worktree_db_id: db_ids_by_worktree_id[&worktree.id()], + // relative_path: path_buf, + // absolute_path, + // language, + // job_handle, + // modified_time: file.mtime, + // }) + // .unwrap(); + // } + // } + // } + // for file in file_mtimes.keys() { + // db_update_tx + // .try_send(DbOperation::Delete { + // worktree_id: db_ids_by_worktree_id[&worktree.id()], + // path: file.to_owned(), + // }) + // .unwrap(); + // } + // } + + // log::trace!( + // "walking worktree took {:?} milliseconds", + // t0.elapsed().as_millis() + // ); + // anyhow::Ok((count, job_count_rx)) + // }) + // .await + // }) } pub fn outstanding_job_count_rx( From 328b7e523c4d7897380af9f3ec17c8f2e904356d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 22 Aug 2023 15:01:21 +0200 Subject: [PATCH 009/142] reorganized to stop the race --- crates/semantic_index/src/semantic_index.rs | 32 ++++++++++++++------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 8849b643c5..79e649838a 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -685,17 +685,19 @@ impl SemanticIndex { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - this.update(&mut cx, |this, _| { - let project_state = ProjectState::new( - _subscription, - worktree_db_ids, - worktree_file_mtimes.clone(), - job_count_rx.clone(), - job_count_tx.clone(), - ); - this.projects.insert(project.downgrade(), project_state); - }); + let job_count_tx_longlived = job_count_tx.clone(); + // this.update(&mut cx, |this, _| { + // let project_state = ProjectState::new( + // _subscription, + // worktree_db_ids, + // worktree_file_mtimes.clone(), + // job_count_rx.clone(), + // job_count_tx.clone(), + // ); + // this.projects.insert(project.downgrade(), project_state); + // }); + let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { @@ -750,6 +752,14 @@ impl SemanticIndex { .await?; this.update(&mut cx, |this, cx| { + let project_state = ProjectState::new( + _subscription, + worktree_db_ids, + worktree_file_mtimes_all, + job_count_rx, + job_count_tx_longlived, + ); + if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { for (worktree_id, index_operations) in &worktree_files { for op in index_operations { @@ -757,6 +767,8 @@ impl SemanticIndex { } } } + + this.projects.insert(project.downgrade(), project_state); }); cx.background().spawn(async move { anyhow::Ok(()) }).await From 471810a3c2ce1ebf2b4ebdbaaa5e94e43e52ac5b Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 22 Aug 2023 15:27:44 -0400 Subject: [PATCH 010/142] WIP Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- assets/settings/default.json | 18 ++++++++---- crates/project/src/terminals.rs | 49 +++++++++++++++++++++++++++++++++ crates/terminal/src/terminal.rs | 14 ++++++++++ 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883b..f4d77f02cf 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -284,8 +284,6 @@ // "directory": "~/zed/projects/" // } // } - // - // "working_directory": "current_project_directory", // Set the cursor blinking behavior in the terminal. // May take 4 values: @@ -334,13 +332,23 @@ // "line_height": { // "custom": 2 // }, - "line_height": "comfortable" + "line_height": "comfortable", // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. - // "font_size": "15" + // "font_size": "15", // Set the terminal's font family. If this option is not included, // the terminal will default to matching the buffer's font family. - // "font_family": "Zed Mono" + // "font_family": "Zed Mono", + // --- + // Whether or not to automatically search for, and activate, Python virtual + // environments. + // Current limitations: + // - Only ".env", "env", ".venv", and "venv" are searched for at the + // root of the project + // - Only works with Posix-complaint shells + // - Only activates the first virtual environment it finds, regardless + // of the nunber of projects in the workspace. + "automatically_activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index db5996829f..e585b659ee 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -3,6 +3,9 @@ use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +#[cfg(target_os = "macos")] +use std::os::unix::ffi::OsStrExt; + pub struct Terminals { pub(crate) local_handles: Vec>, } @@ -47,6 +50,12 @@ impl Project { }) .detach(); + let setting = settings::get::(cx); + + if setting.automatically_activate_python_virtual_environment { + self.set_up_python_virtual_environment(&terminal_handle, cx); + } + terminal_handle }); @@ -54,6 +63,46 @@ impl Project { } } + fn set_up_python_virtual_environment( + &mut self, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + let virtual_environment = self.find_python_virtual_environment(cx); + if let Some(virtual_environment) = virtual_environment { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + + pub fn find_python_virtual_environment( + &mut self, + cx: &mut ModelContext, + ) -> Option { + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + + let worktree_paths = self + .worktrees(cx) + .map(|worktree| worktree.read(cx).abs_path()); + + for worktree_path in worktree_paths { + for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { + let mut path = worktree_path.join(virtual_environment_name); + path.push("bin/activate"); + + if path.exists() { + return Some(path); + } + } + } + + None + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 3bae06a86d..9b0f0bbc86 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,6 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, + pub automatically_activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -176,6 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, + pub automatically_activate_python_virtual_environment: Option, } impl TerminalSettings { @@ -1018,6 +1020,10 @@ impl Terminal { self.pty_tx.notify(input.into_bytes()); } + fn write_bytes_to_pty(&self, input: Vec) { + self.pty_tx.notify(input); + } + pub fn input(&mut self, input: String) { self.events .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); @@ -1026,6 +1032,14 @@ impl Terminal { self.write_to_pty(input); } + pub fn input_bytes(&mut self, input: Vec) { + self.events + .push_back(InternalEvent::Scroll(AlacScroll::Bottom)); + self.events.push_back(InternalEvent::SetSelection(None)); + + self.write_bytes_to_pty(input); + } + pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool { let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta); if let Some(esc) = esc { From 711f156308322edeeb195978e5773e020ecdf918 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:04:36 -0400 Subject: [PATCH 011/142] WIP --- crates/project/src/terminals.rs | 72 +++++++++++++++++++++------------ 1 file changed, 47 insertions(+), 25 deletions(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index e585b659ee..0fa525a82c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,7 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; -use terminal::{Terminal, TerminalBuilder, TerminalSettings}; +use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,10 +23,14 @@ impl Project { )); } else { let settings = settings::get::(cx); + let automatically_activate_python_virtual_environment = settings + .automatically_activate_python_virtual_environment + .clone(); + let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( working_directory.clone(), - settings.shell.clone(), + shell.clone(), settings.env.clone(), Some(settings.blinking.clone()), settings.alternate_scroll, @@ -50,10 +54,13 @@ impl Project { }) .detach(); - let setting = settings::get::(cx); - - if setting.automatically_activate_python_virtual_environment { - self.set_up_python_virtual_environment(&terminal_handle, cx); + if automatically_activate_python_virtual_environment { + let activate_script_path = self.find_activate_script_path(&shell, cx); + self.activate_python_virtual_environment( + activate_script_path, + &terminal_handle, + cx, + ); } terminal_handle @@ -63,36 +70,35 @@ impl Project { } } - fn set_up_python_virtual_environment( - &mut self, - terminal_handle: &ModelHandle, - cx: &mut ModelContext, - ) { - let virtual_environment = self.find_python_virtual_environment(cx); - if let Some(virtual_environment) = virtual_environment { - // Paths are not strings so we need to jump through some hoops to format the command without `format!` - let mut command = Vec::from("source ".as_bytes()); - command.extend_from_slice(virtual_environment.as_os_str().as_bytes()); - command.push(b'\n'); - - terminal_handle.update(cx, |this, _| this.input_bytes(command)); - } - } - - pub fn find_python_virtual_environment( + pub fn find_activate_script_path( &mut self, + shell: &Shell, cx: &mut ModelContext, ) -> Option { - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + let program = match shell { + terminal::Shell::System => "Figure this out", + terminal::Shell::Program(program) => program, + terminal::Shell::WithArguments { program, args } => program, + }; + + // This is so hacky - find a better way to do this + let script_name = if program.contains("fish") { + "activate.fish" + } else { + "activate" + }; let worktree_paths = self .worktrees(cx) .map(|worktree| worktree.read(cx).abs_path()); + const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; + for worktree_path in worktree_paths { for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/activate"); + path.push("bin/"); + path.push(script_name); if path.exists() { return Some(path); @@ -103,6 +109,22 @@ impl Project { None } + fn activate_python_virtual_environment( + &mut self, + activate_script: Option, + terminal_handle: &ModelHandle, + cx: &mut ModelContext, + ) { + if let Some(activate_script) = activate_script { + // Paths are not strings so we need to jump through some hoops to format the command without `format!` + let mut command = Vec::from("source ".as_bytes()); + command.extend_from_slice(activate_script.as_os_str().as_bytes()); + command.push(b'\n'); + + terminal_handle.update(cx, |this, _| this.input_bytes(command)); + } + } + pub fn local_terminal_handles(&self) -> &Vec> { &self.terminals.local_handles } From 7b170304dfe1ece11ba193867c43a90050026d10 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 04:07:10 -0400 Subject: [PATCH 012/142] Shorten setting name --- assets/settings/default.json | 2 +- crates/project/src/terminals.rs | 7 +++---- crates/terminal/src/terminal.rs | 4 ++-- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index f4d77f02cf..27be6ae5d2 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -348,7 +348,7 @@ // - Only works with Posix-complaint shells // - Only activates the first virtual environment it finds, regardless // of the nunber of projects in the workspace. - "automatically_activate_python_virtual_environment": false + "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 0fa525a82c..539d120e97 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -23,9 +23,8 @@ impl Project { )); } else { let settings = settings::get::(cx); - let automatically_activate_python_virtual_environment = settings - .automatically_activate_python_virtual_environment - .clone(); + let activate_python_virtual_environment = + settings.activate_python_virtual_environment.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -54,7 +53,7 @@ impl Project { }) .detach(); - if automatically_activate_python_virtual_environment { + if activate_python_virtual_environment { let activate_script_path = self.find_activate_script_path(&shell, cx); self.activate_python_virtual_environment( activate_script_path, diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 9b0f0bbc86..73ff09225f 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -158,7 +158,7 @@ pub struct TerminalSettings { pub dock: TerminalDockPosition, pub default_width: f32, pub default_height: f32, - pub automatically_activate_python_virtual_environment: bool, + pub activate_python_virtual_environment: bool, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -177,7 +177,7 @@ pub struct TerminalSettingsContent { pub dock: Option, pub default_width: Option, pub default_height: Option, - pub automatically_activate_python_virtual_environment: Option, + pub activate_python_virtual_environment: Option, } impl TerminalSettings { From 09fd99b1e3380f239c4971ea625a526e87b1d338 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 23 Aug 2023 15:09:15 +0200 Subject: [PATCH 013/142] moved semantic_index project intialization to queue and channel method --- crates/semantic_index/src/semantic_index.rs | 267 ++++++++------------ 1 file changed, 108 insertions(+), 159 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 79e649838a..ffe6e74a6d 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -15,8 +15,9 @@ use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; +use postage::stream::Stream; use postage::watch; -use project::{search::PathMatcher, Fs, Project, WorktreeId}; +use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId}; use smol::channel; use std::{ cmp::Ordering, @@ -96,7 +97,8 @@ struct ProjectState { subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, - queue: HashMap>, + job_queue_tx: channel::Sender, + _queue_update_task: Task<()>, } #[derive(Clone)] @@ -116,6 +118,7 @@ impl JobHandle { } impl ProjectState { fn new( + cx: &mut AppContext, subscription: gpui::Subscription, worktree_db_ids: Vec<(WorktreeId, i64)>, worktree_file_mtimes: HashMap>, @@ -125,29 +128,51 @@ impl ProjectState { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); + let (job_queue_tx, job_queue_rx) = channel::unbounded(); + let _queue_update_task = cx.background().spawn({ + let mut worktree_queue = Vec::new(); + async move { + while let Ok(operation) = job_queue_rx.recv().await { + Self::update_queue(&mut worktree_queue, operation); + } + } + }); + Self { worktree_db_ids, worktree_file_mtimes, outstanding_job_count_rx, _outstanding_job_count_tx, subscription, - queue: HashMap::new(), + _queue_update_task, + job_queue_tx, } } - fn add_to_queue(&mut self, worktree_id: WorktreeId, operation: IndexOperation) { - if let Some(worktree_queue) = self.queue.get_mut(&worktree_id) { - worktree_queue.push(operation); - } else { - self.queue.insert(worktree_id, vec![operation]); - } + pub fn get_outstanding_count(&self) -> usize { + self.outstanding_job_count_rx.borrow().clone() } - fn pop(&mut self) -> Option { - self.queue - .iter_mut() - .next() - .and_then(|(_, mut entry)| entry.pop()) + fn update_queue(queue: &mut Vec, operation: IndexOperation) { + match operation { + IndexOperation::FlushQueue => { + for op in queue.pop() { + match op { + IndexOperation::IndexFile { payload, tx } => { + tx.try_send(payload); + } + IndexOperation::DeleteFile { payload, tx } => { + tx.try_send(payload); + } + _ => {} + } + } + } + _ => { + // TODO: This has to accomodate for duplicate files to index. + queue.push(operation); + } + } } fn db_id_for_worktree_id(&self, id: WorktreeId) -> Option { @@ -185,10 +210,16 @@ pub struct PendingFile { job_handle: JobHandle, } -#[derive(Clone)] enum IndexOperation { - IndexFile { file: PendingFile }, - DeleteFile { file: PendingFile }, + IndexFile { + payload: PendingFile, + tx: channel::Sender, + }, + DeleteFile { + payload: DbOperation, + tx: channel::Sender, + }, + FlushQueue, } pub struct SearchResult { @@ -621,6 +652,52 @@ impl SemanticIndex { }) } + // pub fn project_entries_changed( + // &self, + // project: ModelHandle, + // changes: &Arc<[(Arc, ProjectEntryId, PathChange)]>, + // cx: &ModelContext, + // worktree_id: &WorktreeId, + // ) -> Result<()> { + // let parsing_files_tx = self.parsing_files_tx.clone(); + // let db_update_tx = self.db_update_tx.clone(); + // let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { + // let state = self.projects.get(&project.downgrade()); + // if state.is_none() { + // return anyhow::Error(anyhow!("Project not yet initialized")); + // } + // let state = state.unwrap(); + // ( + // state.job_queue_tx.clone(), + // state._outstanding_job_count_tx, + // state.db_id_for_worktree_id(worktree_id), + // ) + // }; + + // for (path, entry_id, path_change) in changes.iter() { + // match path_change { + // PathChange::AddedOrUpdated => { + // let job_handle = JobHandle::new(&outstanding_job_tx); + // job_queue_tx.try_send(IndexOperation::IndexFile { + // payload: PendingFile { + // worktree_db_id, + // relative_path: path, + // absolute_path, + // language, + // modified_time, + // job_handle, + // }, + // tx: parsing_files_tx, + // }) + // } + // PathChange::Removed => {} + // _ => {} + // } + // } + + // Ok(()) + // } + pub fn initialize_project( &mut self, project: ModelHandle, @@ -653,6 +730,7 @@ impl SemanticIndex { }); let language_registry = self.language_registry.clone(); + let parsing_files_tx = self.parsing_files_tx.clone(); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -686,24 +764,13 @@ impl SemanticIndex { let (job_count_tx, job_count_rx) = watch::channel_with(0); let job_count_tx = Arc::new(Mutex::new(job_count_tx)); let job_count_tx_longlived = job_count_tx.clone(); - // this.update(&mut cx, |this, _| { - // let project_state = ProjectState::new( - // _subscription, - // worktree_db_ids, - // worktree_file_mtimes.clone(), - // job_count_rx.clone(), - // job_count_tx.clone(), - // ); - // this.projects.insert(project.downgrade(), project_state); - // }); let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { - let mut worktree_files = HashMap::new(); + let mut worktree_files = Vec::new(); for worktree in worktrees.into_iter() { - let mut candidate_files = Vec::new(); let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -730,8 +797,8 @@ impl SemanticIndex { if !already_stored { let job_handle = JobHandle::new(&job_count_tx); - candidate_files.push(IndexOperation::IndexFile { - file: PendingFile { + worktree_files.push(IndexOperation::IndexFile { + payload: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, absolute_path, @@ -739,12 +806,11 @@ impl SemanticIndex { job_handle, modified_time: file.mtime, }, + tx: parsing_files_tx.clone(), }); } } } - - worktree_files.insert(worktree.id(), candidate_files); } anyhow::Ok(worktree_files) @@ -753,6 +819,7 @@ impl SemanticIndex { this.update(&mut cx, |this, cx| { let project_state = ProjectState::new( + cx, _subscription, worktree_db_ids, worktree_file_mtimes_all, @@ -761,10 +828,8 @@ impl SemanticIndex { ); if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { - for (worktree_id, index_operations) in &worktree_files { - for op in index_operations { - project_state.add_to_queue(*worktree_id, op.clone()); - } + for op in worktree_files { + project_state.job_queue_tx.try_send(op); } } @@ -791,134 +856,18 @@ impl SemanticIndex { let parsing_files_tx = self.parsing_files_tx.clone(); let db_update_tx = self.db_update_tx.clone(); let job_count_rx = state.outstanding_job_count_rx.clone(); - let count = state.queue.values().map(Vec::len).sum(); + let count = state.get_outstanding_count(); + cx.spawn(|this, mut cx| async move { this.update(&mut cx, |this, cx| { - let Some(mut state) = this.projects.get_mut(&project.downgrade()) else { + let Some(state) = this.projects.get_mut(&project.downgrade()) else { return; }; - let Some(mut index_operation) = state.pop() else { return;}; - let _ = match index_operation { - IndexOperation::IndexFile { file } => { - parsing_files_tx.try_send(file); - } - IndexOperation::DeleteFile { file } => { - db_update_tx.try_send(DbOperation::Delete { - worktree_id: file.worktree_db_id, - path: file.relative_path, - }); - } - }; - }); - }) - .detach(); + state.job_queue_tx.try_send(IndexOperation::FlushQueue); + }) + }); Task::Ready(Some(Ok((count, job_count_rx)))) - - // cx.spawn(|this, mut cx| async move { - // futures::future::join_all(worktree_scans_complete).await; - - // let worktree_db_ids = futures::future::join_all(worktree_db_ids).await; - - // let worktrees = project.read_with(&cx, |project, cx| { - // project - // .worktrees(cx) - // .map(|worktree| worktree.read(cx).snapshot()) - // .collect::>() - // }); - - // let mut worktree_file_mtimes = HashMap::new(); - // let mut db_ids_by_worktree_id = HashMap::new(); - // for (worktree, db_id) in worktrees.iter().zip(worktree_db_ids) { - // let db_id = db_id?; - // db_ids_by_worktree_id.insert(worktree.id(), db_id); - // worktree_file_mtimes.insert( - // worktree.id(), - // this.read_with(&cx, |this, _| this.get_file_mtimes(db_id)) - // .await?, - // ); - // } - - // let worktree_db_ids = db_ids_by_worktree_id - // .iter() - // .map(|(a, b)| (*a, *b)) - // .collect(); - - // let (job_count_tx, job_count_rx) = watch::channel_with(0); - // let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - // this.update(&mut cx, |this, _| { - // let project_state = ProjectState::new( - // _subscription, - // worktree_db_ids, - // worktree_file_mtimes.clone(), - // job_count_rx.clone(), - // job_count_tx.clone(), - // ); - // this.projects.insert(project.downgrade(), project_state); - // }); - - // cx.background() - // .spawn(async move { - // let mut count = 0; - // for worktree in worktrees.into_iter() { - // let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); - // for file in worktree.files(false, 0) { - // let absolute_path = worktree.absolutize(&file.path); - - // if let Ok(language) = language_registry - // .language_for_file(&absolute_path, None) - // .await - // { - // if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) - // && &language.name().as_ref() != &"Markdown" - // && language - // .grammar() - // .and_then(|grammar| grammar.embedding_config.as_ref()) - // .is_none() - // { - // continue; - // } - - // let path_buf = file.path.to_path_buf(); - // let stored_mtime = file_mtimes.remove(&file.path.to_path_buf()); - // let already_stored = stored_mtime - // .map_or(false, |existing_mtime| existing_mtime == file.mtime); - - // if !already_stored { - // count += 1; - - // let job_handle = JobHandle::new(&job_count_tx); - // parsing_files_tx - // .try_send(PendingFile { - // worktree_db_id: db_ids_by_worktree_id[&worktree.id()], - // relative_path: path_buf, - // absolute_path, - // language, - // job_handle, - // modified_time: file.mtime, - // }) - // .unwrap(); - // } - // } - // } - // for file in file_mtimes.keys() { - // db_update_tx - // .try_send(DbOperation::Delete { - // worktree_id: db_ids_by_worktree_id[&worktree.id()], - // path: file.to_owned(), - // }) - // .unwrap(); - // } - // } - - // log::trace!( - // "walking worktree took {:?} milliseconds", - // t0.elapsed().as_millis() - // ); - // anyhow::Ok((count, job_count_rx)) - // }) - // .await - // }) } pub fn outstanding_job_count_rx( From 72f0efb7b78c6fe4974e839d84f6281b1f85cb43 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 12:49:13 -0400 Subject: [PATCH 014/142] v0.102.x dev --- Cargo.lock | 2 +- crates/zed/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6490337da..aed2fb3ea9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9632,7 +9632,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.101.0" +version = "0.102.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fa1c67a6d0..a555e58679 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.101.0" +version = "0.102.0" publish = false [lib] From af21546a431354c440b42a5d59d52adbeb01c94d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 14:19:24 -0400 Subject: [PATCH 015/142] collab 0.18.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aed2fb3ea9..6dea29746f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1409,7 +1409,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.17.0" +version = "0.18.0" dependencies = [ "anyhow", "async-tungstenite", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc8c1644cd..b8d0c26960 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.17.0" +version = "0.18.0" publish = false [[bin]] From 6c45be2dc40dd245508d124c4ae3f70249897b69 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 23 Aug 2023 14:54:39 -0400 Subject: [PATCH 016/142] Add `docker system prune` command This will hopefully keep the system drive cleaned up so we don't run issues with not enough disk space. --- .github/workflows/publish_collab_image.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish_collab_image.yml b/.github/workflows/publish_collab_image.yml index 3421409287..b012e65841 100644 --- a/.github/workflows/publish_collab_image.yml +++ b/.github/workflows/publish_collab_image.yml @@ -11,7 +11,7 @@ env: jobs: publish: - name: Publish collab server image + name: Publish collab server image runs-on: - self-hosted - deploy @@ -22,6 +22,9 @@ jobs: - name: Sign into DigitalOcean docker registry run: doctl registry login + - name: Prune Docker system + run: docker system prune + - name: Checkout repo uses: actions/checkout@v3 with: @@ -41,6 +44,6 @@ jobs: - name: Build docker image run: docker build . --tag registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} - + - name: Publish docker image run: docker push registry.digitalocean.com/zed/collab:v${COLLAB_VERSION} From e42b9e910ede97b35414ca9987d5d39b340776d1 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Wed, 23 Aug 2023 22:28:30 +0200 Subject: [PATCH 017/142] fix async calls on project updated entries to ensure that all files are updating appropriately --- crates/search/src/project_search.rs | 1 + crates/semantic_index/src/semantic_index.rs | 161 +++++++++++++------- 2 files changed, 108 insertions(+), 54 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index ca317c0ded..448735fe3c 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -849,6 +849,7 @@ impl ProjectSearchView { let model = model.read(cx); project = model.project.clone(); SemanticIndex::global(cx).map(|semantic| { + dbg!("Initializing project"); semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); }); } diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index ffe6e74a6d..0df3a9cc84 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -156,7 +156,7 @@ impl ProjectState { fn update_queue(queue: &mut Vec, operation: IndexOperation) { match operation { IndexOperation::FlushQueue => { - for op in queue.pop() { + while let Some(op) = queue.pop() { match op { IndexOperation::IndexFile { payload, tx } => { tx.try_send(payload); @@ -652,51 +652,103 @@ impl SemanticIndex { }) } - // pub fn project_entries_changed( - // &self, - // project: ModelHandle, - // changes: &Arc<[(Arc, ProjectEntryId, PathChange)]>, - // cx: &ModelContext, - // worktree_id: &WorktreeId, - // ) -> Result<()> { - // let parsing_files_tx = self.parsing_files_tx.clone(); - // let db_update_tx = self.db_update_tx.clone(); - // let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { - // let state = self.projects.get(&project.downgrade()); - // if state.is_none() { - // return anyhow::Error(anyhow!("Project not yet initialized")); - // } - // let state = state.unwrap(); - // ( - // state.job_queue_tx.clone(), - // state._outstanding_job_count_tx, - // state.db_id_for_worktree_id(worktree_id), - // ) - // }; + fn project_entries_changed( + &self, + project: ModelHandle, + changes: Arc<[(Arc, ProjectEntryId, PathChange)]>, + cx: &mut ModelContext<'_, SemanticIndex>, + worktree_id: &WorktreeId, + ) -> Result<()> { + let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); + let (job_queue_tx, outstanding_job_tx, worktree_db_id) = { + let state = self + .projects + .get(&project.downgrade()) + .ok_or(anyhow!("Project not yet initialized"))?; + let worktree_db_id = state + .db_id_for_worktree_id(*worktree_id) + .ok_or(anyhow!("Worktree ID in Database Not Available"))?; + ( + state.job_queue_tx.clone(), + state._outstanding_job_count_tx.clone(), + worktree_db_id, + ) + }; - // for (path, entry_id, path_change) in changes.iter() { - // match path_change { - // PathChange::AddedOrUpdated => { - // let job_handle = JobHandle::new(&outstanding_job_tx); - // job_queue_tx.try_send(IndexOperation::IndexFile { - // payload: PendingFile { - // worktree_db_id, - // relative_path: path, - // absolute_path, - // language, - // modified_time, - // job_handle, - // }, - // tx: parsing_files_tx, - // }) - // } - // PathChange::Removed => {} - // _ => {} - // } - // } + let language_registry = self.language_registry.clone(); + let parsing_files_tx = parsing_files_tx.clone(); + let db_update_tx = db_update_tx.clone(); - // Ok(()) - // } + let worktree = project + .read(cx) + .worktree_for_id(worktree_id.clone(), cx) + .ok_or(anyhow!("Worktree not available"))? + .read(cx) + .snapshot(); + cx.spawn(|this, mut cx| async move { + let worktree = worktree.clone(); + for (path, entry_id, path_change) in changes.iter() { + let relative_path = path.to_path_buf(); + let absolute_path = worktree.absolutize(path); + + let Some(entry) = worktree.entry_for_id(*entry_id) else { + continue; + }; + if entry.is_ignored || entry.is_symlink || entry.is_external { + continue; + } + + match path_change { + PathChange::AddedOrUpdated | PathChange::Updated => { + log::trace!("File Updated: {:?}", path); + if let Ok(language) = language_registry + .language_for_file(&relative_path, None) + .await + { + if !PARSEABLE_ENTIRE_FILE_TYPES.contains(&language.name().as_ref()) + && &language.name().as_ref() != &"Markdown" + && language + .grammar() + .and_then(|grammar| grammar.embedding_config.as_ref()) + .is_none() + { + continue; + } + + let job_handle = JobHandle::new(&outstanding_job_tx); + let new_operation = IndexOperation::IndexFile { + payload: PendingFile { + worktree_db_id, + relative_path, + absolute_path, + language, + modified_time: entry.mtime, + job_handle, + }, + tx: parsing_files_tx.clone(), + }; + job_queue_tx.try_send(new_operation); + } + } + PathChange::Removed => { + let new_operation = IndexOperation::DeleteFile { + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path: relative_path, + }, + tx: db_update_tx.clone(), + }; + job_queue_tx.try_send(new_operation); + } + _ => {} + } + } + }) + .detach(); + + Ok(()) + } pub fn initialize_project( &mut self, @@ -724,9 +776,8 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - todo!(); - // this.project_entries_changed(project, changes, cx, worktree_id); - } + this.project_entries_changed(project, changes.clone(), cx, worktree_id); + }; }); let language_registry = self.language_registry.clone(); @@ -775,6 +826,10 @@ impl SemanticIndex { for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); + if file.is_external || file.is_ignored || file.is_symlink { + continue; + } + if let Ok(language) = language_registry .language_for_file(&absolute_path, None) .await @@ -827,10 +882,8 @@ impl SemanticIndex { job_count_tx_longlived, ); - if let Some(project_state) = this.projects.get_mut(&project.downgrade()) { - for op in worktree_files { - project_state.job_queue_tx.try_send(op); - } + for op in worktree_files { + project_state.job_queue_tx.try_send(op); } this.projects.insert(project.downgrade(), project_state); @@ -864,10 +917,10 @@ impl SemanticIndex { return; }; state.job_queue_tx.try_send(IndexOperation::FlushQueue); - }) - }); + }); - Task::Ready(Some(Ok((count, job_count_rx)))) + Ok((count, job_count_rx)) + }) } pub fn outstanding_job_count_rx( From 1320fadc30004572415a93aeb9bf2b0a88c92bb8 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Thu, 24 Aug 2023 00:16:35 +0200 Subject: [PATCH 018/142] Bump rust embed (#2883) This is a follow-up to a recent patch I've submitted to this crate to improve compile time and runtime (in older versions file lookup was essentially O(n) with respect to path count, now it's O(log n)) Release Notes: - N/A --- Cargo.lock | 12 ++++++------ Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6dea29746f..ce3f97c2b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6130,9 +6130,9 @@ dependencies = [ [[package]] name = "rust-embed" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a36224c3276f8c4ebc8c20f158eca7ca4359c8db89991c4925132aaaf6702661" +checksum = "b1e7d90385b59f0a6bf3d3b757f3ca4ece2048265d70db20a2016043d4509a40" dependencies = [ "rust-embed-impl", "rust-embed-utils", @@ -6141,9 +6141,9 @@ dependencies = [ [[package]] name = "rust-embed-impl" -version = "6.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49b94b81e5b2c284684141a2fb9e2a31be90638caf040bf9afbc5a0416afe1ac" +checksum = "3c3d8c6fd84090ae348e63a84336b112b5c3918b3bf0493a581f7bd8ee623c29" dependencies = [ "proc-macro2", "quote", @@ -6154,9 +6154,9 @@ dependencies = [ [[package]] name = "rust-embed-utils" -version = "7.8.1" +version = "8.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d38ff6bf570dc3bb7100fce9f7b60c33fa71d80e88da3f2580df4ff2bdded74" +checksum = "873feff8cb7bf86fdf0a71bb21c95159f4e4a37dd7a4bd1855a940909b583ada" dependencies = [ "globset", "sha2 0.10.7", diff --git a/Cargo.toml b/Cargo.toml index a60b4eb610..f5b5994e89 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,7 @@ postage = { version = "0.5", features = ["futures-traits"] } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } -rust-embed = { version = "6.3", features = ["include-exclude"] } +rust-embed = { version = "8.0", features = ["include-exclude"] } schemars = { version = "0.8" } serde = { version = "1.0", features = ["derive", "rc"] } serde_derive = { version = "1.0", features = ["deserialize_in_place"] } diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a555e58679..a821e33327 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -93,7 +93,7 @@ postage.workspace = true rand.workspace = true regex.workspace = true rsa = "0.4" -rust-embed = { version = "6.8.1" } +rust-embed.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true From 2a182b6a7b1df98ec5019bd9d8ccde2e1cd22628 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 16:25:17 -0700 Subject: [PATCH 019/142] Tune styles and disclosable elements --- crates/collab_ui/src/collab_panel.rs | 68 +++++++++++++++++++++---- crates/theme/src/components.rs | 16 +++--- styles/src/component/icon_button.ts | 14 ++--- styles/src/style_tree/collab_panel.ts | 9 ++-- styles/src/style_tree/component_test.ts | 16 ++++-- 5 files changed, 93 insertions(+), 30 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 52711281c7..5623ada42d 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -55,7 +55,7 @@ struct RemoveChannel { } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] -struct ToggleCollapsed { +struct ToggleCollapse { channel_id: u64, } @@ -79,7 +79,16 @@ struct RenameChannel { channel_id: u64, } -actions!(collab_panel, [ToggleFocus, Remove, Secondary]); +actions!( + collab_panel, + [ + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel + ] +); impl_actions!( collab_panel, @@ -89,7 +98,7 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapsed + ToggleCollapse ] ); @@ -113,6 +122,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_selected_channel); cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); + cx.add_action(CollabPanel::collapse_selected_channel); + cx.add_action(CollabPanel::expand_selected_channel) } #[derive(Debug)] @@ -1356,7 +1367,7 @@ impl CollabPanel { .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, this, cx| { if can_collapse { - this.toggle_expanded(section, cx); + this.toggle_section_expanded(section, cx); } }) } @@ -1633,7 +1644,7 @@ impl CollabPanel { }) .align_children_center() .styleable_component() - .disclosable(disclosed, Box::new(ToggleCollapsed { channel_id })) + .disclosable(disclosed, Box::new(ToggleCollapse { channel_id })) .with_id(channel_id as usize) .with_style(theme.disclosure.clone()) .element() @@ -1863,6 +1874,12 @@ impl CollabPanel { OverlayPositionMode::Window }); + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + context_menu.show( position.unwrap_or_default(), if self.context_menu_on_selected { @@ -1871,6 +1888,7 @@ impl CollabPanel { gpui::elements::AnchorCorner::BottomLeft }, vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), @@ -1950,7 +1968,7 @@ impl CollabPanel { | Section::Online | Section::Offline | Section::ChannelInvites => { - self.toggle_expanded(*section, cx); + self.toggle_section_expanded(*section, cx); } }, ListEntry::Contact { contact, calling } => { @@ -2038,7 +2056,7 @@ impl CollabPanel { } } - fn toggle_expanded(&mut self, section: Section, cx: &mut ViewContext) { + fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { self.collapsed_sections.remove(ix); } else { @@ -2047,8 +2065,37 @@ impl CollabPanel { self.update_entries(false, cx); } - fn toggle_channel_collapsed(&mut self, action: &ToggleCollapsed, cx: &mut ViewContext) { + fn collapse_selected_channel( + &mut self, + _: &CollapseSelectedChannel, + cx: &mut ViewContext, + ) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + return; + }; + + if !self.is_channel_collapsed(channel_id) { + return; + } + + self.toggle_channel_collapsed(&ToggleCollapse { channel_id }, cx) + } + + fn toggle_channel_collapsed(&mut self, action: &ToggleCollapse, cx: &mut ViewContext) { let channel_id = action.channel_id; + match self.collapsed_channels.binary_search(&channel_id) { Ok(ix) => { self.collapsed_channels.remove(ix); @@ -2057,8 +2104,9 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; - self.update_entries(false, cx); + self.update_entries(true, cx); cx.notify(); + cx.focus_self(); } fn is_channel_collapsed(&self, channel: ChannelId) -> bool { @@ -2104,6 +2152,8 @@ impl CollabPanel { } fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + self.collapsed_channels + .retain(|&channel| channel != action.channel_id); self.channel_editing_state = Some(ChannelEditingState::Create { parent_id: Some(action.channel_id), pending_name: None, diff --git a/crates/theme/src/components.rs b/crates/theme/src/components.rs index fc208954cf..8eaab91fe6 100644 --- a/crates/theme/src/components.rs +++ b/crates/theme/src/components.rs @@ -4,7 +4,8 @@ use crate::{Interactive, Toggleable}; use self::{action_button::ButtonStyle, disclosure::Disclosable, svg::SvgStyle, toggle::Toggle}; -pub type ToggleIconButtonStyle = Toggleable>>; +pub type IconButtonStyle = Interactive>; +pub type ToggleIconButtonStyle = Toggleable; pub trait ComponentExt { fn toggleable(self, active: bool) -> Toggle; @@ -27,17 +28,19 @@ impl ComponentExt for C { pub mod disclosure { use gpui::{ - elements::{Component, Empty, Flex, ParentElement, SafeStylable}, + elements::{Component, ContainerStyle, Empty, Flex, ParentElement, SafeStylable}, Action, Element, }; use schemars::JsonSchema; use serde_derive::Deserialize; - use super::{action_button::Button, svg::Svg, ComponentExt, ToggleIconButtonStyle}; + use super::{action_button::Button, svg::Svg, ComponentExt, IconButtonStyle}; #[derive(Clone, Default, Deserialize, JsonSchema)] pub struct DisclosureStyle { - pub button: ToggleIconButtonStyle, + pub button: IconButtonStyle, + #[serde(flatten)] + pub container: ContainerStyle, pub spacing: f32, #[serde(flatten)] content: S, @@ -99,6 +102,7 @@ pub mod disclosure { impl Component for Disclosable> { fn render(self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { Flex::row() + .with_spacing(self.style.spacing) .with_child(if let Some(disclosed) = self.disclosed { Button::dynamic_action(self.action) .with_id(self.id) @@ -107,7 +111,6 @@ pub mod disclosure { } else { "icons/file_icons/chevron_right.svg" })) - .toggleable(disclosed) .with_style(self.style.button) .element() .into_any() @@ -119,7 +122,6 @@ pub mod disclosure { .with_width(self.style.button.button_width.unwrap()) .into_any() }) - .with_child(Empty::new().constrained().with_width(self.style.spacing)) .with_child( self.content .with_style(self.style.content) @@ -127,6 +129,8 @@ pub mod disclosure { .flex(1., true), ) .align_children_center() + .contained() + .with_style(self.style.container) .into_any() } } diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 1a2d0bcec4..935909afdb 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -44,10 +44,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO } const padding = { - top: size === Button.size.Small ? 0 : 2, - bottom: size === Button.size.Small ? 0 : 2, - left: size === Button.size.Small ? 0 : 4, - right: size === Button.size.Small ? 0 : 4, + top: size === Button.size.Small ? 2 : 2, + bottom: size === Button.size.Small ? 2 : 2, + left: size === Button.size.Small ? 2 : 4, + right: size === Button.size.Small ? 2 : 4, } return interactive({ @@ -55,10 +55,10 @@ export function icon_button({ color, margin, layer, variant, size }: IconButtonO corner_radius: 6, padding: padding, margin: m, - icon_width: 14, + icon_width: 12, icon_height: 14, - button_width: 20, - button_height: 16, + button_width: size === Button.size.Small ? 16 : 20, + button_height: 14, }, state: { default: { diff --git a/styles/src/style_tree/collab_panel.ts b/styles/src/style_tree/collab_panel.ts index 5242f90c8d..07f367c8af 100644 --- a/styles/src/style_tree/collab_panel.ts +++ b/styles/src/style_tree/collab_panel.ts @@ -14,6 +14,7 @@ import { indicator } from "../component/indicator" export default function contacts_panel(): any { const theme = useTheme() + const CHANNEL_SPACING = 4 as const const NAME_MARGIN = 6 as const const SPACING = 12 as const const INDENT_SIZE = 8 as const @@ -153,8 +154,8 @@ export default function contacts_panel(): any { return { ...collab_modals(), disclosure: { - button: toggleable_icon_button(theme, {}), - spacing: 4, + button: icon_button({ variant: "ghost", size: "sm" }), + spacing: CHANNEL_SPACING, }, log_in_button: interactive({ base: { @@ -198,7 +199,7 @@ export default function contacts_panel(): any { add_channel_button: header_icon_button, leave_call_button: header_icon_button, row_height: ITEM_HEIGHT, - channel_indent: INDENT_SIZE * 2, + channel_indent: INDENT_SIZE * 2 + 2, section_icon_size: 14, header_row: { ...text(layer, "sans", { size: "sm", weight: "bold" }), @@ -268,7 +269,7 @@ export default function contacts_panel(): any { channel_name: { ...text(layer, "sans", { size: "sm" }), margin: { - left: NAME_MARGIN, + left: CHANNEL_SPACING, }, }, list_empty_label_container: { diff --git a/styles/src/style_tree/component_test.ts b/styles/src/style_tree/component_test.ts index eadbb5c2f1..e2bb0915c1 100644 --- a/styles/src/style_tree/component_test.ts +++ b/styles/src/style_tree/component_test.ts @@ -1,18 +1,26 @@ -import { toggle_label_button_style } from "../component/label_button" + import { useTheme } from "../common" import { text_button } from "../component/text_button" -import { toggleable_icon_button } from "../component/icon_button" +import { icon_button } from "../component/icon_button" import { text } from "./components" +import { toggleable } from "../element" export default function contacts_panel(): any { const theme = useTheme() return { button: text_button({}), - toggle: toggle_label_button_style({ active_color: "accent" }), + toggle: toggleable({ + base: text_button({}), + state: { + active: { + ...text_button({ color: "accent" }) + } + } + }), disclosure: { ...text(theme.lowest, "sans", "base"), - button: toggleable_icon_button(theme, {}), + button: icon_button({ variant: "ghost" }), spacing: 4, } } From 4d2f5a8e04b72d7083f43f75aaaaeb9771252f64 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 17:47:03 -0700 Subject: [PATCH 020/142] Fix cursor and hover styles changing when dragging the mouse --- crates/gpui/src/app.rs | 26 ++++++++++++------- crates/gpui/src/app/window.rs | 9 ++++--- .../src/pane/dragged_item_receiver.rs | 6 ++++- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 076a3fdde3..03625c80e7 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3299,15 +3299,15 @@ impl<'a, 'b, V: 'static> ViewContext<'a, 'b, V> { let region_id = MouseRegionId::new(tag, self.view_id, region_id); MouseState { hovered: self.window.hovered_region_ids.contains(®ion_id), - clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region { - if region_id == clicked_region_id { - Some(button) - } else { - None - } - } else { - None - }, + mouse_down: !self.window.clicked_region_ids.is_empty(), + clicked: self + .window + .clicked_region_ids + .iter() + .find(|click_region_id| **click_region_id == region_id) + // If we've gotten here, there should always be a clicked region. + // But let's be defensive and return None if there isn't. + .and_then(|_| self.window.clicked_region.map(|(_, button)| button)), accessed_hovered: false, accessed_clicked: false, } @@ -3823,14 +3823,20 @@ impl<'a, T> DerefMut for Reference<'a, T> { pub struct MouseState { pub(crate) hovered: bool, pub(crate) clicked: Option, + pub(crate) mouse_down: bool, pub(crate) accessed_hovered: bool, pub(crate) accessed_clicked: bool, } impl MouseState { + pub fn dragging(&mut self) -> bool { + self.accessed_hovered = true; + self.hovered && self.mouse_down + } + pub fn hovered(&mut self) -> bool { self.accessed_hovered = true; - self.hovered + self.hovered && (!self.mouse_down || self.clicked.is_some()) } pub fn clicked(&mut self) -> Option { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 5638699565..543b100284 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -617,10 +617,11 @@ impl<'a> WindowContext<'a> { } } - if self - .window - .platform_window - .is_topmost_for_position(*position) + if pressed_button.is_none() + && self + .window + .platform_window + .is_topmost_for_position(*position) { self.platform().set_cursor_style(style_to_assign); } diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 5e487b49b4..bbe391b5b5 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -42,7 +42,11 @@ where let mut handler = MouseEventHandler::above::(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before - let drag_position = if state.hovered() { drag_position } else { None }; + let drag_position = if state.dragging() { + drag_position + } else { + None + }; Stack::new() .with_child(render_child(state, cx)) .with_children(drag_position.map(|drag_position| { From ff75d1663b4364fc288e76790422477e1895dc49 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 23 Aug 2023 18:22:48 -0700 Subject: [PATCH 021/142] Fix stuck click styling when dragging off of a button --- crates/gpui/src/app/window.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 543b100284..4b8b0534d5 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -788,6 +788,11 @@ impl<'a> WindowContext<'a> { .contains_point(self.window.mouse_position) { valid_regions.push(mouse_region.clone()); + } else { + // Let the view know that it hasn't been clicked anymore + if mouse_region.notify_on_click { + notified_views.insert(mouse_region.id().view_id()); + } } } } From 29e43384f020f7e20c3012a1f46c53a717941297 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Sun, 20 Aug 2023 14:25:19 -0700 Subject: [PATCH 022/142] Simplify macro for running a test with both databases --- crates/collab/src/db/db_tests.rs | 1078 +++++++++++++++--------------- crates/collab/src/db/test_db.rs | 17 + 2 files changed, 558 insertions(+), 537 deletions(-) diff --git a/crates/collab/src/db/db_tests.rs b/crates/collab/src/db/db_tests.rs index 8e9a80dbab..8c5dab77bd 100644 --- a/crates/collab/src/db/db_tests.rs +++ b/crates/collab/src/db/db_tests.rs @@ -1,242 +1,234 @@ use super::*; +use crate::test_both_dbs; use gpui::executor::{Background, Deterministic}; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; use test_db::TestDb; -macro_rules! test_both_dbs { - ($postgres_test_name:ident, $sqlite_test_name:ident, $db:ident, $body:block) => { - #[gpui::test] - async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } +test_both_dbs!( + test_get_users, + test_get_users_by_ids_postgres, + test_get_users_by_ids_sqlite +); - #[gpui::test] - async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); - let $db = test_db.db(); - $body - } - }; +async fn test_get_users(db: &Arc) { + let mut user_ids = Vec::new(); + let mut user_metric_ids = Vec::new(); + for i in 1..=4 { + let user = db + .create_user( + &format!("user{i}@example.com"), + false, + NewUserParams { + github_login: format!("user{i}"), + github_user_id: i, + invite_count: 0, + }, + ) + .await + .unwrap(); + user_ids.push(user.user_id); + user_metric_ids.push(user.metrics_id); + } + + assert_eq!( + db.get_users_by_ids(user_ids.clone()).await.unwrap(), + vec![ + User { + id: user_ids[0], + github_login: "user1".to_string(), + github_user_id: Some(1), + email_address: Some("user1@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[0].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[1], + github_login: "user2".to_string(), + github_user_id: Some(2), + email_address: Some("user2@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[1].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[2], + github_login: "user3".to_string(), + github_user_id: Some(3), + email_address: Some("user3@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[2].parse().unwrap(), + ..Default::default() + }, + User { + id: user_ids[3], + github_login: "user4".to_string(), + github_user_id: Some(4), + email_address: Some("user4@example.com".to_string()), + admin: false, + metrics_id: user_metric_ids[3].parse().unwrap(), + ..Default::default() + } + ] + ); } test_both_dbs!( - test_get_users_by_ids_postgres, - test_get_users_by_ids_sqlite, - db, - { - let mut user_ids = Vec::new(); - let mut user_metric_ids = Vec::new(); - for i in 1..=4 { - let user = db - .create_user( - &format!("user{i}@example.com"), - false, - NewUserParams { - github_login: format!("user{i}"), - github_user_id: i, - invite_count: 0, - }, - ) - .await - .unwrap(); - user_ids.push(user.user_id); - user_metric_ids.push(user.metrics_id); - } - - assert_eq!( - db.get_users_by_ids(user_ids.clone()).await.unwrap(), - vec![ - User { - id: user_ids[0], - github_login: "user1".to_string(), - github_user_id: Some(1), - email_address: Some("user1@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[0].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[1], - github_login: "user2".to_string(), - github_user_id: Some(2), - email_address: Some("user2@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[1].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[2], - github_login: "user3".to_string(), - github_user_id: Some(3), - email_address: Some("user3@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[2].parse().unwrap(), - ..Default::default() - }, - User { - id: user_ids[3], - github_login: "user4".to_string(), - github_user_id: Some(4), - email_address: Some("user4@example.com".to_string()), - admin: false, - metrics_id: user_metric_ids[3].parse().unwrap(), - ..Default::default() - } - ] - ); - } -); - -test_both_dbs!( + test_get_or_create_user_by_github_account, test_get_or_create_user_by_github_account_postgres, - test_get_or_create_user_by_github_account_sqlite, - db, - { - let user_id1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "login1".into(), - github_user_id: 101, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_id2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "login2".into(), - github_user_id: 102, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user = db - .get_or_create_user_by_github_account("login1", None, None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id1); - assert_eq!(&user.github_login, "login1"); - assert_eq!(user.github_user_id, Some(101)); - - assert!(db - .get_or_create_user_by_github_account("non-existent-login", None, None) - .await - .unwrap() - .is_none()); - - let user = db - .get_or_create_user_by_github_account("the-new-login2", Some(102), None) - .await - .unwrap() - .unwrap(); - assert_eq!(user.id, user_id2); - assert_eq!(&user.github_login, "the-new-login2"); - assert_eq!(user.github_user_id, Some(102)); - - let user = db - .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) - .await - .unwrap() - .unwrap(); - assert_eq!(&user.github_login, "login3"); - assert_eq!(user.github_user_id, Some(103)); - assert_eq!(user.email_address, Some("user3@example.com".into())); - } + test_get_or_create_user_by_github_account_sqlite ); +async fn test_get_or_create_user_by_github_account(db: &Arc) { + let user_id1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "login1".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_id2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "login2".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user = db + .get_or_create_user_by_github_account("login1", None, None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id1); + assert_eq!(&user.github_login, "login1"); + assert_eq!(user.github_user_id, Some(101)); + + assert!(db + .get_or_create_user_by_github_account("non-existent-login", None, None) + .await + .unwrap() + .is_none()); + + let user = db + .get_or_create_user_by_github_account("the-new-login2", Some(102), None) + .await + .unwrap() + .unwrap(); + assert_eq!(user.id, user_id2); + assert_eq!(&user.github_login, "the-new-login2"); + assert_eq!(user.github_user_id, Some(102)); + + let user = db + .get_or_create_user_by_github_account("login3", Some(103), Some("user3@example.com")) + .await + .unwrap() + .unwrap(); + assert_eq!(&user.github_login, "login3"); + assert_eq!(user.github_user_id, Some(103)); + assert_eq!(user.email_address, Some("user3@example.com".into())); +} + test_both_dbs!( + test_create_access_tokens, test_create_access_tokens_postgres, - test_create_access_tokens_sqlite, - db, - { - let user = db - .create_user( - "u1@example.com", - false, - NewUserParams { - github_login: "u1".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); - let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_1).await.unwrap(), - access_token::Model { - id: token_1, - user_id: user, - hash: "h1".into(), - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - - let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert_eq!( - db.get_access_token(token_2).await.unwrap(), - access_token::Model { - id: token_2, - user_id: user, - hash: "h2".into() - } - ); - assert!(db.get_access_token(token_1).await.is_err()); - - let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); - assert_eq!( - db.get_access_token(token_4).await.unwrap(), - access_token::Model { - id: token_4, - user_id: user, - hash: "h4".into() - } - ); - assert_eq!( - db.get_access_token(token_3).await.unwrap(), - access_token::Model { - id: token_3, - user_id: user, - hash: "h3".into() - } - ); - assert!(db.get_access_token(token_2).await.is_err()); - assert!(db.get_access_token(token_1).await.is_err()); - } + test_create_access_tokens_sqlite ); -test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { +async fn test_create_access_tokens(db: &Arc) { + let user = db + .create_user( + "u1@example.com", + false, + NewUserParams { + github_login: "u1".into(), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let token_1 = db.create_access_token(user, "h1", 2).await.unwrap(); + let token_2 = db.create_access_token(user, "h2", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_1).await.unwrap(), + access_token::Model { + id: token_1, + user_id: user, + hash: "h1".into(), + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + + let token_3 = db.create_access_token(user, "h3", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert_eq!( + db.get_access_token(token_2).await.unwrap(), + access_token::Model { + id: token_2, + user_id: user, + hash: "h2".into() + } + ); + assert!(db.get_access_token(token_1).await.is_err()); + + let token_4 = db.create_access_token(user, "h4", 2).await.unwrap(); + assert_eq!( + db.get_access_token(token_4).await.unwrap(), + access_token::Model { + id: token_4, + user_id: user, + hash: "h4".into() + } + ); + assert_eq!( + db.get_access_token(token_3).await.unwrap(), + access_token::Model { + id: token_3, + user_id: user, + hash: "h3".into() + } + ); + assert!(db.get_access_token(token_2).await.is_err()); + assert!(db.get_access_token(token_1).await.is_err()); +} + +test_both_dbs!( + test_add_contacts, + test_add_contacts_postgres, + test_add_contacts_sqlite +); + +async fn test_add_contacts(db: &Arc) { let mut user_ids = Vec::new(); for i in 0..3 { user_ids.push( @@ -403,9 +395,15 @@ test_both_dbs!(test_add_contacts_postgres, test_add_contacts_sqlite, db, { busy: false, }], ); -}); +} -test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { +test_both_dbs!( + test_metrics_id, + test_metrics_id_postgres, + test_metrics_id_sqlite +); + +async fn test_metrics_id(db: &Arc) { let NewUserResult { user_id: user1, metrics_id: metrics_id1, @@ -444,82 +442,83 @@ test_both_dbs!(test_metrics_id_postgres, test_metrics_id_sqlite, db, { assert_eq!(metrics_id1.len(), 36); assert_eq!(metrics_id2.len(), 36); assert_ne!(metrics_id1, metrics_id2); -}); +} test_both_dbs!( + test_project_count, test_project_count_postgres, - test_project_count_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; + test_project_count_sqlite +); - let user1 = db - .create_user( - &format!("admin@example.com"), - true, - NewUserParams { - github_login: "admin".into(), - github_user_id: 0, - invite_count: 0, - }, - ) - .await - .unwrap(); - let user2 = db - .create_user( - &format!("user@example.com"), - false, - NewUserParams { - github_login: "user".into(), - github_user_id: 1, - invite_count: 0, - }, - ) - .await - .unwrap(); +async fn test_project_count(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; - let room_id = RoomId::from_proto( - db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") - .await - .unwrap() - .id, - ); - db.call( - room_id, - user1.user_id, - ConnectionId { owner_id, id: 0 }, - user2.user_id, - None, + let user1 = db + .create_user( + &format!("admin@example.com"), + true, + NewUserParams { + github_login: "admin".into(), + github_user_id: 0, + invite_count: 0, + }, + ) + .await + .unwrap(); + let user2 = db + .create_user( + &format!("user@example.com"), + false, + NewUserParams { + github_login: "user".into(), + github_user_id: 1, + invite_count: 0, + }, ) .await .unwrap(); - db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + let room_id = RoomId::from_proto( + db.create_room(user1.user_id, ConnectionId { owner_id, id: 0 }, "") .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); + .unwrap() + .id, + ); + db.call( + room_id, + user1.user_id, + ConnectionId { owner_id, id: 0 }, + user2.user_id, + None, + ) + .await + .unwrap(); + db.join_room(room_id, user2.user_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 1); - // Projects shared by admins aren't counted. - db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + db.share_project(room_id, ConnectionId { owner_id, id: 1 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); - db.leave_room(ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); - } -); + // Projects shared by admins aren't counted. + db.share_project(room_id, ConnectionId { owner_id, id: 0 }, &[]) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 2); + + db.leave_room(ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(db.project_count_excluding_admins().await.unwrap(), 0); +} #[test] fn test_fuzzy_like_string() { @@ -878,7 +877,9 @@ async fn test_invite_codes() { assert!(db.has_contact(user5, user1).await.unwrap()); } -test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { +test_both_dbs!(test_channels, test_channels_postgres, test_channels_sqlite); + +async fn test_channels(db: &Arc) { let a_id = db .create_user( "user1@example.com", @@ -1063,268 +1064,271 @@ test_both_dbs!(test_channels_postgres, test_channels_sqlite, db, { assert!(db.get_channel(rust_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_id, a_id).await.unwrap().is_none()); assert!(db.get_channel(cargo_ra_id, a_id).await.unwrap().is_none()); -}); +} test_both_dbs!( + test_joining_channels, test_joining_channels_postgres, - test_joining_channels_sqlite, - db, - { - let owner_id = db.create_server("test").await.unwrap().0 as u32; - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); - - // can join a room with membership to its channel - let joined_room = db - .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) - .await - .unwrap(); - assert_eq!(joined_room.room.participants.len(), 1); - - drop(joined_room); - // cannot join a room without membership to its channel - assert!(db - .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) - .await - .is_err()); - } + test_joining_channels_sqlite ); +async fn test_joining_channels(db: &Arc) { + let owner_id = db.create_server("test").await.unwrap().0 as u32; + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + let room_1 = db.room_id_for_channel(channel_1).await.unwrap(); + + // can join a room with membership to its channel + let joined_room = db + .join_room(room_1, user_1, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + assert_eq!(joined_room.room.participants.len(), 1); + + drop(joined_room); + // cannot join a room without membership to its channel + assert!(db + .join_room(room_1, user_2, ConnectionId { owner_id, id: 1 }) + .await + .is_err()); +} + test_both_dbs!( + test_channel_invites, test_channel_invites_postgres, - test_channel_invites_sqlite, - db, - { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_3 = db - .create_user( - "user3@example.com", - false, - NewUserParams { - github_login: "user3".into(), - github_user_id: 7, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let channel_1_1 = db - .create_root_channel("channel_1", "1", user_1) - .await - .unwrap(); - - let channel_1_2 = db - .create_root_channel("channel_2", "2", user_1) - .await - .unwrap(); - - db.invite_channel_member(channel_1_1, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_2, user_2, user_1, false) - .await - .unwrap(); - db.invite_channel_member(channel_1_1, user_3, user_1, true) - .await - .unwrap(); - - let user_2_invites = db - .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); - - let user_3_invites = db - .get_channel_invites_for_user(user_3) // -> [channel_1_1] - .await - .unwrap() - .into_iter() - .map(|channel| channel.id) - .collect::>(); - - assert_eq!(user_3_invites, &[channel_1_1]); - - let members = db - .get_channel_member_details(channel_1_1, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: false, - }, - proto::ChannelMember { - user_id: user_3.to_proto(), - kind: proto::channel_member::Kind::Invitee.into(), - admin: true, - }, - ] - ); - - db.respond_to_channel_invite(channel_1_1, user_2, true) - .await - .unwrap(); - - let channel_1_3 = db - .create_channel("channel_3", Some(channel_1_1), "1", user_1) - .await - .unwrap(); - - let members = db - .get_channel_member_details(channel_1_3, user_1) - .await - .unwrap(); - assert_eq!( - members, - &[ - proto::ChannelMember { - user_id: user_1.to_proto(), - kind: proto::channel_member::Kind::Member.into(), - admin: true, - }, - proto::ChannelMember { - user_id: user_2.to_proto(), - kind: proto::channel_member::Kind::AncestorMember.into(), - admin: false, - }, - ] - ); - } + test_channel_invites_sqlite ); +async fn test_channel_invites(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_3 = db + .create_user( + "user3@example.com", + false, + NewUserParams { + github_login: "user3".into(), + github_user_id: 7, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let channel_1_1 = db + .create_root_channel("channel_1", "1", user_1) + .await + .unwrap(); + + let channel_1_2 = db + .create_root_channel("channel_2", "2", user_1) + .await + .unwrap(); + + db.invite_channel_member(channel_1_1, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_2, user_2, user_1, false) + .await + .unwrap(); + db.invite_channel_member(channel_1_1, user_3, user_1, true) + .await + .unwrap(); + + let user_2_invites = db + .get_channel_invites_for_user(user_2) // -> [channel_1_1, channel_1_2] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_2_invites, &[channel_1_1, channel_1_2]); + + let user_3_invites = db + .get_channel_invites_for_user(user_3) // -> [channel_1_1] + .await + .unwrap() + .into_iter() + .map(|channel| channel.id) + .collect::>(); + + assert_eq!(user_3_invites, &[channel_1_1]); + + let members = db + .get_channel_member_details(channel_1_1, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: false, + }, + proto::ChannelMember { + user_id: user_3.to_proto(), + kind: proto::channel_member::Kind::Invitee.into(), + admin: true, + }, + ] + ); + + db.respond_to_channel_invite(channel_1_1, user_2, true) + .await + .unwrap(); + + let channel_1_3 = db + .create_channel("channel_3", Some(channel_1_1), "1", user_1) + .await + .unwrap(); + + let members = db + .get_channel_member_details(channel_1_3, user_1) + .await + .unwrap(); + assert_eq!( + members, + &[ + proto::ChannelMember { + user_id: user_1.to_proto(), + kind: proto::channel_member::Kind::Member.into(), + admin: true, + }, + proto::ChannelMember { + user_id: user_2.to_proto(), + kind: proto::channel_member::Kind::AncestorMember.into(), + admin: false, + }, + ] + ); +} + test_both_dbs!( + test_channel_renames, test_channel_renames_postgres, - test_channel_renames_sqlite, - db, - { - db.create_server("test").await.unwrap(); - - let user_1 = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let user_2 = db - .create_user( - "user2@example.com", - false, - NewUserParams { - github_login: "user2".into(), - github_user_id: 6, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); - - db.rename_channel(zed_id, user_1, "#zed-archive") - .await - .unwrap(); - - let zed_archive_id = zed_id; - - let (channel, _) = db - .get_channel(zed_archive_id, user_1) - .await - .unwrap() - .unwrap(); - assert_eq!(channel.name, "zed-archive"); - - let non_permissioned_rename = db - .rename_channel(zed_archive_id, user_2, "hacked-lol") - .await; - assert!(non_permissioned_rename.is_err()); - - let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; - assert!(bad_name_rename.is_err()) - } + test_channel_renames_sqlite ); +async fn test_channel_renames(db: &Arc) { + db.create_server("test").await.unwrap(); + + let user_1 = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + "user2@example.com", + false, + NewUserParams { + github_login: "user2".into(), + github_user_id: 6, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", user_1).await.unwrap(); + + db.rename_channel(zed_id, user_1, "#zed-archive") + .await + .unwrap(); + + let zed_archive_id = zed_id; + + let (channel, _) = db + .get_channel(zed_archive_id, user_1) + .await + .unwrap() + .unwrap(); + assert_eq!(channel.name, "zed-archive"); + + let non_permissioned_rename = db + .rename_channel(zed_archive_id, user_2, "hacked-lol") + .await; + assert!(non_permissioned_rename.is_err()); + + let bad_name_rename = db.rename_channel(zed_id, user_1, "#").await; + assert!(bad_name_rename.is_err()) +} + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/test_db.rs index 064f85c700..40013d5b03 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/test_db.rs @@ -91,6 +91,23 @@ impl TestDb { } } +#[macro_export] +macro_rules! test_both_dbs { + ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { + #[gpui::test] + async fn $postgres_test_name() { + let test_db = TestDb::postgres(Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; + } + + #[gpui::test] + async fn $sqlite_test_name() { + let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); + $test_name(test_db.db()).await; + } + }; +} + impl Drop for TestDb { fn drop(&mut self) { let db = self.db.take().unwrap(); From ff5035ea3761026deadc595e483fd1bd8057230c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 21 Aug 2023 12:00:43 -0700 Subject: [PATCH 023/142] Start work on storing channel buffers --- Cargo.lock | 2 + Cargo.toml | 1 + crates/collab/Cargo.toml | 2 + .../20221109000000_test_schema.sql | 27 +- .../20230819154600_add_channel_buffers.sql | 25 ++ crates/collab/src/db.rs | 2 + crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries.rs | 4 + crates/collab/src/db/queries/buffer_tests.rs | 41 +++ crates/collab/src/db/queries/buffers.rs | 271 ++++++++++++++++++ crates/collab/src/db/tables.rs | 3 + crates/collab/src/db/tables/buffer.rs | 32 +++ .../collab/src/db/tables/buffer_operation.rs | 37 +++ .../collab/src/db/tables/buffer_snapshot.rs | 30 ++ crates/collab/src/db/test_db.rs | 8 +- crates/rpc/Cargo.toml | 2 +- 16 files changed, 484 insertions(+), 4 deletions(-) create mode 100644 crates/collab/migrations/20230819154600_add_channel_buffers.sql create mode 100644 crates/collab/src/db/queries/buffer_tests.rs create mode 100644 crates/collab/src/db/queries/buffers.rs create mode 100644 crates/collab/src/db/tables/buffer.rs create mode 100644 crates/collab/src/db/tables/buffer_operation.rs create mode 100644 crates/collab/src/db/tables/buffer_snapshot.rs diff --git a/Cargo.lock b/Cargo.lock index 101a495b6e..b10d8730fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1444,6 +1444,7 @@ dependencies = [ "pretty_assertions", "project", "prometheus", + "prost 0.8.0", "rand 0.8.5", "reqwest", "rpc", @@ -1456,6 +1457,7 @@ dependencies = [ "settings", "sha-1 0.9.8", "sqlx", + "text", "theme", "time 0.3.24", "tokio", diff --git a/Cargo.toml b/Cargo.toml index cd15a72366..a35b3eea23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -96,6 +96,7 @@ log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" } parking_lot = { version = "0.11.1" } postage = { version = "0.5", features = ["futures-traits"] } +prost = { version = "0.8" } rand = { version = "0.8.5" } refineable = { path = "./crates/refineable" } regex = { version = "1.5" } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index b8d0c26960..49d17bdc63 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -16,6 +16,7 @@ required-features = ["seed-support"] [dependencies] collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } +text = { path = "../text" } rpc = { path = "../rpc" } util = { path = "../util" } @@ -35,6 +36,7 @@ log.workspace = true nanoid = "0.4" parking_lot.workspace = true prometheus = "0.13" +prost.workspace = true rand.workspace = true reqwest = { version = "0.11", features = ["json"], optional = true } scrypt = "0.7" diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 3dceaecef4..1e4663a6f6 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -189,7 +189,8 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now + "created_at" TIMESTAMP NOT NULL DEFAULT now, + "main_buffer_id" INTEGER REFERENCES buffers (id) ); CREATE TABLE "channel_paths" ( @@ -208,3 +209,27 @@ CREATE TABLE "channel_members" ( ); CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channel_members" ("channel_id", "user_id"); + +CREATE TABLE "buffers" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "local_timestamp" INTEGER NOT NULL, + "version" BLOB NOT NULL, + "is_undo" BOOLEAN NOT NULL, + "value" BLOB NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql new file mode 100644 index 0000000000..a4d936fd74 --- /dev/null +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -0,0 +1,25 @@ +CREATE TABLE "buffers" ( + "id" SERIAL PRIMARY KEY, + "epoch" INTEGER NOT NULL DEFAULT 0 +); + +CREATE TABLE "buffer_operations" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "replica_id" INTEGER NOT NULL, + "local_timestamp" INTEGER NOT NULL, + "lamport_timestamp" INTEGER NOT NULL, + "version" BYTEA NOT NULL, + "is_undo" BOOLEAN NOT NULL, + "value" BYTEA NOT NULL, + PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) +); + +CREATE TABLE "buffer_snapshots" ( + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "epoch" INTEGER NOT NULL, + "text" TEXT NOT NULL, + PRIMARY KEY(buffer_id, epoch) +); + +ALTER TABLE "channels" ADD COLUMN "main_buffer_id" INTEGER REFERENCES buffers (id); diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index d322b03589..19915777dc 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -52,6 +52,8 @@ pub struct Database { runtime: Option, } +// The `Database` type has so many methods that its impl blocks are split into +// separate files in the `queries` folder. impl Database { pub async fn new(options: ConnectOptions, executor: Executor) -> Result { Ok(Self { diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 514c973dad..54f9463cca 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -110,6 +110,7 @@ fn value_to_integer(v: Value) -> Result { } } +id_type!(BufferId); id_type!(AccessTokenId); id_type!(ChannelId); id_type!(ChannelMemberId); diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index f67bde30b8..c4a1d57eb4 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -1,6 +1,7 @@ use super::*; pub mod access_tokens; +pub mod buffers; pub mod channels; pub mod contacts; pub mod projects; @@ -8,3 +9,6 @@ pub mod rooms; pub mod servers; pub mod signups; pub mod users; + +#[cfg(test)] +pub mod buffer_tests; diff --git a/crates/collab/src/db/queries/buffer_tests.rs b/crates/collab/src/db/queries/buffer_tests.rs new file mode 100644 index 0000000000..f0e78e1fe4 --- /dev/null +++ b/crates/collab/src/db/queries/buffer_tests.rs @@ -0,0 +1,41 @@ +use super::*; +use crate::test_both_dbs; +use language::proto; +use text::Buffer; + +test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); + +async fn test_buffers(db: &Arc) { + let buffer_id = db.create_buffer().await.unwrap(); + + let mut buffer = Buffer::new(0, 0, "".to_string()); + let mut operations = Vec::new(); + operations.push(buffer.edit([(0..0, "hello world")])); + operations.push(buffer.edit([(5..5, ", cruel")])); + operations.push(buffer.edit([(0..5, "goodbye")])); + operations.push(buffer.undo().unwrap().1); + assert_eq!(buffer.text(), "hello, cruel world"); + + let operations = operations + .into_iter() + .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) + .collect::>(); + + db.update_buffer(buffer_id, &operations).await.unwrap(); + + let buffer_data = db.get_buffer(buffer_id).await.unwrap(); + + let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); + buffer_2 + .apply_ops(buffer_data.operations.into_iter().map(|operation| { + let operation = proto::deserialize_operation(operation).unwrap(); + if let language::Operation::Buffer(operation) = operation { + operation + } else { + unreachable!() + } + })) + .unwrap(); + + assert_eq!(buffer_2.text(), "hello, cruel world"); +} diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs new file mode 100644 index 0000000000..f5ff2e3367 --- /dev/null +++ b/crates/collab/src/db/queries/buffers.rs @@ -0,0 +1,271 @@ +use super::*; +use prost::Message; + +pub struct Buffer { + pub base_text: String, + pub operations: Vec, +} + +impl Database { + pub async fn create_buffer(&self) -> Result { + self.transaction(|tx| async move { + let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + Ok(buffer.id) + }) + .await + } + + pub async fn update_buffer( + &self, + buffer_id: BufferId, + operations: &[proto::Operation], + ) -> Result<()> { + self.transaction(|tx| async move { + let buffer = buffer::Entity::find_by_id(buffer_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = + serialize_edit_operation(&operation.ranges, &operation.new_text); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = serialize_undo_operation(&operation.counts); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } + })) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_buffer(&self, id: BufferId) -> Result { + self.transaction(|tx| async move { + let buffer = buffer::Entity::find_by_id(id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let base_text = if buffer.epoch > 0 { + buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))? + .text + } else { + String::new() + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + let version = deserialize_version(&row.version)?; + let operation = if row.is_undo { + let counts = deserialize_undo_operation(&row.value)?; + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts, + }) + } else { + let (ranges, new_text) = deserialize_edit_operation(&row.value)?; + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges, + new_text, + }) + }; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok(Buffer { + base_text, + operations, + }) + }) + .await + } +} + +mod storage { + #![allow(non_snake_case)] + + use prost::Message; + + pub const VERSION: usize = 1; + + #[derive(Message)] + pub struct VectorClock { + #[prost(message, repeated, tag = "1")] + pub entries: Vec, + } + + #[derive(Message)] + pub struct VectorClockEntry { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub timestamp: u32, + } + + #[derive(Message)] + pub struct TextEdit { + #[prost(message, repeated, tag = "1")] + pub ranges: Vec, + #[prost(string, repeated, tag = "2")] + pub texts: Vec, + } + + #[derive(Message)] + pub struct Range { + #[prost(uint64, tag = "1")] + pub start: u64, + #[prost(uint64, tag = "2")] + pub end: u64, + } + + #[derive(Message)] + pub struct Undo { + #[prost(message, repeated, tag = "1")] + pub entries: Vec, + } + + #[derive(Message)] + pub struct UndoCount { + #[prost(uint32, tag = "1")] + pub replica_id: u32, + #[prost(uint32, tag = "2")] + pub local_timestamp: u32, + #[prost(uint32, tag = "3")] + pub count: u32, + } +} + +fn serialize_version(version: &Vec) -> Vec { + storage::VectorClock { + entries: version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect(), + } + .encode_to_vec() +} + +fn deserialize_version(bytes: &[u8]) -> Result> { + let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; + Ok(clock + .entries + .into_iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect()) +} + +fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec { + storage::TextEdit { + ranges: ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + texts: texts.to_vec(), + } + .encode_to_vec() +} + +fn deserialize_edit_operation(bytes: &[u8]) -> Result<(Vec, Vec)> { + let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; + let ranges = edit + .ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(); + Ok((ranges, edit.texts)) +} + +fn serialize_undo_operation(counts: &Vec) -> Vec { + storage::Undo { + entries: counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + } + .encode_to_vec() +} + +fn deserialize_undo_operation(bytes: &[u8]) -> Result> { + let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; + Ok(undo + .entries + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect()) +} diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index c4c7e4f312..fbf4bff2a6 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -1,4 +1,7 @@ pub mod access_token; +pub mod buffer; +pub mod buffer_operation; +pub mod buffer_snapshot; pub mod channel; pub mod channel_member; pub mod channel_path; diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs new file mode 100644 index 0000000000..84e62cc071 --- /dev/null +++ b/crates/collab/src/db/tables/buffer.rs @@ -0,0 +1,32 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffers")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: BufferId, + pub epoch: i32, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::buffer_operation::Entity")] + Operations, + #[sea_orm(has_many = "super::buffer_snapshot::Entity")] + Snapshots, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Operations.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Snapshots.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs new file mode 100644 index 0000000000..59626c1e77 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -0,0 +1,37 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_operations")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + #[sea_orm(primary_key)] + pub lamport_timestamp: i32, + #[sea_orm(primary_key)] + pub replica_id: i32, + pub local_timestamp: i32, + pub version: Vec, + pub is_undo: bool, + pub value: Vec, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs new file mode 100644 index 0000000000..ca8712a053 --- /dev/null +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -0,0 +1,30 @@ +use crate::db::BufferId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "buffer_snapshots")] +pub struct Model { + #[sea_orm(primary_key)] + pub buffer_id: BufferId, + #[sea_orm(primary_key)] + pub epoch: i32, + pub text: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/test_db.rs index 40013d5b03..71e352eb86 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/test_db.rs @@ -96,13 +96,17 @@ macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] async fn $postgres_test_name() { - let test_db = TestDb::postgres(Deterministic::new(0).build_background()); + let test_db = crate::db::test_db::TestDb::postgres( + gpui::executor::Deterministic::new(0).build_background(), + ); $test_name(test_db.db()).await; } #[gpui::test] async fn $sqlite_test_name() { - let test_db = TestDb::sqlite(Deterministic::new(0).build_background()); + let test_db = crate::db::test_db::TestDb::sqlite( + gpui::executor::Deterministic::new(0).build_background(), + ); $test_name(test_db.db()).await; } }; diff --git a/crates/rpc/Cargo.toml b/crates/rpc/Cargo.toml index 008fa9c316..3c307be4fb 100644 --- a/crates/rpc/Cargo.toml +++ b/crates/rpc/Cargo.toml @@ -23,7 +23,7 @@ async-tungstenite = "0.16" base64 = "0.13" futures.workspace = true parking_lot.workspace = true -prost = "0.8" +prost.workspace = true rand.workspace = true rsa = "0.4" serde.workspace = true From a7a4e2e3699659af9e8bbfd232faa3608aae97ea Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 21 Aug 2023 16:30:57 -0700 Subject: [PATCH 024/142] Add buffer integration test Rearrange channel crate structure Get channel buffer from database co-authored-by: Max --- Cargo.lock | 41 +++++++++ Cargo.toml | 1 + crates/call/Cargo.toml | 1 + crates/call/src/call.rs | 5 +- crates/channel/Cargo.toml | 51 +++++++++++ crates/channel/src/channel.rs | 7 ++ crates/channel/src/channel_buffer.rs | 80 ++++++++++++++++++ .../{client => channel}/src/channel_store.rs | 6 +- .../src/channel_store_tests.rs | 3 + crates/client/Cargo.toml | 1 + crates/client/src/client.rs | 5 -- crates/client/src/user.rs | 4 +- crates/collab/Cargo.toml | 1 + crates/collab/src/db.rs | 5 +- crates/collab/src/db/queries.rs | 3 - crates/collab/src/db/queries/channels.rs | 28 +++++++ crates/collab/src/db/tables/channel.rs | 3 +- crates/collab/src/db/{test_db.rs => tests.rs} | 10 ++- .../src/db/{queries => tests}/buffer_tests.rs | 0 crates/collab/src/db/{ => tests}/db_tests.rs | 31 ++++++- crates/collab/src/rpc.rs | 25 +++++- crates/collab/src/tests.rs | 7 +- .../collab/src/tests/channel_buffer_tests.rs | 84 +++++++++++++++++++ crates/collab/src/tests/channel_tests.rs | 3 +- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_panel.rs | 6 +- .../src/collab_panel/channel_modal.rs | 3 +- crates/rpc/proto/zed.proto | 12 +++ crates/rpc/src/proto.rs | 5 +- crates/workspace/Cargo.toml | 1 + crates/workspace/src/workspace.rs | 3 +- crates/zed/Cargo.toml | 1 + crates/zed/src/main.rs | 5 +- 33 files changed, 403 insertions(+), 39 deletions(-) create mode 100644 crates/channel/Cargo.toml create mode 100644 crates/channel/src/channel.rs create mode 100644 crates/channel/src/channel_buffer.rs rename crates/{client => channel}/src/channel_store.rs (99%) rename crates/{client => channel}/src/channel_store_tests.rs (98%) rename crates/collab/src/db/{test_db.rs => tests.rs} (95%) rename crates/collab/src/db/{queries => tests}/buffer_tests.rs (100%) rename crates/collab/src/db/{ => tests}/db_tests.rs (98%) create mode 100644 crates/collab/src/tests/channel_buffer_tests.rs diff --git a/Cargo.lock b/Cargo.lock index b10d8730fb..a40aa7d89c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1063,6 +1063,7 @@ dependencies = [ "anyhow", "async-broadcast", "audio", + "channel", "client", "collections", "fs", @@ -1190,6 +1191,41 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "channel" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "db", + "futures 0.3.28", + "gpui", + "image", + "language", + "lazy_static", + "log", + "parking_lot 0.11.2", + "postage", + "rand 0.8.5", + "rpc", + "schemars", + "serde", + "serde_derive", + "settings", + "smol", + "staff_mode", + "sum_tree", + "tempfile", + "text", + "thiserror", + "time 0.3.24", + "tiny_http", + "url", + "util", + "uuid 1.4.1", +] + [[package]] name = "chrono" version = "0.4.26" @@ -1354,6 +1390,7 @@ dependencies = [ "staff_mode", "sum_tree", "tempfile", + "text", "thiserror", "time 0.3.24", "tiny_http", @@ -1418,6 +1455,7 @@ dependencies = [ "axum-extra", "base64 0.13.1", "call", + "channel", "clap 3.2.25", "client", "collections", @@ -1480,6 +1518,7 @@ dependencies = [ "anyhow", "auto_update", "call", + "channel", "client", "clock", "collections", @@ -9536,6 +9575,7 @@ dependencies = [ "async-recursion 1.0.4", "bincode", "call", + "channel", "client", "collections", "context_menu", @@ -9661,6 +9701,7 @@ dependencies = [ "backtrace", "breadcrumbs", "call", + "channel", "chrono", "cli", "client", diff --git a/Cargo.toml b/Cargo.toml index a35b3eea23..0fb8f0b6b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "crates/auto_update", "crates/breadcrumbs", "crates/call", + "crates/channel", "crates/cli", "crates/client", "crates/clock", diff --git a/crates/call/Cargo.toml b/crates/call/Cargo.toml index eb448d8d8d..b4e94fe56c 100644 --- a/crates/call/Cargo.toml +++ b/crates/call/Cargo.toml @@ -20,6 +20,7 @@ test-support = [ [dependencies] audio = { path = "../audio" } +channel = { path = "../channel" } client = { path = "../client" } collections = { path = "../collections" } gpui = { path = "../gpui" } diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 5fef53fa81..5af094df05 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -7,9 +7,8 @@ use std::sync::Arc; use anyhow::{anyhow, Result}; use audio::Audio; use call_settings::CallSettings; -use client::{ - proto, ChannelId, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore, -}; +use channel::ChannelId; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml new file mode 100644 index 0000000000..0978462a1a --- /dev/null +++ b/crates/channel/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "channel" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/channel.rs" +doctest = false + +[features] +test-support = ["collections/test-support", "gpui/test-support", "rpc/test-support"] + +[dependencies] +client = { path = "../client" } +collections = { path = "../collections" } +db = { path = "../db" } +gpui = { path = "../gpui" } +util = { path = "../util" } +rpc = { path = "../rpc" } +text = { path = "../text" } +language = { path = "../language" } +settings = { path = "../settings" } +staff_mode = { path = "../staff_mode" } +sum_tree = { path = "../sum_tree" } + +anyhow.workspace = true +futures.workspace = true +image = "0.23" +lazy_static.workspace = true +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +rand.workspace = true +schemars.workspace = true +smol.workspace = true +thiserror.workspace = true +time.workspace = true +tiny_http = "0.8" +uuid = { version = "1.1.2", features = ["v4"] } +url = "2.2" +serde.workspace = true +serde_derive.workspace = true +tempfile = "3" + +[dev-dependencies] +collections = { path = "../collections", features = ["test-support"] } +gpui = { path = "../gpui", features = ["test-support"] } +rpc = { path = "../rpc", features = ["test-support"] } +settings = { path = "../settings", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs new file mode 100644 index 0000000000..67c560a1fc --- /dev/null +++ b/crates/channel/src/channel.rs @@ -0,0 +1,7 @@ +mod channel_store; + +pub mod channel_buffer; +pub use channel_store::*; + +#[cfg(test)] +mod channel_store_tests; diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs new file mode 100644 index 0000000000..10f59bce46 --- /dev/null +++ b/crates/channel/src/channel_buffer.rs @@ -0,0 +1,80 @@ +use crate::ChannelId; +use anyhow::Result; +use client::Client; +use gpui::{Entity, ModelContext, ModelHandle, Task}; +use rpc::proto::GetChannelBuffer; +use std::sync::Arc; + +// Open the channel document +// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor +// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions +// ChannelDocuments -> Buffers -> Editor with that buffer + +// ChannelDocuments { +// ChannleBuffers: HashMap> +// } + +pub struct ChannelBuffer { + channel_id: ChannelId, + buffer: Option>, + client: Arc, +} + +impl Entity for ChannelBuffer { + type Event = (); +} + +impl ChannelBuffer { + pub fn for_channel( + channel_id: ChannelId, + client: Arc, + cx: &mut ModelContext, + ) -> Self { + Self { + channel_id, + client, + buffer: None, + } + } + + fn on_buffer_update( + &mut self, + buffer: ModelHandle, + event: &language::Event, + cx: &mut ModelContext, + ) { + // + } + + pub fn buffer( + &mut self, + cx: &mut ModelContext, + ) -> Task>> { + if let Some(buffer) = &self.buffer { + Task::ready(Ok(buffer.clone())) + } else { + let channel_id = self.channel_id; + let client = self.client.clone(); + cx.spawn(|this, mut cx| async move { + let response = client.request(GetChannelBuffer { channel_id }).await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + + this.buffer = Some(buffer.clone()); + anyhow::Ok(buffer) + }) + }) + } + } +} diff --git a/crates/client/src/channel_store.rs b/crates/channel/src/channel_store.rs similarity index 99% rename from crates/client/src/channel_store.rs rename to crates/channel/src/channel_store.rs index 6352ac791e..b9b2c98acd 100644 --- a/crates/client/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,7 +1,8 @@ -use crate::Status; -use crate::{Client, Subscription, User, UserStore}; use anyhow::anyhow; use anyhow::Result; +use client::Status; +use client::UserId; +use client::{Client, Subscription, User, UserStore}; use collections::HashMap; use collections::HashSet; use futures::channel::mpsc; @@ -13,7 +14,6 @@ use std::sync::Arc; use util::ResultExt; pub type ChannelId = u64; -pub type UserId = u64; pub struct ChannelStore { channels_by_id: HashMap>, diff --git a/crates/client/src/channel_store_tests.rs b/crates/channel/src/channel_store_tests.rs similarity index 98% rename from crates/client/src/channel_store_tests.rs rename to crates/channel/src/channel_store_tests.rs index 51e819349e..18894b1f47 100644 --- a/crates/client/src/channel_store_tests.rs +++ b/crates/channel/src/channel_store_tests.rs @@ -1,4 +1,7 @@ use super::*; +use client::{Client, UserStore}; +use gpui::{AppContext, ModelHandle}; +use rpc::proto; use util::http::FakeHttpClient; #[gpui::test] diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 3ecc515986..64d8f02c8a 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -17,6 +17,7 @@ db = { path = "../db" } gpui = { path = "../gpui" } util = { path = "../util" } rpc = { path = "../rpc" } +text = { path = "../text" } settings = { path = "../settings" } staff_mode = { path = "../staff_mode" } sum_tree = { path = "../sum_tree" } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 8ef3e32ea8..a32c415f7e 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -1,10 +1,6 @@ #[cfg(any(test, feature = "test-support"))] pub mod test; -#[cfg(test)] -mod channel_store_tests; - -pub mod channel_store; pub mod telemetry; pub mod user; @@ -48,7 +44,6 @@ use util::channel::ReleaseChannel; use util::http::HttpClient; use util::{ResultExt, TryFutureExt}; -pub use channel_store::*; pub use rpc::*; pub use telemetry::ClickhouseEvent; pub use user::*; diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index be11d1fb44..1dc384da17 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -10,9 +10,11 @@ use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; +pub type UserId = u64; + #[derive(Default, Debug)] pub struct User { - pub id: u64, + pub id: UserId, pub github_login: String, pub avatar: Option>, } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 49d17bdc63..fc78a03f67 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -64,6 +64,7 @@ collections = { path = "../collections", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } call = { path = "../call", features = ["test-support"] } client = { path = "../client", features = ["test-support"] } +channel = { path = "../channel" } editor = { path = "../editor", features = ["test-support"] } language = { path = "../language", features = ["test-support"] } fs = { path = "../fs", features = ["test-support"] } diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 19915777dc..9c759f79a8 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -1,7 +1,8 @@ #[cfg(test)] -mod db_tests; +pub mod tests; + #[cfg(test)] -pub mod test_db; +pub use tests::TestDb; mod ids; mod queries; diff --git a/crates/collab/src/db/queries.rs b/crates/collab/src/db/queries.rs index c4a1d57eb4..09a8f073b4 100644 --- a/crates/collab/src/db/queries.rs +++ b/crates/collab/src/db/queries.rs @@ -9,6 +9,3 @@ pub mod rooms; pub mod servers; pub mod signups; pub mod users; - -#[cfg(test)] -pub mod buffer_tests; diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index e3d3643a61..85a9304a2e 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -689,6 +689,34 @@ impl Database { }) .await } + + pub async fn get_or_create_buffer_for_channel( + &self, + channel_id: ChannelId, + ) -> Result { + self.transaction(|tx| async move { + let tx = tx; + let channel = channel::Entity::find_by_id(channel_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("invalid channel"))?; + + if let Some(id) = channel.main_buffer_id { + return Ok(id); + } else { + let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + channel::ActiveModel { + id: ActiveValue::Unchanged(channel_id), + main_buffer_id: ActiveValue::Set(Some(buffer.id)), + ..Default::default() + } + .update(&*tx) + .await?; + Ok(buffer.id) + } + }) + .await + } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index f00b4ced62..444d5fa6d9 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::ChannelId; +use crate::db::{BufferId, ChannelId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, + pub main_buffer_id: Option, } impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/test_db.rs b/crates/collab/src/db/tests.rs similarity index 95% rename from crates/collab/src/db/test_db.rs rename to crates/collab/src/db/tests.rs index 71e352eb86..36a0888a62 100644 --- a/crates/collab/src/db/test_db.rs +++ b/crates/collab/src/db/tests.rs @@ -1,3 +1,6 @@ +mod buffer_tests; +mod db_tests; + use super::*; use gpui::executor::Background; use parking_lot::Mutex; @@ -96,7 +99,7 @@ macro_rules! test_both_dbs { ($test_name:ident, $postgres_test_name:ident, $sqlite_test_name:ident) => { #[gpui::test] async fn $postgres_test_name() { - let test_db = crate::db::test_db::TestDb::postgres( + let test_db = crate::db::TestDb::postgres( gpui::executor::Deterministic::new(0).build_background(), ); $test_name(test_db.db()).await; @@ -104,9 +107,8 @@ macro_rules! test_both_dbs { #[gpui::test] async fn $sqlite_test_name() { - let test_db = crate::db::test_db::TestDb::sqlite( - gpui::executor::Deterministic::new(0).build_background(), - ); + let test_db = + crate::db::TestDb::sqlite(gpui::executor::Deterministic::new(0).build_background()); $test_name(test_db.db()).await; } }; diff --git a/crates/collab/src/db/queries/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs similarity index 100% rename from crates/collab/src/db/queries/buffer_tests.rs rename to crates/collab/src/db/tests/buffer_tests.rs diff --git a/crates/collab/src/db/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs similarity index 98% rename from crates/collab/src/db/db_tests.rs rename to crates/collab/src/db/tests/db_tests.rs index 8c5dab77bd..0fffabc7c4 100644 --- a/crates/collab/src/db/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -3,7 +3,7 @@ use crate::test_both_dbs; use gpui::executor::{Background, Deterministic}; use pretty_assertions::{assert_eq, assert_ne}; use std::sync::Arc; -use test_db::TestDb; +use tests::TestDb; test_both_dbs!( test_get_users, @@ -1329,6 +1329,35 @@ async fn test_channel_renames(db: &Arc) { assert!(bad_name_rename.is_err()) } +test_both_dbs!( + test_get_or_create_channel_buffer, + test_get_or_create_channel_buffer_postgres, + test_get_or_create_channel_buffer_sqlite +); + +async fn test_get_or_create_channel_buffer(db: &Arc) { + let a_id = db + .create_user( + "user1@example.com", + false, + NewUserParams { + github_login: "user1".into(), + github_user_id: 5, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + let first_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); + let second_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); + + assert_eq!(first_buffer_id, second_buffer_id); +} + #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 521aa3e7b4..22eb23ce8e 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -35,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, GetChannelBufferResponse, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,6 +248,7 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) + .add_request_handler(get_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2478,6 +2479,26 @@ async fn join_channel( Ok(()) } +async fn get_channel_buffer( + request: proto::GetChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; + + let buffer = db.get_buffer(buffer_id).await?; + + response.send(GetChannelBufferResponse { + base_text: buffer.base_text, + operations: buffer.operations, + })?; + + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index c9f358ca5b..831bccbb72 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -1,14 +1,14 @@ use crate::{ - db::{test_db::TestDb, NewUserParams, UserId}, + db::{tests::TestDb, NewUserParams, UserId}, executor::Executor, rpc::{Server, CLEANUP_TIMEOUT}, AppState, }; use anyhow::anyhow; use call::{ActiveCall, Room}; +use channel::ChannelStore; use client::{ - self, proto::PeerId, ChannelStore, Client, Connection, Credentials, EstablishConnectionError, - UserStore, + self, proto::PeerId, Client, Connection, Credentials, EstablishConnectionError, UserStore, }; use collections::{HashMap, HashSet}; use fs::FakeFs; @@ -31,6 +31,7 @@ use std::{ use util::http::FakeHttpClient; use workspace::Workspace; +mod channel_buffer_tests; mod channel_tests; mod integration_tests; mod randomized_integration_tests; diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs new file mode 100644 index 0000000000..e7f662523e --- /dev/null +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -0,0 +1,84 @@ +use crate::tests::TestServer; + +use channel::channel_buffer::ChannelBuffer; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; +use std::{ops::Range, sync::Arc}; + +#[gpui::test] +async fn test_channel_buffers( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let zed_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let a_document = + cx_a.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)); + let channel_buffer_a = a_document + .update(cx_a, |doc, cx| doc.buffer(cx)) + .await + .unwrap(); + + edit_channel_buffer(&channel_buffer_a, cx_a, [(0..0, "hello world")]); + edit_channel_buffer(&channel_buffer_a, cx_a, [(5..5, ", cruel")]); + edit_channel_buffer(&channel_buffer_a, cx_a, [(0..5, "goodbye")]); + undo_channel_buffer(&channel_buffer_a, cx_a); + + assert_eq!( + channel_buffer_text(&channel_buffer_a, cx_a), + "hello, cruel world" + ); + + let b_document = + cx_b.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)); + let channel_buffer_b = b_document + .update(cx_b, |doc, cx| doc.buffer(cx)) + .await + .unwrap(); + + assert_eq!( + channel_buffer_text(&channel_buffer_b, cx_b), + "hello, cruel world" + ); + + edit_channel_buffer(&channel_buffer_b, cx_b, [(7..12, "beautiful")]); + + deterministic.run_until_parked(); + + assert_eq!( + channel_buffer_text(&channel_buffer_a, cx_a), + "hello, beautiful world" + ); + assert_eq!( + channel_buffer_text(&channel_buffer_b, cx_b), + "hello, beautiful world" + ); +} + +fn edit_channel_buffer( + channel_buffer: &ModelHandle, + cx: &mut TestAppContext, + edits: I, +) where + I: IntoIterator, &'static str)>, +{ + channel_buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx)); +} + +fn undo_channel_buffer(channel_buffer: &ModelHandle, cx: &mut TestAppContext) { + channel_buffer.update(cx, |buffer, cx| buffer.undo(cx)); +} + +fn channel_buffer_text( + channel_buffer: &ModelHandle, + cx: &mut TestAppContext, +) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 06cf3607c0..41d2286772 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -3,7 +3,8 @@ use crate::{ tests::{room_participants, RoomParticipants, TestServer}, }; use call::ActiveCall; -use client::{ChannelId, ChannelMembership, ChannelStore, User}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::User; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use std::sync::Arc; diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 471608c43e..e0177f6609 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -26,6 +26,7 @@ auto_update = { path = "../auto_update" } db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } clock = { path = "../clock" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5623ada42d..ab692dd166 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -4,10 +4,8 @@ mod panel_settings; use anyhow::Result; use call::ActiveCall; -use client::{ - proto::PeerId, Channel, ChannelEvent, ChannelId, ChannelStore, Client, Contact, User, UserStore, -}; - +use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; +use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 75ab40be85..0adf2806d7 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -1,4 +1,5 @@ -use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore}; +use channel::{ChannelId, ChannelMembership, ChannelStore}; +use client::{proto, User, UserId, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index caa5efd2cb..baeaae1876 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -142,6 +142,9 @@ message Envelope { GetChannelMembersResponse get_channel_members_response = 128; SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; + + GetChannelBuffer get_channel_buffer = 131; + GetChannelBufferResponse get_channel_buffer_response = 132; } } @@ -948,6 +951,15 @@ message RenameChannel { string name = 2; } +message GetChannelBuffer { + uint64 channel_id = 1; +} + +message GetChannelBufferResponse { + string base_text = 1; + repeated Operation operations = 2; +} + message RespondToChannelInvite { uint64 channel_id = 1; bool accept = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 92732b00b5..21a491b934 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -248,7 +248,9 @@ messages!( (GetPrivateUserInfo, Foreground), (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), - (GetChannelMembersResponse, Foreground) + (GetChannelMembersResponse, Foreground), + (GetChannelBuffer, Foreground), + (GetChannelBufferResponse, Foreground) ); request_messages!( @@ -315,6 +317,7 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), + (GetChannelBuffer, GetChannelBufferResponse) ); entity_messages!( diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index 8606be4944..e2dae07b8c 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -22,6 +22,7 @@ test-support = [ db = { path = "../db" } call = { path = "../call" } client = { path = "../client" } +channel = { path = "../channel" } collections = { path = "../collections" } context_menu = { path = "../context_menu" } drag_and_drop = { path = "../drag_and_drop" } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 79b701e015..a8354472aa 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -12,9 +12,10 @@ mod workspace_settings; use anyhow::{anyhow, Context, Result}; use call::ActiveCall; +use channel::ChannelStore; use client::{ proto::{self, PeerId}, - ChannelStore, Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use drag_and_drop::DragAndDrop; diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index faa3ae6953..92900f84cb 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -21,6 +21,7 @@ activity_indicator = { path = "../activity_indicator" } auto_update = { path = "../auto_update" } breadcrumbs = { path = "../breadcrumbs" } call = { path = "../call" } +channel = { path = "../channel" } cli = { path = "../cli" } collab_ui = { path = "../collab_ui" } collections = { path = "../collections" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index caeaeceded..b905c1d37b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -3,13 +3,12 @@ use anyhow::{anyhow, Context, Result}; use backtrace::Backtrace; +use channel::ChannelStore; use cli::{ ipc::{self, IpcSender}, CliRequest, CliResponse, IpcHandshake, FORCE_CLI_MODE_ENV_VAR_NAME, }; -use client::{ - self, ChannelStore, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN, -}; +use client::{self, TelemetrySettings, UserStore, ZED_APP_VERSION, ZED_SECRET_CLIENT_TOKEN}; use db::kvp::KEY_VALUE_STORE; use editor::{scroll::autoscroll::Autoscroll, Editor}; use futures::{ From 364ed1f840fc62e3dbb2da464d75cbfec2f100c0 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 21 Aug 2023 17:53:37 -0700 Subject: [PATCH 025/142] WIP: pass synchronize channel buffers integration test --- crates/channel/src/channel_buffer.rs | 113 +++++++++++------- .../20221109000000_test_schema.sql | 21 +++- .../20230819154600_add_channel_buffers.sql | 18 ++- crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/buffers.rs | 105 +++++++++++++--- crates/collab/src/db/queries/channels.rs | 28 ----- crates/collab/src/db/tables.rs | 1 + crates/collab/src/db/tables/buffer.rs | 23 +++- crates/collab/src/db/tables/channel.rs | 9 +- .../db/tables/channel_buffer_collaborator.rs | 42 +++++++ crates/collab/src/db/tests/buffer_tests.rs | 57 ++++++++- crates/collab/src/rpc.rs | 52 ++++++-- .../collab/src/tests/channel_buffer_tests.rs | 40 +++---- crates/rpc/proto/zed.proto | 25 ++-- crates/rpc/src/proto.rs | 11 +- 15 files changed, 411 insertions(+), 135 deletions(-) create mode 100644 crates/collab/src/db/tables/channel_buffer_collaborator.rs diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 10f59bce46..372bd319a1 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,9 +1,10 @@ use crate::ChannelId; use anyhow::Result; use client::Client; -use gpui::{Entity, ModelContext, ModelHandle, Task}; -use rpc::proto::GetChannelBuffer; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use rpc::{proto, TypedEnvelope}; use std::sync::Arc; +use util::ResultExt; // Open the channel document // ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor @@ -14,9 +15,12 @@ use std::sync::Arc; // ChannleBuffers: HashMap> // } +type BufferId = u64; + pub struct ChannelBuffer { channel_id: ChannelId, - buffer: Option>, + buffer_id: BufferId, + buffer: ModelHandle, client: Arc, } @@ -28,53 +32,76 @@ impl ChannelBuffer { pub fn for_channel( channel_id: ChannelId, client: Arc, - cx: &mut ModelContext, - ) -> Self { - Self { - channel_id, - client, - buffer: None, - } + cx: &mut AppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let response = client + .request(proto::OpenChannelBuffer { channel_id }) + .await?; + + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + let buffer_id = response.buffer_id; + + let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); + client.add_model_message_handler(Self::handle_update_channel_buffer); + Self { + buffer_id, + buffer, + client, + channel_id, + } + })) + }) + } + + async fn handle_update_channel_buffer( + this: ModelHandle, + update_channel_buffer: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let ops = update_channel_buffer + .payload + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; + + this.update(&mut cx, |this, cx| { + this.buffer + .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) + })?; + + Ok(()) } fn on_buffer_update( &mut self, - buffer: ModelHandle, + _: ModelHandle, event: &language::Event, - cx: &mut ModelContext, + _: &mut ModelContext, ) { - // - } - - pub fn buffer( - &mut self, - cx: &mut ModelContext, - ) -> Task>> { - if let Some(buffer) = &self.buffer { - Task::ready(Ok(buffer.clone())) - } else { - let channel_id = self.channel_id; - let client = self.client.clone(); - cx.spawn(|this, mut cx| async move { - let response = client.request(GetChannelBuffer { channel_id }).await?; - - let base_text = response.base_text; - let operations = response - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; - - this.update(&mut cx, |this, cx| { - let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); - buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))?; - - cx.subscribe(&buffer, Self::on_buffer_update).detach(); - - this.buffer = Some(buffer.clone()); - anyhow::Ok(buffer) + if let language::Event::Operation(operation) = event { + let operation = language::proto::serialize_operation(operation); + self.client + .send(proto::UpdateChannelBuffer { + buffer_id: self.buffer_id, + operations: vec![operation], }) - }) + .log_err(); } } + + pub fn buffer(&self) -> ModelHandle { + self.buffer.clone() + } } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 1e4663a6f6..12ff2caec5 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -189,8 +189,7 @@ CREATE INDEX "index_followers_on_room_id" ON "followers" ("room_id"); CREATE TABLE "channels" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, "name" VARCHAR NOT NULL, - "created_at" TIMESTAMP NOT NULL DEFAULT now, - "main_buffer_id" INTEGER REFERENCES buffers (id) + "created_at" TIMESTAMP NOT NULL DEFAULT now ); CREATE TABLE "channel_paths" ( @@ -212,9 +211,12 @@ CREATE UNIQUE INDEX "index_channel_members_on_channel_id_and_user_id" ON "channe CREATE TABLE "buffers" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL DEFAULT 0 ); +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, @@ -233,3 +235,18 @@ CREATE TABLE "buffer_snapshots" ( "text" TEXT NOT NULL, PRIMARY KEY(buffer_id, epoch) ); + +CREATE TABLE "channel_buffer_collaborators" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index a4d936fd74..8ccd7acadf 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -1,8 +1,11 @@ CREATE TABLE "buffers" ( "id" SERIAL PRIMARY KEY, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL DEFAULT 0 ); +CREATE INDEX "index_buffers_on_channel_id" ON "buffers" ("channel_id"); + CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, @@ -22,4 +25,17 @@ CREATE TABLE "buffer_snapshots" ( PRIMARY KEY(buffer_id, epoch) ); -ALTER TABLE "channels" ADD COLUMN "main_buffer_id" INTEGER REFERENCES buffers (id); +CREATE TABLE "channel_buffer_collaborators" ( + "id" SERIAL PRIMARY KEY, + "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "connection_id" INTEGER NOT NULL, + "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "replica_id" INTEGER NOT NULL +); + +CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 54f9463cca..8501083f83 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -124,3 +124,4 @@ id_type!(ReplicaId); id_type!(ServerId); id_type!(SignupId); id_type!(UserId); +id_type!(ChannelBufferCollaboratorId); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index f5ff2e3367..ba88e95fb8 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,20 +1,12 @@ use super::*; use prost::Message; -pub struct Buffer { +pub struct ChannelBuffer { pub base_text: String, pub operations: Vec, } impl Database { - pub async fn create_buffer(&self) -> Result { - self.transaction(|tx| async move { - let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - Ok(buffer.id) - }) - .await - } - pub async fn update_buffer( &self, buffer_id: BufferId, @@ -69,13 +61,65 @@ impl Database { .await } - pub async fn get_buffer(&self, id: BufferId) -> Result { + pub async fn join_buffer_for_channel( + &self, + channel_id: ChannelId, + user_id: UserId, + connection: ConnectionId, + ) -> Result { self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; + let tx = tx; + // Get or create buffer from channel + self.check_user_is_channel_member(channel_id, user_id, &tx) + .await?; + + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await?; + + let buffer = if let Some(buffer) = buffer { + buffer + } else { + let buffer = buffer::ActiveModel { + channel_id: ActiveValue::Set(channel_id), + ..Default::default() + } + .insert(&*tx) + .await?; + buffer + }; + + // Join the collaborators + let collaborators = buffer + .find_related(channel_buffer_collaborator::Entity) + .all(&*tx) + .await?; + let replica_ids = collaborators + .iter() + .map(|c| c.replica_id) + .collect::>(); + let mut replica_id = ReplicaId(0); + while replica_ids.contains(&replica_id) { + replica_id.0 += 1; + } + channel_buffer_collaborator::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + connection_id: ActiveValue::Set(connection.id as i32), + connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), + user_id: ActiveValue::Set(user_id), + replica_id: ActiveValue::Set(replica_id), + ..Default::default() + } + .insert(&*tx) + .await?; + + // Assemble the buffer state + let id = buffer.id; let base_text = if buffer.epoch > 0 { buffer_snapshot::Entity::find() .filter( @@ -128,13 +172,44 @@ impl Database { }) } - Ok(Buffer { + Ok(ChannelBuffer { base_text, operations, }) }) .await } + + pub async fn get_buffer_collaborators(&self, buffer: BufferId) -> Result<()> { + todo!() + } + + pub async fn leave_buffer(&self, buffer: BufferId, user: UserId) -> Result<()> { + self.transaction(|tx| async move { + //TODO + // let tx = tx; + // let channel = channel::Entity::find_by_id(channel_id) + // .one(&*tx) + // .await? + // .ok_or_else(|| anyhow!("invalid channel"))?; + + // if let Some(id) = channel.main_buffer_id { + // return Ok(id); + // } else { + // let buffer = buffer::ActiveModel::new().insert(&*tx).await?; + // channel::ActiveModel { + // id: ActiveValue::Unchanged(channel_id), + // main_buffer_id: ActiveValue::Set(Some(buffer.id)), + // ..Default::default() + // } + // .update(&*tx) + // .await?; + // Ok(buffer.id) + // } + Ok(()) + }) + .await + } } mod storage { diff --git a/crates/collab/src/db/queries/channels.rs b/crates/collab/src/db/queries/channels.rs index 85a9304a2e..e3d3643a61 100644 --- a/crates/collab/src/db/queries/channels.rs +++ b/crates/collab/src/db/queries/channels.rs @@ -689,34 +689,6 @@ impl Database { }) .await } - - pub async fn get_or_create_buffer_for_channel( - &self, - channel_id: ChannelId, - ) -> Result { - self.transaction(|tx| async move { - let tx = tx; - let channel = channel::Entity::find_by_id(channel_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("invalid channel"))?; - - if let Some(id) = channel.main_buffer_id { - return Ok(id); - } else { - let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - channel::ActiveModel { - id: ActiveValue::Unchanged(channel_id), - main_buffer_id: ActiveValue::Set(Some(buffer.id)), - ..Default::default() - } - .update(&*tx) - .await?; - Ok(buffer.id) - } - }) - .await - } } #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fbf4bff2a6..fe747e0d27 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -3,6 +3,7 @@ pub mod buffer; pub mod buffer_operation; pub mod buffer_snapshot; pub mod channel; +pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs index 84e62cc071..f0187ad278 100644 --- a/crates/collab/src/db/tables/buffer.rs +++ b/crates/collab/src/db/tables/buffer.rs @@ -1,4 +1,4 @@ -use crate::db::BufferId; +use crate::db::{BufferId, ChannelId}; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] @@ -7,6 +7,7 @@ pub struct Model { #[sea_orm(primary_key)] pub id: BufferId, pub epoch: i32, + pub channel_id: ChannelId, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] @@ -15,6 +16,14 @@ pub enum Relation { Operations, #[sea_orm(has_many = "super::buffer_snapshot::Entity")] Snapshots, + #[sea_orm( + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" + )] + Channel, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + Collaborators, } impl Related for Entity { @@ -29,4 +38,16 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::Channel.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Collaborators.def() + } +} + impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 444d5fa6d9..7b33e3a1dd 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -7,7 +7,6 @@ pub struct Model { #[sea_orm(primary_key)] pub id: ChannelId, pub name: String, - pub main_buffer_id: Option, } impl ActiveModelBehavior for ActiveModel {} @@ -16,6 +15,8 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, + #[sea_orm(has_one = "super::room::Entity")] + Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, } @@ -31,3 +32,9 @@ impl Related for Entity { Relation::Room.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs new file mode 100644 index 0000000000..2e43e93e8e --- /dev/null +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -0,0 +1,42 @@ +use crate::db::{BufferId, ChannelBufferCollaboratorId, ReplicaId, ServerId, UserId}; +use rpc::ConnectionId; +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "channel_buffer_collaborators")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: ChannelBufferCollaboratorId, + pub buffer_id: BufferId, + pub connection_id: i32, + pub connection_server_id: ServerId, + pub user_id: UserId, + pub replica_id: ReplicaId, +} + +impl Model { + pub fn connection(&self) -> ConnectionId { + ConnectionId { + owner_id: self.connection_server_id.0 as u32, + id: self.connection_id as u32, + } + } +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::buffer::Entity", + from = "Column::BufferId", + to = "super::buffer::Column::Id" + )] + Buffer, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Buffer.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index f0e78e1fe4..bf7d7763e2 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -6,7 +6,60 @@ use text::Buffer; test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); async fn test_buffers(db: &Arc) { - let buffer_id = db.create_buffer().await.unwrap(); + // Prep database test info + let a_id = db + .create_user( + "user_a@example.com", + false, + NewUserParams { + github_login: "user_a".into(), + github_user_id: 101, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + let b_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + // This user will not be a part of the channel + let c_id = db + .create_user( + "user_b@example.com", + false, + NewUserParams { + github_login: "user_b".into(), + github_user_id: 102, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); + + db.invite_channel_member(zed_id, b_id, a_id, false) + .await + .unwrap(); + + db.respond_to_channel_invite(zed_id, b_id, true) + .await + .unwrap(); + + // TODO: Join buffer + let buffer_id = db.get_or_create_buffer_for_channel(zed_id); let mut buffer = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); @@ -23,7 +76,7 @@ async fn test_buffers(db: &Arc) { db.update_buffer(buffer_id, &operations).await.unwrap(); - let buffer_data = db.get_buffer(buffer_id).await.unwrap(); + let buffer_data = db.open_buffer(buffer_id).await.unwrap(); let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); buffer_2 diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 22eb23ce8e..6e62b90473 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,7 +2,10 @@ mod connection_pool; use crate::{ auth, - db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, + db::{ + self, BufferId, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, + UserId, + }, executor::Executor, AppState, Result, }; @@ -35,8 +38,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, GetChannelBufferResponse, - LiveKitConnectionInfo, RequestMessage, + self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, + OpenChannelBufferResponse, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -248,7 +251,9 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) - .add_request_handler(get_channel_buffer) + .add_request_handler(open_channel_buffer) + .add_request_handler(close_channel_buffer) + .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) .add_request_handler(join_channel) @@ -2479,9 +2484,9 @@ async fn join_channel( Ok(()) } -async fn get_channel_buffer( - request: proto::GetChannelBuffer, - response: Response, +async fn open_channel_buffer( + request: proto::OpenChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; @@ -2489,9 +2494,12 @@ async fn get_channel_buffer( let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; - let buffer = db.get_buffer(buffer_id).await?; + // TODO: join channel_buffer - response.send(GetChannelBufferResponse { + let buffer = db.open_buffer(buffer_id).await?; + + response.send(OpenChannelBufferResponse { + buffer_id: buffer_id.to_proto(), base_text: buffer.base_text, operations: buffer.operations, })?; @@ -2499,6 +2507,32 @@ async fn get_channel_buffer( Ok(()) } +async fn close_channel_buffer( + request: proto::CloseChannelBuffer, + response: Response, + session: Session, +) -> Result<()> { + let db = session.db().await; + let buffer_id = BufferId::from_proto(request.buffer_id); + + // TODO: close channel buffer here + // + response.send(Ack {})?; + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + + // TODO: Broadcast to buffer members + + Ok(()) +} + async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { let project_id = ProjectId::from_proto(request.project_id); let project_connection_ids = session diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index e7f662523e..c41f5de803 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -19,45 +19,39 @@ async fn test_channel_buffers( .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; - let a_document = - cx_a.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)); - let channel_buffer_a = a_document - .update(cx_a, |doc, cx| doc.buffer(cx)) + let channel_buffer_a = cx_a + .update(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)) .await .unwrap(); - edit_channel_buffer(&channel_buffer_a, cx_a, [(0..0, "hello world")]); - edit_channel_buffer(&channel_buffer_a, cx_a, [(5..5, ", cruel")]); - edit_channel_buffer(&channel_buffer_a, cx_a, [(0..5, "goodbye")]); - undo_channel_buffer(&channel_buffer_a, cx_a); + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - assert_eq!( - channel_buffer_text(&channel_buffer_a, cx_a), - "hello, cruel world" - ); + edit_channel_buffer(&buffer_a, cx_a, [(0..0, "hello world")]); + edit_channel_buffer(&buffer_a, cx_a, [(5..5, ", cruel")]); + edit_channel_buffer(&buffer_a, cx_a, [(0..5, "goodbye")]); + undo_channel_buffer(&buffer_a, cx_a); - let b_document = - cx_b.add_model(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)); - let channel_buffer_b = b_document - .update(cx_b, |doc, cx| doc.buffer(cx)) + assert_eq!(channel_buffer_text(&buffer_a, cx_a), "hello, cruel world"); + + let channel_buffer_b = cx_b + .update(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)) .await .unwrap(); - assert_eq!( - channel_buffer_text(&channel_buffer_b, cx_b), - "hello, cruel world" - ); + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); - edit_channel_buffer(&channel_buffer_b, cx_b, [(7..12, "beautiful")]); + assert_eq!(channel_buffer_text(&buffer_b, cx_b), "hello, cruel world"); + + edit_channel_buffer(&buffer_b, cx_b, [(7..12, "beautiful")]); deterministic.run_until_parked(); assert_eq!( - channel_buffer_text(&channel_buffer_a, cx_a), + channel_buffer_text(&buffer_a, cx_a), "hello, beautiful world" ); assert_eq!( - channel_buffer_text(&channel_buffer_b, cx_b), + channel_buffer_text(&buffer_b, cx_b), "hello, beautiful world" ); } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index baeaae1876..7fb22577f3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -143,8 +143,10 @@ message Envelope { SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; - GetChannelBuffer get_channel_buffer = 131; - GetChannelBufferResponse get_channel_buffer_response = 132; + OpenChannelBuffer open_channel_buffer = 131; + OpenChannelBufferResponse open_channel_buffer_response = 132; + UpdateChannelBuffer update_channel_buffer = 133; + CloseChannelBuffer close_channel_buffer = 134; } } @@ -543,6 +545,11 @@ message UpdateBuffer { repeated Operation operations = 3; } +message UpdateChannelBuffer { + uint64 buffer_id = 2; + repeated Operation operations = 3; +} + message UpdateBufferFile { uint64 project_id = 1; uint64 buffer_id = 2; @@ -951,13 +958,18 @@ message RenameChannel { string name = 2; } -message GetChannelBuffer { +message OpenChannelBuffer { uint64 channel_id = 1; } -message GetChannelBufferResponse { - string base_text = 1; - repeated Operation operations = 2; +message OpenChannelBufferResponse { + uint64 buffer_id = 1; + string base_text = 2; + repeated Operation operations = 3; +} + +message CloseChannelBuffer { + uint64 buffer_id = 1; } message RespondToChannelInvite { @@ -1156,7 +1168,6 @@ enum GitStatus { Conflict = 2; } - message BufferState { uint64 id = 1; optional File file = 2; diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 21a491b934..9d71140aa0 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -249,8 +249,10 @@ messages!( (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), (GetChannelMembersResponse, Foreground), - (GetChannelBuffer, Foreground), - (GetChannelBufferResponse, Foreground) + (OpenChannelBuffer, Foreground), + (OpenChannelBufferResponse, Foreground), + (CloseChannelBuffer, Background), + (UpdateChannelBuffer, Foreground) ); request_messages!( @@ -317,7 +319,8 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (GetChannelBuffer, GetChannelBufferResponse) + (OpenChannelBuffer, OpenChannelBufferResponse), + (CloseChannelBuffer, Ack) ); entity_messages!( @@ -373,6 +376,8 @@ entity_messages!( UpdateDiffBase ); +entity_messages!(buffer_id, UpdateChannelBuffer); + const KIB: usize = 1024; const MIB: usize = KIB * 1024; const MAX_BUFFER_LEN: usize = MIB; From 71611ee7a2ef52a785df74e79af2dd3ececdf706 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 09:47:49 -0700 Subject: [PATCH 026/142] Get join_buffer_for_channel compiling Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 23 +++++--- crates/collab/src/db/tables/channel.rs | 4 +- crates/collab/src/db/tests/buffer_tests.rs | 67 +++++++++++++++++----- crates/collab/src/db/tests/db_tests.rs | 29 ---------- crates/collab/src/rpc.rs | 14 ++--- crates/rpc/proto/zed.proto | 1 + 6 files changed, 73 insertions(+), 65 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index ba88e95fb8..3f86f897d8 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,11 +1,6 @@ use super::*; use prost::Message; -pub struct ChannelBuffer { - pub base_text: String, - pub operations: Vec, -} - impl Database { pub async fn update_buffer( &self, @@ -66,7 +61,7 @@ impl Database { channel_id: ChannelId, user_id: UserId, connection: ConnectionId, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -95,7 +90,7 @@ impl Database { }; // Join the collaborators - let collaborators = buffer + let mut collaborators = buffer .find_related(channel_buffer_collaborator::Entity) .all(&*tx) .await?; @@ -107,7 +102,7 @@ impl Database { while replica_ids.contains(&replica_id) { replica_id.0 += 1; } - channel_buffer_collaborator::ActiveModel { + let collaborator = channel_buffer_collaborator::ActiveModel { buffer_id: ActiveValue::Set(buffer.id), connection_id: ActiveValue::Set(connection.id as i32), connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), @@ -117,6 +112,7 @@ impl Database { } .insert(&*tx) .await?; + collaborators.push(collaborator); // Assemble the buffer state let id = buffer.id; @@ -172,9 +168,18 @@ impl Database { }) } - Ok(ChannelBuffer { + Ok(proto::OpenChannelBufferResponse { + buffer_id: buffer.id.to_proto(), base_text, operations, + collaborators: collaborators + .into_iter() + .map(|collaborator| proto::Collaborator { + peer_id: Some(collaborator.connection().into()), + user_id: collaborator.user_id.to_proto(), + replica_id: collaborator.replica_id.0 as u32, + }) + .collect(), }) }) .await diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 7b33e3a1dd..7f59e8d65f 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -1,4 +1,4 @@ -use crate::db::{BufferId, ChannelId}; +use crate::db::ChannelId; use sea_orm::entity::prelude::*; #[derive(Clone, Debug, Default, PartialEq, Eq, DeriveEntityModel)] @@ -15,7 +15,7 @@ impl ActiveModelBehavior for ActiveModel {} pub enum Relation { #[sea_orm(has_one = "super::room::Entity")] Room, - #[sea_orm(has_one = "super::room::Entity")] + #[sea_orm(has_one = "super::buffer::Entity")] Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index bf7d7763e2..fff9938573 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -3,9 +3,13 @@ use crate::test_both_dbs; use language::proto; use text::Buffer; -test_both_dbs!(test_buffers, test_buffers_postgres, test_buffers_sqlite); +test_both_dbs!( + test_channel_buffers, + test_channel_buffers_postgres, + test_channel_buffers_sqlite +); -async fn test_buffers(db: &Arc) { +async fn test_channel_buffers(db: &Arc) { // Prep database test info let a_id = db .create_user( @@ -48,6 +52,8 @@ async fn test_buffers(db: &Arc) { .unwrap() .user_id; + let owner_id = db.create_server("production").await.unwrap().0 as u32; + let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); db.invite_channel_member(zed_id, b_id, a_id, false) @@ -58,16 +64,19 @@ async fn test_buffers(db: &Arc) { .await .unwrap(); - // TODO: Join buffer - let buffer_id = db.get_or_create_buffer_for_channel(zed_id); + let buffer_response_a = db + .join_buffer_for_channel(zed_id, a_id, ConnectionId { owner_id, id: 1 }) + .await + .unwrap(); + let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); - let mut buffer = Buffer::new(0, 0, "".to_string()); + let mut buffer_a = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); - operations.push(buffer.edit([(0..0, "hello world")])); - operations.push(buffer.edit([(5..5, ", cruel")])); - operations.push(buffer.edit([(0..5, "goodbye")])); - operations.push(buffer.undo().unwrap().1); - assert_eq!(buffer.text(), "hello, cruel world"); + operations.push(buffer_a.edit([(0..0, "hello world")])); + operations.push(buffer_a.edit([(5..5, ", cruel")])); + operations.push(buffer_a.edit([(0..5, "goodbye")])); + operations.push(buffer_a.undo().unwrap().1); + assert_eq!(buffer_a.text(), "hello, cruel world"); let operations = operations .into_iter() @@ -76,11 +85,14 @@ async fn test_buffers(db: &Arc) { db.update_buffer(buffer_id, &operations).await.unwrap(); - let buffer_data = db.open_buffer(buffer_id).await.unwrap(); + let buffer_response_b = db + .join_buffer_for_channel(zed_id, b_id, ConnectionId { owner_id, id: 2 }) + .await + .unwrap(); - let mut buffer_2 = Buffer::new(0, 0, buffer_data.base_text); - buffer_2 - .apply_ops(buffer_data.operations.into_iter().map(|operation| { + let mut buffer_b = Buffer::new(0, 0, buffer_response_b.base_text); + buffer_b + .apply_ops(buffer_response_b.operations.into_iter().map(|operation| { let operation = proto::deserialize_operation(operation).unwrap(); if let language::Operation::Buffer(operation) = operation { operation @@ -90,5 +102,30 @@ async fn test_buffers(db: &Arc) { })) .unwrap(); - assert_eq!(buffer_2.text(), "hello, cruel world"); + assert_eq!(buffer_b.text(), "hello, cruel world"); + + // Ensure that C fails to open the buffer + assert!(db + .join_buffer_for_channel(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .await + .is_err()); + + //Ensure that both collaborators have shown up + assert_eq!( + buffer_response_b.collaborators, + &[ + rpc::proto::Collaborator { + user_id: a_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 1, owner_id }), + replica_id: 0, + }, + rpc::proto::Collaborator { + user_id: b_id.to_proto(), + peer_id: Some(rpc::proto::PeerId { id: 2, owner_id }), + replica_id: 1, + } + ] + ); + + // Leave buffer } diff --git a/crates/collab/src/db/tests/db_tests.rs b/crates/collab/src/db/tests/db_tests.rs index 0fffabc7c4..fc31ee7c4d 100644 --- a/crates/collab/src/db/tests/db_tests.rs +++ b/crates/collab/src/db/tests/db_tests.rs @@ -1329,35 +1329,6 @@ async fn test_channel_renames(db: &Arc) { assert!(bad_name_rename.is_err()) } -test_both_dbs!( - test_get_or_create_channel_buffer, - test_get_or_create_channel_buffer_postgres, - test_get_or_create_channel_buffer_sqlite -); - -async fn test_get_or_create_channel_buffer(db: &Arc) { - let a_id = db - .create_user( - "user1@example.com", - false, - NewUserParams { - github_login: "user1".into(), - github_user_id: 5, - invite_count: 0, - }, - ) - .await - .unwrap() - .user_id; - - let zed_id = db.create_root_channel("zed", "1", a_id).await.unwrap(); - - let first_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); - let second_buffer_id = db.get_or_create_buffer_for_channel(zed_id).await.unwrap(); - - assert_eq!(first_buffer_id, second_buffer_id); -} - #[gpui::test] async fn test_multiple_signup_overwrite() { let test_db = TestDb::postgres(build_background_executor()); diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 6e62b90473..95c6bdefc1 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2492,17 +2492,11 @@ async fn open_channel_buffer( let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); - let buffer_id = db.get_or_create_buffer_for_channel(channel_id).await?; + let open_response = db + .join_buffer_for_channel(channel_id, session.user_id, session.connection_id) + .await?; - // TODO: join channel_buffer - - let buffer = db.open_buffer(buffer_id).await?; - - response.send(OpenChannelBufferResponse { - buffer_id: buffer_id.to_proto(), - base_text: buffer.base_text, - operations: buffer.operations, - })?; + response.send(open_response)?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7fb22577f3..6f19132dc5 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -966,6 +966,7 @@ message OpenChannelBufferResponse { uint64 buffer_id = 1; string base_text = 2; repeated Operation operations = 3; + repeated Collaborator collaborators = 4; } message CloseChannelBuffer { From 95ea6647259d812da38c5b473600dc22ec602a8d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 11:02:13 -0700 Subject: [PATCH 027/142] WIP --- crates/channel/src/channel_buffer.rs | 2 +- .../20221109000000_test_schema.sql | 9 +- .../20230819154600_add_channel_buffers.sql | 9 +- crates/collab/src/db/queries/buffers.rs | 188 ++++++++++-------- crates/collab/src/db/queries/rooms.rs | 36 +++- crates/collab/src/db/tables/buffer.rs | 8 - crates/collab/src/db/tables/channel.rs | 8 + .../db/tables/channel_buffer_collaborator.rs | 17 +- crates/collab/src/db/tests/buffer_tests.rs | 28 ++- crates/collab/src/rpc.rs | 28 +-- crates/rpc/proto/zed.proto | 14 +- crates/rpc/src/proto.rs | 10 +- 12 files changed, 211 insertions(+), 146 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 372bd319a1..d88810ff56 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -36,7 +36,7 @@ impl ChannelBuffer { ) -> Task>> { cx.spawn(|mut cx| async move { let response = client - .request(proto::OpenChannelBuffer { channel_id }) + .request(proto::JoinChannelBuffer { channel_id }) .await?; let base_text = response.base_text; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 12ff2caec5..f39f0cca59 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -238,15 +238,16 @@ CREATE TABLE "buffer_snapshots" ( CREATE TABLE "channel_buffer_collaborators" ( "id" INTEGER PRIMARY KEY AUTOINCREMENT, - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "connection_id" INTEGER NOT NULL, "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT false, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "replica_id" INTEGER NOT NULL ); -CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index 8ccd7acadf..f6bd2879c6 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -27,15 +27,16 @@ CREATE TABLE "buffer_snapshots" ( CREATE TABLE "channel_buffer_collaborators" ( "id" SERIAL PRIMARY KEY, - "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, + "channel_id" INTEGER NOT NULL REFERENCES channels (id) ON DELETE CASCADE, "connection_id" INTEGER NOT NULL, "connection_server_id" INTEGER NOT NULL REFERENCES servers (id) ON DELETE CASCADE, + "connection_lost" BOOLEAN NOT NULL DEFAULT FALSE, "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, "replica_id" INTEGER NOT NULL ); -CREATE INDEX "index_channel_buffer_collaborators_on_buffer_id" ON "channel_buffer_collaborators" ("buffer_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_and_replica_id" ON "channel_buffer_collaborators" ("buffer_id", "replica_id"); +CREATE INDEX "index_channel_buffer_collaborators_on_channel_id" ON "channel_buffer_collaborators" ("channel_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replica_id" ON "channel_buffer_collaborators" ("channel_id", "replica_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); -CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_buffer_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("buffer_id", "connection_id", "connection_server_id"); +CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 3f86f897d8..473dd1afe9 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -2,66 +2,12 @@ use super::*; use prost::Message; impl Database { - pub async fn update_buffer( - &self, - buffer_id: BufferId, - operations: &[proto::Operation], - ) -> Result<()> { - self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(buffer_id) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such buffer"))?; - buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = - serialize_edit_operation(&operation.ranges, &operation.new_text); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = serialize_undo_operation(&operation.counts); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } - })) - .exec(&*tx) - .await?; - - Ok(()) - }) - .await - } - - pub async fn join_buffer_for_channel( + pub async fn join_channel_buffer( &self, channel_id: ChannelId, user_id: UserId, connection: ConnectionId, - ) -> Result { + ) -> Result { self.transaction(|tx| async move { let tx = tx; @@ -90,8 +36,8 @@ impl Database { }; // Join the collaborators - let mut collaborators = buffer - .find_related(channel_buffer_collaborator::Entity) + let mut collaborators = channel_buffer_collaborator::Entity::find() + .filter(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) .all(&*tx) .await?; let replica_ids = collaborators @@ -103,7 +49,7 @@ impl Database { replica_id.0 += 1; } let collaborator = channel_buffer_collaborator::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), + channel_id: ActiveValue::Set(channel_id), connection_id: ActiveValue::Set(connection.id as i32), connection_server_id: ActiveValue::Set(ServerId(connection.owner_id as i32)), user_id: ActiveValue::Set(user_id), @@ -168,7 +114,7 @@ impl Database { }) } - Ok(proto::OpenChannelBufferResponse { + Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), base_text, operations, @@ -185,32 +131,112 @@ impl Database { .await } - pub async fn get_buffer_collaborators(&self, buffer: BufferId) -> Result<()> { + pub async fn leave_channel_buffer( + &self, + channel_id: ChannelId, + connection: ConnectionId, + ) -> Result> { + self.transaction(|tx| async move { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + }) + .await + } + + pub async fn leave_channel_buffers( + &self, + connection: ConnectionId, + ) -> Result> { + // + } + + pub async fn get_channel_buffer_collaborators(&self, channel_id: ChannelId) -> Result<()> { todo!() } - pub async fn leave_buffer(&self, buffer: BufferId, user: UserId) -> Result<()> { + pub async fn update_channel_buffer( + &self, + buffer_id: BufferId, + operations: &[proto::Operation], + ) -> Result<()> { self.transaction(|tx| async move { - //TODO - // let tx = tx; - // let channel = channel::Entity::find_by_id(channel_id) - // .one(&*tx) - // .await? - // .ok_or_else(|| anyhow!("invalid channel"))?; + let buffer = buffer::Entity::find_by_id(buffer_id) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = + serialize_edit_operation(&operation.ranges, &operation.new_text); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = serialize_undo_operation(&operation.counts); + let version = serialize_version(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer_id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } + })) + .exec(&*tx) + .await?; - // if let Some(id) = channel.main_buffer_id { - // return Ok(id); - // } else { - // let buffer = buffer::ActiveModel::new().insert(&*tx).await?; - // channel::ActiveModel { - // id: ActiveValue::Unchanged(channel_id), - // main_buffer_id: ActiveValue::Set(Some(buffer.id)), - // ..Default::default() - // } - // .update(&*tx) - // .await?; - // Ok(buffer.id) - // } Ok(()) }) .await diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index ee79f2cb4f..a85d257187 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -903,15 +903,35 @@ impl Database { ), ) .one(&*tx) - .await? - .ok_or_else(|| anyhow!("not a participant in any room"))?; + .await?; - room_participant::Entity::update(room_participant::ActiveModel { - answering_connection_lost: ActiveValue::set(true), - ..participant.into_active_model() - }) - .exec(&*tx) - .await?; + if let Some(participant) = participant { + room_participant::Entity::update(room_participant::ActiveModel { + answering_connection_lost: ActiveValue::set(true), + ..participant.into_active_model() + }) + .exec(&*tx) + .await?; + } + + channel_buffer_collaborator::Entity::update_many() + .filter( + Condition::all() + .add( + channel_buffer_collaborator::Column::ConnectionId + .eq(connection.id as i32), + ) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .set(channel_buffer_collaborator::ActiveModel { + connection_lost: ActiveValue::set(true), + ..Default::default() + }) + .exec(&*tx) + .await?; Ok(()) }) diff --git a/crates/collab/src/db/tables/buffer.rs b/crates/collab/src/db/tables/buffer.rs index f0187ad278..ec2ffd4a68 100644 --- a/crates/collab/src/db/tables/buffer.rs +++ b/crates/collab/src/db/tables/buffer.rs @@ -22,8 +22,6 @@ pub enum Relation { to = "super::channel::Column::Id" )] Channel, - #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] - Collaborators, } impl Related for Entity { @@ -44,10 +42,4 @@ impl Related for Entity { } } -impl Related for Entity { - fn to() -> RelationDef { - Relation::Collaborators.def() - } -} - impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tables/channel.rs b/crates/collab/src/db/tables/channel.rs index 7f59e8d65f..05895ede4c 100644 --- a/crates/collab/src/db/tables/channel.rs +++ b/crates/collab/src/db/tables/channel.rs @@ -19,6 +19,8 @@ pub enum Relation { Buffer, #[sea_orm(has_many = "super::channel_member::Entity")] Member, + #[sea_orm(has_many = "super::channel_buffer_collaborator::Entity")] + BufferCollaborators, } impl Related for Entity { @@ -38,3 +40,9 @@ impl Related for Entity { Relation::Buffer.def() } } + +impl Related for Entity { + fn to() -> RelationDef { + Relation::BufferCollaborators.def() + } +} diff --git a/crates/collab/src/db/tables/channel_buffer_collaborator.rs b/crates/collab/src/db/tables/channel_buffer_collaborator.rs index 2e43e93e8e..ac2637b36e 100644 --- a/crates/collab/src/db/tables/channel_buffer_collaborator.rs +++ b/crates/collab/src/db/tables/channel_buffer_collaborator.rs @@ -1,4 +1,4 @@ -use crate::db::{BufferId, ChannelBufferCollaboratorId, ReplicaId, ServerId, UserId}; +use crate::db::{ChannelBufferCollaboratorId, ChannelId, ReplicaId, ServerId, UserId}; use rpc::ConnectionId; use sea_orm::entity::prelude::*; @@ -7,9 +7,10 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key)] pub id: ChannelBufferCollaboratorId, - pub buffer_id: BufferId, + pub channel_id: ChannelId, pub connection_id: i32, pub connection_server_id: ServerId, + pub connection_lost: bool, pub user_id: UserId, pub replica_id: ReplicaId, } @@ -26,16 +27,16 @@ impl Model { #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] pub enum Relation { #[sea_orm( - belongs_to = "super::buffer::Entity", - from = "Column::BufferId", - to = "super::buffer::Column::Id" + belongs_to = "super::channel::Entity", + from = "Column::ChannelId", + to = "super::channel::Column::Id" )] - Buffer, + Channel, } -impl Related for Entity { +impl Related for Entity { fn to() -> RelationDef { - Relation::Buffer.def() + Relation::Channel.def() } } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index fff9938573..c25071e1a2 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -37,13 +37,14 @@ async fn test_channel_buffers(db: &Arc) { .await .unwrap() .user_id; + // This user will not be a part of the channel let c_id = db .create_user( - "user_b@example.com", + "user_c@example.com", false, NewUserParams { - github_login: "user_b".into(), + github_login: "user_c".into(), github_user_id: 102, invite_count: 0, }, @@ -64,8 +65,9 @@ async fn test_channel_buffers(db: &Arc) { .await .unwrap(); + let connection_id_a = ConnectionId { owner_id, id: 1 }; let buffer_response_a = db - .join_buffer_for_channel(zed_id, a_id, ConnectionId { owner_id, id: 1 }) + .join_channel_buffer(zed_id, a_id, connection_id_a) .await .unwrap(); let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); @@ -83,10 +85,13 @@ async fn test_channel_buffers(db: &Arc) { .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) .collect::>(); - db.update_buffer(buffer_id, &operations).await.unwrap(); + db.update_channel_buffer(buffer_id, &operations) + .await + .unwrap(); + let connection_id_b = ConnectionId { owner_id, id: 2 }; let buffer_response_b = db - .join_buffer_for_channel(zed_id, b_id, ConnectionId { owner_id, id: 2 }) + .join_channel_buffer(zed_id, b_id, connection_id_b) .await .unwrap(); @@ -106,7 +111,7 @@ async fn test_channel_buffers(db: &Arc) { // Ensure that C fails to open the buffer assert!(db - .join_buffer_for_channel(zed_id, c_id, ConnectionId { owner_id, id: 3 }) + .join_channel_buffer(zed_id, c_id, ConnectionId { owner_id, id: 3 }) .await .is_err()); @@ -127,5 +132,14 @@ async fn test_channel_buffers(db: &Arc) { ] ); - // Leave buffer + let collaborators = db + .leave_channel_buffer(zed_id, connection_id_b) + .await + .unwrap(); + + assert_eq!(collaborators, &[connection_id_a],); + + db.connection_lost(connection_id_a).await.unwrap(); + // assert!() + // Test buffer epoch incrementing? } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 95c6bdefc1..da5e7e6398 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -39,7 +39,7 @@ use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - OpenChannelBufferResponse, RequestMessage, + RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -251,8 +251,8 @@ impl Server { .add_request_handler(remove_channel_member) .add_request_handler(set_channel_member_admin) .add_request_handler(rename_channel) - .add_request_handler(open_channel_buffer) - .add_request_handler(close_channel_buffer) + .add_request_handler(join_channel_buffer) + .add_request_handler(leave_channel_buffer) .add_message_handler(update_channel_buffer) .add_request_handler(get_channel_members) .add_request_handler(respond_to_channel_invite) @@ -2484,16 +2484,16 @@ async fn join_channel( Ok(()) } -async fn open_channel_buffer( - request: proto::OpenChannelBuffer, - response: Response, +async fn join_channel_buffer( + request: proto::JoinChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; let channel_id = ChannelId::from_proto(request.channel_id); let open_response = db - .join_buffer_for_channel(channel_id, session.user_id, session.connection_id) + .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; response.send(open_response)?; @@ -2501,16 +2501,18 @@ async fn open_channel_buffer( Ok(()) } -async fn close_channel_buffer( - request: proto::CloseChannelBuffer, - response: Response, +async fn leave_channel_buffer( + request: proto::LeaveChannelBuffer, + response: Response, session: Session, ) -> Result<()> { let db = session.db().await; - let buffer_id = BufferId::from_proto(request.buffer_id); + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators_to_notify = db + .leave_channel_buffer(channel_id, session.connection_id) + .await?; - // TODO: close channel buffer here - // response.send(Ack {})?; Ok(()) diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 6f19132dc5..88ad46abc7 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -143,10 +143,10 @@ message Envelope { SetChannelMemberAdmin set_channel_member_admin = 129; RenameChannel rename_channel = 130; - OpenChannelBuffer open_channel_buffer = 131; - OpenChannelBufferResponse open_channel_buffer_response = 132; + JoinChannelBuffer join_channel_buffer = 131; + JoinChannelBufferResponse join_channel_buffer_response = 132; UpdateChannelBuffer update_channel_buffer = 133; - CloseChannelBuffer close_channel_buffer = 134; + LeaveChannelBuffer leave_channel_buffer = 134; } } @@ -958,19 +958,19 @@ message RenameChannel { string name = 2; } -message OpenChannelBuffer { +message JoinChannelBuffer { uint64 channel_id = 1; } -message OpenChannelBufferResponse { +message JoinChannelBufferResponse { uint64 buffer_id = 1; string base_text = 2; repeated Operation operations = 3; repeated Collaborator collaborators = 4; } -message CloseChannelBuffer { - uint64 buffer_id = 1; +message LeaveChannelBuffer { + uint64 channel_id = 1; } message RespondToChannelInvite { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 9d71140aa0..68219d3ad8 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -249,9 +249,9 @@ messages!( (GetPrivateUserInfoResponse, Foreground), (GetChannelMembers, Foreground), (GetChannelMembersResponse, Foreground), - (OpenChannelBuffer, Foreground), - (OpenChannelBufferResponse, Foreground), - (CloseChannelBuffer, Background), + (JoinChannelBuffer, Foreground), + (JoinChannelBufferResponse, Foreground), + (LeaveChannelBuffer, Background), (UpdateChannelBuffer, Foreground) ); @@ -319,8 +319,8 @@ request_messages!( (UpdateParticipantLocation, Ack), (UpdateProject, Ack), (UpdateWorktree, Ack), - (OpenChannelBuffer, OpenChannelBufferResponse), - (CloseChannelBuffer, Ack) + (JoinChannelBuffer, JoinChannelBufferResponse), + (LeaveChannelBuffer, Ack) ); entity_messages!( From 5a0315c4d5016bcc59116219dce3a1f09c687ba9 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 22 Aug 2023 13:25:31 -0700 Subject: [PATCH 028/142] Achieve end to end channel buffer synchronization co-authored-by: max --- crates/channel/src/channel.rs | 7 + crates/channel/src/channel_buffer.rs | 91 ++++++++-- crates/collab/src/db/queries/buffers.rs | 167 +++++++++++++----- crates/collab/src/db/tests/buffer_tests.rs | 28 ++- crates/collab/src/rpc.rs | 105 +++++++++-- crates/collab/src/tests.rs | 1 + .../collab/src/tests/channel_buffer_tests.rs | 119 +++++++++---- crates/rpc/proto/zed.proto | 23 ++- crates/rpc/src/proto.rs | 11 +- crates/zed/src/main.rs | 1 + 10 files changed, 425 insertions(+), 128 deletions(-) diff --git a/crates/channel/src/channel.rs b/crates/channel/src/channel.rs index 67c560a1fc..15631b7dd3 100644 --- a/crates/channel/src/channel.rs +++ b/crates/channel/src/channel.rs @@ -1,7 +1,14 @@ mod channel_store; pub mod channel_buffer; +use std::sync::Arc; + pub use channel_store::*; +use client::Client; #[cfg(test)] mod channel_store_tests; + +pub fn init(client: &Arc) { + channel_buffer::init(client); +} diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index d88810ff56..a59fec1553 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -6,30 +6,34 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; -// Open the channel document -// ChannelDocumentView { ChannelDocument, Editor } -> On clone, clones internal ChannelDocument handle, instantiates new editor -// Produces a view which is: (ChannelDocument, Editor), ChannelDocument manages subscriptions -// ChannelDocuments -> Buffers -> Editor with that buffer - -// ChannelDocuments { -// ChannleBuffers: HashMap> -// } - -type BufferId = u64; +pub(crate) fn init(client: &Arc) { + client.add_model_message_handler(ChannelBuffer::handle_update_channel_buffer); + client.add_model_message_handler(ChannelBuffer::handle_add_channel_buffer_collaborator); + client.add_model_message_handler(ChannelBuffer::handle_remove_channel_buffer_collaborator); +} pub struct ChannelBuffer { channel_id: ChannelId, - buffer_id: BufferId, + collaborators: Vec, buffer: ModelHandle, client: Arc, + _subscription: client::Subscription, } impl Entity for ChannelBuffer { type Event = (); + + fn release(&mut self, _: &mut AppContext) { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel_id, + }) + .log_err(); + } } impl ChannelBuffer { - pub fn for_channel( + pub fn join_channel( channel_id: ChannelId, client: Arc, cx: &mut AppContext, @@ -45,19 +49,24 @@ impl ChannelBuffer { .into_iter() .map(language::proto::deserialize_operation) .collect::, _>>()?; - let buffer_id = response.buffer_id; - let buffer = cx.add_model(|cx| language::Buffer::new(0, base_text, cx)); + let collaborators = response.collaborators; + + let buffer = + cx.add_model(|cx| language::Buffer::new(response.replica_id as u16, base_text, cx)); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + let subscription = client.subscribe_to_entity(channel_id)?; + anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); - client.add_model_message_handler(Self::handle_update_channel_buffer); + Self { - buffer_id, buffer, client, channel_id, + collaborators, + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), } })) }) @@ -77,6 +86,7 @@ impl ChannelBuffer { .collect::, _>>()?; this.update(&mut cx, |this, cx| { + cx.notify(); this.buffer .update(cx, |buffer, cx| buffer.apply_ops(ops, cx)) })?; @@ -84,6 +94,49 @@ impl ChannelBuffer { Ok(()) } + async fn handle_add_channel_buffer_collaborator( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let collaborator = envelope.payload.collaborator.ok_or_else(|| { + anyhow::anyhow!( + "Should have gotten a collaborator in the AddChannelBufferCollaborator message" + ) + })?; + + this.update(&mut cx, |this, cx| { + this.collaborators.push(collaborator); + cx.notify(); + }); + + Ok(()) + } + + async fn handle_remove_channel_buffer_collaborator( + this: ModelHandle, + message: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + this.update(&mut cx, |this, cx| { + this.collaborators.retain(|collaborator| { + if collaborator.peer_id == message.payload.peer_id { + this.buffer.update(cx, |buffer, cx| { + buffer.remove_peer(collaborator.replica_id as u16, cx) + }); + false + } else { + true + } + }); + cx.notify(); + }); + + Ok(()) + } + fn on_buffer_update( &mut self, _: ModelHandle, @@ -94,7 +147,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - buffer_id: self.buffer_id, + channel_id: self.channel_id, operations: vec![operation], }) .log_err(); @@ -104,4 +157,8 @@ impl ChannelBuffer { pub fn buffer(&self) -> ModelHandle { self.buffer.clone() } + + pub fn collaborators(&self) -> &[proto::Collaborator] { + &self.collaborators + } } diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 473dd1afe9..7f0e5a75f0 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -11,7 +11,6 @@ impl Database { self.transaction(|tx| async move { let tx = tx; - // Get or create buffer from channel self.check_user_is_channel_member(channel_id, user_id, &tx) .await?; @@ -116,6 +115,7 @@ impl Database { Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), + replica_id: replica_id.to_proto() as u32, base_text, operations, collaborators: collaborators @@ -137,67 +137,128 @@ impl Database { connection: ConnectionId, ) -> Result> { self.transaction(|tx| async move { - let result = channel_buffer_collaborator::Entity::delete_many() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) - .add( - channel_buffer_collaborator::Column::ConnectionId - .eq(connection.id as i32), - ) - .add( - channel_buffer_collaborator::Column::ConnectionServerId - .eq(connection.owner_id as i32), - ), - ) - .exec(&*tx) - .await?; - if result.rows_affected == 0 { - Err(anyhow!("not a collaborator on this project"))?; - } - - let mut connections = Vec::new(); - let mut rows = channel_buffer_collaborator::Entity::find() - .filter( - Condition::all() - .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), - ) - .stream(&*tx) - .await?; - while let Some(row) = rows.next().await { - let row = row?; - connections.push(ConnectionId { - id: row.connection_id as u32, - owner_id: row.connection_server_id.0 as u32, - }); - } - - Ok(connections) + self.leave_channel_buffer_internal(channel_id, connection, &*tx) + .await }) .await } + pub async fn leave_channel_buffer_internal( + &self, + channel_id: ChannelId, + connection: ConnectionId, + tx: &DatabaseTransaction, + ) -> Result> { + let result = channel_buffer_collaborator::Entity::delete_many() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)) + .add(channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32)) + .add( + channel_buffer_collaborator::Column::ConnectionServerId + .eq(connection.owner_id as i32), + ), + ) + .exec(&*tx) + .await?; + if result.rows_affected == 0 { + Err(anyhow!("not a collaborator on this project"))?; + } + + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all().add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) + } + pub async fn leave_channel_buffers( &self, connection: ConnectionId, - ) -> Result> { - // + ) -> Result)>> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryChannelIds { + ChannelId, + } + + let channel_ids: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::ChannelId) + .filter(Condition::all().add( + channel_buffer_collaborator::Column::ConnectionId.eq(connection.id as i32), + )) + .into_values::<_, QueryChannelIds>() + .all(&*tx) + .await?; + + let mut result = Vec::new(); + for channel_id in channel_ids { + let collaborators = self + .leave_channel_buffer_internal(channel_id, connection, &*tx) + .await?; + result.push((channel_id, collaborators)); + } + + Ok(result) + }) + .await } - pub async fn get_channel_buffer_collaborators(&self, channel_id: ChannelId) -> Result<()> { - todo!() + #[cfg(debug_assertions)] + pub async fn get_channel_buffer_collaborators( + &self, + channel_id: ChannelId, + ) -> Result> { + self.transaction(|tx| async move { + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryUserIds { + UserId, + } + + let users: Vec = channel_buffer_collaborator::Entity::find() + .select_only() + .column(channel_buffer_collaborator::Column::UserId) + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .into_values::<_, QueryUserIds>() + .all(&*tx) + .await?; + + Ok(users) + }) + .await } pub async fn update_channel_buffer( &self, - buffer_id: BufferId, + channel_id: ChannelId, + user: UserId, operations: &[proto::Operation], - ) -> Result<()> { + ) -> Result> { self.transaction(|tx| async move { - let buffer = buffer::Entity::find_by_id(buffer_id) + self.check_user_is_channel_member(channel_id, user, &*tx) + .await?; + + let buffer = buffer::Entity::find() + .filter(buffer::Column::ChannelId.eq(channel_id)) .one(&*tx) .await? .ok_or_else(|| anyhow!("no such buffer"))?; + let buffer_id = buffer.id; buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { match operation.variant.as_ref()? { proto::operation::Variant::Edit(operation) => { @@ -237,7 +298,23 @@ impl Database { .exec(&*tx) .await?; - Ok(()) + let mut connections = Vec::new(); + let mut rows = channel_buffer_collaborator::Entity::find() + .filter( + Condition::all() + .add(channel_buffer_collaborator::Column::ChannelId.eq(channel_id)), + ) + .stream(&*tx) + .await?; + while let Some(row) = rows.next().await { + let row = row?; + connections.push(ConnectionId { + id: row.connection_id as u32, + owner_id: row.connection_server_id.0 as u32, + }); + } + + Ok(connections) }) .await } diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index c25071e1a2..08252e382e 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -66,11 +66,10 @@ async fn test_channel_buffers(db: &Arc) { .unwrap(); let connection_id_a = ConnectionId { owner_id, id: 1 }; - let buffer_response_a = db + let _ = db .join_channel_buffer(zed_id, a_id, connection_id_a) .await .unwrap(); - let buffer_id = BufferId::from_proto(buffer_response_a.buffer_id); let mut buffer_a = Buffer::new(0, 0, "".to_string()); let mut operations = Vec::new(); @@ -85,7 +84,7 @@ async fn test_channel_buffers(db: &Arc) { .map(|op| proto::serialize_operation(&language::Operation::Buffer(op))) .collect::>(); - db.update_channel_buffer(buffer_id, &operations) + db.update_channel_buffer(zed_id, a_id, &operations) .await .unwrap(); @@ -115,7 +114,7 @@ async fn test_channel_buffers(db: &Arc) { .await .is_err()); - //Ensure that both collaborators have shown up + // Ensure that both collaborators have shown up assert_eq!( buffer_response_b.collaborators, &[ @@ -132,6 +131,10 @@ async fn test_channel_buffers(db: &Arc) { ] ); + // Ensure that get_channel_buffer_collaborators works + let zed_collaborats = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + assert_eq!(zed_collaborats, &[a_id, b_id]); + let collaborators = db .leave_channel_buffer(zed_id, connection_id_b) .await @@ -139,7 +142,18 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(collaborators, &[connection_id_a],); - db.connection_lost(connection_id_a).await.unwrap(); - // assert!() - // Test buffer epoch incrementing? + let cargo_id = db.create_root_channel("cargo", "2", a_id).await.unwrap(); + let _ = db + .join_channel_buffer(cargo_id, a_id, connection_id_a) + .await + .unwrap(); + + db.leave_channel_buffers(connection_id_a).await.unwrap(); + + let zed_collaborators = db.get_channel_buffer_collaborators(zed_id).await.unwrap(); + let cargo_collaborators = db.get_channel_buffer_collaborators(cargo_id).await.unwrap(); + assert_eq!(zed_collaborators, &[]); + assert_eq!(cargo_collaborators, &[]); + + // TODO: test buffer epoch incrementing } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index da5e7e6398..2bd39c861d 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2,10 +2,7 @@ mod connection_pool; use crate::{ auth, - db::{ - self, BufferId, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, - UserId, - }, + db::{self, ChannelId, ChannelsForUser, Database, ProjectId, RoomId, ServerId, User, UserId}, executor::Executor, AppState, Result, }; @@ -38,8 +35,8 @@ use lazy_static::lazy_static; use prometheus::{register_int_gauge, IntGauge}; use rpc::{ proto::{ - self, Ack, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, LiveKitConnectionInfo, - RequestMessage, + self, Ack, AddChannelBufferCollaborator, AnyTypedEnvelope, EntityMessage, EnvelopedMessage, + LiveKitConnectionInfo, RequestMessage, }, Connection, ConnectionId, Peer, Receipt, TypedEnvelope, }; @@ -860,6 +857,7 @@ async fn connection_lost( futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); + leave_channel_buffers_for_session(&session).await.trace_err(); if !session .connection_pool() @@ -872,6 +870,8 @@ async fn connection_lost( } } update_user_contacts(session.user_id, &session).await?; + + } _ = teardown.changed().fuse() => {} } @@ -2496,8 +2496,51 @@ async fn join_channel_buffer( .join_channel_buffer(channel_id, session.user_id, session.connection_id) .await?; + let replica_id = open_response.replica_id; + let collaborators = open_response.collaborators.clone(); + response.send(open_response)?; + let update = AddChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + collaborator: Some(proto::Collaborator { + user_id: session.user_id.to_proto(), + peer_id: Some(session.connection_id.into()), + replica_id, + }), + }; + channel_buffer_updated( + session.connection_id, + collaborators + .iter() + .filter_map(|collaborator| Some(collaborator.peer_id?.into())), + &update, + &session.peer, + ); + + Ok(()) +} + +async fn update_channel_buffer( + request: proto::UpdateChannelBuffer, + session: Session, +) -> Result<()> { + let db = session.db().await; + let channel_id = ChannelId::from_proto(request.channel_id); + + let collaborators = db + .update_channel_buffer(channel_id, session.user_id, &request.operations) + .await?; + + channel_buffer_updated( + session.connection_id, + collaborators, + &proto::UpdateChannelBuffer { + channel_id: channel_id.to_proto(), + operations: request.operations, + }, + &session.peer, + ); Ok(()) } @@ -2515,18 +2558,28 @@ async fn leave_channel_buffer( response.send(Ack {})?; + channel_buffer_updated( + session.connection_id, + collaborators_to_notify, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + Ok(()) } -async fn update_channel_buffer( - request: proto::UpdateChannelBuffer, - session: Session, -) -> Result<()> { - let db = session.db().await; - - // TODO: Broadcast to buffer members - - Ok(()) +fn channel_buffer_updated( + sender_id: ConnectionId, + collaborators: impl IntoIterator, + message: &T, + peer: &Peer, +) { + broadcast(Some(sender_id), collaborators.into_iter(), |peer_id| { + peer.send(peer_id.into(), message.clone()) + }); } async fn update_diff_base(request: proto::UpdateDiffBase, session: Session) -> Result<()> { @@ -2854,6 +2907,28 @@ async fn leave_room_for_session(session: &Session) -> Result<()> { Ok(()) } +async fn leave_channel_buffers_for_session(session: &Session) -> Result<()> { + let left_channel_buffers = session + .db() + .await + .leave_channel_buffers(session.connection_id) + .await?; + + for (channel_id, connections) in left_channel_buffers { + channel_buffer_updated( + session.connection_id, + connections, + &proto::RemoveChannelBufferCollaborator { + channel_id: channel_id.to_proto(), + peer_id: Some(session.connection_id.into()), + }, + &session.peer, + ); + } + + Ok(()) +} + fn project_left(project: &db::LeftProject, session: &Session) { for connection_id in &project.connection_ids { if project.host_user_id == session.user_id { diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 831bccbb72..25f059c0aa 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -211,6 +211,7 @@ impl TestServer { workspace::init(app_state.clone(), cx); audio::init((), cx); call::init(client.clone(), user_store.clone(), cx); + channel::init(&client); }); client diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index c41f5de803..d9880496f6 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,11 +1,13 @@ -use crate::tests::TestServer; +use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use channel::channel_buffer::ChannelBuffer; +use client::UserId; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; -use std::{ops::Range, sync::Arc}; +use rpc::{proto, RECEIVE_TIMEOUT}; +use std::sync::Arc; #[gpui::test] -async fn test_channel_buffers( +async fn test_core_channel_buffers( deterministic: Arc, cx_a: &mut TestAppContext, cx_b: &mut TestAppContext, @@ -19,60 +21,103 @@ async fn test_channel_buffers( .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) .await; + // Client A joins the channel buffer let channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::for_channel(zed_id, client_a.client().to_owned(), cx)) + .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) .await .unwrap(); + // Client A edits the buffer let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); - edit_channel_buffer(&buffer_a, cx_a, [(0..0, "hello world")]); - edit_channel_buffer(&buffer_a, cx_a, [(5..5, ", cruel")]); - edit_channel_buffer(&buffer_a, cx_a, [(0..5, "goodbye")]); - undo_channel_buffer(&buffer_a, cx_a); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + deterministic.run_until_parked(); - assert_eq!(channel_buffer_text(&buffer_a, cx_a), "hello, cruel world"); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + // Client B joins the channel buffer let channel_buffer_b = cx_b - .update(|cx| ChannelBuffer::for_channel(zed_id, client_b.client().to_owned(), cx)) + .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx)) .await .unwrap(); + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); - assert_eq!(channel_buffer_text(&buffer_b, cx_b), "hello, cruel world"); - - edit_channel_buffer(&buffer_b, cx_b, [(7..12, "beautiful")]); + // Both A and B see the new edit + deterministic.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); deterministic.run_until_parked(); + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = cx_a + .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_b.user_id(), client_a.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer? +} + +#[track_caller] +fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( - channel_buffer_text(&buffer_a, cx_a), - "hello, beautiful world" - ); - assert_eq!( - channel_buffer_text(&buffer_b, cx_b), - "hello, beautiful world" + collaborators + .into_iter() + .map(|collaborator| collaborator.user_id) + .collect::>(), + ids.into_iter().map(|id| id.unwrap()).collect::>() ); } -fn edit_channel_buffer( - channel_buffer: &ModelHandle, - cx: &mut TestAppContext, - edits: I, -) where - I: IntoIterator, &'static str)>, -{ - channel_buffer.update(cx, |buffer, cx| buffer.edit(edits, None, cx)); -} - -fn undo_channel_buffer(channel_buffer: &ModelHandle, cx: &mut TestAppContext) { - channel_buffer.update(cx, |buffer, cx| buffer.undo(cx)); -} - -fn channel_buffer_text( - channel_buffer: &ModelHandle, - cx: &mut TestAppContext, -) -> String { +fn buffer_text(channel_buffer: &ModelHandle, cx: &mut TestAppContext) -> String { channel_buffer.read_with(cx, |buffer, _| buffer.text()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 88ad46abc7..b97feff06b 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -147,6 +147,8 @@ message Envelope { JoinChannelBufferResponse join_channel_buffer_response = 132; UpdateChannelBuffer update_channel_buffer = 133; LeaveChannelBuffer leave_channel_buffer = 134; + AddChannelBufferCollaborator add_channel_buffer_collaborator = 135; + RemoveChannelBufferCollaborator remove_channel_buffer_collaborator = 136; } } @@ -416,6 +418,16 @@ message RemoveProjectCollaborator { PeerId peer_id = 2; } +message AddChannelBufferCollaborator { + uint64 channel_id = 1; + Collaborator collaborator = 2; +} + +message RemoveChannelBufferCollaborator { + uint64 channel_id = 1; + PeerId peer_id = 2; +} + message GetDefinition { uint64 project_id = 1; uint64 buffer_id = 2; @@ -546,8 +558,8 @@ message UpdateBuffer { } message UpdateChannelBuffer { - uint64 buffer_id = 2; - repeated Operation operations = 3; + uint64 channel_id = 1; + repeated Operation operations = 2; } message UpdateBufferFile { @@ -964,9 +976,10 @@ message JoinChannelBuffer { message JoinChannelBufferResponse { uint64 buffer_id = 1; - string base_text = 2; - repeated Operation operations = 3; - repeated Collaborator collaborators = 4; + uint32 replica_id = 2; + string base_text = 3; + repeated Operation operations = 4; + repeated Collaborator collaborators = 5; } message LeaveChannelBuffer { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 68219d3ad8..f0f49c6230 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -252,7 +252,9 @@ messages!( (JoinChannelBuffer, Foreground), (JoinChannelBufferResponse, Foreground), (LeaveChannelBuffer, Background), - (UpdateChannelBuffer, Foreground) + (UpdateChannelBuffer, Foreground), + (RemoveChannelBufferCollaborator, Foreground), + (AddChannelBufferCollaborator, Foreground), ); request_messages!( @@ -376,7 +378,12 @@ entity_messages!( UpdateDiffBase ); -entity_messages!(buffer_id, UpdateChannelBuffer); +entity_messages!( + channel_id, + UpdateChannelBuffer, + RemoveChannelBufferCollaborator, + AddChannelBufferCollaborator +); const KIB: usize = 1024; const MIB: usize = KIB * 1024; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index b905c1d37b..3b1fccb927 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -158,6 +158,7 @@ fn main() { outline::init(cx); project_symbols::init(cx); project_panel::init(Assets, cx); + channel::init(&client); diagnostics::init(cx); search::init(cx); semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); From 4eff8ad18692a505f22e552422f443cfd583d012 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 22 Aug 2023 14:18:32 -0700 Subject: [PATCH 029/142] Add channel notes view co-authored-by: Max --- Cargo.lock | 1 + crates/channel/src/channel_buffer.rs | 14 +++- crates/channel/src/channel_store.rs | 10 +++ .../collab/src/tests/channel_buffer_tests.rs | 19 ++--- crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/channel_view.rs | 69 +++++++++++++++++++ crates/collab_ui/src/collab_panel.rs | 46 ++++++++++++- crates/collab_ui/src/collab_ui.rs | 1 + crates/gpui/src/app.rs | 3 +- 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 crates/collab_ui/src/channel_view.rs diff --git a/Cargo.lock b/Cargo.lock index a40aa7d89c..deed9a176e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1529,6 +1529,7 @@ dependencies = [ "futures 0.3.28", "fuzzy", "gpui", + "language", "log", "menu", "picker", diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index a59fec1553..aa99d5c10b 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::ChannelId; +use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::Client; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; @@ -16,6 +16,7 @@ pub struct ChannelBuffer { channel_id: ChannelId, collaborators: Vec, buffer: ModelHandle, + channel_store: ModelHandle, client: Arc, _subscription: client::Subscription, } @@ -33,7 +34,8 @@ impl Entity for ChannelBuffer { } impl ChannelBuffer { - pub fn join_channel( + pub(crate) fn new( + channel_store: ModelHandle, channel_id: ChannelId, client: Arc, cx: &mut AppContext, @@ -65,6 +67,7 @@ impl ChannelBuffer { buffer, client, channel_id, + channel_store, collaborators, _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), } @@ -161,4 +164,11 @@ impl ChannelBuffer { pub fn collaborators(&self) -> &[proto::Collaborator] { &self.collaborators } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_store + .read(cx) + .channel_for_id(self.channel_id) + .cloned() + } } diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index b9b2c98acd..a6aad19d03 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -13,6 +13,8 @@ use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; +use crate::channel_buffer::ChannelBuffer; + pub type ChannelId = u64; pub struct ChannelStore { @@ -151,6 +153,14 @@ impl ChannelStore { self.channels_by_id.get(&channel_id) } + pub fn open_channel_buffer( + &self, + channel_id: ChannelId, + cx: &mut ModelContext, + ) -> Task>> { + ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx) + } + pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { self.channel_paths.iter().any(|path| { if let Some(ix) = path.iter().position(|id| *id == channel_id) { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index d9880496f6..db98c6abdc 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,6 +1,5 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; -use channel::channel_buffer::ChannelBuffer; use client::UserId; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; @@ -22,8 +21,9 @@ async fn test_core_channel_buffers( .await; // Client A joins the channel buffer - let channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -45,8 +45,9 @@ async fn test_core_channel_buffers( assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); // Client B joins the channel buffer - let channel_buffer_b = cx_b - .update(|cx| ChannelBuffer::join_channel(zed_id, client_b.client().to_owned(), cx)) + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| channel.open_channel_buffer(zed_id, cx)) .await .unwrap(); @@ -79,8 +80,9 @@ async fn test_core_channel_buffers( }); // Client A rejoins the channel buffer - let _channel_buffer_a = cx_a - .update(|cx| ChannelBuffer::join_channel(zed_id, client_a.client().to_owned(), cx)) + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx)) .await .unwrap(); deterministic.run_until_parked(); @@ -104,7 +106,8 @@ async fn test_core_channel_buffers( }); // TODO: - // - Test synchronizing offline updates, what happens to A's channel buffer? + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open } #[track_caller] diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index e0177f6609..1ecb4b8422 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -34,6 +34,7 @@ editor = { path = "../editor" } feedback = { path = "../feedback" } fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } +language = { path = "../language" } menu = { path = "../menu" } picker = { path = "../picker" } project = { path = "../project" } diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs new file mode 100644 index 0000000000..27a2d678f5 --- /dev/null +++ b/crates/collab_ui/src/channel_view.rs @@ -0,0 +1,69 @@ +use channel::channel_buffer::ChannelBuffer; +use editor::Editor; +use gpui::{ + actions, + elements::{ChildView, Label}, + AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, +}; +use language::Language; +use std::sync::Arc; +use workspace::item::{Item, ItemHandle}; + +actions!(channel_view, [Deploy]); + +pub(crate) fn init(cx: &mut AppContext) { + // TODO +} + +pub struct ChannelView { + editor: ViewHandle, + channel_buffer: ModelHandle, +} + +impl ChannelView { + pub fn new( + channel_buffer: ModelHandle, + language: Arc, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx)); + let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + Self { + editor, + channel_buffer, + } + } +} + +impl Entity for ChannelView { + type Event = editor::Event; +} + +impl View for ChannelView { + fn ui_name() -> &'static str { + "ChannelView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + ChildView::new(self.editor.as_any(), cx).into_any() + } +} + +impl Item for ChannelView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + cx: &gpui::AppContext, + ) -> AnyElement { + let channel_name = self + .channel_buffer + .read(cx) + .channel(cx) + .map_or("[Deleted channel]".to_string(), |channel| { + format!("#{}", channel.name) + }); + Label::new(channel_name, style.label.to_owned()).into_any() + } +} diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index ab692dd166..0eb6a65984 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -42,7 +42,10 @@ use workspace::{ Workspace, }; -use crate::face_pile::FacePile; +use crate::{ + channel_view::{self, ChannelView}, + face_pile::FacePile, +}; use channel_modal::ChannelModal; use self::contact_finder::ContactFinder; @@ -77,6 +80,11 @@ struct RenameChannel { channel_id: u64, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +struct OpenChannelBuffer { + channel_id: u64, +} + actions!( collab_panel, [ @@ -96,7 +104,8 @@ impl_actions!( InviteMembers, ManageMembers, RenameChannel, - ToggleCollapse + ToggleCollapse, + OpenChannelBuffer ] ); @@ -106,6 +115,7 @@ pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); contact_finder::init(cx); channel_modal::init(cx); + channel_view::init(cx); cx.add_action(CollabPanel::cancel); cx.add_action(CollabPanel::select_next); @@ -121,7 +131,8 @@ pub fn init(_client: Arc, cx: &mut AppContext) { cx.add_action(CollabPanel::rename_channel); cx.add_action(CollabPanel::toggle_channel_collapsed); cx.add_action(CollabPanel::collapse_selected_channel); - cx.add_action(CollabPanel::expand_selected_channel) + cx.add_action(CollabPanel::expand_selected_channel); + cx.add_action(CollabPanel::open_channel_buffer); } #[derive(Debug)] @@ -1888,6 +1899,7 @@ impl CollabPanel { vec![ ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), ContextMenuItem::Separator, ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), ContextMenuItem::Separator, @@ -2207,6 +2219,34 @@ impl CollabPanel { } } + fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { + let workspace = self.workspace; + let open = self.channel_store.update(cx, |channel_store, cx| { + channel_store.open_channel_buffer(action.channel_id, cx) + }); + + cx.spawn(|_, mut cx| async move { + let channel_buffer = open.await?; + + let markdown = workspace + .read_with(&cx, |workspace, _| { + workspace + .app_state() + .languages + .language_for_name("Markdown") + })? + .await?; + + workspace.update(&mut cx, |workspace, cx| { + let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx)); + workspace.add_item(Box::new(channel_view), cx); + })?; + + anyhow::Ok(()) + }) + .detach(); + } + fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { let Some(channel) = self.selected_channel() else { return; diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 5420dd1db5..04644b62d9 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,3 +1,4 @@ +pub mod channel_view; pub mod collab_panel; mod collab_titlebar_item; mod contact_notification; diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 03625c80e7..890bd55a7f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4687,12 +4687,13 @@ impl AnyWeakModelHandle { } } -#[derive(Copy)] pub struct WeakViewHandle { any_handle: AnyWeakViewHandle, view_type: PhantomData, } +impl Copy for WeakViewHandle {} + impl Debug for WeakViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("WeakViewHandle<{}>", type_name::())) From 1d08f44e702024e26753388c538b93d5fafcd8fd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 15:33:37 -0700 Subject: [PATCH 030/142] Snapshot channel notes buffers when everyone leaves Co-authored-by: Mikayla --- Cargo.lock | 1 + crates/collab/Cargo.toml | 1 + .../20221109000000_test_schema.sql | 1 + .../20230819154600_add_channel_buffers.sql | 1 + crates/collab/src/db/queries/buffers.rs | 352 +++++++++++++----- .../collab/src/db/tables/buffer_snapshot.rs | 1 + crates/collab/src/db/tests/buffer_tests.rs | 10 +- crates/language/src/proto.rs | 1 + crates/text/src/text.rs | 2 +- 9 files changed, 273 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index deed9a176e..0ec2f34185 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1458,6 +1458,7 @@ dependencies = [ "channel", "clap 3.2.25", "client", + "clock", "collections", "ctor", "dashmap", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fc78a03f67..cc1970266d 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -14,6 +14,7 @@ name = "seed" required-features = ["seed-support"] [dependencies] +clock = { path = "../clock" } collections = { path = "../collections" } live_kit_server = { path = "../live_kit_server" } text = { path = "../text" } diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index f39f0cca59..fdae4f2339 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -233,6 +233,7 @@ CREATE TABLE "buffer_snapshots" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, PRIMARY KEY(buffer_id, epoch) ); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index f6bd2879c6..fec18ddb8d 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -22,6 +22,7 @@ CREATE TABLE "buffer_snapshots" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "text" TEXT NOT NULL, + "operation_serialization_version" INTEGER NOT NULL, PRIMARY KEY(buffer_id, epoch) ); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 7f0e5a75f0..b0df905ecb 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,5 +1,7 @@ use super::*; use prost::Message; +use std::ops::Range; +use text::{EditOperation, InsertionTimestamp, UndoOperation}; impl Database { pub async fn join_channel_buffer( @@ -31,6 +33,16 @@ impl Database { } .insert(&*tx) .await?; + buffer_snapshot::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(0), + text: ActiveValue::Set(String::new()), + operation_serialization_version: ActiveValue::Set( + storage::SERIALIZATION_VERSION, + ), + } + .insert(&*tx) + .await?; buffer }; @@ -60,58 +72,7 @@ impl Database { collaborators.push(collaborator); // Assemble the buffer state - let id = buffer.id; - let base_text = if buffer.epoch > 0 { - buffer_snapshot::Entity::find() - .filter( - buffer_snapshot::Column::BufferId - .eq(id) - .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), - ) - .one(&*tx) - .await? - .ok_or_else(|| anyhow!("no such snapshot"))? - .text - } else { - String::new() - }; - - let mut rows = buffer_operation::Entity::find() - .filter( - buffer_operation::Column::BufferId - .eq(id) - .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), - ) - .stream(&*tx) - .await?; - let mut operations = Vec::new(); - while let Some(row) = rows.next().await { - let row = row?; - let version = deserialize_version(&row.version)?; - let operation = if row.is_undo { - let counts = deserialize_undo_operation(&row.value)?; - proto::operation::Variant::Undo(proto::operation::Undo { - replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - counts, - }) - } else { - let (ranges, new_text) = deserialize_edit_operation(&row.value)?; - proto::operation::Variant::Edit(proto::operation::Edit { - replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, - lamport_timestamp: row.lamport_timestamp as u32, - version, - ranges, - new_text, - }) - }; - operations.push(proto::Operation { - variant: Some(operation), - }) - } + let (base_text, operations) = self.get_buffer_state(&buffer, &tx).await?; Ok(proto::JoinChannelBufferResponse { buffer_id: buffer.id.to_proto(), @@ -180,6 +141,12 @@ impl Database { }); } + drop(rows); + + if connections.is_empty() { + self.snapshot_buffer(channel_id, &tx).await?; + } + Ok(connections) } @@ -258,42 +225,23 @@ impl Database { .one(&*tx) .await? .ok_or_else(|| anyhow!("no such buffer"))?; - let buffer_id = buffer.id; + + #[derive(Debug, Clone, Copy, EnumIter, DeriveColumn)] + enum QueryVersion { + OperationSerializationVersion, + } + + let serialization_version: i32 = buffer + .find_related(buffer_snapshot::Entity) + .select_only() + .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) + .into_values::<_, QueryVersion>() + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("missing buffer snapshot"))?; + buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = - serialize_edit_operation(&operation.ranges, &operation.new_text); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = serialize_undo_operation(&operation.counts); - let version = serialize_version(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer_id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } + operation_to_storage(operation, &buffer, serialization_version) })) .exec(&*tx) .await?; @@ -318,6 +266,222 @@ impl Database { }) .await } + + async fn get_buffer_state( + &self, + buffer: &buffer::Model, + tx: &DatabaseTransaction, + ) -> Result<(String, Vec)> { + let id = buffer.id; + let (base_text, version) = if buffer.epoch > 0 { + let snapshot = buffer_snapshot::Entity::find() + .filter( + buffer_snapshot::Column::BufferId + .eq(id) + .and(buffer_snapshot::Column::Epoch.eq(buffer.epoch)), + ) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such snapshot"))?; + + let version = snapshot.operation_serialization_version; + (snapshot.text, version) + } else { + (String::new(), storage::SERIALIZATION_VERSION) + }; + + let mut rows = buffer_operation::Entity::find() + .filter( + buffer_operation::Column::BufferId + .eq(id) + .and(buffer_operation::Column::Epoch.eq(buffer.epoch)), + ) + .stream(&*tx) + .await?; + let mut operations = Vec::new(); + while let Some(row) = rows.next().await { + let row = row?; + + let operation = operation_from_storage(row, version)?; + operations.push(proto::Operation { + variant: Some(operation), + }) + } + + Ok((base_text, operations)) + } + + async fn snapshot_buffer(&self, channel_id: ChannelId, tx: &DatabaseTransaction) -> Result<()> { + let buffer = channel::Model { + id: channel_id, + ..Default::default() + } + .find_related(buffer::Entity) + .one(&*tx) + .await? + .ok_or_else(|| anyhow!("no such buffer"))?; + + let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + + let mut text_buffer = text::Buffer::new(0, 0, base_text); + + text_buffer + .apply_ops( + operations + .into_iter() + .filter_map(deserialize_wire_operation), + ) + .unwrap(); + + let base_text = text_buffer.text(); + let epoch = buffer.epoch + 1; + + buffer_snapshot::Model { + buffer_id: buffer.id, + epoch, + text: base_text, + operation_serialization_version: storage::SERIALIZATION_VERSION, + } + .into_active_model() + .insert(tx) + .await?; + + buffer::ActiveModel { + id: ActiveValue::Unchanged(buffer.id), + epoch: ActiveValue::Set(epoch), + ..Default::default() + } + .save(tx) + .await?; + + Ok(()) + } +} + +fn operation_to_storage( + operation: &proto::Operation, + buffer: &buffer::Model, + _format: i32, +) -> Option { + match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => { + let value = edit_operation_to_storage(&operation.ranges, &operation.new_text); + let version = version_to_storage(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(false), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::Undo(operation) => { + let value = undo_operation_to_storage(&operation.counts); + let version = version_to_storage(&operation.version); + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(operation.replica_id as i32), + lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), + local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), + is_undo: ActiveValue::Set(true), + version: ActiveValue::Set(version), + value: ActiveValue::Set(value), + }) + } + proto::operation::Variant::UpdateSelections(_) => None, + proto::operation::Variant::UpdateDiagnostics(_) => None, + proto::operation::Variant::UpdateCompletionTriggers(_) => None, + } +} + +fn operation_from_storage( + row: buffer_operation::Model, + _format_version: i32, +) -> Result { + let version = version_from_storage(&row.version)?; + let operation = if row.is_undo { + let counts = undo_operation_from_storage(&row.value)?; + proto::operation::Variant::Undo(proto::operation::Undo { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + counts, + }) + } else { + let (ranges, new_text) = edit_operation_from_storage(&row.value)?; + proto::operation::Variant::Edit(proto::operation::Edit { + replica_id: row.replica_id as u32, + local_timestamp: row.local_timestamp as u32, + lamport_timestamp: row.lamport_timestamp as u32, + version, + ranges, + new_text, + }) + }; + Ok(operation) +} + +// This is currently a manual copy of the deserialization code in the client's langauge crate +pub fn deserialize_wire_operation(operation: proto::Operation) -> Option { + match operation.variant? { + proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { + timestamp: InsertionTimestamp { + replica_id: edit.replica_id as text::ReplicaId, + local: edit.local_timestamp, + lamport: edit.lamport_timestamp, + }, + version: deserialize_wire_version(&edit.version), + ranges: edit.ranges.into_iter().map(deserialize_range).collect(), + new_text: edit.new_text.into_iter().map(Arc::from).collect(), + })), + proto::operation::Variant::Undo(undo) => Some(text::Operation::Undo { + lamport_timestamp: clock::Lamport { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.lamport_timestamp, + }, + undo: UndoOperation { + id: clock::Local { + replica_id: undo.replica_id as text::ReplicaId, + value: undo.local_timestamp, + }, + version: deserialize_wire_version(&undo.version), + counts: undo + .counts + .into_iter() + .map(|c| { + ( + clock::Local { + replica_id: c.replica_id as text::ReplicaId, + value: c.local_timestamp, + }, + c.count, + ) + }) + .collect(), + }, + }), + _ => None, + } +} + +pub fn deserialize_range(range: proto::Range) -> Range { + text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) +} + +fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Global { + let mut version = clock::Global::new(); + for entry in message { + version.observe(clock::Local { + replica_id: entry.replica_id as text::ReplicaId, + value: entry.timestamp, + }); + } + version } mod storage { @@ -325,7 +489,7 @@ mod storage { use prost::Message; - pub const VERSION: usize = 1; + pub const SERIALIZATION_VERSION: i32 = 1; #[derive(Message)] pub struct VectorClock { @@ -374,7 +538,7 @@ mod storage { } } -fn serialize_version(version: &Vec) -> Vec { +fn version_to_storage(version: &Vec) -> Vec { storage::VectorClock { entries: version .iter() @@ -387,7 +551,7 @@ fn serialize_version(version: &Vec) -> Vec { .encode_to_vec() } -fn deserialize_version(bytes: &[u8]) -> Result> { +fn version_from_storage(bytes: &[u8]) -> Result> { let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; Ok(clock .entries @@ -399,7 +563,7 @@ fn deserialize_version(bytes: &[u8]) -> Result> { .collect()) } -fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec { +fn edit_operation_to_storage(ranges: &[proto::Range], texts: &[String]) -> Vec { storage::TextEdit { ranges: ranges .iter() @@ -413,7 +577,7 @@ fn serialize_edit_operation(ranges: &[proto::Range], texts: &[String]) -> Vec Result<(Vec, Vec)> { +fn edit_operation_from_storage(bytes: &[u8]) -> Result<(Vec, Vec)> { let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; let ranges = edit .ranges @@ -426,7 +590,7 @@ fn deserialize_edit_operation(bytes: &[u8]) -> Result<(Vec, Vec) -> Vec { +fn undo_operation_to_storage(counts: &Vec) -> Vec { storage::Undo { entries: counts .iter() @@ -440,7 +604,7 @@ fn serialize_undo_operation(counts: &Vec) -> Vec { .encode_to_vec() } -fn deserialize_undo_operation(bytes: &[u8]) -> Result> { +fn undo_operation_from_storage(bytes: &[u8]) -> Result> { let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; Ok(undo .entries diff --git a/crates/collab/src/db/tables/buffer_snapshot.rs b/crates/collab/src/db/tables/buffer_snapshot.rs index ca8712a053..c9de665e43 100644 --- a/crates/collab/src/db/tables/buffer_snapshot.rs +++ b/crates/collab/src/db/tables/buffer_snapshot.rs @@ -9,6 +9,7 @@ pub struct Model { #[sea_orm(primary_key)] pub epoch: i32, pub text: String, + pub operation_serialization_version: i32, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/crates/collab/src/db/tests/buffer_tests.rs b/crates/collab/src/db/tests/buffer_tests.rs index 08252e382e..e71748b88b 100644 --- a/crates/collab/src/db/tests/buffer_tests.rs +++ b/crates/collab/src/db/tests/buffer_tests.rs @@ -10,7 +10,6 @@ test_both_dbs!( ); async fn test_channel_buffers(db: &Arc) { - // Prep database test info let a_id = db .create_user( "user_a@example.com", @@ -155,5 +154,12 @@ async fn test_channel_buffers(db: &Arc) { assert_eq!(zed_collaborators, &[]); assert_eq!(cargo_collaborators, &[]); - // TODO: test buffer epoch incrementing + // When everyone has left the channel, the operations are collapsed into + // a new base text. + let buffer_response_b = db + .join_channel_buffer(zed_id, b_id, connection_id_b) + .await + .unwrap(); + assert_eq!(buffer_response_b.base_text, "hello, cruel world"); + assert_eq!(buffer_response_b.operations, &[]); } diff --git a/crates/language/src/proto.rs b/crates/language/src/proto.rs index 0de3f704c7..09c5ec7fc3 100644 --- a/crates/language/src/proto.rs +++ b/crates/language/src/proto.rs @@ -207,6 +207,7 @@ pub fn serialize_anchor(anchor: &Anchor) -> proto::Anchor { } } +// This behavior is currently copied in the collab database, for snapshotting channel notes pub fn deserialize_operation(message: proto::Operation) -> Result { Ok( match message diff --git a/crates/text/src/text.rs b/crates/text/src/text.rs index 7c94f25e1e..4a97faf015 100644 --- a/crates/text/src/text.rs +++ b/crates/text/src/text.rs @@ -12,7 +12,7 @@ mod undo_map; pub use anchor::*; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use collections::{HashMap, HashSet}; use fs::LineEnding; use locator::Locator; From 11ef5e27407703e312107d353bb933cbd833d44d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 16:42:08 -0700 Subject: [PATCH 031/142] Simplify buffer_operations schema Co-authored-by: Mikayla --- .../20221109000000_test_schema.sql | 3 - .../20230819154600_add_channel_buffers.sql | 3 - crates/collab/src/db/queries/buffers.rs | 278 ++++++++---------- .../collab/src/db/tables/buffer_operation.rs | 3 - 4 files changed, 121 insertions(+), 166 deletions(-) diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index fdae4f2339..7a4cd9fd23 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -222,9 +222,6 @@ CREATE TABLE "buffer_operations" ( "epoch" INTEGER NOT NULL, "replica_id" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - "local_timestamp" INTEGER NOT NULL, - "version" BLOB NOT NULL, - "is_undo" BOOLEAN NOT NULL, "value" BLOB NOT NULL, PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) ); diff --git a/crates/collab/migrations/20230819154600_add_channel_buffers.sql b/crates/collab/migrations/20230819154600_add_channel_buffers.sql index fec18ddb8d..5e6e7ce339 100644 --- a/crates/collab/migrations/20230819154600_add_channel_buffers.sql +++ b/crates/collab/migrations/20230819154600_add_channel_buffers.sql @@ -10,10 +10,7 @@ CREATE TABLE "buffer_operations" ( "buffer_id" INTEGER NOT NULL REFERENCES buffers (id) ON DELETE CASCADE, "epoch" INTEGER NOT NULL, "replica_id" INTEGER NOT NULL, - "local_timestamp" INTEGER NOT NULL, "lamport_timestamp" INTEGER NOT NULL, - "version" BYTEA NOT NULL, - "is_undo" BOOLEAN NOT NULL, "value" BYTEA NOT NULL, PRIMARY KEY(buffer_id, epoch, lamport_timestamp, replica_id) ); diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index b0df905ecb..83f5b87079 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -1,6 +1,5 @@ use super::*; use prost::Message; -use std::ops::Range; use text::{EditOperation, InsertionTimestamp, UndoOperation}; impl Database { @@ -234,6 +233,7 @@ impl Database { let serialization_version: i32 = buffer .find_related(buffer_snapshot::Entity) .select_only() + .column(buffer_snapshot::Column::OperationSerializationVersion) .filter(buffer_snapshot::Column::Epoch.eq(buffer.epoch)) .into_values::<_, QueryVersion>() .one(&*tx) @@ -326,11 +326,7 @@ impl Database { let mut text_buffer = text::Buffer::new(0, 0, base_text); text_buffer - .apply_ops( - operations - .into_iter() - .filter_map(deserialize_wire_operation), - ) + .apply_ops(operations.into_iter().filter_map(operation_from_wire)) .unwrap(); let base_text = text_buffer.text(); @@ -363,71 +359,122 @@ fn operation_to_storage( buffer: &buffer::Model, _format: i32, ) -> Option { - match operation.variant.as_ref()? { - proto::operation::Variant::Edit(operation) => { - let value = edit_operation_to_storage(&operation.ranges, &operation.new_text); - let version = version_to_storage(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(false), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::Undo(operation) => { - let value = undo_operation_to_storage(&operation.counts); - let version = version_to_storage(&operation.version); - Some(buffer_operation::ActiveModel { - buffer_id: ActiveValue::Set(buffer.id), - epoch: ActiveValue::Set(buffer.epoch), - replica_id: ActiveValue::Set(operation.replica_id as i32), - lamport_timestamp: ActiveValue::Set(operation.lamport_timestamp as i32), - local_timestamp: ActiveValue::Set(operation.local_timestamp as i32), - is_undo: ActiveValue::Set(true), - version: ActiveValue::Set(version), - value: ActiveValue::Set(value), - }) - } - proto::operation::Variant::UpdateSelections(_) => None, - proto::operation::Variant::UpdateDiagnostics(_) => None, - proto::operation::Variant::UpdateCompletionTriggers(_) => None, - } + let (replica_id, lamport_timestamp, value) = match operation.variant.as_ref()? { + proto::operation::Variant::Edit(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: false, + edit_ranges: operation + .ranges + .iter() + .map(|range| storage::Range { + start: range.start, + end: range.end, + }) + .collect(), + edit_texts: operation.new_text.clone(), + undo_counts: Vec::new(), + }, + ), + proto::operation::Variant::Undo(operation) => ( + operation.replica_id, + operation.lamport_timestamp, + storage::Operation { + local_timestamp: operation.local_timestamp, + version: version_to_storage(&operation.version), + is_undo: true, + edit_ranges: Vec::new(), + edit_texts: Vec::new(), + undo_counts: operation + .counts + .iter() + .map(|entry| storage::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), + }, + ), + _ => None?, + }; + + Some(buffer_operation::ActiveModel { + buffer_id: ActiveValue::Set(buffer.id), + epoch: ActiveValue::Set(buffer.epoch), + replica_id: ActiveValue::Set(replica_id as i32), + lamport_timestamp: ActiveValue::Set(lamport_timestamp as i32), + value: ActiveValue::Set(value.encode_to_vec()), + }) } fn operation_from_storage( row: buffer_operation::Model, _format_version: i32, ) -> Result { - let version = version_from_storage(&row.version)?; - let operation = if row.is_undo { - let counts = undo_operation_from_storage(&row.value)?; + let operation = + storage::Operation::decode(row.value.as_slice()).map_err(|error| anyhow!("{}", error))?; + let version = version_from_storage(&operation.version); + Ok(if operation.is_undo { proto::operation::Variant::Undo(proto::operation::Undo { replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, + local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, - counts, + counts: operation + .undo_counts + .iter() + .map(|entry| proto::UndoCount { + replica_id: entry.replica_id, + local_timestamp: entry.local_timestamp, + count: entry.count, + }) + .collect(), }) } else { - let (ranges, new_text) = edit_operation_from_storage(&row.value)?; proto::operation::Variant::Edit(proto::operation::Edit { replica_id: row.replica_id as u32, - local_timestamp: row.local_timestamp as u32, + local_timestamp: operation.local_timestamp as u32, lamport_timestamp: row.lamport_timestamp as u32, version, - ranges, - new_text, + ranges: operation + .edit_ranges + .into_iter() + .map(|range| proto::Range { + start: range.start, + end: range.end, + }) + .collect(), + new_text: operation.edit_texts, }) - }; - Ok(operation) + }) +} + +fn version_to_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| storage::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() +} + +fn version_from_storage(version: &Vec) -> Vec { + version + .iter() + .map(|entry| proto::VectorClockEntry { + replica_id: entry.replica_id, + timestamp: entry.timestamp, + }) + .collect() } // This is currently a manual copy of the deserialization code in the client's langauge crate -pub fn deserialize_wire_operation(operation: proto::Operation) -> Option { +pub fn operation_from_wire(operation: proto::Operation) -> Option { match operation.variant? { proto::operation::Variant::Edit(edit) => Some(text::Operation::Edit(EditOperation { timestamp: InsertionTimestamp { @@ -435,8 +482,14 @@ pub fn deserialize_wire_operation(operation: proto::Operation) -> Option Some(text::Operation::Undo { @@ -449,7 +502,7 @@ pub fn deserialize_wire_operation(operation: proto::Operation) -> Option Option Range { - text::FullOffset(range.start as usize)..text::FullOffset(range.end as usize) -} - -fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Global { +fn version_from_wire(message: &[proto::VectorClockEntry]) -> clock::Global { let mut version = clock::Global::new(); for entry in message { version.observe(clock::Local { @@ -486,15 +535,23 @@ fn deserialize_wire_version(message: &[proto::VectorClockEntry]) -> clock::Globa mod storage { #![allow(non_snake_case)] - use prost::Message; - pub const SERIALIZATION_VERSION: i32 = 1; #[derive(Message)] - pub struct VectorClock { - #[prost(message, repeated, tag = "1")] - pub entries: Vec, + pub struct Operation { + #[prost(uint32, tag = "1")] + pub local_timestamp: u32, + #[prost(message, repeated, tag = "2")] + pub version: Vec, + #[prost(bool, tag = "3")] + pub is_undo: bool, + #[prost(message, repeated, tag = "4")] + pub edit_ranges: Vec, + #[prost(string, repeated, tag = "5")] + pub edit_texts: Vec, + #[prost(message, repeated, tag = "6")] + pub undo_counts: Vec, } #[derive(Message)] @@ -505,14 +562,6 @@ mod storage { pub timestamp: u32, } - #[derive(Message)] - pub struct TextEdit { - #[prost(message, repeated, tag = "1")] - pub ranges: Vec, - #[prost(string, repeated, tag = "2")] - pub texts: Vec, - } - #[derive(Message)] pub struct Range { #[prost(uint64, tag = "1")] @@ -521,12 +570,6 @@ mod storage { pub end: u64, } - #[derive(Message)] - pub struct Undo { - #[prost(message, repeated, tag = "1")] - pub entries: Vec, - } - #[derive(Message)] pub struct UndoCount { #[prost(uint32, tag = "1")] @@ -537,82 +580,3 @@ mod storage { pub count: u32, } } - -fn version_to_storage(version: &Vec) -> Vec { - storage::VectorClock { - entries: version - .iter() - .map(|entry| storage::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect(), - } - .encode_to_vec() -} - -fn version_from_storage(bytes: &[u8]) -> Result> { - let clock = storage::VectorClock::decode(bytes).map_err(|error| anyhow!("{}", error))?; - Ok(clock - .entries - .into_iter() - .map(|entry| proto::VectorClockEntry { - replica_id: entry.replica_id, - timestamp: entry.timestamp, - }) - .collect()) -} - -fn edit_operation_to_storage(ranges: &[proto::Range], texts: &[String]) -> Vec { - storage::TextEdit { - ranges: ranges - .iter() - .map(|range| storage::Range { - start: range.start, - end: range.end, - }) - .collect(), - texts: texts.to_vec(), - } - .encode_to_vec() -} - -fn edit_operation_from_storage(bytes: &[u8]) -> Result<(Vec, Vec)> { - let edit = storage::TextEdit::decode(bytes).map_err(|error| anyhow!("{}", error))?; - let ranges = edit - .ranges - .into_iter() - .map(|range| proto::Range { - start: range.start, - end: range.end, - }) - .collect(); - Ok((ranges, edit.texts)) -} - -fn undo_operation_to_storage(counts: &Vec) -> Vec { - storage::Undo { - entries: counts - .iter() - .map(|entry| storage::UndoCount { - replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, - count: entry.count, - }) - .collect(), - } - .encode_to_vec() -} - -fn undo_operation_from_storage(bytes: &[u8]) -> Result> { - let undo = storage::Undo::decode(bytes).map_err(|error| anyhow!("{}", error))?; - Ok(undo - .entries - .iter() - .map(|entry| proto::UndoCount { - replica_id: entry.replica_id, - local_timestamp: entry.local_timestamp, - count: entry.count, - }) - .collect()) -} diff --git a/crates/collab/src/db/tables/buffer_operation.rs b/crates/collab/src/db/tables/buffer_operation.rs index 59626c1e77..37bd4bedfe 100644 --- a/crates/collab/src/db/tables/buffer_operation.rs +++ b/crates/collab/src/db/tables/buffer_operation.rs @@ -12,9 +12,6 @@ pub struct Model { pub lamport_timestamp: i32, #[sea_orm(primary_key)] pub replica_id: i32, - pub local_timestamp: i32, - pub version: Vec, - pub is_undo: bool, pub value: Vec, } From 7e831388050d2e4b026421b3100bf71fa9ef22cc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 22 Aug 2023 18:08:03 -0700 Subject: [PATCH 032/142] Start work on showing consistent replica ids for channel buffers Co-authored-by: Mikayla --- Cargo.lock | 1 + crates/channel/src/channel_buffer.rs | 4 + crates/collab/Cargo.toml | 1 + .../collab/src/tests/channel_buffer_tests.rs | 136 +++++++++++++++++- crates/collab_ui/src/channel_view.rs | 43 +++++- crates/collab_ui/src/collab_panel.rs | 9 +- crates/editor/src/editor.rs | 11 ++ crates/project/src/project.rs | 4 +- 8 files changed, 202 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0ec2f34185..2bda9fda46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1459,6 +1459,7 @@ dependencies = [ "clap 3.2.25", "client", "clock", + "collab_ui", "collections", "ctor", "dashmap", diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index aa99d5c10b..5ee3fd6c84 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -171,4 +171,8 @@ impl ChannelBuffer { .channel_for_id(self.channel_id) .cloned() } + + pub fn replica_id(&self, cx: &AppContext) -> u16 { + self.buffer.read(cx).replica_id() + } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index cc1970266d..8adc38615c 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -78,6 +78,7 @@ rpc = { path = "../rpc", features = ["test-support"] } settings = { path = "../settings", features = ["test-support"] } theme = { path = "../theme" } workspace = { path = "../workspace", features = ["test-support"] } +collab_ui = { path = "../collab_ui", features = ["test-support"] } ctor.workspace = true env_logger.workspace = true diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index db98c6abdc..8fb50055f5 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,8 +1,11 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; - +use call::ActiveCall; use client::UserId; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; +use serde_json::json; use std::sync::Arc; #[gpui::test] @@ -82,7 +85,9 @@ async fn test_core_channel_buffers( // Client A rejoins the channel buffer let _channel_buffer_a = client_a .channel_store() - .update(cx_a, |channels, cx| channels.open_channel_buffer(zed_id, cx)) + .update(cx_a, |channels, cx| { + channels.open_channel_buffer(zed_id, cx) + }) .await .unwrap(); deterministic.run_until_parked(); @@ -110,6 +115,133 @@ async fn test_core_channel_buffers( // - Test interaction with channel deletion while buffer is open } +#[gpui::test] +async fn test_channel_buffer_replica_ids( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "zed", + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + // Clients A and B join a channel. + active_call_a + .update(cx_a, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + active_call_b + .update(cx_b, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + + // Clients A, B, and C join a channel buffer + // C first so that the replica IDs in the project and the channel buffer are different + let channel_buffer_c = client_c + .channel_store() + .update(cx_c, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + // Client B shares a project + client_b + .fs() + .insert_tree("/dir", json!({ "file.txt": "contents" })) + .await; + let (project_b, _) = client_b.build_local_project("/dir", cx_b).await; + let shared_project_id = active_call_b + .update(cx_b, |call, cx| call.share_project(project_b.clone(), cx)) + .await + .unwrap(); + + // Client A joins the project + let project_a = client_a.build_remote_project(shared_project_id, cx_a).await; + deterministic.run_until_parked(); + + // Client C is in a separate project. + client_c.fs().insert_tree("/dir", json!({})).await; + let (project_c, _) = client_c.build_local_project("/dir", cx_c).await; + + // Note that each user has a different replica id in the projects vs the + // channel buffer. + channel_buffer_a.read_with(cx_a, |channel_buffer, cx| { + assert_eq!(project_a.read(cx).replica_id(), 1); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 2); + }); + channel_buffer_b.read_with(cx_b, |channel_buffer, cx| { + assert_eq!(project_b.read(cx).replica_id(), 0); + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 1); + }); + channel_buffer_c.read_with(cx_c, |channel_buffer, cx| { + // C is not in the project + assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); + }); + + let channel_window_a = cx_a + .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); + let channel_window_b = cx_b + .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); + let channel_window_c = cx_c + .add_window(|cx| ChannelView::new(project_c.clone(), channel_buffer_c.clone(), None, cx)); + + let channel_view_a = channel_window_a.root(cx_a); + let channel_view_b = channel_window_b.root(cx_b); + let channel_view_c = channel_window_c.root(cx_c); + + // For clients A and B, the replica ids in the channel buffer are mapped + // so that they match the same users' replica ids in their shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(1, 0), (2, 1)].into_iter().collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(1, 0), (2, 1)].into_iter().collect::>(), + ) + }); + + // Client C only sees themself, as they're not part of any shared project + channel_view_c.read_with(cx_c, |view, cx| { + assert_eq!( + view.project_replica_ids_by_channel_buffer_replica_id(cx), + [(0, 0)].into_iter().collect::>(), + ); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 27a2d678f5..af45eabe69 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,6 @@ use channel::channel_buffer::ChannelBuffer; +use clock::ReplicaId; +use collections::HashMap; use editor::Editor; use gpui::{ actions, @@ -6,6 +8,7 @@ use gpui::{ AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, }; use language::Language; +use project::Project; use std::sync::Arc; use workspace::item::{Item, ItemHandle}; @@ -17,22 +20,56 @@ pub(crate) fn init(cx: &mut AppContext) { pub struct ChannelView { editor: ViewHandle, + project: ModelHandle, channel_buffer: ModelHandle, } impl ChannelView { pub fn new( + project: ModelHandle, channel_buffer: ModelHandle, - language: Arc, + language: Option>, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx)); + buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); - Self { + let this = Self { editor, + project, channel_buffer, + }; + let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); + this.editor + .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); + this + } + + /// Channel Buffer Replica ID -> Project Replica ID + pub fn project_replica_ids_by_channel_buffer_replica_id( + &self, + cx: &AppContext, + ) -> HashMap { + let project = self.project.read(cx); + let mut result = HashMap::default(); + result.insert( + self.channel_buffer.read(cx).replica_id(cx), + project.replica_id(), + ); + for collaborator in self.channel_buffer.read(cx).collaborators() { + let project_replica_id = + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == collaborator.user_id) + .then_some(project_collaborator.replica_id) + }); + if let Some(project_replica_id) = project_replica_id { + result.insert(collaborator.replica_id as ReplicaId, project_replica_id); + } } + result } } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 0eb6a65984..a6bd09e43b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2238,7 +2238,14 @@ impl CollabPanel { .await?; workspace.update(&mut cx, |workspace, cx| { - let channel_view = cx.add_view(|cx| ChannelView::new(channel_buffer, markdown, cx)); + let channel_view = cx.add_view(|cx| { + ChannelView::new( + workspace.project().to_owned(), + channel_buffer, + Some(markdown), + cx, + ) + }); workspace.add_item(Box::new(channel_view), cx); })?; diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 67279b1ba6..e7197d98c5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -559,6 +559,7 @@ pub struct Editor { blink_manager: ModelHandle, show_local_selections: bool, mode: EditorMode, + replica_id_mapping: Option>, show_gutter: bool, show_wrap_guides: Option, placeholder_text: Option>, @@ -1394,6 +1395,7 @@ impl Editor { blink_manager: blink_manager.clone(), show_local_selections: true, mode, + replica_id_mapping: None, show_gutter: mode == EditorMode::Full, show_wrap_guides: None, placeholder_text: None, @@ -1604,6 +1606,15 @@ impl Editor { self.read_only = read_only; } + pub fn set_replica_id_mapping( + &mut self, + mapping: HashMap, + cx: &mut ViewContext, + ) { + self.replica_id_mapping = Some(mapping); + cx.notify(); + } + fn selections_did_change( &mut self, local: bool, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index b120baa951..bc4fa587cb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -11,7 +11,7 @@ mod project_tests; mod worktree_tests; use anyhow::{anyhow, Context, Result}; -use client::{proto, Client, TypedEnvelope, UserStore}; +use client::{proto, Client, TypedEnvelope, UserId, UserStore}; use clock::ReplicaId; use collections::{hash_map, BTreeMap, HashMap, HashSet}; use copilot::Copilot; @@ -250,6 +250,7 @@ enum ProjectClientState { pub struct Collaborator { pub peer_id: proto::PeerId, pub replica_id: ReplicaId, + pub user_id: UserId, } #[derive(Clone, Debug, PartialEq)] @@ -7756,6 +7757,7 @@ impl Collaborator { Ok(Self { peer_id: message.peer_id.ok_or_else(|| anyhow!("invalid peer id"))?, replica_id: message.replica_id as ReplicaId, + user_id: message.user_id as UserId, }) } } From e4794e3134b6449e36ed2771a8849046489cc252 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Wed, 23 Aug 2023 23:45:42 -0600 Subject: [PATCH 033/142] vim: Fix linewise copy of last line with no trailing newline Along the way, delete the VimBindingTestContext by updating the visual tests to no-longer need it. --- crates/vim/src/test.rs | 2 - .../vim/src/test/vim_binding_test_context.rs | 64 ------- crates/vim/src/test/vim_test_context.rs | 10 -- crates/vim/src/utils.rs | 19 +- crates/vim/src/visual.rs | 166 ++++++++---------- .../test_data/test_visual_line_delete.json | 15 +- crates/vim/test_data/test_visual_yank.json | 29 +++ 7 files changed, 118 insertions(+), 187 deletions(-) delete mode 100644 crates/vim/src/test/vim_binding_test_context.rs create mode 100644 crates/vim/test_data/test_visual_yank.json diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 3b1b8c24f2..9cd927601f 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -1,7 +1,6 @@ mod neovim_backed_binding_test_context; mod neovim_backed_test_context; mod neovim_connection; -mod vim_binding_test_context; mod vim_test_context; use std::sync::Arc; @@ -10,7 +9,6 @@ use command_palette::CommandPalette; use editor::DisplayPoint; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; -pub use vim_binding_test_context::*; pub use vim_test_context::*; use indoc::indoc; diff --git a/crates/vim/src/test/vim_binding_test_context.rs b/crates/vim/src/test/vim_binding_test_context.rs deleted file mode 100644 index 04afe0c058..0000000000 --- a/crates/vim/src/test/vim_binding_test_context.rs +++ /dev/null @@ -1,64 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use crate::*; - -use super::VimTestContext; - -pub struct VimBindingTestContext<'a, const COUNT: usize> { - cx: VimTestContext<'a>, - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, -} - -impl<'a, const COUNT: usize> VimBindingTestContext<'a, COUNT> { - pub fn new( - keystrokes_under_test: [&'static str; COUNT], - mode_before: Mode, - mode_after: Mode, - cx: VimTestContext<'a>, - ) -> Self { - Self { - cx, - keystrokes_under_test, - mode_before, - mode_after, - } - } - - pub fn binding( - self, - keystrokes_under_test: [&'static str; NEW_COUNT], - ) -> VimBindingTestContext<'a, NEW_COUNT> { - VimBindingTestContext { - keystrokes_under_test, - cx: self.cx, - mode_before: self.mode_before, - mode_after: self.mode_after, - } - } - - pub fn assert(&mut self, initial_state: &str, state_after: &str) { - self.cx.assert_binding( - self.keystrokes_under_test, - initial_state, - self.mode_before, - state_after, - self.mode_after, - ) - } -} - -impl<'a, const COUNT: usize> Deref for VimBindingTestContext<'a, COUNT> { - type Target = VimTestContext<'a>; - - fn deref(&self) -> &Self::Target { - &self.cx - } -} - -impl<'a, const COUNT: usize> DerefMut for VimBindingTestContext<'a, COUNT> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx - } -} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 24fb16fd3d..9b03739570 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -8,8 +8,6 @@ use search::{BufferSearchBar, ProjectSearchBar}; use crate::{state::Operator, *}; -use super::VimBindingTestContext; - pub struct VimTestContext<'a> { cx: EditorLspTestContext<'a>, } @@ -126,14 +124,6 @@ impl<'a> VimTestContext<'a> { assert_eq!(self.mode(), mode_after, "{}", self.assertion_context()); assert_eq!(self.active_operator(), None, "{}", self.assertion_context()); } - - pub fn binding( - mut self, - keystrokes: [&'static str; COUNT], - ) -> VimBindingTestContext<'a, COUNT> { - let mode = self.mode(); - VimBindingTestContext::new(keystrokes, mode, mode, self) - } } impl<'a> Deref for VimTestContext<'a> { diff --git a/crates/vim/src/utils.rs b/crates/vim/src/utils.rs index c8ca4df72b..4a96b5bbea 100644 --- a/crates/vim/src/utils.rs +++ b/crates/vim/src/utils.rs @@ -1,5 +1,6 @@ use editor::{ClipboardSelection, Editor}; use gpui::{AppContext, ClipboardItem}; +use language::Point; pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut AppContext) { let selections = editor.selections.all_adjusted(cx); @@ -9,7 +10,7 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App { let mut is_first = true; for selection in selections.iter() { - let start = selection.start; + let mut start = selection.start; let end = selection.end; if is_first { is_first = false; @@ -17,9 +18,25 @@ pub fn copy_selections_content(editor: &mut Editor, linewise: bool, cx: &mut App text.push_str("\n"); } let initial_len = text.len(); + + // if the file does not end with \n, and our line-mode selection ends on + // that line, we will have expanded the start of the selection to ensure it + // contains a newline (so that delete works as expected). We undo that change + // here. + let is_last_line = linewise + && end.row == buffer.max_buffer_row() + && buffer.max_point().column > 0 + && start == Point::new(start.row, buffer.line_len(start.row)); + + if is_last_line { + start = Point::new(buffer.max_buffer_row(), 0); + } for chunk in buffer.text_for_range(start..end) { text.push_str(chunk); } + if is_last_line { + text.push_str("\n"); + } clipboard_selections.push(ClipboardSelection { len: text.len() - initial_len, is_entire_line: linewise, diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1a11721a4e..ea4847bd78 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -563,38 +563,41 @@ mod test { #[gpui::test] async fn test_visual_line_delete(cx: &mut gpui::TestAppContext) { - let mut cx = NeovimBackedTestContext::new(cx) - .await - .binding(["shift-v", "x"]); - cx.assert(indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {" The quˇick brown fox jumps over the lazy dog"}) .await; + cx.simulate_shared_keystrokes(["shift-v", "x"]).await; + cx.assert_state_matches().await; + // Test pasting code copied on delete cx.simulate_shared_keystroke("p").await; cx.assert_state_matches().await; - cx.assert_all(indoc! {" + cx.set_shared_state(indoc! {" The quick brown - fox juˇmps over - the laˇzy dog"}) - .await; - let mut cx = cx.binding(["shift-v", "j", "x"]); - cx.assert(indoc! {" - The quˇick brown fox jumps over - the lazy dog"}) - .await; - // Test pasting code copied on delete - cx.simulate_shared_keystroke("p").await; - cx.assert_state_matches().await; - - cx.assert_all(indoc! {" - The quick brown - fox juˇmps over the laˇzy dog"}) .await; + cx.simulate_shared_keystrokes(["shift-v", "x"]).await; + cx.assert_state_matches().await; + cx.assert_shared_clipboard("the lazy dog\n").await; + + for marked_text in cx.each_marked_position(indoc! {" + The quˇick brown + fox jumps over + the lazy dog"}) + { + cx.set_shared_state(&marked_text).await; + cx.simulate_shared_keystrokes(["shift-v", "j", "x"]).await; + cx.assert_state_matches().await; + // Test pasting code copied on delete + cx.simulate_shared_keystroke("p").await; + cx.assert_state_matches().await; + } cx.set_shared_state(indoc! {" The ˇlong line @@ -608,86 +611,57 @@ mod test { #[gpui::test] async fn test_visual_yank(cx: &mut gpui::TestAppContext) { - let cx = VimTestContext::new(cx, true).await; - let mut cx = cx.binding(["v", "w", "y"]); - cx.assert("The quick ˇbrown", "The quick ˇbrown"); - cx.assert_clipboard_content(Some("brown")); - let mut cx = cx.binding(["v", "w", "j", "y"]); - cx.assert( - indoc! {" + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state("The quick ˇbrown").await; + cx.simulate_shared_keystrokes(["v", "w", "y"]).await; + cx.assert_shared_state("The quick ˇbrown").await; + cx.assert_shared_clipboard("brown").await; + + cx.set_shared_state(indoc! {" The ˇquick brown fox jumps over - the lazy dog"}, - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - ); - cx.assert_clipboard_content(Some("lazy d")); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - over - t"})); + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.assert_shared_clipboard(indoc! {" + quick brown + fox jumps o"}) + .await; + + cx.set_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "w", "j", "y"]).await; + cx.assert_shared_state(indoc! {" + The quick brown + fox jumps over + the ˇlazy dog"}) + .await; + cx.assert_shared_clipboard("lazy d").await; + cx.simulate_shared_keystrokes(["shift-v", "y"]).await; + cx.assert_shared_clipboard("the lazy dog\n").await; + let mut cx = cx.binding(["v", "b", "k", "y"]); - cx.assert( - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - indoc! {" - ˇThe quick brown - fox jumps over - the lazy dog"}, - ); + cx.set_shared_state(indoc! {" + The ˇquick brown + fox jumps over + the lazy dog"}) + .await; + cx.simulate_shared_keystrokes(["v", "b", "k", "y"]).await; + cx.assert_shared_state(indoc! {" + ˇThe quick brown + fox jumps over + the lazy dog"}) + .await; cx.assert_clipboard_content(Some("The q")); - cx.assert( - indoc! {" - The quick brown - fox jumps over - the ˇlazy dog"}, - indoc! {" - The quick brown - ˇfox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - fox jumps over - the l"})); - cx.assert( - indoc! {" - The quick brown - fox jumps ˇover - the lazy dog"}, - indoc! {" - The ˇquick brown - fox jumps over - the lazy dog"}, - ); - cx.assert_clipboard_content(Some(indoc! {" - quick brown - fox jumps o"})); } #[gpui::test] diff --git a/crates/vim/test_data/test_visual_line_delete.json b/crates/vim/test_data/test_visual_line_delete.json index 51406266f6..e221a4ad5f 100644 --- a/crates/vim/test_data/test_visual_line_delete.json +++ b/crates/vim/test_data/test_visual_line_delete.json @@ -4,14 +4,11 @@ {"Get":{"state":"fox juˇmps over\nthe lazy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"fox jumps over\nˇThe quick brown\nthe lazy dog","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nthe laˇzy dog","mode":"Normal"}} {"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} {"Key":"shift-v"} {"Key":"x"} {"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} {"Put":{"state":"The quˇick brown\nfox jumps over\nthe lazy dog"}} {"Key":"shift-v"} {"Key":"j"} @@ -19,16 +16,6 @@ {"Get":{"state":"the laˇzy dog","mode":"Normal"}} {"Key":"p"} {"Get":{"state":"the lazy dog\nˇThe quick brown\nfox jumps over","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox juˇmps over\nthe lazy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quˇick brown","mode":"Normal"}} -{"Put":{"state":"The quick brown\nfox jumps over\nthe laˇzy dog"}} -{"Key":"shift-v"} -{"Key":"j"} -{"Key":"x"} -{"Get":{"state":"The quick brown\nfox juˇmps over","mode":"Normal"}} {"Put":{"state":"The ˇlong line\nshould not\ncrash\n"}} {"Key":"shift-v"} {"Key":"$"} diff --git a/crates/vim/test_data/test_visual_yank.json b/crates/vim/test_data/test_visual_yank.json new file mode 100644 index 0000000000..edc3b4f83d --- /dev/null +++ b/crates/vim/test_data/test_visual_yank.json @@ -0,0 +1,29 @@ +{"Put":{"state":"The quick ˇbrown"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"y"} +{"Get":{"state":"The quick ˇbrown","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"brown"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"quick brown\nfox jumps o"}} +{"Put":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog"}} +{"Key":"v"} +{"Key":"w"} +{"Key":"j"} +{"Key":"y"} +{"Get":{"state":"The quick brown\nfox jumps over\nthe ˇlazy dog","mode":"Normal"}} +{"ReadRegister":{"name":"\"","value":"lazy d"}} +{"Key":"shift-v"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":"the lazy dog\n"}} +{"Put":{"state":"The ˇquick brown\nfox jumps over\nthe lazy dog"}} +{"Key":"v"} +{"Key":"b"} +{"Key":"k"} +{"Key":"y"} +{"Get":{"state":"ˇThe quick brown\nfox jumps over\nthe lazy dog","mode":"Normal"}} From 3f9f742530cdb3f5684a22f4934cf3a949e74a58 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 11:45:52 +0200 Subject: [PATCH 034/142] update rate limiting embeddings strategy to delay less --- crates/semantic_index/src/embedding.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/semantic_index/src/embedding.rs b/crates/semantic_index/src/embedding.rs index 77457ec7f6..f2269a786a 100644 --- a/crates/semantic_index/src/embedding.rs +++ b/crates/semantic_index/src/embedding.rs @@ -106,8 +106,8 @@ impl OpenAIEmbeddings { #[async_trait] impl EmbeddingProvider for OpenAIEmbeddings { async fn embed_batch(&self, spans: Vec<&str>) -> Result>> { - const BACKOFF_SECONDS: [usize; 3] = [45, 75, 125]; - const MAX_RETRIES: usize = 3; + const BACKOFF_SECONDS: [usize; 4] = [3, 5, 15, 45]; + const MAX_RETRIES: usize = 4; let api_key = OPENAI_API_KEY .as_ref() From aa07872a24bc25dfe97d4b460b3b7d919e4e9ae9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:36:33 +0200 Subject: [PATCH 035/142] accomodate for duplicate entries in indexing queue Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 37 ++++++++++++++++----- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 0df3a9cc84..65b7e40525 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -12,6 +12,7 @@ use db::VectorDatabase; use embedding::{EmbeddingProvider, OpenAIEmbeddings}; use futures::{channel::oneshot, Future}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; +use isahc::http::header::OccupiedEntry; use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; @@ -130,10 +131,11 @@ impl ProjectState { let (job_queue_tx, job_queue_rx) = channel::unbounded(); let _queue_update_task = cx.background().spawn({ - let mut worktree_queue = Vec::new(); + let mut worktree_queue = HashMap::new(); async move { while let Ok(operation) = job_queue_rx.recv().await { Self::update_queue(&mut worktree_queue, operation); + dbg!(worktree_queue.len()); } } }); @@ -153,24 +155,37 @@ impl ProjectState { self.outstanding_job_count_rx.borrow().clone() } - fn update_queue(queue: &mut Vec, operation: IndexOperation) { + fn update_queue(queue: &mut HashMap, operation: IndexOperation) { match operation { IndexOperation::FlushQueue => { - while let Some(op) = queue.pop() { + let queue = std::mem::take(queue); + for (_, op) in queue { match op { - IndexOperation::IndexFile { payload, tx } => { + IndexOperation::IndexFile { + absolute_path, + payload, + tx, + } => { tx.try_send(payload); } - IndexOperation::DeleteFile { payload, tx } => { + IndexOperation::DeleteFile { + absolute_path, + payload, + tx, + } => { tx.try_send(payload); } _ => {} } } } - _ => { - // TODO: This has to accomodate for duplicate files to index. - queue.push(operation); + IndexOperation::IndexFile { + ref absolute_path, .. + } + | IndexOperation::DeleteFile { + ref absolute_path, .. + } => { + queue.insert(absolute_path.clone(), operation); } } } @@ -209,13 +224,14 @@ pub struct PendingFile { modified_time: SystemTime, job_handle: JobHandle, } - enum IndexOperation { IndexFile { + absolute_path: PathBuf, payload: PendingFile, tx: channel::Sender, }, DeleteFile { + absolute_path: PathBuf, payload: DbOperation, tx: channel::Sender, }, @@ -718,6 +734,7 @@ impl SemanticIndex { let job_handle = JobHandle::new(&outstanding_job_tx); let new_operation = IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), payload: PendingFile { worktree_db_id, relative_path, @@ -733,6 +750,7 @@ impl SemanticIndex { } PathChange::Removed => { let new_operation = IndexOperation::DeleteFile { + absolute_path, payload: DbOperation::Delete { worktree_id: worktree_db_id, path: relative_path, @@ -853,6 +871,7 @@ impl SemanticIndex { if !already_stored { let job_handle = JobHandle::new(&job_count_tx); worktree_files.push(IndexOperation::IndexFile { + absolute_path: absolute_path.clone(), payload: PendingFile { worktree_db_id: db_ids_by_worktree_id[&worktree.id()], relative_path: path_buf, From afe0e74868f65ba16a80a35089f252af20b1bfc8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:42:41 +0200 Subject: [PATCH 036/142] remove worktree_file_mtimes in state as it is no longer used Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 65b7e40525..9d789d0eac 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -94,7 +94,6 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - worktree_file_mtimes: HashMap>, subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, @@ -122,7 +121,6 @@ impl ProjectState { cx: &mut AppContext, subscription: gpui::Subscription, worktree_db_ids: Vec<(WorktreeId, i64)>, - worktree_file_mtimes: HashMap>, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, ) -> Self { @@ -142,7 +140,6 @@ impl ProjectState { Self { worktree_db_ids, - worktree_file_mtimes, outstanding_job_count_rx, _outstanding_job_count_tx, subscription, @@ -834,7 +831,6 @@ impl SemanticIndex { let job_count_tx = Arc::new(Mutex::new(job_count_tx)); let job_count_tx_longlived = job_count_tx.clone(); - let worktree_file_mtimes_all = worktree_file_mtimes.clone(); let worktree_files = cx .background() .spawn(async move { @@ -896,7 +892,6 @@ impl SemanticIndex { cx, _subscription, worktree_db_ids, - worktree_file_mtimes_all, job_count_rx, job_count_tx_longlived, ); From e8e7b294d84b677256f1a10645ab65f7ecae135b Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 12:49:20 +0200 Subject: [PATCH 037/142] add delete files operation for remaining files in database not included in current worktree Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 9d789d0eac..cca63c00aa 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -797,6 +797,7 @@ impl SemanticIndex { let language_registry = self.language_registry.clone(); let parsing_files_tx = self.parsing_files_tx.clone(); + let db_update_tx = self.db_update_tx.clone(); cx.spawn(|this, mut cx| async move { futures::future::join_all(worktree_scans_complete).await; @@ -837,6 +838,7 @@ impl SemanticIndex { let mut worktree_files = Vec::new(); for worktree in worktrees.into_iter() { let mut file_mtimes = worktree_file_mtimes.remove(&worktree.id()).unwrap(); + let worktree_db_id = db_ids_by_worktree_id[&worktree.id()]; for file in worktree.files(false, 0) { let absolute_path = worktree.absolutize(&file.path); @@ -869,7 +871,7 @@ impl SemanticIndex { worktree_files.push(IndexOperation::IndexFile { absolute_path: absolute_path.clone(), payload: PendingFile { - worktree_db_id: db_ids_by_worktree_id[&worktree.id()], + worktree_db_id, relative_path: path_buf, absolute_path, language, @@ -881,6 +883,17 @@ impl SemanticIndex { } } } + // Clean up entries from database that are no longer in the worktree. + for (path, mtime) in file_mtimes { + worktree_files.push(IndexOperation::DeleteFile { + absolute_path: worktree.absolutize(path.as_path()), + payload: DbOperation::Delete { + worktree_id: worktree_db_id, + path, + }, + tx: db_update_tx.clone(), + }); + } } anyhow::Ok(worktree_files) From a1519e4c38bff6879616f261fb95ff0fec7262b9 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:14:19 +0200 Subject: [PATCH 038/142] move semantic search project intialization to a subscribe event for workspace created Co-authored-by: Piotr --- crates/search/src/project_search.rs | 10 ---------- crates/semantic_index/src/semantic_index.rs | 18 +++++++++++++++++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 357aca2ed8..b0ec7219d1 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -844,16 +844,6 @@ impl ProjectSearchView { .detach(); let filters_enabled = false; - // Initialize Semantic Index if Needed - if SemanticIndex::enabled(cx) { - let model = model.read(cx); - project = model.project.clone(); - SemanticIndex::global(cx).map(|semantic| { - dbg!("Initializing project"); - semantic.update(cx, |this, cx| this.initialize_project(project.clone(), cx)); - }); - } - // Check if Worktrees have all been previously indexed let mut this = ProjectSearchView { search_id: model.read(cx).search_id, diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index cca63c00aa..fed320becd 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -35,6 +35,7 @@ use util::{ paths::EMBEDDINGS_DIR, ResultExt, }; +use workspace::WorkspaceCreated; const SEMANTIC_INDEX_VERSION: usize = 7; const EMBEDDINGS_BATCH_SIZE: usize = 80; @@ -56,6 +57,22 @@ pub fn init( return; } + cx.subscribe_global::({ + move |event, mut cx| { + let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; + let workspace = &event.0; + if let Some(workspace) = workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + if project.read(cx).is_local() { + semantic_index.update(cx, |index, cx| { + index.initialize_project(project, cx); + }); + } + } + } + }) + .detach(); + cx.spawn(move |mut cx| async move { let semantic_index = SemanticIndex::new( fs, @@ -133,7 +150,6 @@ impl ProjectState { async move { while let Ok(operation) = job_queue_rx.recv().await { Self::update_queue(&mut worktree_queue, operation); - dbg!(worktree_queue.len()); } } }); From 0b204bfdc8d083c05e6b0637b4f3dbdf0eff9cde Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:40:04 +0200 Subject: [PATCH 039/142] reindex semantic index when search project pane is reactivated in semantic mode Co-authored-by: Piotr --- crates/search/src/project_search.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index b0ec7219d1..f665c4ddcd 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1635,6 +1635,12 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = None; self.active_project_search = None; if let Some(search) = active_pane_item.and_then(|i| i.downcast::()) { + search.update(cx, |search, cx| { + if search.current_mode == SearchMode::Semantic { + search.index_project(cx); + } + }); + self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { From a892a51ec30bf9b6dec54399615e835528dfdac8 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 13:46:43 +0200 Subject: [PATCH 040/142] update initialize project call to accomodate for test scenarios Co-authored-by: Piotr --- crates/semantic_index/src/semantic_index.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index fed320becd..4f932b0622 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -65,7 +65,7 @@ pub fn init( let project = workspace.read(cx).project().clone(); if project.read(cx).is_local() { semantic_index.update(cx, |index, cx| { - index.initialize_project(project, cx); + index.initialize_project(project, cx).detach_and_log_err(cx) }); } } @@ -785,7 +785,7 @@ impl SemanticIndex { &mut self, project: ModelHandle, cx: &mut ModelContext, - ) { + ) -> Task> { let worktree_scans_complete = project .read(cx) .worktrees(cx) @@ -931,10 +931,8 @@ impl SemanticIndex { this.projects.insert(project.downgrade(), project_state); }); - - cx.background().spawn(async move { anyhow::Ok(()) }).await + Result::<(), _>::Ok(()) }) - .detach_and_log_err(cx) } pub fn index_project( From 131950f6702c2af6549438673e5d79a00cface1d Mon Sep 17 00:00:00 2001 From: KCaverly Date: Thu, 24 Aug 2023 18:40:08 +0200 Subject: [PATCH 041/142] add handling for Added file events to semantic index --- crates/semantic_index/src/semantic_index.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 4f932b0622..474025c25e 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -728,9 +728,9 @@ impl SemanticIndex { continue; } + log::trace!("File Event: {:?}, Path: {:?}", &path_change, &path); match path_change { - PathChange::AddedOrUpdated | PathChange::Updated => { - log::trace!("File Updated: {:?}", path); + PathChange::AddedOrUpdated | PathChange::Updated | PathChange::Added => { if let Ok(language) = language_registry .language_for_file(&relative_path, None) .await @@ -786,6 +786,7 @@ impl SemanticIndex { project: ModelHandle, cx: &mut ModelContext, ) -> Task> { + log::trace!("Initializing Project for Semantic Index"); let worktree_scans_complete = project .read(cx) .worktrees(cx) @@ -807,7 +808,7 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - this.project_entries_changed(project, changes.clone(), cx, worktree_id); + this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); }; }); From 199be8241c5ab47d7d41b0b36402ca3133e1944d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Thu, 24 Aug 2023 11:25:20 -0700 Subject: [PATCH 042/142] Add following into channel notes co-authored-by: max --- crates/collab_ui/src/channel_view.rs | 108 ++++++++++++++++++++++++++- crates/editor/src/editor_tests.rs | 4 +- crates/editor/src/items.rs | 3 +- crates/rpc/proto/zed.proto | 5 ++ crates/workspace/src/item.rs | 2 +- crates/workspace/src/workspace.rs | 23 +++--- 6 files changed, 128 insertions(+), 17 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index af45eabe69..c13711b29c 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,27 +1,34 @@ use channel::channel_buffer::ChannelBuffer; +use client::proto; use clock::ReplicaId; use collections::HashMap; use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, - AnyElement, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, View, + ViewContext, ViewHandle, }; use language::Language; use project::Project; use std::sync::Arc; -use workspace::item::{Item, ItemHandle}; +use workspace::{ + item::{FollowableItem, Item, ItemHandle}, + register_followable_item, ViewId, +}; actions!(channel_view, [Deploy]); pub(crate) fn init(cx: &mut AppContext) { - // TODO + register_followable_item::(cx) } pub struct ChannelView { editor: ViewHandle, project: ModelHandle, channel_buffer: ModelHandle, + remote_id: Option, + _editor_event_subscription: Subscription, } impl ChannelView { @@ -34,14 +41,19 @@ impl ChannelView { let buffer = channel_buffer.read(cx).buffer(); buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); + let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + let this = Self { editor, project, channel_buffer, + remote_id: None, + _editor_event_subscription, }; let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); this.editor .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); + this } @@ -82,9 +94,15 @@ impl View for ChannelView { "ChannelView" } - fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> AnyElement { + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { ChildView::new(self.editor.as_any(), cx).into_any() } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(self.editor.as_any()) + } + } } impl Item for ChannelView { @@ -104,3 +122,85 @@ impl Item for ChannelView { Label::new(channel_name, style.label.to_owned()).into_any() } } + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &AppContext) -> Option { + self.channel_buffer.read(cx).channel(cx).map(|channel| { + proto::view::Variant::ChannelView(proto::view::ChannelView { + channel_id: channel.id, + }) + }) + } + + fn from_state_proto( + _: ViewHandle, + workspace: ViewHandle, + remote_id: workspace::ViewId, + state_proto: &mut Option, + cx: &mut AppContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state_proto else { return None }; + let Some(proto::view::Variant::ChannelView(state)) = state_proto.take() else { unreachable!() }; + + let channel_store = &workspace.read(cx).app_state().channel_store.clone(); + let open_channel_buffer = channel_store.update(cx, |store, cx| { + store.open_channel_buffer(state.channel_id, cx) + }); + let project = workspace.read(cx).project().to_owned(); + let language = workspace.read(cx).app_state().languages.clone(); + let get_markdown = language.language_for_name("Markdown"); + + Some(cx.spawn(|mut cx| async move { + let channel_buffer = open_channel_buffer.await?; + let markdown = get_markdown.await?; + + let this = workspace + .update(&mut cx, move |_, cx| { + cx.add_view(|cx| { + let mut this = Self::new(project, channel_buffer, Some(markdown), cx); + this.remote_id = Some(remote_id); + this + }) + }) + .ok_or_else(|| anyhow::anyhow!("workspace droppped"))?; + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + _: &Self::Event, + _: &mut Option, + _: &AppContext, + ) -> bool { + false + } + + fn apply_update_proto( + &mut self, + _: &ModelHandle, + _: proto::update_view::Variant, + _: &mut ViewContext, + ) -> gpui::Task> { + gpui::Task::ready(Ok(())) + } + + fn set_leader_replica_id( + &mut self, + leader_replica_id: Option, + cx: &mut ViewContext, + ) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_replica_id(leader_replica_id, cx) + }) + } + + fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { + Editor::should_unfollow_on_event(event, cx) + } +} diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e031edf538..a2a561402f 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -6384,7 +6384,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, @@ -6479,7 +6479,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { .update(|cx| { Editor::from_state_proto( pane.clone(), - project.clone(), + workspace.clone(), ViewId { creator: Default::default(), id: 0, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 668ea48203..657aae5ff9 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -49,11 +49,12 @@ impl FollowableItem for Editor { fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + workspace: ViewHandle, remote_id: ViewId, state: &mut Option, cx: &mut AppContext, ) -> Option>>> { + let project = workspace.read(cx).project().to_owned(); let Some(proto::view::Variant::Editor(_)) = state else { return None }; let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index b97feff06b..827468b280 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1120,6 +1120,7 @@ message View { oneof variant { Editor editor = 3; + ChannelView channel_view = 4; } message Editor { @@ -1132,6 +1133,10 @@ message View { float scroll_x = 7; float scroll_y = 8; } + + message ChannelView { + uint64 channel_id = 1; + } } message Collaborator { diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a115e0f473..4b5b7a7931 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -674,7 +674,7 @@ pub trait FollowableItem: Item { fn to_state_proto(&self, cx: &AppContext) -> Option; fn from_state_proto( pane: ViewHandle, - project: ModelHandle, + project: ViewHandle, id: ViewId, state: &mut Option, cx: &mut AppContext, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index a8354472aa..62bb7a82a2 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -345,7 +345,7 @@ pub fn register_project_item(cx: &mut AppContext) { type FollowableItemBuilder = fn( ViewHandle, - ModelHandle, + ViewHandle, ViewId, &mut Option, &mut AppContext, @@ -362,8 +362,8 @@ pub fn register_followable_item(cx: &mut AppContext) { builders.insert( TypeId::of::(), ( - |pane, project, id, state, cx| { - I::from_state_proto(pane, project, id, state, cx).map(|task| { + |pane, workspace, id, state, cx| { + I::from_state_proto(pane, workspace, id, state, cx).map(|task| { cx.foreground() .spawn(async move { Ok(Box::new(task.await?) as Box<_>) }) }) @@ -2848,7 +2848,13 @@ impl Workspace { views: Vec, cx: &mut AsyncAppContext, ) -> Result<()> { - let project = this.read_with(cx, |this, _| this.project.clone())?; + let this = this + .upgrade(cx) + .ok_or_else(|| anyhow!("workspace dropped"))?; + let project = this + .read_with(cx, |this, _| this.project.clone()) + .ok_or_else(|| anyhow!("window dropped"))?; + let replica_id = project .read_with(cx, |project, _| { project @@ -2874,12 +2880,11 @@ impl Workspace { let id = ViewId::from_proto(id.clone())?; let mut variant = view.variant.clone(); if variant.is_none() { - Err(anyhow!("missing variant"))?; + Err(anyhow!("missing view variant"))?; } for build_item in &item_builders { - let task = cx.update(|cx| { - build_item(pane.clone(), project.clone(), id, &mut variant, cx) - }); + let task = cx + .update(|cx| build_item(pane.clone(), this.clone(), id, &mut variant, cx)); if let Some(task) = task { item_tasks.push(task); leader_view_ids.push(id); @@ -2907,7 +2912,7 @@ impl Workspace { } Some(()) - })?; + }); } Ok(()) } From 3268cce41a948ea422f0fe0389cbd8d2e260c047 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Aug 2023 11:30:43 -0700 Subject: [PATCH 043/142] Fix error in update_channel_buffer when there are no operations to store Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index 83f5b87079..a38693bace 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -215,7 +215,7 @@ impl Database { user: UserId, operations: &[proto::Operation], ) -> Result> { - self.transaction(|tx| async move { + self.transaction(move |tx| async move { self.check_user_is_channel_member(channel_id, user, &*tx) .await?; @@ -240,11 +240,15 @@ impl Database { .await? .ok_or_else(|| anyhow!("missing buffer snapshot"))?; - buffer_operation::Entity::insert_many(operations.iter().filter_map(|operation| { - operation_to_storage(operation, &buffer, serialization_version) - })) - .exec(&*tx) - .await?; + let operations = operations + .iter() + .filter_map(|op| operation_to_storage(op, &buffer, serialization_version)) + .collect::>(); + if !operations.is_empty() { + buffer_operation::Entity::insert_many(operations) + .exec(&*tx) + .await?; + } let mut connections = Vec::new(); let mut rows = channel_buffer_collaborator::Entity::find() From 24141c2f16261c25ebedfa01414ce4b2a62055c0 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 23 Aug 2023 13:32:16 -0700 Subject: [PATCH 044/142] Ensure collaborators cursor colors are the same in channel buffers as in projects Co-authored-by: Mikayla --- crates/channel/src/channel_buffer.rs | 13 ++- .../collab/src/tests/channel_buffer_tests.rs | 59 ++++++++++-- crates/collab_ui/src/channel_view.rs | 96 +++++++++++++------ crates/editor/src/editor.rs | 10 +- crates/editor/src/element.rs | 50 ++++++---- crates/language/src/buffer.rs | 8 ++ crates/project/src/project.rs | 2 + crates/sum_tree/src/tree_map.rs | 12 ++- crates/theme/src/theme.rs | 1 + styles/src/style_tree/editor.ts | 1 + 10 files changed, 190 insertions(+), 62 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index 5ee3fd6c84..cad3c4f58f 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -21,8 +21,12 @@ pub struct ChannelBuffer { _subscription: client::Subscription, } +pub enum Event { + CollaboratorsChanged, +} + impl Entity for ChannelBuffer { - type Event = (); + type Event = Event; fn release(&mut self, _: &mut AppContext) { self.client @@ -54,8 +58,9 @@ impl ChannelBuffer { let collaborators = response.collaborators; - let buffer = - cx.add_model(|cx| language::Buffer::new(response.replica_id as u16, base_text, cx)); + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; let subscription = client.subscribe_to_entity(channel_id)?; @@ -111,6 +116,7 @@ impl ChannelBuffer { this.update(&mut cx, |this, cx| { this.collaborators.push(collaborator); + cx.emit(Event::CollaboratorsChanged); cx.notify(); }); @@ -134,6 +140,7 @@ impl ChannelBuffer { true } }); + cx.emit(Event::CollaboratorsChanged); cx.notify(); }); diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 8fb50055f5..6a9ef3fc13 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -63,6 +63,10 @@ async fn test_core_channel_buffers( // Client B sees the correct text, and then edits it let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); buffer_b.update(cx_b, |buffer, cx| { buffer.edit([(7..12, "beautiful")], None, cx) @@ -138,6 +142,7 @@ async fn test_channel_buffer_replica_ids( let active_call_a = cx_a.read(ActiveCall::global); let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); // Clients A and B join a channel. active_call_a @@ -190,7 +195,7 @@ async fn test_channel_buffer_replica_ids( // Client C is in a separate project. client_c.fs().insert_tree("/dir", json!({})).await; - let (project_c, _) = client_c.build_local_project("/dir", cx_c).await; + let (separate_project_c, _) = client_c.build_local_project("/dir", cx_c).await; // Note that each user has a different replica id in the projects vs the // channel buffer. @@ -211,8 +216,14 @@ async fn test_channel_buffer_replica_ids( .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); let channel_window_b = cx_b .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); - let channel_window_c = cx_c - .add_window(|cx| ChannelView::new(project_c.clone(), channel_buffer_c.clone(), None, cx)); + let channel_window_c = cx_c.add_window(|cx| { + ChannelView::new( + separate_project_c.clone(), + channel_buffer_c.clone(), + None, + cx, + ) + }); let channel_view_a = channel_window_a.root(cx_a); let channel_view_b = channel_window_b.root(cx_b); @@ -222,24 +233,54 @@ async fn test_channel_buffer_replica_ids( // so that they match the same users' replica ids in their shared project. channel_view_a.read_with(cx_a, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(1, 0), (2, 1)].into_iter().collect::>() + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>() ); }); channel_view_b.read_with(cx_b, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(1, 0), (2, 1)].into_iter().collect::>(), + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1)].into_iter().collect::>(), ) }); // Client C only sees themself, as they're not part of any shared project channel_view_c.read_with(cx_c, |view, cx| { assert_eq!( - view.project_replica_ids_by_channel_buffer_replica_id(cx), - [(0, 0)].into_iter().collect::>(), + view.editor.read(cx).replica_id_map().unwrap(), + &[(0, 0)].into_iter().collect::>(), ); }); + + // Client C joins the project that clients A and B are in. + active_call_c + .update(cx_c, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + let project_c = client_c.build_remote_project(shared_project_id, cx_c).await; + deterministic.run_until_parked(); + project_c.read_with(cx_c, |project, _| { + assert_eq!(project.replica_id(), 2); + }); + + // For clients A and B, client C's replica id in the channel buffer is + // now mapped to their replica id in the shared project. + channel_view_a.read_with(cx_a, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>() + ); + }); + channel_view_b.read_with(cx_b, |view, cx| { + assert_eq!( + view.editor.read(cx).replica_id_map().unwrap(), + &[(1, 0), (2, 1), (0, 2)] + .into_iter() + .collect::>(), + ) + }); } #[track_caller] diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index c13711b29c..dd3969d351 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,4 @@ -use channel::channel_buffer::ChannelBuffer; +use channel::channel_buffer::{self, ChannelBuffer}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -24,7 +24,7 @@ pub(crate) fn init(cx: &mut AppContext) { } pub struct ChannelView { - editor: ViewHandle, + pub editor: ViewHandle, project: ModelHandle, channel_buffer: ModelHandle, remote_id: Option, @@ -43,6 +43,10 @@ impl ChannelView { let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); + cx.subscribe(&project, Self::handle_project_event).detach(); + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + let this = Self { editor, project, @@ -50,38 +54,70 @@ impl ChannelView { remote_id: None, _editor_event_subscription, }; - let mapping = this.project_replica_ids_by_channel_buffer_replica_id(cx); - this.editor - .update(cx, |editor, cx| editor.set_replica_id_mapping(mapping, cx)); - + this.refresh_replica_id_map(cx); this } - /// Channel Buffer Replica ID -> Project Replica ID - pub fn project_replica_ids_by_channel_buffer_replica_id( - &self, - cx: &AppContext, - ) -> HashMap { - let project = self.project.read(cx); - let mut result = HashMap::default(); - result.insert( - self.channel_buffer.read(cx).replica_id(cx), - project.replica_id(), - ); - for collaborator in self.channel_buffer.read(cx).collaborators() { - let project_replica_id = - project - .collaborators() - .values() - .find_map(|project_collaborator| { - (project_collaborator.user_id == collaborator.user_id) - .then_some(project_collaborator.replica_id) - }); - if let Some(project_replica_id) = project_replica_id { - result.insert(collaborator.replica_id as ReplicaId, project_replica_id); - } + fn handle_project_event( + &mut self, + _: ModelHandle, + event: &project::Event, + cx: &mut ViewContext, + ) { + match event { + project::Event::RemoteIdChanged(_) => {} + project::Event::DisconnectedFromHost => {} + project::Event::Closed => {} + project::Event::CollaboratorUpdated { .. } => {} + project::Event::CollaboratorLeft(_) => {} + project::Event::CollaboratorJoined(_) => {} + _ => return, } - result + self.refresh_replica_id_map(cx); + } + + fn handle_channel_buffer_event( + &mut self, + _: ModelHandle, + _: &channel_buffer::Event, + cx: &mut ViewContext, + ) { + self.refresh_replica_id_map(cx); + } + + /// Build a mapping of channel buffer replica ids to the corresponding + /// replica ids in the current project. + /// + /// Using this mapping, a given user can be displayed with the same color + /// in the channel buffer as in other files in the project. Users who are + /// in the channel buffer but not the project will not have a color. + fn refresh_replica_id_map(&self, cx: &mut ViewContext) { + let mut project_replica_ids_by_channel_buffer_replica_id = HashMap::default(); + let project = self.project.read(cx); + let channel_buffer = self.channel_buffer.read(cx); + project_replica_ids_by_channel_buffer_replica_id + .insert(channel_buffer.replica_id(cx), project.replica_id()); + project_replica_ids_by_channel_buffer_replica_id.extend( + channel_buffer + .collaborators() + .iter() + .filter_map(|channel_buffer_collaborator| { + project + .collaborators() + .values() + .find_map(|project_collaborator| { + (project_collaborator.user_id == channel_buffer_collaborator.user_id) + .then_some(( + channel_buffer_collaborator.replica_id as ReplicaId, + project_collaborator.replica_id, + )) + }) + }), + ); + + self.editor.update(cx, |editor, cx| { + editor.set_replica_id_map(Some(project_replica_ids_by_channel_buffer_replica_id), cx) + }); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index e7197d98c5..775f3c07ec 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1606,12 +1606,16 @@ impl Editor { self.read_only = read_only; } - pub fn set_replica_id_mapping( + pub fn replica_id_map(&self) -> Option<&HashMap> { + self.replica_id_mapping.as_ref() + } + + pub fn set_replica_id_map( &mut self, - mapping: HashMap, + mapping: Option>, cx: &mut ViewContext, ) { - self.replica_id_mapping = Some(mapping); + self.replica_id_mapping = mapping; cx.notify(); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0f26e5819c..9f74eed790 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -62,6 +62,7 @@ struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, is_newest: bool, + is_local: bool, range: Range, active_rows: Range, } @@ -73,6 +74,7 @@ impl SelectionLayout { cursor_shape: CursorShape, map: &DisplaySnapshot, is_newest: bool, + is_local: bool, ) -> Self { let point_selection = selection.map(|p| p.to_point(&map.buffer_snapshot)); let display_selection = point_selection.map(|p| p.to_display_point(map)); @@ -109,6 +111,7 @@ impl SelectionLayout { head, cursor_shape, is_newest, + is_local, range, active_rows, } @@ -763,7 +766,6 @@ impl EditorElement { cx: &mut PaintContext, ) { let style = &self.style; - let local_replica_id = editor.replica_id(cx); let scroll_position = layout.position_map.snapshot.scroll_position(); let start_row = layout.visible_display_row_range.start; let scroll_top = scroll_position.y() * layout.position_map.line_height; @@ -852,15 +854,13 @@ impl EditorElement { for (replica_id, selections) in &layout.selections { let replica_id = *replica_id; - let selection_style = style.replica_selection_style(replica_id); + let selection_style = if let Some(replica_id) = replica_id { + style.replica_selection_style(replica_id) + } else { + &style.absent_selection + }; for selection in selections { - if !selection.range.is_empty() - && (replica_id == local_replica_id - || Some(replica_id) == editor.leader_replica_id) - { - invisible_display_ranges.push(selection.range.clone()); - } self.paint_highlighted_range( scene, selection.range.clone(), @@ -874,7 +874,10 @@ impl EditorElement { bounds, ); - if editor.show_local_cursors(cx) || replica_id != local_replica_id { + if selection.is_local && !selection.range.is_empty() { + invisible_display_ranges.push(selection.range.clone()); + } + if !selection.is_local || editor.show_local_cursors(cx) { let cursor_position = selection.head; if layout .visible_display_row_range @@ -2124,7 +2127,7 @@ impl Element for EditorElement { .anchor_before(DisplayPoint::new(end_row, 0).to_offset(&snapshot, Bias::Right)) }; - let mut selections: Vec<(ReplicaId, Vec)> = Vec::new(); + let mut selections: Vec<(Option, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut fold_ranges = Vec::new(); let is_singleton = editor.is_singleton(cx); @@ -2155,8 +2158,14 @@ impl Element for EditorElement { .buffer_snapshot .remote_selections_in_range(&(start_anchor..end_anchor)) { + let replica_id = if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied() + } else { + None + }; + // The local selections match the leader's selections. - if Some(replica_id) == editor.leader_replica_id { + if replica_id.is_some() && replica_id == editor.leader_replica_id { continue; } remote_selections @@ -2168,6 +2177,7 @@ impl Element for EditorElement { cursor_shape, &snapshot.display_snapshot, false, + false, )); } selections.extend(remote_selections); @@ -2191,6 +2201,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, is_newest, + true, ); if is_newest { newest_selection_head = Some(layout.head); @@ -2206,11 +2217,18 @@ impl Element for EditorElement { } // Render the local selections in the leader's color when following. - let local_replica_id = editor - .leader_replica_id - .unwrap_or_else(|| editor.replica_id(cx)); + let local_replica_id = if let Some(leader_replica_id) = editor.leader_replica_id { + leader_replica_id + } else { + let replica_id = editor.replica_id(cx); + if let Some(mapping) = &editor.replica_id_mapping { + mapping.get(&replica_id).copied().unwrap_or(replica_id) + } else { + replica_id + } + }; - selections.push((local_replica_id, layouts)); + selections.push((Some(local_replica_id), layouts)); } let scrollbar_settings = &settings::get::(cx).scrollbar; @@ -2591,7 +2609,7 @@ pub struct LayoutState { blocks: Vec, highlighted_ranges: Vec<(Range, Color)>, fold_ranges: Vec<(BufferRow, Range, Color)>, - selections: Vec<(ReplicaId, Vec)>, + selections: Vec<(Option, Vec)>, scrollbar_row_range: Range, show_scrollbars: bool, is_singleton: bool, diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d032e8e025..1b83ca5964 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -359,6 +359,14 @@ impl Buffer { ) } + pub fn remote(remote_id: u64, replica_id: ReplicaId, base_text: String) -> Self { + Self::build( + TextBuffer::new(replica_id, remote_id, base_text), + None, + None, + ) + } + pub fn from_proto( replica_id: ReplicaId, message: proto::BufferState, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bc4fa587cb..49074268f2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -282,6 +282,7 @@ pub enum Event { old_peer_id: proto::PeerId, new_peer_id: proto::PeerId, }, + CollaboratorJoined(proto::PeerId), CollaboratorLeft(proto::PeerId), RefreshInlayHints, } @@ -5931,6 +5932,7 @@ impl Project { let collaborator = Collaborator::from_proto(collaborator)?; this.update(&mut cx, |this, cx| { this.shared_buffers.remove(&collaborator.peer_id); + cx.emit(Event::CollaboratorJoined(collaborator.peer_id)); this.collaborators .insert(collaborator.peer_id, collaborator); cx.notify(); diff --git a/crates/sum_tree/src/tree_map.rs b/crates/sum_tree/src/tree_map.rs index 4bb98d2ac8..edb9010e50 100644 --- a/crates/sum_tree/src/tree_map.rs +++ b/crates/sum_tree/src/tree_map.rs @@ -2,7 +2,7 @@ use std::{cmp::Ordering, fmt::Debug}; use crate::{Bias, Dimension, Edit, Item, KeyedItem, SeekTarget, SumTree, Summary}; -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, PartialEq, Eq)] pub struct TreeMap(SumTree>) where K: Clone + Debug + Default + Ord, @@ -162,6 +162,16 @@ impl TreeMap { } } +impl Debug for TreeMap +where + K: Clone + Debug + Default + Ord, + V: Clone + Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_map().entries(self.iter()).finish() + } +} + #[derive(Debug)] struct MapSeekTargetAdaptor<'a, T>(&'a T); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 0f34963708..9005fc9757 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -756,6 +756,7 @@ pub struct Editor { pub line_number: Color, pub line_number_active: Color, pub guest_selections: Vec, + pub absent_selection: SelectionStyle, pub syntax: Arc, pub hint: HighlightStyle, pub suggestion: HighlightStyle, diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 9ad008f38d..9277a2e7a1 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -184,6 +184,7 @@ export default function editor(): any { theme.players[6], theme.players[7], ], + absent_selection: theme.players[7], autocomplete: { background: background(theme.middle), corner_radius: 8, From 90f22cb0d24abb5d73dc6801990e5ce741e6a18d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 12:36:01 -0700 Subject: [PATCH 045/142] Replicate editor state when following into channel notes Co-authored-by: Mikayla --- crates/collab_ui/src/channel_view.rs | 57 ++++++++++++++++++++++------ crates/rpc/proto/zed.proto | 1 + 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index dd3969d351..be186fc2e2 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -168,6 +168,13 @@ impl FollowableItem for ChannelView { self.channel_buffer.read(cx).channel(cx).map(|channel| { proto::view::Variant::ChannelView(proto::view::ChannelView { channel_id: channel.id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, }) }) } @@ -176,11 +183,11 @@ impl FollowableItem for ChannelView { _: ViewHandle, workspace: ViewHandle, remote_id: workspace::ViewId, - state_proto: &mut Option, + state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state_proto else { return None }; - let Some(proto::view::Variant::ChannelView(state)) = state_proto.take() else { unreachable!() }; + let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; let channel_store = &workspace.read(cx).app_state().channel_store.clone(); let open_channel_buffer = channel_store.update(cx, |store, cx| { @@ -202,7 +209,29 @@ impl FollowableItem for ChannelView { this }) }) - .ok_or_else(|| anyhow::anyhow!("workspace droppped"))?; + .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; + + if let Some(state) = state.editor { + let task = this.update(&mut cx, |this, cx| { + this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + }) + }); + if let Some(task) = task { + task.await?; + } + } Ok(this) })) @@ -210,20 +239,24 @@ impl FollowableItem for ChannelView { fn add_event_to_update_proto( &self, - _: &Self::Event, - _: &mut Option, - _: &AppContext, + event: &Self::Event, + update: &mut Option, + cx: &AppContext, ) -> bool { - false + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) } fn apply_update_proto( &mut self, - _: &ModelHandle, - _: proto::update_view::Variant, - _: &mut ViewContext, + project: &ModelHandle, + message: proto::update_view::Variant, + cx: &mut ViewContext, ) -> gpui::Task> { - gpui::Task::ready(Ok(())) + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) } fn set_leader_replica_id( diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 827468b280..f032ccce51 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1136,6 +1136,7 @@ message View { message ChannelView { uint64 channel_id = 1; + Editor editor = 2; } } From 5888e7b214685aa1d8dd24e657d84c7b015aa08f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 13:40:44 -0700 Subject: [PATCH 046/142] Dedup channel buffers --- crates/channel/src/channel_buffer.rs | 62 +++++++------- crates/channel/src/channel_store.rs | 81 +++++++++++++++---- .../collab/src/tests/channel_buffer_tests.rs | 56 +++++++++++++ 3 files changed, 152 insertions(+), 47 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index cad3c4f58f..c19899501a 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,7 +1,7 @@ use crate::{Channel, ChannelId, ChannelStore}; use anyhow::Result; use client::Client; -use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; @@ -38,46 +38,44 @@ impl Entity for ChannelBuffer { } impl ChannelBuffer { - pub(crate) fn new( + pub(crate) async fn new( channel_store: ModelHandle, channel_id: ChannelId, client: Arc, - cx: &mut AppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let response = client - .request(proto::JoinChannelBuffer { channel_id }) - .await?; + mut cx: AsyncAppContext, + ) -> Result> { + let response = client + .request(proto::JoinChannelBuffer { channel_id }) + .await?; - let base_text = response.base_text; - let operations = response - .operations - .into_iter() - .map(language::proto::deserialize_operation) - .collect::, _>>()?; + let base_text = response.base_text; + let operations = response + .operations + .into_iter() + .map(language::proto::deserialize_operation) + .collect::, _>>()?; - let collaborators = response.collaborators; + let collaborators = response.collaborators; - let buffer = cx.add_model(|_| { - language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) - }); - buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; + let buffer = cx.add_model(|_| { + language::Buffer::remote(response.buffer_id, response.replica_id as u16, base_text) + }); + buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; - let subscription = client.subscribe_to_entity(channel_id)?; + let subscription = client.subscribe_to_entity(channel_id)?; - anyhow::Ok(cx.add_model(|cx| { - cx.subscribe(&buffer, Self::on_buffer_update).detach(); + anyhow::Ok(cx.add_model(|cx| { + cx.subscribe(&buffer, Self::on_buffer_update).detach(); - Self { - buffer, - client, - channel_id, - channel_store, - collaborators, - _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), - } - })) - }) + Self { + buffer, + client, + channel_id, + channel_store, + collaborators, + _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + } + })) } async fn handle_update_channel_buffer( diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index a6aad19d03..1d83bd1d7f 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -1,20 +1,13 @@ -use anyhow::anyhow; -use anyhow::Result; -use client::Status; -use client::UserId; -use client::{Client, Subscription, User, UserStore}; -use collections::HashMap; -use collections::HashSet; -use futures::channel::mpsc; -use futures::Future; -use futures::StreamExt; -use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task}; +use crate::channel_buffer::ChannelBuffer; +use anyhow::{anyhow, Result}; +use client::{Client, Status, Subscription, User, UserId, UserStore}; +use collections::{hash_map, HashMap, HashSet}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; +use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; use util::ResultExt; -use crate::channel_buffer::ChannelBuffer; - pub type ChannelId = u64; pub struct ChannelStore { @@ -25,6 +18,7 @@ pub struct ChannelStore { channels_with_admin_privileges: HashSet, outgoing_invites: HashSet<(ChannelId, UserId)>, update_channels_tx: mpsc::UnboundedSender, + opened_buffers: HashMap, client: Arc, user_store: ModelHandle, _rpc_subscription: Subscription, @@ -59,6 +53,11 @@ pub enum ChannelMemberStatus { NotMember, } +enum OpenedChannelBuffer { + Open(WeakModelHandle), + Loading(Shared, Arc>>>), +} + impl ChannelStore { pub fn new( client: Arc, @@ -89,6 +88,7 @@ impl ChannelStore { } } }); + Self { channels_by_id: HashMap::default(), channel_invitations: Vec::default(), @@ -96,6 +96,7 @@ impl ChannelStore { channel_participants: Default::default(), channels_with_admin_privileges: Default::default(), outgoing_invites: Default::default(), + opened_buffers: Default::default(), update_channels_tx, client, user_store, @@ -154,11 +155,61 @@ impl ChannelStore { } pub fn open_channel_buffer( - &self, + &mut self, channel_id: ChannelId, cx: &mut ModelContext, ) -> Task>> { - ChannelBuffer::new(cx.handle(), channel_id, self.client.clone(), cx) + // Make sure that a given channel buffer is only opened once per + // app instance, even if this method is called multiple times + // with the same channel id while the first task is still running. + let task = loop { + match self.opened_buffers.entry(channel_id) { + hash_map::Entry::Occupied(e) => match e.get() { + OpenedChannelBuffer::Open(buffer) => { + if let Some(buffer) = buffer.upgrade(cx) { + break Task::ready(Ok(buffer)).shared(); + } else { + self.opened_buffers.remove(&channel_id); + continue; + } + } + OpenedChannelBuffer::Loading(task) => break task.clone(), + }, + hash_map::Entry::Vacant(e) => { + let task = cx + .spawn(|this, cx| { + ChannelBuffer::new(this, channel_id, self.client.clone(), cx) + .map_err(Arc::new) + }) + .shared(); + e.insert(OpenedChannelBuffer::Loading(task.clone())); + cx.spawn({ + let task = task.clone(); + |this, mut cx| async move { + let result = task.await; + this.update(&mut cx, |this, cx| { + if let Ok(buffer) = result { + cx.observe_release(&buffer, move |this, _, _| { + this.opened_buffers.remove(&channel_id); + }) + .detach(); + this.opened_buffers.insert( + channel_id, + OpenedChannelBuffer::Open(buffer.downgrade()), + ); + } else { + this.opened_buffers.remove(&channel_id); + } + }); + } + }) + .detach(); + break task; + } + } + }; + cx.foreground() + .spawn(async move { task.await.map_err(|error| anyhow!("{}", error)) }) } pub fn is_user_admin(&self, channel_id: ChannelId) -> bool { diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 6a9ef3fc13..f7e5751a37 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -3,6 +3,7 @@ use call::ActiveCall; use client::UserId; use collab_ui::channel_view::ChannelView; use collections::HashMap; +use futures::future; use gpui::{executor::Deterministic, ModelHandle, TestAppContext}; use rpc::{proto, RECEIVE_TIMEOUT}; use serde_json::json; @@ -283,6 +284,61 @@ async fn test_channel_buffer_replica_ids( }); } +#[gpui::test] +async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mut TestAppContext) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let zed_id = server.make_channel("zed", (&client_a, cx_a), &mut []).await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer_1, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let model_id = channel_buffer_1.id(); + assert_eq!(channel_buffer_1, channel_buffer_2); + assert_eq!(channel_buffer_1, channel_buffer_3); + + channel_buffer_1.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer_1); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |channel, cx| channel.open_channel_buffer(zed_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.id(), model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( From 1ae54ca62099b5eb9f762ca1f70bc5b179880481 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 14:29:04 -0700 Subject: [PATCH 047/142] Dedup channel views Co-authored-by: Mikayla --- .../collab/src/tests/channel_buffer_tests.rs | 15 +-- crates/collab_ui/src/channel_view.rs | 123 +++++++++++------- crates/collab_ui/src/collab_panel.rs | 42 ++---- 3 files changed, 94 insertions(+), 86 deletions(-) diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index f7e5751a37..0ecd4588c5 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -213,17 +213,12 @@ async fn test_channel_buffer_replica_ids( assert_eq!(channel_buffer.buffer().read(cx).replica_id(), 0); }); - let channel_window_a = cx_a - .add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), None, cx)); - let channel_window_b = cx_b - .add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), None, cx)); + let channel_window_a = + cx_a.add_window(|cx| ChannelView::new(project_a.clone(), channel_buffer_a.clone(), cx)); + let channel_window_b = + cx_b.add_window(|cx| ChannelView::new(project_b.clone(), channel_buffer_b.clone(), cx)); let channel_window_c = cx_c.add_window(|cx| { - ChannelView::new( - separate_project_c.clone(), - channel_buffer_c.clone(), - None, - cx, - ) + ChannelView::new(separate_project_c.clone(), channel_buffer_c.clone(), cx) }); let channel_view_a = channel_window_a.root(cx_a); diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index be186fc2e2..0e2d3636aa 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -1,4 +1,8 @@ -use channel::channel_buffer::{self, ChannelBuffer}; +use anyhow::{anyhow, Result}; +use channel::{ + channel_buffer::{self, ChannelBuffer}, + ChannelId, +}; use client::proto; use clock::ReplicaId; use collections::HashMap; @@ -6,15 +10,13 @@ use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, - AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, View, + AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, }; -use language::Language; use project::Project; -use std::sync::Arc; use workspace::{ item::{FollowableItem, Item, ItemHandle}, - register_followable_item, ViewId, + register_followable_item, Pane, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); @@ -32,14 +34,47 @@ pub struct ChannelView { } impl ChannelView { + pub fn open( + channel_id: ChannelId, + pane: ViewHandle, + workspace: ViewHandle, + cx: &mut AppContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = workspace.app_state().channel_store.clone(); + let markdown = workspace + .app_state() + .languages + .language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await?; + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language(Some(markdown), cx); + }) + }); + + pane.update(&mut cx, |pane, cx| { + pane.items_of_type::() + .find(|channel_view| channel_view.read(cx).channel_buffer == channel_buffer) + .unwrap_or_else(|| cx.add_view(|cx| Self::new(project, channel_buffer, cx))) + }) + .ok_or_else(|| anyhow!("pane was dropped")) + }) + } + pub fn new( project: ModelHandle, channel_buffer: ModelHandle, - language: Option>, cx: &mut ViewContext, ) -> Self { let buffer = channel_buffer.read(cx).buffer(); - buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); + // buffer.update(cx, |buffer, cx| buffer.set_language(language, cx)); let editor = cx.add_view(|cx| Editor::for_buffer(buffer, None, cx)); let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); @@ -157,6 +192,14 @@ impl Item for ChannelView { }); Label::new(channel_name, style.label.to_owned()).into_any() } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { + Some(Self::new( + self.project.clone(), + self.channel_buffer.clone(), + cx, + )) + } } impl FollowableItem for ChannelView { @@ -180,7 +223,7 @@ impl FollowableItem for ChannelView { } fn from_state_proto( - _: ViewHandle, + pane: ViewHandle, workspace: ViewHandle, remote_id: workspace::ViewId, state: &mut Option, @@ -189,48 +232,38 @@ impl FollowableItem for ChannelView { let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; - let channel_store = &workspace.read(cx).app_state().channel_store.clone(); - let open_channel_buffer = channel_store.update(cx, |store, cx| { - store.open_channel_buffer(state.channel_id, cx) - }); - let project = workspace.read(cx).project().to_owned(); - let language = workspace.read(cx).app_state().languages.clone(); - let get_markdown = language.language_for_name("Markdown"); + let open = ChannelView::open(state.channel_id, pane, workspace, cx); Some(cx.spawn(|mut cx| async move { - let channel_buffer = open_channel_buffer.await?; - let markdown = get_markdown.await?; + let this = open.await?; - let this = workspace - .update(&mut cx, move |_, cx| { - cx.add_view(|cx| { - let mut this = Self::new(project, channel_buffer, Some(markdown), cx); - this.remote_id = Some(remote_id); - this - }) + let task = this + .update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } }) - .ok_or_else(|| anyhow::anyhow!("workspace dropped"))?; + .ok_or_else(|| anyhow!("window was closed"))?; - if let Some(state) = state.editor { - let task = this.update(&mut cx, |this, cx| { - this.editor.update(cx, |editor, cx| { - editor.apply_update_proto( - &this.project, - proto::update_view::Variant::Editor(proto::update_view::Editor { - selections: state.selections, - pending_selection: state.pending_selection, - scroll_top_anchor: state.scroll_top_anchor, - scroll_x: state.scroll_x, - scroll_y: state.scroll_y, - ..Default::default() - }), - cx, - ) - }) - }); - if let Some(task) = task { - task.await?; - } + if let Some(task) = task { + task.await?; } Ok(this) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index a6bd09e43b..5bdc6ad6b7 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -2220,38 +2220,18 @@ impl CollabPanel { } fn open_channel_buffer(&mut self, action: &OpenChannelBuffer, cx: &mut ViewContext) { - let workspace = self.workspace; - let open = self.channel_store.update(cx, |channel_store, cx| { - channel_store.open_channel_buffer(action.channel_id, cx) - }); - - cx.spawn(|_, mut cx| async move { - let channel_buffer = open.await?; - - let markdown = workspace - .read_with(&cx, |workspace, _| { - workspace - .app_state() - .languages - .language_for_name("Markdown") - })? - .await?; - - workspace.update(&mut cx, |workspace, cx| { - let channel_view = cx.add_view(|cx| { - ChannelView::new( - workspace.project().to_owned(), - channel_buffer, - Some(markdown), - cx, - ) + if let Some(workspace) = self.workspace.upgrade(cx) { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = ChannelView::open(action.channel_id, pane.clone(), workspace, cx); + cx.spawn(|_, mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + pane.add_item(Box::new(channel_view), true, true, None, cx) }); - workspace.add_item(Box::new(channel_view), cx); - })?; - - anyhow::Ok(()) - }) - .detach(); + anyhow::Ok(()) + }) + .detach(); + } } fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { From a327320f7dd317b40ea397d2cbc43fefc40dbc62 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 15:00:54 -0700 Subject: [PATCH 048/142] Show channel notes in current call section of collab panel Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 188 +++++++++++++++++++++------ 1 file changed, 146 insertions(+), 42 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 5bdc6ad6b7..dece04cb8b 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -18,13 +18,14 @@ use gpui::{ MouseEventHandler, Orientation, OverlayPositionMode, Padding, ParentElement, SafeStylable, Stack, Svg, }, + fonts::TextStyle, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, }, impl_actions, platform::{CursorStyle, MouseButton, PromptLevel}, - serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, FontCache, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use menu::{Confirm, SelectNext, SelectPrev}; @@ -183,6 +184,7 @@ pub struct CollabPanel { #[derive(Serialize, Deserialize)] struct SerializedChannelsPanel { width: Option, + collapsed_channels: Vec, } #[derive(Debug)] @@ -227,6 +229,9 @@ enum ListEntry { channel: Arc, depth: usize, }, + ChannelNotes { + channel_id: ChannelId, + }, ChannelEditor { depth: usize, }, @@ -370,6 +375,12 @@ impl CollabPanel { return channel_row; } } + ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( + *channel_id, + &theme.collab_panel, + is_selected, + cx, + ), ListEntry::ChannelInvite(channel) => Self::render_channel_invite( channel.clone(), this.channel_store.clone(), @@ -509,6 +520,7 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; + panel.collapsed_channels = serialized_panel.collapsed_channels; cx.notify(); }); } @@ -519,12 +531,16 @@ impl CollabPanel { fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; + let collapsed_channels = self.collapsed_channels.clone(); self.pending_serialization = cx.background().spawn( async move { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { width })?, + serde_json::to_string(&SerializedChannelsPanel { + width, + collapsed_channels, + })?, ) .await?; anyhow::Ok(()) @@ -548,6 +564,10 @@ impl CollabPanel { if !self.collapsed_sections.contains(&Section::ActiveCall) { let room = room.read(cx); + if let Some(channel_id) = room.channel_id() { + self.entries.push(ListEntry::ChannelNotes { channel_id }) + } + // Populate the active user. if let Some(user) = user_store.current_user() { self.match_candidates.clear(); @@ -1007,25 +1027,19 @@ impl CollabPanel { ) -> AnyElement { enum JoinProject {} - let font_cache = cx.font_cache(); - let host_avatar_height = theme + let host_avatar_width = theme .contact_avatar .width .or(theme.contact_avatar.height) .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; let project_name = if worktree_root_names.is_empty() { "untitled".to_string() } else { worktree_root_names.join(", ") }; - MouseEventHandler::new::(project_id as usize, cx, |mouse_state, _| { + MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); let row = theme .project_row @@ -1033,39 +1047,20 @@ impl CollabPanel { .style_for(mouse_state); Flex::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + is_last, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) .with_child( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radii: (0.).into(), - }); - })) + Svg::new("icons/file_icons/folder.svg") + .with_color(theme.channel_hash.color) .constrained() - .with_width(host_avatar_height), + .with_width(theme.channel_hash.width) + .aligned() + .left(), ) .with_child( Label::new(project_name, row.name.text.clone()) @@ -1240,7 +1235,7 @@ impl CollabPanel { }); if let Some(name) = channel_name { - Cow::Owned(format!("Current Call - #{}", name)) + Cow::Owned(format!("#{}", name)) } else { Cow::Borrowed("Current Call") } @@ -1676,6 +1671,61 @@ impl CollabPanel { .into_any() } + fn render_channel_notes( + &self, + channel_id: ChannelId, + theme: &theme::CollabPanel, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum ChannelNotes {} + let host_avatar_width = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + + MouseEventHandler::new::(channel_id as usize, cx, |state, cx| { + let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + let row = theme.project_row.in_state(is_selected).style_for(state); + + Flex::::row() + .with_child(render_tree_branch( + tree_branch, + &row.name.text, + true, + vec2f(host_avatar_width, theme.row_height), + cx.font_cache(), + )) + .with_child( + Svg::new("icons/radix/file.svg") + .with_color(theme.channel_hash.color) + .constrained() + .with_width(theme.channel_hash.width) + .aligned() + .left(), + ) + .with_child( + Label::new("notes", theme.channel_name.text.clone()) + .contained() + .with_style(theme.channel_name.container) + .aligned() + .left() + .flex(1., true), + ) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(*theme.channel_row.style_for(is_selected, state)) + .with_padding_left(theme.channel_row.default_style().padding.left) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + this.open_channel_buffer(&OpenChannelBuffer { channel_id }, cx); + }) + .with_cursor_style(CursorStyle::PointingHand) + .into_any() + } + fn render_channel_invite( channel: Arc, channel_store: ModelHandle, @@ -2114,6 +2164,7 @@ impl CollabPanel { self.collapsed_channels.insert(ix, channel_id); } }; + self.serialize(cx); self.update_entries(true, cx); cx.notify(); cx.focus_self(); @@ -2392,6 +2443,51 @@ impl CollabPanel { } } +fn render_tree_branch( + branch_style: theme::TreeBranch, + row_style: &TextStyle, + is_last: bool, + size: Vector2F, + font_cache: &FontCache, +) -> gpui::elements::ConstrainedBox { + let line_height = row_style.line_height(font_cache); + let cap_height = row_style.cap_height(font_cache); + let baseline_offset = row_style.baseline_offset(font_cache) + (size.y() - line_height) / 2.; + + Canvas::new(move |scene, bounds, _, _, _| { + scene.paint_layer(None, |scene| { + let start_x = bounds.min_x() + (bounds.width() / 2.) - (branch_style.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + branch_style.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + branch_style.width), + ), + background: Some(branch_style.color), + border: gpui::Border::default(), + corner_radii: (0.).into(), + }); + }) + }) + .constrained() + .with_width(size.x()) +} + impl View for CollabPanel { fn ui_name() -> &'static str { "CollabPanel" @@ -2601,6 +2697,14 @@ impl PartialEq for ListEntry { return channel_1.id == channel_2.id && depth_1 == depth_2; } } + ListEntry::ChannelNotes { channel_id } => { + if let ListEntry::ChannelNotes { + channel_id: other_id, + } = other + { + return channel_id == other_id; + } + } ListEntry::ChannelInvite(channel_1) => { if let ListEntry::ChannelInvite(channel_2) = other { return channel_1.id == channel_2.id; From 358a20494c177799474baf4c25ab666104c0bb19 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 16:50:13 -0700 Subject: [PATCH 049/142] Make channel notes read-only when disconnected Co-authored-by: Mikayla --- crates/channel/src/channel_buffer.rs | 58 +++++---- crates/channel/src/channel_store.rs | 115 ++++++++++++------ crates/collab/src/rpc.rs | 5 +- .../collab/src/tests/channel_buffer_tests.rs | 76 ++++++++++++ crates/collab/src/tests/channel_tests.rs | 7 +- crates/collab_ui/src/channel_view.rs | 36 +++--- 6 files changed, 217 insertions(+), 80 deletions(-) diff --git a/crates/channel/src/channel_buffer.rs b/crates/channel/src/channel_buffer.rs index c19899501a..29f4d3493c 100644 --- a/crates/channel/src/channel_buffer.rs +++ b/crates/channel/src/channel_buffer.rs @@ -1,4 +1,4 @@ -use crate::{Channel, ChannelId, ChannelStore}; +use crate::Channel; use anyhow::Result; use client::Client; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle}; @@ -13,39 +13,43 @@ pub(crate) fn init(client: &Arc) { } pub struct ChannelBuffer { - channel_id: ChannelId, + pub(crate) channel: Arc, + connected: bool, collaborators: Vec, buffer: ModelHandle, - channel_store: ModelHandle, client: Arc, - _subscription: client::Subscription, + subscription: Option, } pub enum Event { CollaboratorsChanged, + Disconnected, } impl Entity for ChannelBuffer { type Event = Event; fn release(&mut self, _: &mut AppContext) { - self.client - .send(proto::LeaveChannelBuffer { - channel_id: self.channel_id, - }) - .log_err(); + if self.connected { + self.client + .send(proto::LeaveChannelBuffer { + channel_id: self.channel.id, + }) + .log_err(); + } } } impl ChannelBuffer { pub(crate) async fn new( - channel_store: ModelHandle, - channel_id: ChannelId, + channel: Arc, client: Arc, mut cx: AsyncAppContext, ) -> Result> { let response = client - .request(proto::JoinChannelBuffer { channel_id }) + .request(proto::JoinChannelBuffer { + channel_id: channel.id, + }) .await?; let base_text = response.base_text; @@ -62,7 +66,7 @@ impl ChannelBuffer { }); buffer.update(&mut cx, |buffer, cx| buffer.apply_ops(operations, cx))?; - let subscription = client.subscribe_to_entity(channel_id)?; + let subscription = client.subscribe_to_entity(channel.id)?; anyhow::Ok(cx.add_model(|cx| { cx.subscribe(&buffer, Self::on_buffer_update).detach(); @@ -70,10 +74,10 @@ impl ChannelBuffer { Self { buffer, client, - channel_id, - channel_store, + connected: true, collaborators, - _subscription: subscription.set_model(&cx.handle(), &mut cx.to_async()), + channel, + subscription: Some(subscription.set_model(&cx.handle(), &mut cx.to_async())), } })) } @@ -155,7 +159,7 @@ impl ChannelBuffer { let operation = language::proto::serialize_operation(operation); self.client .send(proto::UpdateChannelBuffer { - channel_id: self.channel_id, + channel_id: self.channel.id, operations: vec![operation], }) .log_err(); @@ -170,11 +174,21 @@ impl ChannelBuffer { &self.collaborators } - pub fn channel(&self, cx: &AppContext) -> Option> { - self.channel_store - .read(cx) - .channel_for_id(self.channel_id) - .cloned() + pub fn channel(&self) -> Arc { + self.channel.clone() + } + + pub(crate) fn disconnect(&mut self, cx: &mut ModelContext) { + if self.connected { + self.connected = false; + self.subscription.take(); + cx.emit(Event::Disconnected); + cx.notify() + } + } + + pub fn is_connected(&self) -> bool { + self.connected } pub fn replica_id(&self, cx: &AppContext) -> u16 { diff --git a/crates/channel/src/channel_store.rs b/crates/channel/src/channel_store.rs index 1d83bd1d7f..861f731331 100644 --- a/crates/channel/src/channel_store.rs +++ b/crates/channel/src/channel_store.rs @@ -2,7 +2,7 @@ use crate::channel_buffer::ChannelBuffer; use anyhow::{anyhow, Result}; use client::{Client, Status, Subscription, User, UserId, UserStore}; use collections::{hash_map, HashMap, HashSet}; -use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; +use futures::{channel::mpsc, future::Shared, Future, FutureExt, StreamExt}; use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; use rpc::{proto, TypedEnvelope}; use std::sync::Arc; @@ -71,16 +71,14 @@ impl ChannelStore { let mut connection_status = client.status(); let watch_connection_status = cx.spawn_weak(|this, mut cx| async move { while let Some(status) = connection_status.next().await { - if matches!(status, Status::ConnectionLost | Status::SignedOut) { + if !status.is_connected() { if let Some(this) = this.upgrade(&cx) { this.update(&mut cx, |this, cx| { - this.channels_by_id.clear(); - this.channel_invitations.clear(); - this.channel_participants.clear(); - this.channels_with_admin_privileges.clear(); - this.channel_paths.clear(); - this.outgoing_invites.clear(); - cx.notify(); + if matches!(status, Status::ConnectionLost | Status::SignedOut) { + this.handle_disconnect(cx); + } else { + this.disconnect_buffers(cx); + } }); } else { break; @@ -176,9 +174,17 @@ impl ChannelStore { OpenedChannelBuffer::Loading(task) => break task.clone(), }, hash_map::Entry::Vacant(e) => { + let client = self.client.clone(); let task = cx - .spawn(|this, cx| { - ChannelBuffer::new(this, channel_id, self.client.clone(), cx) + .spawn(|this, cx| async move { + let channel = this.read_with(&cx, |this, _| { + this.channel_for_id(channel_id).cloned().ok_or_else(|| { + Arc::new(anyhow!("no channel for id: {}", channel_id)) + }) + })?; + + ChannelBuffer::new(channel, client, cx) + .await .map_err(Arc::new) }) .shared(); @@ -187,8 +193,8 @@ impl ChannelStore { let task = task.clone(); |this, mut cx| async move { let result = task.await; - this.update(&mut cx, |this, cx| { - if let Ok(buffer) = result { + this.update(&mut cx, |this, cx| match result { + Ok(buffer) => { cx.observe_release(&buffer, move |this, _, _| { this.opened_buffers.remove(&channel_id); }) @@ -197,7 +203,9 @@ impl ChannelStore { channel_id, OpenedChannelBuffer::Open(buffer.downgrade()), ); - } else { + } + Err(error) => { + log::error!("failed to open channel buffer {error:?}"); this.opened_buffers.remove(&channel_id); } }); @@ -474,6 +482,27 @@ impl ChannelStore { Ok(()) } + fn handle_disconnect(&mut self, cx: &mut ModelContext<'_, ChannelStore>) { + self.disconnect_buffers(cx); + self.channels_by_id.clear(); + self.channel_invitations.clear(); + self.channel_participants.clear(); + self.channels_with_admin_privileges.clear(); + self.channel_paths.clear(); + self.outgoing_invites.clear(); + cx.notify(); + } + + fn disconnect_buffers(&mut self, cx: &mut ModelContext) { + for (_, buffer) in self.opened_buffers.drain() { + if let OpenedChannelBuffer::Open(buffer) = buffer { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, |buffer, cx| buffer.disconnect(cx)); + } + } + } + } + pub(crate) fn update_channels( &mut self, payload: proto::UpdateChannels, @@ -508,38 +537,44 @@ impl ChannelStore { .retain(|channel_id, _| !payload.remove_channels.contains(channel_id)); self.channels_with_admin_privileges .retain(|channel_id| !payload.remove_channels.contains(channel_id)); + + for channel_id in &payload.remove_channels { + let channel_id = *channel_id; + if let Some(OpenedChannelBuffer::Open(buffer)) = + self.opened_buffers.remove(&channel_id) + { + if let Some(buffer) = buffer.upgrade(cx) { + buffer.update(cx, ChannelBuffer::disconnect); + } + } + } } - for channel in payload.channels { - if let Some(existing_channel) = self.channels_by_id.get_mut(&channel.id) { - // FIXME: We may be missing a path for this existing channel in certain cases - let existing_channel = Arc::make_mut(existing_channel); - existing_channel.name = channel.name; - continue; - } + for channel_proto in payload.channels { + if let Some(existing_channel) = self.channels_by_id.get_mut(&channel_proto.id) { + Arc::make_mut(existing_channel).name = channel_proto.name; + } else { + let channel = Arc::new(Channel { + id: channel_proto.id, + name: channel_proto.name, + }); + self.channels_by_id.insert(channel.id, channel.clone()); - self.channels_by_id.insert( - channel.id, - Arc::new(Channel { - id: channel.id, - name: channel.name, - }), - ); - - if let Some(parent_id) = channel.parent_id { - let mut ix = 0; - while ix < self.channel_paths.len() { - let path = &self.channel_paths[ix]; - if path.ends_with(&[parent_id]) { - let mut new_path = path.clone(); - new_path.push(channel.id); - self.channel_paths.insert(ix + 1, new_path); + if let Some(parent_id) = channel_proto.parent_id { + let mut ix = 0; + while ix < self.channel_paths.len() { + let path = &self.channel_paths[ix]; + if path.ends_with(&[parent_id]) { + let mut new_path = path.clone(); + new_path.push(channel.id); + self.channel_paths.insert(ix + 1, new_path); + ix += 1; + } ix += 1; } - ix += 1; + } else { + self.channel_paths.push(vec![channel.id]); } - } else { - self.channel_paths.push(vec![channel.id]); } } diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 2bd39c861d..18587c2ba8 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -854,10 +854,13 @@ async fn connection_lost( .await .trace_err(); + leave_channel_buffers_for_session(&session) + .await + .trace_err(); + futures::select_biased! { _ = executor.sleep(RECONNECT_TIMEOUT).fuse() => { leave_room_for_session(&session).await.trace_err(); - leave_channel_buffers_for_session(&session).await.trace_err(); if !session .connection_pool() diff --git a/crates/collab/src/tests/channel_buffer_tests.rs b/crates/collab/src/tests/channel_buffer_tests.rs index 0ecd4588c5..8ac4dbbd3f 100644 --- a/crates/collab/src/tests/channel_buffer_tests.rs +++ b/crates/collab/src/tests/channel_buffer_tests.rs @@ -1,5 +1,6 @@ use crate::{rpc::RECONNECT_TIMEOUT, tests::TestServer}; use call::ActiveCall; +use channel::Channel; use client::UserId; use collab_ui::channel_view::ChannelView; use collections::HashMap; @@ -334,6 +335,81 @@ async fn test_reopen_channel_buffer(deterministic: Arc, cx_a: &mu }); } +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: Arc, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + deterministic.forbid_parking(); + let mut server = TestServer::start(&deterministic).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |channel, cx| { + channel.open_channel_buffer(channel_id, cx) + }) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, _| { + assert_eq!( + buffer.channel().as_ref(), + &Channel { + id: channel_id, + name: "zed".to_string() + } + ); + assert!(!buffer.is_connected()); + }); +} + #[track_caller] fn assert_collaborators(collaborators: &[proto::Collaborator], ids: &[Option]) { assert_eq!( diff --git a/crates/collab/src/tests/channel_tests.rs b/crates/collab/src/tests/channel_tests.rs index 41d2286772..b54b4d349b 100644 --- a/crates/collab/src/tests/channel_tests.rs +++ b/crates/collab/src/tests/channel_tests.rs @@ -799,7 +799,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // Sanity check + // Sanity check, B has the invitation assert_channel_invitations( client_b.channel_store(), cx_b, @@ -811,6 +811,7 @@ async fn test_lost_channel_creation( }], ); + // A creates a subchannel while the invite is still pending. let subchannel_id = client_a .channel_store() .update(cx_a, |channel_store, cx| { @@ -841,7 +842,7 @@ async fn test_lost_channel_creation( ], ); - // Accept the invite + // Client B accepts the invite client_b .channel_store() .update(cx_b, |channel_store, _| { @@ -852,7 +853,7 @@ async fn test_lost_channel_creation( deterministic.run_until_parked(); - // B should now see the channel + // Client B should now see the channel assert_channels( client_b.channel_store(), cx_b, diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 0e2d3636aa..9c125117e1 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -114,10 +114,18 @@ impl ChannelView { fn handle_channel_buffer_event( &mut self, _: ModelHandle, - _: &channel_buffer::Event, + event: &channel_buffer::Event, cx: &mut ViewContext, ) { - self.refresh_replica_id_map(cx); + match event { + channel_buffer::Event::CollaboratorsChanged => { + self.refresh_replica_id_map(cx); + } + channel_buffer::Event::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + } } /// Build a mapping of channel buffer replica ids to the corresponding @@ -183,14 +191,13 @@ impl Item for ChannelView { style: &theme::Tab, cx: &gpui::AppContext, ) -> AnyElement { - let channel_name = self - .channel_buffer - .read(cx) - .channel(cx) - .map_or("[Deleted channel]".to_string(), |channel| { - format!("#{}", channel.name) - }); - Label::new(channel_name, style.label.to_owned()).into_any() + let channel_name = &self.channel_buffer.read(cx).channel().name; + let label = if self.channel_buffer.read(cx).is_connected() { + format!("#{}", channel_name) + } else { + format!("#{} (disconnected)", channel_name) + }; + Label::new(label, style.label.to_owned()).into_any() } fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { @@ -208,8 +215,9 @@ impl FollowableItem for ChannelView { } fn to_state_proto(&self, cx: &AppContext) -> Option { - self.channel_buffer.read(cx).channel(cx).map(|channel| { - proto::view::Variant::ChannelView(proto::view::ChannelView { + let channel = self.channel_buffer.read(cx).channel(); + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { channel_id: channel.id, editor: if let Some(proto::view::Variant::Editor(proto)) = self.editor.read(cx).to_state_proto(cx) @@ -218,8 +226,8 @@ impl FollowableItem for ChannelView { } else { None }, - }) - }) + }, + )) } fn from_state_proto( From c7c220309dbfdf8e90625dd0a47401c6da5019a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 16:55:39 -0700 Subject: [PATCH 050/142] Avoid creating redundant snapshots of channel notes buffers Co-authored-by: Mikayla --- crates/collab/src/db/queries/buffers.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/db/queries/buffers.rs b/crates/collab/src/db/queries/buffers.rs index a38693bace..354accc01a 100644 --- a/crates/collab/src/db/queries/buffers.rs +++ b/crates/collab/src/db/queries/buffers.rs @@ -326,9 +326,11 @@ impl Database { .ok_or_else(|| anyhow!("no such buffer"))?; let (base_text, operations) = self.get_buffer_state(&buffer, tx).await?; + if operations.is_empty() { + return Ok(()); + } let mut text_buffer = text::Buffer::new(0, 0, base_text); - text_buffer .apply_ops(operations.into_iter().filter_map(operation_from_wire)) .unwrap(); From 7b6c0c539c9f8a8e30f98413269f8d17a1b7c224 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 17:17:20 -0700 Subject: [PATCH 051/142] Show non-admin context menu items for all channel members Co-authored-by: Mikayla --- crates/collab_ui/src/collab_panel.rs | 81 +++++++++++++++------------- 1 file changed, 43 insertions(+), 38 deletions(-) diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index dece04cb8b..411a3a2598 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1923,47 +1923,52 @@ impl CollabPanel { channel_id: u64, cx: &mut ViewContext, ) { - if self.channel_store.read(cx).is_user_admin(channel_id) { - self.context_menu_on_selected = position.is_none(); + self.context_menu_on_selected = position.is_none(); - self.context_menu.update(cx, |context_menu, cx| { - context_menu.set_position_mode(if self.context_menu_on_selected { - OverlayPositionMode::Local - } else { - OverlayPositionMode::Window - }); - - let expand_action_name = if self.is_channel_collapsed(channel_id) { - "Expand Subchannels" - } else { - "Collapse Subchannels" - }; - - context_menu.show( - position.unwrap_or_default(), - if self.context_menu_on_selected { - gpui::elements::AnchorCorner::TopRight - } else { - gpui::elements::AnchorCorner::BottomLeft - }, - vec![ - ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), - ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), - ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Invite to Channel", InviteMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Rename", RenameChannel { channel_id }), - ContextMenuItem::action("Manage", ManageMembers { channel_id }), - ContextMenuItem::Separator, - ContextMenuItem::action("Delete", RemoveChannel { channel_id }), - ], - cx, - ); + self.context_menu.update(cx, |context_menu, cx| { + context_menu.set_position_mode(if self.context_menu_on_selected { + OverlayPositionMode::Local + } else { + OverlayPositionMode::Window }); - cx.notify(); - } + let expand_action_name = if self.is_channel_collapsed(channel_id) { + "Expand Subchannels" + } else { + "Collapse Subchannels" + }; + + let mut items = vec![ + ContextMenuItem::action(expand_action_name, ToggleCollapse { channel_id }), + ContextMenuItem::action("Open Notes", OpenChannelBuffer { channel_id }), + ]; + + if self.channel_store.read(cx).is_user_admin(channel_id) { + items.extend([ + ContextMenuItem::Separator, + ContextMenuItem::action("New Subchannel", NewChannel { channel_id }), + ContextMenuItem::action("Rename", RenameChannel { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Invite Members", InviteMembers { channel_id }), + ContextMenuItem::action("Manage Members", ManageMembers { channel_id }), + ContextMenuItem::Separator, + ContextMenuItem::action("Delete", RemoveChannel { channel_id }), + ]); + } + + context_menu.show( + position.unwrap_or_default(), + if self.context_menu_on_selected { + gpui::elements::AnchorCorner::TopRight + } else { + gpui::elements::AnchorCorner::BottomLeft + }, + items, + cx, + ); + }); + + cx.notify(); } fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { From a95dcfa8bc74dbfec05174af5f9b1498737862b5 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 24 Aug 2023 17:18:18 -0700 Subject: [PATCH 052/142] Make channel notes view searchable and navigable via pane history Co-authored-by: Mikayla --- crates/collab_ui/src/channel_view.rs | 37 +++++++++++++++++++++++++++- crates/editor/src/items.rs | 2 +- crates/vim/src/visual.rs | 12 ++++----- crates/workspace/src/item.rs | 8 +++--- 4 files changed, 46 insertions(+), 13 deletions(-) diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index 9c125117e1..bb1e840ffc 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -10,13 +10,17 @@ use editor::Editor; use gpui::{ actions, elements::{ChildView, Label}, + geometry::vector::Vector2F, AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, }; use project::Project; +use std::any::Any; use workspace::{ item::{FollowableItem, Item, ItemHandle}, - register_followable_item, Pane, ViewId, Workspace, WorkspaceId, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, ViewId, Workspace, WorkspaceId, }; actions!(channel_view, [Deploy]); @@ -207,6 +211,37 @@ impl Item for ChannelView { cx, )) } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + true + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &ViewHandle) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { + self.editor.read(cx).pixel_position_of_cursor(cx) + } } impl FollowableItem for ChannelView { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 657aae5ff9..477eab41ac 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -754,7 +754,7 @@ impl Item for Editor { Some(Box::new(handle.clone())) } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { self.pixel_position_of_newest_cursor } diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 1a11721a4e..5e60ef59fc 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -391,7 +391,7 @@ mod test { the lazy dog" }) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); // entering visual mode should select the character // under cursor @@ -400,7 +400,7 @@ mod test { fox jumps over the lazy dog"}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // forwards motions should extend the selection cx.simulate_shared_keystrokes(["w", "j"]).await; @@ -430,7 +430,7 @@ mod test { b "}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["v"]).await; cx.assert_shared_state(indoc! {" a @@ -438,7 +438,7 @@ mod test { ˇ»b "}) .await; - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); // toggles off again cx.simulate_shared_keystrokes(["v"]).await; @@ -510,7 +510,7 @@ mod test { b ˇ"}) .await; - let cursor = cx.update_editor(|editor, _| editor.pixel_position_of_cursor()); + let cursor = cx.update_editor(|editor, cx| editor.pixel_position_of_cursor(cx)); cx.simulate_shared_keystrokes(["shift-v"]).await; cx.assert_shared_state(indoc! {" a @@ -518,7 +518,7 @@ mod test { ˇ"}) .await; assert_eq!(cx.mode(), cx.neovim_mode().await); - cx.update_editor(|editor, _| assert_eq!(cursor, editor.pixel_position_of_cursor())); + cx.update_editor(|editor, cx| assert_eq!(cursor, editor.pixel_position_of_cursor(cx))); cx.simulate_shared_keystrokes(["x"]).await; cx.assert_shared_state(indoc! {" a diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 4b5b7a7931..c218a85234 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -158,9 +158,7 @@ pub trait Item: View { fn should_update_tab_on_event(_: &Self::Event) -> bool { false } - fn is_edit_event(_: &Self::Event) -> bool { - false - } + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -205,7 +203,7 @@ pub trait Item: View { fn show_toolbar(&self) -> bool { true } - fn pixel_position_of_cursor(&self) -> Option { + fn pixel_position_of_cursor(&self, _: &AppContext) -> Option { None } } @@ -623,7 +621,7 @@ impl ItemHandle for ViewHandle { } fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { - self.read(cx).pixel_position_of_cursor() + self.read(cx).pixel_position_of_cursor(cx) } } From 9fe580acb675e573588e9451617310e26be8e9ea Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 01:50:54 -0400 Subject: [PATCH 053/142] WIP --- crates/project/src/terminals.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 539d120e97..2fb66a6c4c 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -77,7 +77,7 @@ impl Project { let program = match shell { terminal::Shell::System => "Figure this out", terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args } => program, + terminal::Shell::WithArguments { program, args: _ } => program, }; // This is so hacky - find a better way to do this From 8288e5591d893ec54e7621aea36062452f3fbd11 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 02:21:07 -0400 Subject: [PATCH 054/142] Automatically enable project search filters when using `Search Inside` --- crates/search/src/project_search.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 52247b9bc0..6bd48138ac 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -893,6 +893,7 @@ impl ProjectSearchView { search .included_files_editor .update(cx, |editor, cx| editor.set_text(filter_str, cx)); + search.filters_enabled = true; search.focus_query_editor(cx) }); } From ee97bc54cfc184d90dfbaff2bac585b8e60df202 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Fri, 25 Aug 2023 10:38:01 +0200 Subject: [PATCH 055/142] cleaned up warnings --- crates/search/src/project_search.rs | 2 +- crates/semantic_index/src/semantic_index.rs | 40 +++++++++---------- .../src/semantic_index_tests.rs | 2 +- 3 files changed, 20 insertions(+), 24 deletions(-) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index f665c4ddcd..a29fad0f54 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -760,7 +760,7 @@ impl ProjectSearchView { } fn new(model: ModelHandle, cx: &mut ViewContext) -> Self { - let mut project; + let project; let excerpts; let mut query_text = String::new(); let mut options = SearchOptions::NONE; diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 474025c25e..70495b59d3 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -12,11 +12,9 @@ use db::VectorDatabase; use embedding::{EmbeddingProvider, OpenAIEmbeddings}; use futures::{channel::oneshot, Future}; use gpui::{AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, Task, WeakModelHandle}; -use isahc::http::header::OccupiedEntry; use language::{Anchor, Buffer, Language, LanguageRegistry}; use parking_lot::Mutex; use parsing::{CodeContextRetriever, Document, PARSEABLE_ENTIRE_FILE_TYPES}; -use postage::stream::Stream; use postage::watch; use project::{search::PathMatcher, Fs, PathChange, Project, ProjectEntryId, WorktreeId}; use smol::channel; @@ -58,7 +56,7 @@ pub fn init( } cx.subscribe_global::({ - move |event, mut cx| { + move |event, cx| { let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; let workspace = &event.0; if let Some(workspace) = workspace.upgrade(cx) { @@ -111,7 +109,7 @@ pub struct SemanticIndex { struct ProjectState { worktree_db_ids: Vec<(WorktreeId, i64)>, - subscription: gpui::Subscription, + _subscription: gpui::Subscription, outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, job_queue_tx: channel::Sender, @@ -141,9 +139,6 @@ impl ProjectState { outstanding_job_count_rx: watch::Receiver, _outstanding_job_count_tx: Arc>>, ) -> Self { - let (job_count_tx, job_count_rx) = watch::channel_with(0); - let job_count_tx = Arc::new(Mutex::new(job_count_tx)); - let (job_queue_tx, job_queue_rx) = channel::unbounded(); let _queue_update_task = cx.background().spawn({ let mut worktree_queue = HashMap::new(); @@ -158,7 +153,7 @@ impl ProjectState { worktree_db_ids, outstanding_job_count_rx, _outstanding_job_count_tx, - subscription, + _subscription: subscription, _queue_update_task, job_queue_tx, } @@ -175,18 +170,18 @@ impl ProjectState { for (_, op) in queue { match op { IndexOperation::IndexFile { - absolute_path, + absolute_path: _, payload, tx, } => { - tx.try_send(payload); + let _ = tx.try_send(payload); } IndexOperation::DeleteFile { - absolute_path, + absolute_path: _, payload, tx, } => { - tx.try_send(payload); + let _ = tx.try_send(payload); } _ => {} } @@ -715,7 +710,7 @@ impl SemanticIndex { .ok_or(anyhow!("Worktree not available"))? .read(cx) .snapshot(); - cx.spawn(|this, mut cx| async move { + cx.spawn(|_, _| async move { let worktree = worktree.clone(); for (path, entry_id, path_change) in changes.iter() { let relative_path = path.to_path_buf(); @@ -758,7 +753,7 @@ impl SemanticIndex { }, tx: parsing_files_tx.clone(), }; - job_queue_tx.try_send(new_operation); + let _ = job_queue_tx.try_send(new_operation); } } PathChange::Removed => { @@ -770,7 +765,7 @@ impl SemanticIndex { }, tx: db_update_tx.clone(), }; - job_queue_tx.try_send(new_operation); + let _ = job_queue_tx.try_send(new_operation); } _ => {} } @@ -808,7 +803,8 @@ impl SemanticIndex { let _subscription = cx.subscribe(&project, |this, project, event, cx| { if let project::Event::WorktreeUpdatedEntries(worktree_id, changes) = event { - this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); + let _ = + this.project_entries_changed(project.clone(), changes.clone(), cx, worktree_id); }; }); @@ -901,7 +897,7 @@ impl SemanticIndex { } } // Clean up entries from database that are no longer in the worktree. - for (path, mtime) in file_mtimes { + for (path, _) in file_mtimes { worktree_files.push(IndexOperation::DeleteFile { absolute_path: worktree.absolutize(path.as_path()), payload: DbOperation::Delete { @@ -927,7 +923,7 @@ impl SemanticIndex { ); for op in worktree_files { - project_state.job_queue_tx.try_send(op); + let _ = project_state.job_queue_tx.try_send(op); } this.projects.insert(project.downgrade(), project_state); @@ -948,17 +944,17 @@ impl SemanticIndex { state.unwrap() }; - let parsing_files_tx = self.parsing_files_tx.clone(); - let db_update_tx = self.db_update_tx.clone(); + // let parsing_files_tx = self.parsing_files_tx.clone(); + // let db_update_tx = self.db_update_tx.clone(); let job_count_rx = state.outstanding_job_count_rx.clone(); let count = state.get_outstanding_count(); cx.spawn(|this, mut cx| async move { - this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, _| { let Some(state) = this.projects.get_mut(&project.downgrade()) else { return; }; - state.job_queue_tx.try_send(IndexOperation::FlushQueue); + let _ = state.job_queue_tx.try_send(IndexOperation::FlushQueue); }); Ok((count, job_count_rx)) diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 0ac5953f0b..32d8bb0fb8 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -87,7 +87,7 @@ async fn test_semantic_index(cx: &mut TestAppContext) { let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; - store + let _ = store .update(cx, |store, cx| { store.initialize_project(project.clone(), cx) }) From d34491e822e6344d8dc186a7a937aac7be4957ca Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 16 Aug 2023 14:21:26 +0300 Subject: [PATCH 056/142] Draft inlay hint part hover detection --- crates/editor/src/display_map.rs | 19 +++- crates/editor/src/element.rs | 122 +++++++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 11 +++ 3 files changed, 125 insertions(+), 27 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index aee41e6c53..9159253142 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -27,7 +27,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; -pub use self::inlay_map::Inlay; +pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum FoldStatus { @@ -387,12 +387,25 @@ impl DisplaySnapshot { } fn display_point_to_point(&self, point: DisplayPoint, bias: Bias) -> Point { + self.inlay_snapshot + .to_buffer_point(self.display_point_to_inlay_point(point, bias)) + } + + pub fn display_point_to_inlay_offset(&self, point: DisplayPoint, bias: Bias) -> InlayOffset { + self.inlay_snapshot + .to_offset(self.display_point_to_inlay_point(point, bias)) + } + + pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { + self.inlay_snapshot.to_offset(point) + } + + fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { let block_point = point.0; let wrap_point = self.block_snapshot.to_wrap_point(block_point); let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); let fold_point = self.tab_snapshot.to_fold_point(tab_point, bias).0; - let inlay_point = fold_point.to_inlay_point(&self.fold_snapshot); - self.inlay_snapshot.to_buffer_point(inlay_point) + fold_point.to_inlay_point(&self.fold_snapshot) } pub fn max_point(&self) -> DisplayPoint { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9f74eed790..e2aaa3a3bb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, + InlayHintLabelPart, ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -455,11 +455,67 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let point = position_to_display_point(position, text_bounds, position_map); - - update_go_to_definition_link(editor, point, cmd, shift, cx); - hover_at(editor, point, cx); + if text_bounds.contains_point(position) { + let (nearest_valid_position, unclipped_position) = + position_map.point_for_position(text_bounds, position); + if nearest_valid_position == unclipped_position { + update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); + hover_at(editor, Some(nearest_valid_position), cx); + return true; + } else { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Left) + .to_point(&position_map.snapshot.display_snapshot); + let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); + let next_valid_position = position_map + .snapshot + .clip_point(unclipped_position, Bias::Right) + .to_point(&position_map.snapshot.display_snapshot); + let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); + } + project::InlayHintLabel::LabelParts(label_parts) => { + // TODO kb how to properly convert it? + let unclipped_inlay_position = InlayPoint::new( + unclipped_position.row(), + unclipped_position.column(), + ); + if let Some(hovered_hint_part) = find_hovered_hint_part( + &position_map.snapshot, + &label_parts, + previous_valid_position, + next_valid_position, + unclipped_inlay_position, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } + } + } + }; + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); true } @@ -909,7 +965,7 @@ impl EditorElement { &text, cursor_row_layout.font_size(), &[( - text.len(), + text.chars().count(), RunStyle { font_id, color: style.background, @@ -1804,6 +1860,40 @@ impl EditorElement { } } +fn find_hovered_hint_part<'a>( + snapshot: &EditorSnapshot, + label_parts: &'a [InlayHintLabelPart], + hint_start: Point, + hint_end: Point, + hovered_position: InlayPoint, +) -> Option<&'a InlayHintLabelPart> { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); + dbg!(( + "~~~~~~~~~", + hint_start, + hint_start_offset, + hint_end, + hint_end_offset, + hovered_position + )); + let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); + if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { + let mut hovered_character = (hovered_offset - hint_start_offset).0; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + } else { + return Some(part); + } + } + } + None +} + struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2663,6 +2753,7 @@ impl PositionMap { let mut target_point = DisplayPoint::new(row, column); let point = self.snapshot.clip_point(target_point, Bias::Left); + // TODO kb looks wrong, need to construct inlay point instead? operate offsets? *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; (point, target_point) @@ -2919,23 +3010,6 @@ impl HighlightedRange { } } -fn position_to_display_point( - position: Vector2F, - text_bounds: RectF, - position_map: &PositionMap, -) -> Option { - if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } - } else { - None - } -} - fn range_to_bounds( range: &Range, content_origin: Vector2F, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70cccf21da..5b9bdd08ec 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -386,6 +386,17 @@ impl InlayHintCache { self.hints.clear(); } + pub fn hint_by_id(&self, excerpt_id: ExcerptId, hint_id: InlayId) -> Option { + self.hints + .get(&excerpt_id)? + .read() + .hints + .iter() + .find(|&(id, _)| id == &hint_id) + .map(|(_, hint)| hint) + .cloned() + } + pub fn hints(&self) -> Vec { let mut hints = Vec::new(); for excerpt_hints in self.hints.values() { From d1cb0b3c27fcbe1b02df3e4eae5426fb15802622 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 11:56:38 +0300 Subject: [PATCH 057/142] Properly detect hovered inlay hint label part --- crates/editor/src/element.rs | 219 +++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 101 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e2aaa3a3bb..5738419551 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,7 +4,7 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayPoint, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ @@ -287,13 +287,13 @@ impl EditorElement { return false; } - let (position, target_position) = position_map.point_for_position(text_bounds, position); - + let point_for_position = position_map.point_for_position(text_bounds, position); + let position = point_for_position.previous_valid; if shift && alt { editor.select( SelectPhase::BeginColumnar { position, - goal_column: target_position.column(), + goal_column: point_for_position.exact_unclipped.column(), }, cx, ); @@ -329,9 +329,13 @@ impl EditorElement { if !text_bounds.contains_point(position) { return false; } - - let (point, _) = position_map.point_for_position(text_bounds, position); - mouse_context_menu::deploy_context_menu(editor, position, point, cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + mouse_context_menu::deploy_context_menu( + editor, + position, + point_for_position.previous_valid, + cx, + ); true } @@ -353,9 +357,10 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - - if point == target_point { + if let Some(point) = position_map + .point_for_position(text_bounds, position) + .as_valid() + { if shift { go_to_fetched_type_definition(editor, point, alt, cx); } else { @@ -383,12 +388,9 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu let point = if text_bounds.contains_point(position) { - let (point, target_point) = position_map.point_for_position(text_bounds, position); - if point == target_point { - Some(point) - } else { - None - } + position_map + .point_for_position(text_bounds, position) + .as_valid() } else { None }; @@ -422,13 +424,12 @@ impl EditorElement { )) } - let (position, target_position) = - position_map.point_for_position(text_bounds, position); + let point_for_position = position_map.point_for_position(text_bounds, position); editor.select( SelectPhase::Update { - position, - goal_column: target_position.column(), + position: point_for_position.previous_valid, + goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) .clamp(Vector2F::zero(), position_map.scroll_max), }, @@ -456,59 +457,74 @@ impl EditorElement { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu if text_bounds.contains_point(position) { - let (nearest_valid_position, unclipped_position) = - position_map.point_for_position(text_bounds, position); - if nearest_valid_position == unclipped_position { - update_go_to_definition_link(editor, Some(nearest_valid_position), cmd, shift, cx); - hover_at(editor, Some(nearest_valid_position), cx); + let point_for_position = position_map.point_for_position(text_bounds, position); + if let Some(point) = point_for_position.as_valid() { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); return true; } else { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_position = position_map + let hint_start_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Left) - .to_point(&position_map.snapshot.display_snapshot); - let previous_valid_anchor = snapshot.anchor_at(previous_valid_position, Bias::Left); - let next_valid_position = position_map + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map .snapshot - .clip_point(unclipped_position, Bias::Right) - .to_point(&position_map.snapshot.display_snapshot); - let next_valid_anchor = snapshot.anchor_at(next_valid_position, Bias::Right); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(position_map.snapshot.display_point_to_inlay_offset( + point_for_position.exact_unclipped, + Bias::Left, + )) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() + }) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - // TODO kb how to properly convert it? - let unclipped_inlay_position = InlayPoint::new( - unclipped_position.row(), - unclipped_position.column(), - ); - if let Some(hovered_hint_part) = find_hovered_hint_part( - &position_map.snapshot, - &label_parts, - previous_valid_position, - next_valid_position, - unclipped_inlay_position, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); + if let Some(cached_hint) = editor + .inlay_hint_cache() + .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.label { + project::InlayHintLabel::String(regular_label) => { + // TODO kb remove + check for tooltip for hover and resolve, if needed + eprintln!("regular string: {regular_label}"); } - } - }; + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + // TODO kb remove + check for tooltip and location and resolve, if needed + eprintln!("hint_part: {hovered_hint_part:?}"); + } + } + }; + } } } } @@ -1861,27 +1877,12 @@ impl EditorElement { } fn find_hovered_hint_part<'a>( - snapshot: &EditorSnapshot, label_parts: &'a [InlayHintLabelPart], - hint_start: Point, - hint_end: Point, - hovered_position: InlayPoint, + hint_range: Range, + hovered_offset: InlayOffset, ) -> Option<&'a InlayHintLabelPart> { - let hint_start_offset = - snapshot.display_point_to_inlay_offset(hint_start.to_display_point(&snapshot), Bias::Left); - let hint_end_offset = - snapshot.display_point_to_inlay_offset(hint_end.to_display_point(&snapshot), Bias::Right); - dbg!(( - "~~~~~~~~~", - hint_start, - hint_start_offset, - hint_end, - hint_end_offset, - hovered_position - )); - let hovered_offset = snapshot.inlay_point_to_inlay_offset(hovered_position); - if hovered_offset >= hint_start_offset && hovered_offset <= hint_end_offset { - let mut hovered_character = (hovered_offset - hint_start_offset).0; + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { @@ -2722,22 +2723,32 @@ struct PositionMap { snapshot: EditorSnapshot, } +#[derive(Debug)] +struct PointForPosition { + previous_valid: DisplayPoint, + next_valid: DisplayPoint, + exact_unclipped: DisplayPoint, + column_overshoot_after_line_end: u32, +} + +impl PointForPosition { + fn as_valid(&self) -> Option { + if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { + Some(self.previous_valid) + } else { + None + } + } +} + impl PositionMap { - /// Returns two display points: - /// 1. The nearest *valid* position in the editor - /// 2. An unclipped, potentially *invalid* position that maps directly to - /// the given pixel position. - fn point_for_position( - &self, - text_bounds: RectF, - position: Vector2F, - ) -> (DisplayPoint, DisplayPoint) { + fn point_for_position(&self, text_bounds: RectF, position: Vector2F) -> PointForPosition { let scroll_position = self.snapshot.scroll_position(); let position = position - text_bounds.origin(); let y = position.y().max(0.0).min(self.size.y()); let x = position.x() + (scroll_position.x() * self.em_width); let row = (y / self.line_height + scroll_position.y()) as u32; - let (column, x_overshoot) = if let Some(line) = self + let (column, x_overshoot_after_line_end) = if let Some(line) = self .line_layouts .get(row as usize - scroll_position.y() as usize) .map(|line_with_spaces| &line_with_spaces.line) @@ -2751,12 +2762,18 @@ impl PositionMap { (0, x) }; - let mut target_point = DisplayPoint::new(row, column); - let point = self.snapshot.clip_point(target_point, Bias::Left); - // TODO kb looks wrong, need to construct inlay point instead? operate offsets? - *target_point.column_mut() += (x_overshoot / self.em_advance) as u32; + let mut exact_unclipped = DisplayPoint::new(row, column); + let previous_valid = self.snapshot.clip_point(exact_unclipped, Bias::Left); + let next_valid = self.snapshot.clip_point(exact_unclipped, Bias::Right); - (point, target_point) + let column_overshoot_after_line_end = (x_overshoot_after_line_end / self.em_advance) as u32; + *exact_unclipped.column_mut() += column_overshoot_after_line_end; + PointForPosition { + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end, + } } } From e4b78e322edfea72039d3b2afce0646c4946d9fd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 12:37:05 +0300 Subject: [PATCH 058/142] Revert "Strip off inlay hints data that should be resolved" Without holding all hints in host's cache, this is impossile. Currenly, we keep hint caches separate and isolated, so this will not work when we actually resolve. --- crates/lsp/src/lsp.rs | 4 +-- crates/project/src/lsp_command.rs | 56 +++++++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d806..78c858a90c 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,9 +434,7 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], - }), + resolve_support: None, dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index a8692257d8..08261b64f1 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1954,7 +1954,7 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - _: &mut AppContext, + cx: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,17 +1963,51 @@ impl LspCommand for InlayHints { position: Some(language::proto::serialize_anchor(&response_hint.position)), padding_left: response_hint.padding_left, padding_right: response_hint.padding_right, - kind: response_hint.kind.map(|kind| kind.name().to_string()), - // Do not pass extra data such as tooltips to clients: host can put tooltip data from the cache during resolution. - tooltip: None, - // Similarly, do not pass label parts to clients: host can return a detailed list during resolution. label: Some(proto::InlayHintLabel { - label: Some(proto::inlay_hint_label::Label::Value( - match response_hint.label { - InlayHintLabel::String(s) => s, - InlayHintLabel::LabelParts(_) => response_hint.text(), - }, - )), + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } }), }) .collect(), From 3434990b70cf79772d7f3f9d9f47a2b385fc2cb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 15:54:30 +0300 Subject: [PATCH 059/142] Store inlay hint resolve data --- crates/editor/src/display_map/inlay_map.rs | 6 +- crates/project/src/lsp_command.rs | 149 ++++++++++++++------- crates/project/src/project.rs | 16 +++ crates/rpc/proto/zed.proto | 16 +++ 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 9794ac45c1..3cea513dea 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1109,7 +1109,7 @@ mod tests { use super::*; use crate::{InlayId, MultiBuffer}; use gpui::AppContext; - use project::{InlayHint, InlayHintLabel}; + use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; @@ -1131,6 +1131,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1151,6 +1152,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1171,6 +1173,7 @@ mod tests { padding_right: false, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text @@ -1191,6 +1194,7 @@ mod tests { padding_right: true, tooltip: None, kind: None, + resolve_state: ResolveState::Resolved, }, ) .text diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..d46ba4f5f7 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,7 +1,7 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, - MarkupContent, Project, ProjectTransaction, + MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; @@ -1817,7 +1817,8 @@ impl LspCommand for InlayHints { server_id: LanguageServerId, mut cx: AsyncAppContext, ) -> Result> { - let (lsp_adapter, _) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; + let (lsp_adapter, lsp_server) = + language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning // `const foo: boolean` into `const foo : boolean` which looks odd. // `rust-analyzer` does not have the padding for this case, and we have to accomodate both. @@ -1833,6 +1834,15 @@ impl LspCommand for InlayHints { .unwrap_or_default() .into_iter() .map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_hint.data), + _ => ResolveState::Resolved, + }; let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), @@ -1910,6 +1920,7 @@ impl LspCommand for InlayHints { }) } }), + resolve_state, } }) .collect()) @@ -1959,57 +1970,69 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) + .map(|response_hint| { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(resolve_data) => { + (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) + } + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, lsp_resolve_state + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + kind: markup_content.kind, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), } }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - }) + resolve_state, + }}) .collect(), version: serialize_version(buffer_version), } @@ -2021,7 +2044,7 @@ impl LspCommand for InlayHints { project: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { buffer .update(&mut cx, |buffer, _| { buffer.wait_for_version(deserialize_version(&message.version)) @@ -2035,6 +2058,27 @@ impl LspCommand for InlayHints { .as_ref() .and_then(|location| location.buffer_id) .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!( + "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", + ) + }); + + let lsp_resolve_state = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => ResolveState::CanResolve(lsp_resolve_state), + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; let hint = InlayHint { buffer_id, position: message_hint @@ -2103,6 +2147,7 @@ impl LspCommand for InlayHints { } }) }), + resolve_state, }; hints.push(hint); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 49074268f2..1628234f98 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -342,6 +342,22 @@ pub struct InlayHint { pub padding_left: bool, pub padding_right: bool, pub tooltip: Option, + pub resolve_state: ResolveState, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolveState { + Resolved, + CanResolve(Option), + Resolving, +} + +impl Hash for ResolveState { + fn hash(&self, state: &mut H) { + // Regular `lsp::LSPAny` is not hashable, so we can't hash it. + // LSP expects this data to not to change between requests, so we only hash the discriminant. + std::mem::discriminant(self).hash(state); + } } impl InlayHint { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index f032ccce51..d0964064ab 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -754,6 +754,7 @@ message InlayHint { bool padding_left = 4; bool padding_right = 5; InlayHintTooltip tooltip = 6; + ResolveState resolve_state = 7; } message InlayHintLabel { @@ -787,6 +788,21 @@ message InlayHintLabelPartTooltip { } } +message ResolveState { + State state = 1; + LspResolveState lsp_resolve_state = 2; + + enum State { + Resolved = 0; + CanResolve = 1; + Resolving = 2; + } + + message LspResolveState { + string value = 1; + } +} + message RefreshInlayHints { uint64 project_id = 1; } From 80e871424194fb8c018d5d85d0d5182492a75238 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 18 Aug 2023 21:09:04 +0300 Subject: [PATCH 060/142] Send inlay hint resolve requests --- crates/editor/src/display_map/inlay_map.rs | 4 - crates/editor/src/element.rs | 177 +++--- crates/editor/src/inlay_hint_cache.rs | 81 ++- crates/project/src/lsp_command.rs | 624 +++++++++++++-------- crates/project/src/project.rs | 140 ++++- crates/rpc/proto/zed.proto | 16 +- crates/rpc/src/proto.rs | 4 + crates/rpc/src/rpc.rs | 2 +- 8 files changed, 705 insertions(+), 343 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 3cea513dea..026f1fc2c2 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1125,7 +1125,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1146,7 +1145,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String("a".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, @@ -1167,7 +1165,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: false, padding_right: false, @@ -1188,7 +1185,6 @@ mod tests { Anchor::min(), &InlayHint { label: InlayHintLabel::String(" a ".to_string()), - buffer_id: 0, position: text::Anchor::default(), padding_left: true, padding_right: true, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5738419551..2d2191e77f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -42,7 +42,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, + InlayHintLabelPart, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -456,82 +456,21 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu + let mut go_to_definition_point = None; + let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); if let Some(point) = point_for_position.as_valid() { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); - hover_at(editor, Some(point), cx); - return true; + go_to_definition_point = Some(point); + hover_at_point = Some(point); } else { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some(position_map.snapshot.display_point_to_inlay_offset( - point_for_position.exact_unclipped, - Bias::Left, - )) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| { - hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt() - }) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - if let Some(cached_hint) = editor - .inlay_hint_cache() - .hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { - match &cached_hint.label { - project::InlayHintLabel::String(regular_label) => { - // TODO kb remove + check for tooltip for hover and resolve, if needed - eprintln!("regular string: {regular_label}"); - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { - // TODO kb remove + check for tooltip and location and resolve, if needed - eprintln!("hint_part: {hovered_hint_part:?}"); - } - } - }; - } - } - } + (go_to_definition_point, hover_at_point) = + inlay_link_and_hover_points(position_map, point_for_position, editor, cx); } }; - update_go_to_definition_link(editor, None, cmd, shift, cx); - hover_at(editor, None, cx); + update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); + hover_at(editor, hover_at_point, cx); true } @@ -1876,6 +1815,104 @@ impl EditorElement { } } +fn inlay_link_and_hover_points( + position_map: &PositionMap, + point_for_position: PointForPosition, + editor: &mut Editor, + cx: &mut ViewContext<'_, '_, Editor>, +) -> (Option, Option) { + let hint_start_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some( + position_map + .snapshot + .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), + ) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + let mut go_to_definition_point = None; + let mut hover_at_point = None; + if let Some(hovered_offset) = hovered_offset { + let buffer = editor.buffer().read(cx); + let snapshot = buffer.snapshot(cx); + let previous_valid_anchor = snapshot.anchor_at( + point_for_position + .previous_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Left, + ); + let next_valid_anchor = snapshot.anchor_at( + point_for_position + .next_valid + .to_point(&position_map.snapshot.display_snapshot), + Bias::Right, + ); + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) + .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + if let Some(cached_hint) = + inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) + { + match &cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + previous_valid_anchor.excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match &cached_hint.label { + project::InlayHintLabel::String(_) => { + if cached_hint.tooltip.is_some() { + dbg!(&cached_hint.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some(hovered_hint_part) = find_hovered_hint_part( + &label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) { + if hovered_hint_part.tooltip.is_some() { + dbg!(&hovered_hint_part.tooltip); // TODO kb + // hover_at_point = Some(hovered_offset); + } + if let Some(location) = &hovered_hint_part.location { + dbg!(location); // TODO kb + // go_to_definition_point = Some(location); + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + } + + (go_to_definition_point, hover_at_point) +} + fn find_hovered_hint_part<'a>( label_parts: &'a [InlayHintLabelPart], hint_range: Range, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 5b9bdd08ec..52a4039a76 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,7 @@ use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; -use project::InlayHint; +use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; @@ -60,7 +60,7 @@ struct ExcerptHintsUpdate { excerpt_id: ExcerptId, remove_from_visible: Vec, remove_from_cache: HashSet, - add_to_cache: HashSet, + add_to_cache: Vec, } #[derive(Debug, Clone, Copy)] @@ -409,6 +409,79 @@ impl InlayHintCache { pub fn version(&self) -> usize { self.version } + + pub fn spawn_hint_resolve( + &self, + buffer_id: u64, + excerpt_id: ExcerptId, + id: InlayId, + cx: &mut ViewContext<'_, '_, Editor>, + ) { + if let Some(excerpt_hints) = self.hints.get(&excerpt_id) { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if let ResolveState::CanResolve(server_id, _) = &cached_hint.resolve_state { + let hint_to_resolve = cached_hint.clone(); + let server_id = *server_id; + cached_hint.resolve_state = ResolveState::Resolving; + drop(guard); + cx.spawn(|editor, mut cx| async move { + let resolved_hint_task = editor.update(&mut cx, |editor, cx| { + editor + .buffer() + .read(cx) + .buffer(buffer_id) + .and_then(|buffer| { + let project = editor.project.as_ref()?; + Some(project.update(cx, |project, cx| { + project.resolve_inlay_hint( + hint_to_resolve, + buffer, + server_id, + cx, + ) + })) + }) + })?; + if let Some(resolved_hint_task) = resolved_hint_task { + if let Some(mut resolved_hint) = + resolved_hint_task.await.context("hint resolve task")? + { + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) + { + if cached_hint.resolve_state == ResolveState::Resolving + { + resolved_hint.resolve_state = + ResolveState::Resolved; + *cached_hint = resolved_hint; + } + } + } + })?; + } + } + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + } + } + } } fn spawn_new_update_tasks( @@ -632,7 +705,7 @@ fn calculate_hint_updates( cached_excerpt_hints: Option>>, visible_hints: &[Inlay], ) -> Option { - let mut add_to_cache: HashSet = HashSet::default(); + let mut add_to_cache = Vec::::new(); let mut excerpt_hints_to_persist = HashMap::default(); for new_hint in new_excerpt_hints { if !contains_position(&fetch_range, new_hint.position, buffer_snapshot) { @@ -659,7 +732,7 @@ fn calculate_hint_updates( None => true, }; if missing_from_cache { - add_to_cache.insert(new_hint); + add_to_cache.push(new_hint); } } diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index d46ba4f5f7..7b4d689a81 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -12,8 +12,9 @@ use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, - range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, CachedLspAdapter, CharKind, CodeAction, - Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1776,6 +1777,371 @@ impl LspCommand for OnTypeFormatting { } } +impl InlayHints { + pub fn lsp_to_project_hint( + lsp_hint: lsp::InlayHint, + buffer_handle: &ModelHandle, + resolve_state: ResolveState, + force_no_type_left_padding: bool, + cx: &AppContext, + ) -> InlayHint { + let kind = lsp_hint.kind.and_then(|kind| match kind { + lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), + lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), + _ => None, + }); + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { + false + } else { + lsp_hint.padding_left.unwrap_or(false) + }; + InlayHint { + position: if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + }, + padding_left, + padding_right: lsp_hint.padding_right.unwrap_or(false), + label: match lsp_hint.label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( + lsp_parts + .into_iter() + .map(|label_part| InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location: label_part.location.map(|lsp_location| { + let target_start = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + Location { + buffer: buffer_handle.clone(), + range: buffer.anchor_after(target_start) + ..buffer.anchor_before(target_end), + } + }), + }) + .collect(), + ), + }, + kind, + tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), + lsp::InlayHintTooltip::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + resolve_state, + } + } + + pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::CanResolve(server_id, resolve_data) => ( + 0, + resolve_data + .map(|json_data| { + serde_json::to_string(&json_data) + .expect("failed to serialize resolve json data") + }) + .map(|value| proto::resolve_state::LspResolveState { + server_id: server_id.0 as u64, + value, + }), + ), + ResolveState::Resolved => (1, None), + ResolveState::Resolving => (2, None), + }; + let resolve_state = Some(proto::ResolveState { + state, + lsp_resolve_state, + }); + proto::InlayHint { + position: Some(language::proto::serialize_anchor(&response_hint.position)), + padding_left: response_hint.padding_left, + padding_right: response_hint.padding_right, + label: Some(proto::InlayHintLabel { + label: Some(match response_hint.label { + InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), + InlayHintLabel::LabelParts(label_parts) => { + proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { + parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + value: label_part.value, + tooltip: label_part.tooltip.map(|tooltip| { + let proto_tooltip = match tooltip { + InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), + InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }), + }; + proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} + }), + location: label_part.location.map(|location| proto::Location { + start: Some(serialize_anchor(&location.range.start)), + end: Some(serialize_anchor(&location.range.end)), + buffer_id: location.buffer.read(cx).remote_id(), + }), + }).collect() + }) + } + }), + }), + kind: response_hint.kind.map(|kind| kind.name().to_string()), + tooltip: response_hint.tooltip.map(|response_tooltip| { + let proto_tooltip = match response_tooltip { + InlayHintTooltip::String(s) => { + proto::inlay_hint_tooltip::Content::Value(s) + } + InlayHintTooltip::MarkupContent(markup_content) => { + proto::inlay_hint_tooltip::Content::MarkupContent( + proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }, + ) + } + }; + proto::InlayHintTooltip { + content: Some(proto_tooltip), + } + }), + resolve_state, + } + } + + pub async fn proto_to_project_hint( + message_hint: proto::InlayHint, + project: &ModelHandle, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let buffer_id = message_hint + .position + .as_ref() + .and_then(|location| location.buffer_id) + .context("missing buffer id")?; + let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { + panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) + }); + let resolve_state_data = resolve_state + .lsp_resolve_state.as_ref() + .map(|lsp_resolve_state| { + serde_json::from_str::>(&lsp_resolve_state.value) + .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) + .map(|state| (LanguageServerId(lsp_resolve_state.server_id as usize), state)) + }) + .transpose()?; + let resolve_state = match resolve_state.state { + 0 => ResolveState::Resolved, + 1 => { + let (server_id, lsp_resolve_state) = resolve_state_data.with_context(|| { + format!( + "No lsp resolve data for the hint that can be resolved: {message_hint:?}" + ) + })?; + ResolveState::CanResolve(server_id, lsp_resolve_state) + } + 2 => ResolveState::Resolving, + invalid => { + anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") + } + }; + Ok(InlayHint { + position: message_hint + .position + .and_then(language::proto::deserialize_anchor) + .context("invalid position")?, + label: match message_hint + .label + .and_then(|label| label.label) + .context("missing label")? + { + proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), + proto::inlay_hint_label::Label::LabelParts(parts) => { + let mut label_parts = Vec::new(); + for part in parts.parts { + let buffer = project + .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) + .await?; + label_parts.push(InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.map(|tooltip| match tooltip.content { + Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => { + InlayHintLabelPartTooltip::String(s) + } + Some( + proto::inlay_hint_label_part_tooltip::Content::MarkupContent( + markup_content, + ), + ) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }), + None => InlayHintLabelPartTooltip::String(String::new()), + }), + location: match part.location { + Some(location) => Some(Location { + range: location + .start + .and_then(language::proto::deserialize_anchor) + .context("invalid start")? + ..location + .end + .and_then(language::proto::deserialize_anchor) + .context("invalid end")?, + buffer, + }), + None => None, + }, + }); + } + + InlayHintLabel::LabelParts(label_parts) + } + }, + padding_left: message_hint.padding_left, + padding_right: message_hint.padding_right, + kind: message_hint + .kind + .as_deref() + .and_then(InlayHintKind::from_name), + tooltip: message_hint.tooltip.and_then(|tooltip| { + Some(match tooltip.content? { + proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), + proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { + InlayHintTooltip::MarkupContent(MarkupContent { + kind: if markup_content.is_markdown { + HoverBlockKind::Markdown + } else { + HoverBlockKind::PlainText + }, + value: markup_content.value, + }) + } + }) + }), + resolve_state, + }) + } + + // TODO kb instead, store all LSP data inside the project::InlayHint? + pub fn project_to_lsp_hint( + hint: InlayHint, + project: &ModelHandle, + snapshot: &BufferSnapshot, + cx: &AsyncAppContext, + ) -> lsp::InlayHint { + lsp::InlayHint { + position: point_to_lsp(hint.position.to_point_utf16(snapshot)), + kind: hint.kind.map(|kind| match kind { + InlayHintKind::Type => lsp::InlayHintKind::TYPE, + InlayHintKind::Parameter => lsp::InlayHintKind::PARAMETER, + }), + text_edits: None, + tooltip: hint.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintTooltip::String(s) => lsp::InlayHintTooltip::String(s), + InlayHintTooltip::MarkupContent(markup_content) => { + lsp::InlayHintTooltip::MarkupContent(lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => lsp::MarkupKind::PlainText, + HoverBlockKind::Markdown => lsp::MarkupKind::Markdown, + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }) + } + }) + }), + label: match hint.label { + InlayHintLabel::String(s) => lsp::InlayHintLabel::String(s), + InlayHintLabel::LabelParts(label_parts) => lsp::InlayHintLabel::LabelParts( + label_parts + .into_iter() + .map(|part| lsp::InlayHintLabelPart { + value: part.value, + tooltip: part.tooltip.and_then(|tooltip| { + Some(match tooltip { + InlayHintLabelPartTooltip::String(s) => { + lsp::InlayHintLabelPartTooltip::String(s) + } + InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: match markup_content.kind { + HoverBlockKind::PlainText => { + lsp::MarkupKind::PlainText + } + HoverBlockKind::Markdown => { + lsp::MarkupKind::Markdown + } + HoverBlockKind::Code { .. } => return None, + }, + value: markup_content.value, + }, + ) + } + }) + }), + location: part.location.and_then(|location| { + let path = cx.read(|cx| { + let project_path = location.buffer.read(cx).project_path(cx)?; + project.read(cx).absolute_path(&project_path, cx) + })?; + Some(lsp::Location::new( + lsp::Url::from_file_path(path).unwrap(), + range_to_lsp( + location.range.start.to_point_utf16(snapshot) + ..location.range.end.to_point_utf16(snapshot), + ), + )) + }), + command: None, + }) + .collect(), + ), + }, + padding_left: Some(hint.padding_left), + padding_right: Some(hint.padding_right), + data: match hint.resolve_state { + ResolveState::CanResolve(_, data) => data, + ResolveState::Resolving | ResolveState::Resolved => None, + }, + } + } +} + #[async_trait(?Send)] impl LspCommand for InlayHints { type Response = Vec; @@ -1829,7 +2195,6 @@ impl LspCommand for InlayHints { let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; cx.read(|cx| { - let origin_buffer = buffer.read(cx); Ok(message .unwrap_or_default() .into_iter() @@ -1840,88 +2205,18 @@ impl LspCommand for InlayHints { resolve_provider: Some(true), .. }, - ))) => ResolveState::CanResolve(lsp_hint.data), + ))) => { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } _ => ResolveState::Resolved, }; - let kind = lsp_hint.kind.and_then(|kind| match kind { - lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), - lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), - _ => None, - }); - let position = origin_buffer - .clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); - let padding_left = - if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { - false - } else { - lsp_hint.padding_left.unwrap_or(false) - }; - InlayHint { - buffer_id: origin_buffer.remote_id(), - position: if kind == Some(InlayHintKind::Parameter) { - origin_buffer.anchor_before(position) - } else { - origin_buffer.anchor_after(position) - }, - padding_left, - padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => { - InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map( - |tooltip| { - match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent( - markup_content, - ) => InlayHintLabelPartTooltip::MarkupContent( - MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }, - ), - } - }, - ), - location: label_part.location.map(|lsp_location| { - let target_start = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = origin_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer.clone(), - range: origin_buffer.anchor_after(target_start) - ..origin_buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ) - } - }, - kind, - tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), - lsp::InlayHintTooltip::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: format!("{:?}", markup_content.kind), - value: markup_content.value, - }) - } - }), + InlayHints::lsp_to_project_hint( + lsp_hint, + &buffer, resolve_state, - } + force_no_type_left_padding, + cx, + ) }) .collect()) }) @@ -1970,69 +2265,7 @@ impl LspCommand for InlayHints { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| { - let (state, lsp_resolve_state) = match response_hint.resolve_state { - ResolveState::CanResolve(resolve_data) => { - (0, resolve_data.map(|json_data| serde_json::to_string(&json_data).expect("failed to serialize resolve json data")).map(|value| proto::resolve_state::LspResolveState{ value })) - } - ResolveState::Resolved => (1, None), - ResolveState::Resolving => (2, None), - }; - let resolve_state = Some(proto::ResolveState { - state, lsp_resolve_state - }); - proto::InlayHint { - position: Some(language::proto::serialize_anchor(&response_hint.position)), - padding_left: response_hint.padding_left, - padding_right: response_hint.padding_right, - label: Some(proto::InlayHintLabel { - label: Some(match response_hint.label { - InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), - InlayHintLabel::LabelParts(label_parts) => { - proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| { - let proto_tooltip = match tooltip { - InlayHintLabelPartTooltip::String(s) => proto::inlay_hint_label_part_tooltip::Content::Value(s), - InlayHintLabelPartTooltip::MarkupContent(markup_content) => proto::inlay_hint_label_part_tooltip::Content::MarkupContent(proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - }; - proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} - }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() - }) - } - }), - }), - kind: response_hint.kind.map(|kind| kind.name().to_string()), - tooltip: response_hint.tooltip.map(|response_tooltip| { - let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } - InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }, - ) - } - }; - proto::InlayHintTooltip { - content: Some(proto_tooltip), - } - }), - resolve_state, - }}) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) .collect(), version: serialize_version(buffer_version), } @@ -2053,104 +2286,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; - let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { - panic!( - "incorrect proto inlay hint message: no resolve state in hint {message_hint:?}", - ) - }); - - let lsp_resolve_state = resolve_state - .lsp_resolve_state.as_ref() - .map(|lsp_resolve_state| { - serde_json::from_str::(&lsp_resolve_state.value) - .with_context(|| format!("incorrect proto inlay hint message: non-json resolve state {lsp_resolve_state:?}")) - }) - .transpose()?; - let resolve_state = match resolve_state.state { - 0 => ResolveState::Resolved, - 1 => ResolveState::CanResolve(lsp_resolve_state), - 2 => ResolveState::Resolving, - invalid => { - anyhow::bail!("Unexpected resolve state {invalid} for hint {message_hint:?}") - } - }; - let hint = InlayHint { - buffer_id, - position: message_hint - .position - .and_then(language::proto::deserialize_anchor) - .context("invalid position")?, - label: match message_hint - .label - .and_then(|label| label.label) - .context("missing label")? - { - proto::inlay_hint_label::Label::Value(s) => InlayHintLabel::String(s), - proto::inlay_hint_label::Label::LabelParts(parts) => { - let mut label_parts = Vec::new(); - for part in parts.parts { - label_parts.push(InlayHintLabelPart { - value: part.value, - tooltip: part.tooltip.map(|tooltip| match tooltip.content { - Some(proto::inlay_hint_label_part_tooltip::Content::Value(s)) => InlayHintLabelPartTooltip::String(s), - Some(proto::inlay_hint_label_part_tooltip::Content::MarkupContent(markup_content)) => InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }), - None => InlayHintLabelPartTooltip::String(String::new()), - }), - location: match part.location { - Some(location) => { - let target_buffer = project - .update(&mut cx, |this, cx| { - this.wait_for_remote_buffer(location.buffer_id, cx) - }) - .await?; - Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer: target_buffer, - })}, - None => None, - }, - }); - } - - InlayHintLabel::LabelParts(label_parts) - } - }, - padding_left: message_hint.padding_left, - padding_right: message_hint.padding_right, - kind: message_hint - .kind - .as_deref() - .and_then(InlayHintKind::from_name), - tooltip: message_hint.tooltip.and_then(|tooltip| { - Some(match tooltip.content? { - proto::inlay_hint_tooltip::Content::Value(s) => InlayHintTooltip::String(s), - proto::inlay_hint_tooltip::Content::MarkupContent(markup_content) => { - InlayHintTooltip::MarkupContent(MarkupContent { - kind: markup_content.kind, - value: markup_content.value, - }) - } - }) - }), - resolve_state, - }; - - hints.push(hint); + hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1628234f98..65be7cceb1 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -333,9 +333,8 @@ pub struct Location { pub range: Range, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHint { - pub buffer_id: u64, pub position: language::Anchor, pub label: InlayHintLabel, pub kind: Option, @@ -348,18 +347,10 @@ pub struct InlayHint { #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolveState { Resolved, - CanResolve(Option), + CanResolve(LanguageServerId, Option), Resolving, } -impl Hash for ResolveState { - fn hash(&self, state: &mut H) { - // Regular `lsp::LSPAny` is not hashable, so we can't hash it. - // LSP expects this data to not to change between requests, so we only hash the discriminant. - std::mem::discriminant(self).hash(state); - } -} - impl InlayHint { pub fn text(&self) -> String { match &self.label { @@ -369,34 +360,34 @@ impl InlayHint { } } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabel { String(String), LabelParts(Vec), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, pub location: Option, } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum InlayHintLabelPartTooltip { String(String), MarkupContent(MarkupContent), } -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct MarkupContent { - pub kind: String, + pub kind: HoverBlockKind, pub value: String, } @@ -430,7 +421,7 @@ pub struct HoverBlock { pub kind: HoverBlockKind, } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, PartialEq, Eq)] pub enum HoverBlockKind { PlainText, Markdown, @@ -567,6 +558,7 @@ impl Project { client.add_model_request_handler(Self::handle_apply_code_action); client.add_model_request_handler(Self::handle_on_type_formatting); client.add_model_request_handler(Self::handle_inlay_hints); + client.add_model_request_handler(Self::handle_resolve_inlay_hint); client.add_model_request_handler(Self::handle_refresh_inlay_hints); client.add_model_request_handler(Self::handle_reload_buffers); client.add_model_request_handler(Self::handle_synchronize_buffers); @@ -4985,7 +4977,7 @@ impl Project { buffer_handle: ModelHandle, range: Range, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task>> { let buffer = buffer_handle.read(cx); let range = buffer.anchor_before(range.start)..buffer.anchor_before(range.end); let range_start = range.start; @@ -5035,6 +5027,79 @@ impl Project { } } + pub fn resolve_inlay_hint( + &self, + hint: InlayHint, + buffer_handle: ModelHandle, + server_id: LanguageServerId, + cx: &mut ModelContext, + ) -> Task>> { + if self.is_local() { + let buffer = buffer_handle.read(cx); + let (_, lang_server) = if let Some((adapter, server)) = + self.language_server_for_buffer(buffer, server_id, cx) + { + (adapter.clone(), server.clone()) + } else { + return Task::ready(Ok(None)); + }; + let can_resolve = lang_server + .capabilities() + .completion_provider + .as_ref() + .and_then(|options| options.resolve_provider) + .unwrap_or(false); + if !can_resolve { + return Task::ready(Ok(None)); + } + + let buffer_snapshot = buffer.snapshot(); + cx.spawn(|project, cx| async move { + let resolve_task = lang_server.request::( + InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + ); + let resolved_hint = resolve_task + .await + .context("inlay hint resolve LSP request")?; + let resolved_hint = cx.read(|cx| { + InlayHints::lsp_to_project_hint( + resolved_hint, + &buffer_handle, + ResolveState::Resolved, + false, + cx, + ) + }); + Ok(Some(resolved_hint)) + }) + } else if let Some(project_id) = self.remote_id() { + let client = self.client.clone(); + let request = proto::ResolveInlayHint { + project_id, + buffer_id: buffer_handle.read(cx).remote_id(), + language_server_id: server_id.0 as u64, + hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + }; + cx.spawn(|project, mut cx| async move { + let response = client + .request(request) + .await + .context("inlay hints proto request")?; + match response.hint { + Some(resolved_hint) => { + InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) + .await + .map(Some) + .context("inlay hints proto response conversion") + } + None => Ok(None), + } + }) + } else { + Task::ready(Err(anyhow!("project does not have a remote id"))) + } + } + #[allow(clippy::type_complexity)] pub fn search( &self, @@ -6832,6 +6897,43 @@ impl Project { })) } + async fn handle_resolve_inlay_hint( + this: ModelHandle, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result { + let proto_hint = envelope + .payload + .hint + .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); + let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) + .await + .context("resolved proto inlay hint conversion")?; + let buffer = this.update(&mut cx, |this, cx| { + this.opened_buffers + .get(&envelope.payload.buffer_id) + .and_then(|buffer| buffer.upgrade(cx)) + .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) + })?; + let resolved_hint = this + .update(&mut cx, |project, cx| { + project.resolve_inlay_hint( + hint, + buffer, + LanguageServerId(envelope.payload.language_server_id as usize), + cx, + ) + }) + .await + .context("inlay hints fetch")? + .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + + Ok(proto::ResolveInlayHintResponse { + hint: resolved_hint, + }) + } + async fn handle_refresh_inlay_hints( this: ModelHandle, _: TypedEnvelope, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index d0964064ab..dbd700e264 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,6 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; + ResolveInlayHint resolve_inlay_hint = 131; + ResolveInlayHintResponse resolve_inlay_hint_response = 132; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; @@ -800,15 +802,27 @@ message ResolveState { message LspResolveState { string value = 1; + uint64 server_id = 2; } } +message ResolveInlayHint { + uint64 project_id = 1; + uint64 buffer_id = 2; + uint64 language_server_id = 3; + InlayHint hint = 4; +} + +message ResolveInlayHintResponse { + InlayHint hint = 1; +} + message RefreshInlayHints { uint64 project_id = 1; } message MarkupContent { - string kind = 1; + bool is_markdown = 1; string value = 2; } diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index f0f49c6230..2e4dce01e1 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -197,6 +197,8 @@ messages!( (OnTypeFormattingResponse, Background), (InlayHints, Background), (InlayHintsResponse, Background), + (ResolveInlayHint, Background), + (ResolveInlayHintResponse, Background), (RefreshInlayHints, Foreground), (Ping, Foreground), (PrepareRename, Background), @@ -299,6 +301,7 @@ request_messages!( (PrepareRename, PrepareRenameResponse), (OnTypeFormatting, OnTypeFormattingResponse), (InlayHints, InlayHintsResponse), + (ResolveInlayHint, ResolveInlayHintResponse), (RefreshInlayHints, Ack), (ReloadBuffers, ReloadBuffersResponse), (RequestContact, Ack), @@ -355,6 +358,7 @@ entity_messages!( PerformRename, OnTypeFormatting, InlayHints, + ResolveInlayHint, RefreshInlayHints, PrepareRename, ReloadBuffers, diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index 3cb8b6bffa..bc9dd6f80b 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 60; +pub const PROTOCOL_VERSION: u32 = 61; From ac86bbac75b7f0347b9f1619f247c388bf8aea19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 16:25:01 +0300 Subject: [PATCH 061/142] Prepare for hover functionality refactoring --- crates/editor/src/element.rs | 37 +++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d2191e77f..405a6e1a08 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -456,21 +456,27 @@ impl EditorElement { ) -> bool { // This will be handled more correctly once https://github.com/zed-industries/zed/issues/1218 is completed // Don't trigger hover popover if mouse is hovering over context menu - let mut go_to_definition_point = None; - let mut hover_at_point = None; if text_bounds.contains_point(position) { let point_for_position = position_map.point_for_position(text_bounds, position); - if let Some(point) = point_for_position.as_valid() { - go_to_definition_point = Some(point); - hover_at_point = Some(point); - } else { - (go_to_definition_point, hover_at_point) = - inlay_link_and_hover_points(position_map, point_for_position, editor, cx); + match point_for_position.as_valid() { + Some(point) => { + update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + hover_at(editor, Some(point), cx); + } + None => { + update_inlay_link_and_hover_points( + position_map, + point_for_position, + editor, + cx, + ); + } } - }; + } else { + update_go_to_definition_link(editor, None, cmd, shift, cx); + hover_at(editor, None, cx); + } - update_go_to_definition_link(editor, go_to_definition_point, cmd, shift, cx); - hover_at(editor, hover_at_point, cx); true } @@ -1815,12 +1821,13 @@ impl EditorElement { } } -fn inlay_link_and_hover_points( +// TODO kb implement +fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, cx: &mut ViewContext<'_, '_, Editor>, -) -> (Option, Option) { +) { let hint_start_offset = position_map .snapshot .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); @@ -1839,8 +1846,6 @@ fn inlay_link_and_hover_points( } else { None }; - let mut go_to_definition_point = None; - let mut hover_at_point = None; if let Some(hovered_offset) = hovered_offset { let buffer = editor.buffer().read(cx); let snapshot = buffer.snapshot(cx); @@ -1909,8 +1914,6 @@ fn inlay_link_and_hover_points( } } } - - (go_to_definition_point, hover_at_point) } fn find_hovered_hint_part<'a>( From 7eab18ec897738b7e6e5a8b71f15bcc3a3af4fe5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 17:54:34 +0300 Subject: [PATCH 062/142] Pass inlay go to definition data --- crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 82 ++++++-- crates/editor/src/items.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 216 +++++++++++++++------ 4 files changed, 229 insertions(+), 84 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 775f3c07ec..fb1e87580a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,9 +64,7 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{ - hide_link_definition, show_link_definition, LinkDefinitionKind, LinkGoToDefinitionState, -}; +use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -8307,14 +8305,11 @@ impl View for Editor { ) -> bool { let pending_selection = self.has_pending_selection(); - if let Some(point) = self.link_go_to_definition_state.last_mouse_location.clone() { + if let Some(point) = &self.link_go_to_definition_state.last_trigger_point { if event.cmd && !pending_selection { + let point = point.clone(); let snapshot = self.snapshot(cx); - let kind = if event.shift { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; + let kind = point.definition_kind(event.shift); show_link_definition(kind, self, point, snapshot, cx); return false; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 405a6e1a08..15b28914af 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,6 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, + GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -42,7 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, ProjectPath, ResolveState, + InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -395,7 +396,15 @@ impl EditorElement { None }; - update_go_to_definition_link(editor, point, cmd, shift, cx); + update_go_to_definition_link( + editor, + point + .map(GoToDefinitionTrigger::Text) + .unwrap_or(GoToDefinitionTrigger::None), + cmd, + shift, + cx, + ); if editor.has_pending_selection() { let mut scroll_delta = Vector2F::zero(); @@ -460,7 +469,13 @@ impl EditorElement { let point_for_position = position_map.point_for_position(text_bounds, position); match point_for_position.as_valid() { Some(point) => { - update_go_to_definition_link(editor, Some(point), cmd, shift, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(point), + cmd, + shift, + cx, + ); hover_at(editor, Some(point), cx); } None => { @@ -468,12 +483,13 @@ impl EditorElement { position_map, point_for_position, editor, + (cmd, shift), cx, ); } } } else { - update_go_to_definition_link(editor, None, cmd, shift, cx); + update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); hover_at(editor, None, cx); } @@ -1821,11 +1837,11 @@ impl EditorElement { } } -// TODO kb implement fn update_inlay_link_and_hover_points( position_map: &PositionMap, point_for_position: PointForPosition, editor: &mut Editor, + (cmd_held, shift_held): (bool, bool), cx: &mut ViewContext<'_, '_, Editor>, ) { let hint_start_offset = position_map @@ -1861,6 +1877,9 @@ fn update_inlay_link_and_hover_points( .to_point(&position_map.snapshot.display_snapshot), Bias::Right, ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -1872,7 +1891,7 @@ fn update_inlay_link_and_hover_points( if let Some(cached_hint) = inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) { - match &cached_hint.resolve_state { + match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( @@ -1884,7 +1903,7 @@ fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { - match &cached_hint.label { + match cached_hint.label { project::InlayHintLabel::String(_) => { if cached_hint.tooltip.is_some() { dbg!(&cached_hint.tooltip); // TODO kb @@ -1893,7 +1912,7 @@ fn update_inlay_link_and_hover_points( } project::InlayHintLabel::LabelParts(label_parts) => { if let Some(hovered_hint_part) = find_hovered_hint_part( - &label_parts, + label_parts, hint_start_offset..hint_end_offset, hovered_offset, ) { @@ -1901,9 +1920,31 @@ fn update_inlay_link_and_hover_points( dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); } - if let Some(location) = &hovered_hint_part.location { - dbg!(location); // TODO kb - // go_to_definition_point = Some(location); + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = cached_hint + .position + .buffer_id + .and_then(|buffer_id| buffer.buffer(buffer_id)) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + hovered_hint.position, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } } } } @@ -1913,14 +1954,27 @@ fn update_inlay_link_and_hover_points( } } } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_at(editor, None, cx); + } } } -fn find_hovered_hint_part<'a>( - label_parts: &'a [InlayHintLabelPart], +fn find_hovered_hint_part( + label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option<&'a InlayHintLabelPart> { +) -> Option { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; for part in label_parts { diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 477eab41ac..30ed56af47 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -615,7 +615,7 @@ impl Item for Editor { fn workspace_deactivated(&mut self, cx: &mut ViewContext) { hide_link_definition(self, cx); - self.link_go_to_definition_state.last_mouse_location = None; + self.link_go_to_definition_state.last_trigger_point = None; } fn is_dirty(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 31df11a019..a001433727 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -7,16 +7,50 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_mouse_location: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, pub task: Option>>, } +pub enum GoToDefinitionTrigger { + Text(DisplayPoint), + InlayHint(Anchor, LocationLink), + None, +} + +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(Anchor, LocationLink), +} + +impl TriggerPoint { + fn anchor(&self) -> &Anchor { + match self { + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(anchor, _) => anchor, + } + } + + pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { + match self { + TriggerPoint::Text(_) => { + if shift { + LinkDefinitionKind::Type + } else { + LinkDefinitionKind::Symbol + } + } + TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + } + } +} + pub fn update_go_to_definition_link( editor: &mut Editor, - point: Option, + origin: GoToDefinitionTrigger, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -25,23 +59,30 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); - let point = point.map(|point| { - snapshot - .buffer_snapshot - .anchor_before(point.to_offset(&snapshot.display_snapshot, Bias::Left)) - }); + let trigger_point = match origin { + GoToDefinitionTrigger::Text(p) => { + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + p.to_offset(&snapshot.display_snapshot, Bias::Left), + ))) + } + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::None => None, + }; // If the new point is the same as the previously stored one, return early if let (Some(a), Some(b)) = ( - &point, - &editor.link_go_to_definition_state.last_mouse_location, + &trigger_point, + &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.cmp(b, &snapshot.buffer_snapshot).is_eq() { + if a.anchor() + .cmp(b.anchor(), &snapshot.buffer_snapshot) + .is_eq() + { return; } } - editor.link_go_to_definition_state.last_mouse_location = point.clone(); + editor.link_go_to_definition_state.last_trigger_point = trigger_point.clone(); if pending_nonempty_selection { hide_link_definition(editor, cx); @@ -49,14 +90,9 @@ pub fn update_go_to_definition_link( } if cmd_held { - if let Some(point) = point { - let kind = if shift_held { - LinkDefinitionKind::Type - } else { - LinkDefinitionKind::Symbol - }; - - show_link_definition(kind, editor, point, snapshot, cx); + if let Some(trigger_point) = trigger_point { + let kind = trigger_point.definition_kind(shift_held); + show_link_definition(kind, editor, trigger_point, snapshot, cx); return; } } @@ -73,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: Anchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -86,10 +122,11 @@ pub fn show_link_definition( return; } + let trigger_anchor = trigger_point.anchor().clone(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) - .text_anchor_for_position(trigger_point.clone(), cx) + .text_anchor_for_position(trigger_anchor.clone(), cx) { output } else { @@ -99,7 +136,7 @@ pub fn show_link_definition( let excerpt_id = if let Some((excerpt_id, _, _)) = editor .buffer() .read(cx) - .excerpt_containing(trigger_point.clone(), cx) + .excerpt_containing(trigger_anchor.clone(), cx) { excerpt_id } else { @@ -116,12 +153,12 @@ pub fn show_link_definition( if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { let point_after_start = symbol_range .start - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_le(); let point_before_end = symbol_range .end - .cmp(&trigger_point, &snapshot.buffer_snapshot) + .cmp(&trigger_anchor, &snapshot.buffer_snapshot) .is_ge(); let point_within_range = point_after_start && point_before_end; @@ -132,34 +169,45 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { - // query the LSP for definition info - let definition_request = cx.update(|cx| { - project.update(cx, |project, cx| match definition_kind { - LinkDefinitionKind::Symbol => project.definition(&buffer, buffer_position, cx), + let result = match trigger_point { + TriggerPoint::Text(_) => { + // query the LSP for definition info + cx.update(|cx| { + project.update(cx, |project, cx| match definition_kind { + LinkDefinitionKind::Symbol => { + project.definition(&buffer, buffer_position, cx) + } - LinkDefinitionKind::Type => { - project.type_definition(&buffer, buffer_position, cx) - } - }) - }); - - let result = definition_request.await.ok().map(|definition_result| { - ( - definition_result.iter().find_map(|link| { - link.origin.as_ref().map(|origin| { - let start = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); - let end = snapshot - .buffer_snapshot - .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - - start..end + LinkDefinitionKind::Type => { + project.type_definition(&buffer, buffer_position, cx) + } }) - }), - definition_result, - ) - }); + }) + .await + .ok() + .map(|definition_result| { + ( + definition_result.iter().find_map(|link| { + link.origin.as_ref().map(|origin| { + let start = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.start); + let end = snapshot + .buffer_snapshot + .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); + + start..end + }) + }), + definition_result, + ) + }) + } + TriggerPoint::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates + Some((Some(trigger_source..trigger_source), vec![trigger_target])) + } + }; this.update(&mut cx, |this, cx| { // Clear any existing highlights @@ -202,7 +250,7 @@ pub fn show_link_definition( // If no symbol range returned from language server, use the surrounding word. let highlight_range = symbol_range.unwrap_or_else(|| { let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_point); + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end) @@ -355,7 +403,13 @@ mod tests { // Press cmd+shift to trigger highlight cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, true, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + true, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -461,7 +515,13 @@ mod tests { }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -482,7 +542,7 @@ mod tests { "}); // Response without source range still highlights word - cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_mouse_location = None); + cx.update_editor(|editor, _| editor.link_go_to_definition_state.last_trigger_point = None); let mut requests = cx.handle_request::(move |url, _, _| async move { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![ lsp::LocationLink { @@ -495,7 +555,13 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -517,7 +583,13 @@ mod tests { Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); requests.next().await; cx.foreground().run_until_parked(); @@ -534,7 +606,13 @@ mod tests { fn do_work() { teˇst(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), false, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + false, + false, + cx, + ); }); cx.foreground().run_until_parked(); @@ -593,7 +671,13 @@ mod tests { // Moving the mouse restores the highlights. cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -607,7 +691,13 @@ mod tests { fn do_work() { tesˇt(); } "}); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); cx.assert_editor_text_highlights::(indoc! {" @@ -703,7 +793,13 @@ mod tests { }); }); cx.update_editor(|editor, cx| { - update_go_to_definition_link(editor, Some(hover_point), true, false, cx); + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::Text(hover_point), + true, + false, + cx, + ); }); cx.foreground().run_until_parked(); assert!(requests.try_next().is_err()); From 477fc865f5b6f2b1bf43cb4350f49eced80e4e29 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 20:21:38 +0300 Subject: [PATCH 063/142] Properly resolve inlay label parts' locations and buffers --- crates/editor/src/display_map/inlay_map.rs | 1 + crates/editor/src/element.rs | 33 +-- crates/editor/src/link_go_to_definition.rs | 43 ++-- crates/project/src/lsp_command.rs | 256 ++++++++++++++------- crates/project/src/project.rs | 21 +- 5 files changed, 222 insertions(+), 132 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 026f1fc2c2..34181a5769 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1010,6 +1010,7 @@ impl InlaySnapshot { }) { Ok(i) | Err(i) => i, }; + // TODO kb add a way to highlight inlay hints through here. for range in &ranges[start_ix..] { if range.start.cmp(&transform_end, &self.buffer).is_ge() { break; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 15b28914af..c28f14d98a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -358,18 +358,15 @@ impl EditorElement { } if !pending_nonempty_selections && cmd && text_bounds.contains_point(position) { - if let Some(point) = position_map - .point_for_position(text_bounds, position) - .as_valid() - { - if shift { - go_to_fetched_type_definition(editor, point, alt, cx); - } else { - go_to_fetched_definition(editor, point, alt, cx); - } - - return true; + let point = position_map.point_for_position(text_bounds, position); + let could_be_inlay = point.as_valid().is_none(); + if shift || could_be_inlay { + go_to_fetched_type_definition(editor, point, alt, cx); + } else { + go_to_fetched_definition(editor, point, alt, cx); } + + return true; } end_selection @@ -2818,14 +2815,24 @@ struct PositionMap { } #[derive(Debug)] -struct PointForPosition { +pub struct PointForPosition { previous_valid: DisplayPoint, - next_valid: DisplayPoint, + pub next_valid: DisplayPoint, exact_unclipped: DisplayPoint, column_overshoot_after_line_end: u32, } impl PointForPosition { + #[cfg(test)] + pub fn valid(valid: DisplayPoint) -> Self { + Self { + previous_valid: valid, + next_valid: valid, + exact_unclipped: valid, + column_overshoot_after_line_end: 0, + } + } + fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a001433727..b043af6132 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,4 @@ -use crate::{Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,7 +7,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, + pub last_trigger_point: Option, pub symbol_range: Option>, pub kind: Option, pub definitions: Vec, @@ -21,29 +21,29 @@ pub enum GoToDefinitionTrigger { } #[derive(Debug, Clone)] -pub enum TriggerPoint { +pub enum TriggerAnchor { Text(Anchor), InlayHint(Anchor, LocationLink), } -impl TriggerPoint { +impl TriggerAnchor { fn anchor(&self) -> &Anchor { match self { - TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(anchor, _) => anchor, + TriggerAnchor::Text(anchor) => anchor, + TriggerAnchor::InlayHint(anchor, _) => anchor, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, link) => LinkDefinitionKind::Type, + TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +61,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +109,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerPoint, + trigger_point: TriggerAnchor, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -170,7 +170,7 @@ pub fn show_link_definition( let task = cx.spawn(|this, mut cx| { async move { let result = match trigger_point { - TriggerPoint::Text(_) => { + TriggerAnchor::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -203,8 +203,9 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates + TriggerAnchor::InlayHint(trigger_source, trigger_target) => { + // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. + // Or highlight inlays differently, in their layer? Some((Some(trigger_source..trigger_source), vec![trigger_target])) } }; @@ -293,7 +294,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { pub fn go_to_fetched_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -302,7 +303,7 @@ pub fn go_to_fetched_definition( pub fn go_to_fetched_type_definition( editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -312,7 +313,7 @@ pub fn go_to_fetched_type_definition( fn go_to_fetched_definition_of_kind( kind: LinkDefinitionKind, editor: &mut Editor, - point: DisplayPoint, + point: PointForPosition, split: bool, cx: &mut ViewContext, ) { @@ -330,7 +331,7 @@ fn go_to_fetched_definition_of_kind( } else { editor.select( SelectPhase::Begin { - position: point, + position: point.next_valid, add: false, click_count: 1, }, @@ -460,7 +461,7 @@ mod tests { }); cx.update_editor(|editor, cx| { - go_to_fetched_type_definition(editor, hover_point, false, cx); + go_to_fetched_type_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); @@ -707,7 +708,7 @@ mod tests { // Cmd click with existing definition doesn't re-request and dismisses highlight cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); // Assert selection moved to to definition cx.lsp @@ -748,7 +749,7 @@ mod tests { ]))) }); cx.update_editor(|editor, cx| { - go_to_fetched_definition(editor, hover_point, false, cx); + go_to_fetched_definition(editor, PointForPosition::valid(hover_point), false, cx); }); requests.next().await; cx.foreground().run_until_parked(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 7b4d689a81..20bb302b5b 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -7,14 +7,15 @@ use anyhow::{anyhow, Context, Result}; use async_trait::async_trait; use client::proto::{self, PeerId}; use fs::LineEnding; +use futures::future; use gpui::{AppContext, AsyncAppContext, ModelHandle}; use language::{ language_settings::{language_settings, InlayHintKind}, point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, - Unclipped, + CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, + Transaction, Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1432,7 +1433,7 @@ impl LspCommand for GetCompletions { }) }); - Ok(futures::future::join_all(completions).await) + Ok(future::join_all(completions).await) } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetCompletions { @@ -1500,7 +1501,7 @@ impl LspCommand for GetCompletions { let completions = message.completions.into_iter().map(|completion| { language::proto::deserialize_completion(completion, language.clone()) }); - futures::future::try_join_all(completions).await + future::try_join_all(completions).await } fn buffer_id_from_proto(message: &proto::GetCompletions) -> u64 { @@ -1778,73 +1779,50 @@ impl LspCommand for OnTypeFormatting { } impl InlayHints { - pub fn lsp_to_project_hint( + pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, + project: &ModelHandle, buffer_handle: &ModelHandle, + server_id: LanguageServerId, resolve_state: ResolveState, force_no_type_left_padding: bool, - cx: &AppContext, - ) -> InlayHint { + cx: &mut AsyncAppContext, + ) -> anyhow::Result { let kind = lsp_hint.kind.and_then(|kind| match kind { lsp::InlayHintKind::TYPE => Some(InlayHintKind::Type), lsp::InlayHintKind::PARAMETER => Some(InlayHintKind::Parameter), _ => None, }); - let buffer = buffer_handle.read(cx); - let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + + let position = cx.update(|cx| { + let buffer = buffer_handle.read(cx); + let position = buffer.clip_point_utf16(point_from_lsp(lsp_hint.position), Bias::Left); + if kind == Some(InlayHintKind::Parameter) { + buffer.anchor_before(position) + } else { + buffer.anchor_after(position) + } + }); + let label = Self::lsp_inlay_label_to_project( + &buffer_handle, + project, + server_id, + lsp_hint.label, + cx, + ) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { lsp_hint.padding_left.unwrap_or(false) }; - InlayHint { - position: if kind == Some(InlayHintKind::Parameter) { - buffer.anchor_before(position) - } else { - buffer.anchor_after(position) - }, + + Ok(InlayHint { + position, padding_left, padding_right: lsp_hint.padding_right.unwrap_or(false), - label: match lsp_hint.label { - lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), - lsp::InlayHintLabel::LabelParts(lsp_parts) => InlayHintLabel::LabelParts( - lsp_parts - .into_iter() - .map(|label_part| InlayHintLabelPart { - value: label_part.value, - tooltip: label_part.tooltip.map(|tooltip| match tooltip { - lsp::InlayHintLabelPartTooltip::String(s) => { - InlayHintLabelPartTooltip::String(s) - } - lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { - InlayHintLabelPartTooltip::MarkupContent(MarkupContent { - kind: match markup_content.kind { - lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, - lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, - }, - value: markup_content.value, - }) - } - }), - location: label_part.location.map(|lsp_location| { - let target_start = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - Location { - buffer: buffer_handle.clone(), - range: buffer.anchor_after(target_start) - ..buffer.anchor_before(target_end), - } - }), - }) - .collect(), - ), - }, + label, kind, tooltip: lsp_hint.tooltip.map(|tooltip| match tooltip { lsp::InlayHintTooltip::String(s) => InlayHintTooltip::String(s), @@ -1859,7 +1837,100 @@ impl InlayHints { } }), resolve_state, - } + }) + } + + async fn lsp_inlay_label_to_project( + buffer: &ModelHandle, + project: &ModelHandle, + server_id: LanguageServerId, + lsp_label: lsp::InlayHintLabel, + cx: &mut AsyncAppContext, + ) -> anyhow::Result { + let label = match lsp_label { + lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), + lsp::InlayHintLabel::LabelParts(lsp_parts) => { + let mut parts_data = Vec::with_capacity(lsp_parts.len()); + buffer.update(cx, |buffer, cx| { + for lsp_part in lsp_parts { + let location_buffer_task = match &lsp_part.location { + Some(lsp_location) => { + let location_buffer_task = project.update(cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer, server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }); + Some(lsp_location.clone()).zip(location_buffer_task) + } + None => None, + }; + + parts_data.push((lsp_part, location_buffer_task)); + } + }); + + let mut parts = Vec::with_capacity(parts_data.len()); + for (lsp_part, location_buffer_task) in parts_data { + let location = match location_buffer_task { + Some((lsp_location, target_buffer_handle_task)) => { + let target_buffer_handle = target_buffer_handle_task + .await + .context("resolving location for label part buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Some(Location { + buffer: target_buffer_handle, + range, + }) + } + None => None, + }; + + parts.push(InlayHintLabelPart { + value: lsp_part.value, + tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { + lsp::InlayHintLabelPartTooltip::String(s) => { + InlayHintLabelPartTooltip::String(s) + } + lsp::InlayHintLabelPartTooltip::MarkupContent(markup_content) => { + InlayHintLabelPartTooltip::MarkupContent(MarkupContent { + kind: match markup_content.kind { + lsp::MarkupKind::PlainText => HoverBlockKind::PlainText, + lsp::MarkupKind::Markdown => HoverBlockKind::Markdown, + }, + value: markup_content.value, + }) + } + }), + location, + }); + } + InlayHintLabel::LabelParts(parts) + } + }; + + Ok(label) } pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { @@ -2115,15 +2186,18 @@ impl InlayHints { }) }), location: part.location.and_then(|location| { - let path = cx.read(|cx| { - let project_path = location.buffer.read(cx).project_path(cx)?; - project.read(cx).absolute_path(&project_path, cx) + let (path, location_snapshot) = cx.read(|cx| { + let buffer = location.buffer.read(cx); + let project_path = buffer.project_path(cx)?; + let location_snapshot = buffer.snapshot(); + let path = project.read(cx).absolute_path(&project_path, cx); + path.zip(Some(location_snapshot)) })?; Some(lsp::Location::new( lsp::Url::from_file_path(path).unwrap(), range_to_lsp( - location.range.start.to_point_utf16(snapshot) - ..location.range.end.to_point_utf16(snapshot), + location.range.start.to_point_utf16(&location_snapshot) + ..location.range.end.to_point_utf16(&location_snapshot), ), )) }), @@ -2182,7 +2256,7 @@ impl LspCommand for InlayHints { buffer: ModelHandle, server_id: LanguageServerId, mut cx: AsyncAppContext, - ) -> Result> { + ) -> anyhow::Result> { let (lsp_adapter, lsp_server) = language_server_for_buffer(&project, &buffer, server_id, &mut cx)?; // `typescript-language-server` adds padding to the left for type hints, turning @@ -2194,32 +2268,38 @@ impl LspCommand for InlayHints { // Hence let's use a heuristic first to handle the most awkward case and look for more. let force_no_type_left_padding = lsp_adapter.name.0.as_ref() == "typescript-language-server"; - cx.read(|cx| { - Ok(message - .unwrap_or_default() - .into_iter() - .map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => { - ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) - } - _ => ResolveState::Resolved, - }; - InlayHints::lsp_to_project_hint( - lsp_hint, - &buffer, - resolve_state, - force_no_type_left_padding, - cx, - ) - }) - .collect()) - }) + + let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { + let resolve_state = match lsp_server.capabilities().inlay_hint_provider { + Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( + lsp::InlayHintOptions { + resolve_provider: Some(true), + .. + }, + ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), + _ => ResolveState::Resolved, + }; + + let project = project.clone(); + let buffer = buffer.clone(); + cx.spawn(|mut cx| async move { + InlayHints::lsp_to_project_hint( + lsp_hint, + &project, + &buffer, + server_id, + resolve_state, + force_no_type_left_padding, + &mut cx, + ) + .await + }) + }); + future::join_all(hints) + .await + .into_iter() + .collect::>() + .context("lsp to project inlay hints conversion") } fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::InlayHints { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 65be7cceb1..fbdbd04664 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5054,22 +5054,23 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, cx| async move { + cx.spawn(|project, mut cx| async move { let resolve_task = lang_server.request::( InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; - let resolved_hint = cx.read(|cx| { - InlayHints::lsp_to_project_hint( - resolved_hint, - &buffer_handle, - ResolveState::Resolved, - false, - cx, - ) - }); + let resolved_hint = InlayHints::lsp_to_project_hint( + resolved_hint, + &project, + &buffer_handle, + server_id, + ResolveState::Resolved, + false, + &mut cx, + ) + .await?; Ok(Some(resolved_hint)) }) } else if let Some(project_id) = self.remote_id() { From 6c5761d05bccadfde383281560ad15e14f452c24 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 21 Aug 2023 23:21:37 +0300 Subject: [PATCH 064/142] Pass inlay highlight information --- crates/editor/src/display_map.rs | 39 +++++- crates/editor/src/display_map/block_map.rs | 14 +- crates/editor/src/display_map/fold_map.rs | 27 ++-- crates/editor/src/display_map/inlay_map.rs | 109 ++++++++-------- crates/editor/src/display_map/tab_map.rs | 36 ++++-- crates/editor/src/display_map/wrap_map.rs | 16 ++- crates/editor/src/editor.rs | 31 ++++- crates/editor/src/element.rs | 9 +- crates/editor/src/link_go_to_definition.rs | 143 +++++++++++++-------- 9 files changed, 279 insertions(+), 145 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9159253142..9df2919351 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -4,7 +4,10 @@ mod inlay_map; mod tab_map; mod wrap_map; -use crate::{Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use crate::{ + link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + MultiBufferSnapshot, ToOffset, ToPoint, +}; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; use fold_map::FoldMap; @@ -40,6 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -50,6 +54,7 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -85,6 +90,7 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), + inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -109,6 +115,7 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), + inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -215,6 +222,16 @@ impl DisplayMap { .insert(Some(type_id), Arc::new((style, ranges))); } + pub fn highlight_inlays( + &mut self, + type_id: TypeId, + ranges: Vec, + style: HighlightStyle, + ) { + self.inlay_highlights + .insert(Some(type_id), Arc::new((style, ranges))); + } + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) @@ -227,6 +244,13 @@ impl DisplayMap { self.text_highlights.remove(&Some(type_id)) } + pub fn clear_inlay_highlights( + &mut self, + type_id: TypeId, + ) -> Option)>> { + self.inlay_highlights.remove(&Some(type_id)) + } + pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -296,6 +320,7 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, + inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -421,6 +446,7 @@ impl DisplaySnapshot { None, None, None, + None, ) .map(|h| h.text) } @@ -429,7 +455,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None) + .chunks(row..row + 1, false, None, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -441,15 +467,16 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - hint_highlights, - suggestion_highlights, + Some(&self.inlay_highlights), + inlay_highlight_style, + suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 4b76ded3d5..083d8be5f3 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,6 +579,7 @@ impl BlockSnapshot { None, None, None, + None, ) .map(|chunk| chunk.text) .collect() @@ -589,8 +590,9 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); let mut cursor = self.transforms.cursor::<(BlockRow, WrapRow)>(); @@ -623,8 +625,9 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), transforms: cursor, @@ -1504,6 +1507,7 @@ mod tests { None, None, None, + None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 0b1523fe75..800b51fcd8 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) .map(|c| c.text) .collect() } @@ -652,8 +652,9 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,8 +676,9 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), inlay_chunk: None, inlay_offset: inlay_start, @@ -687,8 +689,15 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks(start.to_offset(self)..self.len(), false, None, None, None) - .flat_map(|chunk| chunk.text.chars()) + self.chunks( + start.to_offset(self)..self.len(), + false, + None, + None, + None, + None, + ) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1496,7 +1505,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 34181a5769..5617b6a6b6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -15,7 +15,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::TextHighlights; +use super::{InlayHighlights, TextHighlights}; pub struct InlayMap { snapshot: InlaySnapshot, @@ -973,8 +973,9 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); @@ -983,53 +984,50 @@ impl InlaySnapshot { if let Some(text_highlights) = text_highlights { if !text_highlights.is_empty() { while cursor.start().0 < range.end { - if true { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + for (tag, highlights) in text_highlights.iter() { + let style = highlights.0; + let ranges = &highlights.1; - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self - .to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + // TODO kb add a way to highlight inlay hints through here. + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } } @@ -1050,8 +1048,8 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: hint_highlights, - suggestion_highlight_style: suggestion_highlights, + hint_highlight_style: inlay_highlight_style, + suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), snapshot: self, @@ -1060,9 +1058,16 @@ impl InlaySnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(Default::default()..self.len(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + Default::default()..self.len(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1636,6 +1641,8 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), + // TODO kb add tests + None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index ca73f6a1a7..187a8de1d3 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,6 +71,7 @@ impl TabMap { None, None, None, + None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -183,7 +184,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None) + .chunks(range.start..line_end, false, None, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -203,6 +204,7 @@ impl TabSnapshot { None, None, None, + None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -223,9 +225,11 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, + // TODO kb extract into one struct text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = self.to_fold_point(range.start, Bias::Left); @@ -246,8 +250,9 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_column, column: expanded_char_column, @@ -270,9 +275,16 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) - .map(|chunk| chunk.text) - .collect() + self.chunks( + TabPoint::zero()..self.max_point(), + false, + None, + None, + None, + None, + ) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -600,6 +612,7 @@ mod tests { None, None, None, + None, ) .map(|c| c.text) .collect::(), @@ -674,7 +687,8 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) + { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -743,7 +757,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None) + .chunks(start..end, false, None, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f21c7151ad..f8287af799 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - TextHighlights, + InlayHighlights, TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,6 +447,7 @@ impl WrapSnapshot { None, None, None, + None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -576,8 +577,9 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - hint_highlights: Option, - suggestion_highlights: Option, + inlay_highlights: Option<&'a InlayHighlights>, + inlay_highlight_style: Option, + suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); let output_end = WrapPoint::new(rows.end, 0); @@ -595,8 +597,9 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - hint_highlights, - suggestion_highlights, + inlay_highlights, + inlay_highlight_style, + suggestion_highlight_style, ), input_chunk: Default::default(), output_position: output_start, @@ -1326,6 +1329,7 @@ mod tests { None, None, None, + None, ) .map(|h| h.text) } @@ -1350,7 +1354,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None) + .chunks(start_row..end_row, true, None, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index fb1e87580a..f10734a76c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -64,7 +64,9 @@ use language::{ Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; -use link_go_to_definition::{hide_link_definition, show_link_definition, LinkGoToDefinitionState}; +use link_go_to_definition::{ + hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, +}; use log::error; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ @@ -7716,6 +7718,18 @@ impl Editor { cx.notify(); } + pub fn highlight_inlays( + &mut self, + ranges: Vec, + style: HighlightStyle, + cx: &mut ViewContext, + ) { + self.display_map.update(cx, |map, _| { + map.highlight_inlays(TypeId::of::(), ranges, style) + }); + cx.notify(); + } + pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, @@ -7736,6 +7750,19 @@ impl Editor { highlights } + pub fn clear_highlights( + &mut self, + cx: &mut ViewContext, + ) -> Option>)>> { + let highlights = self + .display_map + .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); + if highlights.is_some() { + cx.notify(); + } + highlights + } + pub fn show_local_cursors(&self, cx: &AppContext) -> bool { self.blink_manager.read(cx).visible() && self.focused } @@ -8327,7 +8354,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_text_highlights::(cx); + self.clear_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c28f14d98a..c448ba4f22 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, + GoToDefinitionTrigger, InlayCoordinates, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1927,7 +1927,12 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - hovered_hint.position, + InlayCoordinates { + inlay_id: hovered_hint.id, + inlay_position: hovered_hint.position, + inlay_start: hint_start_offset, + highlight_end: hovered_offset, + }, LocationLink { origin: Some(Location { buffer, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index b043af6132..35e367497e 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,4 +1,7 @@ -use crate::{element::PointForPosition, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase}; +use crate::{ + display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, + EditorSnapshot, InlayId, SelectPhase, +}; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; use project::LocationLink; @@ -7,8 +10,8 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { - pub last_trigger_point: Option, - pub symbol_range: Option>, + pub last_trigger_point: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -16,34 +19,65 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(Anchor, LocationLink), + InlayHint(InlayCoordinates, LocationLink), None, } -#[derive(Debug, Clone)] -pub enum TriggerAnchor { - Text(Anchor), - InlayHint(Anchor, LocationLink), +#[derive(Debug, Clone, Copy)] +pub struct InlayCoordinates { + pub inlay_id: InlayId, + pub inlay_position: Anchor, + pub inlay_start: InlayOffset, + pub highlight_end: InlayOffset, } -impl TriggerAnchor { +#[derive(Debug, Clone)] +pub enum TriggerPoint { + Text(Anchor), + InlayHint(InlayCoordinates, LocationLink), +} + +#[derive(Debug, Clone)] +pub enum SymbolRange { + Text(Range), + Inlay(InlayCoordinates), +} + +impl SymbolRange { + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { + match (self, trigger_point) { + (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); + point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() + } + (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + range.inlay_start.cmp(&point.highlight_end).is_le() + && range.highlight_end.cmp(&point.highlight_end).is_ge() + } + (SymbolRange::Inlay(_), TriggerPoint::Text(_)) + | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + } + } +} + +impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { - TriggerAnchor::Text(anchor) => anchor, - TriggerAnchor::InlayHint(anchor, _) => anchor, + TriggerPoint::Text(anchor) => anchor, + TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, } } pub fn definition_kind(&self, shift: bool) -> LinkDefinitionKind { match self { - TriggerAnchor::Text(_) => { + TriggerPoint::Text(_) => { if shift { LinkDefinitionKind::Type } else { LinkDefinitionKind::Symbol } } - TriggerAnchor::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, } } } @@ -61,11 +95,11 @@ pub fn update_go_to_definition_link( let snapshot = editor.snapshot(cx); let trigger_point = match origin { GoToDefinitionTrigger::Text(p) => { - Some(TriggerAnchor::Text(snapshot.buffer_snapshot.anchor_before( + Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerAnchor::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), GoToDefinitionTrigger::None => None, }; @@ -109,7 +143,7 @@ pub enum LinkDefinitionKind { pub fn show_link_definition( definition_kind: LinkDefinitionKind, editor: &mut Editor, - trigger_point: TriggerAnchor, + trigger_point: TriggerPoint, snapshot: EditorSnapshot, cx: &mut ViewContext, ) { @@ -122,7 +156,7 @@ pub fn show_link_definition( return; } - let trigger_anchor = trigger_point.anchor().clone(); + let trigger_anchor = trigger_point.anchor(); let (buffer, buffer_position) = if let Some(output) = editor .buffer .read(cx) @@ -151,26 +185,15 @@ pub fn show_link_definition( // Don't request again if the location is within the symbol region of a previous request with the same kind if let Some(symbol_range) = &editor.link_go_to_definition_state.symbol_range { - let point_after_start = symbol_range - .start - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_le(); - - let point_before_end = symbol_range - .end - .cmp(&trigger_anchor, &snapshot.buffer_snapshot) - .is_ge(); - - let point_within_range = point_after_start && point_before_end; - if point_within_range && same_kind { + if same_kind && symbol_range.point_within_range(&trigger_point, &snapshot) { return; } } let task = cx.spawn(|this, mut cx| { async move { - let result = match trigger_point { - TriggerAnchor::Text(_) => { + let result = match &trigger_point { + TriggerPoint::Text(_) => { // query the LSP for definition info cx.update(|cx| { project.update(cx, |project, cx| match definition_kind { @@ -196,23 +219,22 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - start..end + SymbolRange::Text(start..end) }) }), definition_result, ) }) } - TriggerAnchor::InlayHint(trigger_source, trigger_target) => { - // TODO kb range is wrong, should be in inlay coordinates have a proper inlay range. - // Or highlight inlays differently, in their layer? - Some((Some(trigger_source..trigger_source), vec![trigger_target])) - } + TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( + Some(SymbolRange::Inlay(trigger_source.clone())), + vec![trigger_target.clone()], + )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_text_highlights::(cx); + this.clear_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -248,22 +270,37 @@ pub fn show_link_definition( }); if any_definition_does_not_contain_current_location { - // If no symbol range returned from language server, use the surrounding word. - let highlight_range = symbol_range.unwrap_or_else(|| { - let snapshot = &snapshot.buffer_snapshot; - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end) - }); - // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - this.highlight_text::( - vec![highlight_range], - style, - cx, - ); + let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); + SymbolRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_trigger, _) => { + SymbolRange::Inlay(inlay_trigger) + } + }); + + match highlight_range { + SymbolRange::Text(text_range) => this + .highlight_text::( + vec![text_range], + style, + cx, + ), + SymbolRange::Inlay(inlay_coordinates) => this + .highlight_inlays::( + vec![inlay_coordinates], + style, + cx, + ), + } } else { hide_link_definition(this, cx); } @@ -289,7 +326,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_text_highlights::(cx); + editor.clear_highlights::(cx); } pub fn go_to_fetched_definition( From f8874a726cc4a3fcc09613d8faa7a215ad8dc915 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 01:02:25 +0300 Subject: [PATCH 065/142] Attempt to highlight inlays --- crates/editor/src/display_map/inlay_map.rs | 168 +++++++++++++++------ crates/editor/src/editor.rs | 13 +- crates/editor/src/element.rs | 23 +-- crates/editor/src/link_go_to_definition.rs | 11 +- 4 files changed, 143 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5617b6a6b6..23b39ca5ab 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -2,7 +2,7 @@ use crate::{ multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet}; +use collections::{BTreeMap, BTreeSet, HashSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -183,7 +183,7 @@ pub struct InlayBufferRows<'a> { max_buffer_row: u32, } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] struct HighlightEndpoint { offset: InlayOffset, is_start: bool, @@ -243,6 +243,7 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } + // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -980,62 +981,89 @@ impl InlaySnapshot { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); + let empty_text_highlights = TextHighlights::default(); + let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); + let empty_inlay_highlights = InlayHighlights::default(); + let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); + let mut highlight_endpoints = Vec::new(); - if let Some(text_highlights) = text_highlights { - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self.buffer.anchor_after( - self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), - ); + if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self + .buffer + .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) + }; - for (tag, highlights) in text_highlights.iter() { - let style = highlights.0; - let ranges = &highlights.1; + let mut covered_tags = HashSet::default(); + for (tag, text_highlights) in text_highlights.iter() { + covered_tags.insert(*tag); + let style = text_highlights.0; + let ranges = &text_highlights.1; - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - // TODO kb add a way to highlight inlay hints through here. - for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { - break; - } - - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), - is_start: false, - tag: *tag, - style, - }); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&transform_start, &self.buffer); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&transform_end, &self.buffer).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + is_start: false, + tag: *tag, + style, + }); } - cursor.next(&()); + if let Some(inlay_highlights) = inlay_highlights.get(tag) { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); + } } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); + + for (tag, inlay_highlights) in inlay_highlights + .iter() + .filter(|(tag, _)| !covered_tags.contains(tag)) + { + self.push_inlay_highlight_range( + inlay_highlights, + transform_start, + transform_end, + &mut highlight_endpoints, + tag, + ); + } + + cursor.next(&()); } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1056,6 +1084,48 @@ impl InlaySnapshot { } } + fn push_inlay_highlight_range( + &self, + inlay_highlights: &std::sync::Arc<( + HighlightStyle, + Vec, + )>, + transform_start: Anchor, + transform_end: Anchor, + highlight_endpoints: &mut Vec, + tag: &Option, + ) { + let style = inlay_highlights.0; + let ranges = &inlay_highlights.1; + let start_ix = match ranges + .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) + { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range + .inlay_position + .cmp(&transform_end, &self.buffer) + .is_ge() + { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.highlight_end, + is_start: false, + tag: *tag, + style, + }); + } + } + #[cfg(test)] pub fn text(&self) -> String { self.chunks( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f10734a76c..6c5d1a45ac 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7750,17 +7750,16 @@ impl Editor { highlights } - pub fn clear_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self + pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { + let inlay_highlights = self + .display_map + .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); + if text_highlights.is_some() || inlay_highlights.is_some() { cx.notify(); } - highlights } pub fn show_local_cursors(&self, cx: &AppContext) -> bool { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index c448ba4f22..10809d8962 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1908,11 +1908,13 @@ fn update_inlay_link_and_hover_points( } } project::InlayHintLabel::LabelParts(label_parts) => { - if let Some(hovered_hint_part) = find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) { + if let Some((hovered_hint_part, part_range)) = + find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { if hovered_hint_part.tooltip.is_some() { dbg!(&hovered_hint_part.tooltip); // TODO kb // hover_at_point = Some(hovered_offset); @@ -1928,10 +1930,9 @@ fn update_inlay_link_and_hover_points( editor, GoToDefinitionTrigger::InlayHint( InlayCoordinates { - inlay_id: hovered_hint.id, inlay_position: hovered_hint.position, - inlay_start: hint_start_offset, - highlight_end: hovered_offset, + highlight_start: part_range.start, + highlight_end: part_range.end, }, LocationLink { origin: Some(Location { @@ -1976,15 +1977,17 @@ fn find_hovered_hint_part( label_parts: Vec, hint_range: Range, hovered_offset: InlayOffset, -) -> Option { +) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; + part_start.0 += part_len; } else { - return Some(part); + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 35e367497e..ee2b536042 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,6 +1,6 @@ use crate::{ display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, InlayId, SelectPhase, + EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; @@ -25,9 +25,8 @@ pub enum GoToDefinitionTrigger { #[derive(Debug, Clone, Copy)] pub struct InlayCoordinates { - pub inlay_id: InlayId, pub inlay_position: Anchor, - pub inlay_start: InlayOffset, + pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, } @@ -51,7 +50,7 @@ impl SymbolRange { point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { - range.inlay_start.cmp(&point.highlight_end).is_le() + range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (SymbolRange::Inlay(_), TriggerPoint::Text(_)) @@ -282,8 +281,8 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_trigger, _) => { - SymbolRange::Inlay(inlay_trigger) + TriggerPoint::InlayHint(inlay_coordinates, _) => { + SymbolRange::Inlay(inlay_coordinates) } }); From 4cc9f2f525e9e2991f1c71cf5a2582bfa492ec83 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 11:34:40 +0300 Subject: [PATCH 066/142] Highlight inlay hint parts on cmd-hover Co-Authored-By: Antonio --- crates/editor/src/display_map/inlay_map.rs | 24 ++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 23b39ca5ab..19b3ec7924 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -210,6 +210,7 @@ pub struct InlayChunks<'a> { buffer_chunks: MultiBufferChunks<'a>, buffer_chunk: Option>, inlay_chunks: Option>, + inlay_chunk: Option<&'a str>, output_offset: InlayOffset, max_output_offset: InlayOffset, hint_highlight_style: Option, @@ -298,13 +299,31 @@ impl<'a> Iterator for InlayChunks<'a> { - self.transforms.start().0; inlay.text.chunks_in_range(start.0..end.0) }); + let inlay_chunk = self + .inlay_chunk + .get_or_insert_with(|| inlay_chunks.next().unwrap()); + let (chunk, remainder) = inlay_chunk.split_at( + inlay_chunk + .len() + .min(next_highlight_endpoint.0 - self.output_offset.0), + ); + *inlay_chunk = remainder; + if inlay_chunk.is_empty() { + self.inlay_chunk = None; + } - let chunk = inlay_chunks.next().unwrap(); self.output_offset.0 += chunk.len(); - let highlight_style = match inlay.id { + let mut highlight_style = match inlay.id { InlayId::Suggestion(_) => self.suggestion_highlight_style, InlayId::Hint(_) => self.hint_highlight_style, }; + if !self.active_highlights.is_empty() { + for active_highlight in self.active_highlights.values() { + highlight_style + .get_or_insert(Default::default()) + .highlight(*active_highlight); + } + } Chunk { text: chunk, highlight_style, @@ -1073,6 +1092,7 @@ impl InlaySnapshot { transforms: cursor, buffer_chunks, inlay_chunks: None, + inlay_chunk: None, buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, From 420f8b7b1597e7ea132cba5ffd4f5459834aef9d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:06:48 +0300 Subject: [PATCH 067/142] Prepare for inlay and text highlight unification --- crates/editor/src/display_map.rs | 8 ++--- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/editor.rs | 21 +++--------- crates/editor/src/element.rs | 4 +-- crates/editor/src/link_go_to_definition.rs | 38 +++++++++++----------- 5 files changed, 30 insertions(+), 43 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 9df2919351..4f08be73b9 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,7 +5,7 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayCoordinates, Anchor, AnchorRangeExt, InlayId, MultiBuffer, + link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; @@ -43,7 +43,7 @@ pub trait ToDisplayPoint { } type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -225,7 +225,7 @@ impl DisplayMap { pub fn highlight_inlays( &mut self, type_id: TypeId, - ranges: Vec, + ranges: Vec, style: HighlightStyle, ) { self.inlay_highlights @@ -247,7 +247,7 @@ impl DisplayMap { pub fn clear_inlay_highlights( &mut self, type_id: TypeId, - ) -> Option)>> { + ) -> Option)>> { self.inlay_highlights.remove(&Some(type_id)) } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 19b3ec7924..cc730f9333 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1108,7 +1108,7 @@ impl InlaySnapshot { &self, inlay_highlights: &std::sync::Arc<( HighlightStyle, - Vec, + Vec, )>, transform_start: Anchor, transform_end: Anchor, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 6c5d1a45ac..af352d2649 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayCoordinates, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7720,7 +7720,7 @@ impl Editor { pub fn highlight_inlays( &mut self, - ranges: Vec, + ranges: Vec, style: HighlightStyle, cx: &mut ViewContext, ) { @@ -7737,20 +7737,7 @@ impl Editor { self.display_map.read(cx).text_highlights(TypeId::of::()) } - pub fn clear_text_highlights( - &mut self, - cx: &mut ViewContext, - ) -> Option>)>> { - let highlights = self - .display_map - .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - if highlights.is_some() { - cx.notify(); - } - highlights - } - - pub fn clear_highlights(&mut self, cx: &mut ViewContext) { + pub fn clear_text_highlights(&mut self, cx: &mut ViewContext) { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); @@ -8353,7 +8340,7 @@ impl View for Editor { self.link_go_to_definition_state.task = None; - self.clear_highlights::(cx); + self.clear_text_highlights::(cx); } false diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 10809d8962..2be5105b33 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -13,7 +13,7 @@ use crate::{ }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayCoordinates, + GoToDefinitionTrigger, InlayRange, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -1929,7 +1929,7 @@ fn update_inlay_link_and_hover_points( update_go_to_definition_link( editor, GoToDefinitionTrigger::InlayHint( - InlayCoordinates { + InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, highlight_end: part_range.end, diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ee2b536042..5bc45720a2 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -11,7 +11,7 @@ use util::TryFutureExt; #[derive(Debug, Default)] pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, - pub symbol_range: Option, + pub symbol_range: Option, pub kind: Option, pub definitions: Vec, pub task: Option>>, @@ -19,12 +19,12 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), None, } #[derive(Debug, Clone, Copy)] -pub struct InlayCoordinates { +pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, pub highlight_end: InlayOffset, @@ -33,28 +33,28 @@ pub struct InlayCoordinates { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayCoordinates, LocationLink), + InlayHint(InlayRange, LocationLink), } #[derive(Debug, Clone)] -pub enum SymbolRange { +pub enum DocumentRange { Text(Range), - Inlay(InlayCoordinates), + Inlay(InlayRange), } -impl SymbolRange { +impl DocumentRange { fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { - (SymbolRange::Text(range), TriggerPoint::Text(point)) => { + (DocumentRange::Text(range), TriggerPoint::Text(point)) => { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (SymbolRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } - (SymbolRange::Inlay(_), TriggerPoint::Text(_)) - | (SymbolRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + (DocumentRange::Inlay(_), TriggerPoint::Text(_)) + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, } } } @@ -218,7 +218,7 @@ pub fn show_link_definition( .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - SymbolRange::Text(start..end) + DocumentRange::Text(start..end) }) }), definition_result, @@ -226,14 +226,14 @@ pub fn show_link_definition( }) } TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(SymbolRange::Inlay(trigger_source.clone())), + Some(DocumentRange::Inlay(trigger_source.clone())), vec![trigger_target.clone()], )), }; this.update(&mut cx, |this, cx| { // Clear any existing highlights - this.clear_highlights::(cx); + this.clear_text_highlights::(cx); this.link_go_to_definition_state.kind = Some(definition_kind); this.link_go_to_definition_state.symbol_range = result .as_ref() @@ -276,24 +276,24 @@ pub fn show_link_definition( let snapshot = &snapshot.buffer_snapshot; // If no symbol range returned from language server, use the surrounding word. let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - SymbolRange::Text( + DocumentRange::Text( snapshot.anchor_before(offset_range.start) ..snapshot.anchor_after(offset_range.end), ) } TriggerPoint::InlayHint(inlay_coordinates, _) => { - SymbolRange::Inlay(inlay_coordinates) + DocumentRange::Inlay(inlay_coordinates) } }); match highlight_range { - SymbolRange::Text(text_range) => this + DocumentRange::Text(text_range) => this .highlight_text::( vec![text_range], style, cx, ), - SymbolRange::Inlay(inlay_coordinates) => this + DocumentRange::Inlay(inlay_coordinates) => this .highlight_inlays::( vec![inlay_coordinates], style, @@ -325,7 +325,7 @@ pub fn hide_link_definition(editor: &mut Editor, cx: &mut ViewContext) { editor.link_go_to_definition_state.task = None; - editor.clear_highlights::(cx); + editor.clear_text_highlights::(cx); } pub fn go_to_fetched_definition( From 12ffbe54fb2c07ceafd2722aea9ee767a31e8c72 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 22 Aug 2023 22:38:49 +0300 Subject: [PATCH 068/142] Unify text and inlay highlights --- crates/editor/src/display_map.rs | 43 +++---- crates/editor/src/display_map/block_map.rs | 6 +- crates/editor/src/display_map/fold_map.rs | 19 +-- crates/editor/src/display_map/inlay_map.rs | 113 +++++------------- crates/editor/src/display_map/tab_map.rs | 29 ++--- crates/editor/src/display_map/wrap_map.rs | 8 +- crates/editor/src/editor.rs | 13 +- crates/editor/src/link_go_to_definition.rs | 7 ++ crates/editor/src/test/editor_test_context.rs | 1 + 9 files changed, 76 insertions(+), 163 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 4f08be73b9..037854435b 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -5,8 +5,8 @@ mod tab_map; mod wrap_map; use crate::{ - link_go_to_definition::InlayRange, Anchor, AnchorRangeExt, InlayId, MultiBuffer, - MultiBufferSnapshot, ToOffset, ToPoint, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, InlayId, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; pub use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; @@ -42,8 +42,7 @@ pub trait ToDisplayPoint { fn to_display_point(&self, map: &DisplaySnapshot) -> DisplayPoint; } -type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec>)>>; -type InlayHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; +type TextHighlights = TreeMap, Arc<(HighlightStyle, Vec)>>; pub struct DisplayMap { buffer: ModelHandle, @@ -54,7 +53,6 @@ pub struct DisplayMap { wrap_map: ModelHandle, block_map: BlockMap, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, pub clip_at_line_ends: bool, } @@ -90,7 +88,6 @@ impl DisplayMap { wrap_map, block_map, text_highlights: Default::default(), - inlay_highlights: Default::default(), clip_at_line_ends: false, } } @@ -115,7 +112,6 @@ impl DisplayMap { wrap_snapshot, block_snapshot, text_highlights: self.text_highlights.clone(), - inlay_highlights: self.inlay_highlights.clone(), clip_at_line_ends: self.clip_at_line_ends, } } @@ -218,8 +214,10 @@ impl DisplayMap { ranges: Vec>, style: HighlightStyle, ) { - self.text_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new((style, ranges.into_iter().map(DocumentRange::Text).collect())), + ); } pub fn highlight_inlays( @@ -228,11 +226,16 @@ impl DisplayMap { ranges: Vec, style: HighlightStyle, ) { - self.inlay_highlights - .insert(Some(type_id), Arc::new((style, ranges))); + self.text_highlights.insert( + Some(type_id), + Arc::new(( + style, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + )), + ); } - pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[Range])> { + pub fn text_highlights(&self, type_id: TypeId) -> Option<(HighlightStyle, &[DocumentRange])> { let highlights = self.text_highlights.get(&Some(type_id))?; Some((highlights.0, &highlights.1)) } @@ -240,17 +243,10 @@ impl DisplayMap { pub fn clear_text_highlights( &mut self, type_id: TypeId, - ) -> Option>)>> { + ) -> Option)>> { self.text_highlights.remove(&Some(type_id)) } - pub fn clear_inlay_highlights( - &mut self, - type_id: TypeId, - ) -> Option)>> { - self.inlay_highlights.remove(&Some(type_id)) - } - pub fn set_font(&self, font_id: FontId, font_size: f32, cx: &mut ModelContext) -> bool { self.wrap_map .update(cx, |map, cx| map.set_font(font_id, font_size, cx)) @@ -320,7 +316,6 @@ pub struct DisplaySnapshot { wrap_snapshot: wrap_map::WrapSnapshot, block_snapshot: block_map::BlockSnapshot, text_highlights: TextHighlights, - inlay_highlights: InlayHighlights, clip_at_line_ends: bool, } @@ -446,7 +441,6 @@ impl DisplaySnapshot { None, None, None, - None, ) .map(|h| h.text) } @@ -455,7 +449,7 @@ impl DisplaySnapshot { pub fn reverse_text_chunks(&self, display_row: u32) -> impl Iterator { (0..=display_row).into_iter().rev().flat_map(|row| { self.block_snapshot - .chunks(row..row + 1, false, None, None, None, None) + .chunks(row..row + 1, false, None, None, None) .map(|h| h.text) .collect::>() .into_iter() @@ -474,7 +468,6 @@ impl DisplaySnapshot { display_rows, language_aware, Some(&self.text_highlights), - Some(&self.inlay_highlights), inlay_highlight_style, suggestion_highlight_style, ) @@ -797,7 +790,7 @@ impl DisplaySnapshot { #[cfg(any(test, feature = "test-support"))] pub fn highlight_ranges( &self, - ) -> Option>)>> { + ) -> Option)>> { let type_id = TypeId::of::(); self.text_highlights.get(&Some(type_id)).cloned() } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 083d8be5f3..8577e92819 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1,6 +1,6 @@ use super::{ wrap_map::{self, WrapEdit, WrapPoint, WrapSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; @@ -579,7 +579,6 @@ impl BlockSnapshot { None, None, None, - None, ) .map(|chunk| chunk.text) .collect() @@ -590,7 +589,6 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { @@ -625,7 +623,6 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1507,7 +1504,6 @@ mod tests { None, None, None, - None, ) .map(|chunk| chunk.text) .collect::(); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 800b51fcd8..dcbc156c47 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -1,6 +1,6 @@ use super::{ inlay_map::{InlayBufferRows, InlayChunks, InlayEdit, InlayOffset, InlayPoint, InlaySnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::{Anchor, AnchorRangeExt, MultiBufferSnapshot, ToOffset}; use gpui::{color::Color, fonts::HighlightStyle}; @@ -475,7 +475,7 @@ pub struct FoldSnapshot { impl FoldSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks(FoldOffset(0)..self.len(), false, None, None, None, None) + self.chunks(FoldOffset(0)..self.len(), false, None, None, None) .map(|c| c.text) .collect() } @@ -652,7 +652,6 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { @@ -676,7 +675,6 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -689,15 +687,8 @@ impl FoldSnapshot { } pub fn chars_at(&self, start: FoldPoint) -> impl '_ + Iterator { - self.chunks( - start.to_offset(self)..self.len(), - false, - None, - None, - None, - None, - ) - .flat_map(|chunk| chunk.text.chars()) + self.chunks(start.to_offset(self)..self.len(), false, None, None, None) + .flat_map(|chunk| chunk.text.chars()) } #[cfg(test)] @@ -1505,7 +1496,7 @@ mod tests { let text = &expected_text[start.0..end.0]; assert_eq!( snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), text, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index cc730f9333..06d166b86c 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1,8 +1,9 @@ use crate::{ + link_go_to_definition::DocumentRange, multi_buffer::{MultiBufferChunks, MultiBufferRows}, Anchor, InlayId, MultiBufferSnapshot, ToOffset, }; -use collections::{BTreeMap, BTreeSet, HashSet}; +use collections::{BTreeMap, BTreeSet}; use gpui::fonts::HighlightStyle; use language::{Chunk, Edit, Point, TextSummary}; use std::{ @@ -15,7 +16,7 @@ use std::{ use sum_tree::{Bias, Cursor, SumTree}; use text::{Patch, Rope}; -use super::{InlayHighlights, TextHighlights}; +use super::TextHighlights; pub struct InlayMap { snapshot: InlaySnapshot, @@ -244,7 +245,6 @@ impl<'a> Iterator for InlayChunks<'a> { return None; } - // TODO kb highlights are not displayed still let mut next_highlight_endpoint = InlayOffset(usize::MAX); while let Some(endpoint) = self.highlight_endpoints.peek().copied() { if endpoint.offset <= self.output_offset { @@ -993,7 +993,6 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { @@ -1002,15 +1001,14 @@ impl InlaySnapshot { let empty_text_highlights = TextHighlights::default(); let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let empty_inlay_highlights = InlayHighlights::default(); - let inlay_highlights = inlay_highlights.unwrap_or_else(|| &empty_inlay_highlights); let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() || !inlay_highlights.is_empty() { + if !text_highlights.is_empty() { while cursor.start().0 < range.end { let transform_start = self .buffer .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); + let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); let transform_end = { let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); @@ -1019,15 +1017,17 @@ impl InlaySnapshot { cursor.start().0 + overshoot, ))) }; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - let mut covered_tags = HashSet::default(); for (tag, text_highlights) in text_highlights.iter() { - covered_tags.insert(*tag); let style = text_highlights.0; let ranges = &text_highlights.1; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&transform_start, &self.buffer); + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); if cmp.is_gt() { cmp::Ordering::Greater } else { @@ -1037,46 +1037,24 @@ impl InlaySnapshot { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&transform_end, &self.buffer).is_ge() { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { break; } highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.start.to_offset(&self.buffer)), + offset: range.start, is_start: true, tag: *tag, style, }); highlight_endpoints.push(HighlightEndpoint { - offset: self.to_inlay_offset(range.end.to_offset(&self.buffer)), + offset: range.end, is_start: false, tag: *tag, style, }); } - - if let Some(inlay_highlights) = inlay_highlights.get(tag) { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); - } - } - - for (tag, inlay_highlights) in inlay_highlights - .iter() - .filter(|(tag, _)| !covered_tags.contains(tag)) - { - self.push_inlay_highlight_range( - inlay_highlights, - transform_start, - transform_end, - &mut highlight_endpoints, - tag, - ); } cursor.next(&()); @@ -1104,60 +1082,23 @@ impl InlaySnapshot { } } - fn push_inlay_highlight_range( - &self, - inlay_highlights: &std::sync::Arc<( - HighlightStyle, - Vec, - )>, - transform_start: Anchor, - transform_end: Anchor, - highlight_endpoints: &mut Vec, - tag: &Option, - ) { - let style = inlay_highlights.0; - let ranges = &inlay_highlights.1; - let start_ix = match ranges - .binary_search_by(|probe| probe.inlay_position.cmp(&transform_start, &self.buffer)) - { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range - .inlay_position - .cmp(&transform_end, &self.buffer) - .is_ge() - { - break; + fn document_to_inlay_range(&self, range: &DocumentRange) -> Range { + match range { + DocumentRange::Text(text_range) => { + self.to_inlay_offset(text_range.start.to_offset(&self.buffer)) + ..self.to_inlay_offset(text_range.end.to_offset(&self.buffer)) + } + DocumentRange::Inlay(inlay_range) => { + inlay_range.highlight_start..inlay_range.highlight_end } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.highlight_end, - is_start: false, - tag: *tag, - style, - }); } } #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - Default::default()..self.len(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(Default::default()..self.len(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } fn check_invariants(&self) { @@ -1651,6 +1592,8 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) + // TODO add inlay highlight tests + .map(DocumentRange::Text) .collect::>(); highlights.insert( @@ -1731,8 +1674,6 @@ mod tests { InlayOffset(start)..InlayOffset(end), false, Some(&highlights), - // TODO kb add tests - None, None, None, ) diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index 187a8de1d3..cae9ccc91f 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,6 +1,6 @@ use super::{ fold_map::{self, FoldChunks, FoldEdit, FoldPoint, FoldSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::fonts::HighlightStyle; @@ -71,7 +71,6 @@ impl TabMap { None, None, None, - None, ) { for (ix, _) in chunk.text.match_indices('\t') { let offset_from_edit = offset_from_edit + (ix as u32); @@ -184,7 +183,7 @@ impl TabSnapshot { self.max_point() }; for c in self - .chunks(range.start..line_end, false, None, None, None, None) + .chunks(range.start..line_end, false, None, None, None) .flat_map(|chunk| chunk.text.chars()) { if c == '\n' { @@ -204,7 +203,6 @@ impl TabSnapshot { None, None, None, - None, ) .flat_map(|chunk| chunk.text.chars()) { @@ -225,9 +223,8 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct + // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { @@ -250,7 +247,6 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -275,16 +271,9 @@ impl TabSnapshot { #[cfg(test)] pub fn text(&self) -> String { - self.chunks( - TabPoint::zero()..self.max_point(), - false, - None, - None, - None, - None, - ) - .map(|chunk| chunk.text) - .collect() + self.chunks(TabPoint::zero()..self.max_point(), false, None, None, None) + .map(|chunk| chunk.text) + .collect() } pub fn max_point(&self) -> TabPoint { @@ -612,7 +601,6 @@ mod tests { None, None, None, - None, ) .map(|c| c.text) .collect::(), @@ -687,8 +675,7 @@ mod tests { let mut chunks = Vec::new(); let mut was_tab = false; let mut text = String::new(); - for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None, None) - { + for chunk in snapshot.chunks(start..snapshot.max_point(), false, None, None, None) { if chunk.is_tab != was_tab { if !text.is_empty() { chunks.push((mem::take(&mut text), was_tab)); @@ -757,7 +744,7 @@ mod tests { let expected_summary = TextSummary::from(expected_text.as_str()); assert_eq!( tabs_snapshot - .chunks(start..end, false, None, None, None, None) + .chunks(start..end, false, None, None, None) .map(|c| c.text) .collect::(), expected_text, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index f8287af799..d4b52d5893 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -1,7 +1,7 @@ use super::{ fold_map::FoldBufferRows, tab_map::{self, TabEdit, TabPoint, TabSnapshot}, - InlayHighlights, TextHighlights, + TextHighlights, }; use crate::MultiBufferSnapshot; use gpui::{ @@ -447,7 +447,6 @@ impl WrapSnapshot { None, None, None, - None, ); let mut edit_transforms = Vec::::new(); for _ in edit.new_rows.start..edit.new_rows.end { @@ -577,7 +576,6 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlights: Option<&'a InlayHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { @@ -597,7 +595,6 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlights, inlay_highlight_style, suggestion_highlight_style, ), @@ -1329,7 +1326,6 @@ mod tests { None, None, None, - None, ) .map(|h| h.text) } @@ -1354,7 +1350,7 @@ mod tests { } let actual_text = self - .chunks(start_row..end_row, true, None, None, None, None) + .chunks(start_row..end_row, true, None, None, None) .map(|c| c.text) .collect::(); assert_eq!( diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index af352d2649..99db78f1c2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -65,7 +65,7 @@ use language::{ OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, }; use log::error; use multi_buffer::ToOffsetUtf16; @@ -7733,7 +7733,7 @@ impl Editor { pub fn text_highlights<'a, T: 'static>( &'a self, cx: &'a AppContext, - ) -> Option<(HighlightStyle, &'a [Range])> { + ) -> Option<(HighlightStyle, &'a [DocumentRange])> { self.display_map.read(cx).text_highlights(TypeId::of::()) } @@ -7741,10 +7741,7 @@ impl Editor { let text_highlights = self .display_map .update(cx, |map, _| map.clear_text_highlights(TypeId::of::())); - let inlay_highlights = self - .display_map - .update(cx, |map, _| map.clear_inlay_highlights(TypeId::of::())); - if text_highlights.is_some() || inlay_highlights.is_some() { + if text_highlights.is_some() { cx.notify(); } } @@ -7953,6 +7950,8 @@ impl Editor { Some( ranges .iter() + // TODO kb mark inlays too + .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) }) @@ -8406,6 +8405,8 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; + // TODO kb mark inlays too + let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 5bc45720a2..30f273065a 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -43,6 +43,13 @@ pub enum DocumentRange { } impl DocumentRange { + pub fn as_text_range(&self) -> Option> { + match self { + Self::Text(range) => Some(range.clone()), + Self::Inlay(_) => None, + } + } + fn point_within_range(&self, trigger_point: &TriggerPoint, snapshot: &EditorSnapshot) -> bool { match (self, trigger_point) { (DocumentRange::Text(range), TriggerPoint::Text(point)) => { diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 118cddaa92..8d7b6aa5c9 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -240,6 +240,7 @@ impl<'a> EditorTestContext<'a> { .map(|ranges| ranges.as_ref().clone().1) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect(); assert_set_eq!(actual_ranges, expected_ranges); From 4b78678923018f47dc80747dd9e826564edd9a21 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 13:24:17 +0300 Subject: [PATCH 069/142] Prepare background highlights for inlay highlights --- crates/editor/src/display_map.rs | 14 +- crates/editor/src/display_map/inlay_map.rs | 4 + crates/editor/src/editor.rs | 163 +++++++++--------- crates/editor/src/test/editor_test_context.rs | 1 + 4 files changed, 96 insertions(+), 86 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 037854435b..34e877a448 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -416,8 +416,18 @@ impl DisplaySnapshot { .to_offset(self.display_point_to_inlay_point(point, bias)) } - pub fn inlay_point_to_inlay_offset(&self, point: InlayPoint) -> InlayOffset { - self.inlay_snapshot.to_offset(point) + pub fn anchor_to_inlay_offset(&self, anchor: Anchor) -> InlayOffset { + self.inlay_snapshot + .to_inlay_offset(anchor.to_offset(&self.buffer_snapshot)) + } + + pub fn inlay_offset_to_display_point(&self, offset: InlayOffset, bias: Bias) -> DisplayPoint { + let inlay_point = self.inlay_snapshot.to_point(offset); + let fold_point = self.fold_snapshot.to_fold_point(inlay_point, bias); + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) } fn display_point_to_inlay_point(&self, point: DisplayPoint, bias: Bias) -> InlayPoint { diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 06d166b86c..5094d2fab9 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,6 +392,10 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } + + pub fn column(self) -> u32 { + self.0.column + } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 99db78f1c2..2089fe0f5f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -535,6 +535,8 @@ type CompletionId = usize; type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; +type BackgroundHighlight = (fn(&Theme) -> Color, Vec); + pub struct Editor { handle: WeakViewHandle, buffer: ModelHandle, @@ -564,8 +566,7 @@ pub struct Editor { show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, - #[allow(clippy::type_complexity)] - background_highlights: BTreeMap Color, Vec>)>, + background_highlights: BTreeMap, nav_history: Option, context_menu: Option, mouse_context_menu: ViewHandle, @@ -6758,10 +6759,18 @@ impl Editor { let rename_range = if let Some(range) = prepare_rename.await? { Some(range) } else { - this.read_with(&cx, |this, cx| { + this.update(&mut cx, |this, cx| { let buffer = this.buffer.read(cx).snapshot(cx); + let display_snapshot = this + .display_map + .update(cx, |display_map, cx| display_map.snapshot(cx)); let mut buffer_highlights = this - .document_highlights_for_position(selection.head(), &buffer) + .document_highlights_for_position( + selection.head(), + &buffer, + &display_snapshot, + ) + .filter_map(|highlight| highlight.as_text_range()) .filter(|highlight| { highlight.start.excerpt_id() == selection.head().excerpt_id() && highlight.end.excerpt_id() == selection.head().excerpt_id() @@ -6816,11 +6825,15 @@ impl Editor { let ranges = this .clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges) + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }) .chain( this.clear_background_highlights::(cx) .into_iter() - .flat_map(|(_, ranges)| ranges), + .flat_map(|(_, ranges)| { + ranges.into_iter().filter_map(|range| range.as_text_range()) + }), ) .collect(); @@ -7488,16 +7501,20 @@ impl Editor { color_fetcher: fn(&Theme) -> Color, cx: &mut ViewContext, ) { - self.background_highlights - .insert(TypeId::of::(), (color_fetcher, ranges)); + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Text).collect(), + ), + ); cx.notify(); } - #[allow(clippy::type_complexity)] pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, - ) -> Option<(fn(&Theme) -> Color, Vec>)> { + ) -> Option { let highlights = self.background_highlights.remove(&TypeId::of::()); if highlights.is_some() { cx.notify(); @@ -7522,7 +7539,8 @@ impl Editor { &'a self, position: Anchor, buffer: &'a MultiBufferSnapshot, - ) -> impl 'a + Iterator> { + display_snapshot: &'a DisplaySnapshot, + ) -> impl 'a + Iterator { let read_highlights = self .background_highlights .get(&TypeId::of::()) @@ -7531,14 +7549,16 @@ impl Editor { .background_highlights .get(&TypeId::of::()) .map(|h| &h.1); - let left_position = position.bias_left(buffer); - let right_position = position.bias_right(buffer); + let left_position = display_snapshot.anchor_to_inlay_offset(position.bias_left(buffer)); + let right_position = display_snapshot.anchor_to_inlay_offset(position.bias_right(buffer)); read_highlights .into_iter() .chain(write_highlights) .flat_map(move |ranges| { let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&left_position, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&left_position); if cmp.is_ge() { Ordering::Greater } else { @@ -7549,9 +7569,12 @@ impl Editor { }; let right_position = right_position.clone(); - ranges[start_ix..] - .iter() - .take_while(move |range| range.start.cmp(&right_position, buffer).is_le()) + ranges[start_ix..].iter().take_while(move |range| { + document_to_inlay_range(range, &display_snapshot) + .start + .cmp(&right_position) + .is_le() + }) }) } @@ -7561,12 +7584,15 @@ impl Editor { display_snapshot: &DisplaySnapshot, theme: &Theme, ) -> Vec<(Range, Color)> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; for (color_fetcher, ranges) in self.background_highlights.values() { let color = color_fetcher(theme); let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7576,63 +7602,18 @@ impl Editor { Ok(i) | Err(i) => i, }; for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); + + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); results.push((start..end, color)) } } results } - pub fn background_highlights_in_range_for( - &self, - search_range: Range, - display_snapshot: &DisplaySnapshot, - theme: &Theme, - ) -> Vec<(Range, Color)> { - let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; - - let color = color_fetcher(theme); - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); - if cmp.is_gt() { - Ordering::Greater - } else { - Ordering::Less - } - }) { - Ok(i) | Err(i) => i, - }; - for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { - break; - } - let start = range - .start - .to_point(buffer) - .to_display_point(display_snapshot); - let end = range - .end - .to_point(buffer) - .to_display_point(display_snapshot); - results.push((start..end, color)) - } - - results - } pub fn background_highlight_row_ranges( &self, @@ -7640,15 +7621,18 @@ impl Editor { display_snapshot: &DisplaySnapshot, count: usize, ) -> Vec> { + let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) + ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let buffer = &display_snapshot.buffer_snapshot; let Some((_, ranges)) = self.background_highlights .get(&TypeId::of::()) else { return vec![]; }; let start_ix = match ranges.binary_search_by(|probe| { - let cmp = probe.end.cmp(&search_range.start, buffer); + let cmp = document_to_inlay_range(probe, display_snapshot) + .end + .cmp(&search_range.start); if cmp.is_gt() { Ordering::Greater } else { @@ -7657,30 +7641,28 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push( - start_display.to_display_point(display_snapshot) - ..=end_display.to_display_point(display_snapshot), - ); + results.push(start_display..=end_display); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { - return vec![]; + return Vec::new(); } for range in &ranges[start_ix..] { - if range.start.cmp(&search_range.end, buffer).is_ge() { + let range = document_to_inlay_range(range, display_snapshot); + if range.start.cmp(&search_range.end).is_ge() { break; } - let end = range.end.to_point(buffer); + let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); if let Some(current_row) = &end_row { - if end.row == current_row.row { + if end.row() == current_row.row() { continue; } } - let start = range.start.to_point(buffer); + let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); if start_row.is_none() { assert_eq!(end_row, None); @@ -7689,7 +7671,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row > current_end.row + 1 { + if start.row() > current_end.row() + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); @@ -8133,6 +8115,19 @@ impl Editor { } } +fn document_to_inlay_range( + range: &DocumentRange, + snapshot: &DisplaySnapshot, +) -> Range { + match range { + DocumentRange::Text(text_range) => { + snapshot.anchor_to_inlay_offset(text_range.start) + ..snapshot.anchor_to_inlay_offset(text_range.end) + } + DocumentRange::Inlay(inlay_range) => inlay_range.highlight_start..inlay_range.highlight_end, + } +} + fn inlay_hint_settings( location: Anchor, snapshot: &MultiBufferSnapshot, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 8d7b6aa5c9..033525395e 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -225,6 +225,7 @@ impl<'a> EditorTestContext<'a> { .map(|h| h.1.clone()) .unwrap_or_default() .into_iter() + .filter_map(|range| range.as_text_range()) .map(|range| range.to_offset(&snapshot.buffer_snapshot)) .collect() }); From bcaff0a18a924412348dbf4dfee7fbb2c6176856 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 15:14:25 +0300 Subject: [PATCH 070/142] Propagate inlay background highlights to data storage --- crates/editor/src/display_map/inlay_map.rs | 2 +- crates/editor/src/display_map/tab_map.rs | 1 - crates/editor/src/editor.rs | 18 +++- crates/editor/src/element.rs | 87 ++++++++++++---- crates/editor/src/hover_popover.rs | 109 +++++++++++++++++++-- crates/project/src/lsp_command.rs | 1 - 6 files changed, 185 insertions(+), 33 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 5094d2fab9..56df722f52 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1596,7 +1596,7 @@ mod tests { .map(|range| { buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) }) - // TODO add inlay highlight tests + // TODO kb add inlay highlight tests .map(DocumentRange::Text) .collect::>(); diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index cae9ccc91f..fcdef17a8b 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -223,7 +223,6 @@ impl TabSnapshot { &'a self, range: Range, language_aware: bool, - // TODO kb extract into one struct? text_highlights: Option<&'a TextHighlights>, inlay_highlight_style: Option, suggestion_highlight_style: Option, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2089fe0f5f..b21da05958 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7511,6 +7511,22 @@ impl Editor { cx.notify(); } + pub fn highlight_inlay_background( + &mut self, + ranges: Vec, + color_fetcher: fn(&Theme) -> Color, + cx: &mut ViewContext, + ) { + self.background_highlights.insert( + TypeId::of::(), + ( + color_fetcher, + ranges.into_iter().map(DocumentRange::Inlay).collect(), + ), + ); + cx.notify(); + } + pub fn clear_background_highlights( &mut self, cx: &mut ViewContext, @@ -7932,7 +7948,6 @@ impl Editor { Some( ranges .iter() - // TODO kb mark inlays too .filter_map(|range| range.as_text_range()) .map(move |range| { range.start.to_offset_utf16(&snapshot)..range.end.to_offset_utf16(&snapshot) @@ -8400,7 +8415,6 @@ impl View for Editor { fn marked_text_range(&self, cx: &AppContext) -> Option> { let snapshot = self.buffer.read(cx).read(cx); let range = self.text_highlights::(cx)?.1.get(0)?; - // TODO kb mark inlays too let range = range.as_text_range()?; Some(range.start.to_offset_utf16(&snapshot).0..range.end.to_offset_utf16(&snapshot).0) } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2be5105b33..e9a154ddb0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -8,8 +8,8 @@ use crate::{ editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, - MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, + MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, @@ -43,7 +43,8 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - InlayHintLabelPart, Location, LocationLink, ProjectPath, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, + Location, LocationLink, ProjectPath, ResolveState, }; use smallvec::SmallVec; use std::{ @@ -1860,8 +1861,7 @@ fn update_inlay_link_and_hover_points( None }; if let Some(hovered_offset) = hovered_offset { - let buffer = editor.buffer().read(cx); - let snapshot = buffer.snapshot(cx); + let snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = snapshot.anchor_at( point_for_position .previous_valid @@ -1885,15 +1885,14 @@ fn update_inlay_link_and_hover_points( .max_by_key(|hint| hint.id) { let inlay_hint_cache = editor.inlay_hint_cache(); - if let Some(cached_hint) = - inlay_hint_cache.hint_by_id(previous_valid_anchor.excerpt_id, hovered_hint.id) - { + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { match cached_hint.resolve_state { ResolveState::CanResolve(_, _) => { if let Some(buffer_id) = previous_valid_anchor.buffer_id { inlay_hint_cache.spawn_hint_resolve( buffer_id, - previous_valid_anchor.excerpt_id, + excerpt_id, hovered_hint.id, cx, ); @@ -1902,9 +1901,33 @@ fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { - if cached_hint.tooltip.is_some() { - dbg!(&cached_hint.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = cached_hint.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; } } project::InlayHintLabel::LabelParts(label_parts) => { @@ -1915,15 +1938,41 @@ fn update_inlay_link_and_hover_points( hovered_offset, ) { - if hovered_hint_part.tooltip.is_some() { - dbg!(&hovered_hint_part.tooltip); // TODO kb - // hover_at_point = Some(hovered_offset); + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; } if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = cached_hint - .position - .buffer_id - .and_then(|buffer_id| buffer.buffer(buffer_id)) + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) { go_to_definition_updated = true; update_go_to_definition_link( diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index e4509a765c..8e8babb44a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1,6 +1,8 @@ use crate::{ - display_map::ToDisplayPoint, Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, - EditorSnapshot, EditorStyle, RangeToAnchorExt, + display_map::{InlayOffset, ToDisplayPoint}, + link_go_to_definition::{DocumentRange, InlayRange}, + Anchor, AnchorRangeExt, DisplayPoint, Editor, EditorSettings, EditorSnapshot, EditorStyle, + ExcerptId, RangeToAnchorExt, }; use futures::FutureExt; use gpui::{ @@ -46,6 +48,84 @@ pub fn hover_at(editor: &mut Editor, point: Option, cx: &mut ViewC } } +pub struct InlayHover { + pub excerpt: ExcerptId, + pub triggered_from: InlayOffset, + pub range: InlayRange, + pub tooltip: HoverBlock, +} + +pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { + if settings::get::(cx).hover_popover_enabled { + if editor.pending_rename.is_some() { + return; + } + + let Some(project) = editor.project.clone() else { + return; + }; + + if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { + if let DocumentRange::Inlay(range) = symbol_range { + if (range.highlight_start..=range.highlight_end) + .contains(&inlay_hover.triggered_from) + { + // Hover triggered from same location as last time. Don't show again. + return; + } + } + hide_hover(editor, cx); + } + + let snapshot = editor.snapshot(cx); + // Don't request again if the location is the same as the previous request + if let Some(triggered_from) = editor.hover_state.triggered_from { + if inlay_hover.triggered_from + == snapshot + .display_snapshot + .anchor_to_inlay_offset(triggered_from) + { + return; + } + } + + let task = cx.spawn(|this, mut cx| { + async move { + cx.background() + .timer(Duration::from_millis(HOVER_DELAY_MILLIS)) + .await; + this.update(&mut cx, |this, _| { + this.hover_state.diagnostic_popover = None; + })?; + + let hover_popover = InfoPopover { + project: project.clone(), + symbol_range: DocumentRange::Inlay(inlay_hover.range), + blocks: vec![inlay_hover.tooltip], + language: None, + rendered_content: None, + }; + + this.update(&mut cx, |this, cx| { + // Highlight the selected symbol using a background highlight + this.highlight_inlay_background::( + vec![inlay_hover.range], + |theme| theme.editor.hover_popover.highlight, + cx, + ); + this.hover_state.info_popover = Some(hover_popover); + cx.notify(); + })?; + + anyhow::Ok(()) + } + .log_err() + }); + + editor.hover_state.info_task = Some(task); + } +} + /// Hides the type information popup. /// Triggered by the `Hover` action when the cursor is not over a symbol or when the /// selections changed. @@ -110,8 +190,13 @@ fn show_hover( if !ignore_timeout { if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if symbol_range - .to_offset(&snapshot.buffer_snapshot) - .contains(&multibuffer_offset) + .as_text_range() + .map(|range| { + range + .to_offset(&snapshot.buffer_snapshot) + .contains(&multibuffer_offset) + }) + .unwrap_or(false) { // Hover triggered from same location as last time. Don't show again. return; @@ -219,7 +304,7 @@ fn show_hover( Some(InfoPopover { project: project.clone(), - symbol_range: range, + symbol_range: DocumentRange::Text(range), blocks: hover_result.contents, language: hover_result.language, rendered_content: None, @@ -227,10 +312,13 @@ fn show_hover( }); this.update(&mut cx, |this, cx| { - if let Some(hover_popover) = hover_popover.as_ref() { + if let Some(symbol_range) = hover_popover + .as_ref() + .and_then(|hover_popover| hover_popover.symbol_range.as_text_range()) + { // Highlight the selected symbol using a background highlight this.highlight_background::( - vec![hover_popover.symbol_range.clone()], + vec![symbol_range], |theme| theme.editor.hover_popover.highlight, cx, ); @@ -497,7 +585,10 @@ impl HoverState { .or_else(|| { self.info_popover .as_ref() - .map(|info_popover| &info_popover.symbol_range.start) + .map(|info_popover| match &info_popover.symbol_range { + DocumentRange::Text(range) => &range.start, + DocumentRange::Inlay(range) => &range.inlay_position, + }) })?; let point = anchor.to_display_point(&snapshot.display_snapshot); @@ -522,7 +613,7 @@ impl HoverState { #[derive(Debug, Clone)] pub struct InfoPopover { pub project: ModelHandle, - pub symbol_range: Range, + symbol_range: DocumentRange, pub blocks: Vec, language: Option>, rendered_content: Option, diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 20bb302b5b..c057718bf3 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -2126,7 +2126,6 @@ impl InlayHints { }) } - // TODO kb instead, store all LSP data inside the project::InlayHint? pub fn project_to_lsp_hint( hint: InlayHint, project: &ModelHandle, From dcf570bb03d3ce7e6290c161c390ddbdd19b6a20 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 19:43:05 +0300 Subject: [PATCH 071/142] Fix resolve status conversion --- crates/editor/src/display_map.rs | 4 +- crates/editor/src/display_map/block_map.rs | 4 +- crates/editor/src/display_map/fold_map.rs | 4 +- crates/editor/src/display_map/inlay_map.rs | 114 ++++++++++----------- crates/editor/src/display_map/tab_map.rs | 4 +- crates/editor/src/display_map/wrap_map.rs | 4 +- crates/editor/src/editor.rs | 3 +- crates/project/src/lsp_command.rs | 4 +- crates/project/src/project.rs | 2 +- 9 files changed, 71 insertions(+), 72 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 34e877a448..611866bcad 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -471,14 +471,14 @@ impl DisplaySnapshot { &self, display_rows: Range, language_aware: bool, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> DisplayChunks<'_> { self.block_snapshot.chunks( display_rows, language_aware, Some(&self.text_highlights), - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 8577e92819..741507004c 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -589,7 +589,7 @@ impl BlockSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> BlockChunks<'a> { let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows); @@ -623,7 +623,7 @@ impl BlockSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index dcbc156c47..d5473027a6 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -652,7 +652,7 @@ impl FoldSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> FoldChunks<'a> { let mut transform_cursor = self.transforms.cursor::<(FoldOffset, InlayOffset)>(); @@ -675,7 +675,7 @@ impl FoldSnapshot { inlay_start..inlay_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), inlay_chunk: None, diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 56df722f52..748e1f0cd8 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -997,74 +997,74 @@ impl InlaySnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> InlayChunks<'a> { let mut cursor = self.transforms.cursor::<(InlayOffset, usize)>(); cursor.seek(&range.start, Bias::Right, &()); - let empty_text_highlights = TextHighlights::default(); - let text_highlights = text_highlights.unwrap_or_else(|| &empty_text_highlights); - let mut highlight_endpoints = Vec::new(); - if !text_highlights.is_empty() { - while cursor.start().0 < range.end { - let transform_start = self - .buffer - .anchor_after(self.to_buffer_offset(cmp::max(range.start, cursor.start().0))); - let transform_start = self.to_inlay_offset(transform_start.to_offset(&self.buffer)); + if let Some(text_highlights) = text_highlights { + if !text_highlights.is_empty() { + while cursor.start().0 < range.end { + let transform_start = self.buffer.anchor_after( + self.to_buffer_offset(cmp::max(range.start, cursor.start().0)), + ); + let transform_start = + self.to_inlay_offset(transform_start.to_offset(&self.buffer)); - let transform_end = { - let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); - self.buffer.anchor_before(self.to_buffer_offset(cmp::min( - cursor.end(&()).0, - cursor.start().0 + overshoot, - ))) - }; - let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); - - for (tag, text_highlights) in text_highlights.iter() { - let style = text_highlights.0; - let ranges = &text_highlights.1; - - let start_ix = match ranges.binary_search_by(|probe| { - let cmp = self - .document_to_inlay_range(probe) - .end - .cmp(&transform_start); - if cmp.is_gt() { - cmp::Ordering::Greater - } else { - cmp::Ordering::Less - } - }) { - Ok(i) | Err(i) => i, + let transform_end = { + let overshoot = InlayOffset(range.end.0 - cursor.start().0 .0); + self.buffer.anchor_before(self.to_buffer_offset(cmp::min( + cursor.end(&()).0, + cursor.start().0 + overshoot, + ))) }; - for range in &ranges[start_ix..] { - let range = self.document_to_inlay_range(range); - if range.start.cmp(&transform_end).is_ge() { - break; + let transform_end = self.to_inlay_offset(transform_end.to_offset(&self.buffer)); + + for (tag, text_highlights) in text_highlights.iter() { + let style = text_highlights.0; + let ranges = &text_highlights.1; + + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = self + .document_to_inlay_range(probe) + .end + .cmp(&transform_start); + if cmp.is_gt() { + cmp::Ordering::Greater + } else { + cmp::Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + let range = self.document_to_inlay_range(range); + if range.start.cmp(&transform_end).is_ge() { + break; + } + + highlight_endpoints.push(HighlightEndpoint { + offset: range.start, + is_start: true, + tag: *tag, + style, + }); + highlight_endpoints.push(HighlightEndpoint { + offset: range.end, + is_start: false, + tag: *tag, + style, + }); } - - highlight_endpoints.push(HighlightEndpoint { - offset: range.start, - is_start: true, - tag: *tag, - style, - }); - highlight_endpoints.push(HighlightEndpoint { - offset: range.end, - is_start: false, - tag: *tag, - style, - }); } - } - cursor.next(&()); + cursor.next(&()); + } + highlight_endpoints.sort(); + cursor.seek(&range.start, Bias::Right, &()); } - highlight_endpoints.sort(); - cursor.seek(&range.start, Bias::Right, &()); } let buffer_range = self.to_buffer_offset(range.start)..self.to_buffer_offset(range.end); @@ -1078,7 +1078,7 @@ impl InlaySnapshot { buffer_chunk: None, output_offset: range.start, max_output_offset: range.end, - hint_highlight_style: inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, highlight_endpoints: highlight_endpoints.into_iter().peekable(), active_highlights: Default::default(), diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index fcdef17a8b..2cf0471b37 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -224,7 +224,7 @@ impl TabSnapshot { range: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> TabChunks<'a> { let (input_start, expanded_char_column, to_next_stop) = @@ -246,7 +246,7 @@ impl TabSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_column, diff --git a/crates/editor/src/display_map/wrap_map.rs b/crates/editor/src/display_map/wrap_map.rs index d4b52d5893..f3600936f9 100644 --- a/crates/editor/src/display_map/wrap_map.rs +++ b/crates/editor/src/display_map/wrap_map.rs @@ -576,7 +576,7 @@ impl WrapSnapshot { rows: Range, language_aware: bool, text_highlights: Option<&'a TextHighlights>, - inlay_highlight_style: Option, + hint_highlight_style: Option, suggestion_highlight_style: Option, ) -> WrapChunks<'a> { let output_start = WrapPoint::new(rows.start, 0); @@ -595,7 +595,7 @@ impl WrapSnapshot { input_start..input_end, language_aware, text_highlights, - inlay_highlight_style, + hint_highlight_style, suggestion_highlight_style, ), input_chunk: Default::default(), diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b21da05958..785d43f0b6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4882,7 +4882,6 @@ impl Editor { if let Some(clipboard_selection) = clipboard_selections.get(ix) { let end_offset = start_offset + clipboard_selection.len; to_insert = &clipboard_text[start_offset..end_offset]; - dbg!(start_offset, end_offset, &clipboard_text, &to_insert); entire_line = clipboard_selection.is_entire_line; start_offset = end_offset + 1; original_indent_column = @@ -7586,7 +7585,7 @@ impl Editor { let right_position = right_position.clone(); ranges[start_ix..].iter().take_while(move |range| { - document_to_inlay_range(range, &display_snapshot) + document_to_inlay_range(range, display_snapshot) .start .cmp(&right_position) .is_le() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index c057718bf3..9f7799c555 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1935,8 +1935,9 @@ impl InlayHints { pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { + ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( - 0, + 1, resolve_data .map(|json_data| { serde_json::to_string(&json_data) @@ -1947,7 +1948,6 @@ impl InlayHints { value, }), ), - ResolveState::Resolved => (1, None), ResolveState::Resolving => (2, None), }; let resolve_state = Some(proto::ResolveState { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index fbdbd04664..1fe307eec2 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5091,7 +5091,7 @@ impl Project { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await .map(Some) - .context("inlay hints proto response conversion") + .context("inlay hints proto resolve response conversion") } None => Ok(None), } From 7cd60d6afb95d777644a27e9e3096129c19771c0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 23 Aug 2023 20:22:38 +0300 Subject: [PATCH 072/142] Simplify and restore client resolve capabilities --- crates/editor/src/display_map/inlay_map.rs | 4 ---- crates/editor/src/editor.rs | 23 ++++++++++++++-------- crates/lsp/src/lsp.rs | 4 +++- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 748e1f0cd8..1b65719448 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -392,10 +392,6 @@ impl InlayPoint { pub fn row(self) -> u32 { self.0.row } - - pub fn column(self) -> u32 { - self.0.column - } } impl InlayMap { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 785d43f0b6..681e1d48b2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7656,13 +7656,16 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let mut push_region = |start: Option, end: Option| { + let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { - results.push(start_display..=end_display); + results.push( + start_display.to_display_point(display_snapshot) + ..=end_display.to_display_point(display_snapshot), + ); } }; - let mut start_row: Option = None; - let mut end_row: Option = None; + let mut start_row: Option = None; + let mut end_row: Option = None; if ranges.len() > count { return Vec::new(); } @@ -7671,13 +7674,17 @@ impl Editor { if range.start.cmp(&search_range.end).is_ge() { break; } - let end = display_snapshot.inlay_offset_to_display_point(range.end, Bias::Right); + let end = display_snapshot + .inlay_offset_to_display_point(range.end, Bias::Right) + .to_point(display_snapshot); if let Some(current_row) = &end_row { - if end.row() == current_row.row() { + if end.row == current_row.row { continue; } } - let start = display_snapshot.inlay_offset_to_display_point(range.start, Bias::Left); + let start = display_snapshot + .inlay_offset_to_display_point(range.start, Bias::Left) + .to_point(display_snapshot); if start_row.is_none() { assert_eq!(end_row, None); @@ -7686,7 +7693,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start.row() > current_end.row() + 1 { + if start.row > current_end.row + 1 { push_region(start_row, end_row); start_row = Some(start); end_row = Some(end); diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() From 852427e87b0f6c7b56b7dca38536a255c25aef86 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:08:08 +0300 Subject: [PATCH 073/142] Use inlay highlights in randomized tests --- crates/editor/src/display_map/inlay_map.rs | 57 +++++++++++++--------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 1b65719448..25b8d3aef6 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -1144,13 +1144,12 @@ fn push_isomorphic(sum_tree: &mut SumTree, summary: TextSummary) { #[cfg(test)] mod tests { use super::*; - use crate::{InlayId, MultiBuffer}; + use crate::{link_go_to_definition::InlayRange, InlayId, MultiBuffer}; use gpui::AppContext; use project::{InlayHint, InlayHintLabel, ResolveState}; use rand::prelude::*; use settings::SettingsStore; use std::{cmp::Reverse, env, sync::Arc}; - use sum_tree::TreeMap; use text::Patch; use util::post_inc; @@ -1579,28 +1578,6 @@ mod tests { let mut buffer_snapshot = buffer.read(cx).snapshot(cx); let mut next_inlay_id = 0; log::info!("buffer text: {:?}", buffer_snapshot.text()); - - let mut highlights = TreeMap::default(); - let highlight_count = rng.gen_range(0_usize..10); - let mut highlight_ranges = (0..highlight_count) - .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) - .collect::>(); - highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); - log::info!("highlighting ranges {:?}", highlight_ranges); - let highlight_ranges = highlight_ranges - .into_iter() - .map(|range| { - buffer_snapshot.anchor_before(range.start)..buffer_snapshot.anchor_after(range.end) - }) - // TODO kb add inlay highlight tests - .map(DocumentRange::Text) - .collect::>(); - - highlights.insert( - Some(TypeId::of::<()>()), - Arc::new((HighlightStyle::default(), highlight_ranges)), - ); - let (mut inlay_map, mut inlay_snapshot) = InlayMap::new(buffer_snapshot.clone()); for _ in 0..operations { let mut inlay_edits = Patch::default(); @@ -1663,6 +1640,38 @@ mod tests { ); } + let mut highlights = TextHighlights::default(); + let highlight_count = rng.gen_range(0_usize..10); + let mut highlight_ranges = (0..highlight_count) + .map(|_| buffer_snapshot.random_byte_range(0, &mut rng)) + .collect::>(); + highlight_ranges.sort_by_key(|range| (range.start, Reverse(range.end))); + log::info!("highlighting ranges {:?}", highlight_ranges); + let highlight_ranges = if rng.gen_bool(0.5) { + highlight_ranges + .into_iter() + .map(|range| InlayRange { + inlay_position: buffer_snapshot.anchor_before(range.start), + highlight_start: inlay_snapshot.to_inlay_offset(range.start), + highlight_end: inlay_snapshot.to_inlay_offset(range.end), + }) + .map(DocumentRange::Inlay) + .collect::>() + } else { + highlight_ranges + .into_iter() + .map(|range| { + buffer_snapshot.anchor_before(range.start) + ..buffer_snapshot.anchor_after(range.end) + }) + .map(DocumentRange::Text) + .collect::>() + }; + highlights.insert( + Some(TypeId::of::<()>()), + Arc::new((HighlightStyle::default(), highlight_ranges)), + ); + for _ in 0..5 { let mut end = rng.gen_range(0..=inlay_snapshot.len().0); end = expected_text.clip_offset(end, Bias::Right); From 3c55c933d4064d06f3d5d3e2cf8336c3deefaf40 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:37:53 +0300 Subject: [PATCH 074/142] Be more lenient with hint resolution, always return some hint --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++--------------- crates/project/src/project.rs | 21 +++++++-------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 52a4039a76..d4325e13d9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -449,30 +449,26 @@ impl InlayHintCache { }) })?; if let Some(resolved_hint_task) = resolved_hint_task { - if let Some(mut resolved_hint) = - resolved_hint_task.await.context("hint resolve task")? - { - editor.update(&mut cx, |editor, _| { - if let Some(excerpt_hints) = - editor.inlay_hint_cache.hints.get(&excerpt_id) + let mut resolved_hint = + resolved_hint_task.await.context("hint resolve task")?; + editor.update(&mut cx, |editor, _| { + if let Some(excerpt_hints) = + editor.inlay_hint_cache.hints.get(&excerpt_id) + { + let mut guard = excerpt_hints.write(); + if let Some(cached_hint) = guard + .hints + .iter_mut() + .find(|(hint_id, _)| hint_id == &id) + .map(|(_, hint)| hint) { - let mut guard = excerpt_hints.write(); - if let Some(cached_hint) = guard - .hints - .iter_mut() - .find(|(hint_id, _)| hint_id == &id) - .map(|(_, hint)| hint) - { - if cached_hint.resolve_state == ResolveState::Resolving - { - resolved_hint.resolve_state = - ResolveState::Resolved; - *cached_hint = resolved_hint; - } + if cached_hint.resolve_state == ResolveState::Resolving { + resolved_hint.resolve_state = ResolveState::Resolved; + *cached_hint = resolved_hint; } } - })?; - } + } + })?; } anyhow::Ok(()) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 1fe307eec2..0bbb61dfcb 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5033,7 +5033,7 @@ impl Project { buffer_handle: ModelHandle, server_id: LanguageServerId, cx: &mut ModelContext, - ) -> Task>> { + ) -> Task> { if self.is_local() { let buffer = buffer_handle.read(cx); let (_, lang_server) = if let Some((adapter, server)) = @@ -5041,7 +5041,7 @@ impl Project { { (adapter.clone(), server.clone()) } else { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); }; let can_resolve = lang_server .capabilities() @@ -5050,7 +5050,7 @@ impl Project { .and_then(|options| options.resolve_provider) .unwrap_or(false); if !can_resolve { - return Task::ready(Ok(None)); + return Task::ready(Ok(hint)); } let buffer_snapshot = buffer.snapshot(); @@ -5071,7 +5071,7 @@ impl Project { &mut cx, ) .await?; - Ok(Some(resolved_hint)) + Ok(resolved_hint) }) } else if let Some(project_id) = self.remote_id() { let client = self.client.clone(); @@ -5079,7 +5079,7 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint, cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), }; cx.spawn(|project, mut cx| async move { let response = client @@ -5090,10 +5090,9 @@ impl Project { Some(resolved_hint) => { InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) .await - .map(Some) .context("inlay hints proto resolve response conversion") } - None => Ok(None), + None => Ok(hint), } }) } else { @@ -6917,7 +6916,7 @@ impl Project { .and_then(|buffer| buffer.upgrade(cx)) .ok_or_else(|| anyhow!("unknown buffer id {}", envelope.payload.buffer_id)) })?; - let resolved_hint = this + let response_hint = this .update(&mut cx, |project, cx| { project.resolve_inlay_hint( hint, @@ -6927,11 +6926,11 @@ impl Project { ) }) .await - .context("inlay hints fetch")? - .map(|hint| cx.read(|cx| InlayHints::project_to_proto_hint(hint, cx))); + .context("inlay hints fetch")?; + let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); Ok(proto::ResolveInlayHintResponse { - hint: resolved_hint, + hint: Some(resolved_hint), }) } From abd2d012b1914fcea6830116a5dea88fbd69963d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 24 Aug 2023 16:50:53 +0300 Subject: [PATCH 075/142] Properly binary search cached inlay hints --- crates/editor/src/inlay_hint_cache.rs | 38 ++++++++++++++++++++------- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d4325e13d9..71b65de676 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -714,13 +714,21 @@ fn calculate_hint_updates( probe.1.position.cmp(&new_hint.position, buffer_snapshot) }) { Ok(ix) => { - let (cached_inlay_id, cached_hint) = &cached_excerpt_hints.hints[ix]; - if cached_hint == &new_hint { - excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); - false - } else { - true + let mut missing_from_cache = true; + for (cached_inlay_id, cached_hint) in &cached_excerpt_hints.hints[ix..] { + if new_hint + .position + .cmp(&cached_hint.position, buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint == &new_hint { + excerpt_hints_to_persist.insert(*cached_inlay_id, cached_hint.kind); + missing_from_cache = false; + } } + missing_from_cache } Err(_) => true, } @@ -820,11 +828,21 @@ fn apply_hint_update( .binary_search_by(|probe| probe.1.position.cmp(&new_hint.position, &buffer_snapshot)) { Ok(i) => { - if cached_hints[i].1.text() == new_hint.text() { - None - } else { - Some(i) + let mut insert_position = Some(i); + for (_, cached_hint) in &cached_hints[i..] { + if new_hint + .position + .cmp(&cached_hint.position, &buffer_snapshot) + .is_gt() + { + break; + } + if cached_hint.text() == new_hint.text() { + insert_position = None; + break; + } } + insert_position } Err(i) => Some(i), }; From f19c659ed6aa63e56effddfcf7d3952d325854da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 11:45:07 +0300 Subject: [PATCH 076/142] Add link_go_to_definition test for inlays --- crates/editor/src/element.rs | 232 +----------- crates/editor/src/hover_popover.rs | 23 +- crates/editor/src/inlay_hint_cache.rs | 14 +- crates/editor/src/link_go_to_definition.rs | 413 ++++++++++++++++++++- 4 files changed, 447 insertions(+), 235 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e9a154ddb0..3ba807308c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -4,16 +4,16 @@ use super::{ MAX_LINE_LEN, }; use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, InlayOffset, TransformBlock}, + display_map::{BlockStyle, DisplaySnapshot, FoldStatus, TransformBlock}, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::{ - hide_hover, hover_at, hover_at_inlay, InlayHover, HOVER_POPOVER_GAP, - MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, + hide_hover, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, + MIN_POPOVER_LINE_HEIGHT, }, link_go_to_definition::{ go_to_fetched_definition, go_to_fetched_type_definition, update_go_to_definition_link, - GoToDefinitionTrigger, InlayRange, + update_inlay_link_and_hover_points, GoToDefinitionTrigger, }, mouse_context_menu, EditorSettings, EditorStyle, GutterHover, UnfoldAt, }; @@ -43,8 +43,7 @@ use language::{ }; use project::{ project_settings::{GitGutterSetting, ProjectSettings}, - HoverBlock, HoverBlockKind, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, - Location, LocationLink, ProjectPath, ResolveState, + ProjectPath, }; use smallvec::SmallVec; use std::{ @@ -478,10 +477,11 @@ impl EditorElement { } None => { update_inlay_link_and_hover_points( - position_map, + &position_map.snapshot, point_for_position, editor, - (cmd, shift), + cmd, + shift, cx, ); } @@ -1835,214 +1835,6 @@ impl EditorElement { } } -fn update_inlay_link_and_hover_points( - position_map: &PositionMap, - point_for_position: PointForPosition, - editor: &mut Editor, - (cmd_held, shift_held): (bool, bool), - cx: &mut ViewContext<'_, '_, Editor>, -) { - let hint_start_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); - let hint_end_offset = position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { - Some( - position_map - .snapshot - .display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left), - ) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) - } else { - None - }; - if let Some(hovered_offset) = hovered_offset { - let snapshot = editor.buffer().read(cx).snapshot(cx); - let previous_valid_anchor = snapshot.anchor_at( - point_for_position - .previous_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Left, - ); - let next_valid_anchor = snapshot.anchor_at( - point_for_position - .next_valid - .to_point(&position_map.snapshot.display_snapshot), - Bias::Right, - ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; - if let Some(hovered_hint) = editor - .visible_inlay_hints(cx) - .into_iter() - .skip_while(|hint| hint.position.cmp(&previous_valid_anchor, &snapshot).is_lt()) - .take_while(|hint| hint.position.cmp(&next_valid_anchor, &snapshot).is_le()) - .max_by_key(|hint| hint.id) - { - let inlay_hint_cache = editor.inlay_hint_cache(); - let excerpt_id = previous_valid_anchor.excerpt_id; - if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { - match cached_hint.resolve_state { - ResolveState::CanResolve(_, _) => { - if let Some(buffer_id) = previous_valid_anchor.buffer_id { - inlay_hint_cache.spawn_hint_resolve( - buffer_id, - excerpt_id, - hovered_hint.id, - cx, - ); - } - } - ResolveState::Resolved => { - match cached_hint.label { - project::InlayHintLabel::String(_) => { - if let Some(tooltip) = cached_hint.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintTooltip::String(text) => HoverBlock { - text, - kind: HoverBlockKind::PlainText, - }, - InlayHintTooltip::MarkupContent(content) => { - HoverBlock { - text: content.value, - kind: content.kind, - } - } - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, - }, - }, - cx, - ); - hover_updated = true; - } - } - project::InlayHintLabel::LabelParts(label_parts) => { - if let Some((hovered_hint_part, part_range)) = - find_hovered_hint_part( - label_parts, - hint_start_offset..hint_end_offset, - hovered_offset, - ) - { - if let Some(tooltip) = hovered_hint_part.tooltip { - hover_at_inlay( - editor, - InlayHover { - excerpt: excerpt_id, - tooltip: match tooltip { - InlayHintLabelPartTooltip::String(text) => { - HoverBlock { - text, - kind: HoverBlockKind::PlainText, - } - } - InlayHintLabelPartTooltip::MarkupContent( - content, - ) => HoverBlock { - text: content.value, - kind: content.kind, - }, - }, - triggered_from: hovered_offset, - range: InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - }, - cx, - ); - hover_updated = true; - } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } - } - } - } - }; - } - ResolveState::Resolving => {} - } - } - } - - if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); - } - if !hover_updated { - hover_at(editor, None, cx); - } - } -} - -fn find_hovered_hint_part( - label_parts: Vec, - hint_range: Range, - hovered_offset: InlayOffset, -) -> Option<(InlayHintLabelPart, Range)> { - if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { - let mut hovered_character = (hovered_offset - hint_range.start).0; - let mut part_start = hint_range.start; - for part in label_parts { - let part_len = part.value.chars().count(); - if hovered_character >= part_len { - hovered_character -= part_len; - part_start.0 += part_len; - } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); - } - } - } - None -} - struct HighlightedChunk<'a> { chunk: &'a str, style: Option, @@ -2871,12 +2663,12 @@ struct PositionMap { snapshot: EditorSnapshot, } -#[derive(Debug)] +#[derive(Debug, Copy, Clone)] pub struct PointForPosition { - previous_valid: DisplayPoint, + pub previous_valid: DisplayPoint, pub next_valid: DisplayPoint, - exact_unclipped: DisplayPoint, - column_overshoot_after_line_end: u32, + pub exact_unclipped: DisplayPoint, + pub column_overshoot_after_line_end: u32, } impl PointForPosition { diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 8e8babb44a..6eae470bad 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -13,7 +13,7 @@ use gpui::{ AnyElement, AppContext, CursorRegion, Element, ModelHandle, MouseRegion, Task, ViewContext, }; use language::{Bias, DiagnosticEntry, DiagnosticSeverity, Language, LanguageRegistry}; -use project::{HoverBlock, HoverBlockKind, Project}; +use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart, Project}; use std::{ops::Range, sync::Arc, time::Duration}; use util::TryFutureExt; @@ -55,6 +55,27 @@ pub struct InlayHover { pub tooltip: HoverBlock, } +pub fn find_hovered_hint_part( + label_parts: Vec, + hint_range: Range, + hovered_offset: InlayOffset, +) -> Option<(InlayHintLabelPart, Range)> { + if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { + let mut hovered_character = (hovered_offset - hint_range.start).0; + let mut part_start = hint_range.start; + for part in label_parts { + let part_len = part.value.chars().count(); + if hovered_character >= part_len { + hovered_character -= part_len; + part_start.0 += part_len; + } else { + return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + } + } + } + None +} + pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut ViewContext) { if settings::get::(cx).hover_popover_enabled { if editor.pending_rename.is_some() { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 71b65de676..b0c7d9e0f1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -904,7 +904,7 @@ fn apply_hint_update( } #[cfg(test)] -mod tests { +pub mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; use crate::{ @@ -2989,15 +2989,11 @@ all hints should be invalidated and requeried for all of its visible excerpts" ("/a/main.rs", editor, fake_server) } - fn cached_hint_labels(editor: &Editor) -> Vec { + pub fn cached_hint_labels(editor: &Editor) -> Vec { let mut labels = Vec::new(); for (_, excerpt_hints) in &editor.inlay_hint_cache().hints { - let excerpt_hints = excerpt_hints.read(); - for (_, inlay) in excerpt_hints.hints.iter() { - match &inlay.label { - project::InlayHintLabel::String(s) => labels.push(s.to_string()), - _ => unreachable!(), - } + for (_, inlay) in &excerpt_hints.read().hints { + labels.push(inlay.text()); } } @@ -3005,7 +3001,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" labels } - fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { + pub fn visible_hint_labels(editor: &Editor, cx: &ViewContext<'_, '_, Editor>) -> Vec { let mut hints = editor .visible_inlay_hints(cx) .into_iter() diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 30f273065a..ea22ea5eae 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1,10 +1,15 @@ use crate::{ - display_map::InlayOffset, element::PointForPosition, Anchor, DisplayPoint, Editor, - EditorSnapshot, SelectPhase, + display_map::{DisplaySnapshot, InlayOffset}, + element::PointForPosition, + hover_popover::{self, InlayHover}, + Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; use gpui::{Task, ViewContext}; use language::{Bias, ToOffset}; -use project::LocationLink; +use project::{ + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, + LocationLink, ResolveState, +}; use std::ops::Range; use util::TryFutureExt; @@ -23,7 +28,7 @@ pub enum GoToDefinitionTrigger { None, } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct InlayRange { pub inlay_position: Anchor, pub highlight_start: InlayOffset, @@ -140,6 +145,192 @@ pub fn update_go_to_definition_link( hide_link_definition(editor, cx); } +pub fn update_inlay_link_and_hover_points( + snapshot: &DisplaySnapshot, + point_for_position: PointForPosition, + editor: &mut Editor, + cmd_held: bool, + shift_held: bool, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let hint_start_offset = + snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); + let hint_end_offset = + snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); + let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; + let hovered_offset = if offset_overshoot == 0 { + Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) + } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { + Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) + } else { + None + }; + if let Some(hovered_offset) = hovered_offset { + let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); + let previous_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.previous_valid.to_point(snapshot), + Bias::Left, + ); + let next_valid_anchor = buffer_snapshot.anchor_at( + point_for_position.next_valid.to_point(snapshot), + Bias::Right, + ); + + let mut go_to_definition_updated = false; + let mut hover_updated = false; + if let Some(hovered_hint) = editor + .visible_inlay_hints(cx) + .into_iter() + .skip_while(|hint| { + hint.position + .cmp(&previous_valid_anchor, &buffer_snapshot) + .is_lt() + }) + .take_while(|hint| { + hint.position + .cmp(&next_valid_anchor, &buffer_snapshot) + .is_le() + }) + .max_by_key(|hint| hint.id) + { + let inlay_hint_cache = editor.inlay_hint_cache(); + let excerpt_id = previous_valid_anchor.excerpt_id; + if let Some(cached_hint) = inlay_hint_cache.hint_by_id(excerpt_id, hovered_hint.id) { + match cached_hint.resolve_state { + ResolveState::CanResolve(_, _) => { + if let Some(buffer_id) = previous_valid_anchor.buffer_id { + inlay_hint_cache.spawn_hint_resolve( + buffer_id, + excerpt_id, + hovered_hint.id, + cx, + ); + } + } + ResolveState::Resolved => { + match cached_hint.label { + project::InlayHintLabel::String(_) => { + if let Some(tooltip) = cached_hint.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintTooltip::String(text) => HoverBlock { + text, + kind: HoverBlockKind::PlainText, + }, + InlayHintTooltip::MarkupContent(content) => { + HoverBlock { + text: content.value, + kind: content.kind, + } + } + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: hint_start_offset, + highlight_end: hint_end_offset, + }, + }, + cx, + ); + hover_updated = true; + } + } + project::InlayHintLabel::LabelParts(label_parts) => { + if let Some((hovered_hint_part, part_range)) = + hover_popover::find_hovered_hint_part( + label_parts, + hint_start_offset..hint_end_offset, + hovered_offset, + ) + { + if let Some(tooltip) = hovered_hint_part.tooltip { + hover_popover::hover_at_inlay( + editor, + InlayHover { + excerpt: excerpt_id, + tooltip: match tooltip { + InlayHintLabelPartTooltip::String(text) => { + HoverBlock { + text, + kind: HoverBlockKind::PlainText, + } + } + InlayHintLabelPartTooltip::MarkupContent( + content, + ) => HoverBlock { + text: content.value, + kind: content.kind, + }, + }, + triggered_from: hovered_offset, + range: InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + }, + cx, + ); + hover_updated = true; + } + if let Some(location) = hovered_hint_part.location { + if let Some(buffer) = + cached_hint.position.buffer_id.and_then(|buffer_id| { + editor.buffer().read(cx).buffer(buffer_id) + }) + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + LocationLink { + origin: Some(Location { + buffer, + range: cached_hint.position + ..cached_hint.position, + }), + target: location, + }, + ), + cmd_held, + shift_held, + cx, + ); + } + } + } + } + }; + } + ResolveState::Resolving => {} + } + } + } + + if !go_to_definition_updated { + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::None, + cmd_held, + shift_held, + cx, + ); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); + } + } +} + #[derive(Debug, Clone, Copy, PartialEq)] pub enum LinkDefinitionKind { Symbol, @@ -391,14 +582,21 @@ fn go_to_fetched_definition_of_kind( #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + display_map::ToDisplayPoint, + editor_tests::init_test, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + test::editor_lsp_test_context::EditorLspTestContext, + }; use futures::StreamExt; use gpui::{ platform::{self, Modifiers, ModifiersChangedEvent}, View, }; use indoc::indoc; + use language::language_settings::InlayHintSettings; use lsp::request::{GotoDefinition, GotoTypeDefinition}; + use util::assert_set_eq; #[gpui::test] async fn test_link_go_to_type_definition(cx: &mut gpui::TestAppContext) { @@ -853,4 +1051,209 @@ mod tests { "}); cx.foreground().run_until_parked(); } + + #[gpui::test] + async fn test_link_go_to_inlay(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + cx, + ) + .await; + cx.set_state(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "}); + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + fn main() { + let variableˇ = TestStruct; + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + fn main() { + let variable = TestStruct; + } + "}); + + let expected_uri = cx.buffer_lsp_url.clone(); + let inlay_label = ": TestStruct"; + cx.lsp + .handle_request::(move |params, _| { + let expected_uri = expected_uri.clone(); + async move { + assert_eq!(params.text_document.uri, expected_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: inlay_label.to_string(), + location: Some(lsp::Location { + uri: params.text_document.uri, + range: target_range, + }), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![inlay_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + fn main() { + let variable« »= TestStruct; + } + "}) + .get(0) + .cloned() + .unwrap(); + let hint_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + } + }); + // Press cmd to trigger highlight + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let expected_highlight_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_ranges = vec![InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_highlight_start, + highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + }]; + assert_set_eq!(actual_ranges, expected_ranges); + }); + + // Unpress cmd causes highlight to go away + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: false, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + }); + // Assert no link highlights + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let actual_ranges = snapshot + .highlight_ranges::() + .map(|ranges| ranges.as_ref().clone().1) + .unwrap_or_default() + .into_iter() + .map(|range| match range { + DocumentRange::Text(range) => { + panic!("Unexpected regular text selection range {range:?}") + } + DocumentRange::Inlay(inlay_range) => inlay_range, + }) + .collect::>(); + + assert!(actual_ranges.is_empty(), "When no cmd is pressed, should have no hint label selected, but got: {actual_ranges:?}"); + }); + + // Cmd+click without existing definition requests and jumps + cx.update_editor(|editor, cx| { + editor.modifiers_changed( + &platform::ModifiersChangedEvent { + modifiers: Modifiers { + cmd: true, + ..Default::default() + }, + ..Default::default() + }, + cx, + ); + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + hint_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + go_to_fetched_type_definition(editor, hint_hover_position, false, cx); + }); + cx.foreground().run_until_parked(); + cx.assert_editor_state(indoc! {" + struct «TestStructˇ»; + + fn main() { + let variable = TestStruct; + } + "}); + } } From e44516cc6c8197f401934394d939d89fd414a634 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:26:04 +0300 Subject: [PATCH 077/142] Add hover tests --- crates/editor/src/hover_popover.rs | 318 ++++++++++++++++++++- crates/editor/src/link_go_to_definition.rs | 12 +- crates/project/src/lsp_command.rs | 30 +- crates/project/src/project.rs | 8 +- 4 files changed, 344 insertions(+), 24 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 6eae470bad..19020b643a 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -804,10 +804,17 @@ impl DiagnosticPopover { #[cfg(test)] mod tests { use super::*; - use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext}; + use crate::{ + editor_tests::init_test, + element::PointForPosition, + inlay_hint_cache::tests::{cached_hint_labels, visible_hint_labels}, + link_go_to_definition::update_inlay_link_and_hover_points, + test::editor_lsp_test_context::EditorLspTestContext, + }; + use collections::BTreeSet; use gpui::fonts::Weight; use indoc::indoc; - use language::{Diagnostic, DiagnosticSet}; + use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet}; use lsp::LanguageServerId; use project::{HoverBlock, HoverBlockKind}; use smol::stream::StreamExt; @@ -1243,4 +1250,311 @@ mod tests { editor }); } + + #[gpui::test] + async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) { + init_test(cx, |settings| { + settings.defaults.inlay_hints = Some(InlayHintSettings { + enabled: true, + show_type_hints: true, + show_parameter_hints: true, + show_other_hints: true, + }) + }); + + let mut cx = EditorLspTestContext::new_rust( + lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Right( + lsp::InlayHintServerCapabilities::Options(lsp::InlayHintOptions { + resolve_provider: Some(true), + ..Default::default() + }), + )), + ..Default::default() + }, + cx, + ) + .await; + + cx.set_state(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "}); + + let hint_start_offset = cx.ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variableˇ = TestNewType(TestStruct); + } + "})[0] + .start; + let hint_position = cx.to_lsp(hint_start_offset); + let new_type_target_range = cx.lsp_range(indoc! {" + struct TestStruct; + + // ================== + + struct «TestNewType»(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + let struct_target_range = cx.lsp_range(indoc! {" + struct «TestStruct»; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable = TestNewType(TestStruct); + } + "}); + + let uri = cx.buffer_lsp_url.clone(); + let new_type_label = "TestNewType"; + let struct_label = "TestStruct"; + let entire_hint_label = ": TestNewType"; + let closure_uri = uri.clone(); + cx.lsp + .handle_request::(move |params, _| { + let task_uri = closure_uri.clone(); + async move { + assert_eq!(params.text_document.uri, task_uri); + Ok(Some(vec![lsp::InlayHint { + position: hint_position, + label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { + value: entire_hint_label.to_string(), + ..Default::default() + }]), + kind: Some(lsp::InlayHintKind::TYPE), + text_edits: None, + tooltip: None, + padding_left: Some(false), + padding_right: Some(false), + data: None, + }])) + } + }) + .next() + .await; + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let expected_layers = vec![entire_hint_label.to_string()]; + assert_eq!(expected_layers, cached_hint_labels(editor)); + assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + }); + + let inlay_range = cx + .ranges(indoc! {" + struct TestStruct; + + // ================== + + struct TestNewType(T); + + fn main() { + let variable« »= TestNewType(TestStruct); + } + "}) + .get(0) + .cloned() + .unwrap(); + let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() + + new_type_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + + let resolve_closure_uri = uri.clone(); + cx.lsp + .handle_request::( + move |mut hint_to_resolve, _| { + let mut resolved_hint_positions = BTreeSet::new(); + let task_uri = resolve_closure_uri.clone(); + async move { + let inserted = resolved_hint_positions.insert(hint_to_resolve.position); + assert!(inserted, "Hint {hint_to_resolve:?} was resolved twice"); + + // `: TestNewType` + hint_to_resolve.label = lsp::InlayHintLabel::LabelParts(vec![ + lsp::InlayHintLabelPart { + value: ": ".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: new_type_label.to_string(), + location: Some(lsp::Location { + uri: task_uri.clone(), + range: new_type_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::String(format!( + "A tooltip for `{new_type_label}`" + ))), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: "<".to_string(), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: struct_label.to_string(), + location: Some(lsp::Location { + uri: task_uri, + range: struct_target_range, + }), + tooltip: Some(lsp::InlayHintLabelPartTooltip::MarkupContent( + lsp::MarkupContent { + kind: lsp::MarkupKind::Markdown, + value: format!("A tooltip for `{struct_label}`"), + }, + )), + ..Default::default() + }, + lsp::InlayHintLabelPart { + value: ">".to_string(), + ..Default::default() + }, + ]); + + Ok(hint_to_resolve) + } + }, + ) + .next() + .await; + cx.foreground().run_until_parked(); + + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + new_type_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + + let expected_new_type_label_start = InlayOffset(entire_inlay_start.0 + ": ".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_new_type_label_start, + highlight_end: InlayOffset( + expected_new_type_label_start.0 + new_type_label.len() + ), + }), + "Popover range should match the new type label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for new type hint") + .text, + format!("A tooltip for `{new_type_label}`"), + "Rendered text should not anyhow alter backticks" + ); + }); + + let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + PointForPosition { + previous_valid: inlay_range.start.to_display_point(&snapshot), + next_valid: inlay_range.end.to_display_point(&snapshot), + exact_unclipped: inlay_range.end.to_display_point(&snapshot), + column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() + + struct_label.len() / 2) + as u32, + } + }); + cx.update_editor(|editor, cx| { + update_inlay_link_and_hover_points( + &editor.snapshot(cx), + struct_hint_part_hover_position, + editor, + true, + false, + cx, + ); + }); + cx.foreground() + .advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100)); + cx.foreground().run_until_parked(); + cx.update_editor(|editor, cx| { + let snapshot = editor.snapshot(cx); + let hover_state = &editor.hover_state; + assert!(hover_state.diagnostic_popover.is_none() && hover_state.info_popover.is_some()); + let popover = hover_state.info_popover.as_ref().unwrap(); + let buffer_snapshot = editor.buffer().update(cx, |buffer, cx| buffer.snapshot(cx)); + let entire_inlay_start = snapshot.display_point_to_inlay_offset( + inlay_range.start.to_display_point(&snapshot), + Bias::Left, + ); + let expected_struct_label_start = + InlayOffset(entire_inlay_start.0 + ": ".len() + new_type_label.len() + "<".len()); + assert_eq!( + popover.symbol_range, + DocumentRange::Inlay(InlayRange { + inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), + highlight_start: expected_struct_label_start, + highlight_end: InlayOffset(expected_struct_label_start.0 + struct_label.len()), + }), + "Popover range should match the struct label part" + ); + assert_eq!( + popover + .rendered_content + .as_ref() + .expect("should have label text for struct hint") + .text, + format!("A tooltip for {struct_label}"), + "Rendered markdown element should remove backticks from text" + ); + }); + } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ea22ea5eae..926c0d6dde 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -41,7 +41,7 @@ pub enum TriggerPoint { InlayHint(InlayRange, LocationLink), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DocumentRange { Text(Range), Inlay(InlayRange), @@ -1096,7 +1096,7 @@ mod tests { "}); let expected_uri = cx.buffer_lsp_url.clone(); - let inlay_label = ": TestStruct"; + let hint_label = ": TestStruct"; cx.lsp .handle_request::(move |params, _| { let expected_uri = expected_uri.clone(); @@ -1105,7 +1105,7 @@ mod tests { Ok(Some(vec![lsp::InlayHint { position: hint_position, label: lsp::InlayHintLabel::LabelParts(vec![lsp::InlayHintLabelPart { - value: inlay_label.to_string(), + value: hint_label.to_string(), location: Some(lsp::Location { uri: params.text_document.uri, range: target_range, @@ -1125,7 +1125,7 @@ mod tests { .await; cx.foreground().run_until_parked(); cx.update_editor(|editor, cx| { - let expected_layers = vec![inlay_label.to_string()]; + let expected_layers = vec![hint_label.to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor)); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); }); @@ -1147,7 +1147,7 @@ mod tests { previous_valid: inlay_range.start.to_display_point(&snapshot), next_valid: inlay_range.end.to_display_point(&snapshot), exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (inlay_label.len() / 2) as u32, + column_overshoot_after_line_end: (hint_label.len() / 2) as u32, } }); // Press cmd to trigger highlight @@ -1185,7 +1185,7 @@ mod tests { let expected_ranges = vec![InlayRange { inlay_position: buffer_snapshot.anchor_at(inlay_range.start, Bias::Right), highlight_start: expected_highlight_start, - highlight_end: InlayOffset(expected_highlight_start.0 + inlay_label.len()), + highlight_end: InlayOffset(expected_highlight_start.0 + hint_label.len()), }]; assert_set_eq!(actual_ranges, expected_ranges); }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 9f7799c555..292f9a5226 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -17,7 +17,7 @@ use language::{ CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, Unclipped, }; -use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, ServerCapabilities}; +use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions { @@ -2213,6 +2213,22 @@ impl InlayHints { }, } } + + pub fn can_resolve_inlays(capabilities: &ServerCapabilities) -> bool { + capabilities + .inlay_hint_provider + .as_ref() + .and_then(|options| match options { + OneOf::Left(_is_supported) => None, + OneOf::Right(capabilities) => match capabilities { + lsp::InlayHintServerCapabilities::Options(o) => o.resolve_provider, + lsp::InlayHintServerCapabilities::RegistrationOptions(o) => { + o.inlay_hint_options.resolve_provider + } + }, + }) + .unwrap_or(false) + } } #[async_trait(?Send)] @@ -2269,14 +2285,10 @@ impl LspCommand for InlayHints { lsp_adapter.name.0.as_ref() == "typescript-language-server"; let hints = message.unwrap_or_default().into_iter().map(|lsp_hint| { - let resolve_state = match lsp_server.capabilities().inlay_hint_provider { - Some(lsp::OneOf::Right(lsp::InlayHintServerCapabilities::Options( - lsp::InlayHintOptions { - resolve_provider: Some(true), - .. - }, - ))) => ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()), - _ => ResolveState::Resolved, + let resolve_state = if InlayHints::can_resolve_inlays(lsp_server.capabilities()) { + ResolveState::CanResolve(lsp_server.server_id(), lsp_hint.data.clone()) + } else { + ResolveState::Resolved }; let project = project.clone(); diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 0bbb61dfcb..c7765bf55a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -5043,13 +5043,7 @@ impl Project { } else { return Task::ready(Ok(hint)); }; - let can_resolve = lang_server - .capabilities() - .completion_provider - .as_ref() - .and_then(|options| options.resolve_provider) - .unwrap_or(false); - if !can_resolve { + if !InlayHints::can_resolve_inlays(lang_server.capabilities()) { return Task::ready(Ok(hint)); } From 8ed280a029c397b572b961db7aa892e1ed726562 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:30:07 +0300 Subject: [PATCH 078/142] Rebase fixes --- Cargo.lock | 2 +- crates/rpc/proto/zed.proto | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d669be5d9d..8197f883c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1220,7 +1220,7 @@ dependencies = [ "tempfile", "text", "thiserror", - "time 0.3.24", + "time 0.3.27", "tiny_http", "url", "util", diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index dbd700e264..ce47830af2 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -128,8 +128,8 @@ message Envelope { InlayHints inlay_hints = 116; InlayHintsResponse inlay_hints_response = 117; - ResolveInlayHint resolve_inlay_hint = 131; - ResolveInlayHintResponse resolve_inlay_hint_response = 132; + ResolveInlayHint resolve_inlay_hint = 137; + ResolveInlayHintResponse resolve_inlay_hint_response = 138; RefreshInlayHints refresh_inlay_hints = 118; CreateChannel create_channel = 119; From 0a18aa694f13f8cb0f5b3c015f317cb6c03d5dfe Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 14:46:39 +0300 Subject: [PATCH 079/142] Use stricter inlay range checks to avoid stuck highlights Often, hint ranges are separated by a single '<` char as in `Option>`. When moving the caret from left to right, avoid inclusive ranges to faster update the matching hint underline. --- crates/editor/src/hover_popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 19020b643a..3ce936ae82 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -88,7 +88,7 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie if let Some(InfoPopover { symbol_range, .. }) = &editor.hover_state.info_popover { if let DocumentRange::Inlay(range) = symbol_range { - if (range.highlight_start..=range.highlight_end) + if (range.highlight_start..range.highlight_end) .contains(&inlay_hover.triggered_from) { // Hover triggered from same location as last time. Don't show again. From a63e1571dc0d143e3facf7fd61456a31d4250ce5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 01:10:01 +0300 Subject: [PATCH 080/142] Defer querying inlay hints for invisible editor ranges This way, only the visible part gets frequently queried on typing (and hint /refresh requests that follow), with queries for invisible ranges cancelled eagerly. --- crates/editor/src/inlay_hint_cache.rs | 312 +++++++++++++++++--------- 1 file changed, 203 insertions(+), 109 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b0c7d9e0f1..b7f04c68b1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -2,6 +2,7 @@ use std::{ cmp, ops::{ControlFlow, Range}, sync::Arc, + time::Duration, }; use crate::{ @@ -9,6 +10,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; +use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; @@ -17,7 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; -use sum_tree::Bias; +use text::ToOffset; use util::post_inc; pub struct InlayHintCache { @@ -81,7 +83,11 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { + fn new(query_ranges: QueryRanges, task: Task<()>) -> Self { + let mut sorted_ranges = Vec::new(); + sorted_ranges.extend(query_ranges.before_visible); + sorted_ranges.extend(query_ranges.visible); + sorted_ranges.extend(query_ranges.after_visible); Self { tasks: vec![task], sorted_ranges, @@ -91,83 +97,104 @@ impl TasksForRanges { fn update_cached_tasks( &mut self, buffer_snapshot: &BufferSnapshot, - query_range: Range, + query_ranges: QueryRanges, invalidate: InvalidationStrategy, - spawn_task: impl FnOnce(Vec>) -> Task<()>, + spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let ranges_to_query = match invalidate { + let query_ranges = match invalidate { InvalidationStrategy::None => { - let mut ranges_to_query = Vec::new(); - let mut latest_cached_range = None::<&mut Range>; - for cached_range in self - .sorted_ranges - .iter_mut() - .skip_while(|cached_range| { - cached_range - .end - .cmp(&query_range.start, buffer_snapshot) - .is_lt() - }) - .take_while(|cached_range| { - cached_range - .start - .cmp(&query_range.end, buffer_snapshot) - .is_le() - }) - { - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) - < cached_range.start.offset - { - ranges_to_query.push(latest_cached_range.end..cached_range.start); - cached_range.start = latest_cached_range.end; - } - } - None => { - if query_range - .start - .cmp(&cached_range.start, buffer_snapshot) - .is_lt() - { - ranges_to_query.push(query_range.start..cached_range.start); - cached_range.start = query_range.start; - } - } - } - latest_cached_range = Some(cached_range); - } - - match latest_cached_range { - Some(latest_cached_range) => { - if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset - { - ranges_to_query.push(latest_cached_range.end..query_range.end); - latest_cached_range.end = query_range.end; - } - } - None => { - ranges_to_query.push(query_range.clone()); - self.sorted_ranges.push(query_range); - self.sorted_ranges.sort_by(|range_a, range_b| { - range_a.start.cmp(&range_b.start, buffer_snapshot) - }); - } - } - - ranges_to_query + let mut updated_ranges = query_ranges; + updated_ranges.before_visible = updated_ranges + .before_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.visible = updated_ranges + .visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges.after_visible = updated_ranges + .after_visible + .into_iter() + .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .collect(); + updated_ranges } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); self.sorted_ranges.clear(); - vec![query_range] + query_ranges } }; - if !ranges_to_query.is_empty() { - self.tasks.push(spawn_task(ranges_to_query)); + if !query_ranges.is_empty() { + self.tasks.push(spawn_task(query_ranges)); } } + + fn remove_cached_ranges( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + ) -> Vec> { + let mut ranges_to_query = Vec::new(); + let mut latest_cached_range = None::<&mut Range>; + for cached_range in self + .sorted_ranges + .iter_mut() + .skip_while(|cached_range| { + cached_range + .end + .cmp(&query_range.start, buffer_snapshot) + .is_lt() + }) + .take_while(|cached_range| { + cached_range + .start + .cmp(&query_range.end, buffer_snapshot) + .is_le() + }) + { + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < cached_range.start.offset + { + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; + } + } + } + latest_cached_range = Some(cached_range); + } + + match latest_cached_range { + Some(latest_cached_range) => { + if latest_cached_range.end.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(latest_cached_range.end..query_range.end); + latest_cached_range.end = query_range.end; + } + } + None => { + ranges_to_query.push(query_range.clone()); + self.sorted_ranges.push(query_range); + self.sorted_ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + } + } + + ranges_to_query + } } impl InlayHintCache { @@ -515,11 +542,11 @@ fn spawn_new_update_tasks( } }; - let (multi_buffer_snapshot, Some(query_range)) = + let (multi_buffer_snapshot, Some(query_ranges)) = editor.buffer.update(cx, |multi_buffer, cx| { ( multi_buffer.snapshot(cx), - determine_query_range( + determine_query_ranges( multi_buffer, excerpt_id, &excerpt_buffer, @@ -535,10 +562,10 @@ fn spawn_new_update_tasks( invalidate, }; - let new_update_task = |fetch_ranges| { + let new_update_task = |query_ranges| { new_update_task( query, - fetch_ranges, + query_ranges, multi_buffer_snapshot, buffer_snapshot.clone(), Arc::clone(&visible_hints), @@ -551,57 +578,100 @@ fn spawn_new_update_tasks( hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( &buffer_snapshot, - query_range, + query_ranges, invalidate, new_update_task, ); } hash_map::Entry::Vacant(v) => { v.insert(TasksForRanges::new( - vec![query_range.clone()], - new_update_task(vec![query_range]), + query_ranges.clone(), + new_update_task(query_ranges), )); } } } } -fn determine_query_range( +#[derive(Debug, Clone)] +struct QueryRanges { + before_visible: Vec>, + visible: Vec>, + after_visible: Vec>, +} + +impl QueryRanges { + fn is_empty(&self) -> bool { + self.before_visible.is_empty() && self.visible.is_empty() && self.after_visible.is_empty() + } +} + +fn determine_query_ranges( multi_buffer: &mut MultiBuffer, excerpt_id: ExcerptId, excerpt_buffer: &ModelHandle, excerpt_visible_range: Range, cx: &mut ModelContext<'_, MultiBuffer>, -) -> Option> { +) -> Option { let full_excerpt_range = multi_buffer .excerpts_for_buffer(excerpt_buffer, cx) .into_iter() .find(|(id, _)| id == &excerpt_id) .map(|(_, range)| range.context)?; - let buffer = excerpt_buffer.read(cx); + let snapshot = buffer.snapshot(); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start_offset = excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset); - let start = buffer.anchor_before(buffer.clip_offset(start_offset, Bias::Left)); - let end_offset = excerpt_visible_range - .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) - .min(buffer.len()); - let end = buffer.anchor_after(buffer.clip_offset(end_offset, Bias::Right)); - if start.cmp(&end, buffer).is_eq() { - None + + let visible_range = if excerpt_visible_range.start == excerpt_visible_range.end { + return None; } else { - Some(start..end) - } + vec![ + buffer.anchor_before(excerpt_visible_range.start) + ..buffer.anchor_after(excerpt_visible_range.end), + ] + }; + + let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); + let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + Vec::new() + } else { + let after_range_end_offset = excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + vec![ + buffer.anchor_before(excerpt_visible_range.end) + ..buffer.anchor_after(after_range_end_offset), + ] + }; + + let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); + let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + Vec::new() + } else { + let before_range_start_offset = excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range_start_offset); + vec![ + buffer.anchor_before(before_range_start_offset) + ..buffer.anchor_after(excerpt_visible_range.start), + ] + }; + + Some(QueryRanges { + before_visible: before_visible_range, + visible: visible_range, + after_visible: after_visible_range, + }) } +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; + fn new_update_task( query: ExcerptQuery, - hint_fetch_ranges: Vec>, + query_ranges: QueryRanges, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -609,24 +679,48 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, cx| async move { - let task_update_results = - futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { - fetch_and_update_hints( - editor.clone(), - multi_buffer_snapshot.clone(), - buffer_snapshot.clone(), - Arc::clone(&visible_hints), - cached_excerpt_hints.as_ref().map(Arc::clone), - query, - range, - cx.clone(), - ) - })) + let fetch_and_update_hints = |range| { + fetch_and_update_hints( + editor.clone(), + multi_buffer_snapshot.clone(), + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints.as_ref().map(Arc::clone), + query, + range, + cx.clone(), + ) + }; + let visible_range_update_results = future::join_all( + query_ranges + .visible + .into_iter() + .map(|visible_range| fetch_and_update_hints(visible_range)), + ) + .await; + for result in visible_range_update_results { + if let Err(e) = result { + error!("visible range inlay hint update task failed: {e:#}"); + } + } + + cx.background() + .timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )) .await; - for result in task_update_results { + let invisible_range_update_results = future::join_all( + query_ranges + .before_visible + .into_iter() + .chain(query_ranges.after_visible.into_iter()) + .map(|invisible_range| fetch_and_update_hints(invisible_range)), + ) + .await; + for result in invisible_range_update_results { if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); + error!("invisible range inlay hint update task failed: {e:#}"); } } }) @@ -1816,7 +1910,7 @@ pub mod tests { }); })); } - let _ = futures::future::join_all(edits).await; + let _ = future::join_all(edits).await; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { From c10c3e2b544895b166d7d2f34f34f095a839f498 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 15:09:06 +0300 Subject: [PATCH 081/142] Only invalidate when doing first, visible range query --- crates/editor/src/inlay_hint_cache.rs | 30 ++++++++++++++------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b7f04c68b1..edd20bed30 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -679,7 +679,7 @@ fn new_update_task( cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, cx| async move { - let fetch_and_update_hints = |range| { + let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -687,17 +687,16 @@ fn new_update_task( Arc::clone(&visible_hints), cached_excerpt_hints.as_ref().map(Arc::clone), query, + invalidate, range, cx.clone(), ) }; - let visible_range_update_results = future::join_all( - query_ranges - .visible - .into_iter() - .map(|visible_range| fetch_and_update_hints(visible_range)), - ) - .await; + let visible_range_update_results = + future::join_all(query_ranges.visible.into_iter().map(|visible_range| { + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + })) + .await; for result in visible_range_update_results { if let Err(e) = result { error!("visible range inlay hint update task failed: {e:#}"); @@ -715,7 +714,7 @@ fn new_update_task( .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(invisible_range)), + .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), ) .await; for result in invisible_range_update_results { @@ -733,6 +732,7 @@ async fn fetch_and_update_hints( visible_hints: Arc>, cached_excerpt_hints: Option>>, query: ExcerptQuery, + invalidate: bool, fetch_range: Range, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { @@ -761,7 +761,8 @@ async fn fetch_and_update_hints( .background() .spawn(async move { calculate_hint_updates( - query, + query.excerpt_id, + invalidate, backround_fetch_range, new_hints, &background_task_buffer_snapshot, @@ -788,7 +789,8 @@ async fn fetch_and_update_hints( } fn calculate_hint_updates( - query: ExcerptQuery, + excerpt_id: ExcerptId, + invalidate: bool, fetch_range: Range, new_excerpt_hints: Vec, buffer_snapshot: &BufferSnapshot, @@ -836,11 +838,11 @@ fn calculate_hint_updates( let mut remove_from_visible = Vec::new(); let mut remove_from_cache = HashSet::default(); - if query.invalidate.should_invalidate() { + if invalidate { remove_from_visible.extend( visible_hints .iter() - .filter(|hint| hint.position.excerpt_id == query.excerpt_id) + .filter(|hint| hint.position.excerpt_id == excerpt_id) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -863,7 +865,7 @@ fn calculate_hint_updates( None } else { Some(ExcerptHintsUpdate { - excerpt_id: query.excerpt_id, + excerpt_id, remove_from_visible, remove_from_cache, add_to_cache, From 2b95f0580e340f54c129d0d4f88e6480298e5fb2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 16:17:45 +0300 Subject: [PATCH 082/142] Fix the tests --- crates/editor/src/inlay_hint_cache.rs | 269 +++++++++++++++----------- 1 file changed, 159 insertions(+), 110 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index edd20bed30..54ed8f8f11 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -632,31 +632,38 @@ fn determine_query_ranges( }; let full_excerpt_range_end_offset = full_excerpt_range.end.to_offset(&snapshot); - let after_visible_range = if excerpt_visible_range.end == full_excerpt_range_end_offset { + let after_visible_range_start = excerpt_visible_range + .end + .saturating_add(1) + .min(full_excerpt_range_end_offset) + .min(buffer.len()); + let after_visible_range = if after_visible_range_start == full_excerpt_range_end_offset { Vec::new() } else { - let after_range_end_offset = excerpt_visible_range - .end + let after_range_end_offset = after_visible_range_start .saturating_add(excerpt_visible_len) .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(excerpt_visible_range.end) + buffer.anchor_before(after_visible_range_start) ..buffer.anchor_after(after_range_end_offset), ] }; let full_excerpt_range_start_offset = full_excerpt_range.start.to_offset(&snapshot); - let before_visible_range = if excerpt_visible_range.start == full_excerpt_range_start_offset { + let before_visible_range_end = excerpt_visible_range + .start + .saturating_sub(1) + .max(full_excerpt_range_start_offset); + let before_visible_range = if before_visible_range_end == full_excerpt_range_start_offset { Vec::new() } else { - let before_range_start_offset = excerpt_visible_range - .start + let before_range_start_offset = before_visible_range_end .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(excerpt_visible_range.start), + ..buffer.anchor_after(before_visible_range_end), ] }; @@ -667,7 +674,7 @@ fn determine_query_ranges( }) } -const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 300; +const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( query: ExcerptQuery, @@ -1001,7 +1008,7 @@ fn apply_hint_update( #[cfg(test)] pub mod tests { - use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; + use std::sync::atomic::{AtomicBool, AtomicU32, AtomicUsize, Ordering}; use crate::{ scroll::{autoscroll::Autoscroll, scroll_amount::ScrollAmount}, @@ -1079,13 +1086,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1104,13 +1111,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after an edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1129,13 +1136,13 @@ pub mod tests { edits_made += 1; cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string(), "1".to_string(), "2".to_string()]; + let expected_hints = vec!["0".to_string(), "1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get new hints after hint refresh/ request" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( inlay_cache.allowed_hint_kinds, allowed_hint_kinds, @@ -1189,13 +1196,13 @@ pub mod tests { let mut edits_made = 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1220,13 +1227,13 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should not update hints while the work task is running" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1244,13 +1251,13 @@ pub mod tests { edits_made += 1; editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "New hints should be queried after the work task is done" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, edits_made, @@ -1363,13 +1370,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should get its first hints when opening the editor" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 1, @@ -1421,13 +1428,13 @@ pub mod tests { .await; cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should have a separate verison, repeating Rust editor rules" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1437,13 +1444,13 @@ pub mod tests { }); cx.foreground().run_until_parked(); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust inlay cache should change after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, 2, @@ -1451,13 +1458,13 @@ pub mod tests { ); }); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["0".to_string()]; + let expected_hints = vec!["0".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should not be affected by Rust editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); @@ -1467,23 +1474,23 @@ pub mod tests { }); cx.foreground().run_until_parked(); md_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Rust editor should not be affected by Markdown editor changes" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); rs_editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; + let expected_hints = vec!["1".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Markdown editor should also change independently" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 2); }); } @@ -2009,7 +2016,7 @@ pub mod tests { .downcast::() .unwrap(); let lsp_request_ranges = Arc::new(Mutex::new(Vec::new())); - let lsp_request_count = Arc::new(AtomicU32::new(0)); + let lsp_request_count = Arc::new(AtomicUsize::new(0)); let closure_lsp_request_ranges = Arc::clone(&lsp_request_ranges); let closure_lsp_request_count = Arc::clone(&lsp_request_count); fake_server @@ -2023,10 +2030,9 @@ pub mod tests { ); task_lsp_request_ranges.lock().push(params.range); - let query_start = params.range.start; let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { - position: query_start, + position: params.range.end, label: lsp::InlayHintLabel::String(i.to_string()), kind: None, text_edits: None, @@ -2063,28 +2069,51 @@ pub mod tests { }) } - let initial_visible_range = editor_visible_range(&editor, cx); - let expected_initial_query_range_end = - lsp::Position::new(initial_visible_range.end.row * 2, 1); + // in large buffers, requests are made for more than visible range of a buffer. + // invisible parts are queried later, to avoid excessive requests on quick typing. + // wait the timeout needed to get all requests. + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); + let initial_visible_range = editor_visible_range(&editor, cx); + let lsp_initial_visible_range = lsp::Range::new( + lsp::Position::new( + initial_visible_range.start.row, + initial_visible_range.start.column, + ), + lsp::Position::new( + initial_visible_range.end.row, + initial_visible_range.end.column, + ), + ); + let expected_initial_query_range_end = + lsp::Position::new(initial_visible_range.end.row * 2, 2); + let mut expected_invisible_query_start = lsp_initial_visible_range.end; + expected_invisible_query_start.character += 1; editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "When scroll is at the edge of a big document, double of its visible part range should be queried for hints in one single big request, but got: {ranges:?}"); - let query_range = &ranges[0]; - assert_eq!(query_range.start, lsp::Position::new(0, 0), "Should query initially from the beginning of the document"); - assert_eq!(query_range.end, expected_initial_query_range_end, "Should query initially for double lines of the visible part of the document"); + assert_eq!(ranges.len(), 2, + "When scroll is at the edge of a big document, its visible part and the same range further should be queried in order, but got: {ranges:?}"); + let visible_query_range = &ranges[0]; + assert_eq!(visible_query_range.start, lsp_initial_visible_range.start); + assert_eq!(visible_query_range.end, lsp_initial_visible_range.end); + let invisible_query_range = &ranges[1]; - assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); - let expected_layers = vec!["1".to_string()]; + assert_eq!(invisible_query_range.start, expected_invisible_query_start, "Should initially query visible edge of the document"); + assert_eq!(invisible_query_range.end, expected_initial_query_range_end, "Should initially query visible edge of the document"); + + let requests_count = lsp_request_count.load(Ordering::Acquire); + assert_eq!(requests_count, 2, "Visible + invisible request"); + let expected_hints = vec!["1".to_string(), "2".to_string()]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from both LSP requests made for a big file" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx), "Should display only hints from the visible range"); assert_eq!( - editor.inlay_hint_cache().version, 1, + editor.inlay_hint_cache().version, requests_count, "LSP queries should've bumped the cache version" ); }); @@ -2093,11 +2122,13 @@ pub mod tests { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); }); - + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); + cx.foreground().run_until_parked(); let visible_range_after_scrolls = editor_visible_range(&editor, cx); let visible_line_count = editor.update(cx, |editor, _| editor.visible_line_count().unwrap()); - cx.foreground().run_until_parked(); let selection_in_cached_range = editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges .lock() @@ -2124,26 +2155,28 @@ pub mod tests { lsp::Position::new( visible_range_after_scrolls.end.row + visible_line_count.ceil() as u32, - 0 + 1, ), "Second scroll should query one more screen down after the end of the visible range" ); + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 4, "Should query for hints after every scroll"); + let expected_hints = vec![ + "1".to_string(), + "2".to_string(), + "3".to_string(), + "4".to_string(), + ]; assert_eq!( - lsp_request_count.load(Ordering::Acquire), - 3, - "Should query for hints after every scroll" - ); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; - assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - 3, + lsp_requests, "Should update the cache for every LSP response with hints added" ); @@ -2157,6 +2190,9 @@ pub mod tests { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |_, _| { let ranges = lsp_request_ranges @@ -2165,33 +2201,43 @@ pub mod tests { .sorted_by_key(|r| r.start) .collect::>(); assert!(ranges.is_empty(), "No new ranges or LSP queries should be made after returning to the selection with cached hints"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3); + assert_eq!(lsp_request_count.load(Ordering::Acquire), 4); }); editor.update(cx, |editor, cx| { editor.handle_input("++++more text++++", cx); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - assert_eq!(ranges.len(), 1, - "On edit, should scroll to selection and query a range around it. Instead, got query ranges {ranges:?}"); - let query_range = &ranges[0]; - assert!(query_range.start.line < selection_in_cached_range.row, + assert_eq!(ranges.len(), 3, + "On edit, should scroll to selection and query a range around it: visible + same range above and below. Instead, got query ranges {ranges:?}"); + let visible_query_range = &ranges[0]; + let above_query_range = &ranges[1]; + let below_query_range = &ranges[2]; + assert!(above_query_range.end.character < visible_query_range.start.character || above_query_range.end.line + 1 == visible_query_range.start.line, + "Above range {above_query_range:?} should be before visible range {visible_query_range:?}"); + assert!(visible_query_range.end.character < below_query_range.start.character || visible_query_range.end.line + 1 == below_query_range.start.line, + "Visible range {visible_query_range:?} should be before below range {below_query_range:?}"); + assert!(above_query_range.start.line < selection_in_cached_range.row, "Hints should be queried with the selected range after the query range start"); - assert!(query_range.end.line > selection_in_cached_range.row, + assert!(below_query_range.end.line > selection_in_cached_range.row, "Hints should be queried with the selected range before the query range end"); - assert!(query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, + assert!(above_query_range.start.line <= selection_in_cached_range.row - (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen before"); - assert!(query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, + assert!(below_query_range.end.line >= selection_in_cached_range.row + (visible_line_count * 3.0 / 2.0) as u32, "Hints query range should contain one more screen after"); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 4, "Should query for hints once after the edit"); - let expected_layers = vec!["4".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor), + let lsp_requests = lsp_request_count.load(Ordering::Acquire); + assert_eq!(lsp_requests, 7, "There should be a visible range and two ranges above and below it queried"); + let expected_hints = vec!["5".to_string(), "6".to_string(), "7".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor), "Should have hints from the new LSP response after the edit"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, lsp_requests, "Should update the cache for every LSP response with hints added"); }); } @@ -2402,19 +2448,19 @@ pub mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "When scroll is at the edge of a multibuffer, its visible excerpts only should be queried for inlay hints" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), "Every visible excerpt hints should bump the verison"); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Every visible excerpt hints should bump the verison"); }); editor.update(cx, |editor, cx| { @@ -2430,7 +2476,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2441,10 +2487,10 @@ pub mod tests { "other hint #1".to_string(), "other hint #2".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "With more scrolls of the multibuffer, more hints should be added into the cache and nothing invalidated without edits"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len(), + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len(), "Due to every excerpt having one hint, we update cache per new excerpt scrolled"); }); @@ -2453,9 +2499,12 @@ pub mod tests { s.select_ranges([Point::new(100, 0)..Point::new(100, 0)]) }); }); + cx.foreground().advance_clock(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS + 100, + )); cx.foreground().run_until_parked(); let last_scroll_update_version = editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2469,11 +2518,11 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, all hints for all excerpts should be fetched"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); - assert_eq!(editor.inlay_hint_cache().version, expected_layers.len()); - expected_layers.len() + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); + assert_eq!(editor.inlay_hint_cache().version, expected_hints.len()); + expected_hints.len() }); editor.update(cx, |editor, cx| { @@ -2483,7 +2532,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint #0".to_string(), "main hint #1".to_string(), "main hint #2".to_string(), @@ -2497,9 +2546,9 @@ pub mod tests { "other hint #4".to_string(), "other hint #5".to_string(), ]; - assert_eq!(expected_layers, cached_hint_labels(editor), + assert_eq!(expected_hints, cached_hint_labels(editor), "After multibuffer was scrolled to the end, further scrolls up should not bring more hints"); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, last_scroll_update_version, "No updates should happen during scrolling already scolled buffer"); }); @@ -2512,7 +2561,7 @@ pub mod tests { }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec![ + let expected_hints = vec![ "main hint(edited) #0".to_string(), "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), @@ -2523,15 +2572,15 @@ pub mod tests { "other hint(edited) #1".to_string(), ]; assert_eq!( - expected_layers, + expected_hints, cached_hint_labels(editor), "After multibuffer edit, editor gets scolled back to the last selection; \ all hints should be invalidated and requeried for all of its visible excerpts" ); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); let current_cache_version = editor.inlay_hint_cache().version; - let minimum_expected_version = last_scroll_update_version + expected_layers.len(); + let minimum_expected_version = last_scroll_update_version + expected_hints.len(); assert!( current_cache_version == minimum_expected_version || current_cache_version == minimum_expected_version + 1, "Due to every excerpt having one hint, cache should update per new excerpt received + 1 potential sporadic update" @@ -2872,9 +2921,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let expected_layers = vec!["1".to_string()]; - assert_eq!(expected_layers, cached_hint_labels(editor)); - assert_eq!(expected_layers, visible_hint_labels(editor, cx)); + let expected_hints = vec!["1".to_string()]; + assert_eq!(expected_hints, cached_hint_labels(editor)); + assert_eq!(expected_hints, visible_hint_labels(editor, cx)); assert_eq!(editor.inlay_hint_cache().version, 1); }); } From 44c340b5f25dfd6bc2a27192e5f05a9e8c154101 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 25 Aug 2023 17:33:17 +0300 Subject: [PATCH 083/142] Properly invalidate the hint cache --- crates/editor/src/inlay_hint_cache.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 54ed8f8f11..b33b93a348 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -785,6 +785,7 @@ async fn fetch_and_update_hints( editor, new_update, query, + invalidate, buffer_snapshot, multi_buffer_snapshot, cx, @@ -893,6 +894,7 @@ fn apply_hint_update( editor: &mut Editor, new_update: ExcerptHintsUpdate, query: ExcerptQuery, + invalidate: bool, buffer_snapshot: BufferSnapshot, multi_buffer_snapshot: MultiBufferSnapshot, cx: &mut ViewContext<'_, '_, Editor>, @@ -970,7 +972,7 @@ fn apply_hint_update( cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); - if query.invalidate.should_invalidate() { + if invalidate { let mut outdated_excerpt_caches = HashSet::default(); for (excerpt_id, excerpt_hints) in &editor.inlay_hint_cache().hints { let excerpt_hints = excerpt_hints.read(); From 732af201dc599ef58a0ce650658775c6060ee58e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 09:59:16 -0700 Subject: [PATCH 084/142] Upgrade to rust 1.72 --- Dockerfile | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 77d011490e..208700f7fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.71-bullseye as builder +FROM rust:1.72-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 50003020e9..7ed8b98280 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.71" +channel = "1.72" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From 404f76739c3e032b83b0f69f13441f3b3a4025a6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:11:32 -0700 Subject: [PATCH 085/142] Format let-else statements --- crates/ai/src/assistant.rs | 13 +++- crates/call/src/room.rs | 4 +- crates/collab/src/db.rs | 4 +- crates/collab/src/db/queries/rooms.rs | 6 +- .../src/tests/randomized_integration_tests.rs | 35 +++++++---- crates/collab_ui/src/channel_view.rs | 8 ++- .../src/collab_panel/channel_modal.rs | 9 +-- crates/editor/src/editor.rs | 60 ++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 10 +++- crates/editor/src/items.rs | 20 +++++-- crates/editor/src/multi_buffer.rs | 4 +- .../gpui/src/keymap_matcher/keymap_context.rs | 4 +- crates/language/src/buffer.rs | 4 +- crates/language/src/syntax_map.rs | 12 +++- .../src/syntax_map/syntax_map_tests.rs | 8 ++- crates/language_tools/src/lsp_log.rs | 4 +- crates/project/src/lsp_command.rs | 14 ++++- crates/project/src/worktree.rs | 12 ++-- .../quick_action_bar/src/quick_action_bar.rs | 4 +- crates/search/src/project_search.rs | 4 +- crates/semantic_index/src/semantic_index.rs | 4 +- crates/settings/src/keymap_file.rs | 19 +++--- crates/vcs_menu/src/lib.rs | 32 +++++----- crates/vim/src/normal/paste.rs | 2 +- crates/vim/src/visual.rs | 7 ++- crates/workspace/src/pane_group.rs | 4 +- crates/workspace/src/workspace.rs | 8 ++- crates/zed/src/languages/python.rs | 4 +- 28 files changed, 210 insertions(+), 109 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 81299bbdc2..2699bf40a0 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1128,7 +1128,9 @@ impl Conversation { stream: true, }; - let Some(api_key) = self.api_key.borrow().clone() else { continue }; + let Some(api_key) = self.api_key.borrow().clone() else { + continue; + }; let stream = stream_completion(api_key, cx.background().clone(), request); let assistant_message = self .insert_message_after( @@ -1484,7 +1486,9 @@ impl Conversation { }) { current_message = messages.next(); } - let Some(message) = current_message.as_ref() else { break }; + let Some(message) = current_message.as_ref() else { + break; + }; // Skip offsets that are in the same message. while offsets.peek().map_or(false, |offset| { @@ -1921,7 +1925,10 @@ impl ConversationEditor { let Some(panel) = workspace.panel::(cx) else { return; }; - let Some(editor) = workspace.active_item(cx).and_then(|item| item.act_as::(cx)) else { + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { return; }; diff --git a/crates/call/src/room.rs b/crates/call/src/room.rs index 6f01b1d757..cc7445dbcc 100644 --- a/crates/call/src/room.rs +++ b/crates/call/src/room.rs @@ -644,7 +644,9 @@ impl Room { if let Some(participants) = remote_participants.log_err() { for (participant, user) in room.participants.into_iter().zip(participants) { - let Some(peer_id) = participant.peer_id else { continue }; + let Some(peer_id) = participant.peer_id else { + continue; + }; this.participant_user_ids.insert(participant.user_id); let old_projects = this diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 9c759f79a8..888158188f 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -249,7 +249,9 @@ impl Database { let mut tx = Arc::new(Some(tx)); let result = f(TransactionHandle(tx.clone())).await; let Some(tx) = Arc::get_mut(&mut tx).and_then(|tx| tx.take()) else { - return Err(anyhow!("couldn't complete transaction because it's still in use"))?; + return Err(anyhow!( + "couldn't complete transaction because it's still in use" + ))?; }; Ok((tx, result)) diff --git a/crates/collab/src/db/queries/rooms.rs b/crates/collab/src/db/queries/rooms.rs index a85d257187..435e729fed 100644 --- a/crates/collab/src/db/queries/rooms.rs +++ b/crates/collab/src/db/queries/rooms.rs @@ -465,9 +465,9 @@ impl Database { let mut rejoined_projects = Vec::new(); for rejoined_project in &rejoin_room.rejoined_projects { let project_id = ProjectId::from_proto(rejoined_project.id); - let Some(project) = project::Entity::find_by_id(project_id) - .one(&*tx) - .await? else { continue }; + let Some(project) = project::Entity::find_by_id(project_id).one(&*tx).await? else { + continue; + }; let mut worktrees = Vec::new(); let db_worktrees = project.find_related(worktree::Entity).all(&*tx).await?; diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 18fe6734cd..3557843828 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -121,7 +121,9 @@ async fn test_random_collaboration( let mut operation_channels = Vec::new(); loop { - let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { break }; + let Some((next_operation, applied)) = plan.lock().next_server_operation(&clients) else { + break; + }; applied.store(true, SeqCst); let did_apply = apply_server_operation( deterministic.clone(), @@ -224,7 +226,9 @@ async fn apply_server_operation( let client_ix = clients .iter() .position(|(client, cx)| client.current_user_id(cx) == removed_user_id); - let Some(client_ix) = client_ix else { return false }; + let Some(client_ix) = client_ix else { + return false; + }; let user_connection_ids = server .connection_pool .lock() @@ -1591,10 +1595,11 @@ impl TestPlan { 81.. => match self.rng.gen_range(0..100_u32) { // Add a worktree to a local project 0..=50 => { - let Some(project) = client - .local_projects() - .choose(&mut self.rng) - .cloned() else { continue }; + let Some(project) = + client.local_projects().choose(&mut self.rng).cloned() + else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let mut paths = client.fs().paths(false); paths.remove(0); @@ -1611,7 +1616,9 @@ impl TestPlan { // Add an entry to a worktree _ => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); let worktree = project.read_with(cx, |project, cx| { @@ -1645,7 +1652,9 @@ impl TestPlan { // Query and mutate buffers 60..=90 => { - let Some(project) = choose_random_project(client, &mut self.rng) else { continue }; + let Some(project) = choose_random_project(client, &mut self.rng) else { + continue; + }; let project_root_name = root_name_for_project(&project, cx); let is_local = project.read_with(cx, |project, _| project.is_local()); @@ -1656,7 +1665,10 @@ impl TestPlan { .buffers_for_project(&project) .iter() .choose(&mut self.rng) - .cloned() else { continue }; + .cloned() + else { + continue; + }; let full_path = buffer .read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx)); @@ -2026,7 +2038,10 @@ async fn simulate_client( client.app_state.languages.add(Arc::new(language)); while let Some(batch_id) = operation_rx.next().await { - let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) else { break }; + let Some((operation, applied)) = plan.lock().next_client_operation(&client, batch_id, &cx) + else { + break; + }; applied.store(true, SeqCst); match apply_client_operation(&client, operation, &mut cx).await { Ok(()) => {} diff --git a/crates/collab_ui/src/channel_view.rs b/crates/collab_ui/src/channel_view.rs index bb1e840ffc..a34f10b2db 100644 --- a/crates/collab_ui/src/channel_view.rs +++ b/crates/collab_ui/src/channel_view.rs @@ -272,8 +272,12 @@ impl FollowableItem for ChannelView { state: &mut Option, cx: &mut AppContext, ) -> Option>>> { - let Some(proto::view::Variant::ChannelView(_)) = state else { return None }; - let Some(proto::view::Variant::ChannelView(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; let open = ChannelView::open(state.channel_id, pane, workspace, cx); diff --git a/crates/collab_ui/src/collab_panel/channel_modal.rs b/crates/collab_ui/src/collab_panel/channel_modal.rs index 0adf2806d7..4c811a2df5 100644 --- a/crates/collab_ui/src/collab_panel/channel_modal.rs +++ b/crates/collab_ui/src/collab_panel/channel_modal.rs @@ -152,12 +152,9 @@ impl View for ChannelModal { let theme = &theme::current(cx).collab_panel.tabbed_modal; let mode = self.picker.read(cx).delegate().mode; - let Some(channel) = self - .channel_store - .read(cx) - .channel_for_id(self.channel_id) else { - return Empty::new().into_any() - }; + let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else { + return Empty::new().into_any(); + }; enum InviteMembers {} enum ManageMembers {} diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 681e1d48b2..c5ff1f027d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -6243,7 +6243,9 @@ impl Editor { ) { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_offsets_with(|snapshot, selection| { - let Some(enclosing_bracket_ranges) = snapshot.enclosing_bracket_ranges(selection.start..selection.end) else { + let Some(enclosing_bracket_ranges) = + snapshot.enclosing_bracket_ranges(selection.start..selection.end) + else { return; }; @@ -6255,7 +6257,8 @@ impl Editor { let close = close.to_inclusive(); let length = close.end() - open.start; let inside = selection.start >= open.end && selection.end <= *close.start(); - let in_bracket_range = open.to_inclusive().contains(&selection.head()) || close.contains(&selection.head()); + let in_bracket_range = open.to_inclusive().contains(&selection.head()) + || close.contains(&selection.head()); // If best is next to a bracket and current isn't, skip if !in_bracket_range && best_in_bracket_range { @@ -6270,19 +6273,21 @@ impl Editor { best_length = length; best_inside = inside; best_in_bracket_range = in_bracket_range; - best_destination = Some(if close.contains(&selection.start) && close.contains(&selection.end) { - if inside { - open.end + best_destination = Some( + if close.contains(&selection.start) && close.contains(&selection.end) { + if inside { + open.end + } else { + open.start + } } else { - open.start - } - } else { - if inside { - *close.start() - } else { - *close.end() - } - }); + if inside { + *close.start() + } else { + *close.end() + } + }, + ); } if let Some(destination) = best_destination { @@ -6526,7 +6531,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let buffer = self.buffer.read(cx); let head = self.selections.newest::(cx).head(); let (buffer, head) = if let Some(text_anchor) = buffer.text_anchor_for_position(head, cx) { @@ -6557,7 +6564,9 @@ impl Editor { split: bool, cx: &mut ViewContext, ) { - let Some(workspace) = self.workspace(cx) else { return }; + let Some(workspace) = self.workspace(cx) else { + return; + }; let pane = workspace.read(cx).active_pane().clone(); // If there is one definition, just open it directly if definitions.len() == 1 { @@ -7639,10 +7648,9 @@ impl Editor { let search_range = display_snapshot.anchor_to_inlay_offset(search_range.start) ..display_snapshot.anchor_to_inlay_offset(search_range.end); let mut results = Vec::new(); - let Some((_, ranges)) = self.background_highlights - .get(&TypeId::of::()) else { - return vec![]; - }; + let Some((_, ranges)) = self.background_highlights.get(&TypeId::of::()) else { + return vec![]; + }; let start_ix = match ranges.binary_search_by(|probe| { let cmp = document_to_inlay_range(probe, display_snapshot) @@ -7993,9 +8001,7 @@ impl Editor { suggestion_accepted: bool, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are either getting suggestions in a new, unsaved file, or in a file without an extension let file_extension = self @@ -8024,9 +8030,7 @@ impl Editor { file_extension: Option, cx: &AppContext, ) { - let Some(project) = &self.project else { - return - }; + let Some(project) = &self.project else { return }; // If None, we are in a file without an extension let file = self @@ -8127,7 +8131,9 @@ impl Editor { } } - let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { return; }; + let Some(lines) = serde_json::to_string_pretty(&lines).log_err() else { + return; + }; cx.write_to_clipboard(ClipboardItem::new(lines)); } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b33b93a348..a4b91e826d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -341,7 +341,10 @@ impl InlayHintCache { shown_excerpt_hints_to_remove.retain(|(shown_anchor, shown_hint_id)| { let Some(buffer) = shown_anchor .buffer_id - .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) else { return false }; + .and_then(|buffer_id| multi_buffer.buffer(buffer_id)) + else { + return false; + }; let buffer_snapshot = buffer.read(cx).snapshot(); loop { match excerpt_cache.peek() { @@ -554,7 +557,10 @@ fn spawn_new_update_tasks( cx, ), ) - }) else { return; }; + }) + else { + return; + }; let query = ExcerptQuery { buffer_id, excerpt_id, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 30ed56af47..d999872592 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -55,8 +55,12 @@ impl FollowableItem for Editor { cx: &mut AppContext, ) -> Option>>> { let project = workspace.read(cx).project().to_owned(); - let Some(proto::view::Variant::Editor(_)) = state else { return None }; - let Some(proto::view::Variant::Editor(state)) = state.take() else { unreachable!() }; + let Some(proto::view::Variant::Editor(_)) = state else { + return None; + }; + let Some(proto::view::Variant::Editor(state)) = state.take() else { + unreachable!() + }; let client = project.read(cx).client(); let replica_id = project.read(cx).replica_id(); @@ -341,10 +345,16 @@ async fn update_editor_from_message( let mut insertions = message.inserted_excerpts.into_iter().peekable(); while let Some(insertion) = insertions.next() { - let Some(excerpt) = insertion.excerpt else { continue }; - let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { continue }; + let Some(excerpt) = insertion.excerpt else { + continue; + }; + let Some(previous_excerpt_id) = insertion.previous_excerpt_id else { + continue; + }; let buffer_id = excerpt.buffer_id; - let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { continue }; + let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else { + continue; + }; let adjacent_excerpts = iter::from_fn(|| { let insertion = insertions.peek()?; diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 9dd40af898..b9bf4f52a1 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -2756,7 +2756,9 @@ impl MultiBufferSnapshot { // Get the ranges of the innermost pair of brackets. let mut result: Option<(Range, Range)> = None; - let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { return None; }; + let Some(enclosing_bracket_ranges) = self.enclosing_bracket_ranges(range.clone()) else { + return None; + }; for (open, close) in enclosing_bracket_ranges { let len = close.end - open.start; diff --git a/crates/gpui/src/keymap_matcher/keymap_context.rs b/crates/gpui/src/keymap_matcher/keymap_context.rs index fd60a8f4b5..d9c54dbc8e 100644 --- a/crates/gpui/src/keymap_matcher/keymap_context.rs +++ b/crates/gpui/src/keymap_matcher/keymap_context.rs @@ -67,7 +67,9 @@ impl KeymapContextPredicate { } pub fn eval(&self, contexts: &[KeymapContext]) -> bool { - let Some(context) = contexts.first() else { return false }; + let Some(context) = contexts.first() else { + return false; + }; match self { Self::Identifier(name) => (&context.set).contains(name.as_str()), Self::Equal(left, right) => context diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 1b83ca5964..7569925256 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2484,7 +2484,9 @@ impl BufferSnapshot { matches.advance(); - let Some((open, close)) = open.zip(close) else { continue }; + let Some((open, close)) = open.zip(close) else { + continue; + }; let bracket_range = open.start..=close.end; if !bracket_range.overlaps(&range) { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index b6e1d16e18..18f2e9b264 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -310,7 +310,9 @@ impl SyntaxSnapshot { // Ignore edits that end before the start of this layer, and don't consider them // for any subsequent layers at this same depth. loop { - let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { continue 'outer }; + let Some((_, edit_range)) = edits.get(first_edit_ix_for_depth) else { + continue 'outer; + }; if edit_range.end.cmp(&layer.range.start, text).is_le() { first_edit_ix_for_depth += 1; } else { @@ -391,7 +393,9 @@ impl SyntaxSnapshot { .filter::<_, ()>(|summary| summary.contains_unknown_injections); cursor.next(text); while let Some(layer) = cursor.item() { - let SyntaxLayerContent::Pending { language_name } = &layer.content else { unreachable!() }; + let SyntaxLayerContent::Pending { language_name } = &layer.content else { + unreachable!() + }; if registry .language_for_name_or_extension(language_name) .now_or_never() @@ -533,7 +537,9 @@ impl SyntaxSnapshot { let content = match step.language { ParseStepLanguage::Loaded { language } => { - let Some(grammar) = language.grammar() else { continue }; + let Some(grammar) = language.grammar() else { + continue; + }; let tree; let changed_ranges; diff --git a/crates/language/src/syntax_map/syntax_map_tests.rs b/crates/language/src/syntax_map/syntax_map_tests.rs index c7babf207e..bd50608122 100644 --- a/crates/language/src/syntax_map/syntax_map_tests.rs +++ b/crates/language/src/syntax_map/syntax_map_tests.rs @@ -932,8 +932,12 @@ fn check_interpolation( .zip(new_syntax_map.layers.iter()) { assert_eq!(old_layer.range, new_layer.range); - let Some(old_tree) = old_layer.content.tree() else { continue }; - let Some(new_tree) = new_layer.content.tree() else { continue }; + let Some(old_tree) = old_layer.content.tree() else { + continue; + }; + let Some(new_tree) = new_layer.content.tree() else { + continue; + }; let old_start_byte = old_layer.range.start.to_offset(old_buffer); let new_start_byte = new_layer.range.start.to_offset(new_buffer); let old_start_point = old_layer.range.start.to_point(old_buffer).to_ts_point(); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index 16fb019c62..d18f57ffe9 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -549,7 +549,9 @@ impl View for LspLogToolbarItemView { fn render(&mut self, cx: &mut ViewContext) -> AnyElement { let theme = theme::current(cx).clone(); - let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; + let Some(log_view) = self.log_view.as_ref() else { + return Empty::new().into_any(); + }; let log_view = log_view.read(cx); let menu_rows = log_view.menu_items(cx).unwrap_or_default(); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 292f9a5226..8ca2571869 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1655,7 +1655,11 @@ impl LspCommand for OnTypeFormatting { type ProtoRequest = proto::OnTypeFormatting; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(on_type_formatting_options) = &server_capabilities.document_on_type_formatting_provider else { return false }; + let Some(on_type_formatting_options) = + &server_capabilities.document_on_type_formatting_provider + else { + return false; + }; on_type_formatting_options .first_trigger_character .contains(&self.trigger) @@ -1769,7 +1773,9 @@ impl LspCommand for OnTypeFormatting { _: ModelHandle, _: AsyncAppContext, ) -> Result> { - let Some(transaction) = message.transaction else { return Ok(None) }; + let Some(transaction) = message.transaction else { + return Ok(None); + }; Ok(Some(language::proto::deserialize_transaction(transaction)?)) } @@ -2238,7 +2244,9 @@ impl LspCommand for InlayHints { type ProtoRequest = proto::InlayHints; fn check_capabilities(&self, server_capabilities: &lsp::ServerCapabilities) -> bool { - let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { return false }; + let Some(inlay_hint_provider) = &server_capabilities.inlay_hint_provider else { + return false; + }; match inlay_hint_provider { lsp::OneOf::Left(enabled) => *enabled, lsp::OneOf::Right(inlay_hint_capabilities) => match inlay_hint_capabilities { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index 9e30796bbc..c128de7910 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2317,9 +2317,10 @@ impl BackgroundScannerState { for changed_path in changed_paths { let Some(dot_git_dir) = changed_path .ancestors() - .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) else { - continue; - }; + .find(|ancestor| ancestor.file_name() == Some(&*DOT_GIT)) + else { + continue; + }; // Avoid processing the same repository multiple times, if multiple paths // within it have changed. @@ -2348,7 +2349,10 @@ impl BackgroundScannerState { let Some(work_dir) = self .snapshot .entry_for_id(entry_id) - .map(|entry| RepositoryWorkDirectory(entry.path.clone())) else { continue }; + .map(|entry| RepositoryWorkDirectory(entry.path.clone())) + else { + continue; + }; log::info!("reload git repository {:?}", dot_git_dir); let repository = repository.repo_ptr.lock(); diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 3055399c13..1804c2b1fc 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -40,7 +40,9 @@ impl View for QuickActionBar { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let Some(editor) = self.active_editor() else { return Empty::new().into_any(); }; + let Some(editor) = self.active_editor() else { + return Empty::new().into_any(); + }; let inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut bar = Flex::row().with_child(render_quick_action_bar_button( diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index eba4058729..d7633a45e4 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -885,7 +885,9 @@ impl ProjectSearchView { if !dir_entry.is_dir() { return; } - let Some(filter_str) = dir_entry.path.to_str() else { return; }; + let Some(filter_str) = dir_entry.path.to_str() else { + return; + }; let model = cx.add_model(|cx| ProjectSearch::new(workspace.project().clone(), cx)); let search = cx.add_view(|cx| ProjectSearchView::new(model, cx)); diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index 70495b59d3..736f2c98a8 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -57,7 +57,9 @@ pub fn init( cx.subscribe_global::({ move |event, cx| { - let Some(semantic_index) = SemanticIndex::global(cx) else { return; }; + let Some(semantic_index) = SemanticIndex::global(cx) else { + return; + }; let workspace = &event.0; if let Some(workspace) = workspace.upgrade(cx) { let project = workspace.read(cx).project().clone(); diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 93cb2ab3d7..28cc2db784 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,20 +63,23 @@ impl KeymapFile { // string. But `RawValue` currently does not work inside of an untagged enum. match action { Value::Array(items) => { - let Ok([name, data]): Result<[serde_json::Value; 2], _> = items.try_into() else { + let Ok([name, data]): Result<[serde_json::Value; 2], _> = + items.try_into() + else { return Some(Err(anyhow!("Expected array of length 2"))); }; let serde_json::Value::String(name) = name else { - return Some(Err(anyhow!("Expected first item in array to be a string."))) + return Some(Err(anyhow!( + "Expected first item in array to be a string." + ))); }; - cx.deserialize_action( - &name, - Some(data), - ) - }, + cx.deserialize_action(&name, Some(data)) + } Value::String(name) => cx.deserialize_action(&name, None), Value::Null => Ok(no_action()), - _ => return Some(Err(anyhow!("Expected two-element array, got {action:?}"))), + _ => { + return Some(Err(anyhow!("Expected two-element array, got {action:?}"))) + } } .with_context(|| { format!( diff --git a/crates/vcs_menu/src/lib.rs b/crates/vcs_menu/src/lib.rs index 5d20550517..73ed4b059e 100644 --- a/crates/vcs_menu/src/lib.rs +++ b/crates/vcs_menu/src/lib.rs @@ -107,20 +107,15 @@ impl PickerDelegate for BranchListDelegate { let delegate = view.delegate(); let project = delegate.workspace.read(cx).project().read(&cx); - let Some(worktree) = project - .visible_worktrees(cx) - .next() - else { + let Some(worktree) = project.visible_worktrees(cx).next() else { bail!("Cannot update branch list as there are no visible worktrees") }; - let mut cwd = worktree .read(cx) - .abs_path() - .to_path_buf(); + let mut cwd = worktree.read(cx).abs_path().to_path_buf(); cwd.push(".git"); - let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")}; - let mut branches = repo - .lock() - .branches()?; + let Some(repo) = project.fs().open_repo(&cwd) else { + bail!("Project does not have associated git repository.") + }; + let mut branches = repo.lock().branches()?; const RECENT_BRANCHES_COUNT: usize = 10; if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT { // Truncate list of recent branches @@ -142,8 +137,13 @@ impl PickerDelegate for BranchListDelegate { }) .collect::>()) }) - .log_err() else { return; }; - let Some(candidates) = candidates.log_err() else {return;}; + .log_err() + else { + return; + }; + let Some(candidates) = candidates.log_err() else { + return; + }; let matches = if query.is_empty() { candidates .into_iter() @@ -184,7 +184,11 @@ impl PickerDelegate for BranchListDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { let current_pick = self.selected_index(); - let Some(current_pick) = self.matches.get(current_pick).map(|pick| pick.string.clone()) else { + let Some(current_pick) = self + .matches + .get(current_pick) + .map(|pick| pick.string.clone()) + else { return; }; cx.spawn(|picker, mut cx| async move { diff --git a/crates/vim/src/normal/paste.rs b/crates/vim/src/normal/paste.rs index 3d16bb3552..3c437f9177 100644 --- a/crates/vim/src/normal/paste.rs +++ b/crates/vim/src/normal/paste.rs @@ -33,7 +33,7 @@ fn paste(_: &mut Workspace, action: &Paste, cx: &mut ViewContext) { editor.set_clip_at_line_ends(false, cx); let Some(item) = cx.read_from_clipboard() else { - return + return; }; let clipboard_text = Cow::Borrowed(item.text()); if clipboard_text.is_empty() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index d198eb628d..6d40f844e7 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -77,7 +77,10 @@ pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContex } let Some((new_head, goal)) = - motion.move_point(map, current_head, selection.goal, times) else { return }; + motion.move_point(map, current_head, selection.goal, times) + else { + return; + }; selection.set_head(new_head, goal); @@ -132,7 +135,7 @@ pub fn visual_block_motion( } let Some((new_head, _)) = move_selection(&map, head, goal) else { - return + return; }; head = new_head; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 7528fb7468..93fb484214 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -742,8 +742,8 @@ mod element { while proposed_current_pixel_change.abs() > 0. { let Some(current_ix) = successors.next() else { - break; - }; + break; + }; let next_target_size = f32::max( size(current_ix + 1, flexes.as_slice()) - proposed_current_pixel_change, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 62bb7a82a2..be8148256d 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -2314,8 +2314,12 @@ impl Workspace { item_id_to_move: usize, cx: &mut ViewContext, ) { - let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; }; - let Some(from) = from.upgrade(cx) else { return; }; + let Some(pane_to_split) = pane_to_split.upgrade(cx) else { + return; + }; + let Some(from) = from.upgrade(cx) else { + return; + }; let new_pane = self.add_pane(cx); self.move_item(from.clone(), new_pane.clone(), item_id_to_move, 0, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 41ad28ba86..331d829cbc 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -89,7 +89,9 @@ impl LspAdapter for PythonLspAdapter { // to allow our own fuzzy score to be used to break ties. // // see https://github.com/microsoft/pyright/blob/95ef4e103b9b2f129c9320427e51b73ea7cf78bd/packages/pyright-internal/src/languageService/completionProvider.ts#LL2873 - let Some(sort_text) = &mut item.sort_text else { return }; + let Some(sort_text) = &mut item.sort_text else { + return; + }; let mut parts = sort_text.split('.'); let Some(first) = parts.next() else { return }; let Some(second) = parts.next() else { return }; From f798be6e273d6744663a6988afc01a3463a4c5bd Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 10:25:21 -0700 Subject: [PATCH 086/142] Fix rust 1.72 warnings about shadowed glob re-exports --- crates/language/src/buffer.rs | 2 +- crates/language/src/language.rs | 2 +- crates/lsp/src/lsp.rs | 2 +- crates/project/src/project_tests.rs | 2 +- crates/project/src/worktree.rs | 4 ++-- crates/project/src/worktree_tests.rs | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 7569925256..d5586af5ef 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -14,7 +14,7 @@ use crate::{ CodeLabel, LanguageScope, Outline, }; use anyhow::{anyhow, Result}; -use clock::ReplicaId; +pub use clock::ReplicaId; use fs::LineEnding; use futures::FutureExt as _; use gpui::{fonts::HighlightStyle, AppContext, Entity, ModelContext, Task}; diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 82245d67ca..7a9e6b83ce 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -18,7 +18,7 @@ use futures::{ FutureExt, TryFutureExt as _, }; use gpui::{executor::Background, AppContext, AsyncAppContext, Task}; -use highlight_map::HighlightMap; +pub use highlight_map::HighlightMap; use lazy_static::lazy_static; use lsp::{CodeActionKind, LanguageServerBinary}; use parking_lot::{Mutex, RwLock}; diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index e0ae64d806..7cba039552 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -77,7 +77,7 @@ pub enum Subscription { } #[derive(Serialize, Deserialize)] -struct Request<'a, T> { +pub struct Request<'a, T> { jsonrpc: &'static str, id: usize, method: &'a str, diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index 259c10ca05..a504900c83 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -1,4 +1,4 @@ -use crate::{search::PathMatcher, worktree::WorktreeHandle, Event, *}; +use crate::{search::PathMatcher, worktree::WorktreeModelHandle, Event, *}; use fs::{FakeFs, LineEnding, RealFs}; use futures::{future, StreamExt}; use gpui::{executor::Deterministic, test::subscribe, AppContext}; diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index c128de7910..e6e0f37cc7 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -4030,7 +4030,7 @@ struct UpdateIgnoreStatusJob { scan_queue: Sender, } -pub trait WorktreeHandle { +pub trait WorktreeModelHandle { #[cfg(any(test, feature = "test-support"))] fn flush_fs_events<'a>( &self, @@ -4038,7 +4038,7 @@ pub trait WorktreeHandle { ) -> futures::future::LocalBoxFuture<'a, ()>; } -impl WorktreeHandle for ModelHandle { +impl WorktreeModelHandle for ModelHandle { // When the worktree's FS event stream sometimes delivers "redundant" events for FS changes that // occurred before the worktree was constructed. These events can cause the worktree to perform // extra directory scans, and emit extra scan-state notifications. diff --git a/crates/project/src/worktree_tests.rs b/crates/project/src/worktree_tests.rs index 6f5b363509..4253f45b0c 100644 --- a/crates/project/src/worktree_tests.rs +++ b/crates/project/src/worktree_tests.rs @@ -1,5 +1,5 @@ use crate::{ - worktree::{Event, Snapshot, WorktreeHandle}, + worktree::{Event, Snapshot, WorktreeModelHandle}, Entry, EntryKind, PathChange, Worktree, }; use anyhow::Result; From 1f3e009b3255d46a7708b2a4f2c1edd1f35a8d05 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:34:07 -0600 Subject: [PATCH 087/142] Fix zed-industries/community#1950 --- crates/vim/src/visual.rs | 36 ++++++++++++++----- .../vim/test_data/test_visual_block_mode.json | 6 ++++ 2 files changed, 33 insertions(+), 9 deletions(-) diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index 6d40f844e7..b68da870f0 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -126,10 +126,15 @@ pub fn visual_block_motion( let map = &s.display_map(); let mut head = s.newest_anchor().head().to_display_point(map); let mut tail = s.oldest_anchor().tail().to_display_point(map); - let mut goal = s.newest_anchor().goal; + + let (start, end) = match s.newest_anchor().goal { + SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), + SelectionGoal::Column(start) if preserve_goal => (start, start + 1), + _ => (tail.column(), head.column()), + }; + let goal = SelectionGoal::ColumnRange { start, end }; let was_reversed = tail.column() > head.column(); - if !was_reversed && !preserve_goal { head = movement::saturating_left(map, head); } @@ -149,13 +154,6 @@ pub fn visual_block_motion( head = movement::saturating_right(map, head) } - let (start, end) = match goal { - SelectionGoal::ColumnRange { start, end } if preserve_goal => (start, end), - SelectionGoal::Column(start) if preserve_goal => (start, start + 1), - _ => (tail.column(), head.column()), - }; - goal = SelectionGoal::ColumnRange { start, end }; - let columns = if is_reversed { head.column()..tail.column() } else if head.column() == tail.column() { @@ -791,6 +789,26 @@ mod test { " }) .await; + + //https://github.com/zed-industries/community/issues/1950 + cx.set_shared_state(indoc! { + "Theˇ quick brown + + fox jumps over + the lazy dog + " + }) + .await; + cx.simulate_shared_keystrokes(["l", "ctrl-v", "j", "j"]) + .await; + cx.assert_shared_state(indoc! { + "The «qˇ»uick brown + + fox «jˇ»umps over + the lazy dog + " + }) + .await; } #[gpui::test] diff --git a/crates/vim/test_data/test_visual_block_mode.json b/crates/vim/test_data/test_visual_block_mode.json index ac306de4ab..2239ef43a8 100644 --- a/crates/vim/test_data/test_visual_block_mode.json +++ b/crates/vim/test_data/test_visual_block_mode.json @@ -30,3 +30,9 @@ {"Key":"o"} {"Key":"escape"} {"Get":{"state":"Theˇouick\nbroo\nfoxo\njumo over the\n\nlazy dog\n","mode":"Normal"}} +{"Put":{"state":"Theˇ quick brown\n\nfox jumps over\nthe lazy dog\n"}} +{"Key":"l"} +{"Key":"ctrl-v"} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"The «qˇ»uick brown\n\nfox «jˇ»umps over\nthe lazy dog\n","mode":"VisualBlock"}} From 790aa5d476f2263f2a08a705bb82c7ce5b8d2b53 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:40 -0600 Subject: [PATCH 088/142] Add relative_line_mode Co-Authored-By: joseph@zed.dev --- assets/settings/default.json | 1 + crates/editor/src/editor_settings.rs | 2 + crates/editor/src/element.rs | 81 +++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 24412b883b..c6c3ff5991 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -98,6 +98,7 @@ // Whether to show selections in the scrollbar. "selections": true }, + "relative_line_numbers": false, // Inlay hint related settings "inlay_hints": { // Global switch to toggle hints on and off, switched off by default. diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index f4499b5651..b06f23429a 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -9,6 +9,7 @@ pub struct EditorSettings { pub show_completions_on_input: bool, pub use_on_type_format: bool, pub scrollbar: Scrollbar, + pub relative_line_numbers: bool, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] @@ -34,6 +35,7 @@ pub struct EditorSettingsContent { pub show_completions_on_input: Option, pub use_on_type_format: Option, pub scrollbar: Option, + pub relative_line_numbers: Option, } #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c..d088fc2e3a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1439,10 +1439,47 @@ impl EditorElement { .collect() } + fn calculate_relative_line_numbers( + &self, + rows: &Range, + buffer_rows: &Vec>, + relative_to: Option, + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + if let Some(relative_to) = relative_to { + let head_idx = (relative_to - rows.start) as usize; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() { + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx; + while i > 0 && buffer_rows[i].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i].is_some() { + relative_rows.insert(i, delta); + delta += 1; + } + } + } + + relative_rows + } + fn layout_line_numbers( &self, rows: Range, active_rows: &BTreeMap, + newest_selection_head: Option, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1455,21 +1492,33 @@ impl EditorElement { let mut line_number_layouts = Vec::with_capacity(rows.len()); let mut fold_statuses = Vec::with_capacity(rows.len()); let mut line_number = String::new(); - for (ix, row) in snapshot + let is_relative = settings::get::(cx).relative_line_numbers; + let relative_to = if is_relative { + newest_selection_head.map(|head| head.row()) + } else { + None + }; + + let buffer_rows = snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .enumerate() - { + .collect::>(); + + let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); + + for (ix, row) in buffer_rows.iter().enumerate() { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = row { + if let Some(buffer_row) = *row { if include_line_numbers { line_number.clear(); - write!(&mut line_number, "{}", buffer_row + 1).unwrap(); + let default_number = buffer_row + 1; + let number = relative_rows.get(&ix).unwrap_or(&default_number); + write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, style.text.font_size, @@ -2296,6 +2345,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, + newest_selection_head, is_singleton, &snapshot, cx, @@ -3054,7 +3104,6 @@ mod tests { #[gpui::test] fn test_layout_line_numbers(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let editor = cx .add_window(|cx| { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); @@ -3066,10 +3115,28 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), false, &snapshot, cx) + .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) .0 }); assert_eq!(layouts.len(), 6); + + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + let rows = 0..6; + let buffer_rows = snapshot + .buffer_rows(rows.start) + .take((rows.end - rows.start) as usize) + .collect::>(); + + element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + }); + assert_eq!(relative_rows[&0], 3); + assert_eq!(relative_rows[&1], 2); + assert_eq!(relative_rows[&2], 1); + // current line has no relative number + assert_eq!(relative_rows[&4], 1); + assert_eq!(relative_rows[&5], 2); } #[gpui::test] From 8d5dc266a3b83f1c7b776049b96820f433dbcffa Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 23:39:13 -0600 Subject: [PATCH 089/142] Fix relative line numbers when newest cursor offscreen --- crates/editor/src/element.rs | 112 ++++++++++++++++++++++------------- 1 file changed, 71 insertions(+), 41 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index d088fc2e3a..04f84fb616 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1441,34 +1441,48 @@ impl EditorElement { fn calculate_relative_line_numbers( &self, + snapshot: &EditorSnapshot, rows: &Range, - buffer_rows: &Vec>, relative_to: Option, - ) -> HashMap { - let mut relative_rows: HashMap = Default::default(); - if let Some(relative_to) = relative_to { - let head_idx = (relative_to - rows.start) as usize; - let mut delta = 1; - let mut i = head_idx + 1; - while i < buffer_rows.len() { - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; - } - i += 1; - } - delta = 1; - i = head_idx; - while i > 0 && buffer_rows[i].is_none() { - i -= 1; - } + ) -> HashMap { + let mut relative_rows: HashMap = Default::default(); + let Some(relative_to) = relative_to else { + return relative_rows; + }; - while i > 0 { - i -= 1; - if buffer_rows[i].is_some() { - relative_rows.insert(i, delta); - delta += 1; + let start = rows.start.min(relative_to); + let end = rows.end.max(relative_to); + + let buffer_rows = snapshot + .buffer_rows(start) + .take(1 + (end - start) as usize) + .collect::>(); + + let head_idx = relative_to - start; + let mut delta = 1; + let mut i = head_idx + 1; + while i < buffer_rows.len() as u32 { + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); } + delta += 1; + } + i += 1; + } + delta = 1; + i = head_idx.min(buffer_rows.len() as u32 - 1); + while i > 0 && buffer_rows[i as usize].is_none() { + i -= 1; + } + + while i > 0 { + i -= 1; + if buffer_rows[i as usize].is_some() { + if rows.contains(&(i + start)) { + relative_rows.insert(i + start, delta); + } + delta += 1; } } @@ -1499,25 +1513,26 @@ impl EditorElement { None }; - let buffer_rows = snapshot + let relative_rows = self.calculate_relative_line_numbers(&snapshot, &rows, relative_to); + + for (ix, row) in snapshot .buffer_rows(rows.start) .take((rows.end - rows.start) as usize) - .collect::>(); - - let relative_rows = self.calculate_relative_line_numbers(&rows, &buffer_rows, relative_to); - - for (ix, row) in buffer_rows.iter().enumerate() { + .enumerate() + { let display_row = rows.start + ix as u32; let (active, color) = if active_rows.contains_key(&display_row) { (true, style.line_number_active) } else { (false, style.line_number) }; - if let Some(buffer_row) = *row { + if let Some(buffer_row) = row { if include_line_numbers { line_number.clear(); let default_number = buffer_row + 1; - let number = relative_rows.get(&ix).unwrap_or(&default_number); + let number = relative_rows + .get(&(ix as u32 + rows.start)) + .unwrap_or(&default_number); write!(&mut line_number, "{}", number).unwrap(); line_number_layouts.push(Some(cx.text_layout_cache().layout_str( &line_number, @@ -2345,7 +2360,7 @@ impl Element for EditorElement { let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head, + newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), is_singleton, &snapshot, cx, @@ -3122,14 +3137,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - - let rows = 0..6; - let buffer_rows = snapshot - .buffer_rows(rows.start) - .take((rows.end - rows.start) as usize) - .collect::>(); - - element.calculate_relative_line_numbers(&rows, &buffer_rows, Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3137,6 +3145,28 @@ mod tests { // current line has no relative number assert_eq!(relative_rows[&4], 1); assert_eq!(relative_rows[&5], 2); + + // works if cursor is before screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&3], 2); + assert_eq!(relative_rows[&4], 3); + assert_eq!(relative_rows[&5], 4); + + // works if cursor is after screen + let relative_rows = editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + }); + assert_eq!(relative_rows.len(), 3); + assert_eq!(relative_rows[&0], 5); + assert_eq!(relative_rows[&1], 4); + assert_eq!(relative_rows[&2], 3); } #[gpui::test] From f18cdcba546f0965aa2708324064e4639c46eac5 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 10:56:29 -0600 Subject: [PATCH 090/142] Fix relative line numbers in vim visual mode In visual mode when your selection ends with a newline we show the cursor at the end of the previous line (not the start of the current line). We had only been accounting for this if the cursor was on-screen. --- crates/editor/src/element.rs | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 04f84fb616..2a623b9b6b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1493,7 +1493,7 @@ impl EditorElement { &self, rows: Range, active_rows: &BTreeMap, - newest_selection_head: Option, + newest_selection_head: DisplayPoint, is_singleton: bool, snapshot: &EditorSnapshot, cx: &ViewContext, @@ -1508,7 +1508,7 @@ impl EditorElement { let mut line_number = String::new(); let is_relative = settings::get::(cx).relative_line_numbers; let relative_to = if is_relative { - newest_selection_head.map(|head| head.row()) + Some(newest_selection_head.row()) } else { None }; @@ -2357,10 +2357,22 @@ impl Element for EditorElement { }) .collect(); + let head_for_relative = newest_selection_head.unwrap_or_else(|| { + let newest = editor.selections.newest::(cx); + SelectionLayout::new( + newest, + editor.selections.line_mode, + editor.cursor_shape, + &snapshot.display_snapshot, + true, + ) + .head + }); + let (line_number_layouts, fold_statuses) = self.layout_line_numbers( start_row..end_row, &active_rows, - newest_selection_head.or_else(|| Some(editor.selections.newest_display(cx).head())), + head_for_relative, is_singleton, &snapshot, cx, @@ -3130,14 +3142,21 @@ mod tests { let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); element - .layout_line_numbers(0..6, &Default::default(), None, false, &snapshot, cx) + .layout_line_numbers( + 0..6, + &Default::default(), + DisplayPoint::new(0, 0), + false, + &snapshot, + cx, + ) .0 }); assert_eq!(layouts.len(), 6); let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) + element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3150,7 +3169,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) + element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3161,7 +3180,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) + element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From bde67b2b9c6fff9d1977e43efd251bff023a53b0 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 25 Aug 2023 11:08:39 -0600 Subject: [PATCH 091/142] Fix merge-conflict --- crates/editor/src/element.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2a623b9b6b..6f93b07f65 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2365,6 +2365,7 @@ impl Element for EditorElement { editor.cursor_shape, &snapshot.display_snapshot, true, + true, ) .head }); @@ -3156,7 +3157,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..6), 3) + element.calculate_relative_line_numbers(&snapshot, &(0..6), Some(3)) }); assert_eq!(relative_rows[&0], 3); assert_eq!(relative_rows[&1], 2); @@ -3169,7 +3170,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(3..6), 1) + element.calculate_relative_line_numbers(&snapshot, &(3..6), Some(1)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&3], 2); @@ -3180,7 +3181,7 @@ mod tests { let relative_rows = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); - element.calculate_relative_line_numbers(&snapshot, &(0..3), 6) + element.calculate_relative_line_numbers(&snapshot, &(0..3), Some(6)) }); assert_eq!(relative_rows.len(), 3); assert_eq!(relative_rows[&0], 5); From 507a5db09c53014e8e45c00fe39c71e992f978ab Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 25 Aug 2023 15:06:31 -0400 Subject: [PATCH 092/142] WIP Co-Authored-By: Mikayla Maki --- assets/settings/default.json | 27 ++- crates/project/src/terminals.rs | 59 +++---- crates/terminal/src/terminal.rs | 122 +------------- crates/terminal/src/terminal_settings.rs | 163 +++++++++++++++++++ crates/terminal_view/src/terminal_element.rs | 3 +- crates/terminal_view/src/terminal_panel.rs | 2 +- crates/terminal_view/src/terminal_view.rs | 5 +- 7 files changed, 213 insertions(+), 168 deletions(-) create mode 100644 crates/terminal/src/terminal_settings.rs diff --git a/assets/settings/default.json b/assets/settings/default.json index 27be6ae5d2..b1d36c938f 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -333,6 +333,24 @@ // "custom": 2 // }, "line_height": "comfortable", + // Activate the python virtual environment, if one is found, in the + // terminal's working directory (as resolved by the working_directory + // setting). Set this to "off" to disable this behavior. + "detect_venv": { + "on": { + // Default directories to search for virtual environments, relative + // to the current working directory. We recommend overriding this + // in your project's settings, rather than globally. + "directories": [ + ".env", + "env", + ".venv", + "venv" + ], + // Can also be 'csh' and 'fish' + "activate_script": "default" + } + } // Set the terminal's font size. If this option is not included, // the terminal will default to matching the buffer's font size. // "font_size": "15", @@ -340,15 +358,6 @@ // the terminal will default to matching the buffer's font family. // "font_family": "Zed Mono", // --- - // Whether or not to automatically search for, and activate, Python virtual - // environments. - // Current limitations: - // - Only ".env", "env", ".venv", and "venv" are searched for at the - // root of the project - // - Only works with Posix-complaint shells - // - Only activates the first virtual environment it finds, regardless - // of the nunber of projects in the workspace. - "activate_python_virtual_environment": false }, // Difference settings for semantic_index "semantic_index": { diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 2fb66a6c4c..68a0431316 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,7 +1,10 @@ use crate::Project; use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; -use std::path::PathBuf; -use terminal::{Shell, Terminal, TerminalBuilder, TerminalSettings}; +use std::path::{Path, PathBuf}; +use terminal::{ + terminal_settings::{self, TerminalSettings, VenvSettingsContent}, + Terminal, TerminalBuilder, +}; #[cfg(target_os = "macos")] use std::os::unix::ffi::OsStrExt; @@ -23,8 +26,7 @@ impl Project { )); } else { let settings = settings::get::(cx); - let activate_python_virtual_environment = - settings.activate_python_virtual_environment.clone(); + let python_settings = settings.detect_venv.clone(); let shell = settings.shell.clone(); let terminal = TerminalBuilder::new( @@ -53,15 +55,15 @@ impl Project { }) .detach(); - if activate_python_virtual_environment { - let activate_script_path = self.find_activate_script_path(&shell, cx); + if let Some(python_settings) = &python_settings.as_option() { + let activate_script_path = + self.find_activate_script_path(&python_settings, working_directory); self.activate_python_virtual_environment( activate_script_path, &terminal_handle, cx, ); } - terminal_handle }); @@ -71,37 +73,26 @@ impl Project { pub fn find_activate_script_path( &mut self, - shell: &Shell, - cx: &mut ModelContext, + settings: &VenvSettingsContent, + working_directory: Option, ) -> Option { - let program = match shell { - terminal::Shell::System => "Figure this out", - terminal::Shell::Program(program) => program, - terminal::Shell::WithArguments { program, args: _ } => program, + // When we are unable to resolve the working directory, the terminal builder + // defaults to '/'. We should probably encode this directly somewhere, but for + // now, let's just hard code it here. + let working_directory = working_directory.unwrap_or_else(|| Path::new("/").to_path_buf()); + let activate_script_name = match settings.activate_script { + terminal_settings::ActivateScript::Default => "activate", + terminal_settings::ActivateScript::Csh => "activate.csh", + terminal_settings::ActivateScript::Fish => "activate.fish", }; - // This is so hacky - find a better way to do this - let script_name = if program.contains("fish") { - "activate.fish" - } else { - "activate" - }; + for virtual_environment_name in settings.directories { + let mut path = working_directory.join(virtual_environment_name); + path.push("bin/"); + path.push(activate_script_name); - let worktree_paths = self - .worktrees(cx) - .map(|worktree| worktree.read(cx).abs_path()); - - const VIRTUAL_ENVIRONMENT_NAMES: [&str; 4] = [".env", "env", ".venv", "venv"]; - - for worktree_path in worktree_paths { - for virtual_environment_name in VIRTUAL_ENVIRONMENT_NAMES { - let mut path = worktree_path.join(virtual_environment_name); - path.push("bin/"); - path.push(script_name); - - if path.exists() { - return Some(path); - } + if path.exists() { + return Some(path); } } diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 73ff09225f..e28e0ca5c1 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -1,5 +1,6 @@ pub mod mappings; pub use alacritty_terminal; +pub mod terminal_settings; use alacritty_terminal::{ ansi::{ClearMode, Handler}, @@ -31,8 +32,8 @@ use mappings::mouse::{ }; use procinfo::LocalProcessInfo; -use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings}; use util::truncate_and_trailoff; use std::{ @@ -48,7 +49,6 @@ use std::{ use thiserror::Error; use gpui::{ - fonts, geometry::vector::{vec2f, Vector2F}, keymap_matcher::Keystroke, platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, @@ -134,124 +134,6 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TerminalDockPosition { - Left, - Bottom, - Right, -} - -#[derive(Deserialize)] -pub struct TerminalSettings { - pub shell: Shell, - pub working_directory: WorkingDirectory, - font_size: Option, - pub font_family: Option, - pub line_height: TerminalLineHeight, - pub font_features: Option, - pub env: HashMap, - pub blinking: TerminalBlink, - pub alternate_scroll: AlternateScroll, - pub option_as_meta: bool, - pub copy_on_select: bool, - pub dock: TerminalDockPosition, - pub default_width: f32, - pub default_height: f32, - pub activate_python_virtual_environment: bool, -} - -#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] -pub struct TerminalSettingsContent { - pub shell: Option, - pub working_directory: Option, - pub font_size: Option, - pub font_family: Option, - pub line_height: Option, - pub font_features: Option, - pub env: Option>, - pub blinking: Option, - pub alternate_scroll: Option, - pub option_as_meta: Option, - pub copy_on_select: Option, - pub dock: Option, - pub default_width: Option, - pub default_height: Option, - pub activate_python_virtual_environment: Option, -} - -impl TerminalSettings { - pub fn font_size(&self, cx: &AppContext) -> Option { - self.font_size - .map(|size| theme::adjusted_font_size(size, cx)) - } -} - -impl settings::Setting for TerminalSettings { - const KEY: Option<&'static str> = Some("terminal"); - - type FileContent = TerminalSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &AppContext, - ) -> Result { - Self::load_via_json_merge(default_value, user_values) - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] -#[serde(rename_all = "snake_case")] -pub enum TerminalLineHeight { - #[default] - Comfortable, - Standard, - Custom(f32), -} - -impl TerminalLineHeight { - pub fn value(&self) -> f32 { - match self { - TerminalLineHeight::Comfortable => 1.618, - TerminalLineHeight::Standard => 1.3, - TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - } - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum TerminalBlink { - Off, - TerminalControlled, - On, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum Shell { - System, - Program(String), - WithArguments { program: String, args: Vec }, -} - -#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum AlternateScroll { - On, - Off, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum WorkingDirectory { - CurrentProjectDirectory, - FirstProjectDirectory, - AlwaysHome, - Always { directory: String }, -} - #[derive(Clone, Copy, Debug, Serialize, Deserialize)] pub struct TerminalSize { pub cell_width: f32, diff --git a/crates/terminal/src/terminal_settings.rs b/crates/terminal/src/terminal_settings.rs new file mode 100644 index 0000000000..e0649ebf65 --- /dev/null +++ b/crates/terminal/src/terminal_settings.rs @@ -0,0 +1,163 @@ +use std::{collections::HashMap, path::PathBuf}; + +use gpui::{fonts, AppContext}; +use schemars::JsonSchema; +use serde_derive::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + +#[derive(Deserialize)] +pub struct TerminalSettings { + pub shell: Shell, + pub working_directory: WorkingDirectory, + font_size: Option, + pub font_family: Option, + pub line_height: TerminalLineHeight, + pub font_features: Option, + pub env: HashMap, + pub blinking: TerminalBlink, + pub alternate_scroll: AlternateScroll, + pub option_as_meta: bool, + pub copy_on_select: bool, + pub dock: TerminalDockPosition, + pub default_width: f32, + pub default_height: f32, + pub detect_venv: VenvSettings, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum VenvSettings { + #[default] + Off, + On { + activate_script: Option, + directories: Option>, + }, +} + +pub struct VenvSettingsContent<'a> { + pub activate_script: ActivateScript, + pub directories: &'a [PathBuf], +} + +impl VenvSettings { + pub fn as_option(&self) -> Option { + match self { + VenvSettings::Off => None, + VenvSettings::On { + activate_script, + directories, + } => Some(VenvSettingsContent { + activate_script: activate_script.unwrap_or(ActivateScript::Default), + directories: directories.as_deref().unwrap_or(&[]), + }), + } + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ActivateScript { + #[default] + Default, + Csh, + Fish, +} + +#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] +pub struct TerminalSettingsContent { + pub shell: Option, + pub working_directory: Option, + pub font_size: Option, + pub font_family: Option, + pub line_height: Option, + pub font_features: Option, + pub env: Option>, + pub blinking: Option, + pub alternate_scroll: Option, + pub option_as_meta: Option, + pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub detect_venv: Option, +} + +impl TerminalSettings { + pub fn font_size(&self, cx: &AppContext) -> Option { + self.font_size + .map(|size| theme::adjusted_font_size(size, cx)) + } +} + +impl settings::Setting for TerminalSettings { + const KEY: Option<&'static str> = Some("terminal"); + + type FileContent = TerminalSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, JsonSchema, Default)] +#[serde(rename_all = "snake_case")] +pub enum TerminalLineHeight { + #[default] + Comfortable, + Standard, + Custom(f32), +} + +impl TerminalLineHeight { + pub fn value(&self) -> f32 { + match self { + TerminalLineHeight::Comfortable => 1.618, + TerminalLineHeight::Standard => 1.3, + TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum TerminalBlink { + Off, + TerminalControlled, + On, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum Shell { + System, + Program(String), + WithArguments { program: String, args: Vec }, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AlternateScroll { + On, + Off, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum WorkingDirectory { + CurrentProjectDirectory, + FirstProjectDirectory, + AlwaysHome, + Always { directory: String }, +} diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index 1d12b83c5c..b3d87f531a 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -25,7 +25,8 @@ use terminal::{ term::{cell::Flags, TermMode}, }, mappings::colors::convert_color, - IndexedCell, Terminal, TerminalContent, TerminalSettings, TerminalSize, + terminal_settings::TerminalSettings, + IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{TerminalStyle, ThemeSettings}; use util::ResultExt; diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 472e748359..9fb3939e1f 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -9,7 +9,7 @@ use gpui::{ use project::Fs; use serde::{Deserialize, Serialize}; use settings::SettingsStore; -use terminal::{TerminalDockPosition, TerminalSettings}; +use terminal::terminal_settings::{TerminalDockPosition, TerminalSettings}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 92465d6b32..104d181a7b 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -33,7 +33,8 @@ use terminal::{ index::Point, term::{search::RegexSearch, TermMode}, }, - Event, MaybeNavigationTarget, Terminal, TerminalBlink, WorkingDirectory, + terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, + Event, MaybeNavigationTarget, Terminal, }; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -44,8 +45,6 @@ use workspace::{ NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; -pub use terminal::TerminalSettings; - const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); ///Event to transmit the scroll from the element to the view From 0280d5d01092d03c854267c42c0c64d45ff1a586 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 12:53:20 -0600 Subject: [PATCH 093/142] vim change for wrapped lines --- crates/vim/src/motion.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 8cd29e5e9f..a85c6fc0a3 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -1,4 +1,4 @@ -use std::sync::Arc; +use std::{cmp, sync::Arc}; use editor::{ char_kind, @@ -404,9 +404,13 @@ fn down( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = cmp::min(map.max_buffer_row(), start_row + times as u32); + + while point.to_point(map).row < target { (point, goal) = movement::down(map, point, goal, true); } + (point, goal) } @@ -416,7 +420,10 @@ fn up( mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - for _ in 0..times { + let start_row = point.to_point(map).row; + let target = start_row.saturating_sub(times as u32); + + while point.to_point(map).row > target { (point, goal) = movement::up(map, point, goal, true); } (point, goal) From 20aa2a4c54df6268c2c18b36af4c36e599583138 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:11:51 -0600 Subject: [PATCH 094/142] vim: Fix relative line motion Before this change up and down were in display co-ordinates, after this change they are in fold coordinates (which matches the vim behaviour). To make this work without causing usabliity problems, a bunch of extra keyboard shortcuts now work: - vim: `z {o,c}` to open,close a fold - vim: `z f` to fold current visual selection - vim: `g {j,k,up,down}` to move up/down a display line - vim: `g {0,^,$,home,end}` to get to start/end of a display line Fixes: zed-industries/community#1562 --- assets/keymaps/vim.json | 57 ++++ crates/editor/src/display_map.rs | 17 +- crates/editor/src/editor.rs | 14 +- crates/vim/src/motion.rs | 265 ++++++++++++++---- crates/vim/src/normal.rs | 27 +- crates/vim/src/normal/change.rs | 6 +- crates/vim/src/normal/substitute.rs | 5 +- crates/vim/src/test.rs | 142 ++++++++++ .../src/test/neovim_backed_test_context.rs | 28 +- crates/vim/src/test/neovim_connection.rs | 24 ++ crates/vim/src/visual.rs | 11 +- crates/vim/test_data/test_folds.json | 23 ++ crates/vim/test_data/test_wrapped_lines.json | 26 ++ 13 files changed, 579 insertions(+), 66 deletions(-) create mode 100644 crates/vim/test_data/test_folds.json create mode 100644 crates/vim/test_data/test_wrapped_lines.json diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index c0de3420f2..c7e6199f44 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -137,10 +137,67 @@ "partialWord": true } ], + "g j": [ + "vim::Down", + { + "displayLines": true + } + ], + "g down": [ + "vim::Down", + { + "displayLines": true + } + ], + "g k": [ + "vim::Up", + { + "displayLines": true + } + ], + "g up": [ + "vim::Up", + { + "displayLines": true + } + ], + "g $": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g end": [ + "vim::EndOfLine", + { + "displayLines": true + } + ], + "g 0": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g home": [ + "vim::StartOfLine", + { + "displayLines": true + } + ], + "g ^": [ + "vim::FirstNonWhitespace", + { + "displayLines": true + } + ], // z commands "z t": "editor::ScrollCursorTop", "z z": "editor::ScrollCursorCenter", "z b": "editor::ScrollCursorBottom", + "z c": "editor::Fold", + "z o": "editor::UnfoldLines", + "z f": "editor::FoldSelectedRanges", // Count support "1": [ "vim::Number", diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcad..fae4109b94 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -30,6 +30,7 @@ pub use block_map::{ BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, TransformBlock, }; +pub use self::fold_map::FoldPoint; pub use self::inlay_map::{Inlay, InlayOffset, InlayPoint}; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -310,7 +311,7 @@ impl DisplayMap { pub struct DisplaySnapshot { pub buffer_snapshot: MultiBufferSnapshot, - fold_snapshot: fold_map::FoldSnapshot, + pub fold_snapshot: fold_map::FoldSnapshot, inlay_snapshot: inlay_map::InlaySnapshot, tab_snapshot: tab_map::TabSnapshot, wrap_snapshot: wrap_map::WrapSnapshot, @@ -438,6 +439,20 @@ impl DisplaySnapshot { fold_point.to_inlay_point(&self.fold_snapshot) } + pub fn display_point_to_fold_point(&self, point: DisplayPoint, bias: Bias) -> FoldPoint { + let block_point = point.0; + let wrap_point = self.block_snapshot.to_wrap_point(block_point); + let tab_point = self.wrap_snapshot.to_tab_point(wrap_point); + self.tab_snapshot.to_fold_point(tab_point, bias).0 + } + + pub fn fold_point_to_display_point(&self, fold_point: FoldPoint) -> DisplayPoint { + let tab_point = self.tab_snapshot.to_tab_point(fold_point); + let wrap_point = self.wrap_snapshot.tab_point_to_wrap_point(tab_point); + let block_point = self.block_snapshot.to_block_point(wrap_point); + DisplayPoint(block_point) + } + pub fn max_point(&self) -> DisplayPoint { DisplayPoint(self.block_snapshot.max_point()) } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027d..ac9a972f96 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7198,7 +7198,7 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); - let selections = self.selections.all::(cx); + let selections = self.selections.all_adjusted(cx); for selection in selections { let range = selection.range().sorted(); let buffer_start_row = range.start.row; @@ -7274,7 +7274,17 @@ impl Editor { pub fn fold_selected_ranges(&mut self, _: &FoldSelectedRanges, cx: &mut ViewContext) { let selections = self.selections.all::(cx); - let ranges = selections.into_iter().map(|s| s.start..s.end); + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let line_mode = self.selections.line_mode; + let ranges = selections.into_iter().map(|s| { + if line_mode { + let start = Point::new(s.start.row, 0); + let end = Point::new(s.end.row, display_map.buffer_snapshot.line_len(s.end.row)); + start..end + } else { + s.start..s.end + } + }); self.fold_ranges(ranges, true, cx); } diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index a85c6fc0a3..2edbb8ff1c 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -2,7 +2,7 @@ use std::{cmp, sync::Arc}; use editor::{ char_kind, - display_map::{DisplaySnapshot, ToDisplayPoint}, + display_map::{DisplaySnapshot, FoldPoint, ToDisplayPoint}, movement, Bias, CharKind, DisplayPoint, ToOffset, }; use gpui::{actions, impl_actions, AppContext, WindowContext}; @@ -21,16 +21,16 @@ use crate::{ pub enum Motion { Left, Backspace, - Down, - Up, + Down { display_lines: bool }, + Up { display_lines: bool }, Right, NextWordStart { ignore_punctuation: bool }, NextWordEnd { ignore_punctuation: bool }, PreviousWordStart { ignore_punctuation: bool }, - FirstNonWhitespace, + FirstNonWhitespace { display_lines: bool }, CurrentLine, - StartOfLine, - EndOfLine, + StartOfLine { display_lines: bool }, + EndOfLine { display_lines: bool }, StartOfParagraph, EndOfParagraph, StartOfDocument, @@ -62,6 +62,41 @@ struct PreviousWordStart { ignore_punctuation: bool, } +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Up { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct Down { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct FirstNonWhitespace { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct EndOfLine { + #[serde(default)] + display_lines: bool, +} + +#[derive(Clone, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] +struct StartOfLine { + #[serde(default)] + display_lines: bool, +} + #[derive(Clone, Deserialize, PartialEq)] struct RepeatFind { #[serde(default)] @@ -73,12 +108,7 @@ actions!( [ Left, Backspace, - Down, - Up, Right, - FirstNonWhitespace, - StartOfLine, - EndOfLine, CurrentLine, StartOfParagraph, EndOfParagraph, @@ -90,20 +120,63 @@ actions!( ); impl_actions!( vim, - [NextWordStart, NextWordEnd, PreviousWordStart, RepeatFind] + [ + NextWordStart, + NextWordEnd, + PreviousWordStart, + RepeatFind, + Up, + Down, + FirstNonWhitespace, + EndOfLine, + StartOfLine, + ] ); pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &Left, cx: _| motion(Motion::Left, cx)); cx.add_action(|_: &mut Workspace, _: &Backspace, cx: _| motion(Motion::Backspace, cx)); - cx.add_action(|_: &mut Workspace, _: &Down, cx: _| motion(Motion::Down, cx)); - cx.add_action(|_: &mut Workspace, _: &Up, cx: _| motion(Motion::Up, cx)); + cx.add_action(|_: &mut Workspace, action: &Down, cx: _| { + motion( + Motion::Down { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &Up, cx: _| { + motion( + Motion::Up { + display_lines: action.display_lines, + }, + cx, + ) + }); cx.add_action(|_: &mut Workspace, _: &Right, cx: _| motion(Motion::Right, cx)); - cx.add_action(|_: &mut Workspace, _: &FirstNonWhitespace, cx: _| { - motion(Motion::FirstNonWhitespace, cx) + cx.add_action(|_: &mut Workspace, action: &FirstNonWhitespace, cx: _| { + motion( + Motion::FirstNonWhitespace { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &StartOfLine, cx: _| { + motion( + Motion::StartOfLine { + display_lines: action.display_lines, + }, + cx, + ) + }); + cx.add_action(|_: &mut Workspace, action: &EndOfLine, cx: _| { + motion( + Motion::EndOfLine { + display_lines: action.display_lines, + }, + cx, + ) }); - cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); - cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { motion(Motion::StartOfParagraph, cx) @@ -192,19 +265,25 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart - | StartOfParagraph | EndOfParagraph => true, - EndOfLine + Down { .. } + | Up { .. } + | StartOfDocument + | EndOfDocument + | CurrentLine + | NextLineStart + | StartOfParagraph + | EndOfParagraph => true, + EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -213,21 +292,21 @@ impl Motion { use Motion::*; match self { StartOfDocument | EndOfDocument | CurrentLine => true, - Down - | Up - | EndOfLine + Down { .. } + | Up { .. } + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } | Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } | NextLineStart => false, } @@ -236,12 +315,12 @@ impl Motion { pub fn inclusive(&self) -> bool { use Motion::*; match self { - Down - | Up + Down { .. } + | Up { .. } | StartOfDocument | EndOfDocument | CurrentLine - | EndOfLine + | EndOfLine { .. } | NextWordEnd { .. } | Matching | FindForward { .. } @@ -249,12 +328,12 @@ impl Motion { Left | Backspace | Right - | StartOfLine + | StartOfLine { .. } | StartOfParagraph | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } - | FirstNonWhitespace + | FirstNonWhitespace { .. } | FindBackward { .. } => false, } } @@ -272,8 +351,18 @@ impl Motion { let (new_point, goal) = match self { Left => (left(map, point, times), SelectionGoal::None), Backspace => (backspace(map, point, times), SelectionGoal::None), - Down => down(map, point, goal, times), - Up => up(map, point, goal, times), + Down { + display_lines: false, + } => down(map, point, goal, times), + Down { + display_lines: true, + } => down_display(map, point, goal, times), + Up { + display_lines: false, + } => up(map, point, goal, times), + Up { + display_lines: true, + } => up_display(map, point, goal, times), Right => (right(map, point, times), SelectionGoal::None), NextWordStart { ignore_punctuation } => ( next_word_start(map, point, *ignore_punctuation, times), @@ -287,9 +376,17 @@ impl Motion { previous_word_start(map, point, *ignore_punctuation, times), SelectionGoal::None, ), - FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), - StartOfLine => (start_of_line(map, point), SelectionGoal::None), - EndOfLine => (end_of_line(map, point), SelectionGoal::None), + FirstNonWhitespace { display_lines } => ( + first_non_whitespace(map, *display_lines, point), + SelectionGoal::None, + ), + StartOfLine { display_lines } => ( + start_of_line(map, *display_lines, point), + SelectionGoal::None, + ), + EndOfLine { display_lines } => { + (end_of_line(map, *display_lines, point), SelectionGoal::None) + } StartOfParagraph => ( movement::start_of_paragraph(map, point, times), SelectionGoal::None, @@ -298,7 +395,7 @@ impl Motion { map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), - CurrentLine => (end_of_line(map, point), SelectionGoal::None), + CurrentLine => (end_of_line(map, false, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( end_of_document(map, point, maybe_times), @@ -399,15 +496,39 @@ fn backspace(map: &DisplaySnapshot, mut point: DisplayPoint, times: usize) -> Di } fn down( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = cmp::min( + start.row() + times as u32, + map.buffer_snapshot.max_point().row, + ); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn down_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = cmp::min(map.max_buffer_row(), start_row + times as u32); - - while point.to_point(map).row < target { + for _ in 0..times { (point, goal) = movement::down(map, point, goal, true); } @@ -415,17 +536,39 @@ fn down( } fn up( + map: &DisplaySnapshot, + point: DisplayPoint, + mut goal: SelectionGoal, + times: usize, +) -> (DisplayPoint, SelectionGoal) { + let start = map.display_point_to_fold_point(point, Bias::Left); + + let goal_column = match goal { + SelectionGoal::Column(column) => column, + SelectionGoal::ColumnRange { end, .. } => end, + _ => { + goal = SelectionGoal::Column(start.column()); + start.column() + } + }; + + let new_row = start.row().saturating_sub(times as u32); + let new_col = cmp::min(goal_column, map.fold_snapshot.line_len(new_row)); + let point = map.fold_point_to_display_point(FoldPoint::new(new_row, new_col)); + + (map.clip_point(point, Bias::Left), goal) +} + +fn up_display( map: &DisplaySnapshot, mut point: DisplayPoint, mut goal: SelectionGoal, times: usize, ) -> (DisplayPoint, SelectionGoal) { - let start_row = point.to_point(map).row; - let target = start_row.saturating_sub(times as u32); - - while point.to_point(map).row > target { + for _ in 0..times { (point, goal) = movement::up(map, point, goal, true); } + (point, goal) } @@ -516,8 +659,12 @@ fn previous_word_start( point } -fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoint { - let mut last_point = DisplayPoint::new(from.row(), 0); +fn first_non_whitespace( + map: &DisplaySnapshot, + display_lines: bool, + from: DisplayPoint, +) -> DisplayPoint { + let mut last_point = start_of_line(map, display_lines, from); let language = map.buffer_snapshot.language_at(from.to_point(map)); for (ch, point) in map.chars_at(last_point) { if ch == '\n' { @@ -534,12 +681,23 @@ fn first_non_whitespace(map: &DisplaySnapshot, from: DisplayPoint) -> DisplayPoi map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.prev_line_boundary(point.to_point(map)).1 +fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) + } else { + map.prev_line_boundary(point.to_point(map)).1 + } } -fn end_of_line(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPoint { - map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) +fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { + if display_lines { + map.clip_point( + DisplayPoint::new(point.row(), map.line_len(point.row())), + Bias::Left, + ) + } else { + map.clip_point(map.next_line_boundary(point.to_point(map)).1, Bias::Left) + } } fn start_of_document(map: &DisplaySnapshot, point: DisplayPoint, line: usize) -> DisplayPoint { @@ -664,6 +822,7 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> let new_row = (point.row() + times as u32).min(map.max_buffer_row()); first_non_whitespace( map, + false, map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), ) } diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 3a2d15a878..2b03632c42 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -78,13 +78,27 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &ChangeToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - change_motion(vim, Motion::EndOfLine, times, cx); + change_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); cx.add_action(|_: &mut Workspace, _: &DeleteToEndOfLine, cx| { Vim::update(cx, |vim, cx| { let times = vim.pop_number_operator(cx); - delete_motion(vim, Motion::EndOfLine, times, cx); + delete_motion( + vim, + Motion::EndOfLine { + display_lines: false, + }, + times, + cx, + ); }) }); scroll::init(cx); @@ -165,7 +179,10 @@ fn insert_first_non_whitespace( vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::FirstNonWhitespace.move_point(map, cursor, goal, None) + Motion::FirstNonWhitespace { + display_lines: false, + } + .move_point(map, cursor, goal, None) }); }); }); @@ -178,7 +195,7 @@ fn insert_end_of_line(_: &mut Workspace, _: &InsertEndOfLine, cx: &mut ViewConte vim.update_active_editor(cx, |editor, cx| { editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); }); @@ -238,7 +255,7 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex }); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.maybe_move_cursors_with(|map, cursor, goal| { - Motion::EndOfLine.move_point(map, cursor, goal, None) + Motion::CurrentLine.move_point(map, cursor, goal, None) }); }); editor.edit_with_autoindent(edits, cx); diff --git a/crates/vim/src/normal/change.rs b/crates/vim/src/normal/change.rs index 50bc049a3a..5591de89c6 100644 --- a/crates/vim/src/normal/change.rs +++ b/crates/vim/src/normal/change.rs @@ -10,7 +10,11 @@ pub fn change_motion(vim: &mut Vim, motion: Motion, times: Option, cx: &m // Some motions ignore failure when switching to normal mode let mut motion_succeeded = matches!( motion, - Motion::Left | Motion::Right | Motion::EndOfLine | Motion::Backspace | Motion::StartOfLine + Motion::Left + | Motion::Right + | Motion::EndOfLine { .. } + | Motion::Backspace + | Motion::StartOfLine { .. } ); vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { diff --git a/crates/vim/src/normal/substitute.rs b/crates/vim/src/normal/substitute.rs index 1d53c6831c..b04596240a 100644 --- a/crates/vim/src/normal/substitute.rs +++ b/crates/vim/src/normal/substitute.rs @@ -15,7 +15,10 @@ pub fn substitute(vim: &mut Vim, count: Option, cx: &mut WindowContext) { } if line_mode { Motion::CurrentLine.expand_selection(map, selection, None, false); - if let Some((point, _)) = Motion::FirstNonWhitespace.move_point( + if let Some((point, _)) = (Motion::FirstNonWhitespace { + display_lines: false, + }) + .move_point( map, selection.start, selection.goal, diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 9cd927601f..cfde221dc5 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -285,3 +285,145 @@ async fn test_word_characters(cx: &mut gpui::TestAppContext) { Mode::Visual, ) } + +#[gpui::test] +async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_wrap(12).await; + // tests line wrap as follows: + // 1: twelve char + // twelve char + // 2: twelve char + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + cx.simulate_shared_keystrokes(["k"]).await; + cx.assert_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["g", "j"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + tˇwelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "k"]).await; + cx.assert_shared_state(indoc! { " + twelve char tˇwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "^"]).await; + cx.assert_shared_state(indoc! { " + twelve char ˇtwelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["^"]).await; + cx.assert_shared_state(indoc! { " + ˇtwelve char twelve char + twelve char + "}) + .await; + + cx.simulate_shared_keystrokes(["g", "$"]).await; + cx.assert_shared_state(indoc! { " + twelve charˇ twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["$"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve chaˇr + twelve char + "}) + .await; +} + +#[gpui::test] +async fn test_folds(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + cx.set_neovim_option("foldmethod=manual").await; + + cx.set_shared_state(indoc! { " + fn boop() { + ˇbarp() + bazp() + } + "}) + .await; + cx.simulate_shared_keystrokes(["shift-v", "j", "z", "f"]) + .await; + + // visual display is now: + // fn boop () { + // [FOLDED] + // } + + // TODO: this should not be needed but currently zf does not + // return to normal mode. + cx.simulate_shared_keystrokes(["escape"]).await; + + // skip over fold downward + cx.simulate_shared_keystrokes(["g", "g"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + cx.simulate_shared_keystrokes(["j", "j"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + barp() + bazp() + ˇ} + "}) + .await; + + // skip over fold upward + cx.simulate_shared_keystrokes(["2", "k"]).await; + cx.assert_shared_state(indoc! { " + ˇfn boop() { + barp() + bazp() + } + "}) + .await; + + // yank the fold + cx.simulate_shared_keystrokes(["down", "y", "y"]).await; + cx.assert_shared_clipboard(" barp()\n bazp()\n").await; + + // re-open + cx.simulate_shared_keystrokes(["z", "o"]).await; + cx.assert_shared_state(indoc! { " + fn boop() { + ˇ barp() + bazp() + } + "}) + .await; +} diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index f4b0e96183..bc37f2fdd6 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,14 @@ +use editor::EditorSettings; use indoc::indoc; +use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; -use language::OffsetRangeExt; +use language::{ + language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + OffsetRangeExt, +}; use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; @@ -127,6 +132,27 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn set_shared_wrap(&mut self, columns: u32) { + if columns < 12 { + panic!("nvim doesn't support columns < 12") + } + self.neovim.set_option("wrap").await; + self.neovim.set_option("columns=12").await; + + self.update(|cx| { + cx.update_global(|settings: &mut SettingsStore, cx| { + settings.update_user_settings::(cx, |settings| { + settings.defaults.soft_wrap = Some(SoftWrap::PreferredLineLength); + settings.defaults.preferred_line_length = Some(columns); + }); + }) + }) + } + + pub async fn set_neovim_option(&mut self, option: &str) { + self.neovim.set_option(option).await; + } + pub async fn assert_shared_state(&mut self, marked_text: &str) { let neovim = self.neovim_state().await; let editor = self.editor_state(); diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index 68f3374772..3e59080b13 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -41,6 +41,7 @@ pub enum NeovimData { Key(String), Get { state: String, mode: Option }, ReadRegister { name: char, value: String }, + SetOption { value: String }, } pub struct NeovimConnection { @@ -222,6 +223,29 @@ impl NeovimConnection { ); } + #[cfg(feature = "neovim")] + pub async fn set_option(&mut self, value: &str) { + self.nvim + .command_output(format!("set {}", value).as_str()) + .await + .unwrap(); + + self.data.push_back(NeovimData::SetOption { + value: value.to_string(), + }) + } + + #[cfg(not(feature = "neovim"))] + pub async fn set_option(&mut self, value: &str) { + assert_eq!( + self.data.pop_front(), + Some(NeovimData::SetOption { + value: value.to_string(), + }), + "operation does not match recorded script. re-record with --features=neovim" + ); + } + #[cfg(not(feature = "neovim"))] pub async fn read_register(&mut self, register: char) -> String { if let Some(NeovimData::Get { .. }) = self.data.front() { diff --git a/crates/vim/src/visual.rs b/crates/vim/src/visual.rs index b68da870f0..ee46a0d209 100644 --- a/crates/vim/src/visual.rs +++ b/crates/vim/src/visual.rs @@ -51,8 +51,15 @@ pub fn init(cx: &mut AppContext) { pub fn visual_motion(motion: Motion, times: Option, cx: &mut WindowContext) { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |editor, cx| { - if vim.state().mode == Mode::VisualBlock && !matches!(motion, Motion::EndOfLine) { - let is_up_or_down = matches!(motion, Motion::Up | Motion::Down); + if vim.state().mode == Mode::VisualBlock + && !matches!( + motion, + Motion::EndOfLine { + display_lines: false + } + ) + { + let is_up_or_down = matches!(motion, Motion::Up { .. } | Motion::Down { .. }); visual_block_motion(is_up_or_down, editor, cx, |map, point, goal| { motion.move_point(map, point, goal, times) }) diff --git a/crates/vim/test_data/test_folds.json b/crates/vim/test_data/test_folds.json new file mode 100644 index 0000000000..668df5ce26 --- /dev/null +++ b/crates/vim/test_data/test_folds.json @@ -0,0 +1,23 @@ +{"SetOption":{"value":"foldmethod=manual"}} +{"Put":{"state":"fn boop() {\n ˇbarp()\n bazp()\n}\n"}} +{"Key":"shift-v"} +{"Key":"j"} +{"Key":"z"} +{"Key":"f"} +{"Key":"escape"} +{"Key":"g"} +{"Key":"g"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"j"} +{"Key":"j"} +{"Get":{"state":"fn boop() {\n barp()\n bazp()\nˇ}\n","mode":"Normal"}} +{"Key":"2"} +{"Key":"k"} +{"Get":{"state":"ˇfn boop() {\n barp()\n bazp()\n}\n","mode":"Normal"}} +{"Key":"down"} +{"Key":"y"} +{"Key":"y"} +{"ReadRegister":{"name":"\"","value":" barp()\n bazp()\n"}} +{"Key":"z"} +{"Key":"o"} +{"Get":{"state":"fn boop() {\nˇ barp()\n bazp()\n}\n","mode":"Normal"}} diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json new file mode 100644 index 0000000000..f9f54c5c43 --- /dev/null +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -0,0 +1,26 @@ +{"SetOption":{"value":"wrap"}} +{"SetOption":{"value":"columns=12"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"k"} +{"Get":{"state":"tˇwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"j"} +{"Get":{"state":"twelve char twelve char\ntˇwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"k"} +{"Get":{"state":"twelve char tˇwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"^"} +{"Get":{"state":"twelve char ˇtwelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"^"} +{"Get":{"state":"ˇtwelve char twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"g"} +{"Key":"$"} +{"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} +{"Key":"$"} +{"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} From dee1a433dd7be8dbc0fc5590882ddc4017b7a281 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 24 Aug 2023 22:48:58 -0600 Subject: [PATCH 095/142] A few more fixes for wrapped line motions --- crates/vim/src/motion.rs | 22 +++--- crates/vim/src/normal.rs | 23 +++--- crates/vim/src/test.rs | 73 +++++++++++++++++++ .../src/test/neovim_backed_test_context.rs | 3 +- crates/vim/test_data/test_wrapped_lines.json | 24 ++++++ 5 files changed, 123 insertions(+), 22 deletions(-) diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index 2edbb8ff1c..0d3fb700ef 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -535,7 +535,7 @@ fn down_display( (point, goal) } -fn up( +pub(crate) fn up( map: &DisplaySnapshot, point: DisplayPoint, mut goal: SelectionGoal, @@ -681,7 +681,11 @@ fn first_non_whitespace( map.clip_point(last_point, Bias::Left) } -fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn start_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point(DisplayPoint::new(point.row(), 0), Bias::Right) } else { @@ -689,7 +693,11 @@ fn start_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint } } -fn end_of_line(map: &DisplaySnapshot, display_lines: bool, point: DisplayPoint) -> DisplayPoint { +pub(crate) fn end_of_line( + map: &DisplaySnapshot, + display_lines: bool, + point: DisplayPoint, +) -> DisplayPoint { if display_lines { map.clip_point( DisplayPoint::new(point.row(), map.line_len(point.row())), @@ -819,12 +827,8 @@ fn find_backward( } fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> DisplayPoint { - let new_row = (point.row() + times as u32).min(map.max_buffer_row()); - first_non_whitespace( - map, - false, - map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left), - ) + let correct_line = down(map, point, SelectionGoal::None, times).0; + first_non_whitespace(map, false, correct_line) } #[cfg(test)] diff --git a/crates/vim/src/normal.rs b/crates/vim/src/normal.rs index 2b03632c42..a73c518809 100644 --- a/crates/vim/src/normal.rs +++ b/crates/vim/src/normal.rs @@ -10,7 +10,7 @@ mod yank; use std::sync::Arc; use crate::{ - motion::Motion, + motion::{self, Motion}, object::Object, state::{Mode, Operator}, Vim, @@ -214,19 +214,19 @@ fn insert_line_above(_: &mut Workspace, _: &InsertLineAbove, cx: &mut ViewContex .collect(); let edits = selection_start_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let start_of_line = map - .clip_point(DisplayPoint::new(row, 0), Bias::Left) - .to_point(&map); + let start_of_line = + motion::start_of_line(&map, false, DisplayPoint::new(row, 0)) + .to_point(&map); let mut new_text = " ".repeat(indent as usize); new_text.push('\n'); (start_of_line..start_of_line, new_text) }); editor.edit_with_autoindent(edits, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.move_cursors_with(|map, mut cursor, _| { - *cursor.row_mut() -= 1; - *cursor.column_mut() = map.line_len(cursor.row()); - (map.clip_point(cursor, Bias::Left), SelectionGoal::None) + s.move_cursors_with(|map, cursor, _| { + let previous_line = motion::up(map, cursor, SelectionGoal::None, 1).0; + let insert_point = motion::end_of_line(map, false, previous_line); + (insert_point, SelectionGoal::None) }); }); }); @@ -240,15 +240,16 @@ fn insert_line_below(_: &mut Workspace, _: &InsertLineBelow, cx: &mut ViewContex vim.update_active_editor(cx, |editor, cx| { editor.transact(cx, |editor, cx| { let (map, old_selections) = editor.selections.all_display(cx); + let selection_end_rows: HashSet = old_selections .into_iter() .map(|selection| selection.end.row()) .collect(); let edits = selection_end_rows.into_iter().map(|row| { let (indent, _) = map.line_indent(row); - let end_of_line = map - .clip_point(DisplayPoint::new(row, map.line_len(row)), Bias::Left) - .to_point(&map); + let end_of_line = + motion::end_of_line(&map, false, DisplayPoint::new(row, 0)).to_point(&map); + let mut new_text = "\n".to_string(); new_text.push_str(&" ".repeat(indent as usize)); (end_of_line..end_of_line, new_text) diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index cfde221dc5..88fa375851 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -358,6 +358,79 @@ async fn test_wrapped_lines(cx: &mut gpui::TestAppContext) { twelve char "}) .await; + + cx.set_shared_state(indoc! { " + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["enter"]).await; + cx.assert_shared_state(indoc! { " + twelve char twelve char + ˇtwelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["o", "o", "escape"]).await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve char + ˇo + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + tˇwelve char twelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-a", "a", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + twelve char twelve charˇa + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-i", "i", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇitwelve char twelve chara + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-d"]).await; + cx.assert_shared_state(indoc! { " + twelve char + ˇ + twelve char + "}) + .await; + + cx.set_shared_state(indoc! { " + twelve char + twelve char tˇwelve char + twelve char + "}) + .await; + cx.simulate_shared_keystrokes(["shift-o", "o", "escape"]) + .await; + cx.assert_shared_state(indoc! { " + twelve char + ˇo + twelve char twelve char + twelve char + "}) + .await; } #[gpui::test] diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index bc37f2fdd6..d04b1b7768 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,4 +1,3 @@ -use editor::EditorSettings; use indoc::indoc; use settings::SettingsStore; use std::ops::{Deref, DerefMut, Range}; @@ -6,7 +5,7 @@ use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; use language::{ - language_settings::{AllLanguageSettings, LanguageSettings, SoftWrap}, + language_settings::{AllLanguageSettings, SoftWrap}, OffsetRangeExt, }; use util::test::{generate_marked_text, marked_text_offsets}; diff --git a/crates/vim/test_data/test_wrapped_lines.json b/crates/vim/test_data/test_wrapped_lines.json index f9f54c5c43..1ebbd4f205 100644 --- a/crates/vim/test_data/test_wrapped_lines.json +++ b/crates/vim/test_data/test_wrapped_lines.json @@ -24,3 +24,27 @@ {"Get":{"state":"twelve charˇ twelve char\ntwelve char\n","mode":"Normal"}} {"Key":"$"} {"Get":{"state":"twelve char twelve chaˇr\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"tˇwelve char twelve char\ntwelve char\n"}} +{"Key":"enter"} +{"Get":{"state":"twelve char twelve char\nˇtwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve char\nˇo\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntˇwelve char twelve char\ntwelve char\n"}} +{"Key":"shift-a"} +{"Key":"a"} +{"Key":"escape"} +{"Get":{"state":"twelve char\ntwelve char twelve charˇa\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-i"} +{"Key":"i"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇitwelve char twelve chara\ntwelve char\n","mode":"Normal"}} +{"Key":"shift-d"} +{"Get":{"state":"twelve char\nˇ\ntwelve char\n","mode":"Normal"}} +{"Put":{"state":"twelve char\ntwelve char tˇwelve char\ntwelve char\n"}} +{"Key":"shift-o"} +{"Key":"o"} +{"Key":"escape"} +{"Get":{"state":"twelve char\nˇo\ntwelve char twelve char\ntwelve char\n","mode":"Normal"}} From 6fdf101745d18c16d54423d8b6131d4c8b31679c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 14:34:19 -0700 Subject: [PATCH 096/142] Update database and RPC to provide configured feature flags --- crates/client/src/test.rs | 1 + .../20221109000000_test_schema.sql | 19 ++++++ ...0230825190322_add_server_feature_flags.sql | 16 +++++ crates/collab/src/db/ids.rs | 1 + crates/collab/src/db/queries/users.rs | 54 +++++++++++++++++ crates/collab/src/db/tables.rs | 2 + crates/collab/src/db/tables/feature_flag.rs | 40 +++++++++++++ crates/collab/src/db/tables/user.rs | 23 +++++++ crates/collab/src/db/tables/user_feature.rs | 42 +++++++++++++ crates/collab/src/db/tests.rs | 1 + .../collab/src/db/tests/feature_flag_tests.rs | 60 +++++++++++++++++++ crates/collab/src/rpc.rs | 15 +++-- crates/rpc/proto/zed.proto | 1 + crates/rpc/src/rpc.rs | 2 +- 14 files changed, 268 insertions(+), 9 deletions(-) create mode 100644 crates/collab/migrations/20230825190322_add_server_feature_flags.sql create mode 100644 crates/collab/src/db/tables/feature_flag.rs create mode 100644 crates/collab/src/db/tables/user_feature.rs create mode 100644 crates/collab/src/db/tests/feature_flag_tests.rs diff --git a/crates/client/src/test.rs b/crates/client/src/test.rs index 4c12a20566..00e7cd1508 100644 --- a/crates/client/src/test.rs +++ b/crates/client/src/test.rs @@ -168,6 +168,7 @@ impl FakeServer { GetPrivateUserInfoResponse { metrics_id: "the-metrics-id".into(), staff: false, + flags: Default::default(), }, ) .await; diff --git a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql index 7a4cd9fd23..80477dcb3c 100644 --- a/crates/collab/migrations.sqlite/20221109000000_test_schema.sql +++ b/crates/collab/migrations.sqlite/20221109000000_test_schema.sql @@ -249,3 +249,22 @@ CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_and_replic CREATE INDEX "index_channel_buffer_collaborators_on_connection_server_id" ON "channel_buffer_collaborators" ("connection_server_id"); CREATE INDEX "index_channel_buffer_collaborators_on_connection_id" ON "channel_buffer_collaborators" ("connection_id"); CREATE UNIQUE INDEX "index_channel_buffer_collaborators_on_channel_id_connection_id_and_server_id" ON "channel_buffer_collaborators" ("channel_id", "connection_id", "connection_server_id"); + + +CREATE TABLE "feature_flags" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT, + "flag" TEXT NOT NULL UNIQUE +); + +CREATE INDEX "index_feature_flags" ON "feature_flags" ("id"); + + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users (id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags (id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/migrations/20230825190322_add_server_feature_flags.sql b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql new file mode 100644 index 0000000000..fffde54a20 --- /dev/null +++ b/crates/collab/migrations/20230825190322_add_server_feature_flags.sql @@ -0,0 +1,16 @@ +CREATE TABLE "feature_flags" ( + "id" SERIAL PRIMARY KEY, + "flag" VARCHAR(255) NOT NULL UNIQUE +); + +CREATE UNIQUE INDEX "index_feature_flags" ON "feature_flags" ("id"); + +CREATE TABLE "user_features" ( + "user_id" INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, + "feature_id" INTEGER NOT NULL REFERENCES feature_flags(id) ON DELETE CASCADE, + PRIMARY KEY (user_id, feature_id) +); + +CREATE UNIQUE INDEX "index_user_features_user_id_and_feature_id" ON "user_features" ("user_id", "feature_id"); +CREATE INDEX "index_user_features_on_user_id" ON "user_features" ("user_id"); +CREATE INDEX "index_user_features_on_feature_id" ON "user_features" ("feature_id"); diff --git a/crates/collab/src/db/ids.rs b/crates/collab/src/db/ids.rs index 8501083f83..b33ea57183 100644 --- a/crates/collab/src/db/ids.rs +++ b/crates/collab/src/db/ids.rs @@ -125,3 +125,4 @@ id_type!(ServerId); id_type!(SignupId); id_type!(UserId); id_type!(ChannelBufferCollaboratorId); +id_type!(FlagId); diff --git a/crates/collab/src/db/queries/users.rs b/crates/collab/src/db/queries/users.rs index bac0f14f83..bd7c3e9ffd 100644 --- a/crates/collab/src/db/queries/users.rs +++ b/crates/collab/src/db/queries/users.rs @@ -240,4 +240,58 @@ impl Database { result.push('%'); result } + + #[cfg(debug_assertions)] + pub async fn create_user_flag(&self, flag: &str) -> Result { + self.transaction(|tx| async move { + let flag = feature_flag::Entity::insert(feature_flag::ActiveModel { + flag: ActiveValue::set(flag.to_string()), + ..Default::default() + }) + .exec(&*tx) + .await? + .last_insert_id; + + Ok(flag) + }) + .await + } + + #[cfg(debug_assertions)] + pub async fn add_user_flag(&self, user: UserId, flag: FlagId) -> Result<()> { + self.transaction(|tx| async move { + user_feature::Entity::insert(user_feature::ActiveModel { + user_id: ActiveValue::set(user), + feature_id: ActiveValue::set(flag), + }) + .exec(&*tx) + .await?; + + Ok(()) + }) + .await + } + + pub async fn get_user_flags(&self, user: UserId) -> Result> { + self.transaction(|tx| async move { + #[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)] + enum QueryAs { + Flag, + } + + let flags = user::Model { + id: user, + ..Default::default() + } + .find_linked(user::UserFlags) + .select_only() + .column(feature_flag::Column::Flag) + .into_values::<_, QueryAs>() + .all(&*tx) + .await?; + + Ok(flags) + }) + .await + } } diff --git a/crates/collab/src/db/tables.rs b/crates/collab/src/db/tables.rs index fe747e0d27..1765cee065 100644 --- a/crates/collab/src/db/tables.rs +++ b/crates/collab/src/db/tables.rs @@ -7,6 +7,7 @@ pub mod channel_buffer_collaborator; pub mod channel_member; pub mod channel_path; pub mod contact; +pub mod feature_flag; pub mod follower; pub mod language_server; pub mod project; @@ -16,6 +17,7 @@ pub mod room_participant; pub mod server; pub mod signup; pub mod user; +pub mod user_feature; pub mod worktree; pub mod worktree_diagnostic_summary; pub mod worktree_entry; diff --git a/crates/collab/src/db/tables/feature_flag.rs b/crates/collab/src/db/tables/feature_flag.rs new file mode 100644 index 0000000000..41c1451c64 --- /dev/null +++ b/crates/collab/src/db/tables/feature_flag.rs @@ -0,0 +1,40 @@ +use sea_orm::entity::prelude::*; + +use crate::db::FlagId; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "feature_flags")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: FlagId, + pub flag: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeature, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeature.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} + +pub struct FlaggedUsers; + +impl Linked for FlaggedUsers { + type FromEntity = Entity; + + type ToEntity = super::user::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::Flag.def().rev(), + super::user_feature::Relation::User.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user.rs b/crates/collab/src/db/tables/user.rs index 402b06c2a7..739693527f 100644 --- a/crates/collab/src/db/tables/user.rs +++ b/crates/collab/src/db/tables/user.rs @@ -28,6 +28,8 @@ pub enum Relation { HostedProjects, #[sea_orm(has_many = "super::channel_member::Entity")] ChannelMemberships, + #[sea_orm(has_many = "super::user_feature::Entity")] + UserFeatures, } impl Related for Entity { @@ -54,4 +56,25 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::UserFeatures.def() + } +} + impl ActiveModelBehavior for ActiveModel {} + +pub struct UserFlags; + +impl Linked for UserFlags { + type FromEntity = Entity; + + type ToEntity = super::feature_flag::Entity; + + fn link(&self) -> Vec { + vec![ + super::user_feature::Relation::User.def().rev(), + super::user_feature::Relation::Flag.def(), + ] + } +} diff --git a/crates/collab/src/db/tables/user_feature.rs b/crates/collab/src/db/tables/user_feature.rs new file mode 100644 index 0000000000..cc24b5e796 --- /dev/null +++ b/crates/collab/src/db/tables/user_feature.rs @@ -0,0 +1,42 @@ +use sea_orm::entity::prelude::*; + +use crate::db::{FlagId, UserId}; + +#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] +#[sea_orm(table_name = "user_features")] +pub struct Model { + #[sea_orm(primary_key)] + pub user_id: UserId, + #[sea_orm(primary_key)] + pub feature_id: FlagId, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::feature_flag::Entity", + from = "Column::FeatureId", + to = "super::feature_flag::Column::Id" + )] + Flag, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::UserId", + to = "super::user::Column::Id" + )] + User, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Flag.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::User.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/crates/collab/src/db/tests.rs b/crates/collab/src/db/tests.rs index 36a0888a62..ee961006cb 100644 --- a/crates/collab/src/db/tests.rs +++ b/crates/collab/src/db/tests.rs @@ -1,5 +1,6 @@ mod buffer_tests; mod db_tests; +mod feature_flag_tests; use super::*; use gpui::executor::Background; diff --git a/crates/collab/src/db/tests/feature_flag_tests.rs b/crates/collab/src/db/tests/feature_flag_tests.rs new file mode 100644 index 0000000000..9d5f039747 --- /dev/null +++ b/crates/collab/src/db/tests/feature_flag_tests.rs @@ -0,0 +1,60 @@ +use crate::{ + db::{Database, NewUserParams}, + test_both_dbs, +}; +use std::sync::Arc; + +test_both_dbs!( + test_get_user_flags, + test_get_user_flags_postgres, + test_get_user_flags_sqlite +); + +async fn test_get_user_flags(db: &Arc) { + let user_1 = db + .create_user( + &format!("user1@example.com"), + false, + NewUserParams { + github_login: format!("user1"), + github_user_id: 1, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + let user_2 = db + .create_user( + &format!("user2@example.com"), + false, + NewUserParams { + github_login: format!("user2"), + github_user_id: 2, + invite_count: 0, + }, + ) + .await + .unwrap() + .user_id; + + const CHANNELS_ALPHA: &'static str = "channels-alpha"; + const NEW_SEARCH: &'static str = "new-search"; + + let channels_flag = db.create_user_flag(CHANNELS_ALPHA).await.unwrap(); + let search_flag = db.create_user_flag(NEW_SEARCH).await.unwrap(); + + db.add_user_flag(user_1, channels_flag).await.unwrap(); + db.add_user_flag(user_1, search_flag).await.unwrap(); + + db.add_user_flag(user_2, channels_flag).await.unwrap(); + + let mut user_1_flags = db.get_user_flags(user_1).await.unwrap(); + user_1_flags.sort(); + assert_eq!(user_1_flags, &[CHANNELS_ALPHA, NEW_SEARCH]); + + let mut user_2_flags = db.get_user_flags(user_2).await.unwrap(); + user_2_flags.sort(); + assert_eq!(user_2_flags, &[CHANNELS_ALPHA]); +} diff --git a/crates/collab/src/rpc.rs b/crates/collab/src/rpc.rs index 18587c2ba8..6b44711c42 100644 --- a/crates/collab/src/rpc.rs +++ b/crates/collab/src/rpc.rs @@ -2609,20 +2609,19 @@ async fn get_private_user_info( response: Response, session: Session, ) -> Result<()> { - let metrics_id = session - .db() - .await - .get_user_metrics_id(session.user_id) - .await?; - let user = session - .db() - .await + let db = session.db().await; + + let metrics_id = db.get_user_metrics_id(session.user_id).await?; + let user = db .get_user_by_id(session.user_id) .await? .ok_or_else(|| anyhow!("user not found"))?; + let flags = db.get_user_flags(session.user_id).await?; + response.send(proto::GetPrivateUserInfoResponse { metrics_id, staff: user.admin, + flags, })?; Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af2..e0356ebb9a 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -1111,6 +1111,7 @@ message GetPrivateUserInfo {} message GetPrivateUserInfoResponse { string metrics_id = 1; bool staff = 2; + repeated string flags = 3; } // Entities diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index bc9dd6f80b..d64cbae929 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 61; +pub const PROTOCOL_VERSION: u32 = 62; From a3b2c03b1720a9dbda35ff8084a850d8b4560cd8 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 25 Aug 2023 16:13:12 -0700 Subject: [PATCH 097/142] Fix bugs in autoscroll with 'fit' strategy * Scroll to the newest cursor if all cursors can't fit in the viewport. * Refuse to layout an editor less tall than one line height. Co-authored-by: Nathan --- crates/editor/src/editor_tests.rs | 68 ++++++++++++++++++++++++ crates/editor/src/element.rs | 7 +-- crates/editor/src/scroll/autoscroll.rs | 71 +++++++++++++++----------- 3 files changed, 110 insertions(+), 36 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a2a561402f..25a5d45282 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1434,6 +1434,74 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_autoscroll(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + let mut cx = EditorTestContext::new(cx).await; + + let line_height = cx.update_editor(|editor, cx| { + editor.set_vertical_scroll_margin(2, cx); + editor.style(cx).text.line_height(cx.font_cache()) + }); + + let window = cx.window; + window.simulate_resize(vec2f(1000., 6.0 * line_height), &mut cx); + + cx.set_state( + &r#"ˇone + two + three + four + five + six + seven + eight + nine + ten + "#, + ); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 0.0)); + }); + + // Add a cursor below the visible area. Since both cursors cannot fit + // on screen, the editor autoscrolls to reveal the newest cursor, and + // allows the vertical scroll margin below that cursor. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(0, 0)..Point::new(0, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 3.0)); + }); + + // Move down. The editor cursor scrolls down to track the newest cursor. + cx.update_editor(|editor, cx| { + editor.move_down(&Default::default(), cx); + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 4.0)); + }); + + // Add a cursor above the visible area. Since both cursors fit on screen, + // the editor scrolls to show both. + cx.update_editor(|editor, cx| { + editor.change_selections(Some(Autoscroll::fit()), cx, |selections| { + selections.select_ranges([ + Point::new(1, 0)..Point::new(1, 0), + Point::new(6, 0)..Point::new(6, 0), + ]); + }) + }); + cx.update_editor(|editor, cx| { + assert_eq!(editor.snapshot(cx).scroll_position(), vec2f(0., 1.0)); + }); +} + #[gpui::test] async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3ba807308c..1f77979adb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2113,14 +2113,11 @@ impl Element for EditorElement { scroll_height .min(constraint.max_along(Axis::Vertical)) .max(constraint.min_along(Axis::Vertical)) + .max(line_height) .min(line_height * max_lines as f32), ) } else if let EditorMode::SingleLine = snapshot.mode { - size.set_y( - line_height - .min(constraint.max_along(Axis::Vertical)) - .max(constraint.min_along(Axis::Vertical)), - ) + size.set_y(line_height.max(constraint.min_along(Axis::Vertical))) } else if size.y().is_infinite() { size.set_y(scroll_height); } diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index e83e2286b1..ffada50179 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -65,47 +65,52 @@ impl Editor { self.set_scroll_position(scroll_position, cx); } - let (autoscroll, local) = - if let Some(autoscroll) = self.scroll_manager.autoscroll_request.take() { - autoscroll - } else { - return false; - }; + let Some((autoscroll, local)) = self.scroll_manager.autoscroll_request.take() else { + return false; + }; - let first_cursor_top; - let last_cursor_bottom; + let mut target_top; + let mut target_bottom; if let Some(highlighted_rows) = &self.highlighted_rows { - first_cursor_top = highlighted_rows.start as f32; - last_cursor_bottom = first_cursor_top + 1.; - } else if autoscroll == Autoscroll::newest() { - let newest_selection = self.selections.newest::(cx); - first_cursor_top = newest_selection.head().to_display_point(&display_map).row() as f32; - last_cursor_bottom = first_cursor_top + 1.; + target_top = highlighted_rows.start as f32; + target_bottom = target_top + 1.; } else { let selections = self.selections.all::(cx); - first_cursor_top = selections + target_top = selections .first() .unwrap() .head() .to_display_point(&display_map) .row() as f32; - last_cursor_bottom = selections + target_bottom = selections .last() .unwrap() .head() .to_display_point(&display_map) .row() as f32 + 1.0; + + // If the selections can't all fit on screen, scroll to the newest. + if autoscroll == Autoscroll::newest() + || autoscroll == Autoscroll::fit() && target_bottom - target_top > visible_lines + { + let newest_selection_top = selections + .iter() + .max_by_key(|s| s.id) + .unwrap() + .head() + .to_display_point(&display_map) + .row() as f32; + target_top = newest_selection_top; + target_bottom = newest_selection_top + 1.; + } } let margin = if matches!(self.mode, EditorMode::AutoHeight { .. }) { 0. } else { - ((visible_lines - (last_cursor_bottom - first_cursor_top)) / 2.0).floor() + ((visible_lines - (target_bottom - target_top)) / 2.0).floor() }; - if margin < 0.0 { - return false; - } let strategy = match autoscroll { Autoscroll::Strategy(strategy) => strategy, @@ -113,8 +118,8 @@ impl Editor { let last_autoscroll = &self.scroll_manager.last_autoscroll; if let Some(last_autoscroll) = last_autoscroll { if self.scroll_manager.anchor.offset == last_autoscroll.0 - && first_cursor_top == last_autoscroll.1 - && last_cursor_bottom == last_autoscroll.2 + && target_top == last_autoscroll.1 + && target_bottom == last_autoscroll.2 { last_autoscroll.3.next() } else { @@ -129,37 +134,41 @@ impl Editor { match strategy { AutoscrollStrategy::Fit | AutoscrollStrategy::Newest => { let margin = margin.min(self.scroll_manager.vertical_scroll_margin); - let target_top = (first_cursor_top - margin).max(0.0); - let target_bottom = last_cursor_bottom + margin; + let target_top = (target_top - margin).max(0.0); + let target_bottom = target_bottom + margin; let start_row = scroll_position.y(); let end_row = start_row + visible_lines; - if target_top < start_row { + let needs_scroll_up = target_top < start_row; + let needs_scroll_down = target_bottom >= end_row; + + if needs_scroll_up && !needs_scroll_down { scroll_position.set_y(target_top); self.set_scroll_position_internal(scroll_position, local, true, cx); - } else if target_bottom >= end_row { + } + if !needs_scroll_up && needs_scroll_down { scroll_position.set_y(target_bottom - visible_lines); self.set_scroll_position_internal(scroll_position, local, true, cx); } } AutoscrollStrategy::Center => { - scroll_position.set_y((first_cursor_top - margin).max(0.0)); + scroll_position.set_y((target_top - margin).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Top => { - scroll_position.set_y((first_cursor_top).max(0.0)); + scroll_position.set_y((target_top).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } AutoscrollStrategy::Bottom => { - scroll_position.set_y((last_cursor_bottom - visible_lines).max(0.0)); + scroll_position.set_y((target_bottom - visible_lines).max(0.0)); self.set_scroll_position_internal(scroll_position, local, true, cx); } } self.scroll_manager.last_autoscroll = Some(( self.scroll_manager.anchor.offset, - first_cursor_top, - last_cursor_bottom, + target_top, + target_bottom, strategy, )); From 2495d6581efcef080cc33eaa3928ee9d487dbc34 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Sat, 26 Aug 2023 01:31:52 +0200 Subject: [PATCH 098/142] Un serialize project search (#2857) This is the first batch of improvements to current project search. There are few things we can do better still, but I want to get this out in next Preview. Most of the slowness at this point seems to stem from updating UI too often. Release Notes: - Improved project search by making it report results sooner. --------- Co-authored-by: Julia Risley --- crates/collab/src/tests/integration_tests.rs | 21 +- .../src/tests/randomized_integration_tests.rs | 18 +- crates/editor/src/multi_buffer.rs | 94 ++- crates/project/src/project.rs | 583 ++++++++++++------ crates/project/src/project_tests.rs | 11 +- crates/search/src/project_search.rs | 62 +- 6 files changed, 489 insertions(+), 300 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 9bee8d434c..b1227b9501 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -4,7 +4,7 @@ use crate::{ }; use call::{room, ActiveCall, ParticipantLocation, Room}; use client::{User, RECEIVE_TIMEOUT}; -use collections::HashSet; +use collections::{HashMap, HashSet}; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, @@ -4821,15 +4821,16 @@ async fn test_project_search( let project_b = client_b.build_remote_project(project_id, cx_b).await; // Perform a search as the guest. - let results = project_b - .update(cx_b, |project, cx| { - project.search( - SearchQuery::text("world", false, false, Vec::new(), Vec::new()), - cx, - ) - }) - .await - .unwrap(); + let mut results = HashMap::default(); + let mut search_rx = project_b.update(cx_b, |project, cx| { + project.search( + SearchQuery::text("world", false, false, Vec::new(), Vec::new()), + cx, + ) + }); + while let Some((buffer, ranges)) = search_rx.next().await { + results.entry(buffer).or_insert(ranges); + } let mut ranges_by_path = results .into_iter() diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 3557843828..814f248b6d 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -6,7 +6,7 @@ use crate::{ use anyhow::{anyhow, Result}; use call::ActiveCall; use client::RECEIVE_TIMEOUT; -use collections::BTreeMap; +use collections::{BTreeMap, HashMap}; use editor::Bias; use fs::{repository::GitFileStatus, FakeFs, Fs as _}; use futures::StreamExt as _; @@ -722,7 +722,7 @@ async fn apply_client_operation( if detach { "detaching" } else { "awaiting" } ); - let search = project.update(cx, |project, cx| { + let mut search = project.update(cx, |project, cx| { project.search( SearchQuery::text(query, false, false, Vec::new(), Vec::new()), cx, @@ -730,15 +730,13 @@ async fn apply_client_operation( }); drop(project); let search = cx.background().spawn(async move { - search - .await - .map_err(|err| anyhow!("search request failed: {:?}", err)) + let mut results = HashMap::default(); + while let Some((buffer, ranges)) = search.next().await { + results.entry(buffer).or_insert(ranges); + } + results }); - if detach { - cx.update(|cx| search.detach_and_log_err(cx)); - } else { - search.await?; - } + search.await; } ClientOperation::WriteFsEntry { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index b9bf4f52a1..5c0d8b641c 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -6,7 +6,7 @@ use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet}; use futures::{channel::mpsc, SinkExt}; use git::diff::DiffHunk; -use gpui::{AppContext, Entity, ModelContext, ModelHandle, Task}; +use gpui::{AppContext, Entity, ModelContext, ModelHandle}; pub use language::Completion; use language::{ char_kind, @@ -788,59 +788,59 @@ impl MultiBuffer { pub fn stream_excerpts_with_context_lines( &mut self, - excerpts: Vec<(ModelHandle, Vec>)>, + buffer: ModelHandle, + ranges: Vec>, context_line_count: u32, cx: &mut ModelContext, - ) -> (Task<()>, mpsc::Receiver>) { + ) -> mpsc::Receiver> { let (mut tx, rx) = mpsc::channel(256); - let task = cx.spawn(|this, mut cx| async move { - for (buffer, ranges) in excerpts { - let (buffer_id, buffer_snapshot) = - buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); + cx.spawn(|this, mut cx| async move { + let (buffer_id, buffer_snapshot) = + buffer.read_with(&cx, |buffer, _| (buffer.remote_id(), buffer.snapshot())); - let mut excerpt_ranges = Vec::new(); - let mut range_counts = Vec::new(); - cx.background() - .scoped(|scope| { - scope.spawn(async { - let (ranges, counts) = - build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); - excerpt_ranges = ranges; - range_counts = counts; - }); - }) - .await; - - let mut ranges = ranges.into_iter(); - let mut range_counts = range_counts.into_iter(); - for excerpt_ranges in excerpt_ranges.chunks(100) { - let excerpt_ids = this.update(&mut cx, |this, cx| { - this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + let mut excerpt_ranges = Vec::new(); + let mut range_counts = Vec::new(); + cx.background() + .scoped(|scope| { + scope.spawn(async { + let (ranges, counts) = + build_excerpt_ranges(&buffer_snapshot, &ranges, context_line_count); + excerpt_ranges = ranges; + range_counts = counts; }); + }) + .await; - for (excerpt_id, range_count) in - excerpt_ids.into_iter().zip(range_counts.by_ref()) - { - for range in ranges.by_ref().take(range_count) { - let start = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.start, - }; - let end = Anchor { - buffer_id: Some(buffer_id), - excerpt_id: excerpt_id.clone(), - text_anchor: range.end, - }; - if tx.send(start..end).await.is_err() { - break; - } + let mut ranges = ranges.into_iter(); + let mut range_counts = range_counts.into_iter(); + for excerpt_ranges in excerpt_ranges.chunks(100) { + let excerpt_ids = this.update(&mut cx, |this, cx| { + this.push_excerpts(buffer.clone(), excerpt_ranges.iter().cloned(), cx) + }); + + for (excerpt_id, range_count) in excerpt_ids.into_iter().zip(range_counts.by_ref()) + { + for range in ranges.by_ref().take(range_count) { + let start = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.start, + }; + let end = Anchor { + buffer_id: Some(buffer_id), + excerpt_id: excerpt_id.clone(), + text_anchor: range.end, + }; + if tx.send(start..end).await.is_err() { + break; } } } } - }); - (task, rx) + }) + .detach(); + + rx } pub fn push_excerpts( @@ -4438,7 +4438,7 @@ mod tests { async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); - let (task, anchor_ranges) = multibuffer.update(cx, |multibuffer, cx| { + let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); let ranges = vec![ snapshot.anchor_before(Point::new(3, 2))..snapshot.anchor_before(Point::new(4, 2)), @@ -4446,12 +4446,10 @@ mod tests { snapshot.anchor_before(Point::new(15, 0)) ..snapshot.anchor_before(Point::new(15, 0)), ]; - multibuffer.stream_excerpts_with_context_lines(vec![(buffer.clone(), ranges)], 2, cx) + multibuffer.stream_excerpts_with_context_lines(buffer.clone(), ranges, 2, cx) }); let anchor_ranges = anchor_ranges.collect::>().await; - // Ensure task is finished when stream completes. - task.await; let snapshot = multibuffer.read_with(cx, |multibuffer, cx| multibuffer.snapshot(cx)); assert_eq!( diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index c7765bf55a..bb18a41ad4 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -26,8 +26,8 @@ use futures::{ }; use globset::{Glob, GlobSet, GlobSetBuilder}; use gpui::{ - AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, ModelContext, - ModelHandle, Task, WeakModelHandle, + executor::Background, AnyModelHandle, AppContext, AsyncAppContext, BorrowAppContext, Entity, + ModelContext, ModelHandle, Task, WeakModelHandle, }; use itertools::Itertools; use language::{ @@ -37,11 +37,11 @@ use language::{ deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version, serialize_anchor, serialize_version, }, - range_from_lsp, range_to_lsp, Bias, Buffer, CachedLspAdapter, CodeAction, CodeLabel, - Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, File as _, - Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, OffsetRangeExt, - Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, ToOffset, - ToPointUtf16, Transaction, Unclipped, + range_from_lsp, range_to_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CodeAction, + CodeLabel, Completion, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Event as BufferEvent, + File as _, Language, LanguageRegistry, LanguageServerName, LocalFile, LspAdapterDelegate, + OffsetRangeExt, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot, + ToOffset, ToPointUtf16, Transaction, Unclipped, }; use log::error; use lsp::{ @@ -57,8 +57,8 @@ use serde::Serialize; use settings::SettingsStore; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; +use smol::channel::{Receiver, Sender}; use std::{ - cell::RefCell, cmp::{self, Ordering}, convert::TryInto, hash::Hash, @@ -67,7 +67,6 @@ use std::{ ops::Range, path::{self, Component, Path, PathBuf}, process::Stdio, - rc::Rc, str, sync::{ atomic::{AtomicUsize, Ordering::SeqCst}, @@ -525,6 +524,28 @@ impl FormatTrigger { } } } +#[derive(Clone, Debug, PartialEq)] +enum SearchMatchCandidate { + OpenBuffer { + buffer: ModelHandle, + // This might be an unnamed file without representation on filesystem + path: Option>, + }, + Path { + worktree_id: WorktreeId, + path: Arc, + }, +} + +type SearchMatchCandidateIndex = usize; +impl SearchMatchCandidate { + fn path(&self) -> Option> { + match self { + SearchMatchCandidate::OpenBuffer { path, .. } => path.clone(), + SearchMatchCandidate::Path { path, .. } => Some(path.clone()), + } + } +} impl Project { pub fn init_settings(cx: &mut AppContext) { @@ -5099,187 +5120,11 @@ impl Project { &self, query: SearchQuery, cx: &mut ModelContext, - ) -> Task, Vec>>>> { + ) -> Receiver<(ModelHandle, Vec>)> { if self.is_local() { - let snapshots = self - .visible_worktrees(cx) - .filter_map(|tree| { - let tree = tree.read(cx).as_local()?; - Some(tree.snapshot()) - }) - .collect::>(); - - let background = cx.background().clone(); - let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); - if path_count == 0 { - return Task::ready(Ok(Default::default())); - } - let workers = background.num_cpus().min(path_count); - let (matching_paths_tx, mut matching_paths_rx) = smol::channel::bounded(1024); - cx.background() - .spawn({ - let fs = self.fs.clone(); - let background = cx.background().clone(); - let query = query.clone(); - async move { - let fs = &fs; - let query = &query; - let matching_paths_tx = &matching_paths_tx; - let paths_per_worker = (path_count + workers - 1) / workers; - let snapshots = &snapshots; - background - .scoped(|scope| { - for worker_ix in 0..workers { - let worker_start_ix = worker_ix * paths_per_worker; - let worker_end_ix = worker_start_ix + paths_per_worker; - scope.spawn(async move { - let mut snapshot_start_ix = 0; - let mut abs_path = PathBuf::new(); - for snapshot in snapshots { - let snapshot_end_ix = - snapshot_start_ix + snapshot.visible_file_count(); - if worker_end_ix <= snapshot_start_ix { - break; - } else if worker_start_ix > snapshot_end_ix { - snapshot_start_ix = snapshot_end_ix; - continue; - } else { - let start_in_snapshot = worker_start_ix - .saturating_sub(snapshot_start_ix); - let end_in_snapshot = - cmp::min(worker_end_ix, snapshot_end_ix) - - snapshot_start_ix; - - for entry in snapshot - .files(false, start_in_snapshot) - .take(end_in_snapshot - start_in_snapshot) - { - if matching_paths_tx.is_closed() { - break; - } - let matches = if query - .file_matches(Some(&entry.path)) - { - abs_path.clear(); - abs_path.push(&snapshot.abs_path()); - abs_path.push(&entry.path); - if let Some(file) = - fs.open_sync(&abs_path).await.log_err() - { - query.detect(file).unwrap_or(false) - } else { - false - } - } else { - false - }; - - if matches { - let project_path = - (snapshot.id(), entry.path.clone()); - if matching_paths_tx - .send(project_path) - .await - .is_err() - { - break; - } - } - } - - snapshot_start_ix = snapshot_end_ix; - } - } - }); - } - }) - .await; - } - }) - .detach(); - - let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); - let open_buffers = self - .opened_buffers - .values() - .filter_map(|b| b.upgrade(cx)) - .collect::>(); - cx.spawn(|this, cx| async move { - for buffer in &open_buffers { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer.clone(), snapshot)).await?; - } - - let open_buffers = Rc::new(RefCell::new(open_buffers)); - while let Some(project_path) = matching_paths_rx.next().await { - if buffers_tx.is_closed() { - break; - } - - let this = this.clone(); - let open_buffers = open_buffers.clone(); - let buffers_tx = buffers_tx.clone(); - cx.spawn(|mut cx| async move { - if let Some(buffer) = this - .update(&mut cx, |this, cx| this.open_buffer(project_path, cx)) - .await - .log_err() - { - if open_buffers.borrow_mut().insert(buffer.clone()) { - let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); - buffers_tx.send((buffer, snapshot)).await?; - } - } - - Ok::<_, anyhow::Error>(()) - }) - .detach(); - } - - Ok::<_, anyhow::Error>(()) - }) - .detach_and_log_err(cx); - - let background = cx.background().clone(); - cx.background().spawn(async move { - let query = &query; - let mut matched_buffers = Vec::new(); - for _ in 0..workers { - matched_buffers.push(HashMap::default()); - } - background - .scoped(|scope| { - for worker_matched_buffers in matched_buffers.iter_mut() { - let mut buffers_rx = buffers_rx.clone(); - scope.spawn(async move { - while let Some((buffer, snapshot)) = buffers_rx.next().await { - let buffer_matches = if query.file_matches( - snapshot.file().map(|file| file.path().as_ref()), - ) { - query - .search(&snapshot, None) - .await - .iter() - .map(|range| { - snapshot.anchor_before(range.start) - ..snapshot.anchor_after(range.end) - }) - .collect() - } else { - Vec::new() - }; - if !buffer_matches.is_empty() { - worker_matched_buffers - .insert(buffer.clone(), buffer_matches); - } - } - }); - } - }) - .await; - Ok(matched_buffers.into_iter().flatten().collect()) - }) + self.search_local(query, cx) } else if let Some(project_id) = self.remote_id() { + let (tx, rx) = smol::channel::unbounded(); let request = self.client.request(query.to_proto(project_id)); cx.spawn(|this, mut cx| async move { let response = request.await?; @@ -5303,13 +5148,303 @@ impl Project { .or_insert(Vec::new()) .push(start..end) } - Ok(result) + for (buffer, ranges) in result { + let _ = tx.send((buffer, ranges)).await; + } + Result::<(), anyhow::Error>::Ok(()) }) + .detach_and_log_err(cx); + rx } else { - Task::ready(Ok(Default::default())) + unimplemented!(); } } + pub fn search_local( + &self, + query: SearchQuery, + cx: &mut ModelContext, + ) -> Receiver<(ModelHandle, Vec>)> { + // Local search is split into several phases. + // TL;DR is that we do 2 passes; initial pass to pick files which contain at least one match + // and the second phase that finds positions of all the matches found in the candidate files. + // The Receiver obtained from this function returns matches sorted by buffer path. Files without a buffer path are reported first. + // + // It gets a bit hairy though, because we must account for files that do not have a persistent representation + // on FS. Namely, if you have an untitled buffer or unsaved changes in a buffer, we want to scan that too. + // + // 1. We initialize a queue of match candidates and feed all opened buffers into it (== unsaved files / untitled buffers). + // Then, we go through a worktree and check for files that do match a predicate. If the file had an opened version, we skip the scan + // of FS version for that file altogether - after all, what we have in memory is more up-to-date than what's in FS. + // 2. At this point, we have a list of all potentially matching buffers/files. + // We sort that list by buffer path - this list is retained for later use. + // We ensure that all buffers are now opened and available in project. + // 3. We run a scan over all the candidate buffers on multiple background threads. + // We cannot assume that there will even be a match - while at least one match + // is guaranteed for files obtained from FS, the buffers we got from memory (unsaved files/unnamed buffers) might not have a match at all. + // There is also an auxilliary background thread responsible for result gathering. + // This is where the sorted list of buffers comes into play to maintain sorted order; Whenever this background thread receives a notification (buffer has/doesn't have matches), + // it keeps it around. It reports matches in sorted order, though it accepts them in unsorted order as well. + // As soon as the match info on next position in sorted order becomes available, it reports it (if it's a match) or skips to the next + // entry - which might already be available thanks to out-of-order processing. + // + // We could also report matches fully out-of-order, without maintaining a sorted list of matching paths. + // This however would mean that project search (that is the main user of this function) would have to do the sorting itself, on the go. + // This isn't as straightforward as running an insertion sort sadly, and would also mean that it would have to care about maintaining match index + // in face of constantly updating list of sorted matches. + // Meanwhile, this implementation offers index stability, since the matches are already reported in a sorted order. + let snapshots = self + .visible_worktrees(cx) + .filter_map(|tree| { + let tree = tree.read(cx).as_local()?; + Some(tree.snapshot()) + }) + .collect::>(); + + let background = cx.background().clone(); + let path_count: usize = snapshots.iter().map(|s| s.visible_file_count()).sum(); + if path_count == 0 { + let (_, rx) = smol::channel::bounded(1024); + return rx; + } + let workers = background.num_cpus().min(path_count); + let (matching_paths_tx, matching_paths_rx) = smol::channel::bounded(1024); + let mut unnamed_files = vec![]; + let opened_buffers = self + .opened_buffers + .iter() + .filter_map(|(_, b)| { + let buffer = b.upgrade(cx)?; + let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); + if let Some(path) = snapshot.file().map(|file| file.path()) { + Some((path.clone(), (buffer, snapshot))) + } else { + unnamed_files.push(buffer); + None + } + }) + .collect(); + cx.background() + .spawn(Self::background_search( + unnamed_files, + opened_buffers, + cx.background().clone(), + self.fs.clone(), + workers, + query.clone(), + path_count, + snapshots, + matching_paths_tx, + )) + .detach(); + + let (buffers, buffers_rx) = Self::sort_candidates_and_open_buffers(matching_paths_rx, cx); + let background = cx.background().clone(); + let (result_tx, result_rx) = smol::channel::bounded(1024); + cx.background() + .spawn(async move { + let Ok(buffers) = buffers.await else { + return; + }; + + let buffers_len = buffers.len(); + if buffers_len == 0 { + return; + } + let query = &query; + let (finished_tx, mut finished_rx) = smol::channel::unbounded(); + background + .scoped(|scope| { + #[derive(Clone)] + struct FinishedStatus { + entry: Option<(ModelHandle, Vec>)>, + buffer_index: SearchMatchCandidateIndex, + } + + for _ in 0..workers { + let finished_tx = finished_tx.clone(); + let mut buffers_rx = buffers_rx.clone(); + scope.spawn(async move { + while let Some((entry, buffer_index)) = buffers_rx.next().await { + let buffer_matches = if let Some((_, snapshot)) = entry.as_ref() + { + if query.file_matches( + snapshot.file().map(|file| file.path().as_ref()), + ) { + query + .search(&snapshot, None) + .await + .iter() + .map(|range| { + snapshot.anchor_before(range.start) + ..snapshot.anchor_after(range.end) + }) + .collect() + } else { + Vec::new() + } + } else { + Vec::new() + }; + + let status = if !buffer_matches.is_empty() { + let entry = if let Some((buffer, _)) = entry.as_ref() { + Some((buffer.clone(), buffer_matches)) + } else { + None + }; + FinishedStatus { + entry, + buffer_index, + } + } else { + FinishedStatus { + entry: None, + buffer_index, + } + }; + if finished_tx.send(status).await.is_err() { + break; + } + } + }); + } + // Report sorted matches + scope.spawn(async move { + let mut current_index = 0; + let mut scratch = vec![None; buffers_len]; + while let Some(status) = finished_rx.next().await { + debug_assert!( + scratch[status.buffer_index].is_none(), + "Got match status of position {} twice", + status.buffer_index + ); + let index = status.buffer_index; + scratch[index] = Some(status); + while current_index < buffers_len { + let Some(current_entry) = scratch[current_index].take() else { + // We intentionally **do not** increment `current_index` here. When next element arrives + // from `finished_rx`, we will inspect the same position again, hoping for it to be Some(_) + // this time. + break; + }; + if let Some(entry) = current_entry.entry { + result_tx.send(entry).await.log_err(); + } + current_index += 1; + } + if current_index == buffers_len { + break; + } + } + }); + }) + .await; + }) + .detach(); + result_rx + } + /// Pick paths that might potentially contain a match of a given search query. + async fn background_search( + unnamed_buffers: Vec>, + opened_buffers: HashMap, (ModelHandle, BufferSnapshot)>, + background: Arc, + fs: Arc, + workers: usize, + query: SearchQuery, + path_count: usize, + snapshots: Vec, + matching_paths_tx: Sender, + ) { + let fs = &fs; + let query = &query; + let matching_paths_tx = &matching_paths_tx; + let snapshots = &snapshots; + let paths_per_worker = (path_count + workers - 1) / workers; + for buffer in unnamed_buffers { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: None, + }) + .await + .log_err(); + } + for (path, (buffer, _)) in opened_buffers.iter() { + matching_paths_tx + .send(SearchMatchCandidate::OpenBuffer { + buffer: buffer.clone(), + path: Some(path.clone()), + }) + .await + .log_err(); + } + background + .scoped(|scope| { + for worker_ix in 0..workers { + let worker_start_ix = worker_ix * paths_per_worker; + let worker_end_ix = worker_start_ix + paths_per_worker; + let unnamed_buffers = opened_buffers.clone(); + scope.spawn(async move { + let mut snapshot_start_ix = 0; + let mut abs_path = PathBuf::new(); + for snapshot in snapshots { + let snapshot_end_ix = snapshot_start_ix + snapshot.visible_file_count(); + if worker_end_ix <= snapshot_start_ix { + break; + } else if worker_start_ix > snapshot_end_ix { + snapshot_start_ix = snapshot_end_ix; + continue; + } else { + let start_in_snapshot = + worker_start_ix.saturating_sub(snapshot_start_ix); + let end_in_snapshot = + cmp::min(worker_end_ix, snapshot_end_ix) - snapshot_start_ix; + + for entry in snapshot + .files(false, start_in_snapshot) + .take(end_in_snapshot - start_in_snapshot) + { + if matching_paths_tx.is_closed() { + break; + } + if unnamed_buffers.contains_key(&entry.path) { + continue; + } + let matches = if query.file_matches(Some(&entry.path)) { + abs_path.clear(); + abs_path.push(&snapshot.abs_path()); + abs_path.push(&entry.path); + if let Some(file) = fs.open_sync(&abs_path).await.log_err() + { + query.detect(file).unwrap_or(false) + } else { + false + } + } else { + false + }; + + if matches { + let project_path = SearchMatchCandidate::Path { + worktree_id: snapshot.id(), + path: entry.path.clone(), + }; + if matching_paths_tx.send(project_path).await.is_err() { + break; + } + } + } + + snapshot_start_ix = snapshot_end_ix; + } + } + }); + } + }) + .await; + } + // TODO: Wire this up to allow selecting a server? fn request_lsp( &self, @@ -5384,6 +5519,61 @@ impl Project { Task::ready(Ok(Default::default())) } + fn sort_candidates_and_open_buffers( + mut matching_paths_rx: Receiver, + cx: &mut ModelContext, + ) -> ( + futures::channel::oneshot::Receiver>, + Receiver<( + Option<(ModelHandle, BufferSnapshot)>, + SearchMatchCandidateIndex, + )>, + ) { + let (buffers_tx, buffers_rx) = smol::channel::bounded(1024); + let (sorted_buffers_tx, sorted_buffers_rx) = futures::channel::oneshot::channel(); + cx.spawn(|this, cx| async move { + let mut buffers = vec![]; + while let Some(entry) = matching_paths_rx.next().await { + buffers.push(entry); + } + buffers.sort_by_key(|candidate| candidate.path()); + let matching_paths = buffers.clone(); + let _ = sorted_buffers_tx.send(buffers); + for (index, candidate) in matching_paths.into_iter().enumerate() { + if buffers_tx.is_closed() { + break; + } + let this = this.clone(); + let buffers_tx = buffers_tx.clone(); + cx.spawn(|mut cx| async move { + let buffer = match candidate { + SearchMatchCandidate::OpenBuffer { buffer, .. } => Some(buffer), + SearchMatchCandidate::Path { worktree_id, path } => this + .update(&mut cx, |this, cx| { + this.open_buffer((worktree_id, path), cx) + }) + .await + .log_err(), + }; + if let Some(buffer) = buffer { + let snapshot = buffer.read_with(&cx, |buffer, _| buffer.snapshot()); + buffers_tx + .send((Some((buffer, snapshot)), index)) + .await + .log_err(); + } else { + buffers_tx.send((None, index)).await.log_err(); + } + + Ok::<_, anyhow::Error>(()) + }) + .detach(); + } + }) + .detach(); + (sorted_buffers_rx, buffers_rx) + } + pub fn find_or_create_local_worktree( &mut self, abs_path: impl AsRef, @@ -7006,17 +7196,17 @@ impl Project { ) -> Result { let peer_id = envelope.original_sender_id()?; let query = SearchQuery::from_proto(envelope.payload)?; - let result = this - .update(&mut cx, |this, cx| this.search(query, cx)) - .await?; + let mut result = this.update(&mut cx, |this, cx| this.search(query, cx)); - this.update(&mut cx, |this, cx| { + cx.spawn(|mut cx| async move { let mut locations = Vec::new(); - for (buffer, ranges) in result { + while let Some((buffer, ranges)) = result.next().await { for range in ranges { let start = serialize_anchor(&range.start); let end = serialize_anchor(&range.end); - let buffer_id = this.create_buffer_for_peer(&buffer, peer_id, cx); + let buffer_id = this.update(&mut cx, |this, cx| { + this.create_buffer_for_peer(&buffer, peer_id, cx) + }); locations.push(proto::Location { buffer_id, start: Some(start), @@ -7026,6 +7216,7 @@ impl Project { } Ok(proto::SearchProjectResponse { locations }) }) + .await } async fn handle_open_buffer_for_symbol( diff --git a/crates/project/src/project_tests.rs b/crates/project/src/project_tests.rs index a504900c83..7c5983a0a9 100644 --- a/crates/project/src/project_tests.rs +++ b/crates/project/src/project_tests.rs @@ -3953,11 +3953,12 @@ async fn search( query: SearchQuery, cx: &mut gpui::TestAppContext, ) -> Result>>> { - let results = project - .update(cx, |project, cx| project.search(query, cx)) - .await?; - - Ok(results + let mut search_rx = project.update(cx, |project, cx| project.search(query, cx)); + let mut result = HashMap::default(); + while let Some((buffer, range)) = search_rx.next().await { + result.entry(buffer).or_insert(range); + } + Ok(result .into_iter() .map(|(buffer, ranges)| { buffer.read_with(cx, |buffer, _| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index d7633a45e4..6364183877 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -185,28 +185,26 @@ impl ProjectSearch { self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { - let matches = search.await.log_err()?; + let mut matches = search; let this = this.upgrade(&cx)?; - let mut matches = matches.into_iter().collect::>(); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.match_ranges.clear(); + this.excerpts.update(cx, |this, cx| this.clear(cx)); this.no_results = Some(true); - matches.sort_by_key(|(buffer, _)| buffer.read(cx).file().map(|file| file.path())); - this.excerpts.update(cx, |excerpts, cx| { - excerpts.clear(cx); - excerpts.stream_excerpts_with_context_lines(matches, 1, cx) - }) }); - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + while let Some((buffer, anchors)) = matches.next().await { + let mut ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, anchors, 1, cx) + }) }); + + while let Some(range) = ranges.next().await { + this.update(&mut cx, |this, _| this.match_ranges.push(range)); + } + this.update(&mut cx, |_, cx| cx.notify()); } this.update(&mut cx, |this, cx| { @@ -238,29 +236,31 @@ impl ProjectSearch { self.no_results = Some(true); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; + let matches = results + .into_iter() + .map(|result| (result.buffer, vec![result.range.start..result.range.start])); - let (_task, mut match_ranges) = this.update(&mut cx, |this, cx| { + this.update(&mut cx, |this, cx| { this.excerpts.update(cx, |excerpts, cx| { excerpts.clear(cx); - - let matches = results - .into_iter() - .map(|result| (result.buffer, vec![result.range.start..result.range.start])) - .collect(); - - excerpts.stream_excerpts_with_context_lines(matches, 3, cx) }) }); - - while let Some(match_range) = match_ranges.next().await { - this.update(&mut cx, |this, cx| { - this.match_ranges.push(match_range); - while let Ok(Some(match_range)) = match_ranges.try_next() { - this.match_ranges.push(match_range); - } + for (buffer, ranges) in matches { + let mut match_ranges = this.update(&mut cx, |this, cx| { this.no_results = Some(false); - cx.notify(); + this.excerpts.update(cx, |excerpts, cx| { + excerpts.stream_excerpts_with_context_lines(buffer, ranges, 3, cx) + }) }); + while let Some(match_range) = match_ranges.next().await { + this.update(&mut cx, |this, cx| { + this.match_ranges.push(match_range); + while let Ok(Some(match_range)) = match_ranges.try_next() { + this.match_ranges.push(match_range); + } + cx.notify(); + }); + } } this.update(&mut cx, |this, cx| { From ddd7ab116f981d2871f266d63721aaca5942fa15 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 00:05:39 +0300 Subject: [PATCH 099/142] Do not convert lsp::Location of hint labels before resolve --- crates/editor/src/link_go_to_definition.rs | 130 ++++++++----- crates/project/src/lsp_command.rs | 208 ++++++--------------- crates/project/src/project.rs | 29 ++- crates/rpc/proto/zed.proto | 5 +- 4 files changed, 163 insertions(+), 209 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 926c0d6dde..8d46194f42 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,13 +4,15 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; +use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{Bias, ToOffset}; +use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use lsp::LanguageServerId; use project::{ HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, ResolveState, }; -use std::ops::Range; +use std::{ops::Range, sync::Arc}; use util::TryFutureExt; #[derive(Debug, Default)] @@ -24,7 +26,7 @@ pub struct LinkGoToDefinitionState { pub enum GoToDefinitionTrigger { Text(DisplayPoint), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), None, } @@ -38,7 +40,7 @@ pub struct InlayRange { #[derive(Debug, Clone)] pub enum TriggerPoint { Text(Anchor), - InlayHint(InlayRange, LocationLink), + InlayHint(InlayRange, lsp::Location, LanguageServerId), } #[derive(Debug, Clone, PartialEq, Eq)] @@ -61,12 +63,12 @@ impl DocumentRange { let point_after_start = range.start.cmp(point, &snapshot.buffer_snapshot).is_le(); point_after_start && range.end.cmp(point, &snapshot.buffer_snapshot).is_ge() } - (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _)) => { + (DocumentRange::Inlay(range), TriggerPoint::InlayHint(point, _, _)) => { range.highlight_start.cmp(&point.highlight_end).is_le() && range.highlight_end.cmp(&point.highlight_end).is_ge() } (DocumentRange::Inlay(_), TriggerPoint::Text(_)) - | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _)) => false, + | (DocumentRange::Text(_), TriggerPoint::InlayHint(_, _, _)) => false, } } } @@ -75,7 +77,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, } } @@ -88,7 +90,7 @@ impl TriggerPoint { LinkDefinitionKind::Symbol } } - TriggerPoint::InlayHint(_, _) => LinkDefinitionKind::Type, + TriggerPoint::InlayHint(_, _, _) => LinkDefinitionKind::Type, } } } @@ -110,7 +112,9 @@ pub fn update_go_to_definition_link( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, target) => Some(TriggerPoint::InlayHint(p, target)), + GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) + } GoToDefinitionTrigger::None => None, }; @@ -277,35 +281,25 @@ pub fn update_inlay_link_and_hover_points( ); hover_updated = true; } - if let Some(location) = hovered_hint_part.location { - if let Some(buffer) = - cached_hint.position.buffer_id.and_then(|buffer_id| { - editor.buffer().read(cx).buffer(buffer_id) - }) - { - go_to_definition_updated = true; - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::InlayHint( - InlayRange { - inlay_position: hovered_hint.position, - highlight_start: part_range.start, - highlight_end: part_range.end, - }, - LocationLink { - origin: Some(Location { - buffer, - range: cached_hint.position - ..cached_hint.position, - }), - target: location, - }, - ), - cmd_held, - shift_held, - cx, - ); - } + if let Some((language_server_id, location)) = + hovered_hint_part.location + { + go_to_definition_updated = true; + update_go_to_definition_link( + editor, + GoToDefinitionTrigger::InlayHint( + InlayRange { + inlay_position: hovered_hint.position, + highlight_start: part_range.start, + highlight_end: part_range.end, + }, + location, + language_server_id, + ), + cmd_held, + shift_held, + cx, + ); } } } @@ -415,7 +409,6 @@ pub fn show_link_definition( let end = snapshot .buffer_snapshot .anchor_in_excerpt(excerpt_id.clone(), origin.range.end); - DocumentRange::Text(start..end) }) }), @@ -423,10 +416,59 @@ pub fn show_link_definition( ) }) } - TriggerPoint::InlayHint(trigger_source, trigger_target) => Some(( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![trigger_target.clone()], - )), + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { + let target = match project.update(&mut cx, |project, cx| { + let language_server_name = project + .language_server_for_buffer(buffer.read(cx), *server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + *server_id, + language_server_name, + cx, + ) + }) + }) { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = cx.read(|cx| { + let target_buffer = target_buffer_handle.read(cx); + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }); + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + + target.map(|target| { + ( + Some(DocumentRange::Inlay(trigger_source.clone())), + vec![LocationLink { + origin: Some(Location { + buffer: buffer.clone(), + range: trigger_source.inlay_position.text_anchor + ..trigger_source.inlay_position.text_anchor, + }), + target, + }], + ) + }) + } }; this.update(&mut cx, |this, cx| { @@ -479,7 +521,7 @@ pub fn show_link_definition( ..snapshot.anchor_after(offset_range.end), ) } - TriggerPoint::InlayHint(inlay_coordinates, _) => { + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { DocumentRange::Inlay(inlay_coordinates) } }); diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 8ca2571869..8239cf8690 100644 --- a/crates/project/src/lsp_command.rs +++ b/crates/project/src/lsp_command.rs @@ -1,6 +1,6 @@ use crate::{ DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, - InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Item, Location, LocationLink, + InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, MarkupContent, Project, ProjectTransaction, ResolveState, }; use anyhow::{anyhow, Context, Result}; @@ -14,8 +14,8 @@ use language::{ point_from_lsp, point_to_lsp, proto::{deserialize_anchor, deserialize_version, serialize_anchor, serialize_version}, range_from_lsp, range_to_lsp, Anchor, Bias, Buffer, BufferSnapshot, CachedLspAdapter, CharKind, - CodeAction, Completion, LanguageServerName, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, - Transaction, Unclipped, + CodeAction, Completion, OffsetRangeExt, PointUtf16, ToOffset, ToPointUtf16, Transaction, + Unclipped, }; use lsp::{DocumentHighlightKind, LanguageServer, LanguageServerId, OneOf, ServerCapabilities}; use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc}; @@ -1787,7 +1787,6 @@ impl LspCommand for OnTypeFormatting { impl InlayHints { pub async fn lsp_to_project_hint( lsp_hint: lsp::InlayHint, - project: &ModelHandle, buffer_handle: &ModelHandle, server_id: LanguageServerId, resolve_state: ResolveState, @@ -1809,15 +1808,9 @@ impl InlayHints { buffer.anchor_after(position) } }); - let label = Self::lsp_inlay_label_to_project( - &buffer_handle, - project, - server_id, - lsp_hint.label, - cx, - ) - .await - .context("lsp to project inlay hint conversion")?; + let label = Self::lsp_inlay_label_to_project(lsp_hint.label, server_id) + .await + .context("lsp to project inlay hint conversion")?; let padding_left = if force_no_type_left_padding && kind == Some(InlayHintKind::Type) { false } else { @@ -1847,72 +1840,14 @@ impl InlayHints { } async fn lsp_inlay_label_to_project( - buffer: &ModelHandle, - project: &ModelHandle, - server_id: LanguageServerId, lsp_label: lsp::InlayHintLabel, - cx: &mut AsyncAppContext, + server_id: LanguageServerId, ) -> anyhow::Result { let label = match lsp_label { lsp::InlayHintLabel::String(s) => InlayHintLabel::String(s), lsp::InlayHintLabel::LabelParts(lsp_parts) => { - let mut parts_data = Vec::with_capacity(lsp_parts.len()); - buffer.update(cx, |buffer, cx| { - for lsp_part in lsp_parts { - let location_buffer_task = match &lsp_part.location { - Some(lsp_location) => { - let location_buffer_task = project.update(cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer, server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - server_id, - language_server_name, - cx, - ) - }) - }); - Some(lsp_location.clone()).zip(location_buffer_task) - } - None => None, - }; - - parts_data.push((lsp_part, location_buffer_task)); - } - }); - - let mut parts = Vec::with_capacity(parts_data.len()); - for (lsp_part, location_buffer_task) in parts_data { - let location = match location_buffer_task { - Some((lsp_location, target_buffer_handle_task)) => { - let target_buffer_handle = target_buffer_handle_task - .await - .context("resolving location for label part buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Some(Location { - buffer: target_buffer_handle, - range, - }) - } - None => None, - }; - + let mut parts = Vec::with_capacity(lsp_parts.len()); + for lsp_part in lsp_parts { parts.push(InlayHintLabelPart { value: lsp_part.value, tooltip: lsp_part.tooltip.map(|tooltip| match tooltip { @@ -1929,7 +1864,7 @@ impl InlayHints { }) } }), - location, + location: Some(server_id).zip(lsp_part.location), }); } InlayHintLabel::LabelParts(parts) @@ -1939,7 +1874,7 @@ impl InlayHints { Ok(label) } - pub fn project_to_proto_hint(response_hint: InlayHint, cx: &AppContext) -> proto::InlayHint { + pub fn project_to_proto_hint(response_hint: InlayHint) -> proto::InlayHint { let (state, lsp_resolve_state) = match response_hint.resolve_state { ResolveState::Resolved => (0, None), ResolveState::CanResolve(server_id, resolve_data) => ( @@ -1969,7 +1904,11 @@ impl InlayHints { InlayHintLabel::String(s) => proto::inlay_hint_label::Label::Value(s), InlayHintLabel::LabelParts(label_parts) => { proto::inlay_hint_label::Label::LabelParts(proto::InlayHintLabelParts { - parts: label_parts.into_iter().map(|label_part| proto::InlayHintLabelPart { + parts: label_parts.into_iter().map(|label_part| { + let location_url = label_part.location.as_ref().map(|(_, location)| location.uri.to_string()); + let location_range_start = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.start).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + let location_range_end = label_part.location.as_ref().map(|(_, location)| point_from_lsp(location.range.end).0).map(|point| proto::PointUtf16 { row: point.row, column: point.column }); + proto::InlayHintLabelPart { value: label_part.value, tooltip: label_part.tooltip.map(|tooltip| { let proto_tooltip = match tooltip { @@ -1981,12 +1920,11 @@ impl InlayHints { }; proto::InlayHintLabelPartTooltip {content: Some(proto_tooltip)} }), - location: label_part.location.map(|location| proto::Location { - start: Some(serialize_anchor(&location.range.start)), - end: Some(serialize_anchor(&location.range.end)), - buffer_id: location.buffer.read(cx).remote_id(), - }), - }).collect() + location_url, + location_range_start, + location_range_end, + language_server_id: label_part.location.as_ref().map(|(server_id, _)| server_id.0 as u64), + }}).collect() }) } }), @@ -1994,16 +1932,12 @@ impl InlayHints { kind: response_hint.kind.map(|kind| kind.name().to_string()), tooltip: response_hint.tooltip.map(|response_tooltip| { let proto_tooltip = match response_tooltip { - InlayHintTooltip::String(s) => { - proto::inlay_hint_tooltip::Content::Value(s) - } + InlayHintTooltip::String(s) => proto::inlay_hint_tooltip::Content::Value(s), InlayHintTooltip::MarkupContent(markup_content) => { - proto::inlay_hint_tooltip::Content::MarkupContent( - proto::MarkupContent { - is_markdown: markup_content.kind == HoverBlockKind::Markdown, - value: markup_content.value, - }, - ) + proto::inlay_hint_tooltip::Content::MarkupContent(proto::MarkupContent { + is_markdown: markup_content.kind == HoverBlockKind::Markdown, + value: markup_content.value, + }) } }; proto::InlayHintTooltip { @@ -2014,16 +1948,7 @@ impl InlayHints { } } - pub async fn proto_to_project_hint( - message_hint: proto::InlayHint, - project: &ModelHandle, - cx: &mut AsyncAppContext, - ) -> anyhow::Result { - let buffer_id = message_hint - .position - .as_ref() - .and_then(|location| location.buffer_id) - .context("missing buffer id")?; + pub fn proto_to_project_hint(message_hint: proto::InlayHint) -> anyhow::Result { let resolve_state = message_hint.resolve_state.as_ref().unwrap_or_else(|| { panic!("incorrect proto inlay hint message: no resolve state in hint {message_hint:?}",) }); @@ -2064,9 +1989,6 @@ impl InlayHints { proto::inlay_hint_label::Label::LabelParts(parts) => { let mut label_parts = Vec::new(); for part in parts.parts { - let buffer = project - .update(cx, |this, cx| this.wait_for_remote_buffer(buffer_id, cx)) - .await?; label_parts.push(InlayHintLabelPart { value: part.value, tooltip: part.tooltip.map(|tooltip| match tooltip.content { @@ -2087,19 +2009,35 @@ impl InlayHints { }), None => InlayHintLabelPartTooltip::String(String::new()), }), - location: match part.location { - Some(location) => Some(Location { - range: location - .start - .and_then(language::proto::deserialize_anchor) - .context("invalid start")? - ..location - .end - .and_then(language::proto::deserialize_anchor) - .context("invalid end")?, - buffer, - }), - None => None, + location: { + match part + .location_url + .zip( + part.location_range_start.and_then(|start| { + Some(start..part.location_range_end?) + }), + ) + .zip(part.language_server_id) + { + Some(((uri, range), server_id)) => Some(( + LanguageServerId(server_id as usize), + lsp::Location { + uri: lsp::Url::parse(&uri) + .context("invalid uri in hint part {part:?}")?, + range: lsp::Range::new( + point_to_lsp(PointUtf16::new( + range.start.row, + range.start.column, + )), + point_to_lsp(PointUtf16::new( + range.end.row, + range.end.column, + )), + ), + }, + )), + None => None, + } }, }); } @@ -2132,12 +2070,7 @@ impl InlayHints { }) } - pub fn project_to_lsp_hint( - hint: InlayHint, - project: &ModelHandle, - snapshot: &BufferSnapshot, - cx: &AsyncAppContext, - ) -> lsp::InlayHint { + pub fn project_to_lsp_hint(hint: InlayHint, snapshot: &BufferSnapshot) -> lsp::InlayHint { lsp::InlayHint { position: point_to_lsp(hint.position.to_point_utf16(snapshot)), kind: hint.kind.map(|kind| match kind { @@ -2190,22 +2123,7 @@ impl InlayHints { } }) }), - location: part.location.and_then(|location| { - let (path, location_snapshot) = cx.read(|cx| { - let buffer = location.buffer.read(cx); - let project_path = buffer.project_path(cx)?; - let location_snapshot = buffer.snapshot(); - let path = project.read(cx).absolute_path(&project_path, cx); - path.zip(Some(location_snapshot)) - })?; - Some(lsp::Location::new( - lsp::Url::from_file_path(path).unwrap(), - range_to_lsp( - location.range.start.to_point_utf16(&location_snapshot) - ..location.range.end.to_point_utf16(&location_snapshot), - ), - )) - }), + location: part.location.map(|(_, location)| location), command: None, }) .collect(), @@ -2299,12 +2217,10 @@ impl LspCommand for InlayHints { ResolveState::Resolved }; - let project = project.clone(); let buffer = buffer.clone(); cx.spawn(|mut cx| async move { InlayHints::lsp_to_project_hint( lsp_hint, - &project, &buffer, server_id, resolve_state, @@ -2359,12 +2275,12 @@ impl LspCommand for InlayHints { _: &mut Project, _: PeerId, buffer_version: &clock::Global, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response .into_iter() - .map(|response_hint| InlayHints::project_to_proto_hint(response_hint, cx)) + .map(|response_hint| InlayHints::project_to_proto_hint(response_hint)) .collect(), version: serialize_version(buffer_version), } @@ -2373,7 +2289,7 @@ impl LspCommand for InlayHints { async fn response_from_proto( self, message: proto::InlayHintsResponse, - project: ModelHandle, + _: ModelHandle, buffer: ModelHandle, mut cx: AsyncAppContext, ) -> anyhow::Result> { @@ -2385,7 +2301,7 @@ impl LspCommand for InlayHints { let mut hints = Vec::new(); for message_hint in message.hints { - hints.push(InlayHints::proto_to_project_hint(message_hint, &project, &mut cx).await?); + hints.push(InlayHints::proto_to_project_hint(message_hint)?); } Ok(hints) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index bb18a41ad4..547c7dcc95 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -369,7 +369,7 @@ pub enum InlayHintLabel { pub struct InlayHintLabelPart { pub value: String, pub tooltip: Option, - pub location: Option, + pub location: Option<(LanguageServerId, lsp::Location)>, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -1708,7 +1708,7 @@ impl Project { } /// LanguageServerName is owned, because it is inserted into a map - fn open_local_buffer_via_lsp( + pub fn open_local_buffer_via_lsp( &mut self, abs_path: lsp::Url, language_server_id: LanguageServerId, @@ -5069,16 +5069,15 @@ impl Project { } let buffer_snapshot = buffer.snapshot(); - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, mut cx| async move { let resolve_task = lang_server.request::( - InlayHints::project_to_lsp_hint(hint, &project, &buffer_snapshot, &cx), + InlayHints::project_to_lsp_hint(hint, &buffer_snapshot), ); let resolved_hint = resolve_task .await .context("inlay hint resolve LSP request")?; let resolved_hint = InlayHints::lsp_to_project_hint( resolved_hint, - &project, &buffer_handle, server_id, ResolveState::Resolved, @@ -5094,19 +5093,16 @@ impl Project { project_id, buffer_id: buffer_handle.read(cx).remote_id(), language_server_id: server_id.0 as u64, - hint: Some(InlayHints::project_to_proto_hint(hint.clone(), cx)), + hint: Some(InlayHints::project_to_proto_hint(hint.clone())), }; - cx.spawn(|project, mut cx| async move { + cx.spawn(|_, _| async move { let response = client .request(request) .await .context("inlay hints proto request")?; match response.hint { - Some(resolved_hint) => { - InlayHints::proto_to_project_hint(resolved_hint, &project, &mut cx) - .await - .context("inlay hints proto resolve response conversion") - } + Some(resolved_hint) => InlayHints::proto_to_project_hint(resolved_hint) + .context("inlay hints proto resolve response conversion"), None => Ok(hint), } }) @@ -7091,8 +7087,7 @@ impl Project { .payload .hint .expect("incorrect protobuf resolve inlay hint message: missing the inlay hint"); - let hint = InlayHints::proto_to_project_hint(proto_hint, &this, &mut cx) - .await + let hint = InlayHints::proto_to_project_hint(proto_hint) .context("resolved proto inlay hint conversion")?; let buffer = this.update(&mut cx, |this, cx| { this.opened_buffers @@ -7111,10 +7106,8 @@ impl Project { }) .await .context("inlay hints fetch")?; - let resolved_hint = cx.read(|cx| InlayHints::project_to_proto_hint(response_hint, cx)); - Ok(proto::ResolveInlayHintResponse { - hint: Some(resolved_hint), + hint: Some(InlayHints::project_to_proto_hint(response_hint)), }) } @@ -7882,7 +7875,7 @@ impl Project { self.language_servers_for_buffer(buffer, cx).next() } - fn language_server_for_buffer( + pub fn language_server_for_buffer( &self, buffer: &Buffer, server_id: LanguageServerId, diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index ce47830af2..3d855c9c69 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -773,7 +773,10 @@ message InlayHintLabelParts { message InlayHintLabelPart { string value = 1; InlayHintLabelPartTooltip tooltip = 2; - Location location = 3; + optional string location_url = 3; + PointUtf16 location_range_start = 4; + PointUtf16 location_range_end = 5; + optional uint64 language_server_id = 6; } message InlayHintTooltip { From 665d86ea737c07abf16b9c9f00e24cb6214e0332 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 01:55:17 +0300 Subject: [PATCH 100/142] Defer navigation target buffer opening --- crates/editor/src/editor.rs | 222 +++++++++++++++------ crates/editor/src/element.rs | 8 +- crates/editor/src/link_go_to_definition.rs | 190 +++++++----------- 3 files changed, 246 insertions(+), 174 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c5ff1f027d..79186d6e8c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -23,7 +23,7 @@ pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{anyhow, Result}; +use anyhow::{anyhow, Context, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; use clock::{Global, ReplicaId}; @@ -60,21 +60,24 @@ use itertools::Itertools; pub use language::{char_kind, CharKind}; use language::{ language_settings::{self, all_language_settings, InlayHintSettings}, - AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, CursorShape, - Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, OffsetRangeExt, - OffsetUtf16, Point, Selection, SelectionGoal, TransactionId, + point_from_lsp, AutoindentMode, BracketPair, Buffer, CodeAction, CodeLabel, Completion, + CursorShape, Diagnostic, DiagnosticSeverity, File, IndentKind, IndentSize, Language, + LanguageServerName, OffsetRangeExt, OffsetUtf16, Point, Selection, SelectionGoal, + TransactionId, }; use link_go_to_definition::{ - hide_link_definition, show_link_definition, DocumentRange, InlayRange, LinkGoToDefinitionState, + hide_link_definition, show_link_definition, DocumentRange, GoToDefinitionLink, InlayRange, + LinkGoToDefinitionState, }; use log::error; +use lsp::LanguageServerId; use multi_buffer::ToOffsetUtf16; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use ordered_float::OrderedFloat; -use project::{FormatTrigger, Location, LocationLink, Project, ProjectPath, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::{seq::SliceRandom, thread_rng}; use scroll::{ autoscroll::Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide, @@ -6551,7 +6554,14 @@ impl Editor { cx.spawn_labeled("Fetching Definition...", |editor, mut cx| async move { let definitions = definitions.await?; editor.update(&mut cx, |editor, cx| { - editor.navigate_to_definitions(definitions, split, cx); + editor.navigate_to_definitions( + definitions + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), + split, + cx, + ); })?; Ok::<(), anyhow::Error>(()) }) @@ -6560,7 +6570,7 @@ impl Editor { pub fn navigate_to_definitions( &mut self, - mut definitions: Vec, + mut definitions: Vec, split: bool, cx: &mut ViewContext, ) { @@ -6571,67 +6581,167 @@ impl Editor { // If there is one definition, just open it directly if definitions.len() == 1 { let definition = definitions.pop().unwrap(); - let range = definition - .target - .range - .to_offset(definition.target.buffer.read(cx)); - - let range = self.range_for_match(&range); - if Some(&definition.target.buffer) == self.buffer.read(cx).as_singleton().as_ref() { - self.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - } else { - cx.window_context().defer(move |cx| { - let target_editor: ViewHandle = workspace.update(cx, |workspace, cx| { - if split { - workspace.split_project_item(definition.target.buffer.clone(), cx) + let target_task = match definition { + GoToDefinitionLink::Text(link) => Task::Ready(Some(Ok(Some(link.target)))), + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + self.compute_target_location(lsp_location, server_id, cx) + } + }; + cx.spawn(|editor, mut cx| async move { + let target = target_task.await.context("target resolution task")?; + if let Some(target) = target { + editor.update(&mut cx, |editor, cx| { + let range = target.range.to_offset(target.buffer.read(cx)); + let range = editor.range_for_match(&range); + if Some(&target.buffer) == editor.buffer.read(cx).as_singleton().as_ref() { + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([range]); + }); } else { - workspace.open_project_item(definition.target.buffer.clone(), cx) + cx.window_context().defer(move |cx| { + let target_editor: ViewHandle = + workspace.update(cx, |workspace, cx| { + if split { + workspace.split_project_item(target.buffer.clone(), cx) + } else { + workspace.open_project_item(target.buffer.clone(), cx) + } + }); + target_editor.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. + pane.update(cx, |pane, _| pane.disable_history()); + target_editor.change_selections( + Some(Autoscroll::fit()), + cx, + |s| { + s.select_ranges([range]); + }, + ); + pane.update(cx, |pane, _| pane.enable_history()); + }); + }); } - }); - target_editor.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. - pane.update(cx, |pane, _| pane.disable_history()); - target_editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([range]); - }); - pane.update(cx, |pane, _| pane.enable_history()); - }); - }); - } + }) + } else { + Ok(()) + } + }) + .detach_and_log_err(cx); } else if !definitions.is_empty() { let replica_id = self.replica_id(cx); - cx.window_context().defer(move |cx| { - let title = definitions - .iter() - .find(|definition| definition.origin.is_some()) - .and_then(|definition| { - definition.origin.as_ref().map(|origin| { - let buffer = origin.buffer.read(cx); - format!( - "Definitions for {}", - buffer - .text_for_range(origin.range.clone()) - .collect::() - ) - }) + cx.spawn(|editor, mut cx| async move { + let (title, location_tasks) = editor + .update(&mut cx, |editor, cx| { + let title = definitions + .iter() + .find_map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + link.origin.as_ref().map(|origin| { + let buffer = origin.buffer.read(cx); + format!( + "Definitions for {}", + buffer + .text_for_range(origin.range.clone()) + .collect::() + ) + }) + } + GoToDefinitionLink::InlayHint(_, _) => None, + }) + .unwrap_or("Definitions".to_string()); + let location_tasks = definitions + .into_iter() + .map(|definition| match definition { + GoToDefinitionLink::Text(link) => { + Task::Ready(Some(Ok(Some(link.target)))) + } + GoToDefinitionLink::InlayHint(lsp_location, server_id) => { + editor.compute_target_location(lsp_location, server_id, cx) + } + }) + .collect::>(); + (title, location_tasks) }) - .unwrap_or("Definitions".to_owned()); - let locations = definitions + .context("location tasks preparation")?; + + let locations = futures::future::join_all(location_tasks) + .await .into_iter() - .map(|definition| definition.target) - .collect(); - workspace.update(cx, |workspace, cx| { + .filter_map(|location| location.transpose()) + .collect::>() + .context("location tasks")?; + workspace.update(&mut cx, |workspace, cx| { Self::open_locations_in_multibuffer( workspace, locations, replica_id, title, split, cx, ) }); - }); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } } + fn compute_target_location( + &self, + lsp_location: lsp::Location, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) -> Task>> { + let Some(project) = self.project.clone() else { + return Task::Ready(Some(Ok(None))); + }; + + cx.spawn(move |editor, mut cx| async move { + let location_task = editor.update(&mut cx, |editor, cx| { + project.update(cx, |project, cx| { + let language_server_name = + editor.buffer.read(cx).as_singleton().and_then(|buffer| { + project + .language_server_for_buffer(buffer.read(cx), server_id, cx) + .map(|(_, lsp_adapter)| { + LanguageServerName(Arc::from(lsp_adapter.name())) + }) + }); + language_server_name.map(|language_server_name| { + project.open_local_buffer_via_lsp( + lsp_location.uri.clone(), + server_id, + language_server_name, + cx, + ) + }) + }) + })?; + let location = match location_task { + Some(task) => Some({ + let target_buffer_handle = task.await.context("open local buffer")?; + let range = { + target_buffer_handle.update(&mut cx, |target_buffer, _| { + let target_start = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.start), + Bias::Left, + ); + let target_end = target_buffer.clip_point_utf16( + point_from_lsp(lsp_location.range.end), + Bias::Left, + ); + target_buffer.anchor_after(target_start) + ..target_buffer.anchor_before(target_end) + }) + }; + Location { + buffer: target_buffer_handle, + range, + } + }), + None => None, + }; + Ok(location) + }) + } + pub fn find_all_references( workspace: &mut Workspace, _: &FindAllReferences, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 5b52cc3c72..684f92a96a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -395,9 +395,7 @@ impl EditorElement { update_go_to_definition_link( editor, - point - .map(GoToDefinitionTrigger::Text) - .unwrap_or(GoToDefinitionTrigger::None), + point.map(GoToDefinitionTrigger::Text), cmd, shift, cx, @@ -468,7 +466,7 @@ impl EditorElement { Some(point) => { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(point), + Some(GoToDefinitionTrigger::Text(point)), cmd, shift, cx, @@ -487,7 +485,7 @@ impl EditorElement { } } } else { - update_go_to_definition_link(editor, GoToDefinitionTrigger::None, cmd, shift, cx); + update_go_to_definition_link(editor, None, cmd, shift, cx); hover_at(editor, None, cx); } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 8d46194f42..ceeda0829a 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -4,15 +4,14 @@ use crate::{ hover_popover::{self, InlayHover}, Anchor, DisplayPoint, Editor, EditorSnapshot, SelectPhase, }; -use anyhow::Context; use gpui::{Task, ViewContext}; -use language::{point_from_lsp, Bias, LanguageServerName, ToOffset}; +use language::{Bias, ToOffset}; use lsp::LanguageServerId; use project::{ - HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, Location, - LocationLink, ResolveState, + HoverBlock, HoverBlockKind, InlayHintLabelPartTooltip, InlayHintTooltip, LocationLink, + ResolveState, }; -use std::{ops::Range, sync::Arc}; +use std::ops::Range; use util::TryFutureExt; #[derive(Debug, Default)] @@ -20,14 +19,19 @@ pub struct LinkGoToDefinitionState { pub last_trigger_point: Option, pub symbol_range: Option, pub kind: Option, - pub definitions: Vec, + pub definitions: Vec, pub task: Option>>, } pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), - None, +} + +#[derive(Debug, Clone)] +pub enum GoToDefinitionLink { + Text(LocationLink), + InlayHint(lsp::Location, LanguageServerId), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -97,7 +101,7 @@ impl TriggerPoint { pub fn update_go_to_definition_link( editor: &mut Editor, - origin: GoToDefinitionTrigger, + origin: Option, cmd_held: bool, shift_held: bool, cx: &mut ViewContext, @@ -107,15 +111,15 @@ pub fn update_go_to_definition_link( // Store new mouse point as an anchor let snapshot = editor.snapshot(cx); let trigger_point = match origin { - GoToDefinitionTrigger::Text(p) => { + Some(GoToDefinitionTrigger::Text(p)) => { Some(TriggerPoint::Text(snapshot.buffer_snapshot.anchor_before( p.to_offset(&snapshot.display_snapshot, Bias::Left), ))) } - GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id) => { + Some(GoToDefinitionTrigger::InlayHint(p, lsp_location, language_server_id)) => { Some(TriggerPoint::InlayHint(p, lsp_location, language_server_id)) } - GoToDefinitionTrigger::None => None, + None => None, }; // If the new point is the same as the previously stored one, return early @@ -287,7 +291,7 @@ pub fn update_inlay_link_and_hover_points( go_to_definition_updated = true; update_go_to_definition_link( editor, - GoToDefinitionTrigger::InlayHint( + Some(GoToDefinitionTrigger::InlayHint( InlayRange { inlay_position: hovered_hint.position, highlight_start: part_range.start, @@ -295,7 +299,7 @@ pub fn update_inlay_link_and_hover_points( }, location, language_server_id, - ), + )), cmd_held, shift_held, cx, @@ -311,13 +315,7 @@ pub fn update_inlay_link_and_hover_points( } if !go_to_definition_updated { - update_go_to_definition_link( - editor, - GoToDefinitionTrigger::None, - cmd_held, - shift_held, - cx, - ); + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); } if !hover_updated { hover_popover::hover_at(editor, None, cx); @@ -412,63 +410,20 @@ pub fn show_link_definition( DocumentRange::Text(start..end) }) }), - definition_result, - ) - }) - } - TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => { - let target = match project.update(&mut cx, |project, cx| { - let language_server_name = project - .language_server_for_buffer(buffer.read(cx), *server_id, cx) - .map(|(_, lsp_adapter)| { - LanguageServerName(Arc::from(lsp_adapter.name())) - }); - language_server_name.map(|language_server_name| { - project.open_local_buffer_via_lsp( - lsp_location.uri.clone(), - *server_id, - language_server_name, - cx, - ) - }) - }) { - Some(task) => Some({ - let target_buffer_handle = task.await.context("open local buffer")?; - let range = cx.read(|cx| { - let target_buffer = target_buffer_handle.read(cx); - let target_start = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.start), - Bias::Left, - ); - let target_end = target_buffer.clip_point_utf16( - point_from_lsp(lsp_location.range.end), - Bias::Left, - ); - target_buffer.anchor_after(target_start) - ..target_buffer.anchor_before(target_end) - }); - Location { - buffer: target_buffer_handle, - range, - } - }), - None => None, - }; - - target.map(|target| { - ( - Some(DocumentRange::Inlay(trigger_source.clone())), - vec![LocationLink { - origin: Some(Location { - buffer: buffer.clone(), - range: trigger_source.inlay_position.text_anchor - ..trigger_source.inlay_position.text_anchor, - }), - target, - }], + definition_result + .into_iter() + .map(GoToDefinitionLink::Text) + .collect(), ) }) } + TriggerPoint::InlayHint(trigger_source, lsp_location, server_id) => Some(( + Some(DocumentRange::Inlay(*trigger_source)), + vec![GoToDefinitionLink::InlayHint( + lsp_location.clone(), + *server_id, + )], + )), }; this.update(&mut cx, |this, cx| { @@ -488,43 +443,52 @@ pub fn show_link_definition( // the current location. let any_definition_does_not_contain_current_location = definitions.iter().any(|definition| { - let target = &definition.target; - if target.buffer == buffer { - let range = &target.range; - // Expand range by one character as lsp definition ranges include positions adjacent - // but not contained by the symbol range - let start = buffer_snapshot.clip_offset( - range.start.to_offset(&buffer_snapshot).saturating_sub(1), - Bias::Left, - ); - let end = buffer_snapshot.clip_offset( - range.end.to_offset(&buffer_snapshot) + 1, - Bias::Right, - ); - let offset = buffer_position.to_offset(&buffer_snapshot); - !(start <= offset && end >= offset) - } else { - true + match &definition { + GoToDefinitionLink::Text(link) => { + if link.target.buffer == buffer { + let range = &link.target.range; + // Expand range by one character as lsp definition ranges include positions adjacent + // but not contained by the symbol range + let start = buffer_snapshot.clip_offset( + range + .start + .to_offset(&buffer_snapshot) + .saturating_sub(1), + Bias::Left, + ); + let end = buffer_snapshot.clip_offset( + range.end.to_offset(&buffer_snapshot) + 1, + Bias::Right, + ); + let offset = buffer_position.to_offset(&buffer_snapshot); + !(start <= offset && end >= offset) + } else { + true + } + } + GoToDefinitionLink::InlayHint(_, _) => true, } }); if any_definition_does_not_contain_current_location { // Highlight symbol using theme link definition highlight style let style = theme::current(cx).editor.link_definition; - let highlight_range = symbol_range.unwrap_or_else(|| match trigger_point { - TriggerPoint::Text(trigger_anchor) => { - let snapshot = &snapshot.buffer_snapshot; - // If no symbol range returned from language server, use the surrounding word. - let (offset_range, _) = snapshot.surrounding_word(trigger_anchor); - DocumentRange::Text( - snapshot.anchor_before(offset_range.start) - ..snapshot.anchor_after(offset_range.end), - ) - } - TriggerPoint::InlayHint(inlay_coordinates, _, _) => { - DocumentRange::Inlay(inlay_coordinates) - } - }); + let highlight_range = + symbol_range.unwrap_or_else(|| match &trigger_point { + TriggerPoint::Text(trigger_anchor) => { + let snapshot = &snapshot.buffer_snapshot; + // If no symbol range returned from language server, use the surrounding word. + let (offset_range, _) = + snapshot.surrounding_word(*trigger_anchor); + DocumentRange::Text( + snapshot.anchor_before(offset_range.start) + ..snapshot.anchor_after(offset_range.end), + ) + } + TriggerPoint::InlayHint(inlay_coordinates, _, _) => { + DocumentRange::Inlay(*inlay_coordinates) + } + }); match highlight_range { DocumentRange::Text(text_range) => this @@ -689,7 +653,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, true, cx, @@ -801,7 +765,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -841,7 +805,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -869,7 +833,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -892,7 +856,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), false, false, cx, @@ -957,7 +921,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -977,7 +941,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, @@ -1079,7 +1043,7 @@ mod tests { cx.update_editor(|editor, cx| { update_go_to_definition_link( editor, - GoToDefinitionTrigger::Text(hover_point), + Some(GoToDefinitionTrigger::Text(hover_point)), true, false, cx, From b2b091879057bf9be983bb8d305ef36ed64387f3 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:13:36 +0300 Subject: [PATCH 101/142] Consider padding during hint highlight range mapping --- crates/editor/src/hover_popover.rs | 15 +++++++++++++-- crates/editor/src/link_go_to_definition.rs | 15 +++++++++++++-- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 3ce936ae82..89e8d8e246 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,19 +57,30 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, + padding_left: bool, + padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - for part in label_parts { + let last_label_part_index = label_parts.len() - 1; + for (i, part) in label_parts.into_iter().enumerate() { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - return Some((part, part_start..InlayOffset(part_start.0 + part_len))); + let mut part_end = InlayOffset(part_start.0 + part_len); + if padding_left { + part_start.0 += 1; + part_end.0 += 1; + } + if padding_right && i == last_label_part_index { + part_end.0 -= 1; + } + return Some((part, part_start..part_end)); } } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index ceeda0829a..2ada17de06 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -218,6 +218,15 @@ pub fn update_inlay_link_and_hover_points( ResolveState::Resolved => { match cached_hint.label { project::InlayHintLabel::String(_) => { + let mut highlight_start = hint_start_offset; + let mut highlight_end = hint_end_offset; + if cached_hint.padding_left { + highlight_start.0 += 1; + highlight_end.0 += 1; + } + if cached_hint.padding_right { + highlight_end.0 -= 1; + } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -238,8 +247,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start: hint_start_offset, - highlight_end: hint_end_offset, + highlight_start, + highlight_end, }, }, cx, @@ -251,6 +260,8 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, + cached_hint.padding_left, + cached_hint.padding_right, hint_start_offset..hint_end_offset, hovered_offset, ) From e6c4802488f39ebc3f5ce6c3d400fba54c038342 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 02:44:19 +0300 Subject: [PATCH 102/142] Properly clip request offsets --- crates/editor/src/inlay_hint_cache.rs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a4b91e826d..0067c832d3 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -632,8 +633,8 @@ fn determine_query_ranges( return None; } else { vec![ - buffer.anchor_before(excerpt_visible_range.start) - ..buffer.anchor_after(excerpt_visible_range.end), + buffer.anchor_before(snapshot.clip_offset(excerpt_visible_range.start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(excerpt_visible_range.end, Bias::Right)), ] }; @@ -651,8 +652,8 @@ fn determine_query_ranges( .min(full_excerpt_range_end_offset) .min(buffer.len()); vec![ - buffer.anchor_before(after_visible_range_start) - ..buffer.anchor_after(after_range_end_offset), + buffer.anchor_before(snapshot.clip_offset(after_visible_range_start, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(after_range_end_offset, Bias::Right)), ] }; @@ -668,8 +669,8 @@ fn determine_query_ranges( .saturating_sub(excerpt_visible_len) .max(full_excerpt_range_start_offset); vec![ - buffer.anchor_before(before_range_start_offset) - ..buffer.anchor_after(before_visible_range_end), + buffer.anchor_before(snapshot.clip_offset(before_range_start_offset, Bias::Left)) + ..buffer.anchor_after(snapshot.clip_offset(before_visible_range_end, Bias::Right)), ] }; From 81c64647e82777a6286bbf7534b7aa41d47beaf5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:00:53 +0300 Subject: [PATCH 103/142] Fix the test --- crates/editor/src/link_go_to_definition.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 2ada17de06..280993dd26 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -975,6 +975,7 @@ mod tests { // the cached location instead Ok(Some(lsp::GotoDefinitionResponse::Link(vec![]))) }); + cx.foreground().run_until_parked(); cx.assert_editor_state(indoc! {" fn «testˇ»() { do_work(); } fn do_work() { test(); } From 74565ed0b86afa85fb8e26cb35219182ecf667b7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 25 Aug 2023 17:00:53 -0700 Subject: [PATCH 104/142] Add feature flags handling to the client, rewrite staff mode to a trait extension style --- Cargo.lock | 28 +++---- Cargo.toml | 2 +- crates/channel/Cargo.toml | 2 +- crates/client/Cargo.toml | 2 +- crates/client/src/telemetry.rs | 2 - crates/client/src/user.rs | 35 ++++---- crates/collab_ui/Cargo.toml | 2 +- crates/collab_ui/src/collab_panel.rs | 35 ++++---- .../{staff_mode => feature_flags}/Cargo.toml | 4 +- crates/feature_flags/src/feature_flags.rs | 79 +++++++++++++++++++ crates/rpc/src/rpc.rs | 2 +- crates/settings/Cargo.toml | 2 +- crates/staff_mode/src/staff_mode.rs | 36 --------- crates/theme_selector/Cargo.toml | 2 +- crates/theme_selector/src/theme_selector.rs | 4 +- crates/zed/Cargo.toml | 2 +- crates/zed/src/languages/json.rs | 4 +- crates/zed/src/main.rs | 7 +- 18 files changed, 143 insertions(+), 107 deletions(-) rename crates/{staff_mode => feature_flags}/Cargo.toml (71%) create mode 100644 crates/feature_flags/src/feature_flags.rs delete mode 100644 crates/staff_mode/src/staff_mode.rs diff --git a/Cargo.lock b/Cargo.lock index 8197f883c0..bfdb1b6092 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1200,6 +1200,7 @@ dependencies = [ "client", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1215,7 +1216,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1374,6 +1374,7 @@ dependencies = [ "async-tungstenite", "collections", "db", + "feature_flags", "futures 0.3.28", "gpui", "image", @@ -1388,7 +1389,6 @@ dependencies = [ "serde_derive", "settings", "smol", - "staff_mode", "sum_tree", "tempfile", "text", @@ -1528,6 +1528,7 @@ dependencies = [ "context_menu", "db", "editor", + "feature_flags", "feedback", "futures 0.3.28", "fuzzy", @@ -1543,7 +1544,6 @@ dependencies = [ "serde", "serde_derive", "settings", - "staff_mode", "theme", "theme_selector", "util", @@ -2529,6 +2529,14 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" +[[package]] +name = "feature_flags" +version = "0.1.0" +dependencies = [ + "anyhow", + "gpui", +] + [[package]] name = "feedback" version = "0.1.0" @@ -6834,6 +6842,7 @@ version = "0.1.0" dependencies = [ "anyhow", "collections", + "feature_flags", "fs", "futures 0.3.28", "gpui", @@ -6849,7 +6858,6 @@ dependencies = [ "serde_json_lenient", "smallvec", "sqlez", - "staff_mode", "toml 0.5.11", "tree-sitter", "tree-sitter-json 0.19.0", @@ -7284,14 +7292,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "staff_mode" -version = "0.1.0" -dependencies = [ - "anyhow", - "gpui", -] - [[package]] name = "static_assertions" version = "1.1.0" @@ -7672,6 +7672,7 @@ name = "theme_selector" version = "0.1.0" dependencies = [ "editor", + "feature_flags", "fs", "fuzzy", "gpui", @@ -7681,7 +7682,6 @@ dependencies = [ "postage", "settings", "smol", - "staff_mode", "theme", "util", "workspace", @@ -9726,6 +9726,7 @@ dependencies = [ "diagnostics", "editor", "env_logger 0.9.3", + "feature_flags", "feedback", "file_finder", "fs", @@ -9772,7 +9773,6 @@ dependencies = [ "simplelog", "smallvec", "smol", - "staff_mode", "sum_tree", "tempdir", "terminal_view", diff --git a/Cargo.toml b/Cargo.toml index 0fb8f0b6b7..5938ecb402 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -62,7 +62,7 @@ members = [ "crates/snippet", "crates/sqlez", "crates/sqlez_macros", - "crates/staff_mode", + "crates/feature_flags", "crates/sum_tree", "crates/terminal", "crates/text", diff --git a/crates/channel/Cargo.toml b/crates/channel/Cargo.toml index 0978462a1a..c2191fdfa3 100644 --- a/crates/channel/Cargo.toml +++ b/crates/channel/Cargo.toml @@ -21,7 +21,7 @@ rpc = { path = "../rpc" } text = { path = "../text" } language = { path = "../language" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 64d8f02c8a..e3038e5bcc 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -19,7 +19,7 @@ util = { path = "../util" } rpc = { path = "../rpc" } text = { path = "../text" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } anyhow.workspace = true diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 48886377ba..9cc5d13af0 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -135,8 +135,6 @@ impl Telemetry { } } - /// This method takes the entire TelemetrySettings struct in order to force client code - /// to pull the struct out of the settings global. Do not remove! pub fn set_authenticated_user_info( self: &Arc, metrics_id: Option, diff --git a/crates/client/src/user.rs b/crates/client/src/user.rs index 1dc384da17..5f13aa40ac 100644 --- a/crates/client/src/user.rs +++ b/crates/client/src/user.rs @@ -1,11 +1,11 @@ use super::{proto, Client, Status, TypedEnvelope}; use anyhow::{anyhow, Context, Result}; use collections::{hash_map::Entry, HashMap, HashSet}; +use feature_flags::FeatureFlagAppExt; use futures::{channel::mpsc, future, AsyncReadExt, Future, StreamExt}; use gpui::{AsyncAppContext, Entity, ImageData, ModelContext, ModelHandle, Task}; use postage::{sink::Sink, watch}; use rpc::proto::{RequestMessage, UsersResponse}; -use staff_mode::StaffMode; use std::sync::{Arc, Weak}; use util::http::HttpClient; use util::TryFutureExt as _; @@ -145,26 +145,23 @@ impl UserStore { let fetch_metrics_id = client.request(proto::GetPrivateUserInfo {}).log_err(); let (user, info) = futures::join!(fetch_user, fetch_metrics_id); - cx.read(|cx| { - client.telemetry.set_authenticated_user_info( - info.as_ref().map(|info| info.metrics_id.clone()), - info.as_ref().map(|info| info.staff).unwrap_or(false), - cx, - ) - }); - cx.update(|cx| { - cx.update_default_global(|staff_mode: &mut StaffMode, _| { - if !staff_mode.0 { - *staff_mode = StaffMode( - info.as_ref() - .map(|info| info.staff) - .unwrap_or_default(), - ) - } - () + if let Some(info) = info { + cx.update(|cx| { + cx.update_flags(info.staff, info.flags); + client.telemetry.set_authenticated_user_info( + Some(info.metrics_id.clone()), + info.staff, + cx, + ) }); - }); + } else { + cx.read(|cx| { + client + .telemetry + .set_authenticated_user_info(None, false, cx) + }); + } current_user_tx.send(user).await.ok(); diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 1ecb4b8422..da32308558 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -40,7 +40,7 @@ picker = { path = "../picker" } project = { path = "../project" } recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } -staff_mode = {path = "../staff_mode"} +feature_flags = {path = "../feature_flags"} theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } vcs_menu = { path = "../vcs_menu" } diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 411a3a2598..0593bfcb1f 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -9,6 +9,8 @@ use client::{proto::PeerId, Client, Contact, User, UserStore}; use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; use editor::{Cancel, Editor}; + +use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use futures::StreamExt; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ @@ -33,7 +35,6 @@ use panel_settings::{CollaborationPanelDockPosition, CollaborationPanelSettings} use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; -use staff_mode::StaffMode; use std::{borrow::Cow, mem, sync::Arc}; use theme::{components::ComponentExt, IconButton}; use util::{iife, ResultExt, TryFutureExt}; @@ -182,9 +183,9 @@ pub struct CollabPanel { } #[derive(Serialize, Deserialize)] -struct SerializedChannelsPanel { +struct SerializedCollabPanel { width: Option, - collapsed_channels: Vec, + collapsed_channels: Option>, } #[derive(Debug)] @@ -472,9 +473,10 @@ impl CollabPanel { })); this.subscriptions .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); - this.subscriptions.push( - cx.observe_global::(move |this, cx| this.update_entries(true, cx)), - ); + this.subscriptions + .push(cx.observe_flag::(move |_, this, cx| { + this.update_entries(true, cx) + })); this.subscriptions.push(cx.subscribe( &this.channel_store, |this, _channel_store, e, cx| match e { @@ -510,7 +512,7 @@ impl CollabPanel { .log_err() .flatten() { - Some(serde_json::from_str::(&panel)?) + Some(serde_json::from_str::(&panel)?) } else { None }; @@ -520,7 +522,9 @@ impl CollabPanel { if let Some(serialized_panel) = serialized_panel { panel.update(cx, |panel, cx| { panel.width = serialized_panel.width; - panel.collapsed_channels = serialized_panel.collapsed_channels; + panel.collapsed_channels = serialized_panel + .collapsed_channels + .unwrap_or_else(|| Vec::new()); cx.notify(); }); } @@ -537,9 +541,9 @@ impl CollabPanel { KEY_VALUE_STORE .write_kvp( COLLABORATION_PANEL_KEY.into(), - serde_json::to_string(&SerializedChannelsPanel { + serde_json::to_string(&SerializedCollabPanel { width, - collapsed_channels, + collapsed_channels: Some(collapsed_channels), })?, ) .await?; @@ -672,7 +676,8 @@ impl CollabPanel { } let mut request_entries = Vec::new(); - if self.include_channels_section(cx) { + + if cx.has_flag::() { self.entries.push(ListEntry::Header(Section::Channels, 0)); if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { @@ -1909,14 +1914,6 @@ impl CollabPanel { .into_any() } - fn include_channels_section(&self, cx: &AppContext) -> bool { - if cx.has_global::() { - cx.global::().0 - } else { - false - } - } - fn deploy_channel_context_menu( &mut self, position: Option, diff --git a/crates/staff_mode/Cargo.toml b/crates/feature_flags/Cargo.toml similarity index 71% rename from crates/staff_mode/Cargo.toml rename to crates/feature_flags/Cargo.toml index 2193bd11b1..af273fe403 100644 --- a/crates/staff_mode/Cargo.toml +++ b/crates/feature_flags/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "staff_mode" +name = "feature_flags" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/staff_mode.rs" +path = "src/feature_flags.rs" [dependencies] gpui = { path = "../gpui" } diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs new file mode 100644 index 0000000000..d14152b04c --- /dev/null +++ b/crates/feature_flags/src/feature_flags.rs @@ -0,0 +1,79 @@ +use gpui::{AppContext, Subscription, ViewContext}; + +#[derive(Default)] +struct FeatureFlags { + flags: Vec, + staff: bool, +} + +impl FeatureFlags { + fn has_flag(&self, flag: &str) -> bool { + self.staff || self.flags.iter().find(|f| f.as_str() == flag).is_some() + } +} + +pub trait FeatureFlag { + const NAME: &'static str; +} + +pub enum ChannelsAlpha {} + +impl FeatureFlag for ChannelsAlpha { + const NAME: &'static str = "channels_alpha"; +} + +pub trait FeatureFlagViewExt { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static; +} + +impl FeatureFlagViewExt for ViewContext<'_, '_, V> { + fn observe_flag(&mut self, callback: F) -> Subscription + where + F: Fn(bool, &mut V, &mut ViewContext) + 'static, + { + self.observe_global::(move |v, cx| { + let feature_flags = cx.global::(); + callback(feature_flags.has_flag(::NAME), v, cx); + }) + } +} + +pub trait FeatureFlagAppExt { + fn update_flags(&mut self, staff: bool, flags: Vec); + fn set_staff(&mut self, staff: bool); + fn has_flag(&self) -> bool; + fn is_staff(&self) -> bool; +} + +impl FeatureFlagAppExt for AppContext { + fn update_flags(&mut self, staff: bool, flags: Vec) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + feature_flags.flags = flags; + }) + } + + fn set_staff(&mut self, staff: bool) { + self.update_default_global::(|feature_flags, _| { + feature_flags.staff = staff; + }) + } + + fn has_flag(&self) -> bool { + if self.has_global::() { + self.global::().has_flag(T::NAME) + } else { + false + } + } + + fn is_staff(&self) -> bool { + if self.has_global::() { + return self.global::().staff; + } else { + false + } + } +} diff --git a/crates/rpc/src/rpc.rs b/crates/rpc/src/rpc.rs index d64cbae929..bc9dd6f80b 100644 --- a/crates/rpc/src/rpc.rs +++ b/crates/rpc/src/rpc.rs @@ -6,4 +6,4 @@ pub use conn::Connection; pub use peer::*; mod macros; -pub const PROTOCOL_VERSION: u32 = 62; +pub const PROTOCOL_VERSION: u32 = 61; diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index 06b81a0c61..f89b80902d 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -16,7 +16,7 @@ collections = { path = "../collections" } gpui = { path = "../gpui" } sqlez = { path = "../sqlez" } fs = { path = "../fs" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } util = { path = "../util" } anyhow.workspace = true diff --git a/crates/staff_mode/src/staff_mode.rs b/crates/staff_mode/src/staff_mode.rs deleted file mode 100644 index 49fadc0b2c..0000000000 --- a/crates/staff_mode/src/staff_mode.rs +++ /dev/null @@ -1,36 +0,0 @@ -use gpui::AppContext; - -#[derive(Debug, Default)] -pub struct StaffMode(pub bool); - -impl std::ops::Deref for StaffMode { - type Target = bool; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -/// Despite what the type system requires me to tell you, the init function will only be called a once -/// as soon as we know that the staff mode is enabled. -pub fn staff_mode(cx: &mut AppContext, mut init: F) { - if **cx.default_global::() { - init(cx) - } else { - let mut once = Some(()); - cx.observe_global::(move |cx| { - if **cx.global::() && once.take().is_some() { - init(cx); - } - }) - .detach(); - } -} - -/// Immediately checks and runs the init function if the staff mode is not enabled. -/// This is only included for symettry with staff_mode() above -pub fn not_staff_mode(cx: &mut AppContext, init: F) { - if !**cx.default_global::() { - init(cx) - } -} diff --git a/crates/theme_selector/Cargo.toml b/crates/theme_selector/Cargo.toml index 377f64aad6..7e97d39186 100644 --- a/crates/theme_selector/Cargo.toml +++ b/crates/theme_selector/Cargo.toml @@ -16,7 +16,7 @@ gpui = { path = "../gpui" } picker = { path = "../picker" } theme = { path = "../theme" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } workspace = { path = "../workspace" } util = { path = "../util" } log.workspace = true diff --git a/crates/theme_selector/src/theme_selector.rs b/crates/theme_selector/src/theme_selector.rs index 5510005733..1969b0256a 100644 --- a/crates/theme_selector/src/theme_selector.rs +++ b/crates/theme_selector/src/theme_selector.rs @@ -1,9 +1,9 @@ +use feature_flags::FeatureFlagAppExt; use fs::Fs; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; use gpui::{actions, elements::*, AnyElement, AppContext, Element, MouseState, ViewContext}; use picker::{Picker, PickerDelegate, PickerEvent}; use settings::{update_settings_file, SettingsStore}; -use staff_mode::StaffMode; use std::sync::Arc; use theme::{Theme, ThemeMeta, ThemeRegistry, ThemeSettings}; use util::ResultExt; @@ -54,7 +54,7 @@ impl ThemeSelectorDelegate { fn new(fs: Arc, cx: &mut ViewContext) -> Self { let original_theme = theme::current(cx).clone(); - let staff_mode = **cx.default_global::(); + let staff_mode = cx.is_staff(); let registry = cx.global::>(); let mut theme_names = registry.list(staff_mode).collect::>(); theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 92900f84cb..2a97764647 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -60,7 +60,7 @@ quick_action_bar = { path = "../quick_action_bar" } recent_projects = { path = "../recent_projects" } rpc = { path = "../rpc" } settings = { path = "../settings" } -staff_mode = { path = "../staff_mode" } +feature_flags = { path = "../feature_flags" } sum_tree = { path = "../sum_tree" } text = { path = "../text" } terminal_view = { path = "../terminal_view" } diff --git a/crates/zed/src/languages/json.rs b/crates/zed/src/languages/json.rs index b7e4ab4ba7..61d19ce5b6 100644 --- a/crates/zed/src/languages/json.rs +++ b/crates/zed/src/languages/json.rs @@ -1,6 +1,7 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use collections::HashMap; +use feature_flags::FeatureFlagAppExt; use futures::{future::BoxFuture, FutureExt, StreamExt}; use gpui::AppContext; use language::{LanguageRegistry, LanguageServerName, LspAdapter, LspAdapterDelegate}; @@ -9,7 +10,6 @@ use node_runtime::NodeRuntime; use serde_json::json; use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore}; use smol::fs; -use staff_mode::StaffMode; use std::{ any::Any, ffi::OsString, @@ -104,7 +104,7 @@ impl LspAdapter for JsonLspAdapter { cx: &mut AppContext, ) -> Option> { let action_names = cx.all_action_names().collect::>(); - let staff_mode = cx.default_global::().0; + let staff_mode = cx.is_staff(); let language_names = &self.languages.language_names(); let settings_schema = cx.global::().json_schema( &SettingsJsonSchemaParams { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3b1fccb927..da726eef65 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -53,8 +53,6 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use fs::RealFs; -#[cfg(debug_assertions)] -use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::AppState; use zed::{ @@ -122,7 +120,10 @@ fn main() { cx.set_global(*RELEASE_CHANNEL); #[cfg(debug_assertions)] - cx.set_global(StaffMode(true)); + { + use feature_flags::FeatureFlagAppExt; + cx.set_staff(true); + } let mut store = SettingsStore::default(); store From 2b007930a9ed71c5534e771096ecaa10294d9edd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 03:52:52 +0300 Subject: [PATCH 105/142] Remove query ranges for failed inlay hint requests --- crates/editor/src/inlay_hint_cache.rs | 122 +++++++++++++++++++++----- 1 file changed, 100 insertions(+), 22 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 0067c832d3..1526e16129 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -108,17 +108,23 @@ impl TasksForRanges { updated_ranges.before_visible = updated_ranges .before_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.visible = updated_ranges .visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges.after_visible = updated_ranges .after_visible .into_iter() - .flat_map(|query_range| self.remove_cached_ranges(buffer_snapshot, query_range)) + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) .collect(); updated_ranges } @@ -134,7 +140,7 @@ impl TasksForRanges { } } - fn remove_cached_ranges( + fn remove_cached_ranges_from_query( &mut self, buffer_snapshot: &BufferSnapshot, query_range: Range, @@ -196,6 +202,52 @@ impl TasksForRanges { ranges_to_query } + + fn remove_from_cached_ranges( + &mut self, + buffer: &BufferSnapshot, + range_to_remove: Range, + ) { + self.sorted_ranges = self + .sorted_ranges + .drain(..) + .filter_map(|mut cached_range| { + if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() + || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + { + Some(vec![cached_range]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + { + None + } else if range_to_remove + .start + .cmp(&cached_range.start, buffer) + .is_ge() + && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + { + Some(vec![ + cached_range.start..range_to_remove.start, + range_to_remove.end..cached_range.end, + ]) + } else if cached_range + .start + .cmp(&range_to_remove.start, buffer) + .is_ge() + { + cached_range.start = range_to_remove.end; + Some(vec![cached_range]) + } else { + cached_range.end = range_to_remove.start; + Some(vec![cached_range]) + } + }) + .flatten() + .collect(); + } } impl InlayHintCache { @@ -692,7 +744,8 @@ fn new_update_task( cached_excerpt_hints: Option>>, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { - cx.spawn(|editor, cx| async move { + cx.spawn(|editor, mut cx| async move { + let closure_cx = cx.clone(); let fetch_and_update_hints = |invalidate, range| { fetch_and_update_hints( editor.clone(), @@ -703,37 +756,62 @@ fn new_update_task( query, invalidate, range, - cx.clone(), + closure_cx.clone(), ) }; - let visible_range_update_results = - future::join_all(query_ranges.visible.into_iter().map(|visible_range| { - fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) - })) - .await; - for result in visible_range_update_results { + let visible_range_update_results = future::join_all(query_ranges.visible.into_iter().map( + |visible_range| async move { + ( + visible_range.clone(), + fetch_and_update_hints(query.invalidate.should_invalidate(), visible_range) + .await, + ) + }, + )) + .await; + + let hint_delay = cx.background().timer(Duration::from_millis( + INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, + )); + + let mut query_range_failed = |range: Range, e: anyhow::Error| { + error!("inlay hint update task for range {range:?} failed: {e:#}"); + editor + .update(&mut cx, |editor, _| { + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + } + }) + .ok() + }; + + for (range, result) in visible_range_update_results { if let Err(e) = result { - error!("visible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } - cx.background() - .timer(Duration::from_millis( - INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, - )) - .await; - + hint_delay.await; let invisible_range_update_results = future::join_all( query_ranges .before_visible .into_iter() .chain(query_ranges.after_visible.into_iter()) - .map(|invisible_range| fetch_and_update_hints(false, invisible_range)), + .map(|invisible_range| async move { + ( + invisible_range.clone(), + fetch_and_update_hints(false, invisible_range).await, + ) + }), ) .await; - for result in invisible_range_update_results { + for (range, result) in invisible_range_update_results { if let Err(e) = result { - error!("invisible range inlay hint update task failed: {e:#}"); + query_range_failed(range, e); } } }) From e6fb909d8990ee43f4f3b4381e0d3d8b4615bc7e Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:06:50 +0300 Subject: [PATCH 106/142] Limit LSP non-invalidating queries --- crates/editor/src/inlay_hint_cache.rs | 40 +++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1526e16129..4e040a8a7e 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -19,6 +19,7 @@ use project::{InlayHint, ResolveState}; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use smol::lock::Semaphore; use sum_tree::Bias; use text::ToOffset; use util::post_inc; @@ -29,6 +30,7 @@ pub struct InlayHintCache { version: usize, pub(super) enabled: bool, update_tasks: HashMap, + lsp_request_limiter: Arc, } #[derive(Debug)] @@ -258,6 +260,7 @@ impl InlayHintCache { hints: HashMap::default(), update_tasks: HashMap::default(), version: 0, + lsp_request_limiter: Arc::new(Semaphore::new(MAX_CONCURRENT_LSP_REQUESTS)), } } @@ -629,6 +632,7 @@ fn spawn_new_update_tasks( buffer_snapshot.clone(), Arc::clone(&visible_hints), cached_excerpt_hints, + Arc::clone(&editor.inlay_hint_cache.lsp_request_limiter), cx, ) }; @@ -733,6 +737,7 @@ fn determine_query_ranges( }) } +const MAX_CONCURRENT_LSP_REQUESTS: usize = 5; const INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS: u64 = 400; fn new_update_task( @@ -742,6 +747,7 @@ fn new_update_task( buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, + lsp_request_limiter: Arc, cx: &mut ViewContext<'_, '_, Editor>, ) -> Task<()> { cx.spawn(|editor, mut cx| async move { @@ -756,6 +762,7 @@ fn new_update_task( query, invalidate, range, + Arc::clone(&lsp_request_limiter), closure_cx.clone(), ) }; @@ -826,10 +833,41 @@ async fn fetch_and_update_hints( query: ExcerptQuery, invalidate: bool, fetch_range: Range, + lsp_request_limiter: Arc, mut cx: gpui::AsyncAppContext, ) -> anyhow::Result<()> { + let (lsp_request_guard, got_throttled) = if query.invalidate.should_invalidate() { + (None, false) + } else { + match lsp_request_limiter.try_acquire() { + Some(guard) => (Some(guard), false), + None => (Some(lsp_request_limiter.acquire().await), true), + } + }; let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { + if got_throttled { + if let Some((_, _, current_visible_range)) = editor + .excerpt_visible_offsets(None, cx) + .remove(&query.excerpt_id) + { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + if !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + { + return None; + } + } + } editor .buffer() .read(cx) @@ -847,6 +885,8 @@ async fn fetch_and_update_hints( Some(task) => task.await.context("inlay hint fetch task")?, None => return Ok(()), }; + drop(lsp_request_guard); + let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx From 3fc48fc2774785b8bbc96a04646d1b079cf9a9da Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 13:46:55 +0300 Subject: [PATCH 107/142] Log LSP inlay hint path --- crates/editor/src/editor.rs | 13 ++++++++ crates/editor/src/inlay_hint_cache.rs | 44 ++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 79186d6e8c..8f0c8c9f22 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1252,6 +1252,17 @@ enum InlayHintRefreshReason { BufferEdited(HashSet>), RefreshRequested, } +impl InlayHintRefreshReason { + fn description(&self) -> &'static str { + match self { + InlayHintRefreshReason::Toggle(_) => "toggle", + InlayHintRefreshReason::SettingsChange(_) => "settings change", + InlayHintRefreshReason::NewLinesShown => "new lines shown", + InlayHintRefreshReason::BufferEdited(_) => "buffer edited", + InlayHintRefreshReason::RefreshRequested => "refresh requested", + } + } +} impl Editor { pub fn single_line( @@ -2741,6 +2752,7 @@ impl Editor { return; } + let reason_description = reason.description(); let (invalidate_cache, required_languages) = match reason { InlayHintRefreshReason::Toggle(enabled) => { self.inlay_hint_cache.enabled = enabled; @@ -2790,6 +2802,7 @@ impl Editor { to_remove, to_insert, }) = self.inlay_hint_cache.spawn_hint_refresh( + reason_description, self.excerpt_visible_offsets(required_languages.as_ref(), cx), invalidate_cache, cx, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 4e040a8a7e..e16971fbbe 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -13,7 +13,6 @@ use clock::Global; use futures::future; use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; -use log::error; use parking_lot::RwLock; use project::{InlayHint, ResolveState}; @@ -21,7 +20,7 @@ use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; use smol::lock::Semaphore; use sum_tree::Bias; -use text::ToOffset; +use text::{ToOffset, ToPoint}; use util::post_inc; pub struct InlayHintCache { @@ -74,6 +73,7 @@ struct ExcerptQuery { excerpt_id: ExcerptId, cache_version: usize, invalidate: InvalidationStrategy, + reason: &'static str, } impl InvalidationStrategy { @@ -317,6 +317,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, @@ -345,7 +346,14 @@ impl InlayHintCache { cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { - spawn_new_update_tasks(editor, excerpts_to_query, invalidate, cache_version, cx) + spawn_new_update_tasks( + editor, + reason, + excerpts_to_query, + invalidate, + cache_version, + cx, + ) }) .ok(); }) @@ -568,6 +576,7 @@ impl InlayHintCache { fn spawn_new_update_tasks( editor: &mut Editor, + reason: &'static str, excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, update_cache_version: usize, @@ -622,6 +631,7 @@ fn spawn_new_update_tasks( excerpt_id, cache_version: update_cache_version, invalidate, + reason, }; let new_update_task = |query_ranges| { @@ -782,7 +792,7 @@ fn new_update_task( )); let mut query_range_failed = |range: Range, e: anyhow::Error| { - error!("inlay hint update task for range {range:?} failed: {e:#}"); + log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { if let Some(task_ranges) = editor @@ -844,6 +854,8 @@ async fn fetch_and_update_hints( None => (Some(lsp_request_limiter.acquire().await), true), } }; + let fetch_range_to_log = + fetch_range.start.to_point(&buffer_snapshot)..fetch_range.end.to_point(&buffer_snapshot); let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { @@ -882,10 +894,25 @@ async fn fetch_and_update_hints( .ok() .flatten(); let new_hints = match inlay_hints_fetch_task { - Some(task) => task.await.context("inlay hint fetch task")?, + Some(fetch_task) => { + log::debug!( + "Fetching inlay hints for range {fetch_range_to_log:?}, reason: {query_reason}, invalidate: {invalidate}", + query_reason = query.reason, + ); + log::trace!( + "Currently visible hints: {visible_hints:?}, cached hints present: {}", + cached_excerpt_hints.is_some(), + ); + fetch_task.await.context("inlay hint fetch task")? + } None => return Ok(()), }; drop(lsp_request_guard); + log::debug!( + "Fetched {} hints for range {fetch_range_to_log:?}", + new_hints.len() + ); + log::trace!("Fetched hints: {new_hints:?}"); let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); @@ -904,6 +931,13 @@ async fn fetch_and_update_hints( }) .await; if let Some(new_update) = new_update { + log::info!( + "Applying update for range {fetch_range_to_log:?}: remove from editor: {}, remove from cache: {}, add to cache: {}", + new_update.remove_from_visible.len(), + new_update.remove_from_cache.len(), + new_update.add_to_cache.len() + ); + log::trace!("New update: {new_update:?}"); editor .update(&mut cx, |editor, cx| { apply_hint_update( From 48659d3b3cdd997d3e94ebcb2c4e77e2b6b86211 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:13:21 +0300 Subject: [PATCH 108/142] Treat multibuffer edit events properly Miltibuffer emits edit events even if it only got an excerpt added/removed/etc. Separate buffer edits and trigger hint invalidation refresh for them only, also trigger hint new lines refresh on excerpt addition events. --- crates/editor/src/editor.rs | 67 +++++++++++++++++-------------- crates/editor/src/multi_buffer.rs | 36 +++++++++++++---- 2 files changed, 65 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f0c8c9f22..5e9d604de5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7896,7 +7896,9 @@ impl Editor { cx: &mut ViewContext, ) { match event { - multi_buffer::Event::Edited => { + multi_buffer::Event::Edited { + sigleton_buffer_edited, + } => { self.refresh_active_diagnostics(cx); self.refresh_code_actions(cx); if self.has_active_copilot_suggestion(cx) { @@ -7904,30 +7906,32 @@ impl Editor { } cx.emit(Event::BufferEdited); - if let Some(project) = &self.project { - let project = project.read(cx); - let languages_affected = multibuffer - .read(cx) - .all_buffers() - .into_iter() - .filter_map(|buffer| { - let buffer = buffer.read(cx); - let language = buffer.language()?; - if project.is_local() - && project.language_servers_for_buffer(buffer, cx).count() == 0 - { - None - } else { - Some(language) - } - }) - .cloned() - .collect::>(); - if !languages_affected.is_empty() { - self.refresh_inlay_hints( - InlayHintRefreshReason::BufferEdited(languages_affected), - cx, - ); + if *sigleton_buffer_edited { + if let Some(project) = &self.project { + let project = project.read(cx); + let languages_affected = multibuffer + .read(cx) + .all_buffers() + .into_iter() + .filter_map(|buffer| { + let buffer = buffer.read(cx); + let language = buffer.language()?; + if project.is_local() + && project.language_servers_for_buffer(buffer, cx).count() == 0 + { + None + } else { + Some(language) + } + }) + .cloned() + .collect::>(); + if !languages_affected.is_empty() { + self.refresh_inlay_hints( + InlayHintRefreshReason::BufferEdited(languages_affected), + cx, + ); + } } } } @@ -7935,11 +7939,14 @@ impl Editor { buffer, predecessor, excerpts, - } => cx.emit(Event::ExcerptsAdded { - buffer: buffer.clone(), - predecessor: *predecessor, - excerpts: excerpts.clone(), - }), + } => { + cx.emit(Event::ExcerptsAdded { + buffer: buffer.clone(), + predecessor: *predecessor, + excerpts: excerpts.clone(), + }); + self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); + } multi_buffer::Event::ExcerptsRemoved { ids } => { cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 5c0d8b641c..e84cfb85aa 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -67,7 +67,9 @@ pub enum Event { ExcerptsEdited { ids: Vec, }, - Edited, + Edited { + sigleton_buffer_edited: bool, + }, Reloaded, DiffBaseChanged, LanguageChanged, @@ -1022,7 +1024,9 @@ impl MultiBuffer { old: edit_start..edit_start, new: edit_start..edit_end, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsAdded { buffer, predecessor: prev_excerpt_id, @@ -1046,7 +1050,9 @@ impl MultiBuffer { old: 0..prev_len, new: 0..0, }]); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1254,7 +1260,9 @@ impl MultiBuffer { } self.subscriptions.publish_mut(edits); - cx.emit(Event::Edited); + cx.emit(Event::Edited { + sigleton_buffer_edited: false, + }); cx.emit(Event::ExcerptsRemoved { ids }); cx.notify(); } @@ -1315,7 +1323,9 @@ impl MultiBuffer { cx: &mut ModelContext, ) { cx.emit(match event { - language::Event::Edited => Event::Edited, + language::Event::Edited => Event::Edited { + sigleton_buffer_edited: true, + }, language::Event::DirtyChanged => Event::DirtyChanged, language::Event::Saved => Event::Saved, language::Event::FileHandleChanged => Event::FileHandleChanged, @@ -4078,7 +4088,7 @@ mod tests { multibuffer.update(cx, |_, cx| { let events = events.clone(); cx.subscribe(&multibuffer, move |_, _, event, _| { - if let Event::Edited = event { + if let Event::Edited { .. } = event { events.borrow_mut().push(event.clone()) } }) @@ -4133,7 +4143,17 @@ mod tests { // Adding excerpts emits an edited event. assert_eq!( events.borrow().as_slice(), - &[Event::Edited, Event::Edited, Event::Edited] + &[ + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + }, + Event::Edited { + sigleton_buffer_edited: false + } + ] ); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4312,7 +4332,7 @@ mod tests { excerpts, } => follower.insert_excerpts_with_ids_after(predecessor, buffer, excerpts, cx), Event::ExcerptsRemoved { ids } => follower.remove_excerpts(ids, cx), - Event::Edited => { + Event::Edited { .. } => { *follower_edit_event_count.borrow_mut() += 1; } _ => {} From 9bdf76f4453739d9593e2b50e6f0614f7affadc1 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:42:20 +0300 Subject: [PATCH 109/142] Properly handle hover-less areas hover --- crates/editor/src/link_go_to_definition.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 280993dd26..84fd9b5cc1 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -173,6 +173,8 @@ pub fn update_inlay_link_and_hover_points( } else { None }; + let mut go_to_definition_updated = false; + let mut hover_updated = false; if let Some(hovered_offset) = hovered_offset { let buffer_snapshot = editor.buffer().read(cx).snapshot(cx); let previous_valid_anchor = buffer_snapshot.anchor_at( @@ -183,9 +185,6 @@ pub fn update_inlay_link_and_hover_points( point_for_position.next_valid.to_point(snapshot), Bias::Right, ); - - let mut go_to_definition_updated = false; - let mut hover_updated = false; if let Some(hovered_hint) = editor .visible_inlay_hints(cx) .into_iter() @@ -324,13 +323,13 @@ pub fn update_inlay_link_and_hover_points( } } } + } - if !go_to_definition_updated { - update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); - } - if !hover_updated { - hover_popover::hover_at(editor, None, cx); - } + if !go_to_definition_updated { + update_go_to_definition_link(editor, None, cmd_held, shift_held, cx); + } + if !hover_updated { + hover_popover::hover_at(editor, None, cx); } } From 2a42a08f46116d402286388571403339153c1b63 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 14:47:38 +0300 Subject: [PATCH 110/142] Invalidate skipped throttled hint fetch tasks' ranges --- crates/editor/src/inlay_hint_cache.rs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e16971fbbe..ff0d0082ae 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -208,7 +208,7 @@ impl TasksForRanges { fn remove_from_cached_ranges( &mut self, buffer: &BufferSnapshot, - range_to_remove: Range, + range_to_remove: &Range, ) { self.sorted_ranges = self .sorted_ranges @@ -791,7 +791,7 @@ fn new_update_task( INVISIBLE_RANGES_HINTS_REQUEST_DELAY_MILLIS, )); - let mut query_range_failed = |range: Range, e: anyhow::Error| { + let mut query_range_failed = |range: &Range, e: anyhow::Error| { log::error!("inlay hint update task for range {range:?} failed: {e:#}"); editor .update(&mut cx, |editor, _| { @@ -800,7 +800,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, range); + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); } }) .ok() @@ -808,7 +808,7 @@ fn new_update_task( for (range, result) in visible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } @@ -828,7 +828,7 @@ fn new_update_task( .await; for (range, result) in invisible_range_update_results { if let Err(e) = result { - query_range_failed(range, e); + query_range_failed(&range, e); } } }) @@ -876,6 +876,14 @@ async fn fetch_and_update_hints( && !double_visible_range .contains(&fetch_range.end.to_offset(&buffer_snapshot)) { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) + { + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + } return None; } } From 84284099e201532c2e6f51be2efc5a1dce3ca255 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:04:48 +0300 Subject: [PATCH 111/142] Properly handle padding when highlighting inlay hints --- crates/editor/src/hover_popover.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 89e8d8e246..f5f663660d 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -65,8 +65,7 @@ pub fn find_hovered_hint_part( if hovered_offset >= hint_range.start && hovered_offset <= hint_range.end { let mut hovered_character = (hovered_offset - hint_range.start).0; let mut part_start = hint_range.start; - let last_label_part_index = label_parts.len() - 1; - for (i, part) in label_parts.into_iter().enumerate() { + for part in label_parts { let part_len = part.value.chars().count(); if hovered_character >= part_len { hovered_character -= part_len; @@ -77,8 +76,9 @@ pub fn find_hovered_hint_part( part_start.0 += 1; part_end.0 += 1; } - if padding_right && i == last_label_part_index { - part_end.0 -= 1; + if padding_right { + part_start.0 += 1; + part_end.0 += 1; } return Some((part, part_start..part_end)); } From f8a8b998cebbd486e94fc5b7c95d83fe36d05c96 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 15:21:45 +0300 Subject: [PATCH 112/142] Properly react on excerpts drop --- crates/editor/src/editor.rs | 21 ++++++++++++++++----- crates/editor/src/inlay_hint_cache.rs | 22 ++++++++++++++++++++-- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5e9d604de5..01aa59574d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1251,15 +1251,17 @@ enum InlayHintRefreshReason { NewLinesShown, BufferEdited(HashSet>), RefreshRequested, + ExcerptsRemoved(Vec), } impl InlayHintRefreshReason { fn description(&self) -> &'static str { match self { - InlayHintRefreshReason::Toggle(_) => "toggle", - InlayHintRefreshReason::SettingsChange(_) => "settings change", - InlayHintRefreshReason::NewLinesShown => "new lines shown", - InlayHintRefreshReason::BufferEdited(_) => "buffer edited", - InlayHintRefreshReason::RefreshRequested => "refresh requested", + Self::Toggle(_) => "toggle", + Self::SettingsChange(_) => "settings change", + Self::NewLinesShown => "new lines shown", + Self::BufferEdited(_) => "buffer edited", + Self::RefreshRequested => "refresh requested", + Self::ExcerptsRemoved(_) => "excerpts removed", } } } @@ -2789,6 +2791,14 @@ impl Editor { ControlFlow::Continue(()) => (InvalidationStrategy::RefreshRequested, None), } } + InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { + let InlaySplice { + to_remove, + to_insert, + } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); + self.splice_inlay_hints(to_remove, to_insert, cx); + return; + } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), InlayHintRefreshReason::BufferEdited(buffer_languages) => { (InvalidationStrategy::BufferEdited, Some(buffer_languages)) @@ -7948,6 +7958,7 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::NewLinesShown, cx); } multi_buffer::Event::ExcerptsRemoved { ids } => { + self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) } multi_buffer::Event::Reparsed => cx.emit(Event::Reparsed), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index ff0d0082ae..1ed35ef629 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -474,6 +474,24 @@ impl InlayHintCache { } } + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + let mut to_remove = Vec::new(); + for excerpt_to_remove in excerpts_removed { + self.update_tasks.remove(&excerpt_to_remove); + if let Some(cached_hints) = self.hints.remove(&excerpt_to_remove) { + let cached_hints = cached_hints.read(); + to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); + } + } + if !to_remove.is_empty() { + self.version += 1; + } + InlaySplice { + to_remove, + to_insert: Vec::new(), + } + } + pub fn clear(&mut self) { self.version += 1; self.update_tasks.clear(); @@ -2956,7 +2974,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 2, + 3, "Excerpt removal should trigger a cache update" ); }); @@ -2984,7 +3002,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, + 4, "Settings change should trigger a cache update" ); }); From 73937876b6fb3fca890805a941b203ef1adfe7dd Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:12:04 +0300 Subject: [PATCH 113/142] Properly omit throttled hint queries --- crates/editor/src/inlay_hint_cache.rs | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1ed35ef629..2adf3caaf1 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -877,33 +877,33 @@ async fn fetch_and_update_hints( let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { if got_throttled { - if let Some((_, _, current_visible_range)) = editor - .excerpt_visible_offsets(None, cx) - .remove(&query.excerpt_id) - { - let visible_offset_length = current_visible_range.len(); - let double_visible_range = current_visible_range - .start - .saturating_sub(visible_offset_length) - ..current_visible_range - .end - .saturating_add(visible_offset_length) - .min(buffer_snapshot.len()); - if !double_visible_range - .contains(&fetch_range.start.to_offset(&buffer_snapshot)) - && !double_visible_range - .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + let query_not_around_visible_range = match editor.excerpt_visible_offsets(None, cx).remove(&query.excerpt_id) { + Some((_, _, current_visible_range)) => { + let visible_offset_length = current_visible_range.len(); + let double_visible_range = current_visible_range + .start + .saturating_sub(visible_offset_length) + ..current_visible_range + .end + .saturating_add(visible_offset_length) + .min(buffer_snapshot.len()); + !double_visible_range + .contains(&fetch_range.start.to_offset(&buffer_snapshot)) + && !double_visible_range + .contains(&fetch_range.end.to_offset(&buffer_snapshot)) + }, + None => true, + }; + if query_not_around_visible_range { + log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); + if let Some(task_ranges) = editor + .inlay_hint_cache + .update_tasks + .get_mut(&query.excerpt_id) { - log::trace!("Fetching inlay hints for range {fetch_range_to_log:?} got throttled and fell off the current visible range, skipping."); - if let Some(task_ranges) = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); - } - return None; + task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); } + return None; } } editor From 5cf51211b64b0c70e442402939d08e5bee4e17c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sat, 26 Aug 2023 21:37:34 +0300 Subject: [PATCH 114/142] Use better names, simplify --- crates/editor/src/editor.rs | 8 +- crates/editor/src/inlay_hint_cache.rs | 119 ++++++++++++-------------- 2 files changed, 58 insertions(+), 69 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 01aa59574d..2d934a1dc8 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2792,11 +2792,13 @@ impl Editor { } } InlayHintRefreshReason::ExcerptsRemoved(excerpts_removed) => { - let InlaySplice { + if let Some(InlaySplice { to_remove, to_insert, - } = self.inlay_hint_cache.remove_excerpts(excerpts_removed); - self.splice_inlay_hints(to_remove, to_insert, cx); + }) = self.inlay_hint_cache.remove_excerpts(excerpts_removed) + { + self.splice_inlay_hints(to_remove, to_insert, cx); + } return; } InlayHintRefreshReason::NewLinesShown => (InvalidationStrategy::None, None), diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2adf3caaf1..34898aea2e 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -104,37 +104,34 @@ impl TasksForRanges { invalidate: InvalidationStrategy, spawn_task: impl FnOnce(QueryRanges) -> Task<()>, ) { - let query_ranges = match invalidate { - InvalidationStrategy::None => { - let mut updated_ranges = query_ranges; - updated_ranges.before_visible = updated_ranges - .before_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.visible = updated_ranges - .visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges.after_visible = updated_ranges - .after_visible - .into_iter() - .flat_map(|query_range| { - self.remove_cached_ranges_from_query(buffer_snapshot, query_range) - }) - .collect(); - updated_ranges - } - InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { - self.tasks.clear(); - self.sorted_ranges.clear(); - query_ranges - } + let query_ranges = if invalidate.should_invalidate() { + self.tasks.clear(); + self.sorted_ranges.clear(); + query_ranges + } else { + let mut non_cached_query_ranges = query_ranges; + non_cached_query_ranges.before_visible = non_cached_query_ranges + .before_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.visible = non_cached_query_ranges + .visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges.after_visible = non_cached_query_ranges + .after_visible + .into_iter() + .flat_map(|query_range| { + self.remove_cached_ranges_from_query(buffer_snapshot, query_range) + }) + .collect(); + non_cached_query_ranges }; if !query_ranges.is_empty() { @@ -205,45 +202,31 @@ impl TasksForRanges { ranges_to_query } - fn remove_from_cached_ranges( - &mut self, - buffer: &BufferSnapshot, - range_to_remove: &Range, - ) { + fn invalidate_range(&mut self, buffer: &BufferSnapshot, range: &Range) { self.sorted_ranges = self .sorted_ranges .drain(..) .filter_map(|mut cached_range| { - if cached_range.start.cmp(&range_to_remove.end, buffer).is_gt() - || cached_range.end.cmp(&range_to_remove.start, buffer).is_lt() + if cached_range.start.cmp(&range.end, buffer).is_gt() + || cached_range.end.cmp(&range.start, buffer).is_lt() { Some(vec![cached_range]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - && cached_range.end.cmp(&range_to_remove.end, buffer).is_le() + } else if cached_range.start.cmp(&range.start, buffer).is_ge() + && cached_range.end.cmp(&range.end, buffer).is_le() { None - } else if range_to_remove - .start - .cmp(&cached_range.start, buffer) - .is_ge() - && range_to_remove.end.cmp(&cached_range.end, buffer).is_le() + } else if range.start.cmp(&cached_range.start, buffer).is_ge() + && range.end.cmp(&cached_range.end, buffer).is_le() { Some(vec![ - cached_range.start..range_to_remove.start, - range_to_remove.end..cached_range.end, + cached_range.start..range.start, + range.end..cached_range.end, ]) - } else if cached_range - .start - .cmp(&range_to_remove.start, buffer) - .is_ge() - { - cached_range.start = range_to_remove.end; + } else if cached_range.start.cmp(&range.start, buffer).is_ge() { + cached_range.start = range.end; Some(vec![cached_range]) } else { - cached_range.end = range_to_remove.start; + cached_range.end = range.start; Some(vec![cached_range]) } }) @@ -474,7 +457,7 @@ impl InlayHintCache { } } - pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> InlaySplice { + pub fn remove_excerpts(&mut self, excerpts_removed: Vec) -> Option { let mut to_remove = Vec::new(); for excerpt_to_remove in excerpts_removed { self.update_tasks.remove(&excerpt_to_remove); @@ -483,17 +466,21 @@ impl InlayHintCache { to_remove.extend(cached_hints.hints.iter().map(|(id, _)| *id)); } } - if !to_remove.is_empty() { + if to_remove.is_empty() { + None + } else { self.version += 1; - } - InlaySplice { - to_remove, - to_insert: Vec::new(), + Some(InlaySplice { + to_remove, + to_insert: Vec::new(), + }) } } pub fn clear(&mut self) { - self.version += 1; + if !self.update_tasks.is_empty() || !self.hints.is_empty() { + self.version += 1; + } self.update_tasks.clear(); self.hints.clear(); } @@ -818,7 +805,7 @@ fn new_update_task( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &range); + task_ranges.invalidate_range(&buffer_snapshot, &range); } }) .ok() @@ -901,7 +888,7 @@ async fn fetch_and_update_hints( .update_tasks .get_mut(&query.excerpt_id) { - task_ranges.remove_from_cached_ranges(&buffer_snapshot, &fetch_range); + task_ranges.invalidate_range(&buffer_snapshot, &fetch_range); } return None; } From dad64edde107d7d6bd7d8f835c299ddf1d43d5df Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 15:14:45 +0300 Subject: [PATCH 115/142] Better highlight hint ranges --- crates/editor/src/hover_popover.rs | 14 ++-------- crates/editor/src/link_go_to_definition.rs | 32 ++++++++++------------ 2 files changed, 16 insertions(+), 30 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index f5f663660d..4eda65fc12 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -57,8 +57,6 @@ pub struct InlayHover { pub fn find_hovered_hint_part( label_parts: Vec, - padding_left: bool, - padding_right: bool, hint_range: Range, hovered_offset: InlayOffset, ) -> Option<(InlayHintLabelPart, Range)> { @@ -67,19 +65,11 @@ pub fn find_hovered_hint_part( let mut part_start = hint_range.start; for part in label_parts { let part_len = part.value.chars().count(); - if hovered_character >= part_len { + if hovered_character > part_len { hovered_character -= part_len; part_start.0 += part_len; } else { - let mut part_end = InlayOffset(part_start.0 + part_len); - if padding_left { - part_start.0 += 1; - part_end.0 += 1; - } - if padding_right { - part_start.0 += 1; - part_end.0 += 1; - } + let part_end = InlayOffset(part_start.0 + part_len); return Some((part, part_start..part_end)); } } diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 84fd9b5cc1..a36c673eae 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -165,11 +165,8 @@ pub fn update_inlay_link_and_hover_points( snapshot.display_point_to_inlay_offset(point_for_position.previous_valid, Bias::Left); let hint_end_offset = snapshot.display_point_to_inlay_offset(point_for_position.next_valid, Bias::Right); - let offset_overshoot = point_for_position.column_overshoot_after_line_end as usize; - let hovered_offset = if offset_overshoot == 0 { + let hovered_offset = if point_for_position.column_overshoot_after_line_end == 0 { Some(snapshot.display_point_to_inlay_offset(point_for_position.exact_unclipped, Bias::Left)) - } else if (hint_end_offset - hint_start_offset).0 >= offset_overshoot { - Some(InlayOffset(hint_start_offset.0 + offset_overshoot)) } else { None }; @@ -215,17 +212,18 @@ pub fn update_inlay_link_and_hover_points( } } ResolveState::Resolved => { + let mut actual_hint_start = hint_start_offset; + let mut actual_hint_end = hint_end_offset; + if cached_hint.padding_left { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } + if cached_hint.padding_right { + actual_hint_start.0 += 1; + actual_hint_end.0 += 1; + } match cached_hint.label { project::InlayHintLabel::String(_) => { - let mut highlight_start = hint_start_offset; - let mut highlight_end = hint_end_offset; - if cached_hint.padding_left { - highlight_start.0 += 1; - highlight_end.0 += 1; - } - if cached_hint.padding_right { - highlight_end.0 -= 1; - } if let Some(tooltip) = cached_hint.tooltip { hover_popover::hover_at_inlay( editor, @@ -246,8 +244,8 @@ pub fn update_inlay_link_and_hover_points( triggered_from: hovered_offset, range: InlayRange { inlay_position: hovered_hint.position, - highlight_start, - highlight_end, + highlight_start: actual_hint_start, + highlight_end: actual_hint_end, }, }, cx, @@ -259,9 +257,7 @@ pub fn update_inlay_link_and_hover_points( if let Some((hovered_hint_part, part_range)) = hover_popover::find_hovered_hint_part( label_parts, - cached_hint.padding_left, - cached_hint.padding_right, - hint_start_offset..hint_end_offset, + actual_hint_start..actual_hint_end, hovered_offset, ) { From 693e91f3351d64e85f9f4fe7de466a109445d139 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 18:23:40 +0300 Subject: [PATCH 116/142] Properly compare previous hover trigger point when hover changes --- crates/editor/src/link_go_to_definition.rs | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index a36c673eae..909c07880b 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -23,6 +23,7 @@ pub struct LinkGoToDefinitionState { pub task: Option>>, } +#[derive(Debug)] pub enum GoToDefinitionTrigger { Text(DisplayPoint), InlayHint(InlayRange, lsp::Location, LanguageServerId), @@ -81,7 +82,7 @@ impl TriggerPoint { fn anchor(&self) -> &Anchor { match self { TriggerPoint::Text(anchor) => anchor, - TriggerPoint::InlayHint(coordinates, _, _) => &coordinates.inlay_position, + TriggerPoint::InlayHint(range, _, _) => &range.inlay_position, } } @@ -127,11 +128,22 @@ pub fn update_go_to_definition_link( &trigger_point, &editor.link_go_to_definition_state.last_trigger_point, ) { - if a.anchor() - .cmp(b.anchor(), &snapshot.buffer_snapshot) - .is_eq() - { - return; + match (a, b) { + (TriggerPoint::Text(anchor_a), TriggerPoint::Text(anchor_b)) => { + if anchor_a.cmp(anchor_b, &snapshot.buffer_snapshot).is_eq() { + return; + } + } + (TriggerPoint::InlayHint(range_a, _, _), TriggerPoint::InlayHint(range_b, _, _)) => { + if range_a + .inlay_position + .cmp(&range_b.inlay_position, &snapshot.buffer_snapshot) + .is_eq() + { + return; + } + } + _ => {} } } From 81e70905bb20ded519ed987c0948289a541a3a19 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:12:32 +0300 Subject: [PATCH 117/142] Do not allow cmd+click in invalid inlay context --- crates/editor/src/element.rs | 2 +- crates/editor/src/link_go_to_definition.rs | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 684f92a96a..62f4c8c806 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2755,7 +2755,7 @@ impl PointForPosition { } } - fn as_valid(&self) -> Option { + pub fn as_valid(&self) -> Option { if self.previous_valid == self.exact_unclipped && self.next_valid == self.exact_unclipped { Some(self.previous_valid) } else { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 909c07880b..9ca39f9b30 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -596,9 +596,11 @@ fn go_to_fetched_definition_of_kind( cx, ); - match kind { - LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), - LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + if point.as_valid().is_some() { + match kind { + LinkDefinitionKind::Symbol => editor.go_to_definition(&Default::default(), cx), + LinkDefinitionKind::Type => editor.go_to_type_definition(&Default::default(), cx), + } } } } From 38da2a587a74eeb37e6a715a37ea03df82a430e2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Sun, 27 Aug 2023 19:41:15 +0300 Subject: [PATCH 118/142] Fix the tests --- crates/editor/src/hover_popover.rs | 40 +++++++++++++++------- crates/editor/src/link_go_to_definition.rs | 16 ++++++--- 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 4eda65fc12..2f278ce262 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -1376,13 +1376,21 @@ mod tests { .unwrap(); let new_type_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(new_type_label).unwrap() + new_type_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(new_type_label).unwrap() - + new_type_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { @@ -1504,13 +1512,21 @@ mod tests { let struct_hint_part_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + + (entire_hint_label.find(struct_label).unwrap() + struct_label.len() / 2) + as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (entire_hint_label.find(struct_label).unwrap() - + struct_label.len() / 2) - as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); cx.update_editor(|editor, cx| { diff --git a/crates/editor/src/link_go_to_definition.rs b/crates/editor/src/link_go_to_definition.rs index 9ca39f9b30..1f9a3aab73 100644 --- a/crates/editor/src/link_go_to_definition.rs +++ b/crates/editor/src/link_go_to_definition.rs @@ -1170,11 +1170,19 @@ mod tests { .unwrap(); let hint_hover_position = cx.update_editor(|editor, cx| { let snapshot = editor.snapshot(cx); + let previous_valid = inlay_range.start.to_display_point(&snapshot); + let next_valid = inlay_range.end.to_display_point(&snapshot); + assert_eq!(previous_valid.row(), next_valid.row()); + assert!(previous_valid.column() < next_valid.column()); + let exact_unclipped = DisplayPoint::new( + previous_valid.row(), + previous_valid.column() + (hint_label.len() / 2) as u32, + ); PointForPosition { - previous_valid: inlay_range.start.to_display_point(&snapshot), - next_valid: inlay_range.end.to_display_point(&snapshot), - exact_unclipped: inlay_range.end.to_display_point(&snapshot), - column_overshoot_after_line_end: (hint_label.len() / 2) as u32, + previous_valid, + next_valid, + exact_unclipped, + column_overshoot_after_line_end: 0, } }); // Press cmd to trigger highlight From 3bfe78b1dfb0b991d16de1a0be4ac9ec46b741f8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 00:27:59 +0300 Subject: [PATCH 119/142] Use proper property names for inlay hint resolve capabilities --- crates/lsp/src/lsp.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 7cba039552..d49dafff2f 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -435,7 +435,13 @@ impl LanguageServer { }), inlay_hint: Some(InlayHintClientCapabilities { resolve_support: Some(InlayHintResolveClientCapabilities { - properties: vec!["textEdits".to_string(), "tooltip".to_string()], + properties: vec![ + "textEdits".to_string(), + "tooltip".to_string(), + "label.tooltip".to_string(), + "label.location".to_string(), + "label.command".to_string(), + ], }), dynamic_registration: Some(false), }), From 506ec01df3112b9d392ff56cb716d8e831306af9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 28 Aug 2023 11:19:57 +0300 Subject: [PATCH 120/142] Allow `[` and `]` symbols in terminal links ` ./src/pages/[[...slug]].tsx` is a valid file path in macOs and Linux, and should be available for cmd-hover-click in terminal. --- crates/terminal/src/terminal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index e28e0ca5c1..83ba056485 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -78,7 +78,7 @@ lazy_static! { // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings. static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap(); - static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.:/@\-~]+"#).unwrap(); + static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap(); } ///Upward flowing events, for changing the title and such From 07b9c6c302d89b3e23f61e9943b2c6b3ab113d99 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Aug 2023 11:51:50 +0200 Subject: [PATCH 121/142] language: Make Buffer::new take an explicit ID (#2900) See Linear description for the full explanation of the issue. This PR is mostly a mechanical change, except for the one case where we do pass in an explicit `next_id` instead of `model_id` in project.rs. Release Notes: - Fixed a bug where some results were not reported in project search in presence of unnamed buffers. --- crates/ai/src/assistant.rs | 8 +-- crates/copilot/src/copilot.rs | 4 +- crates/editor/src/display_map.rs | 9 ++- crates/editor/src/editor.rs | 6 +- crates/editor/src/editor_tests.rs | 41 +++++++----- crates/editor/src/movement.rs | 3 +- crates/editor/src/multi_buffer.rs | 47 ++++++++------ crates/language/src/buffer.rs | 8 +-- crates/language/src/buffer_tests.rs | 90 +++++++++++++++----------- crates/language_tools/src/lsp_log.rs | 8 ++- crates/project/src/project.rs | 4 +- crates/search/src/buffer_search.rs | 6 +- crates/zed/src/languages/c.rs | 2 +- crates/zed/src/languages/python.rs | 2 +- crates/zed/src/languages/rust.rs | 2 +- crates/zed/src/languages/typescript.rs | 5 +- 16 files changed, 141 insertions(+), 104 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 2699bf40a0..3c561b0e03 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -855,14 +855,14 @@ impl Conversation { ) -> Self { let markdown = language_registry.language_for_name("Markdown"); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); buffer.set_language_registry(language_registry); cx.spawn_weak(|buffer, mut cx| async move { let markdown = markdown.await?; let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) @@ -944,7 +944,7 @@ impl Conversation { let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, saved_conversation.text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -958,7 +958,7 @@ impl Conversation { let buffer = buffer .upgrade(&cx) .ok_or_else(|| anyhow!("buffer was dropped"))?; - buffer.update(&mut cx, |buffer, cx| { + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) }); anyhow::Ok(()) diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index ab2d861190..427134894f 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -980,7 +980,7 @@ mod tests { deterministic.forbid_parking(); let (copilot, mut lsp) = Copilot::fake(cx); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "Hello", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Hello")); let buffer_1_uri: lsp::Url = format!("buffer://{}", buffer_1.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_1, cx)); assert_eq!( @@ -996,7 +996,7 @@ mod tests { } ); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "Goodbye", cx)); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "Goodbye")); let buffer_2_uri: lsp::Url = format!("buffer://{}", buffer_2.id()).parse().unwrap(); copilot.update(cx, |copilot, cx| copilot.register_buffer(&buffer_2, cx)); assert_eq!( diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 611866bcad..5698ccede1 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -1362,7 +1362,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |s| s.defaults.tab_size = Some(2.try_into().unwrap()))); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1451,7 +1452,8 @@ pub mod tests { cx.update(|cx| init_test(cx, |_| {})); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); @@ -1523,7 +1525,8 @@ pub mod tests { let (text, highlighted_ranges) = marked_text_ranges(r#"constˇ «a»: B = "c «d»""#, false); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); buffer.condition(cx, |buf, _| !buf.is_parsing()).await; let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2d934a1dc8..38b9100729 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1271,7 +1271,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::SingleLine, buffer, None, field_editor_style, cx) } @@ -1280,7 +1280,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) } @@ -1290,7 +1290,7 @@ impl Editor { field_editor_style: Option>, cx: &mut ViewContext, ) -> Self { - let buffer = cx.add_model(|cx| Buffer::new(0, String::new(), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); Self::new( EditorMode::AutoHeight { max_lines }, diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 25a5d45282..fbc8a0b235 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -42,7 +42,7 @@ fn test_edit_events(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "123456", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "123456"); buffer.set_group_interval(Duration::from_secs(1)); buffer }); @@ -174,7 +174,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { init_test(cx, |_| {}); let mut now = Instant::now(); - let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); + let buffer = cx.add_model(|cx| language::Buffer::new(0, cx.model_id() as u64, "123456")); let group_interval = buffer.read_with(cx, |buffer, _| buffer.transaction_group_interval()); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx @@ -247,7 +247,7 @@ fn test_ime_composition(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.add_model(|cx| { - let mut buffer = language::Buffer::new(0, "abcde", cx); + let mut buffer = language::Buffer::new(0, cx.model_id() as u64, "abcde"); // Ensure automatic grouping doesn't occur. buffer.set_group_interval(Duration::ZERO); buffer @@ -2281,10 +2281,12 @@ fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) { None, )); - let toml_buffer = - cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx).with_language(toml_language, cx)); + let toml_buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n").with_language(toml_language, cx) + }); let rust_buffer = cx.add_model(|cx| { - Buffer::new(0, "const c: usize = 3;\n", cx).with_language(rust_language, cx) + Buffer::new(0, cx.model_id() as u64, "const c: usize = 3;\n") + .with_language(rust_language, cx) }); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -3754,7 +3756,8 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -3917,7 +3920,8 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -4480,7 +4484,8 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -4628,7 +4633,8 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor @@ -5834,7 +5840,7 @@ async fn test_toggle_block_comment(cx: &mut gpui::TestAppContext) { fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -5918,7 +5924,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { primary: None, } }); - let buffer = cx.add_model(|cx| Buffer::new(0, initial_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, initial_text)); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts(buffer, excerpt_ranges, cx); @@ -5976,7 +5982,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { fn test_refresh_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6063,7 +6069,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(3, 4, 'a'), cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(3, 4, 'a'))); let mut excerpt1_id = None; let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); @@ -6160,7 +6166,8 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { "{{} }\n", // ); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let view = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) @@ -7160,8 +7167,8 @@ async fn test_copilot_multibuffer( let (copilot, copilot_lsp) = Copilot::fake(cx); cx.update(|cx| cx.set_global(copilot)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "a = 1\nb = 2\n", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "c = 3\nd = 4\n", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a = 1\nb = 2\n")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "c = 3\nd = 4\n")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 6b3032b2a3..def6340e38 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -756,7 +756,8 @@ mod tests { .select_font(family_id, &Default::default()) .unwrap(); - let buffer = cx.add_model(|cx| Buffer::new(0, "abc\ndefg\nhijkl\nmn", cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abc\ndefg\nhijkl\nmn")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index e84cfb85aa..3ace5adbc7 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1570,7 +1570,7 @@ impl MultiBuffer { #[cfg(any(test, feature = "test-support"))] impl MultiBuffer { pub fn build_simple(text: &str, cx: &mut gpui::AppContext) -> ModelHandle { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); cx.add_model(|cx| Self::singleton(buffer, cx)) } @@ -1580,7 +1580,7 @@ impl MultiBuffer { ) -> ModelHandle { let multi = cx.add_model(|_| Self::new(0)); for (text, ranges) in excerpts { - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let excerpt_ranges = ranges.into_iter().map(|range| ExcerptRange { context: range, primary: None, @@ -1672,7 +1672,7 @@ impl MultiBuffer { if excerpt_ids.is_empty() || (rng.gen() && excerpt_ids.len() < max_excerpts) { let buffer_handle = if rng.gen() || self.buffers.borrow().is_empty() { let text = RandomCharIter::new(&mut *rng).take(10).collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, text, cx))); + buffers.push(cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text))); let buffer = buffers.last().unwrap().read(cx); log::info!( "Creating new buffer {} with text: {:?}", @@ -4022,7 +4022,8 @@ mod tests { #[gpui::test] fn test_singleton(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let snapshot = multibuffer.read(cx).snapshot(cx); @@ -4049,7 +4050,7 @@ mod tests { #[gpui::test] fn test_remote(cx: &mut AppContext) { - let host_buffer = cx.add_model(|cx| Buffer::new(0, "a", cx)); + let host_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "a")); let guest_buffer = cx.add_model(|cx| { let state = host_buffer.read(cx).to_proto(); let ops = cx @@ -4080,8 +4081,10 @@ mod tests { #[gpui::test] fn test_excerpt_boundaries_and_clipping(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(6, 6, 'g'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(6, 6, 'g'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let events = Rc::new(RefCell::new(Vec::::new())); @@ -4314,8 +4317,10 @@ mod tests { #[gpui::test] fn test_excerpt_events(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'a'), cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, sample_text(10, 3, 'm'), cx)); + let buffer_1 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'a'))); + let buffer_2 = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(10, 3, 'm'))); let leader_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let follower_multibuffer = cx.add_model(|_| MultiBuffer::new(0)); @@ -4420,7 +4425,8 @@ mod tests { #[gpui::test] fn test_push_excerpts_with_context_lines(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { multibuffer.push_excerpts_with_context_lines( @@ -4456,7 +4462,8 @@ mod tests { #[gpui::test] async fn test_stream_excerpts_with_context_lines(cx: &mut TestAppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, sample_text(20, 3, 'a'), cx)); + let buffer = + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, sample_text(20, 3, 'a'))); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let anchor_ranges = multibuffer.update(cx, |multibuffer, cx| { let snapshot = buffer.read(cx); @@ -4502,7 +4509,7 @@ mod tests { #[gpui::test] fn test_singleton_multibuffer_anchors(cx: &mut AppContext) { - let buffer = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); let multibuffer = cx.add_model(|cx| MultiBuffer::singleton(buffer.clone(), cx)); let old_snapshot = multibuffer.read(cx).snapshot(cx); buffer.update(cx, |buffer, cx| { @@ -4522,8 +4529,8 @@ mod tests { #[gpui::test] fn test_multibuffer_anchors(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "efghi", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "efghi")); let multibuffer = cx.add_model(|cx| { let mut multibuffer = MultiBuffer::new(0); multibuffer.push_excerpts( @@ -4580,8 +4587,8 @@ mod tests { #[gpui::test] fn test_resolving_anchors_after_replacing_their_excerpts(cx: &mut AppContext) { - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "abcd", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "ABCDEFGHIJKLMNOP", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcd")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "ABCDEFGHIJKLMNOP")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); // Create an insertion id in buffer 1 that doesn't exist in buffer 2. @@ -4976,7 +4983,9 @@ mod tests { let base_text = util::RandomCharIter::new(&mut rng) .take(10) .collect::(); - buffers.push(cx.add_model(|cx| Buffer::new(0, base_text, cx))); + buffers.push( + cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text)), + ); buffers.last().unwrap() } else { buffers.choose(&mut rng).unwrap() @@ -5317,8 +5326,8 @@ mod tests { fn test_history(cx: &mut AppContext) { cx.set_global(SettingsStore::test(cx)); - let buffer_1 = cx.add_model(|cx| Buffer::new(0, "1234", cx)); - let buffer_2 = cx.add_model(|cx| Buffer::new(0, "5678", cx)); + let buffer_1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "1234")); + let buffer_2 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "5678")); let multibuffer = cx.add_model(|_| MultiBuffer::new(0)); let group_interval = multibuffer.read(cx).history.group_interval; multibuffer.update(cx, |multibuffer, cx| { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d5586af5ef..902ed26b57 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -347,13 +347,9 @@ impl CharKind { } impl Buffer { - pub fn new>( - replica_id: ReplicaId, - base_text: T, - cx: &mut ModelContext, - ) -> Self { + pub fn new>(replica_id: ReplicaId, id: u64, base_text: T) -> Self { Self::build( - TextBuffer::new(replica_id, cx.model_id() as u64, base_text.into()), + TextBuffer::new(replica_id, id, base_text.into()), None, None, ) diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9d4b9c38fe..db3749aa25 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -43,8 +43,8 @@ fn test_line_endings(cx: &mut gpui::AppContext) { init_settings(cx, |_| {}); cx.add_model(|cx| { - let mut buffer = - Buffer::new(0, "one\r\ntwo\rthree", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "one\r\ntwo\rthree") + .with_language(Arc::new(rust_lang()), cx); assert_eq!(buffer.text(), "one\ntwo\nthree"); assert_eq!(buffer.line_ending(), LineEnding::Windows); @@ -138,8 +138,8 @@ fn test_edit_events(cx: &mut gpui::AppContext) { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); - let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx)); - let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx)); + let buffer1 = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "abcdef")); + let buffer2 = cx.add_model(|cx| Buffer::new(1, cx.model_id() as u64, "abcdef")); let buffer1_ops = Rc::new(RefCell::new(Vec::new())); buffer1.update(cx, { let buffer1_ops = buffer1_ops.clone(); @@ -222,7 +222,7 @@ fn test_edit_events(cx: &mut gpui::AppContext) { #[gpui::test] async fn test_apply_diff(cx: &mut gpui::TestAppContext) { let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n"; - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); let anchor = buffer.read_with(cx, |buffer, _| buffer.anchor_before(Point::new(3, 3))); let text = "a\nccc\ndddd\nffffff\n"; @@ -254,7 +254,7 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { ] .join("\n"); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, text)); // Spawn a task to format the buffer's whitespace. // Pause so that the foratting task starts running. @@ -318,8 +318,9 @@ async fn test_normalize_whitespace(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_reparse(cx: &mut gpui::TestAppContext) { let text = "fn a() {}"; - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); // Wait for the initial text to parse buffer.condition(cx, |buffer, _| !buffer.is_parsing()).await; @@ -443,7 +444,8 @@ async fn test_reparse(cx: &mut gpui::TestAppContext) { #[gpui::test] async fn test_resetting_language(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "{}", cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "{}").with_language(Arc::new(rust_lang()), cx); buffer.set_sync_parse_timeout(Duration::ZERO); buffer }); @@ -491,8 +493,9 @@ async fn test_outline(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -576,8 +579,9 @@ async fn test_outline_nodes_with_newlines(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let outline = buffer .read_with(cx, |buffer, _| buffer.snapshot().outline(None)) .unwrap(); @@ -613,7 +617,9 @@ async fn test_outline_with_extra_context(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(language), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // extra context nodes are included in the outline. @@ -655,8 +661,9 @@ async fn test_symbols_containing(cx: &mut gpui::TestAppContext) { "# .unindent(); - let buffer = - cx.add_model(|cx| Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| { + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx) + }); let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot()); // point is at the start of an item @@ -877,7 +884,8 @@ fn test_enclosing_bracket_ranges_where_brackets_are_not_outermost_children(cx: & fn test_range_for_syntax_ancestor(cx: &mut AppContext) { 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 buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); let snapshot = buffer.snapshot(); assert_eq!( @@ -917,7 +925,8 @@ fn test_autoindent_with_soft_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n \n}"); @@ -959,7 +968,8 @@ fn test_autoindent_with_hard_tabs(cx: &mut AppContext) { cx.add_model(|cx| { let text = "fn a() {}"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit([(8..8, "\n\n")], Some(AutoindentMode::EachLine), cx); assert_eq!(buffer.text(), "fn a() {\n\t\n}"); @@ -1000,6 +1010,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { c; @@ -1007,7 +1018,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1073,6 +1083,7 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { b(); @@ -1080,7 +1091,6 @@ fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut AppC " .replace("|", "") // marker to preserve trailing whitespace .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1136,13 +1146,13 @@ fn test_autoindent_does_not_adjust_lines_within_newly_created_errors(cx: &mut Ap cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() { i } " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1198,11 +1208,11 @@ fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut AppContext) { cx.add_model(|cx| { let mut buffer = Buffer::new( 0, + cx.model_id() as u64, " fn a() {} " .unindent(), - cx, ) .with_language(Arc::new(rust_lang()), cx); @@ -1254,7 +1264,8 @@ fn test_autoindent_with_edit_at_end_of_buffer(cx: &mut AppContext) { cx.add_model(|cx| { let text = "a\nb"; - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(0..1, "\n"), (2..3, "\n")], Some(AutoindentMode::EachLine), @@ -1280,7 +1291,8 @@ fn test_autoindent_multi_line_insertion(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); buffer.edit( [(Point::new(3, 0)..Point::new(3, 0), "e(\n f()\n);\n")], Some(AutoindentMode::EachLine), @@ -1317,7 +1329,8 @@ fn test_autoindent_block_mode(cx: &mut AppContext) { } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // When this text was copied, both of the quotation marks were at the same // indent level, but the indentation of the first line was not included in @@ -1402,7 +1415,8 @@ fn test_autoindent_block_mode_without_original_indent_columns(cx: &mut AppContex } "# .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language(Arc::new(rust_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(rust_lang()), cx); // The original indent columns are not known, so this text is // auto-indented in a block as if the first line was copied in @@ -1481,7 +1495,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut AppContext) { " .unindent(); - let mut buffer = Buffer::new(0, text, cx).with_language( + let mut buffer = Buffer::new(0, cx.model_id() as u64, text).with_language( Arc::new(Language::new( LanguageConfig { name: "Markdown".into(), @@ -1557,7 +1571,7 @@ fn test_autoindent_with_injected_languages(cx: &mut AppContext) { false, ); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry); buffer.set_language(Some(html_language), cx); buffer.edit( @@ -1593,7 +1607,8 @@ fn test_autoindent_query_with_outdent_captures(cx: &mut AppContext) { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(Arc::new(ruby_lang()), cx); + let mut buffer = + Buffer::new(0, cx.model_id() as u64, "").with_language(Arc::new(ruby_lang()), cx); let text = r#" class C @@ -1683,7 +1698,8 @@ fn test_language_scope_at_with_javascript(cx: &mut AppContext) { let text = r#"a["b"] = ;"#; - let buffer = Buffer::new(0, text, cx).with_language(Arc::new(language), cx); + let buffer = + Buffer::new(0, cx.model_id() as u64, text).with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); let config = snapshot.language_scope_at(0).unwrap(); @@ -1762,7 +1778,8 @@ fn test_language_scope_at_with_rust(cx: &mut AppContext) { "# .unindent(); - let buffer = Buffer::new(0, text.clone(), cx).with_language(Arc::new(language), cx); + let buffer = Buffer::new(0, cx.model_id() as u64, text.clone()) + .with_language(Arc::new(language), cx); let snapshot = buffer.snapshot(); // By default, all brackets are enabled @@ -1806,7 +1823,7 @@ fn test_language_scope_at_with_combined_injections(cx: &mut AppContext) { language_registry.add(Arc::new(html_lang())); language_registry.add(Arc::new(erb_lang())); - let mut buffer = Buffer::new(0, text, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, text); buffer.set_language_registry(language_registry.clone()); buffer.set_language( language_registry @@ -1838,7 +1855,7 @@ fn test_serialization(cx: &mut gpui::AppContext) { let mut now = Instant::now(); let buffer1 = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "abc", cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "abc"); buffer.edit([(3..3, "D")], None, cx); now += Duration::from_secs(1); @@ -1893,7 +1910,7 @@ fn test_random_collaboration(cx: &mut AppContext, mut rng: StdRng) { let mut replica_ids = Vec::new(); let mut buffers = Vec::new(); let network = Rc::new(RefCell::new(Network::new(rng.clone()))); - let base_buffer = cx.add_model(|cx| Buffer::new(0, base_text.as_str(), cx)); + let base_buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, base_text.as_str())); for i in 0..rng.gen_range(min_peers..=max_peers) { let buffer = cx.add_model(|cx| { @@ -2394,7 +2411,8 @@ fn assert_bracket_pairs( ) { let (expected_text, selection_ranges) = marked_text_ranges(selection_text, false); let buffer = cx.add_model(|cx| { - Buffer::new(0, expected_text.clone(), cx).with_language(Arc::new(language), cx) + Buffer::new(0, cx.model_id() as u64, expected_text.clone()) + .with_language(Arc::new(language), cx) }); let buffer = buffer.update(cx, |buffer, _cx| buffer.snapshot()); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index d18f57ffe9..51bdb4c5ce 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -176,7 +176,9 @@ impl LogStore { cx.notify(); LanguageServerState { rpc_state: None, - log_buffer: cx.add_model(|cx| Buffer::new(0, "", cx)).clone(), + log_buffer: cx + .add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")) + .clone(), } }) .log_buffer @@ -241,7 +243,7 @@ impl LogStore { let rpc_state = server_state.rpc_state.get_or_insert_with(|| { let io_tx = self.io_tx.clone(); let language = project.read(cx).languages().language_for_name("JSON"); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); cx.spawn_weak({ let buffer = buffer.clone(); |_, mut cx| async move { @@ -327,7 +329,7 @@ impl LspLogView { .projects .get(&project.downgrade()) .and_then(|project| project.servers.keys().copied().next()); - let buffer = cx.add_model(|cx| Buffer::new(0, "", cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, "")); let mut this = Self { editor: Self::editor_for_buffer(project.clone(), buffer, cx), project, diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 547c7dcc95..f839c8d5c5 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1568,9 +1568,9 @@ impl Project { if self.is_remote() { return Err(anyhow!("creating buffers as a guest is not supported yet")); } - + let id = post_inc(&mut self.next_buffer_id); let buffer = cx.add_model(|cx| { - Buffer::new(self.replica_id(), text, cx) + Buffer::new(self.replica_id(), id, text) .with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx) }); self.register_buffer(&buffer, cx)?; diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 4078cb572d..e708781eca 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -837,6 +837,7 @@ mod tests { let buffer = cx.add_model(|cx| { Buffer::new( 0, + cx.model_id() as u64, 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 @@ -844,7 +845,6 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(), - cx, ) }); let window = cx.add_window(|_| EmptyView); @@ -1225,7 +1225,7 @@ mod tests { expected_query_matches_count > 1, "Should pick a query with multiple results" ); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1412,7 +1412,7 @@ mod tests { for "find" or "find and replace" operations on strings, or for input validation. "# .unindent(); - let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); + let buffer = cx.add_model(|cx| Buffer::new(0, cx.model_id() as u64, buffer_text)); let window = cx.add_window(|_| EmptyView); let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); diff --git a/crates/zed/src/languages/c.rs b/crates/zed/src/languages/c.rs index 47aa2b739c..c5041136c9 100644 --- a/crates/zed/src/languages/c.rs +++ b/crates/zed/src/languages/c.rs @@ -289,7 +289,7 @@ mod tests { let language = crate::languages::language("c", tree_sitter_c::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // empty function buffer.edit([(0..0, "int main() {}")], None, cx); diff --git a/crates/zed/src/languages/python.rs b/crates/zed/src/languages/python.rs index 331d829cbc..d89a4171e9 100644 --- a/crates/zed/src/languages/python.rs +++ b/crates/zed/src/languages/python.rs @@ -210,7 +210,7 @@ mod tests { }); cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); let append = |buffer: &mut Buffer, text: &str, cx: &mut ModelContext| { let ix = buffer.len(); buffer.edit([(ix..ix, text)], Some(AutoindentMode::EachLine), cx); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 3c7f84fec7..d550d126bb 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -474,7 +474,7 @@ mod tests { let language = crate::languages::language("rust", tree_sitter_rust::language(), None).await; cx.add_model(|cx| { - let mut buffer = Buffer::new(0, "", cx).with_language(language, cx); + let mut buffer = Buffer::new(0, cx.model_id() as u64, "").with_language(language, cx); // indent between braces buffer.set_text("fn a() {}", cx); diff --git a/crates/zed/src/languages/typescript.rs b/crates/zed/src/languages/typescript.rs index 0a47d365b5..34a512f300 100644 --- a/crates/zed/src/languages/typescript.rs +++ b/crates/zed/src/languages/typescript.rs @@ -356,8 +356,9 @@ mod tests { "# .unindent(); - let buffer = - cx.add_model(|cx| language::Buffer::new(0, text, cx).with_language(language, cx)); + let buffer = cx.add_model(|cx| { + language::Buffer::new(0, cx.model_id() as u64, text).with_language(language, cx) + }); let outline = buffer.read_with(cx, |buffer, _| buffer.snapshot().outline(None).unwrap()); assert_eq!( outline From 9aad602af7b71e24377518e9dee9febc7ab96779 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 28 Aug 2023 18:20:10 +0200 Subject: [PATCH 122/142] chore: Bump memchr to 2.6.0 (#2902) Fresh off the press, memchr 2.6.0 adds vector search routines for aarch64. That directly improves our search performance for both text and regex searches. Per BurntSushi's claims, the simple string searches in ripgrep got ~2 times faster (more details available in https://github.com/BurntSushi/memchr/pull/129). Release Notes: - N/A --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bfdb1b6092..347976691d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4269,9 +4269,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "76fc44e2588d5b436dbc3c6cf62aef290f90dab6235744a93dfe1cc18f451e2c" [[package]] name = "memfd" From 04354675ca2d2b9bedebcd4f2a5191289aaad9de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 10:35:23 -0700 Subject: [PATCH 123/142] Remove search dismiss button --- crates/search/src/buffer_search.rs | 9 +-------- crates/search/src/project_search.rs | 11 ----------- crates/search/src/search_bar.rs | 28 ---------------------------- 3 files changed, 1 insertion(+), 47 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index e708781eca..01d4b7693f 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -214,7 +214,7 @@ impl View for BufferSearchBar { let icon_style = theme.search.editor_icon.clone(); let nav_column = Flex::row() - .with_child(self.render_action_button("Select All", cx)) + .with_child(self.render_action_button("all", cx)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .with_child(Flex::row().with_children(match_count)) @@ -268,13 +268,6 @@ impl View for BufferSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Buffer Search", - &theme.search, - cx, - |_, this, cx| this.dismiss(&Default::default(), cx), - Some(Box::new(Dismiss)), - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 6364183877..8542124a25 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1597,17 +1597,6 @@ impl View for ProjectSearchBar { .contained() .with_style(theme.search.modes_container), ) - .with_child(super::search_bar::render_close_button( - "Dismiss Project Search", - &theme.search, - cx, - |_, this, cx| { - if let Some(search) = this.active_project_search.as_mut() { - search.update(cx, |_, cx| cx.emit(ViewEvent::Dismiss)) - } - }, - None, - )) .constrained() .with_height(theme.search.search_bar_row_height) .aligned() diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 7d3c5261ea..4676b8f027 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -13,34 +13,6 @@ use crate::{ SelectNextMatch, SelectPrevMatch, }; -pub(super) fn render_close_button( - tooltip: &'static str, - theme: &theme::Search, - cx: &mut ViewContext, - on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, - dismiss_action: Option>, -) -> AnyElement { - let tooltip_style = theme::current(cx).tooltip.clone(); - - enum CloseButton {} - MouseEventHandler::new::(0, cx, |state, _| { - let style = theme.dismiss_button.style_for(state); - Svg::new("icons/x_mark_8.svg") - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.search_bar_row_height) - }) - .on_click(MouseButton::Left, on_click) - .with_cursor_style(CursorStyle::PointingHand) - .with_tooltip::(0, tooltip.to_string(), dismiss_action, tooltip_style, cx) - .into_any() -} - pub(super) fn render_nav_button( icon: &'static str, direction: Direction, From a1d2ae3095a7e279cbd202ab6bbbd97f06e7035e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 28 Aug 2023 13:32:30 -0600 Subject: [PATCH 124/142] Add -l option to build script When you pass -l, we build for the local architecture only and copy the resulting app bundle to /Applications. You can provide a bundle name as an optional argument. --- script/bundle | 159 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 115 insertions(+), 44 deletions(-) diff --git a/script/bundle b/script/bundle index 9f50862cd5..49da1072ce 100755 --- a/script/bundle +++ b/script/bundle @@ -5,11 +5,29 @@ set -e build_flag="--release" target_dir="release" open_result=false +local_only=false +overwrite_local_app=false +bundle_name="" + +# Function for displaying help info +help_info() { + echo " +Usage: ${0##*/} [options] [bundle_name] +Build the application bundle. + +Options: + -d Compile in debug mode and print the app bundle's path. + -l Compile for local architecture only and copy bundle to /Applications. + -o Open the resulting DMG or the app itself in local mode. + -f Overwrite the local app bundle if it exists. + -h Display this help and exit. + " +} # If -o option is specified, the folder of the resulting dmg will be opened in finder # If -d is specified, Zed will be compiled in debug mode and the application's path printed # If -od or -do is specified Zed will be bundled in debug and the application will be run. -while getopts 'od' flag +while getopts 'dlfoh' flag do case "${flag}" in o) open_result=true;; @@ -17,9 +35,21 @@ do build_flag=""; target_dir="debug" ;; + l) local_only=true;; + f) overwrite_local_app=true;; + h) + help_info + exit 0 + ;; esac done +shift $((OPTIND-1)) + +if [ "$1" ]; then + bundle_name=$1 +fi + export ZED_BUNDLE=true export MACOSX_DEPLOYMENT_TARGET=10.15.7 @@ -33,14 +63,24 @@ rustup target add wasm32-wasi # Deal with versions of macOS that don't include libstdc++ headers export CXXFLAGS="-stdlib=libc++" -echo "Compiling zed binary for aarch64-apple-darwin" -cargo build ${build_flag} --package zed --target aarch64-apple-darwin -echo "Compiling zed binary for x86_64-apple-darwin" -cargo build ${build_flag} --package zed --target x86_64-apple-darwin -echo "Compiling cli binary for aarch64-apple-darwin" -cargo build ${build_flag} --package cli --target aarch64-apple-darwin -echo "Compiling cli binary for x86_64-apple-darwin" -cargo build ${build_flag} --package cli --target x86_64-apple-darwin +version_info=$(rustc --version --verbose) +host_line=$(echo "$version_info" | grep host) +local_target_triple=${host_line#*: } + +if [ "$local_only" = true ]; then + echo "Building for local target only." + cargo build ${build_flag} --package zed + cargo build ${build_flag} --package cli +else + echo "Compiling zed binary for aarch64-apple-darwin" + cargo build ${build_flag} --package zed --target aarch64-apple-darwin + echo "Compiling zed binary for x86_64-apple-darwin" + cargo build ${build_flag} --package zed --target x86_64-apple-darwin + echo "Compiling cli binary for aarch64-apple-darwin" + cargo build ${build_flag} --package cli --target aarch64-apple-darwin + echo "Compiling cli binary for x86_64-apple-darwin" + cargo build ${build_flag} --package cli --target x86_64-apple-darwin +fi echo "Creating application bundle" pushd crates/zed @@ -50,27 +90,34 @@ sed \ -i .backup \ "s/package.metadata.bundle-${channel}/package.metadata.bundle/" \ Cargo.toml -app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) + +if [ "$local_only" = true ]; then + app_path=$(cargo bundle ${build_flag} --select-workspace-root | xargs) +else + app_path=$(cargo bundle ${build_flag} --target x86_64-apple-darwin --select-workspace-root | xargs) +fi mv Cargo.toml.backup Cargo.toml popd echo "Bundled ${app_path}" -echo "Creating fat binaries" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ - -output \ - "${app_path}/Contents/MacOS/zed" -lipo \ - -create \ - target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ - -output \ - "${app_path}/Contents/MacOS/cli" +if [ "$local_only" = false ]; then + echo "Creating fat binaries" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/Zed \ + -output \ + "${app_path}/Contents/MacOS/zed" + lipo \ + -create \ + target/{x86_64-apple-darwin,aarch64-apple-darwin}/${target_dir}/cli \ + -output \ + "${app_path}/Contents/MacOS/cli" +fi echo "Copying WebRTC.framework into the frameworks folder" mkdir "${app_path}/Contents/Frameworks" -cp -R target/x86_64-apple-darwin/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" +cp -R target/${local_target_triple}/${target_dir}/WebRTC.framework "${app_path}/Contents/Frameworks/" if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then echo "Signing bundle with Apple-issued certificate" @@ -99,31 +146,55 @@ if [ "$target_dir" = "debug" ]; then exit 0 fi -dmg_target_directory="target/${target_dir}" -dmg_source_directory="${dmg_target_directory}/dmg" -dmg_file_path="${dmg_target_directory}/Zed.dmg" +if [ "$local_only" = true ]; then + # If bundle_name is not set or empty, use the basename of $app_path + if [ -z "$bundle_name" ]; then + bundle_name=$(basename "$app_path") + else + # If bundle_name doesn't end in .app, append it + if [[ "$bundle_name" != *.app ]]; then + bundle_name="$bundle_name.app" + fi + fi -echo "Creating DMG" -rm -rf ${dmg_source_directory} -mkdir -p ${dmg_source_directory} -mv "${app_path}" "${dmg_source_directory}" + if [ "$overwrite_local_app" = true ]; then + rm -rf "/Applications/$bundle_name" + fi + mv "$app_path" "/Applications/$bundle_name" -ln -s /Applications ${dmg_source_directory} -hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" -# If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. -# This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. -rm ${dmg_source_directory}/Applications + if [ "$open_result" = true ]; then + open "/Applications/$bundle_name" + else + echo "Installed application bundle:" + echo "/Applications/$bundle_name" + fi +else + echo "Creating DMG" + dmg_target_directory="target/${target_dir}" + dmg_source_directory="${dmg_target_directory}/dmg" + dmg_file_path="${dmg_target_directory}/Zed.dmg" -echo "Adding license agreement to DMG" -npm install --global dmg-license minimist -dmg-license script/eula/eula.json "${dmg_file_path}" + rm -rf ${dmg_source_directory} + mkdir -p ${dmg_source_directory} + mv "${app_path}" "${dmg_source_directory}" -if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then - echo "Notarizing DMG with Apple" - npm install -g notarize-cli - npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" -fi + ln -s /Applications ${dmg_source_directory} + hdiutil create -volname Zed -srcfolder "${dmg_source_directory}" -ov -format UDZO "${dmg_file_path}" + # If someone runs this bundle script locally, a symlink will be placed in `dmg_source_directory`. + # This symlink causes CPU issues with Zed if the Zed codebase is the project being worked on, so we simply remove it for now. + rm ${dmg_source_directory}/Applications -if [ "$open_result" = true ]; then - open $dmg_target_directory + echo "Adding license agreement to DMG" + npm install --global dmg-license minimist + dmg-license script/eula/eula.json "${dmg_file_path}" + + if [[ -n $MACOS_CERTIFICATE && -n $MACOS_CERTIFICATE_PASSWORD && -n $APPLE_NOTARIZATION_USERNAME && -n $APPLE_NOTARIZATION_PASSWORD ]]; then + echo "Notarizing DMG with Apple" + npm install -g notarize-cli + npx notarize-cli --file "${dmg_file_path}" --bundle-id dev.zed.Zed --username "$APPLE_NOTARIZATION_USERNAME" --password "$APPLE_NOTARIZATION_PASSWORD" + fi + + if [ "$open_result" = true ]; then + open $dmg_target_directory + fi fi From 9521f6da42c30291660d6294b07ddb1349161d94 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:21 -0700 Subject: [PATCH 125/142] Simplify implementation of flex with spacing --- crates/gpui/src/elements/flex.rs | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index d43a152a64..80dfb0625c 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -88,8 +88,7 @@ impl Flex { cx: &mut LayoutContext, ) { let cross_axis = self.axis.invert(); - let last = self.children.len() - 1; - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if let Some(metadata) = child.metadata::() { if let Some((flex, expanded)) = metadata.flex { if expanded != layout_expanded { @@ -101,10 +100,6 @@ impl Flex { } else { let space_per_flex = *remaining_space / *remaining_flex; space_per_flex * flex - } - if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing }; let child_min = if expanded { child_max } else { 0. }; let child_constraint = match self.axis { @@ -144,13 +139,12 @@ impl Element for Flex { cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { let mut total_flex = None; - let mut fixed_space = 0.0; + let mut fixed_space = self.children.len().saturating_sub(1) as f32 * self.spacing; let mut contains_float = false; let cross_axis = self.axis.invert(); let mut cross_axis_max: f32 = 0.0; - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { let metadata = child.metadata::(); contains_float |= metadata.map_or(false, |metadata| metadata.float); @@ -168,12 +162,7 @@ impl Element for Flex { ), }; let size = child.layout(child_constraint, view, cx); - fixed_space += size.along(self.axis) - + if ix == 0 || ix == last { - self.spacing / 2. - } else { - self.spacing - }; + fixed_space += size.along(self.axis); cross_axis_max = cross_axis_max.max(size.along(cross_axis)); } } @@ -333,8 +322,7 @@ impl Element for Flex { } } - let last = self.children.len().saturating_sub(1); - for (ix, child) in &mut self.children.iter_mut().enumerate() { + for child in self.children.iter_mut() { if remaining_space > 0. { if let Some(metadata) = child.metadata::() { if metadata.float { @@ -372,11 +360,9 @@ impl Element for Flex { child.paint(scene, aligned_child_origin, visible_bounds, view, cx); - let spacing = if ix == last { 0. } else { self.spacing }; - match self.axis { - Axis::Horizontal => child_origin += vec2f(child.size().x() + spacing, 0.0), - Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + spacing), + Axis::Horizontal => child_origin += vec2f(child.size().x() + self.spacing, 0.0), + Axis::Vertical => child_origin += vec2f(0.0, child.size().y() + self.spacing), } } From bb448b91d5404fdab1b86bda34d7ad68882b2001 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:16:41 -0700 Subject: [PATCH 126/142] Don't add a quick actions toolbar item for non-editor views Rather than adding primary toolbar item that renders as empty, don't add an item at all. This prevents spurious spacing from being added after other primary toolbar items. --- crates/quick_action_bar/src/quick_action_bar.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index 1804c2b1fc..8595645e59 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -152,9 +152,10 @@ impl ToolbarItemView for QuickActionBar { cx.notify(); } })); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + ToolbarItemLocation::Hidden } - - ToolbarItemLocation::PrimaryRight { flex: None } } None => { self.active_item = None; From 78f9a1f280a3dc77f7f26941c77ec57319e7a073 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:18:30 -0700 Subject: [PATCH 127/142] Remove padding from workspace toolbar, increase its content height to compensate The padding makes it difficult to layout toolbar items correctly when they are more than one row tall. --- styles/src/style_tree/workspace.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/workspace.ts b/styles/src/style_tree/workspace.ts index ecfb572f7e..43a6cec585 100644 --- a/styles/src/style_tree/workspace.ts +++ b/styles/src/style_tree/workspace.ts @@ -129,7 +129,7 @@ export default function workspace(): any { status_bar: statusBar(), titlebar: titlebar(), toolbar: { - height: 34, + height: 42, background: background(theme.highest), border: border(theme.highest, { bottom: true }), item_spacing: 8, @@ -138,7 +138,7 @@ export default function workspace(): any { variant: "ghost", active_color: "accent", }), - padding: { left: 8, right: 8, top: 4, bottom: 4 }, + padding: { left: 8, right: 8 }, }, breadcrumb_height: 24, breadcrumbs: interactive({ From 3eee282a6b72dc15971ed8bfbe2f1c29ed8caa4e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 14:20:09 -0700 Subject: [PATCH 128/142] Overhaul search bar layout * Use a single row, instead of centering the search bar within a double-row toolbar. * Search query controls on the left, navigation on the right * Semantic is the final mode, for greater stability between buffer and project search. * Prevent query editor from moving when toggling path filters --- crates/search/src/buffer_search.rs | 74 +++++----- crates/search/src/mode.rs | 23 --- crates/search/src/project_search.rs | 216 ++++++++++++++-------------- crates/search/src/search_bar.rs | 27 ++-- crates/theme/src/theme.rs | 2 +- crates/workspace/src/toolbar.rs | 13 +- styles/src/style_tree/search.ts | 41 +----- 7 files changed, 164 insertions(+), 232 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 01d4b7693f..26ae86f375 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::{next_mode, SearchMode}, + mode::{next_mode, SearchMode, Side}, search_bar::{render_nav_button, render_search_mode_button}, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -156,11 +156,12 @@ impl View for BufferSearchBar { self.query_editor.update(cx, |editor, cx| { editor.set_placeholder_text(new_placeholder_text, cx); }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { let is_active = self.current_mode == mode; render_search_mode_button( mode, + side, is_active, move |_, this, cx| { this.activate_search_mode(mode, cx); @@ -212,20 +213,11 @@ impl View for BufferSearchBar { ) }; - let icon_style = theme.search.editor_icon.clone(); - let nav_column = Flex::row() - .with_child(self.render_action_button("all", cx)) - .with_child(nav_button_for_direction("<", Direction::Prev, cx)) - .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(match_count)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let query = Flex::row() + let query_column = Flex::row() .with_child( - Svg::for_style(icon_style.icon) + Svg::for_style(theme.search.editor_icon.clone().icon) .contained() - .with_style(icon_style.container), + .with_style(theme.search.editor_icon.clone().container), ) .with_child(ChildView::new(&self.query_editor, cx).flex(1., true)) .with_child( @@ -244,42 +236,45 @@ impl View for BufferSearchBar { .contained(), ) .align_children_center() - .flex(1., true); - let editor_column = Flex::row() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) .contained() + .with_style(query_container_style) .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) .with_height(theme.search.search_bar_row_height) .flex(1., false); + let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + Some(Side::Right), + cx, + )) + .contained() + .with_style(theme.search.modes_container) + .constrained() + .with_height(theme.search.search_bar_row_height); + + let nav_column = Flex::row() + .with_child(Flex::row().with_children(match_count)) + .with_child(self.render_action_button("all", cx)) + .with_child(nav_button_for_direction("<", Direction::Prev, cx)) + .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() .flex_float(); + Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) - .aligned() .into_any_named("search bar") } } @@ -333,8 +328,9 @@ impl ToolbarItemView for BufferSearchBar { ToolbarItemLocation::Hidden } } + fn row_count(&self, _: &ViewContext) -> usize { - 2 + 1 } } diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 2c180be761..56d6dfa024 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -48,29 +48,6 @@ impl SearchMode { SearchMode::Regex => Box::new(ActivateRegexMode), } } - - pub(crate) fn border_right(&self) -> bool { - match self { - SearchMode::Regex => true, - SearchMode::Text => true, - SearchMode::Semantic => true, - } - } - - pub(crate) fn border_left(&self) -> bool { - match self { - SearchMode::Text => true, - _ => false, - } - } - - pub(crate) fn button_side(&self) -> Option { - match self { - SearchMode::Text => Some(Side::Left), - SearchMode::Semantic => None, - SearchMode::Regex => Some(Side::Right), - } - } } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 8542124a25..c2ecde4ce5 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ history::SearchHistory, - mode::SearchMode, + mode::{SearchMode, Side}, search_bar::{render_nav_button, render_option_button_icon, render_search_mode_button}, ActivateRegexMode, CycleMode, NextHistoryQuery, PreviousHistoryQuery, SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleWholeWord, @@ -1424,8 +1424,13 @@ impl View for ProjectSearchBar { }, cx, ); + let search = _search.read(cx); + let is_semantic_available = SemanticIndex::enabled(cx); let is_semantic_disabled = search.semantic_state.is_none(); + let icon_style = theme.search.editor_icon.clone(); + let is_active = search.active_match_index.is_some(); + let render_option_button_icon = |path, option, cx: &mut ViewContext| { crate::search_bar::render_option_button_icon( self.is_option_enabled(option, cx), @@ -1451,28 +1456,23 @@ impl View for ProjectSearchBar { render_option_button_icon("icons/word_search_12.svg", SearchOptions::WHOLE_WORD, cx) }); - let search = _search.read(cx); - let icon_style = theme.search.editor_icon.clone(); - - // Editor Functionality - let query = Flex::row() - .with_child( - Svg::for_style(icon_style.icon) - .contained() - .with_style(icon_style.container), + let search_button_for_mode = |mode, side, cx: &mut ViewContext| { + let is_active = if let Some(search) = self.active_project_search.as_ref() { + let search = search.read(cx); + search.current_mode == mode + } else { + false + }; + render_search_mode_button( + mode, + side, + is_active, + move |_, this, cx| { + this.activate_search_mode(mode, cx); + }, + cx, ) - .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) - .with_child( - Flex::row() - .with_child(filter_button) - .with_children(case_sensitive) - .with_children(whole_word) - .flex(1., false) - .constrained() - .contained(), - ) - .align_children_center() - .flex(1., true); + }; let search = _search.read(cx); @@ -1490,50 +1490,6 @@ impl View for ProjectSearchBar { theme.search.include_exclude_editor.input.container }; - let included_files_view = ChildView::new(&search.included_files_editor, cx) - .contained() - .flex(1., true); - let excluded_files_view = ChildView::new(&search.excluded_files_editor, cx) - .contained() - .flex(1., true); - let filters = search.filters_enabled.then(|| { - Flex::row() - .with_child( - included_files_view - .contained() - .with_style(include_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .with_child( - excluded_files_view - .contained() - .with_style(exclude_container_style) - .constrained() - .with_height(theme.search.search_bar_row_height) - .with_min_width(theme.search.include_exclude_editor.min_width) - .with_max_width(theme.search.include_exclude_editor.max_width), - ) - .contained() - .with_padding_top(theme.workspace.toolbar.container.padding.bottom) - }); - - let editor_column = Flex::column() - .with_child( - query - .contained() - .with_style(query_container_style) - .constrained() - .with_min_width(theme.search.editor.min_width) - .with_max_width(theme.search.editor.max_width) - .with_height(theme.search.search_bar_row_height) - .flex(1., false), - ) - .with_children(filters) - .flex(1., false); - let matches = search.active_match_index.map(|match_ix| { Label::new( format!( @@ -1548,25 +1504,81 @@ impl View for ProjectSearchBar { .aligned() }); - let search_button_for_mode = |mode, cx: &mut ViewContext| { - let is_active = if let Some(search) = self.active_project_search.as_ref() { - let search = search.read(cx); - search.current_mode == mode - } else { - false - }; - render_search_mode_button( - mode, - is_active, - move |_, this, cx| { - this.activate_search_mode(mode, cx); - }, - cx, + let query_column = Flex::column() + .with_spacing(theme.search.search_row_spacing) + .with_child( + Flex::row() + .with_child( + Svg::for_style(icon_style.icon) + .contained() + .with_style(icon_style.container), + ) + .with_child(ChildView::new(&search.query_editor, cx).flex(1., true)) + .with_child( + Flex::row() + .with_child(filter_button) + .with_children(case_sensitive) + .with_children(whole_word) + .flex(1., false) + .constrained() + .contained(), + ) + .align_children_center() + .contained() + .with_style(query_container_style) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .with_height(theme.search.search_bar_row_height) + .flex(1., false), ) - }; - let is_active = search.active_match_index.is_some(); - let semantic_index = SemanticIndex::enabled(cx) - .then(|| search_button_for_mode(SearchMode::Semantic, cx)); + .with_children(search.filters_enabled.then(|| { + Flex::row() + .with_child( + ChildView::new(&search.included_files_editor, cx) + .contained() + .with_style(include_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .with_child( + ChildView::new(&search.excluded_files_editor, cx) + .contained() + .with_style(exclude_container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) + .flex(1., true), + ) + .constrained() + .with_min_width(theme.search.editor.min_width) + .with_max_width(theme.search.editor.max_width) + .flex(1., false) + })) + .flex(1., false); + + let mode_column = + Flex::row() + .with_child(search_button_for_mode( + SearchMode::Text, + Some(Side::Left), + cx, + )) + .with_child(search_button_for_mode( + SearchMode::Regex, + if is_semantic_available { + None + } else { + Some(Side::Right) + }, + cx, + )) + .with_children(is_semantic_available.then(|| { + search_button_for_mode(SearchMode::Semantic, Some(Side::Right), cx) + })) + .contained() + .with_style(theme.search.modes_container); + let nav_button_for_direction = |label, direction, cx: &mut ViewContext| { render_nav_button( label, @@ -1582,32 +1594,17 @@ impl View for ProjectSearchBar { }; let nav_column = Flex::row() + .with_child(Flex::row().with_children(matches)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) - .with_child(Flex::row().with_children(matches)) - .constrained() - .with_height(theme.search.search_bar_row_height); - - let mode_column = Flex::row() - .with_child( - Flex::row() - .with_child(search_button_for_mode(SearchMode::Text, cx)) - .with_children(semantic_index) - .with_child(search_button_for_mode(SearchMode::Regex, cx)) - .contained() - .with_style(theme.search.modes_container), - ) .constrained() .with_height(theme.search.search_bar_row_height) - .aligned() - .right() - .top() .flex_float(); Flex::row() - .with_child(editor_column) - .with_child(nav_column) + .with_child(query_column) .with_child(mode_column) + .with_child(nav_column) .contained() .with_style(theme.search.container) .into_any_named("project search") @@ -1636,7 +1633,7 @@ impl ToolbarItemView for ProjectSearchBar { self.subscription = Some(cx.observe(&search, |_, _, cx| cx.notify())); self.active_project_search = Some(search); ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), + flex: Some((1., true)), } } else { ToolbarItemLocation::Hidden @@ -1644,13 +1641,12 @@ impl ToolbarItemView for ProjectSearchBar { } fn row_count(&self, cx: &ViewContext) -> usize { - self.active_project_search - .as_ref() - .map(|search| { - let offset = search.read(cx).filters_enabled as usize; - 2 + offset - }) - .unwrap_or_else(|| 2) + if let Some(search) = self.active_project_search.as_ref() { + if search.read(cx).filters_enabled { + return 2; + } + } + 1 } } diff --git a/crates/search/src/search_bar.rs b/crates/search/src/search_bar.rs index 4676b8f027..d1a5a0380a 100644 --- a/crates/search/src/search_bar.rs +++ b/crates/search/src/search_bar.rs @@ -83,6 +83,7 @@ pub(super) fn render_nav_button( pub(crate) fn render_search_mode_button( mode: SearchMode, + side: Option, is_active: bool, on_click: impl Fn(MouseClick, &mut V, &mut EventContext) + 'static, cx: &mut ViewContext, @@ -91,41 +92,41 @@ pub(crate) fn render_search_mode_button( enum SearchModeButton {} MouseEventHandler::new::(mode.region_id(), cx, |state, cx| { let theme = theme::current(cx); - let mut style = theme + let style = theme .search .mode_button .in_state(is_active) .style_for(state) .clone(); - style.container.border.left = mode.border_left(); - style.container.border.right = mode.border_right(); - let label = Label::new(mode.label(), style.text.clone()) - .aligned() - .contained(); - let mut container_style = style.container.clone(); - if let Some(button_side) = mode.button_side() { + let mut container_style = style.container; + if let Some(button_side) = side { if button_side == Side::Left { + container_style.border.left = true; container_style.corner_radii = CornerRadii { bottom_right: 0., top_right: 0., ..container_style.corner_radii }; - label.with_style(container_style) } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii { bottom_left: 0., top_left: 0., ..container_style.corner_radii }; - label.with_style(container_style) } } else { + container_style.border.left = false; container_style.corner_radii = CornerRadii::default(); - label.with_style(container_style) } - .constrained() - .with_height(theme.search.search_bar_row_height) + + Label::new(mode.label(), style.text) + .aligned() + .contained() + .with_style(container_style) + .constrained() + .with_height(theme.search.search_bar_row_height) }) .on_click(MouseButton::Left, on_click) .with_cursor_style(CursorStyle::PointingHand) diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9005fc9757..a5faba8eaf 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -437,11 +437,11 @@ pub struct Search { pub match_index: ContainedText, pub major_results_status: TextStyle, pub minor_results_status: TextStyle, - pub dismiss_button: Interactive, pub editor_icon: IconStyle, pub mode_button: Toggleable>, pub nav_button: Toggleable>, pub search_bar_row_height: f32, + pub search_row_spacing: f32, pub option_button_height: f32, pub modes_container: ContainerStyle, } diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 72c879d6d4..c3f4bb9723 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -81,10 +81,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryLeft { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let left_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_right(spacing); + let left_item = ChildView::new(item.as_any(), cx).aligned(); if let Some((flex, expanded)) = flex { primary_left_items.push(left_item.flex(flex, expanded).into_any()); } else { @@ -94,11 +91,7 @@ impl View for Toolbar { ToolbarItemLocation::PrimaryRight { flex } => { primary_items_row_count = primary_items_row_count.max(item.row_count(cx)); - let right_item = ChildView::new(item.as_any(), cx) - .aligned() - .contained() - .with_margin_left(spacing) - .flex_float(); + let right_item = ChildView::new(item.as_any(), cx).aligned().flex_float(); if let Some((flex, expanded)) = flex { primary_right_items.push(right_item.flex(flex, expanded).into_any()); } else { @@ -120,7 +113,7 @@ impl View for Toolbar { let container_style = theme.container; let height = theme.height * primary_items_row_count as f32; - let mut primary_items = Flex::row(); + let mut primary_items = Flex::row().with_spacing(spacing); primary_items.extend(primary_left_items); primary_items.extend(primary_right_items); diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 4493634a8e..27e8c43a4d 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -34,7 +34,7 @@ export default function search(): any { } return { - padding: { top: 16, bottom: 16, left: 16, right: 16 }, + padding: { top: 4, bottom: 4 }, // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive match_background: with_opacity( foreground(theme.highest, "accent"), @@ -210,6 +210,7 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, + right: 9, }, }, option_button_group: { @@ -232,34 +233,6 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, - dismiss_button: interactive({ - base: { - color: foreground(theme.highest, "variant"), - icon_width: 14, - button_width: 32, - corner_radius: 6, - padding: { - // // top: 10, - // bottom: 10, - left: 10, - right: 10, - }, - - background: background(theme.highest, "variant"), - - border: border(theme.highest, "on"), - }, - state: { - hovered: { - color: foreground(theme.highest, "hovered"), - background: background(theme.highest, "variant", "hovered") - }, - clicked: { - color: foreground(theme.highest, "pressed"), - background: background(theme.highest, "variant", "pressed") - }, - }, - }), editor_icon: { icon: { color: foreground(theme.highest, "variant"), @@ -375,13 +348,9 @@ export default function search(): any { }) } }), - search_bar_row_height: 32, + search_bar_row_height: 34, + search_row_spacing: 8, option_button_height: 22, - modes_container: { - margin: { - right: 9 - } - } - + modes_container: {} } } From 70bea758979f40c6cff2ed01f75e29a1bade579e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 15:15:54 -0700 Subject: [PATCH 129/142] Change cycle mode action to reflect new mode button order --- crates/search/src/mode.rs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/search/src/mode.rs b/crates/search/src/mode.rs index 56d6dfa024..8afc2bd3f4 100644 --- a/crates/search/src/mode.rs +++ b/crates/search/src/mode.rs @@ -51,15 +51,15 @@ impl SearchMode { } pub(crate) fn next_mode(mode: &SearchMode, semantic_enabled: bool) -> SearchMode { - let next_text_state = if semantic_enabled { - SearchMode::Semantic - } else { - SearchMode::Regex - }; - match mode { - SearchMode::Text => next_text_state, - SearchMode::Semantic => SearchMode::Regex, - SearchMode::Regex => SearchMode::Text, + SearchMode::Text => SearchMode::Regex, + SearchMode::Regex => { + if semantic_enabled { + SearchMode::Semantic + } else { + SearchMode::Text + } + } + SearchMode::Semantic => SearchMode::Text, } } From 89eab78cf78c9f9b4b82b2af27ef1c7582c8eaed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 15:48:55 -0700 Subject: [PATCH 130/142] Debounce document highlight and code actions requests --- crates/editor/src/editor.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 38b9100729..537b886b2d 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,6 +111,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); @@ -3292,7 +3294,7 @@ impl Editor { } fn refresh_code_actions(&mut self, cx: &mut ViewContext) -> Option<()> { - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let (start_buffer, start) = buffer.text_anchor_for_position(newest_selection.start, cx)?; @@ -3301,11 +3303,15 @@ impl Editor { return None; } - let actions = project.update(cx, |project, cx| { - project.code_actions(&start_buffer, start..end, cx) - }); self.code_actions_task = Some(cx.spawn(|this, mut cx| async move { - let actions = actions.await; + cx.background().timer(CODE_ACTIONS_DEBOUNCE_TIMEOUT).await; + + let actions = project + .update(&mut cx, |project, cx| { + project.code_actions(&start_buffer, start..end, cx) + }) + .await; + this.update(&mut cx, |this, cx| { this.available_code_actions = actions.log_err().and_then(|actions| { if actions.is_empty() { @@ -3326,7 +3332,7 @@ impl Editor { return None; } - let project = self.project.as_ref()?; + let project = self.project.clone()?; let buffer = self.buffer.read(cx); let newest_selection = self.selections.newest_anchor().clone(); let cursor_position = newest_selection.head(); @@ -3337,12 +3343,19 @@ impl Editor { return None; } - let highlights = project.update(cx, |project, cx| { - project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) - }); - self.document_highlights_task = Some(cx.spawn(|this, mut cx| async move { - if let Some(highlights) = highlights.await.log_err() { + cx.background() + .timer(DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT) + .await; + + let highlights = project + .update(&mut cx, |project, cx| { + project.document_highlights(&cursor_buffer, cursor_buffer_position, cx) + }) + .await + .log_err(); + + if let Some(highlights) = highlights { this.update(&mut cx, |this, cx| { if this.pending_rename.is_some() { return; From 5142049515dc39e99605a67505ff3dd85bf1b59d Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 17:45:19 -0700 Subject: [PATCH 131/142] Demote some Peer logging to trace level --- crates/rpc/src/peer.rs | 54 +++++++++++++++++++++--------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/crates/rpc/src/peer.rs b/crates/rpc/src/peer.rs index 72ddfa567b..91b914f169 100644 --- a/crates/rpc/src/peer.rs +++ b/crates/rpc/src/peer.rs @@ -171,12 +171,12 @@ impl Peer { let this = self.clone(); let response_channels = connection_state.response_channels.clone(); let handle_io = async move { - tracing::debug!(%connection_id, "handle io future: start"); + tracing::trace!(%connection_id, "handle io future: start"); let _end_connection = util::defer(|| { response_channels.lock().take(); this.connections.write().remove(&connection_id); - tracing::debug!(%connection_id, "handle io future: end"); + tracing::trace!(%connection_id, "handle io future: end"); }); // Send messages on this frequency so the connection isn't closed. @@ -188,68 +188,68 @@ impl Peer { futures::pin_mut!(receive_timeout); loop { - tracing::debug!(%connection_id, "outer loop iteration start"); + tracing::trace!(%connection_id, "outer loop iteration start"); let read_message = reader.read().fuse(); futures::pin_mut!(read_message); loop { - tracing::debug!(%connection_id, "inner loop iteration start"); + tracing::trace!(%connection_id, "inner loop iteration start"); futures::select_biased! { outgoing = outgoing_rx.next().fuse() => match outgoing { Some(outgoing) => { - tracing::debug!(%connection_id, "outgoing rpc message: writing"); + tracing::trace!(%connection_id, "outgoing rpc message: writing"); futures::select_biased! { result = writer.write(outgoing).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: done writing"); + tracing::trace!(%connection_id, "outgoing rpc message: done writing"); result.context("failed to write RPC message")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after sending message"); + tracing::trace!(%connection_id, "keepalive interval: resetting after sending message"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "outgoing rpc message: writing timed out"); + tracing::trace!(%connection_id, "outgoing rpc message: writing timed out"); Err(anyhow!("timed out writing message"))?; } } } None => { - tracing::debug!(%connection_id, "outgoing rpc message: channel closed"); + tracing::trace!(%connection_id, "outgoing rpc message: channel closed"); return Ok(()) }, }, _ = keepalive_timer => { - tracing::debug!(%connection_id, "keepalive interval: pinging"); + tracing::trace!(%connection_id, "keepalive interval: pinging"); futures::select_biased! { result = writer.write(proto::Message::Ping).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: done pinging"); + tracing::trace!(%connection_id, "keepalive interval: done pinging"); result.context("failed to send keepalive")?; - tracing::debug!(%connection_id, "keepalive interval: resetting after pinging"); + tracing::trace!(%connection_id, "keepalive interval: resetting after pinging"); keepalive_timer.set(create_timer(KEEPALIVE_INTERVAL).fuse()); } _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "keepalive interval: pinging timed out"); + tracing::trace!(%connection_id, "keepalive interval: pinging timed out"); Err(anyhow!("timed out sending keepalive"))?; } } } incoming = read_message => { let incoming = incoming.context("error reading rpc message from socket")?; - tracing::debug!(%connection_id, "incoming rpc message: received"); - tracing::debug!(%connection_id, "receive timeout: resetting"); + tracing::trace!(%connection_id, "incoming rpc message: received"); + tracing::trace!(%connection_id, "receive timeout: resetting"); receive_timeout.set(create_timer(RECEIVE_TIMEOUT).fuse()); if let proto::Message::Envelope(incoming) = incoming { - tracing::debug!(%connection_id, "incoming rpc message: processing"); + tracing::trace!(%connection_id, "incoming rpc message: processing"); futures::select_biased! { result = incoming_tx.send(incoming).fuse() => match result { Ok(_) => { - tracing::debug!(%connection_id, "incoming rpc message: processed"); + tracing::trace!(%connection_id, "incoming rpc message: processed"); } Err(_) => { - tracing::debug!(%connection_id, "incoming rpc message: channel closed"); + tracing::trace!(%connection_id, "incoming rpc message: channel closed"); return Ok(()) } }, _ = create_timer(WRITE_TIMEOUT).fuse() => { - tracing::debug!(%connection_id, "incoming rpc message: processing timed out"); + tracing::trace!(%connection_id, "incoming rpc message: processing timed out"); Err(anyhow!("timed out processing incoming message"))? } } @@ -257,7 +257,7 @@ impl Peer { break; }, _ = receive_timeout => { - tracing::debug!(%connection_id, "receive timeout: delay between messages too long"); + tracing::trace!(%connection_id, "receive timeout: delay between messages too long"); Err(anyhow!("delay between messages too long"))? } } @@ -274,13 +274,13 @@ impl Peer { let response_channels = response_channels.clone(); async move { let message_id = incoming.id; - tracing::debug!(?incoming, "incoming message future: start"); + tracing::trace!(?incoming, "incoming message future: start"); let _end = util::defer(move || { - tracing::debug!(%connection_id, message_id, "incoming message future: end"); + tracing::trace!(%connection_id, message_id, "incoming message future: end"); }); if let Some(responding_to) = incoming.responding_to { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -290,7 +290,7 @@ impl Peer { if let Some(tx) = channel { let requester_resumed = oneshot::channel(); if let Err(error) = tx.send((incoming, requester_resumed.0)) { - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to = responding_to, @@ -299,14 +299,14 @@ impl Peer { ); } - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, "incoming response: waiting to resume requester" ); let _ = requester_resumed.1.await; - tracing::debug!( + tracing::trace!( %connection_id, message_id, responding_to, @@ -323,7 +323,7 @@ impl Peer { None } else { - tracing::debug!(%connection_id, message_id, "incoming message: received"); + tracing::trace!(%connection_id, message_id, "incoming message: received"); proto::build_typed_envelope(connection_id, incoming).or_else(|| { tracing::error!( %connection_id, From 791f6cf9e748fe5cb6121b821f23fd614ee29f15 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 28 Aug 2023 17:45:32 -0700 Subject: [PATCH 132/142] Update some tests to reflect code action debouncing --- crates/collab/src/tests/integration_tests.rs | 92 ++++++++++---------- crates/editor/src/editor.rs | 4 +- crates/zed/src/zed.rs | 2 + 3 files changed, 50 insertions(+), 48 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index b1227b9501..f64a82e32e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -5321,7 +5321,7 @@ async fn test_collaborating_with_code_actions( .unwrap(); let mut fake_language_server = fake_language_servers.next().await.unwrap(); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5330,9 +5330,9 @@ async fn test_collaborating_with_code_actions( assert_eq!(params.range.start, lsp::Position::new(0, 0)); assert_eq!(params.range.end, lsp::Position::new(0, 0)); Ok(None) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Move cursor to a location that contains code actions. editor_b.update(cx_b, |editor, cx| { @@ -5342,7 +5342,7 @@ async fn test_collaborating_with_code_actions( cx.focus(&editor_b); }); - fake_language_server + let mut requests = fake_language_server .handle_request::(|params, _| async move { assert_eq!( params.text_document.uri, @@ -5394,9 +5394,9 @@ async fn test_collaborating_with_code_actions( ..Default::default() }, )])) - }) - .next() - .await; + }); + deterministic.advance_clock(editor::CODE_ACTIONS_DEBOUNCE_TIMEOUT * 2); + requests.next().await; // Toggle code actions and wait for them to display. editor_b.update(cx_b, |editor, cx| { @@ -7864,6 +7864,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( client_a.language_registry().add(Arc::clone(&language)); client_b.language_registry().add(language); + // Client A opens a project. client_a .fs() .insert_tree( @@ -7884,6 +7885,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); + // Client B joins the project let project_b = client_b.build_remote_project(project_id, cx_b).await; active_call_b .update(cx_b, |call, cx| call.set_location(Some(&project_b), cx)) @@ -7893,6 +7895,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); cx_a.foreground().start_waiting(); + // The host opens a rust file. let _buffer_a = project_a .update(cx_a, |project, cx| { project.open_local_buffer("/a/main.rs", cx) @@ -7900,7 +7903,6 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); let fake_language_server = fake_language_servers.next().await.unwrap(); - let next_call_id = Arc::new(AtomicU32::new(0)); let editor_a = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -7909,6 +7911,9 @@ async fn test_mutual_editor_inlay_hint_cache_update( .unwrap() .downcast::() .unwrap(); + + // Set up the language server to return an additional inlay hint on each request. + let next_call_id = Arc::new(AtomicU32::new(0)); fake_language_server .handle_request::(move |params, _| { let task_next_call_id = Arc::clone(&next_call_id); @@ -7917,33 +7922,28 @@ async fn test_mutual_editor_inlay_hint_cache_update( params.text_document.uri, lsp::Url::from_file_path("/a/main.rs").unwrap(), ); - let mut current_call_id = Arc::clone(&task_next_call_id).fetch_add(1, SeqCst); - let mut new_hints = Vec::with_capacity(current_call_id as usize); - loop { - new_hints.push(lsp::InlayHint { - position: lsp::Position::new(0, current_call_id), - label: lsp::InlayHintLabel::String(current_call_id.to_string()), - kind: None, - text_edits: None, - tooltip: None, - padding_left: None, - padding_right: None, - data: None, - }); - if current_call_id == 0 { - break; - } - current_call_id -= 1; - } - Ok(Some(new_hints)) + let call_count = task_next_call_id.fetch_add(1, SeqCst); + Ok(Some( + (0..=call_count) + .map(|ix| lsp::InlayHint { + position: lsp::Position::new(0, ix), + label: lsp::InlayHintLabel::String(ix.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }) + .collect(), + )) } }) .next() .await .unwrap(); - cx_a.foreground().finish_waiting(); - cx_a.foreground().run_until_parked(); + deterministic.run_until_parked(); let mut edits_made = 1; editor_a.update(cx_a, |editor, _| { @@ -7969,7 +7969,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .downcast::() .unwrap(); - cx_b.foreground().run_until_parked(); + deterministic.run_until_parked(); editor_b.update(cx_b, |editor, _| { assert_eq!( vec!["0".to_string(), "1".to_string()], @@ -7990,18 +7990,9 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_b); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { - assert_eq!( - vec!["0".to_string(), "1".to_string(), "2".to_string()], - extract_hint_labels(editor), - "Host should get hints from the 1st edit and 1st LSP query" - ); - let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version(), edits_made); - }); - editor_b.update(cx_b, |editor, _| { assert_eq!( vec![ "0".to_string(), @@ -8015,6 +8006,15 @@ async fn test_mutual_editor_inlay_hint_cache_update( let inlay_cache = editor.inlay_hint_cache(); assert_eq!(inlay_cache.version(), edits_made); }); + editor_b.update(cx_b, |editor, _| { + assert_eq!( + vec!["0".to_string(), "1".to_string(), "2".to_string(),], + extract_hint_labels(editor), + "Guest should get hints the 1st edit and 2nd LSP query" + ); + let inlay_cache = editor.inlay_hint_cache(); + assert_eq!(inlay_cache.version(), edits_made); + }); editor_a.update(cx_a, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges([13..13])); @@ -8022,8 +8022,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( cx.focus(&editor_a); edits_made += 1; }); - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ @@ -8062,8 +8062,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .expect("inlay refresh request failed"); edits_made += 1; - cx_a.foreground().run_until_parked(); - cx_b.foreground().run_until_parked(); + + deterministic.run_until_parked(); editor_a.update(cx_a, |editor, _| { assert_eq!( vec![ diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 537b886b2d..73774a9239 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -111,8 +111,8 @@ const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; const COPILOT_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); -const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); -const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); +pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); +pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index de05c259c8..9ea406fc3e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1706,6 +1706,8 @@ mod tests { .remove_file(Path::new("/root/a/file2"), Default::default()) .await .unwrap(); + cx.foreground().run_until_parked(); + workspace .update(cx, |w, cx| w.go_back(w.active_pane().downgrade(), cx)) .await From 7c498feb85dc6d704036f8a9f6dcda12da3b8a0c Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 10:40:20 +0300 Subject: [PATCH 133/142] Trim off surrounding `[]` when parsing terminal hover links Terminal has to accept `[` and `]` as valid word parts, due to `[slug].tsx` being a valid file name. Yet, terminal has to exclude these to match paths in strings like `[/some/path/[slug].tsx]`. --- crates/terminal/src/terminal.rs | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 83ba056485..ea919c8f84 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -8,7 +8,7 @@ use alacritty_terminal::{ event::{Event as AlacTermEvent, EventListener, Notify, WindowSize}, event_loop::{EventLoop, Msg, Notifier}, grid::{Dimensions, Scroll as AlacScroll}, - index::{Column, Direction as AlacDirection, Line, Point}, + index::{Boundary, Column, Direction as AlacDirection, Line, Point}, selection::{Selection, SelectionRange, SelectionType}, sync::FairMutex, term::{ @@ -724,14 +724,13 @@ impl Terminal { self.last_content.size, term.grid().display_offset(), ) - .grid_clamp(term, alacritty_terminal::index::Boundary::Grid); + .grid_clamp(term, Boundary::Grid); let link = term.grid().index(point).hyperlink(); let found_word = if link.is_some() { let mut min_index = point; loop { - let new_min_index = - min_index.sub(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_min_index = min_index.sub(term, Boundary::Cursor, 1); if new_min_index == min_index { break; } else if term.grid().index(new_min_index).hyperlink() != link { @@ -743,8 +742,7 @@ impl Terminal { let mut max_index = point; loop { - let new_max_index = - max_index.add(term, alacritty_terminal::index::Boundary::Cursor, 1); + let new_max_index = max_index.add(term, Boundary::Cursor, 1); if new_max_index == max_index { break; } else if term.grid().index(new_max_index).hyperlink() != link { @@ -761,11 +759,34 @@ impl Terminal { } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) { let maybe_url_or_path = term.bounds_to_string(*word_match.start(), *word_match.end()); + let original_match = word_match.clone(); + let (sanitized_match, sanitized_word) = + if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') { + ( + Match::new( + word_match.start().add(term, Boundary::Cursor, 1), + word_match.end().sub(term, Boundary::Cursor, 1), + ), + maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(), + ) + } else { + (word_match, maybe_url_or_path) + }; + let is_url = match regex_match_at(term, point, &URL_REGEX) { - Some(url_match) => url_match == word_match, + Some(url_match) => { + // `]` is a valid symbol in the `file://` URL, so the regex match will include it + // consider that when ensuring that the URL match is the same as the original word + if sanitized_match != original_match { + url_match.start() == sanitized_match.start() + && url_match.end() == original_match.end() + } else { + url_match == sanitized_match + } + } None => false, }; - Some((maybe_url_or_path, is_url, word_match)) + Some((sanitized_word, is_url, sanitized_match)) } else { None }; From ea0e5e880e839ab2e0b3fd2e0dd456f418352d9c Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 29 Aug 2023 15:56:50 +0200 Subject: [PATCH 134/142] chore: Use IsTerminal trait instead of relying on libc to detect stdout being a terminal (#2908) IsTerminal was added in 1.70. Release Notes: - N/A --- crates/zed/src/main.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index da726eef65..3e0a8a7a07 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -31,7 +31,7 @@ use std::{ env, ffi::OsStr, fs::OpenOptions, - io::Write as _, + io::{IsTerminal, Write as _}, os::unix::prelude::OsStrExt, panic, path::{Path, PathBuf}, @@ -635,8 +635,7 @@ async fn load_login_shell_environment() -> Result<()> { } fn stdout_is_a_pty() -> bool { - std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() - && unsafe { libc::isatty(libc::STDOUT_FILENO as i32) != 0 } + std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_none() && std::io::stdout().is_terminal() } fn collect_path_args() -> Vec { From 53558bc603f0c24883e8e3d95ed4a48edad6e249 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:05:59 -0400 Subject: [PATCH 135/142] Remove baseurl to prevent theme import issue --- styles/tsconfig.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/styles/tsconfig.json b/styles/tsconfig.json index 281bd74b21..c7eaa50eed 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -21,8 +21,7 @@ "experimentalDecorators": true, "strictPropertyInitialization": false, "skipLibCheck": true, - "useUnknownInCatchVariables": false, - "baseUrl": "." + "useUnknownInCatchVariables": false }, "exclude": [ "node_modules" From a5b12d535fec69cb3282c7be8385b608c53ba7e3 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:06:13 -0400 Subject: [PATCH 136/142] Add margin and padding functions --- styles/src/component/margin.ts | 34 +++++++++++++++++++++++++++++++++ styles/src/component/padding.ts | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 styles/src/component/margin.ts create mode 100644 styles/src/component/padding.ts diff --git a/styles/src/component/margin.ts b/styles/src/component/margin.ts new file mode 100644 index 0000000000..f6262405f0 --- /dev/null +++ b/styles/src/component/margin.ts @@ -0,0 +1,34 @@ +type MarginOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type MarginStyle = { + top: number + bottom: number + left: number + right: number +} + +export const margin_style = (options: MarginOptions): MarginStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Margin must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} diff --git a/styles/src/component/padding.ts b/styles/src/component/padding.ts new file mode 100644 index 0000000000..96792bf766 --- /dev/null +++ b/styles/src/component/padding.ts @@ -0,0 +1,34 @@ +type PaddingOptions = { + all?: number + left?: number + right?: number + top?: number + bottom?: number +} + +export type PaddingStyle = { + top: number + bottom: number + left: number + right: number +} + +export const padding_style = (options: PaddingOptions): PaddingStyle => { + const { all, top, bottom, left, right } = options + + if (all !== undefined) return { + top: all, + bottom: all, + left: all, + right: all + } + + if (top === undefined && bottom === undefined && left === undefined && right === undefined) throw new Error("Padding must have at least one value") + + return { + top: top || 0, + bottom: bottom || 0, + left: left || 0, + right: right || 0 + } +} From 05da4b740a05762fa8181905ac544088caa304d1 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:28:06 -0400 Subject: [PATCH 137/142] Update spacing, button heights --- styles/src/style_tree/search.ts | 78 +++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 27e8c43a4d..c3dbff4341 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -3,8 +3,21 @@ import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +const search_results = () => { + const theme = useTheme() + + return { + // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive + match_background: with_opacity( + foreground(theme.highest, "accent"), + 0.4 + ), + } +} + export default function search(): any { const theme = useTheme() + const SEARCH_ROW_SPACING = 12 // Search input const editor = { @@ -35,11 +48,7 @@ export default function search(): any { return { padding: { top: 4, bottom: 4 }, - // TODO: Add an activeMatchBackground on the rust side to differentiate between active and inactive - match_background: with_opacity( - foreground(theme.highest, "accent"), - 0.4 - ), + option_button: toggleable({ base: interactive({ base: { @@ -167,7 +176,9 @@ export default function search(): any { // top: 2, }, margin: { - right: 9, + top: 1, + bottom: 1, + right: SEARCH_ROW_SPACING } }, state: { @@ -210,13 +221,13 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), padding: { left: 9, - right: 9, + right: SEARCH_ROW_SPACING, }, }, option_button_group: { padding: { - left: 12, - right: 12, + left: SEARCH_ROW_SPACING, + right: SEARCH_ROW_SPACING, }, }, include_exclude_inputs: { @@ -233,24 +244,26 @@ export default function search(): any { ...text(theme.highest, "mono", "variant"), size: 13, }, + // Input Icon editor_icon: { icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/magnifying_glass_12.svg", + color: foreground(theme.highest, "disabled"), + asset: "icons/magnifying_glass.svg", dimensions: { - width: 12, - height: 12, + width: 14, + height: 14, } }, container: { - margin: { right: 6 }, - padding: { left: 2, right: 2 }, + margin: { right: 4 }, + padding: { left: 1, right: 1 }, } }, + // Toggle group buttons - Text | Regex | Semantic mode_button: toggleable({ base: interactive({ base: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", "variant", { size: "sm" }), background: background(theme.highest, "variant"), border: { @@ -258,21 +271,24 @@ export default function search(): any { left: false, right: false }, - + margin: { + top: 1, + bottom: 1, + }, padding: { - left: 10, - right: 10, + left: 12, + right: 12, }, corner_radius: 6, }, state: { hovered: { - ...text(theme.highest, "mono", "variant", "hovered"), + ...text(theme.highest, "mono", "variant", "hovered", { size: "sm" }), background: background(theme.highest, "variant", "hovered"), border: border(theme.highest, "on", "hovered"), }, clicked: { - ...text(theme.highest, "mono", "variant", "pressed"), + ...text(theme.highest, "mono", "variant", "pressed", { size: "sm" }), background: background(theme.highest, "variant", "pressed"), border: border(theme.highest, "on", "pressed"), }, @@ -281,20 +297,21 @@ export default function search(): any { state: { active: { default: { - ...text(theme.highest, "mono", "on"), + ...text(theme.highest, "mono", "on", { size: "sm" }), background: background(theme.highest, "on") }, hovered: { - ...text(theme.highest, "mono", "on", "hovered"), + ...text(theme.highest, "mono", "on", "hovered", { size: "sm" }), background: background(theme.highest, "on", "hovered") }, clicked: { - ...text(theme.highest, "mono", "on", "pressed"), + ...text(theme.highest, "mono", "on", "pressed", { size: "sm" }), background: background(theme.highest, "on", "pressed") }, }, }, }), + // Next/Previous Match buttons nav_button: toggleable({ state: { inactive: interactive({ @@ -307,7 +324,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -327,7 +347,10 @@ export default function search(): any { left: false, right: false, }, - + margin: { + top: 1, + bottom: 1, + }, padding: { left: 10, right: 10, @@ -351,6 +374,7 @@ export default function search(): any { search_bar_row_height: 34, search_row_spacing: 8, option_button_height: 22, - modes_container: {} + modes_container: {}, + ...search_results() } } From f6faeea7207fe39a7b84c08460ef29a88cb7bef8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:46 -0400 Subject: [PATCH 138/142] Add disabled as an option on text_button --- styles/src/component/text_button.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index b911cd5b77..ead017a803 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -17,6 +17,7 @@ interface TextButtonOptions { variant?: Button.Variant color?: keyof Theme["lowest"] margin?: Partial + disabled?: boolean text_properties?: TextProperties } @@ -29,6 +30,7 @@ export function text_button({ color, layer, margin, + disabled, text_properties, }: TextButtonOptions = {}) { const theme = useTheme() @@ -65,13 +67,17 @@ export function text_button({ state: { default: { background: background_color, - color: foreground(layer ?? theme.lowest, color), + color: + disabled + ? foreground(layer ?? theme.lowest, "disabled") + : foreground(layer ?? theme.lowest, color), }, - hovered: { - background: background(layer ?? theme.lowest, color, "hovered"), - color: foreground(layer ?? theme.lowest, color, "hovered"), - }, - clicked: { + hovered: + disabled ? {} : { + background: background(layer ?? theme.lowest, color, "hovered"), + color: foreground(layer ?? theme.lowest, color, "hovered"), + }, + clicked: disabled ? {} : { background: background(layer ?? theme.lowest, color, "pressed"), color: foreground(layer ?? theme.lowest, color, "pressed"), }, From f626c61b1e4466543324710d84995d48233aef0f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 13:40:58 -0400 Subject: [PATCH 139/142] Update action_button style --- styles/src/style_tree/search.ts | 49 ++++++--------------------------- 1 file changed, 8 insertions(+), 41 deletions(-) diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index c3dbff4341..73f35b9dd4 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -2,6 +2,7 @@ import { with_opacity } from "../theme/color" import { background, border, foreground, text } from "./components" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" +import { text_button } from "../component/text_button" const search_results = () => { const theme = useTheme() @@ -162,49 +163,13 @@ export default function search(): any { }, }, }), + // Search tool buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ - base: interactive({ - base: { - ...text(theme.highest, "mono", "disabled"), - background: background(theme.highest, "disabled"), - corner_radius: 6, - border: border(theme.highest, "disabled"), - padding: { - // bottom: 2, - left: 10, - right: 10, - // top: 2, - }, - margin: { - top: 1, - bottom: 1, - right: SEARCH_ROW_SPACING - } - }, - state: { - hovered: {} - }, - }), state: { - active: interactive({ - base: { - ...text(theme.highest, "mono", "on"), - background: background(theme.highest, "on"), - border: border(theme.highest, "on"), - }, - state: { - hovered: { - ...text(theme.highest, "mono", "on", "hovered"), - background: background(theme.highest, "on", "hovered"), - border: border(theme.highest, "on", "hovered"), - }, - clicked: { - ...text(theme.highest, "mono", "on", "pressed"), - background: background(theme.highest, "on", "pressed"), - border: border(theme.highest, "on", "pressed"), - }, - }, - }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) } }), editor, @@ -312,6 +277,8 @@ export default function search(): any { }, }), // Next/Previous Match buttons + // HACK: This is not how disabled elements should be created + // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled nav_button: toggleable({ state: { inactive: interactive({ From bbb222b6fcaa791d6b4a57eef7ce8aa7553fcd1f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 29 Aug 2023 20:56:26 +0300 Subject: [PATCH 140/142] Add a default binding for toggling inlay hints --- assets/keymaps/default.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 1bd973e83b..a1c771da02 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -521,7 +521,8 @@ "ctrl-alt-cmd-f": "workspace::FollowNextCollaborator", // TODO: Move this to a dock open action "cmd-shift-c": "collab_panel::ToggleFocus", - "cmd-alt-i": "zed::DebugElements" + "cmd-alt-i": "zed::DebugElements", + "ctrl-shift-:": "editor::ToggleInlayHints", } }, { From f0ab27a83da2459af999bb0e0f565ec702a92089 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:04:17 -0400 Subject: [PATCH 141/142] Reorder "Select All" button --- crates/search/src/buffer_search.rs | 2 +- styles/src/style_tree/search.ts | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 26ae86f375..78729df936 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -261,8 +261,8 @@ impl View for BufferSearchBar { .with_height(theme.search.search_bar_row_height); let nav_column = Flex::row() - .with_child(Flex::row().with_children(match_count)) .with_child(self.render_action_button("all", cx)) + .with_child(Flex::row().with_children(match_count)) .with_child(nav_button_for_direction("<", Direction::Prev, cx)) .with_child(nav_button_for_direction(">", Direction::Next, cx)) .constrained() diff --git a/styles/src/style_tree/search.ts b/styles/src/style_tree/search.ts index 73f35b9dd4..c37a4e4b9a 100644 --- a/styles/src/style_tree/search.ts +++ b/styles/src/style_tree/search.ts @@ -168,8 +168,8 @@ export default function search(): any { // Disabled elements should use a disabled state of an interactive element, not a toggleable element with the inactive state being disabled action_button: toggleable({ state: { - inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING } }), - active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING } }) + inactive: text_button({ variant: "ghost", layer: theme.highest, disabled: true, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }), + active: text_button({ variant: "ghost", layer: theme.highest, margin: { right: SEARCH_ROW_SPACING }, text_properties: { size: "sm" } }) } }), editor, @@ -183,9 +183,8 @@ export default function search(): any { border: border(theme.highest, "negative"), }, match_index: { - ...text(theme.highest, "mono", "variant"), + ...text(theme.highest, "mono", { size: "sm" }), padding: { - left: 9, right: SEARCH_ROW_SPACING, }, }, From e89ccf2e2692ab6b0e33070e9caa180af5208177 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 29 Aug 2023 14:09:31 -0400 Subject: [PATCH 142/142] Remove unused `label_button` --- styles/src/component/label_button.ts | 78 ---------------------------- 1 file changed, 78 deletions(-) delete mode 100644 styles/src/component/label_button.ts diff --git a/styles/src/component/label_button.ts b/styles/src/component/label_button.ts deleted file mode 100644 index 3f1c54a7f6..0000000000 --- a/styles/src/component/label_button.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { Interactive, interactive, toggleable, Toggleable } from "../element" -import { TextStyle, background, text } from "../style_tree/components" -import { useTheme } from "../theme" -import { Button } from "./button" - -type LabelButtonStyle = { - corder_radius: number - background: string | null - padding: { - top: number - bottom: number - left: number - right: number - }, - margin: Button.Options['margin'] - button_height: number -} & TextStyle - -/** Styles an Interactive<ContainedText> */ -export function label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Interactive { - const theme = useTheme() - - const base = Button.button_base(options) - const layer = options.layer ?? theme.middle - const color = options.color ?? "base" - - const default_state = { - ...base, - ...text(layer ?? theme.lowest, "sans", color), - font_size: Button.FONT_SIZE, - } - - return interactive({ - base: default_state, - state: { - hovered: { - background: background(layer, options.background ?? color, "hovered") - }, - clicked: { - background: background(layer, options.background ?? color, "pressed") - } - } - }) -} - -/** Styles an Toggleable<Interactive<ContainedText>> */ -export function toggle_label_button_style( - options: Partial = { - variant: Button.variant.Default, - shape: Button.shape.Rectangle, - states: { - hovered: true, - pressed: true - } - } -): Toggleable> { - const activeOptions = { - ...options, - color: options.active_color || options.color, - background: options.active_background || options.background - } - - return toggleable({ - state: { - inactive: label_button_style(options), - active: label_button_style(activeOptions), - }, - }) -}