From fbf1552be910a61e9a5a237d836847736fc197ae Mon Sep 17 00:00:00 2001 From: Sergey Onufrienko Date: Mon, 10 Jul 2023 20:41:39 +0100 Subject: [PATCH 001/105] Add color_family to theme --- styles/src/theme/create_theme.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index d2701f8341a..3f4a0766547 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -1,4 +1,4 @@ -import { Scale, Color } from "chroma-js" +import chroma, { Scale, Color } from "chroma-js" import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" export { Syntax, ThemeSyntax, SyntaxHighlightStyle } import { @@ -32,6 +32,7 @@ export interface Theme { players: Players syntax?: Partial + color_family: ColorFamily } export interface Meta { @@ -69,6 +70,12 @@ export interface Players { "7": Player } +export interface ColorFamily { + range: ColorFamilyRange +} + +export type ColorFamilyRange = Partial<{ [K in keyof RampSet]: number }> + export interface Shadow { blur: number color: string @@ -162,6 +169,10 @@ export function create_theme(theme: ThemeConfig): Theme { "7": player(ramps.yellow), } + const color_family = { + range: build_color_family(ramps) + } + return { name, is_light, @@ -177,6 +188,7 @@ export function create_theme(theme: ThemeConfig): Theme { players, syntax, + color_family, } } @@ -187,6 +199,16 @@ function player(ramp: Scale): Player { } } +function build_color_family(ramps: RampSet): ColorFamilyRange { + const color_family: ColorFamilyRange = {} + + for (const ramp in ramps) { + color_family[ramp as keyof RampSet] = chroma(ramps[ramp as keyof RampSet](0.5)).luminance() + } + + return color_family +} + function lowest_layer(ramps: RampSet): Layer { return { base: build_style_set(ramps.neutral, 0.2, 1), From 036d3e811aa48bf62368dfcd6c9e5812bc16398f Mon Sep 17 00:00:00 2001 From: Sergey Onufrienko Date: Thu, 13 Jul 2023 22:09:31 +0100 Subject: [PATCH 002/105] feat: add low, high, range and scaling --- styles/src/theme/create_theme.ts | 33 ++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index 3f4a0766547..e0da345bc52 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -70,11 +70,14 @@ export interface Players { "7": Player } -export interface ColorFamily { - range: ColorFamilyRange -} +export type ColorFamily = Partial<{ [K in keyof RampSet]: ColorFamilyRange }> -export type ColorFamilyRange = Partial<{ [K in keyof RampSet]: number }> +export interface ColorFamilyRange { + low: number + high: number + range: number + scaling_value: number +} export interface Shadow { blur: number @@ -169,9 +172,7 @@ export function create_theme(theme: ThemeConfig): Theme { "7": player(ramps.yellow), } - const color_family = { - range: build_color_family(ramps) - } + const color_family = build_color_family(ramps) return { name, @@ -199,11 +200,23 @@ function player(ramp: Scale): Player { } } -function build_color_family(ramps: RampSet): ColorFamilyRange { - const color_family: ColorFamilyRange = {} +function build_color_family(ramps: RampSet): ColorFamily { + const color_family: ColorFamily = {} for (const ramp in ramps) { - color_family[ramp as keyof RampSet] = chroma(ramps[ramp as keyof RampSet](0.5)).luminance() + const ramp_value = ramps[ramp as keyof RampSet] + + const lightnessValues = [ramp_value(0).get('hsl.l') * 100, ramp_value(1).get('hsl.l') * 100] + const low = Math.min(...lightnessValues) + const high = Math.max(...lightnessValues) + const range = high - low + + color_family[ramp as keyof RampSet] = { + low, + high, + range, + scaling_value: 100 / range, + } } return color_family From 1b03c5d69c20ebc4dca4790ccf5be43a6afe5011 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 25 Jul 2023 17:32:31 -0600 Subject: [PATCH 003/105] Pass PaintContext to Element::paint I want to use this on another branch, but it's a sweeping change, so this prepares the ground for it. This can always be reverted if it doesn't work out. --- crates/collab_ui/src/collab_titlebar_item.rs | 6 +- crates/collab_ui/src/face_pile.rs | 4 +- crates/editor/src/element.rs | 13 ++- crates/gpui/src/app.rs | 87 +++++++++++++++++++ crates/gpui/src/app/window.rs | 6 +- crates/gpui/src/elements.rs | 26 ++++-- crates/gpui/src/elements/align.rs | 5 +- crates/gpui/src/elements/canvas.rs | 4 +- crates/gpui/src/elements/clipped.rs | 5 +- crates/gpui/src/elements/constrained_box.rs | 5 +- crates/gpui/src/elements/container.rs | 5 +- crates/gpui/src/elements/empty.rs | 4 +- crates/gpui/src/elements/expanded.rs | 5 +- crates/gpui/src/elements/flex.rs | 8 +- crates/gpui/src/elements/hook.rs | 5 +- crates/gpui/src/elements/image.rs | 6 +- crates/gpui/src/elements/keystroke_label.rs | 2 +- crates/gpui/src/elements/label.rs | 4 +- crates/gpui/src/elements/list.rs | 10 +-- .../gpui/src/elements/mouse_event_handler.rs | 6 +- crates/gpui/src/elements/overlay.rs | 6 +- crates/gpui/src/elements/resizable.rs | 6 +- crates/gpui/src/elements/stack.rs | 5 +- crates/gpui/src/elements/svg.rs | 3 +- crates/gpui/src/elements/text.rs | 6 +- crates/gpui/src/elements/tooltip.rs | 6 +- crates/gpui/src/elements/uniform_list.rs | 4 +- crates/gpui/src/fonts.rs | 26 ++++++ crates/terminal_view/src/terminal_element.rs | 7 +- crates/workspace/src/pane.rs | 6 +- crates/workspace/src/pane_group.rs | 6 +- crates/workspace/src/status_bar.rs | 6 +- 32 files changed, 223 insertions(+), 80 deletions(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index ce8d10d655d..04abdf8c1cc 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -15,8 +15,8 @@ use gpui::{ geometry::{rect::RectF, vector::vec2f, PathBuilder}, json::{self, ToJson}, platform::{CursorStyle, MouseButton}, - AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View, - ViewContext, ViewHandle, WeakViewHandle, + AppContext, Entity, ImageData, LayoutContext, ModelHandle, PaintContext, SceneBuilder, + Subscription, View, ViewContext, ViewHandle, WeakViewHandle, }; use picker::PickerEvent; use project::{Project, RepositoryEntry}; @@ -1312,7 +1312,7 @@ impl Element for AvatarRibbon { _: RectF, _: &mut Self::LayoutState, _: &mut CollabTitlebarItem, - _: &mut ViewContext, + _: &mut PaintContext, ) -> Self::PaintState { let mut path = PathBuilder::new(); path.reset(bounds.lower_left()); diff --git a/crates/collab_ui/src/face_pile.rs b/crates/collab_ui/src/face_pile.rs index 1bbceee9af1..9685d86b402 100644 --- a/crates/collab_ui/src/face_pile.rs +++ b/crates/collab_ui/src/face_pile.rs @@ -7,7 +7,7 @@ use gpui::{ }, json::ToJson, serde_json::{self, json}, - AnyElement, Axis, Element, LayoutContext, SceneBuilder, ViewContext, + AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext, }; use crate::CollabTitlebarItem; @@ -54,7 +54,7 @@ impl Element for FacePile { visible_bounds: RectF, _layout: &mut Self::LayoutState, view: &mut CollabTitlebarItem, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b48fa5b56dd..94a849b0d05 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -32,7 +32,7 @@ use gpui::{ platform::{CursorStyle, Modifiers, MouseButton, MouseButtonEvent, MouseMovedEvent}, text_layout::{self, Line, RunStyle, TextLayoutCache}, AnyElement, Axis, Border, CursorRegion, Element, EventContext, FontCache, LayoutContext, - MouseRegion, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext, + MouseRegion, PaintContext, Quad, SceneBuilder, SizeConstraint, ViewContext, WindowContext, }; use itertools::Itertools; use json::json; @@ -2455,7 +2455,7 @@ impl Element for EditorElement { visible_bounds: RectF, layout: &mut Self::LayoutState, editor: &mut Editor, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); scene.push_layer(Some(visible_bounds)); @@ -3051,7 +3051,14 @@ mod tests { let mut scene = SceneBuilder::new(1.0); let bounds = RectF::new(Default::default(), size); editor.update(cx, |editor, cx| { - element.paint(&mut scene, bounds, bounds, &mut state, editor, cx); + element.paint( + &mut scene, + bounds, + bounds, + &mut state, + editor, + &mut PaintContext::new(cx), + ); }); } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7af363d596b..fd22dc466e2 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -44,6 +44,7 @@ use window_input_handler::WindowInputHandler; use crate::{ elements::{AnyElement, AnyRootElement, RootElement}, executor::{self, Task}, + fonts::TextStyle, json, keymap_matcher::{self, Binding, KeymapContext, KeymapMatcher, Keystroke, MatchResult}, platform::{ @@ -3363,6 +3364,7 @@ pub struct LayoutContext<'a, 'b, 'c, V: View> { view_context: &'c mut ViewContext<'a, 'b, V>, new_parents: &'c mut HashMap, views_to_notify_if_ancestors_change: &'c mut HashMap>, + text_style_stack: Vec>, pub refreshing: bool, } @@ -3377,6 +3379,7 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { view_context, new_parents, views_to_notify_if_ancestors_change, + text_style_stack: Vec::new(), refreshing, } } @@ -3428,6 +3431,24 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { .or_default() .push(self_view_id); } + + pub fn text_style(&self) -> Arc { + self.text_style_stack + .last() + .cloned() + .unwrap_or(Default::default()) + } + + pub fn with_text_style(&mut self, style: S, f: F) -> T + where + S: Into>, + F: FnOnce(&mut Self) -> T, + { + self.text_style_stack.push(style.into()); + let result = f(self); + self.text_style_stack.pop(); + result + } } impl<'a, 'b, 'c, V: View> Deref for LayoutContext<'a, 'b, 'c, V> { @@ -3464,6 +3485,72 @@ impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { } } +pub struct PaintContext<'a, 'b, 'c, V: View> { + view_context: &'c mut ViewContext<'a, 'b, V>, + text_style_stack: Vec>, +} + +impl<'a, 'b, 'c, V: View> PaintContext<'a, 'b, 'c, V> { + pub fn new(view_context: &'c mut ViewContext<'a, 'b, V>) -> Self { + Self { + view_context, + text_style_stack: Vec::new(), + } + } + + pub fn text_style(&self) -> Arc { + self.text_style_stack + .last() + .cloned() + .unwrap_or(Default::default()) + } + + pub fn with_text_style(&mut self, style: S, f: F) -> T + where + S: Into>, + F: FnOnce(&mut Self) -> T, + { + self.text_style_stack.push(style.into()); + let result = f(self); + self.text_style_stack.pop(); + result + } +} + +impl<'a, 'b, 'c, V: View> Deref for PaintContext<'a, 'b, 'c, V> { + type Target = ViewContext<'a, 'b, V>; + + fn deref(&self) -> &Self::Target { + &self.view_context + } +} + +impl DerefMut for PaintContext<'_, '_, '_, V> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.view_context + } +} + +impl BorrowAppContext for PaintContext<'_, '_, '_, V> { + fn read_with T>(&self, f: F) -> T { + BorrowAppContext::read_with(&*self.view_context, f) + } + + fn update T>(&mut self, f: F) -> T { + BorrowAppContext::update(&mut *self.view_context, f) + } +} + +impl BorrowWindowContext for PaintContext<'_, '_, '_, V> { + fn read_with T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_with(&*self.view_context, window_id, f) + } + + fn update T>(&mut self, window_id: usize, f: F) -> T { + BorrowWindowContext::update(&mut *self.view_context, window_id, f) + } +} + pub struct EventContext<'a, 'b, 'c, V: View> { view_context: &'c mut ViewContext<'a, 'b, V>, pub(crate) handled: bool, diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 1dc88d2e717..c6ab7c6ebb2 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -14,8 +14,8 @@ use crate::{ text_layout::TextLayoutCache, util::post_inc, Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, - Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, - View, ViewContext, ViewHandle, WindowInvalidation, + Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, PaintContext, SceneBuilder, + Subscription, View, ViewContext, ViewHandle, WindowInvalidation, }; use anyhow::{anyhow, bail, Result}; use collections::{HashMap, HashSet}; @@ -1400,7 +1400,7 @@ impl Element for ChildView { visible_bounds: RectF, _: &mut Self::LayoutState, _: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { if let Some(mut rendered_view) = cx.window.rendered_views.remove(&self.view_id) { rendered_view diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 78403444fff..5bed935319f 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -33,8 +33,8 @@ use crate::{ rect::RectF, vector::{vec2f, Vector2F}, }, - json, Action, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, WeakViewHandle, - WindowContext, + json, Action, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext, + WeakViewHandle, WindowContext, }; use anyhow::{anyhow, Result}; use collections::HashMap; @@ -61,7 +61,7 @@ pub trait Element: 'static { visible_bounds: RectF, layout: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState; fn rect_for_text_range( @@ -298,7 +298,14 @@ impl> AnyElementState for ElementState { mut layout, } => { let bounds = RectF::new(origin, size); - let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx); + let paint = element.paint( + scene, + bounds, + visible_bounds, + &mut layout, + view, + &mut PaintContext::new(cx), + ); ElementState::PostPaint { element, constraint, @@ -316,7 +323,14 @@ impl> AnyElementState for ElementState { .. } => { let bounds = RectF::new(origin, bounds.size()); - let paint = element.paint(scene, bounds, visible_bounds, &mut layout, view, cx); + let paint = element.paint( + scene, + bounds, + visible_bounds, + &mut layout, + view, + &mut PaintContext::new(cx), + ); ElementState::PostPaint { element, constraint, @@ -513,7 +527,7 @@ impl Element for AnyElement { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { self.paint(scene, bounds.origin(), visible_bounds, view, cx); } diff --git a/crates/gpui/src/elements/align.rs b/crates/gpui/src/elements/align.rs index 165cfcf190c..e60c1ff9074 100644 --- a/crates/gpui/src/elements/align.rs +++ b/crates/gpui/src/elements/align.rs @@ -1,6 +1,7 @@ use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; use json::ToJson; @@ -69,7 +70,7 @@ impl Element for Align { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let my_center = bounds.size() / 2.; let my_target = my_center + my_center * self.alignment; diff --git a/crates/gpui/src/elements/canvas.rs b/crates/gpui/src/elements/canvas.rs index bbd8d0393cd..2d33ba45e54 100644 --- a/crates/gpui/src/elements/canvas.rs +++ b/crates/gpui/src/elements/canvas.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; use super::Element; use crate::{ json::{self, json}, - SceneBuilder, View, ViewContext, + PaintContext, SceneBuilder, View, ViewContext, }; use json::ToJson; use pathfinder_geometry::{ @@ -56,7 +56,7 @@ where visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { self.0(scene, bounds, visible_bounds, view, cx) } diff --git a/crates/gpui/src/elements/clipped.rs b/crates/gpui/src/elements/clipped.rs index a87dc3e7735..4e8cd4bc15e 100644 --- a/crates/gpui/src/elements/clipped.rs +++ b/crates/gpui/src/elements/clipped.rs @@ -4,7 +4,8 @@ use pathfinder_geometry::{rect::RectF, vector::Vector2F}; use serde_json::json; use crate::{ - json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; pub struct Clipped { @@ -37,7 +38,7 @@ impl Element for Clipped { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { scene.paint_layer(Some(bounds), |scene| { self.child diff --git a/crates/gpui/src/elements/constrained_box.rs b/crates/gpui/src/elements/constrained_box.rs index 46916c74f10..0d540a47b44 100644 --- a/crates/gpui/src/elements/constrained_box.rs +++ b/crates/gpui/src/elements/constrained_box.rs @@ -5,7 +5,8 @@ use serde_json::json; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; pub struct ConstrainedBox { @@ -156,7 +157,7 @@ impl Element for ConstrainedBox { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { scene.paint_layer(Some(visible_bounds), |scene| { self.child diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 3b95feb9ef8..656847980c9 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -10,7 +10,8 @@ use crate::{ json::ToJson, platform::CursorStyle, scene::{self, Border, CursorRegion, Quad}, - AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -214,7 +215,7 @@ impl Element for Container { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let quad_bounds = RectF::from_points( bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top), diff --git a/crates/gpui/src/elements/empty.rs b/crates/gpui/src/elements/empty.rs index 42a3824bfcb..70580684538 100644 --- a/crates/gpui/src/elements/empty.rs +++ b/crates/gpui/src/elements/empty.rs @@ -6,7 +6,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, - LayoutContext, SceneBuilder, View, ViewContext, + LayoutContext, PaintContext, SceneBuilder, View, ViewContext, }; use crate::{Element, SizeConstraint}; @@ -57,7 +57,7 @@ impl Element for Empty { _: RectF, _: &mut Self::LayoutState, _: &mut V, - _: &mut ViewContext, + _: &mut PaintContext, ) -> Self::PaintState { } diff --git a/crates/gpui/src/elements/expanded.rs b/crates/gpui/src/elements/expanded.rs index 1fb935b2b8f..1f4f2f40a1e 100644 --- a/crates/gpui/src/elements/expanded.rs +++ b/crates/gpui/src/elements/expanded.rs @@ -2,7 +2,8 @@ use std::ops::Range; use crate::{ geometry::{rect::RectF, vector::Vector2F}, - json, AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + json, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; use serde_json::json; @@ -61,7 +62,7 @@ impl Element for Expanded { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { self.child .paint(scene, bounds.origin(), visible_bounds, view, cx); diff --git a/crates/gpui/src/elements/flex.rs b/crates/gpui/src/elements/flex.rs index 857f3f56fc0..3000b9575d5 100644 --- a/crates/gpui/src/elements/flex.rs +++ b/crates/gpui/src/elements/flex.rs @@ -2,8 +2,8 @@ use std::{any::Any, cell::Cell, f32::INFINITY, ops::Range, rc::Rc}; use crate::{ json::{self, ToJson, Value}, - AnyElement, Axis, Element, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, - Vector2FExt, View, ViewContext, + AnyElement, Axis, Element, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, + SizeConstraint, Vector2FExt, View, ViewContext, }; use pathfinder_geometry::{ rect::RectF, @@ -258,7 +258,7 @@ impl Element for Flex { visible_bounds: RectF, remaining_space: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); @@ -449,7 +449,7 @@ impl Element for FlexItem { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { self.child .paint(scene, bounds.origin(), visible_bounds, view, cx) diff --git a/crates/gpui/src/elements/hook.rs b/crates/gpui/src/elements/hook.rs index 310b3c25ebe..7ac6c95f3e5 100644 --- a/crates/gpui/src/elements/hook.rs +++ b/crates/gpui/src/elements/hook.rs @@ -3,7 +3,8 @@ use std::ops::Range; use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::json, - AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; pub struct Hook { @@ -52,7 +53,7 @@ impl Element for Hook { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { self.child .paint(scene, bounds.origin(), visible_bounds, view, cx); diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index df200eae7ff..e87c0659175 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -5,8 +5,8 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, - scene, Border, Element, ImageData, LayoutContext, SceneBuilder, SizeConstraint, View, - ViewContext, + scene, Border, Element, ImageData, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, + View, ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -97,7 +97,7 @@ impl Element for Image { _: RectF, layout: &mut Self::LayoutState, _: &mut V, - _: &mut ViewContext, + _: &mut PaintContext, ) -> Self::PaintState { if let Some(data) = layout { scene.push_image(scene::Image { diff --git a/crates/gpui/src/elements/keystroke_label.rs b/crates/gpui/src/elements/keystroke_label.rs index c011649b2e9..268f3ccb1c8 100644 --- a/crates/gpui/src/elements/keystroke_label.rs +++ b/crates/gpui/src/elements/keystroke_label.rs @@ -66,7 +66,7 @@ impl Element for KeystrokeLabel { visible_bounds: RectF, element: &mut AnyElement, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { element.paint(scene, bounds.origin(), visible_bounds, view, cx); } diff --git a/crates/gpui/src/elements/label.rs b/crates/gpui/src/elements/label.rs index d9cf537333c..80555c7442c 100644 --- a/crates/gpui/src/elements/label.rs +++ b/crates/gpui/src/elements/label.rs @@ -8,7 +8,7 @@ use crate::{ }, json::{ToJson, Value}, text_layout::{Line, RunStyle}, - Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -163,7 +163,7 @@ impl Element for Label { visible_bounds: RectF, line: &mut Self::LayoutState, _: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); line.paint( diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index 4c6298d8f55..54545199056 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -4,8 +4,8 @@ use crate::{ vector::{vec2f, Vector2F}, }, json::json, - AnyElement, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View, - ViewContext, + AnyElement, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, SizeConstraint, + View, ViewContext, }; use std::{cell::RefCell, collections::VecDeque, fmt::Debug, ops::Range, rc::Rc}; use sum_tree::{Bias, SumTree}; @@ -255,7 +255,7 @@ impl Element for List { visible_bounds: RectF, scroll_top: &mut ListOffset, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); scene.push_layer(Some(visible_bounds)); @@ -647,7 +647,7 @@ impl<'a> sum_tree::SeekTarget<'a, ListItemSummary, ListItemSummary> for Height { #[cfg(test)] mod tests { use super::*; - use crate::{elements::Empty, geometry::vector::vec2f, Entity}; + use crate::{elements::Empty, geometry::vector::vec2f, Entity, PaintContext}; use rand::prelude::*; use std::env; @@ -988,7 +988,7 @@ mod tests { _: RectF, _: &mut (), _: &mut V, - _: &mut ViewContext, + _: &mut PaintContext, ) { unimplemented!() } diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index 1b8142d9646..6005277f735 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -10,8 +10,8 @@ use crate::{ CursorRegion, HandlerSet, MouseClick, MouseClickOut, MouseDown, MouseDownOut, MouseDrag, MouseHover, MouseMove, MouseMoveOut, MouseScrollWheel, MouseUp, MouseUpOut, }, - AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, SceneBuilder, - SizeConstraint, View, ViewContext, + AnyElement, Element, EventContext, LayoutContext, MouseRegion, MouseState, PaintContext, + SceneBuilder, SizeConstraint, View, ViewContext, }; use serde_json::json; use std::{marker::PhantomData, ops::Range}; @@ -256,7 +256,7 @@ impl Element for MouseEventHandler { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { if self.above { self.child diff --git a/crates/gpui/src/elements/overlay.rs b/crates/gpui/src/elements/overlay.rs index 0f7e4a35c67..56e3d10de3c 100644 --- a/crates/gpui/src/elements/overlay.rs +++ b/crates/gpui/src/elements/overlay.rs @@ -3,8 +3,8 @@ use std::ops::Range; use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::ToJson, - AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View, - ViewContext, + AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, + SizeConstraint, View, ViewContext, }; use serde_json::json; @@ -143,7 +143,7 @@ impl Element for Overlay { _: RectF, size: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { let (anchor_position, mut bounds) = match self.position_mode { OverlayPositionMode::Window => { diff --git a/crates/gpui/src/elements/resizable.rs b/crates/gpui/src/elements/resizable.rs index da4b3473b30..2e252cfaabb 100644 --- a/crates/gpui/src/elements/resizable.rs +++ b/crates/gpui/src/elements/resizable.rs @@ -7,8 +7,8 @@ use crate::{ geometry::rect::RectF, platform::{CursorStyle, MouseButton}, scene::MouseDrag, - AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View, - ViewContext, + AnyElement, Axis, Element, LayoutContext, MouseRegion, PaintContext, SceneBuilder, + SizeConstraint, View, ViewContext, }; #[derive(Copy, Clone, Debug)] @@ -125,7 +125,7 @@ impl Element for Resizable { visible_bounds: pathfinder_geometry::rect::RectF, constraint: &mut SizeConstraint, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { scene.push_stacking_context(None, None); diff --git a/crates/gpui/src/elements/stack.rs b/crates/gpui/src/elements/stack.rs index 196c04d2034..8f9d1e4d053 100644 --- a/crates/gpui/src/elements/stack.rs +++ b/crates/gpui/src/elements/stack.rs @@ -3,7 +3,8 @@ use std::ops::Range; use crate::{ geometry::{rect::RectF, vector::Vector2F}, json::{self, json, ToJson}, - AnyElement, Element, LayoutContext, SceneBuilder, SizeConstraint, View, ViewContext, + AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, + ViewContext, }; /// Element which renders it's children in a stack on top of each other. @@ -57,7 +58,7 @@ impl Element for Stack { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { for child in &mut self.children { scene.paint_layer(None, |scene| { diff --git a/crates/gpui/src/elements/svg.rs b/crates/gpui/src/elements/svg.rs index 9792f16cbe1..c4d58cd7a77 100644 --- a/crates/gpui/src/elements/svg.rs +++ b/crates/gpui/src/elements/svg.rs @@ -1,5 +1,6 @@ use super::constrain_size_preserving_aspect_ratio; use crate::json::ToJson; +use crate::PaintContext; use crate::{ color::Color, geometry::{ @@ -73,7 +74,7 @@ impl Element for Svg { _visible_bounds: RectF, svg: &mut Self::LayoutState, _: &mut V, - _: &mut ViewContext, + _: &mut PaintContext, ) { if let Some(svg) = svg.clone() { scene.push_icon(scene::Icon { diff --git a/crates/gpui/src/elements/text.rs b/crates/gpui/src/elements/text.rs index 66654fbe93f..9357c31f193 100644 --- a/crates/gpui/src/elements/text.rs +++ b/crates/gpui/src/elements/text.rs @@ -7,8 +7,8 @@ use crate::{ }, json::{ToJson, Value}, text_layout::{Line, RunStyle, ShapedBoundary}, - AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache, - View, ViewContext, + AppContext, Element, FontCache, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, + TextLayoutCache, View, ViewContext, }; use log::warn; use serde_json::json; @@ -171,7 +171,7 @@ impl Element for Text { visible_bounds: RectF, layout: &mut Self::LayoutState, _: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let mut origin = bounds.origin(); let empty = Vec::new(); diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index f21b1c363c0..0510baa9e45 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -6,8 +6,8 @@ use crate::{ fonts::TextStyle, geometry::{rect::RectF, vector::Vector2F}, json::json, - Action, Axis, ElementStateHandle, LayoutContext, SceneBuilder, SizeConstraint, Task, View, - ViewContext, + Action, Axis, ElementStateHandle, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, + Task, View, ViewContext, }; use schemars::JsonSchema; use serde::Deserialize; @@ -194,7 +194,7 @@ impl Element for Tooltip { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) { self.child .paint(scene, bounds.origin(), visible_bounds, view, cx); diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 8344914da0d..b9bfadb17f1 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -6,7 +6,7 @@ use crate::{ }, json::{self, json}, platform::ScrollWheelEvent, - AnyElement, LayoutContext, MouseRegion, SceneBuilder, View, ViewContext, + AnyElement, LayoutContext, MouseRegion, PaintContext, SceneBuilder, View, ViewContext, }; use json::ToJson; use std::{cell::RefCell, cmp, ops::Range, rc::Rc}; @@ -278,7 +278,7 @@ impl Element for UniformList { visible_bounds: RectF, layout: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = visible_bounds.intersection(bounds).unwrap_or_default(); diff --git a/crates/gpui/src/fonts.rs b/crates/gpui/src/fonts.rs index 3b4a94dd0e6..b003042866e 100644 --- a/crates/gpui/src/fonts.rs +++ b/crates/gpui/src/fonts.rs @@ -71,6 +71,32 @@ pub struct TextStyle { pub underline: Underline, } +impl TextStyle { + pub fn refine(self, refinement: TextStyleRefinement) -> TextStyle { + TextStyle { + color: refinement.color.unwrap_or(self.color), + font_family_name: refinement + .font_family_name + .unwrap_or_else(|| self.font_family_name.clone()), + font_family_id: refinement.font_family_id.unwrap_or(self.font_family_id), + font_id: refinement.font_id.unwrap_or(self.font_id), + font_size: refinement.font_size.unwrap_or(self.font_size), + font_properties: refinement.font_properties.unwrap_or(self.font_properties), + underline: refinement.underline.unwrap_or(self.underline), + } + } +} + +pub struct TextStyleRefinement { + pub color: Option, + pub font_family_name: Option>, + pub font_family_id: Option, + pub font_id: Option, + pub font_size: Option, + pub font_properties: Option, + pub underline: Option, +} + #[derive(JsonSchema)] #[serde(remote = "Properties")] pub struct PropertiesDef { diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index e29beb3ad52..9c402d139a0 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -10,8 +10,9 @@ use gpui::{ platform::{CursorStyle, MouseButton}, serde_json::json, text_layout::{Line, RunStyle}, - AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion, Quad, - SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext, WeakModelHandle, + AnyElement, Element, EventContext, FontCache, LayoutContext, ModelContext, MouseRegion, + PaintContext, Quad, SceneBuilder, SizeConstraint, TextLayoutCache, ViewContext, + WeakModelHandle, }; use itertools::Itertools; use language::CursorShape; @@ -730,7 +731,7 @@ impl Element for TerminalElement { visible_bounds: RectF, layout: &mut Self::LayoutState, view: &mut TerminalView, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 2972c307f26..928c9b23f82 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -25,8 +25,8 @@ use gpui::{ keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton, NavigationDirection, PromptLevel}, Action, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, EventContext, - LayoutContext, ModelHandle, MouseRegion, Quad, Task, View, ViewContext, ViewHandle, - WeakViewHandle, WindowContext, + LayoutContext, ModelHandle, MouseRegion, PaintContext, Quad, Task, View, ViewContext, + ViewHandle, WeakViewHandle, WindowContext, }; use project::{Project, ProjectEntryId, ProjectPath}; use serde::Deserialize; @@ -1896,7 +1896,7 @@ impl Element for PaneBackdrop { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut V, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let background = theme::current(cx).editor.background; diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index e60f6deb2ff..4a90d92b350 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -593,8 +593,8 @@ mod element { }, json::{self, ToJson}, platform::{CursorStyle, MouseButton}, - AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, RectFExt, - SceneBuilder, SizeConstraint, Vector2FExt, ViewContext, + AnyElement, Axis, CursorRegion, Element, LayoutContext, MouseRegion, PaintContext, + RectFExt, SceneBuilder, SizeConstraint, Vector2FExt, ViewContext, }; use crate::{ @@ -765,7 +765,7 @@ mod element { visible_bounds: RectF, remaining_space: &mut Self::LayoutState, view: &mut Workspace, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let can_resize = settings::get::(cx).active_pane_magnification == 1.; let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); diff --git a/crates/workspace/src/status_bar.rs b/crates/workspace/src/status_bar.rs index 6fc1467566b..3def545d710 100644 --- a/crates/workspace/src/status_bar.rs +++ b/crates/workspace/src/status_bar.rs @@ -8,8 +8,8 @@ use gpui::{ vector::{vec2f, Vector2F}, }, json::{json, ToJson}, - AnyElement, AnyViewHandle, Entity, LayoutContext, SceneBuilder, SizeConstraint, Subscription, - View, ViewContext, ViewHandle, WindowContext, + AnyElement, AnyViewHandle, Entity, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, + Subscription, View, ViewContext, ViewHandle, WindowContext, }; pub trait StatusItemView: View { @@ -177,7 +177,7 @@ impl Element for StatusBarElement { visible_bounds: RectF, _: &mut Self::LayoutState, view: &mut StatusBar, - cx: &mut ViewContext, + cx: &mut PaintContext, ) -> Self::PaintState { let origin_y = bounds.upper_right().y(); let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); From d3b89e16f26d24965267637000e642382cb4b675 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 28 Jul 2023 14:56:13 -0700 Subject: [PATCH 004/105] Make wrap guides respect scroll position --- crates/editor/src/element.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index b9bf74ee85f..cb46e74af0b 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -546,8 +546,18 @@ impl EditorElement { }); } + let scroll_left = + layout.position_map.snapshot.scroll_position().x() * layout.position_map.em_width; + for (wrap_position, active) in layout.wrap_guides.iter() { - let x = text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.; + let x = + (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.) + - scroll_left; + + if x < text_bounds.origin_x() { + continue; + } + let color = if *active { self.style.active_wrap_guide } else { From fe43bacb6fa9a22d57d174df92a05a8cb6739e9d Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 28 Jul 2023 18:53:24 -0400 Subject: [PATCH 005/105] Put LiveKitBridge Swift build directory in `target` Helps it get caught in a cargo clean --- crates/live_kit_client/build.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/live_kit_client/build.rs b/crates/live_kit_client/build.rs index bcd3f76dca9..3fa0e003e74 100644 --- a/crates/live_kit_client/build.rs +++ b/crates/live_kit_client/build.rs @@ -58,11 +58,14 @@ fn build_bridge(swift_target: &SwiftTarget) { "cargo:rerun-if-changed={}/Package.resolved", SWIFT_PACKAGE_NAME ); + let swift_package_root = swift_package_root(); + let swift_target_folder = swift_target_folder(); if !Command::new("swift") .arg("build") .args(["--configuration", &env::var("PROFILE").unwrap()]) .args(["--triple", &swift_target.target.triple]) + .args(["--build-path".into(), swift_target_folder]) .current_dir(&swift_package_root) .status() .unwrap() @@ -128,6 +131,12 @@ fn swift_package_root() -> PathBuf { env::current_dir().unwrap().join(SWIFT_PACKAGE_NAME) } +fn swift_target_folder() -> PathBuf { + env::current_dir() + .unwrap() + .join(format!("../../target/{SWIFT_PACKAGE_NAME}")) +} + fn copy_dir(source: &Path, destination: &Path) { assert!( Command::new("rm") @@ -155,8 +164,7 @@ fn copy_dir(source: &Path, destination: &Path) { impl SwiftTarget { fn out_dir_path(&self) -> PathBuf { - swift_package_root() - .join(".build") + swift_target_folder() .join(&self.target.unversioned_triple) .join(env::var("PROFILE").unwrap()) } From 2c47efcce91328ee8d567f2871fae1e3cd107b63 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 22:36:15 -0400 Subject: [PATCH 006/105] Add a command to collapse all entires --- crates/project_panel/src/project_panel.rs | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index e6e1cff5981..b650d272fbd 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -115,6 +115,7 @@ actions!( [ ExpandSelectedEntry, CollapseSelectedEntry, + CollapseAllEntries, NewDirectory, NewFile, Copy, @@ -140,6 +141,7 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { file_associations::init(assets, cx); cx.add_action(ProjectPanel::expand_selected_entry); cx.add_action(ProjectPanel::collapse_selected_entry); + cx.add_action(ProjectPanel::collapse_all_entries); cx.add_action(ProjectPanel::select_prev); cx.add_action(ProjectPanel::select_next); cx.add_action(ProjectPanel::new_file); @@ -514,6 +516,12 @@ impl ProjectPanel { } } + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { @@ -2678,6 +2686,73 @@ mod tests { ); } + #[gpui::test] + async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { + init_test_with_editor(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/project_root", + json!({ + "dir_1": { + "nested_dir": { + "file_a.py": "# File contents", + "file_b.py": "# File contents", + "file_c.py": "# File contents", + }, + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + }, + "dir_2": { + "file_1.py": "# File contents", + "file_2.py": "# File contents", + "file_3.py": "# File contents", + } + }), + ) + .await; + + let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; + let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + + let new_search_events_count = Arc::new(AtomicUsize::new(0)); + let _subscription = panel.update(cx, |_, cx| { + let subcription_count = Arc::clone(&new_search_events_count); + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!(event, Event::NewSearchInDirectory { .. }) { + subcription_count.fetch_add(1, atomic::Ordering::SeqCst); + } + }) + }); + + panel.update(cx, |panel, cx| { + panel.collapse_all_entries(&CollapseAllEntries, cx) + }); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &["v project_root", " > dir_1", " > dir_2",] + ); + + // Open dir_1 and make sure nested_dir was collapsed during + toggle_expand_dir(&panel, "project_root/dir_1", cx); + cx.foreground().run_until_parked(); + assert_eq!( + visible_entries_as_strings(&panel, 0..10, cx), + &[ + "v project_root", + " v dir_1 <== selected", + " > nested_dir", + " file_1.py", + " file_2.py", + " file_3.py", + " > dir_2", + ] + ); + } + fn toggle_expand_dir( panel: &ViewHandle, path: impl AsRef, @@ -2878,3 +2953,4 @@ mod tests { }); } } +// TODO - a workspace command? From b0e81c58dc9d85a93153563b1c71753c425c4247 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 23:06:40 -0400 Subject: [PATCH 007/105] Remove unused code in test --- crates/project_panel/src/project_panel.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b650d272fbd..b2b8b2e4bd7 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2717,16 +2717,6 @@ mod tests { let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - let new_search_events_count = Arc::new(AtomicUsize::new(0)); - let _subscription = panel.update(cx, |_, cx| { - let subcription_count = Arc::clone(&new_search_events_count); - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!(event, Event::NewSearchInDirectory { .. }) { - subcription_count.fetch_add(1, atomic::Ordering::SeqCst); - } - }) - }); - panel.update(cx, |panel, cx| { panel.collapse_all_entries(&CollapseAllEntries, cx) }); From 0bd6e7bac3179b49f69125b573c78dce438f286a Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 28 Jul 2023 23:13:36 -0400 Subject: [PATCH 008/105] Fix comment --- crates/project_panel/src/project_panel.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index b2b8b2e4bd7..0be52646e63 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -2726,7 +2726,7 @@ mod tests { &["v project_root", " > dir_1", " > dir_2",] ); - // Open dir_1 and make sure nested_dir was collapsed during + // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries toggle_expand_dir(&panel, "project_root/dir_1", cx); cx.foreground().run_until_parked(); assert_eq!( From d58f031696ce039c0068344803551558c391d85c Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Fri, 28 Jul 2023 22:27:36 -0700 Subject: [PATCH 009/105] disable wrap guides in the assitant panel --- crates/ai/src/assistant.rs | 1 + crates/editor/src/editor.rs | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 8a4c04d3387..957c5e1c063 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1637,6 +1637,7 @@ impl ConversationEditor { let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_gutter(false, cx); + editor.set_show_wrap_guides(false, cx); editor }); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index b4145edb648..5270d6f9518 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -543,6 +543,7 @@ pub struct Editor { show_local_selections: bool, mode: EditorMode, show_gutter: bool, + show_wrap_guides: Option, placeholder_text: Option>, highlighted_rows: Option>, #[allow(clippy::type_complexity)] @@ -1375,6 +1376,7 @@ impl Editor { show_local_selections: true, mode, show_gutter: mode == EditorMode::Full, + show_wrap_guides: None, placeholder_text: None, highlighted_rows: None, background_highlights: Default::default(), @@ -7187,6 +7189,10 @@ impl Editor { pub fn wrap_guides(&self, cx: &AppContext) -> SmallVec<[(usize, bool); 2]> { let mut wrap_guides = smallvec::smallvec![]; + if self.show_wrap_guides == Some(false) { + return wrap_guides; + } + let settings = self.buffer.read(cx).settings_at(0, cx); if settings.show_wrap_guides { if let SoftWrap::Column(soft_wrap) = self.soft_wrap_mode(cx) { @@ -7244,6 +7250,11 @@ impl Editor { cx.notify(); } + pub fn set_show_wrap_guides(&mut self, show_gutter: bool, cx: &mut ViewContext) { + self.show_wrap_guides = Some(show_gutter); + cx.notify(); + } + pub fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { if let Some(buffer) = self.buffer().read(cx).as_singleton() { if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local()) { From 8926266952948063a666a248f2e4b7bf8edcfde6 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Sat, 29 Jul 2023 23:53:16 -0700 Subject: [PATCH 010/105] Halve opacity on wrap guides --- styles/src/style_tree/editor.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index 832e7762649..deab45d4b21 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -182,8 +182,8 @@ export default function editor(): any { line_number: with_opacity(foreground(layer), 0.35), line_number_active: foreground(layer), rename_fade: 0.6, - wrap_guide: with_opacity(foreground(layer), 0.1), - active_wrap_guide: with_opacity(foreground(layer), 0.2), + wrap_guide: with_opacity(foreground(layer), 0.05), + active_wrap_guide: with_opacity(foreground(layer), 0.1), unnecessary_code_fade: 0.5, selection: theme.players[0], whitespace: theme.ramps.neutral(0.5).hex(), From a5dd8dd0a9c14e85bc95a8b84989366f5ff590fa Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 31 Jul 2023 10:02:28 -0400 Subject: [PATCH 011/105] add lua embedding query for semantic search --- Cargo.lock | 39 +++---- crates/semantic_index/Cargo.toml | 1 + .../src/semantic_index_tests.rs | 102 ++++++++++++++++++ crates/zed/src/languages/lua/config.toml | 1 + crates/zed/src/languages/lua/embedding.scm | 10 ++ 5 files changed, 134 insertions(+), 19 deletions(-) create mode 100644 crates/zed/src/languages/lua/embedding.scm diff --git a/Cargo.lock b/Cargo.lock index 7c6a213d0d4..6c558cbb090 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2042,9 +2042,9 @@ dependencies = [ [[package]] name = "curl-sys" -version = "0.4.64+curl-8.2.0" +version = "0.4.65+curl-8.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f96069f0b1cb1241c838740659a771ef143363f52772a9ce1bd9c04c75eee0dc" +checksum = "961ba061c9ef2fe34bbd12b807152d96f0badd2bebe7b90ce6c8c8b7572a0986" dependencies = [ "cc", "libc", @@ -3033,9 +3033,9 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" [[package]] name = "globset" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1391ab1f92ffcc08911957149833e682aa3fe252b9f45f966d2ef972274c97df" +checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006" dependencies = [ "aho-corasick 1.0.2", "bstr", @@ -6688,6 +6688,7 @@ dependencies = [ "tree-sitter-cpp 0.20.2", "tree-sitter-elixir 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tree-sitter-json 0.19.0", + "tree-sitter-lua", "tree-sitter-rust", "tree-sitter-toml 0.20.0", "tree-sitter-typescript 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6722,18 +6723,18 @@ checksum = "5a9f47faea3cad316faa914d013d24f471cd90bfca1a0c70f05a3f42c6441e99" [[package]] name = "serde" -version = "1.0.175" +version = "1.0.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d25439cd7397d044e2748a6fe2432b5e85db703d6d097bd014b3c0ad1ebff0b" +checksum = "63ba2516aa6bf82e0b19ca8b50019d52df58455d3cf9bdaf6315225fdd0c560a" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.175" +version = "1.0.177" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b23f7ade6f110613c0d63858ddb8b94c1041f550eab58a16b371bdf2c9c80ab4" +checksum = "401797fe7833d72109fedec6bfcbe67c0eed9b99772f26eb8afd261f0abc6fd3" dependencies = [ "proc-macro2", "quote", @@ -6762,9 +6763,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "indexmap 2.0.0", "itoa 1.0.9", @@ -6786,9 +6787,9 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e168eaaf71e8f9bd6037feb05190485708e019f4fd87d161b3c0a0d37daf85e5" +checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", @@ -9031,9 +9032,9 @@ checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" [[package]] name = "wasm-encoder" -version = "0.31.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a3d1b4a575ffb873679402b2aedb3117555eb65c27b1b86c8a91e574bc2a2a" +checksum = "41763f20eafed1399fff1afb466496d3a959f58241436cfdc17e3f5ca954de16" dependencies = [ "leb128", ] @@ -9255,9 +9256,9 @@ dependencies = [ [[package]] name = "wast" -version = "62.0.0" +version = "62.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7f7ee878019d69436895f019b65f62c33da63595d8e857cbdc87c13ecb29a32" +checksum = "b8ae06f09dbe377b889fbd620ff8fa21e1d49d1d9d364983c0cdbf9870cb9f1f" dependencies = [ "leb128", "memchr", @@ -9267,11 +9268,11 @@ dependencies = [ [[package]] name = "wat" -version = "1.0.68" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "295572bf24aa5b685a971a83ad3e8b6e684aaad8a9be24bc7bf59bed84cc1c08" +checksum = "842e15861d203fb4a96d314b0751cdeaf0f6f8b35e8d81d2953af2af5e44e637" dependencies = [ - "wast 62.0.0", + "wast 62.0.1", ] [[package]] diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index a1f126bfb84..942f61d2988 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -60,3 +60,4 @@ tree-sitter-rust = "*" tree-sitter-toml = "*" tree-sitter-cpp = "*" tree-sitter-elixir = "*" +tree-sitter-lua = "*" diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index acf5a9d72b4..4eedade69d9 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -486,6 +486,79 @@ async fn test_code_context_retrieval_javascript() { ) } +#[gpui::test] +async fn test_code_context_retrieval_lua() { + let language = lua_lang(); + let mut retriever = CodeContextRetriever::new(); + + let text = r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end + end + "#.unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + (r#" + -- Creates a new class + -- @param baseclass The Baseclass of this class, or nil. + -- @return A new class reference. + function classes.class(baseclass) + -- Create the class definition and metatable. + local classdef = {} + -- Find the super class, either Object or user-defined. + baseclass = baseclass or classes.Object + -- If this class definition does not know of a function, it will 'look up' to the Baseclass via the __index of the metatable. + setmetatable(classdef, { __index = baseclass }) + -- All class instances have a reference to the class object. + classdef.class = classdef + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + --[ ... ]-- + --[ ... ]-- + end + end"#.unindent(), + 114), + (r#" + --- Recursivly allocates the inheritance tree of the instance. + -- @param mastertable The 'root' of the inheritance tree. + -- @return Returns the instance with the allocated inheritance tree. + function classdef.alloc(mastertable) + -- All class instances have a reference to a superclass object. + local instance = { super = baseclass.alloc(mastertable) } + -- Any functions this instance does not know of will 'look up' to the superclass definition. + setmetatable(instance, { __index = classdef, __newindex = mastertable }) + return instance + end"#.unindent(), 809), + ] + ); +} + #[gpui::test] async fn test_code_context_retrieval_elixir() { let language = elixir_lang(); @@ -1084,6 +1157,35 @@ fn cpp_lang() -> Arc { ) } +fn lua_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Lua".into(), + path_suffixes: vec!["lua".into()], + collapsed_placeholder: "--[ ... ]--".to_string(), + ..Default::default() + }, + Some(tree_sitter_lua::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + (function_declaration + "function" @name + name: (_) @name + (comment)* @collapse + body: (block) @collapse + ) @item + ) + "#, + ) + .unwrap(), + ) +} + fn elixir_lang() -> Arc { Arc::new( Language::new( diff --git a/crates/zed/src/languages/lua/config.toml b/crates/zed/src/languages/lua/config.toml index fe44a3d2aaa..d3e44edfe97 100644 --- a/crates/zed/src/languages/lua/config.toml +++ b/crates/zed/src/languages/lua/config.toml @@ -7,3 +7,4 @@ brackets = [ { start = "[", end = "]", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, ] +collapsed_placeholder = "--[ ... ]--" diff --git a/crates/zed/src/languages/lua/embedding.scm b/crates/zed/src/languages/lua/embedding.scm new file mode 100644 index 00000000000..0d1065089fc --- /dev/null +++ b/crates/zed/src/languages/lua/embedding.scm @@ -0,0 +1,10 @@ +( + (comment)* @context + . + (function_declaration + "function" @name + name: (_) @name + (comment)* @collapse + body: (block) @collapse + ) @item +) From ca4e21881efc5d0293e545a1059971cab356bc54 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 31 Jul 2023 10:54:30 -0400 Subject: [PATCH 012/105] add ruby support for semantic search --- Cargo.lock | 1 + crates/semantic_index/Cargo.toml | 1 + .../src/semantic_index_tests.rs | 231 ++++++++++++++++++ crates/zed/src/languages/ruby/config.toml | 1 + crates/zed/src/languages/ruby/embedding.scm | 22 ++ 5 files changed, 256 insertions(+) create mode 100644 crates/zed/src/languages/ruby/embedding.scm diff --git a/Cargo.lock b/Cargo.lock index 6c558cbb090..b4a8f13bea5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6689,6 +6689,7 @@ dependencies = [ "tree-sitter-elixir 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", "tree-sitter-json 0.19.0", "tree-sitter-lua", + "tree-sitter-ruby", "tree-sitter-rust", "tree-sitter-toml 0.20.0", "tree-sitter-typescript 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 942f61d2988..637f3f44874 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -61,3 +61,4 @@ tree-sitter-toml = "*" tree-sitter-cpp = "*" tree-sitter-elixir = "*" tree-sitter-lua = "*" +tree-sitter-ruby = "*" diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 4eedade69d9..58d34649c7e 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -827,6 +827,196 @@ async fn test_code_context_retrieval_cpp() { ); } +#[gpui::test] +async fn test_code_context_retrieval_ruby() { + let language = ruby_lang(); + let mut retriever = CodeContextRetriever::new(); + + let text = r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end + + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end + end + + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + @name, @legs = name, legs + end + + def <=>(other) + legs <=> other.legs + end + end + + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + # This concern is inspired by "sudo mode" on GitHub. It + # is a way to re-authenticate a user before allowing them + # to see or perform an action. + # + # Add `before_action :require_challenge!` to actions you + # want to protect. + # + # The user will be shown a page to enter the challenge (which + # is either the password, or just the username when no + # password exists). Upon passing, there is a grace period + # during which no challenge will be asked from the user. + # + # Accessing challenge-protected resources during the grace + # period will refresh the grace period. + module ChallengableConcern + extend ActiveSupport::Concern + + CHALLENGE_TIMEOUT = 1.hour.freeze + + def require_challenge! + # ... + end + + def challenge_passed? + # ... + end + end"# + .unindent(), + 558, + ), + ( + r#" + def require_challenge! + return if skip_challenge? + + if challenge_passed_recently? + session[:challenge_passed_at] = Time.now.utc + return + end + + @challenge = Form::Challenge.new(return_to: request.url) + + if params.key?(:form_challenge) + if challenge_passed? + session[:challenge_passed_at] = Time.now.utc + else + flash.now[:alert] = I18n.t('challenge.invalid_password') + render_challenge + end + else + render_challenge + end + end"# + .unindent(), + 663, + ), + ( + r#" + def challenge_passed? + current_user.valid_password?(challenge_params[:current_password]) + end"# + .unindent(), + 1254, + ), + ( + r#" + class Animal + include Comparable + + attr_reader :legs + + def initialize(name, legs) + # ... + end + + def <=>(other) + # ... + end + end"# + .unindent(), + 1363, + ), + ( + r#" + def initialize(name, legs) + @name, @legs = name, legs + end"# + .unindent(), + 1427, + ), + ( + r#" + def <=>(other) + legs <=> other.legs + end"# + .unindent(), + 1501, + ), + ( + r#" + # Singleton method for car object + def car.wheels + puts "There are four wheels" + end"# + .unindent(), + 1591, + ), + ], + ); +} + #[gpui::test] fn test_dot_product(mut rng: StdRng) { assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.); @@ -1186,6 +1376,47 @@ fn lua_lang() -> Arc { ) } +fn ruby_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "Ruby".into(), + path_suffixes: vec!["rb".into()], + collapsed_placeholder: "# ...".to_string(), + ..Default::default() + }, + Some(tree_sitter_ruby::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (module + "module" @name + name: (_) @name) + (method + "def" @name + name: (_) @name + body: (body_statement) @collapse) + (class + "class" @name + name: (_) @name) + (singleton_method + "def" @name + object: (_) @name + "." @name + name: (_) @name + body: (body_statement) @collapse) + ] @item + ) + "#, + ) + .unwrap(), + ) +} + fn elixir_lang() -> Arc { Arc::new( Language::new( diff --git a/crates/zed/src/languages/ruby/config.toml b/crates/zed/src/languages/ruby/config.toml index a0b26bff920..6c8c6150155 100644 --- a/crates/zed/src/languages/ruby/config.toml +++ b/crates/zed/src/languages/ruby/config.toml @@ -10,3 +10,4 @@ brackets = [ { start = "\"", end = "\"", close = true, newline = false, not_in = ["comment", "string"] }, { start = "'", end = "'", close = true, newline = false, not_in = ["comment", "string"] }, ] +collapsed_placeholder = "# ..." diff --git a/crates/zed/src/languages/ruby/embedding.scm b/crates/zed/src/languages/ruby/embedding.scm new file mode 100644 index 00000000000..7a101e6b092 --- /dev/null +++ b/crates/zed/src/languages/ruby/embedding.scm @@ -0,0 +1,22 @@ +( + (comment)* @context + . + [ + (module + "module" @name + name: (_) @name) + (method + "def" @name + name: (_) @name + body: (body_statement) @collapse) + (class + "class" @name + name: (_) @name) + (singleton_method + "def" @name + object: (_) @name + "." @name + name: (_) @name + body: (body_statement) @collapse) + ] @item + ) From 89edb3d1b534b962f3e4e81c6055b6fd92fef868 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 31 Jul 2023 11:41:18 -0400 Subject: [PATCH 013/105] fix templating bug for parseable entire files --- crates/semantic_index/src/parsing.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index c952ef3a4ed..677406931e0 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -59,7 +59,7 @@ impl CodeContextRetriever { let document_span = ENTIRE_FILE_TEMPLATE .replace("", relative_path.to_string_lossy().as_ref()) .replace("", language_name.as_ref()) - .replace("item", &content); + .replace("", &content); Ok(vec![Document { range: 0..content.len(), From e07a81b22590257bb7bc8c4362d6afe3a01d401c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 12:49:55 -0400 Subject: [PATCH 014/105] Add additional storage filetypes --- assets/icons/file_icons/file_types.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 0ccf9c2bb7c..67791aaecb4 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,5 +1,16 @@ { "suffixes": { + "db": "storage", + "sqlite": "storage", + "myi": "storage", + "myd": "storage", + "mdf": "storage", + "csv": "storage", + "bak": "backup", + "dat": "storage", + "dll": "storage", + "sav": "storage", + "tsv": "storage", "aac": "audio", "bash": "terminal", "bmp": "image", From c4709418d142888b5d94ae61c5aad98f28c6b21a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 12:50:30 -0400 Subject: [PATCH 015/105] Format --- assets/icons/file_icons/file_types.json | 341 ++++++++++++------------ 1 file changed, 175 insertions(+), 166 deletions(-) diff --git a/assets/icons/file_icons/file_types.json b/assets/icons/file_icons/file_types.json index 67791aaecb4..9ea75d07309 100644 --- a/assets/icons/file_icons/file_types.json +++ b/assets/icons/file_icons/file_types.json @@ -1,170 +1,179 @@ { - "suffixes": { - "db": "storage", - "sqlite": "storage", - "myi": "storage", - "myd": "storage", - "mdf": "storage", - "csv": "storage", - "bak": "backup", - "dat": "storage", - "dll": "storage", - "sav": "storage", - "tsv": "storage", - "aac": "audio", - "bash": "terminal", - "bmp": "image", - "c": "code", - "conf": "settings", - "cpp": "code", - "cc": "code", - "css": "code", - "doc": "document", - "docx": "document", - "eslintrc": "eslint", - "eslintrc.js": "eslint", - "eslintrc.json": "eslint", - "flac": "audio", - "fish": "terminal", - "gitattributes": "vcs", - "gitignore": "vcs", - "gitmodules": "vcs", - "gif": "image", - "go": "code", - "h": "code", - "handlebars": "code", - "hbs": "template", - "htm": "template", - "html": "template", - "svelte": "template", - "hpp": "code", - "ico": "image", - "ini": "settings", - "java": "code", - "jpeg": "image", - "jpg": "image", - "js": "code", - "json": "storage", - "lock": "lock", - "log": "log", - "md": "document", - "mdx": "document", - "mp3": "audio", - "mp4": "video", - "ods": "document", - "odp": "document", - "odt": "document", - "ogg": "video", - "pdf": "document", - "php": "code", - "png": "image", - "ppt": "document", - "pptx": "document", - "prettierrc": "prettier", - "prettierignore": "prettier", - "ps1": "terminal", - "psd": "image", - "py": "code", - "rb": "code", - "rkt": "code", - "rs": "rust", - "rtf": "document", - "scm": "code", - "sh": "terminal", - "bashrc": "terminal", - "bash_profile": "terminal", - "bash_aliases": "terminal", - "bash_logout": "terminal", - "profile": "terminal", - "zshrc": "terminal", - "zshenv": "terminal", - "zsh_profile": "terminal", - "zsh_aliases": "terminal", - "zsh_histfile": "terminal", - "zlogin": "terminal", - "sql": "code", - "svg": "image", - "swift": "code", - "tiff": "image", - "toml": "toml", - "ts": "typescript", - "tsx": "code", - "txt": "document", - "wav": "audio", - "webm": "video", - "xls": "document", - "xlsx": "document", - "xml": "template", - "yaml": "settings", - "yml": "settings", - "zsh": "terminal" - }, - "types": { - "audio": { - "icon": "icons/file_icons/audio.svg" + "suffixes": { + "aac": "audio", + "accdb": "storage", + "bak": "backup", + "bash": "terminal", + "bash_aliases": "terminal", + "bash_logout": "terminal", + "bash_profile": "terminal", + "bashrc": "terminal", + "bmp": "image", + "c": "code", + "cc": "code", + "conf": "settings", + "cpp": "code", + "css": "code", + "csv": "storage", + "dat": "storage", + "db": "storage", + "dbf": "storage", + "dll": "storage", + "doc": "document", + "docx": "document", + "eslintrc": "eslint", + "eslintrc.js": "eslint", + "eslintrc.json": "eslint", + "fmp": "storage", + "fp7": "storage", + "flac": "audio", + "fish": "terminal", + "frm": "storage", + "gdb": "storage", + "gitattributes": "vcs", + "gitignore": "vcs", + "gitmodules": "vcs", + "gif": "image", + "go": "code", + "h": "code", + "handlebars": "code", + "hbs": "template", + "htm": "template", + "html": "template", + "ib": "storage", + "ico": "image", + "ini": "settings", + "java": "code", + "jpeg": "image", + "jpg": "image", + "js": "code", + "json": "storage", + "ldf": "storage", + "lock": "lock", + "log": "log", + "mdb": "storage", + "md": "document", + "mdf": "storage", + "mdx": "document", + "mp3": "audio", + "mp4": "video", + "myd": "storage", + "myi": "storage", + "ods": "document", + "odp": "document", + "odt": "document", + "ogg": "video", + "pdb": "storage", + "pdf": "document", + "php": "code", + "png": "image", + "ppt": "document", + "pptx": "document", + "prettierignore": "prettier", + "prettierrc": "prettier", + "profile": "terminal", + "ps1": "terminal", + "psd": "image", + "py": "code", + "rb": "code", + "rkt": "code", + "rs": "rust", + "rtf": "document", + "sav": "storage", + "scm": "code", + "sh": "terminal", + "sqlite": "storage", + "sdf": "storage", + "svelte": "template", + "svg": "image", + "swift": "code", + "ts": "typescript", + "tsx": "code", + "tiff": "image", + "toml": "toml", + "tsv": "storage", + "txt": "document", + "wav": "audio", + "webm": "video", + "xls": "document", + "xlsx": "document", + "xml": "template", + "yaml": "settings", + "yml": "settings", + "zlogin": "terminal", + "zsh": "terminal", + "zsh_aliases": "terminal", + "zshenv": "terminal", + "zsh_histfile": "terminal", + "zsh_profile": "terminal", + "zshrc": "terminal" }, - "code": { - "icon": "icons/file_icons/code.svg" - }, - "collapsed_chevron": { - "icon": "icons/file_icons/chevron_right.svg" - }, - "collapsed_folder": { - "icon": "icons/file_icons/folder.svg" - }, - "default": { - "icon": "icons/file_icons/file.svg" - }, - "document": { - "icon": "icons/file_icons/book.svg" - }, - "eslint": { - "icon": "icons/file_icons/eslint.svg" - }, - "expanded_chevron": { - "icon": "icons/file_icons/chevron_down.svg" - }, - "expanded_folder": { - "icon": "icons/file_icons/folder_open.svg" - }, - "image": { - "icon": "icons/file_icons/image.svg" - }, - "lock": { - "icon": "icons/file_icons/lock.svg" - }, - "log": { - "icon": "icons/file_icons/info.svg" - }, - "prettier": { - "icon": "icons/file_icons/prettier.svg" - }, - "rust": { - "icon": "icons/file_icons/rust.svg" - }, - "settings": { - "icon": "icons/file_icons/settings.svg" - }, - "storage": { - "icon": "icons/file_icons/database.svg" - }, - "template": { - "icon": "icons/file_icons/html.svg" - }, - "terminal": { - "icon": "icons/file_icons/terminal.svg" - }, - "toml": { - "icon": "icons/file_icons/toml.svg" - }, - "typescript": { - "icon": "icons/file_icons/typescript.svg" - }, - "vcs": { - "icon": "icons/file_icons/git.svg" - }, - "video": { - "icon": "icons/file_icons/video.svg" + "types": { + "audio": { + "icon": "icons/file_icons/audio.svg" + }, + "code": { + "icon": "icons/file_icons/code.svg" + }, + "collapsed_chevron": { + "icon": "icons/file_icons/chevron_right.svg" + }, + "collapsed_folder": { + "icon": "icons/file_icons/folder.svg" + }, + "default": { + "icon": "icons/file_icons/file.svg" + }, + "document": { + "icon": "icons/file_icons/book.svg" + }, + "eslint": { + "icon": "icons/file_icons/eslint.svg" + }, + "expanded_chevron": { + "icon": "icons/file_icons/chevron_down.svg" + }, + "expanded_folder": { + "icon": "icons/file_icons/folder_open.svg" + }, + "image": { + "icon": "icons/file_icons/image.svg" + }, + "lock": { + "icon": "icons/file_icons/lock.svg" + }, + "log": { + "icon": "icons/file_icons/info.svg" + }, + "prettier": { + "icon": "icons/file_icons/prettier.svg" + }, + "rust": { + "icon": "icons/file_icons/rust.svg" + }, + "settings": { + "icon": "icons/file_icons/settings.svg" + }, + "storage": { + "icon": "icons/file_icons/database.svg" + }, + "template": { + "icon": "icons/file_icons/html.svg" + }, + "terminal": { + "icon": "icons/file_icons/terminal.svg" + }, + "toml": { + "icon": "icons/file_icons/toml.svg" + }, + "typescript": { + "icon": "icons/file_icons/typescript.svg" + }, + "vcs": { + "icon": "icons/file_icons/git.svg" + }, + "video": { + "icon": "icons/file_icons/video.svg" + } } - } } From bb288eb941fba31ba98a89f9b2b32940ed493ac5 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 31 Jul 2023 13:08:40 -0400 Subject: [PATCH 016/105] Ensure json uses a tab size of 4 --- .zed/settings.json | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .zed/settings.json diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000000..d4b3375b0d2 --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,5 @@ +{ + "JSON": { + "tab_size": 4 + } +} From 88474a60485257814313b4a6ff799642feeafe6a Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Mon, 31 Jul 2023 10:54:29 -0700 Subject: [PATCH 017/105] Clip wrap guides from under the scrollbar --- crates/editor/src/element.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index cb46e74af0b..750beaea138 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -554,7 +554,9 @@ impl EditorElement { (text_bounds.origin_x() + wrap_position + layout.position_map.em_width / 2.) - scroll_left; - if x < text_bounds.origin_x() { + if x < text_bounds.origin_x() + || (layout.show_scrollbars && x > self.scrollbar_left(&bounds)) + { continue; } @@ -1046,6 +1048,10 @@ impl EditorElement { scene.pop_layer(); } + fn scrollbar_left(&self, bounds: &RectF) -> f32 { + bounds.max_x() - self.style.theme.scrollbar.width + } + fn paint_scrollbar( &mut self, scene: &mut SceneBuilder, @@ -1064,7 +1070,7 @@ impl EditorElement { let top = bounds.min_y(); let bottom = bounds.max_y(); let right = bounds.max_x(); - let left = right - style.width; + let left = self.scrollbar_left(&bounds); let row_range = &layout.scrollbar_row_range; let max_row = layout.max_row as f32 + (row_range.end - row_range.start); From 599f6748274ca40e91e593d5d049982b2fbded45 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 31 Jul 2023 16:36:09 -0400 Subject: [PATCH 018/105] add php support for semantic search --- Cargo.lock | 65 ++---- crates/semantic_index/Cargo.toml | 17 +- crates/semantic_index/src/parsing.rs | 7 +- .../src/semantic_index_tests.rs | 205 ++++++++++++++++++ crates/zed/src/languages/php/config.toml | 1 + crates/zed/src/languages/php/embedding.scm | 36 +++ crates/zed/src/languages/php/outline.scm | 7 +- 7 files changed, 275 insertions(+), 63 deletions(-) create mode 100644 crates/zed/src/languages/php/embedding.scm diff --git a/Cargo.lock b/Cargo.lock index b4a8f13bea5..f6a85aa70c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2341,7 +2341,7 @@ dependencies = [ "tree-sitter", "tree-sitter-html", "tree-sitter-rust", - "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)", + "tree-sitter-typescript", "unindent", "util", "workspace", @@ -3851,7 +3851,7 @@ dependencies = [ "text", "theme", "tree-sitter", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e)", + "tree-sitter-elixir", "tree-sitter-embedded-template", "tree-sitter-heex", "tree-sitter-html", @@ -3860,7 +3860,7 @@ dependencies = [ "tree-sitter-python", "tree-sitter-ruby", "tree-sitter-rust", - "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)", + "tree-sitter-typescript", "unicase", "unindent", "util", @@ -6685,14 +6685,15 @@ dependencies = [ "theme", "tiktoken-rs 0.5.0", "tree-sitter", - "tree-sitter-cpp 0.20.2", - "tree-sitter-elixir 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", - "tree-sitter-json 0.19.0", + "tree-sitter-cpp", + "tree-sitter-elixir", + "tree-sitter-json 0.20.0", "tree-sitter-lua", + "tree-sitter-php", "tree-sitter-ruby", "tree-sitter-rust", - "tree-sitter-toml 0.20.0", - "tree-sitter-typescript 0.20.2 (registry+https://github.com/rust-lang/crates.io-index)", + "tree-sitter-toml", + "tree-sitter-typescript", "unindent", "util", "workspace", @@ -8257,16 +8258,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-cpp" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c88fd925d0333e63ac64e521f5bd79c53019e569ffbbccfeef346a326f459e9" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-css" version = "0.19.0" @@ -8276,16 +8267,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-elixir" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a9916f3e1c80b3c8aab8582604e97e8720cb9b893489b347cf999f80f9d469e" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-elixir" version = "0.1.0" @@ -8464,26 +8445,6 @@ dependencies = [ "tree-sitter", ] -[[package]] -name = "tree-sitter-toml" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca517f578a98b23d20780247cc2688407fa81effad5b627a5a364ec3339b53e8" -dependencies = [ - "cc", - "tree-sitter", -] - -[[package]] -name = "tree-sitter-typescript" -version = "0.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "079c695c32d39ad089101c66393aeaca30e967fba3486a91f573d2f0e12d290a" -dependencies = [ - "cc", - "tree-sitter", -] - [[package]] name = "tree-sitter-typescript" version = "0.20.2" @@ -9923,9 +9884,9 @@ dependencies = [ "tree-sitter", "tree-sitter-bash", "tree-sitter-c", - "tree-sitter-cpp 0.20.0", + "tree-sitter-cpp", "tree-sitter-css", - "tree-sitter-elixir 0.1.0 (git+https://github.com/elixir-lang/tree-sitter-elixir?rev=4ba9dab6e2602960d95b2b625f3386c27e08084e)", + "tree-sitter-elixir", "tree-sitter-elm", "tree-sitter-embedded-template", "tree-sitter-glsl", @@ -9942,8 +9903,8 @@ dependencies = [ "tree-sitter-rust", "tree-sitter-scheme", "tree-sitter-svelte", - "tree-sitter-toml 0.5.1", - "tree-sitter-typescript 0.20.2 (git+https://github.com/tree-sitter/tree-sitter-typescript?rev=5d20856f34315b068c41edaee2ac8a100081d259)", + "tree-sitter-toml", + "tree-sitter-typescript", "tree-sitter-yaml", "unindent", "url", diff --git a/crates/semantic_index/Cargo.toml b/crates/semantic_index/Cargo.toml index 637f3f44874..3c7a6ff5df6 100644 --- a/crates/semantic_index/Cargo.toml +++ b/crates/semantic_index/Cargo.toml @@ -54,11 +54,12 @@ tempdir.workspace = true ctor.workspace = true env_logger.workspace = true -tree-sitter-typescript = "*" -tree-sitter-json = "*" -tree-sitter-rust = "*" -tree-sitter-toml = "*" -tree-sitter-cpp = "*" -tree-sitter-elixir = "*" -tree-sitter-lua = "*" -tree-sitter-ruby = "*" +tree-sitter-typescript.workspace = true +tree-sitter-json.workspace = true +tree-sitter-rust.workspace = true +tree-sitter-toml.workspace = true +tree-sitter-cpp.workspace = true +tree-sitter-elixir.workspace = true +tree-sitter-lua.workspace = true +tree-sitter-ruby.workspace = true +tree-sitter-php.workspace = true diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 677406931e0..3f7a850a574 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -200,7 +200,12 @@ impl CodeContextRetriever { let mut document_content = String::new(); for context_range in &context_match.context_ranges { - document_content.push_str(&content[context_range.clone()]); + add_content_from_range( + &mut document_content, + content, + context_range.clone(), + context_match.start_col, + ); document_content.push_str("\n"); } diff --git a/crates/semantic_index/src/semantic_index_tests.rs b/crates/semantic_index/src/semantic_index_tests.rs index 58d34649c7e..0411a8e5ecd 100644 --- a/crates/semantic_index/src/semantic_index_tests.rs +++ b/crates/semantic_index/src/semantic_index_tests.rs @@ -1017,6 +1017,156 @@ async fn test_code_context_retrieval_ruby() { ); } +#[gpui::test] +async fn test_code_context_retrieval_php() { + let language = php_lang(); + let mut retriever = CodeContextRetriever::new(); + + let text = r#" + 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + } + + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + } + } + + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + } + + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + } + + ?>"# + .unindent(); + + let documents = retriever.parse_file(&text, language.clone()).unwrap(); + + assert_documents_eq( + &documents, + &[ + ( + r#" + /* + This is a multiple-lines comment block + that spans over multiple + lines + */ + function functionName() { + echo "Hello world!"; + }"# + .unindent(), + 123, + ), + ( + r#" + trait HasAchievements + { + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + {/* ... */} + + public function achievements(): BelongsToMany + {/* ... */} + }"# + .unindent(), + 177, + ), + (r#" + /** + * @throws \Exception + */ + public function grantAchievement(Achievement $achievement, $progress = null): void + { + if ($progress > 100) { + throw new Exception(message: 'Progress cannot be greater than 100'); + } + + if ($this->achievements()->find($achievement->id)) { + throw new Exception(message: 'User already has this Achievement'); + } + + $this->achievements()->attach($achievement, [ + 'progress' => $progress ?? null, + ]); + + $this->when(value: ($progress === null) || ($progress === 100), callback: fn (): ?array => event(new AchievementAwarded(achievement: $achievement, user: $this))); + }"#.unindent(), 245), + (r#" + public function achievements(): BelongsToMany + { + return $this->belongsToMany(related: Achievement::class) + ->withPivot(columns: 'progress') + ->where('is_secret', false) + ->using(AchievementUser::class); + }"#.unindent(), 902), + (r#" + interface Multiplier + { + public function qualifies(array $data): bool; + + public function setMultiplier(): int; + }"#.unindent(), + 1146), + (r#" + enum AuditType: string + { + case Add = 'add'; + case Remove = 'remove'; + case Reset = 'reset'; + case LevelUp = 'level_up'; + }"#.unindent(), 1265) + ], + ); +} + #[gpui::test] fn test_dot_product(mut rng: StdRng) { assert_eq!(dot(&[1., 0., 0., 0., 0.], &[0., 1., 0., 0., 0.]), 0.); @@ -1376,6 +1526,61 @@ fn lua_lang() -> Arc { ) } +fn php_lang() -> Arc { + Arc::new( + Language::new( + LanguageConfig { + name: "PHP".into(), + path_suffixes: vec!["php".into()], + collapsed_placeholder: "/* ... */".into(), + ..Default::default() + }, + Some(tree_sitter_php::language()), + ) + .with_embedding_query( + r#" + ( + (comment)* @context + . + [ + (function_definition + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (trait_declaration + "trait" @name + name: (_) @name) + + (method_declaration + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (interface_declaration + "interface" @name + name: (_) @name + ) + + (enum_declaration + "enum" @name + name: (_) @name + ) + + ] @item + ) + "#, + ) + .unwrap(), + ) +} + fn ruby_lang() -> Arc { Arc::new( Language::new( diff --git a/crates/zed/src/languages/php/config.toml b/crates/zed/src/languages/php/config.toml index e9de52745a1..19acb949e25 100644 --- a/crates/zed/src/languages/php/config.toml +++ b/crates/zed/src/languages/php/config.toml @@ -9,3 +9,4 @@ brackets = [ { start = "(", end = ")", close = true, newline = true }, { start = "\"", end = "\"", close = true, newline = false, not_in = ["string"] }, ] +collapsed_placeholder = "/* ... */" diff --git a/crates/zed/src/languages/php/embedding.scm b/crates/zed/src/languages/php/embedding.scm new file mode 100644 index 00000000000..db277775b38 --- /dev/null +++ b/crates/zed/src/languages/php/embedding.scm @@ -0,0 +1,36 @@ +( + (comment)* @context + . + [ + (function_definition + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (trait_declaration + "trait" @name + name: (_) @name) + + (method_declaration + "function" @name + name: (_) @name + body: (_ + "{" @keep + "}" @keep) @collapse + ) + + (interface_declaration + "interface" @name + name: (_) @name + ) + + (enum_declaration + "enum" @name + name: (_) @name + ) + + ] @item + ) diff --git a/crates/zed/src/languages/php/outline.scm b/crates/zed/src/languages/php/outline.scm index 4934bc494d0..87986f1032e 100644 --- a/crates/zed/src/languages/php/outline.scm +++ b/crates/zed/src/languages/php/outline.scm @@ -8,8 +8,6 @@ name: (_) @name ) @item - - (method_declaration "function" @context name: (_) @name @@ -24,3 +22,8 @@ "enum" @context name: (_) @name ) @item + +(trait_declaration + "trait" @context + name: (_) @name + ) @item From 646dabe1133b32c852bf9b41b22234723f442602 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 31 Jul 2023 16:40:03 +0300 Subject: [PATCH 019/105] Add buffer search history --- assets/keymaps/default.json | 7 + crates/gpui/src/app.rs | 6 + crates/search/src/buffer_search.rs | 230 ++++++++++++++++++++++++++++- crates/search/src/search.rs | 187 +++++++++++++++++++++++ crates/vim/src/normal/search.rs | 2 +- crates/vim/src/test.rs | 4 +- 6 files changed, 428 insertions(+), 8 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index adc55f8c91e..57fde112bfc 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -227,6 +227,13 @@ "alt-enter": "search::SelectAllMatches" } }, + { + "context": "BufferSearchBar > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, { "context": "ProjectSearchBar", "bindings": { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 7af363d596b..da601ba3510 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1128,6 +1128,12 @@ impl AppContext { self.keystroke_matcher.clear_bindings(); } + pub fn binding_for_action(&self, action: &dyn Action) -> Option<&Binding> { + self.keystroke_matcher + .bindings_for_action(action.id()) + .find(|binding| binding.action().eq(action)) + } + pub fn default_global(&mut self) -> &T { let type_id = TypeId::of::(); self.update(|this| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 54293050989..45842aa5617 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1,6 +1,6 @@ use crate::{ - SearchOptions, SelectAllMatches, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, - ToggleRegex, ToggleWholeWord, + NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectAllMatches, + SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use collections::HashMap; use editor::Editor; @@ -46,6 +46,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(BufferSearchBar::select_prev_match_on_pane); cx.add_action(BufferSearchBar::select_all_matches_on_pane); cx.add_action(BufferSearchBar::handle_editor_cancel); + cx.add_action(BufferSearchBar::next_history_query); + cx.add_action(BufferSearchBar::previous_history_query); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); add_toggle_option_action::(SearchOptions::WHOLE_WORD, cx); add_toggle_option_action::(SearchOptions::REGEX, cx); @@ -65,7 +67,7 @@ fn add_toggle_option_action(option: SearchOptions, cx: &mut AppContex } pub struct BufferSearchBar { - pub query_editor: ViewHandle, + query_editor: ViewHandle, active_searchable_item: Option>, active_match_index: Option, active_searchable_item_subscription: Option, @@ -76,6 +78,7 @@ pub struct BufferSearchBar { default_options: SearchOptions, query_contains_error: bool, dismissed: bool, + search_history: SearchHistory, } impl Entity for BufferSearchBar { @@ -106,6 +109,48 @@ impl View for BufferSearchBar { .map(|active_searchable_item| active_searchable_item.supported_options()) .unwrap_or_default(); + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + Flex::row() .with_child( Flex::row() @@ -258,6 +303,7 @@ impl BufferSearchBar { pending_search: None, query_contains_error: false, dismissed: true, + search_history: SearchHistory::default(), } } @@ -341,7 +387,7 @@ impl BufferSearchBar { cx: &mut ViewContext, ) -> oneshot::Receiver<()> { let options = options.unwrap_or(self.default_options); - if query != self.query_editor.read(cx).text(cx) || self.search_options != options { + if query != self.query(cx) || self.search_options != options { self.query_editor.update(cx, |query_editor, cx| { query_editor.buffer().update(cx, |query_buffer, cx| { let len = query_buffer.len(cx); @@ -674,7 +720,7 @@ impl BufferSearchBar { fn update_matches(&mut self, cx: &mut ViewContext) -> oneshot::Receiver<()> { let (done_tx, done_rx) = oneshot::channel(); - let query = self.query_editor.read(cx).text(cx); + let query = self.query(cx); self.pending_search.take(); if let Some(active_searchable_item) = self.active_searchable_item.as_ref() { if query.is_empty() { @@ -707,6 +753,7 @@ impl BufferSearchBar { ) }; + let query_text = query.as_str().to_string(); let matches = active_searchable_item.find_matches(query, cx); let active_searchable_item = active_searchable_item.downgrade(); @@ -720,6 +767,7 @@ impl BufferSearchBar { .insert(active_searchable_item.downgrade(), matches); this.update_match_index(cx); + this.search_history.add(query_text); if !this.dismissed { let matches = this .searchable_items_with_matches @@ -753,6 +801,28 @@ impl BufferSearchBar { cx.notify(); } } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(new_query) = self.search_history.next().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } else { + self.search_history.reset_selection(); + let _ = self.search("", Some(self.search_options), cx); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if self.query(cx).is_empty() { + if let Some(new_query) = self.search_history.current().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + return; + } + } + + if let Some(new_query) = self.search_history.previous().map(str::to_string) { + let _ = self.search(&new_query, Some(self.search_options), cx); + } + } } #[cfg(test)] @@ -1333,4 +1403,154 @@ mod tests { ); }); } + + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + crate::project_search::tests::init_test(cx); + + let buffer_text = r#" + A regular expression (shortened as regex or regexp;[1] also referred to as + rational expression[2][3]) is a sequence of characters that specifies a search + pattern in text. Usually such patterns are used by string-searching algorithms + 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 (window_id, _root_view) = cx.add_window(|_| EmptyView); + + let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + + let search_bar = cx.add_view(window_id, |cx| { + let mut search_bar = BufferSearchBar::new(cx); + search_bar.set_active_pane_item(Some(&editor), cx); + search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history. + search_bar + .update(cx, |search_bar, cx| search_bar.search("a", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| search_bar.search("b", None, cx)) + .await + .unwrap(); + search_bar + .update(cx, |search_bar, cx| { + search_bar.search("c", Some(SearchOptions::CASE_SENSITIVE), cx) + }) + .await + .unwrap(); + // Ensure that the latest search is active. + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "a"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_bar + .update(cx, |search_bar, cx| search_bar.search("ba", None, cx)) + .await + .unwrap(); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "b"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "c"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), "ba"); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_bar.read_with(cx, |search_bar, cx| { + assert_eq!(search_bar.query(cx), ""); + assert_eq!(search_bar.search_options, SearchOptions::NONE); + }); + } } diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 58cda0c7dc5..18e39155274 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -3,6 +3,7 @@ pub use buffer_search::BufferSearchBar; use gpui::{actions, Action, AppContext}; use project::search::SearchQuery; pub use project_search::{ProjectSearchBar, ProjectSearchView}; +use smallvec::SmallVec; pub mod buffer_search; pub mod project_search; @@ -21,6 +22,8 @@ actions!( SelectNextMatch, SelectPrevMatch, SelectAllMatches, + NextHistoryQuery, + PreviousHistoryQuery, ] ); @@ -65,3 +68,187 @@ impl SearchOptions { options } } + +const SEARCH_HISTORY_LIMIT: usize = 20; + +#[derive(Default, Debug)] +pub struct SearchHistory { + history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, + selected: Option, +} + +impl SearchHistory { + pub fn add(&mut self, search_string: String) { + if let Some(i) = self.selected { + if search_string == self.history[i] { + return; + } + } + + if let Some(previously_searched) = self.history.last_mut() { + if search_string.find(previously_searched.as_str()).is_some() { + *previously_searched = search_string; + self.selected = Some(self.history.len() - 1); + return; + } + } + + self.history.push(search_string); + if self.history.len() > SEARCH_HISTORY_LIMIT { + self.history.remove(0); + } + self.selected = Some(self.history.len() - 1); + } + + pub fn next(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let selected = self.selected?; + if selected == history_size - 1 { + return None; + } + let next_index = selected + 1; + self.selected = Some(next_index); + Some(&self.history[next_index]) + } + + pub fn current(&self) -> Option<&str> { + Some(&self.history[self.selected?]) + } + + pub fn previous(&mut self) -> Option<&str> { + let history_size = self.history.len(); + if history_size == 0 { + return None; + } + + let prev_index = match self.selected { + Some(selected_index) => { + if selected_index == 0 { + return None; + } else { + selected_index - 1 + } + } + None => history_size - 1, + }; + + self.selected = Some(prev_index); + Some(&self.history[prev_index]) + } + + pub fn reset_selection(&mut self) { + self.selected = None; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_add() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.current(), + None, + "No current selection should be set fo the default search history" + ); + + search_history.add("rust".to_string()); + assert_eq!( + search_history.current(), + Some("rust"), + "Newly added item should be selected" + ); + + // check if duplicates are not added + search_history.add("rust".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should not add a duplicate" + ); + assert_eq!(search_history.current(), Some("rust")); + + // check if new string containing the previous string replaces it + search_history.add("rustlang".to_string()); + assert_eq!( + search_history.history.len(), + 1, + "Should replace previous item if it's a substring" + ); + assert_eq!(search_history.current(), Some("rustlang")); + + // push enough items to test SEARCH_HISTORY_LIMIT + for i in 0..SEARCH_HISTORY_LIMIT * 2 { + search_history.add(format!("item{i}")); + } + assert!(search_history.history.len() <= SEARCH_HISTORY_LIMIT); + } + + #[test] + fn test_next_and_previous() { + let mut search_history = SearchHistory::default(); + assert_eq!( + search_history.next(), + None, + "Default search history should not have a next item" + ); + + search_history.add("Rust".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("JavaScript".to_string()); + assert_eq!(search_history.next(), None); + search_history.add("TypeScript".to_string()); + assert_eq!(search_history.next(), None); + + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.previous(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.previous(), Some("Rust")); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.previous(), None); + assert_eq!(search_history.current(), Some("Rust")); + + assert_eq!(search_history.next(), Some("JavaScript")); + assert_eq!(search_history.current(), Some("JavaScript")); + + assert_eq!(search_history.next(), Some("TypeScript")); + assert_eq!(search_history.current(), Some("TypeScript")); + + assert_eq!(search_history.next(), None); + assert_eq!(search_history.current(), Some("TypeScript")); + } + + #[test] + fn test_reset_selection() { + let mut search_history = SearchHistory::default(); + search_history.add("Rust".to_string()); + search_history.add("JavaScript".to_string()); + search_history.add("TypeScript".to_string()); + + assert_eq!(search_history.current(), Some("TypeScript")); + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + assert_eq!( + search_history.previous(), + Some("TypeScript"), + "Should start from the end after reset on previous item query" + ); + + search_history.previous(); + assert_eq!(search_history.current(), Some("JavaScript")); + search_history.previous(); + assert_eq!(search_history.current(), Some("Rust")); + + search_history.reset_selection(); + assert_eq!(search_history.current(), None); + } +} diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index d584c575d29..614866d9c9c 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -222,7 +222,7 @@ mod test { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + assert_eq!(bar.query(cx), "cc"); }); deterministic.run_until_parked(); diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index 98d8cb87499..474f2128fc5 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -99,7 +99,7 @@ async fn test_buffer_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), ""); + assert_eq!(bar.query(cx), ""); }) } @@ -175,7 +175,7 @@ async fn test_selection_on_search(cx: &mut gpui::TestAppContext) { }); search_bar.read_with(cx.cx, |bar, cx| { - assert_eq!(bar.query_editor.read(cx).text(cx), "cc"); + assert_eq!(bar.query(cx), "cc"); }); // wait for the query editor change event to fire. From 634baeedb4a0a59b303182519ef1279b72319c8f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 1 Aug 2023 01:23:51 +0300 Subject: [PATCH 020/105] Add project search history --- assets/keymaps/default.json | 7 + crates/search/src/project_search.rs | 283 +++++++++++++++++++++++++++- crates/search/src/search.rs | 2 +- 3 files changed, 289 insertions(+), 3 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 57fde112bfc..38ec8ffb405 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -240,6 +240,13 @@ "escape": "project_search::ToggleFocus" } }, + { + "context": "ProjectSearchBar > Editor", + "bindings": { + "up": "search::PreviousHistoryQuery", + "down": "search::NextHistoryQuery" + } + }, { "context": "ProjectSearchView", "bindings": { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 87307264f50..1b4e32f4b83 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1,6 +1,6 @@ use crate::{ - SearchOptions, SelectNextMatch, SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, - ToggleWholeWord, + NextHistoryQuery, PreviousHistoryQuery, SearchHistory, SearchOptions, SelectNextMatch, + SelectPrevMatch, ToggleCaseSensitive, ToggleRegex, ToggleWholeWord, }; use anyhow::Context; use collections::HashMap; @@ -56,6 +56,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(ProjectSearchBar::search_in_new); cx.add_action(ProjectSearchBar::select_next_match); cx.add_action(ProjectSearchBar::select_prev_match); + cx.add_action(ProjectSearchBar::next_history_query); + cx.add_action(ProjectSearchBar::previous_history_query); cx.capture_action(ProjectSearchBar::tab); cx.capture_action(ProjectSearchBar::tab_previous); add_toggle_option_action::(SearchOptions::CASE_SENSITIVE, cx); @@ -83,6 +85,7 @@ struct ProjectSearch { match_ranges: Vec>, active_query: Option, search_id: usize, + search_history: SearchHistory, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -131,6 +134,7 @@ impl ProjectSearch { match_ranges: Default::default(), active_query: None, search_id: 0, + search_history: SearchHistory::default(), } } @@ -144,6 +148,7 @@ impl ProjectSearch { match_ranges: self.match_ranges.clone(), active_query: self.active_query.clone(), search_id: self.search_id, + search_history: self.search_history.clone(), }) } @@ -152,6 +157,7 @@ impl ProjectSearch { .project .update(cx, |project, cx| project.search(query.clone(), cx)); self.search_id += 1; + self.search_history.add(query.as_str().to_string()); self.active_query = Some(query); self.match_ranges.clear(); self.pending_search = Some(cx.spawn_weak(|this, mut cx| async move { @@ -202,6 +208,7 @@ impl ProjectSearch { }); self.search_id += 1; self.match_ranges.clear(); + self.search_history.add(query.as_str().to_string()); self.pending_search = Some(cx.spawn(|this, mut cx| async move { let results = search?.await.log_err()?; @@ -278,6 +285,49 @@ impl View for ProjectSearchView { Cow::Borrowed("No results") }; + let previous_query_keystrokes = + cx.binding_for_action(&PreviousHistoryQuery {}) + .map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let next_query_keystrokes = + cx.binding_for_action(&NextHistoryQuery {}).map(|binding| { + binding + .keystrokes() + .iter() + .map(|k| k.to_string()) + .collect::>() + }); + let new_placeholder_text = match (previous_query_keystrokes, next_query_keystrokes) { + (Some(previous_query_keystrokes), Some(next_query_keystrokes)) => { + format!( + "Search ({}/{} for previous/next query)", + previous_query_keystrokes.join(" "), + next_query_keystrokes.join(" ") + ) + } + (None, Some(next_query_keystrokes)) => { + format!( + "Search ({} for next query)", + next_query_keystrokes.join(" ") + ) + } + (Some(previous_query_keystrokes), None) => { + format!( + "Search ({} for previous query)", + previous_query_keystrokes.join(" ") + ) + } + (None, None) => String::new(), + }; + self.query_editor.update(cx, |editor, cx| { + editor.set_placeholder_text(new_placeholder_text, cx); + }); + MouseEventHandler::::new(0, cx, |_, _| { Label::new(text, theme.search.results_status.clone()) .aligned() @@ -1152,6 +1202,47 @@ impl ProjectSearchBar { false } } + + fn next_history_query(&mut self, _: &NextHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + let new_query = search_view.model.update(cx, |model, _| { + if let Some(new_query) = model.search_history.next().map(str::to_string) { + new_query + } else { + model.search_history.reset_selection(); + String::new() + } + }); + search_view.set_query(&new_query, cx); + }); + } + } + + fn previous_history_query(&mut self, _: &PreviousHistoryQuery, cx: &mut ViewContext) { + if let Some(search_view) = self.active_project_search.as_ref() { + search_view.update(cx, |search_view, cx| { + if search_view.query_editor.read(cx).text(cx).is_empty() { + if let Some(new_query) = search_view + .model + .read(cx) + .search_history + .current() + .map(str::to_string) + { + search_view.set_query(&new_query, cx); + return; + } + } + + if let Some(new_query) = search_view.model.update(cx, |model, _| { + model.search_history.previous().map(str::to_string) + }) { + search_view.set_query(&new_query, cx); + } + }); + } + } } impl Entity for ProjectSearchBar { @@ -1333,6 +1424,7 @@ pub mod tests { use editor::DisplayPoint; use gpui::{color::Color, executor::Deterministic, TestAppContext}; use project::FakeFs; + use semantic_index::semantic_index_settings::SemanticIndexSettings; use serde_json::json; use settings::SettingsStore; use std::sync::Arc; @@ -1758,6 +1850,192 @@ pub mod tests { }); } + #[gpui::test] + async fn test_search_query_history(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/dir", + json!({ + "one.rs": "const ONE: usize = 1;", + "two.rs": "const TWO: usize = one::ONE + one::ONE;", + "three.rs": "const THREE: usize = one::ONE + two::TWO;", + "four.rs": "const FOUR: usize = one::ONE + three::THREE;", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; + let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + workspace.update(cx, |workspace, cx| { + ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) + }); + + let search_view = cx.read(|cx| { + workspace + .read(cx) + .active_pane() + .read(cx) + .active_item() + .and_then(|item| item.downcast::()) + .expect("Search view expected to appear after new search event trigger") + }); + + let search_bar = cx.add_view(window_id, |cx| { + let mut search_bar = ProjectSearchBar::new(); + search_bar.set_active_pane_item(Some(&search_view), cx); + // search_bar.show(cx); + search_bar + }); + + // Add 3 search items into the history + another unsubmitted one. + search_view.update(cx, |search_view, cx| { + search_view.search_options = SearchOptions::CASE_SENSITIVE; + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("ONE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("THREE", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + search_view.query_editor.update(cx, |query_editor, cx| { + query_editor.set_text("JUST_TEXT_INPUT", cx) + }); + }); + cx.foreground().run_until_parked(); + + // Ensure that the latest input with search settings is active. + search_view.update(cx, |search_view, cx| { + assert_eq!( + search_view.query_editor.read(cx).text(cx), + "JUST_TEXT_INPUT" + ); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next history query after the latest should set the query to the empty string. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // First previous query for empty current query should set the query to the latest submitted one. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Further previous items should go over the history in reverse order. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Previous items should never go behind the first history item. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "ONE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // Next items should go over the history in the original order. + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + search_view.update(cx, |search_view, cx| { + search_view + .query_editor + .update(cx, |query_editor, cx| query_editor.set_text("TWO_NEW", cx)); + search_view.search(cx); + }); + cx.foreground().run_until_parked(); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + + // New search input should add another entry to history and move the selection to the end of the history. + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.previous_history_query(&PreviousHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "THREE"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), "TWO_NEW"); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + search_bar.update(cx, |search_bar, cx| { + search_bar.next_history_query(&NextHistoryQuery, cx); + }); + search_view.update(cx, |search_view, cx| { + assert_eq!(search_view.query_editor.read(cx).text(cx), ""); + assert_eq!(search_view.search_options, SearchOptions::CASE_SENSITIVE); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); let fonts = cx.font_cache(); @@ -1767,6 +2045,7 @@ pub mod tests { cx.update(|cx| { cx.set_global(SettingsStore::test(cx)); cx.set_global(ActiveSearches::default()); + settings::register::(cx); theme::init((), cx); cx.update_global::(|store, _| { diff --git a/crates/search/src/search.rs b/crates/search/src/search.rs index 18e39155274..f1711afec20 100644 --- a/crates/search/src/search.rs +++ b/crates/search/src/search.rs @@ -71,7 +71,7 @@ impl SearchOptions { const SEARCH_HISTORY_LIMIT: usize = 20; -#[derive(Default, Debug)] +#[derive(Default, Debug, Clone)] pub struct SearchHistory { history: SmallVec<[String; SEARCH_HISTORY_LIMIT]>, selected: Option, From 9a50b43eaa0f52a0a223e198141b8091ec618548 Mon Sep 17 00:00:00 2001 From: KCaverly Date: Mon, 31 Jul 2023 21:03:02 -0400 Subject: [PATCH 021/105] add templating languages html, erb, heex, svelte as entire parseable file types --- crates/semantic_index/src/parsing.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 3f7a850a574..643db8c7982 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -21,7 +21,8 @@ const CODE_CONTEXT_TEMPLATE: &str = "The below code snippet is from file ''\n\n```\n\n```"; const ENTIRE_FILE_TEMPLATE: &str = "The below snippet is from file ''\n\n```\n\n```"; -pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &["TOML", "YAML", "CSS"]; +pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = + &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"]; pub struct CodeContextRetriever { pub parser: Parser, From e221f23018d9b9883327712874b1237725922a0a Mon Sep 17 00:00:00 2001 From: KCaverly Date: Tue, 1 Aug 2023 10:30:34 -0400 Subject: [PATCH 022/105] add support for markdown files to semantic search --- crates/semantic_index/src/parsing.rs | 16 ++++++++++++++++ crates/semantic_index/src/semantic_index.rs | 1 + 2 files changed, 17 insertions(+) diff --git a/crates/semantic_index/src/parsing.rs b/crates/semantic_index/src/parsing.rs index 643db8c7982..cef23862c56 100644 --- a/crates/semantic_index/src/parsing.rs +++ b/crates/semantic_index/src/parsing.rs @@ -21,6 +21,7 @@ const CODE_CONTEXT_TEMPLATE: &str = "The below code snippet is from file ''\n\n```\n\n```"; const ENTIRE_FILE_TEMPLATE: &str = "The below snippet is from file ''\n\n```\n\n```"; +const MARKDOWN_CONTEXT_TEMPLATE: &str = "The below file contents is from file ''\n\n"; pub const PARSEABLE_ENTIRE_FILE_TYPES: &[&str] = &["TOML", "YAML", "CSS", "HEEX", "ERB", "SVELTE", "HTML"]; @@ -70,6 +71,19 @@ impl CodeContextRetriever { }]) } + fn parse_markdown_file(&self, relative_path: &Path, content: &str) -> Result> { + let document_span = MARKDOWN_CONTEXT_TEMPLATE + .replace("", relative_path.to_string_lossy().as_ref()) + .replace("", &content); + + Ok(vec![Document { + range: 0..content.len(), + content: document_span, + embedding: Vec::new(), + name: "Markdown".to_string(), + }]) + } + fn get_matches_in_file( &mut self, content: &str, @@ -136,6 +150,8 @@ impl CodeContextRetriever { if PARSEABLE_ENTIRE_FILE_TYPES.contains(&language_name.as_ref()) { return self.parse_entire_file(relative_path, language_name, &content); + } else if &language_name.to_string() == &"Markdown".to_string() { + return self.parse_markdown_file(relative_path, &content); } let mut documents = self.parse_file(content, language)?; diff --git a/crates/semantic_index/src/semantic_index.rs b/crates/semantic_index/src/semantic_index.rs index bd114de216a..23c75f40149 100644 --- a/crates/semantic_index/src/semantic_index.rs +++ b/crates/semantic_index/src/semantic_index.rs @@ -613,6 +613,7 @@ impl SemanticIndex { .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()) From eb26fb2d45357e35d7d39edb8e2b3c6e0c1e9dbb Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 1 Aug 2023 11:52:53 -0400 Subject: [PATCH 023/105] Fix variable names --- crates/editor/src/editor.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 5270d6f9518..a4d9259a6d7 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4221,7 +4221,7 @@ impl Editor { _: &SortLinesCaseSensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, |text| text.sort()) + self.manipulate_lines(cx, |lines| lines.sort()) } pub fn sort_lines_case_insensitive( @@ -4229,7 +4229,7 @@ impl Editor { _: &SortLinesCaseInsensitive, cx: &mut ViewContext, ) { - self.manipulate_lines(cx, |text| text.sort_by_key(|line| line.to_lowercase())) + self.manipulate_lines(cx, |lines| lines.sort_by_key(|line| line.to_lowercase())) } pub fn reverse_lines(&mut self, _: &ReverseLines, cx: &mut ViewContext) { @@ -4267,19 +4267,19 @@ impl Editor { let text = buffer .text_for_range(start_point..end_point) .collect::(); - let mut text = text.split("\n").collect_vec(); + let mut lines = text.split("\n").collect_vec(); - let text_len = text.len(); - callback(&mut text); + let lines_len = lines.len(); + callback(&mut lines); // This is a current limitation with selections. // If we wanted to support removing or adding lines, we'd need to fix the logic associated with selections. debug_assert!( - text.len() == text_len, + lines.len() == lines_len, "callback should not change the number of lines" ); - edits.push((start_point..end_point, text.join("\n"))); + edits.push((start_point..end_point, lines.join("\n"))); let start_anchor = buffer.anchor_after(start_point); let end_anchor = buffer.anchor_before(end_point); From 3cee181f99c6de5ae264a119124d0ca94c0ffe3d Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 1 Aug 2023 14:04:29 -0400 Subject: [PATCH 024/105] Improve panic message usefulness on local dev builds --- crates/zed/src/main.rs | 42 +++++++++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e44ab3e33ac..2a1fef6a56e 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -45,6 +45,7 @@ use std::{ use sum_tree::Bias; use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; use util::{ + channel::ReleaseChannel, http::{self, HttpClient}, paths::PathLikeWithPosition, }; @@ -415,22 +416,41 @@ fn init_panic_hook(app: &App, installation_id: Option) { panic::set_hook(Box::new(move |info| { let prior_panic_count = PANIC_COUNT.fetch_add(1, Ordering::SeqCst); if prior_panic_count > 0 { - std::panic::resume_unwind(Box::new(())); + // Give the panic-ing thread time to write the panic file + loop { + std::thread::yield_now(); + } + } + + let thread = thread::current(); + let thread_name = thread.name().unwrap_or(""); + + let payload = info + .payload() + .downcast_ref::<&str>() + .map(|s| s.to_string()) + .or_else(|| info.payload().downcast_ref::().map(|s| s.clone())) + .unwrap_or_else(|| "Box".to_string()); + + if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Dev { + let location = info.location().unwrap(); + let backtrace = Backtrace::new(); + eprintln!( + "Thread {:?} panicked with {:?} at {}:{}:{}\n{:?}", + thread_name, + payload, + location.file(), + location.line(), + location.column(), + backtrace, + ); + std::process::exit(-1); } let app_version = ZED_APP_VERSION .or_else(|| platform.app_version().ok()) .map_or("dev".to_string(), |v| v.to_string()); - let thread = thread::current(); - let thread = thread.name().unwrap_or(""); - - let payload = info.payload(); - let payload = None - .or_else(|| payload.downcast_ref::<&str>().map(|s| s.to_string())) - .or_else(|| payload.downcast_ref::().map(|s| s.clone())) - .unwrap_or_else(|| "Box".to_string()); - let backtrace = Backtrace::new(); let mut backtrace = backtrace .frames() @@ -447,7 +467,7 @@ fn init_panic_hook(app: &App, installation_id: Option) { } let panic_data = Panic { - thread: thread.into(), + thread: thread_name.into(), payload: payload.into(), location_data: info.location().map(|location| LocationData { file: location.file().into(), From b695c42e1152ad9019b2c6bfcaf21834fe00b386 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 1 Aug 2023 22:28:04 -0600 Subject: [PATCH 025/105] WIP: Return WindowHandle from AppContext::add_window --- crates/gpui/src/app.rs | 352 ++++++++++++++++-------- crates/gpui/src/app/ref_counts.rs | 23 ++ crates/gpui/src/app/test_app_context.rs | 11 +- 3 files changed, 268 insertions(+), 118 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index da601ba3510..b2d732d1700 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -130,8 +130,12 @@ pub trait BorrowAppContext { } pub trait BorrowWindowContext { - fn read_with T>(&self, window_id: usize, f: F) -> T; - fn update T>(&mut self, window_id: usize, f: F) -> T; + fn read_with(&self, window_id: usize, f: F) -> T + where + F: FnOnce(&WindowContext) -> T; + fn update(&mut self, window_id: usize, f: F) -> T + where + F: FnOnce(&mut WindowContext) -> T; } #[derive(Clone)] @@ -402,7 +406,7 @@ impl AsyncAppContext { &mut self, window_options: WindowOptions, build_root_view: F, - ) -> (usize, ViewHandle) + ) -> WindowHandle where T: View, F: FnOnce(&mut ViewContext) -> T, @@ -1300,7 +1304,7 @@ impl AppContext { &mut self, window_options: WindowOptions, build_root_view: F, - ) -> (usize, ViewHandle) + ) -> WindowHandle where V: View, F: FnOnce(&mut ViewContext) -> V, @@ -1311,9 +1315,8 @@ impl AppContext { this.platform .open_window(window_id, window_options, this.foreground.clone()); let window = this.build_window(window_id, platform_window, build_root_view); - let root_view = window.root_view().clone().downcast::().unwrap(); this.windows.insert(window_id, window); - (window_id, root_view) + WindowHandle::new(window_id, this.ref_counts.clone()) }) } @@ -3802,6 +3805,131 @@ impl Clone for WeakModelHandle { impl Copy for WeakModelHandle {} +pub struct WindowHandle { + any_handle: AnyWindowHandle, + view_type: PhantomData, +} + +impl WindowHandle { + fn id(&self) -> usize { + self.any_handle.id() + } + + fn new(window_id: usize, ref_counts: Arc>) -> Self { + WindowHandle { + any_handle: AnyWindowHandle::new::(window_id, ref_counts), + view_type: PhantomData, + } + } + + fn root(&self, cx: &impl BorrowAppContext) -> ViewHandle { + self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap()) + } + + pub fn read_with(&self, cx: &C, read: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&WindowContext) -> R, + { + cx.read_with(|cx| cx.read_window(self.id(), read).unwrap()) + } + + pub fn update(&self, cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut WindowContext) -> R, + { + cx.update(|cx| cx.update_window(self.id(), update).unwrap()) + } + + pub fn update_root(&self, cx: &mut C, update: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&mut V, &mut ViewContext) -> R, + { + let window_id = self.id(); + cx.update(|cx| { + cx.update_window(window_id, |cx| { + cx.root_view() + .clone() + .downcast::() + .unwrap() + .update(cx, update) + }) + .unwrap() + }) + } + + pub fn read_root<'a>(&self, cx: &'a AppContext) -> &'a V { + let root_view = cx + .read_window(self.id(), |cx| cx.root_view().clone().downcast().unwrap()) + .unwrap(); + root_view.read(cx) + } + + pub fn read_root_with(&self, cx: &C, read: F) -> R + where + C: BorrowAppContext, + F: FnOnce(&V, &ViewContext) -> R, + { + self.read_with(cx, |cx| { + cx.root_view() + .downcast_ref::() + .unwrap() + .read_with(cx, read) + }) + } + + pub fn add_view(&self, cx: &mut C, build_view: F) -> ViewHandle + where + C: BorrowAppContext, + U: View, + F: FnOnce(&mut ViewContext) -> U, + { + self.update(cx, |cx| cx.add_view(build_view)) + } +} + +pub struct AnyWindowHandle { + window_id: usize, + root_view_type: TypeId, + ref_counts: Arc>, + + #[cfg(any(test, feature = "test-support"))] + handle_id: usize, +} + +impl AnyWindowHandle { + fn new(window_id: usize, ref_counts: Arc>) -> Self { + ref_counts.lock().inc_window(window_id); + + #[cfg(any(test, feature = "test-support"))] + let handle_id = ref_counts + .lock() + .leak_detector + .lock() + .handle_created(None, window_id); + + Self { + window_id, + root_view_type: TypeId::of::(), + ref_counts, + #[cfg(any(test, feature = "test-support"))] + handle_id, + } + } + + pub fn id(&self) -> usize { + self.window_id + } +} + +impl Drop for AnyWindowHandle { + fn drop(&mut self) { + self.ref_counts.lock().dec_window(self.window_id) + } +} + #[repr(transparent)] pub struct ViewHandle { any_handle: AnyViewHandle, @@ -4684,11 +4812,11 @@ mod tests { } } - let (_, view) = cx.add_window(|_| View { render_count: 0 }); + let window = cx.add_window(|_| View { render_count: 0 }); let called_defer = Rc::new(AtomicBool::new(false)); let called_after_window_update = Rc::new(AtomicBool::new(false)); - view.update(cx, |this, cx| { + window.update_root(cx, |this, cx| { assert_eq!(this.render_count, 1); cx.defer({ let called_defer = called_defer.clone(); @@ -4712,7 +4840,7 @@ mod tests { assert!(called_defer.load(SeqCst)); assert!(called_after_window_update.load(SeqCst)); - assert_eq!(view.read_with(cx, |view, _| view.render_count), 3); + assert_eq!(window.read_root_with(cx, |view, _| view.render_count), 3); } #[crate::test(self)] @@ -4751,9 +4879,9 @@ mod tests { } } - let (window_id, _root_view) = cx.add_window(|cx| View::new(None, cx)); - let handle_1 = cx.add_view(window_id, |cx| View::new(None, cx)); - let handle_2 = cx.add_view(window_id, |cx| View::new(Some(handle_1.clone()), cx)); + let window = cx.add_window(|cx| View::new(None, cx)); + let handle_1 = window.add_view(cx, |cx| View::new(None, cx)); + let handle_2 = window.add_view(cx, |cx| View::new(Some(handle_1.clone()), cx)); assert_eq!(cx.read(|cx| cx.views.len()), 3); handle_1.update(cx, |view, cx| { @@ -4813,11 +4941,11 @@ mod tests { } let mouse_down_count = Arc::new(AtomicUsize::new(0)); - let (window_id, _) = cx.add_window(Default::default(), |_| View { + let window = cx.add_window(Default::default(), |_| View { mouse_down_count: mouse_down_count.clone(), }); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { // Ensure window's root element is in a valid lifecycle state. cx.dispatch_event( Event::MouseDown(MouseButtonEvent { @@ -4876,9 +5004,11 @@ mod tests { let model = cx.add_model(|_| Model { released: model_released.clone(), }); - let (window_id, view) = cx.add_window(Default::default(), |_| View { + let window = cx.add_window(Default::default(), |_| View { released: view_released.clone(), }); + let view = window.root(cx); + assert!(!model_released.get()); assert!(!view_released.get()); @@ -4900,7 +5030,7 @@ mod tests { assert!(model_release_observed.get()); drop(view); - cx.update_window(window_id, |cx| cx.remove_window()); + window.update(cx, |cx| cx.remove_window()); assert!(view_released.get()); assert!(view_release_observed.get()); } @@ -4913,8 +5043,9 @@ mod tests { type Event = String; } - let (window_id, handle_1) = cx.add_window(|_| TestView::default()); - let handle_2 = cx.add_view(window_id, |_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let handle_1 = window.root(cx); + let handle_2 = window.add_view(cx, |_| TestView::default()); let handle_3 = cx.add_model(|_| Model); handle_1.update(cx, |_, cx| { @@ -5140,9 +5271,9 @@ mod tests { type Event = (); } - let (window_id, _root_view) = cx.add_window(|_| TestView::default()); - let observing_view = cx.add_view(window_id, |_| TestView::default()); - let emitting_view = cx.add_view(window_id, |_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let observing_view = window.add_view(cx, |_| TestView::default()); + let emitting_view = window.add_view(cx, |_| TestView::default()); let observing_model = cx.add_model(|_| Model); let observed_model = cx.add_model(|_| Model); @@ -5165,7 +5296,7 @@ mod tests { #[crate::test(self)] fn test_view_emit_before_subscribe_in_same_update_cycle(cx: &mut AppContext) { - let (_, view) = cx.add_window::(Default::default(), |cx| { + let window = cx.add_window::(Default::default(), |cx| { drop(cx.subscribe(&cx.handle(), { move |this, _, _, _| this.events.push("dropped before flush".into()) })); @@ -5181,7 +5312,7 @@ mod tests { TestView { events: Vec::new() } }); - assert_eq!(view.read(cx).events, ["before emit"]); + assert_eq!(window.read_root(cx).events, ["before emit"]); } #[crate::test(self)] @@ -5195,7 +5326,8 @@ mod tests { type Event = (); } - let (_, view) = cx.add_window(|_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let view = window.root(cx); let model = cx.add_model(|_| Model { state: "old-state".into(), }); @@ -5216,7 +5348,7 @@ mod tests { #[crate::test(self)] fn test_view_notify_before_observe_in_same_update_cycle(cx: &mut AppContext) { - let (_, view) = cx.add_window::(Default::default(), |cx| { + let window = cx.add_window::(Default::default(), |cx| { drop(cx.observe(&cx.handle(), { move |this, _, _| this.events.push("dropped before flush".into()) })); @@ -5232,7 +5364,7 @@ mod tests { TestView { events: Vec::new() } }); - assert_eq!(view.read(cx).events, ["before notify"]); + assert_eq!(window.read_root(cx).events, ["before notify"]); } #[crate::test(self)] @@ -5243,7 +5375,8 @@ mod tests { } let model = cx.add_model(|_| Model); - let (_, view) = cx.add_window(|_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let view = window.root(cx); view.update(cx, |_, cx| { model.update(cx, |_, cx| cx.notify()); @@ -5267,8 +5400,8 @@ mod tests { type Event = (); } - let (window_id, _root_view) = cx.add_window(|_| TestView::default()); - let observing_view = cx.add_view(window_id, |_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let observing_view = window.add_view(cx, |_| TestView::default()); let observing_model = cx.add_model(|_| Model); let observed_model = cx.add_model(|_| Model); @@ -5390,9 +5523,9 @@ mod tests { } } - let (window_id, _root_view) = cx.add_window(|_| View); - let observing_view = cx.add_view(window_id, |_| View); - let observed_view = cx.add_view(window_id, |_| View); + let window = cx.add_window(|_| View); + let observing_view = window.add_view(cx, |_| View); + let observed_view = window.add_view(cx, |_| View); let observation_count = Rc::new(RefCell::new(0)); observing_view.update(cx, |_, cx| { @@ -5474,13 +5607,14 @@ mod tests { } let view_events: Arc>> = Default::default(); - let (window_id, view_1) = cx.add_window(|_| View { + let window = cx.add_window(|_| View { events: view_events.clone(), name: "view 1".to_string(), child: None, }); - let view_2 = cx - .update_window(window_id, |cx| { + let view_1 = window.root(cx); + let view_2 = window + .update(cx, |cx| { let view_2 = cx.add_view(|_| View { events: view_events.clone(), name: "view 2".to_string(), @@ -5731,40 +5865,34 @@ mod tests { }) .detach(); - let (window_id, view_1) = - cx.add_window(Default::default(), |_| ViewA { id: 1, child: None }); - let view_2 = cx - .update_window(window_id, |cx| { - let child = cx.add_view(|_| ViewB { id: 2, child: None }); - view_1.update(cx, |view, cx| { - view.child = Some(child.clone().into_any()); - cx.notify(); - }); - child - }) - .unwrap(); - let view_3 = cx - .update_window(window_id, |cx| { - let child = cx.add_view(|_| ViewA { id: 3, child: None }); - view_2.update(cx, |view, cx| { - view.child = Some(child.clone().into_any()); - cx.notify(); - }); - child - }) - .unwrap(); - let view_4 = cx - .update_window(window_id, |cx| { - let child = cx.add_view(|_| ViewB { id: 4, child: None }); - view_3.update(cx, |view, cx| { - view.child = Some(child.clone().into_any()); - cx.notify(); - }); - child - }) - .unwrap(); + let window = cx.add_window(Default::default(), |_| ViewA { id: 1, child: None }); + let view_1 = window.root(cx); + let view_2 = window.update(cx, |cx| { + let child = cx.add_view(|_| ViewB { id: 2, child: None }); + view_1.update(cx, |view, cx| { + view.child = Some(child.clone().into_any()); + cx.notify(); + }); + child + }); + let view_3 = window.update(cx, |cx| { + let child = cx.add_view(|_| ViewA { id: 3, child: None }); + view_2.update(cx, |view, cx| { + view.child = Some(child.clone().into_any()); + cx.notify(); + }); + child + }); + let view_4 = window.update(cx, |cx| { + let child = cx.add_view(|_| ViewB { id: 4, child: None }); + view_3.update(cx, |view, cx| { + view.child = Some(child.clone().into_any()); + cx.notify(); + }); + child + }); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_action(Some(view_4.id()), &Action("bar".to_string())) }); @@ -5786,31 +5914,27 @@ mod tests { // Remove view_1, which doesn't propagate the action - let (window_id, view_2) = - cx.add_window(Default::default(), |_| ViewB { id: 2, child: None }); - let view_3 = cx - .update_window(window_id, |cx| { - let child = cx.add_view(|_| ViewA { id: 3, child: None }); - view_2.update(cx, |view, cx| { - view.child = Some(child.clone().into_any()); - cx.notify(); - }); - child - }) - .unwrap(); - let view_4 = cx - .update_window(window_id, |cx| { - let child = cx.add_view(|_| ViewB { id: 4, child: None }); - view_3.update(cx, |view, cx| { - view.child = Some(child.clone().into_any()); - cx.notify(); - }); - child - }) - .unwrap(); + let window = cx.add_window(Default::default(), |_| ViewB { id: 2, child: None }); + let view_2 = window.root(cx); + let view_3 = window.update(cx, |cx| { + let child = cx.add_view(|_| ViewA { id: 3, child: None }); + view_2.update(cx, |view, cx| { + view.child = Some(child.clone().into_any()); + cx.notify(); + }); + child + }); + let view_4 = window.update(cx, |cx| { + let child = cx.add_view(|_| ViewB { id: 4, child: None }); + view_3.update(cx, |view, cx| { + view.child = Some(child.clone().into_any()); + cx.notify(); + }); + child + }); actions.borrow_mut().clear(); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_action(Some(view_4.id()), &Action("bar".to_string())) }); @@ -5887,7 +6011,7 @@ mod tests { view_3.keymap_context.add_identifier("b"); view_3.keymap_context.add_identifier("c"); - let (window_id, _view_1) = cx.add_window(Default::default(), |cx| { + let window = cx.add_window(Default::default(), |cx| { let view_2 = cx.add_view(|cx| { let view_3 = cx.add_view(|cx| { cx.focus_self(); @@ -6006,13 +6130,14 @@ mod tests { } } - let (window_id, view_1) = cx.add_window(|cx| { + let window = cx.add_window(|cx| { let view_2 = cx.add_view(|cx| { cx.focus_self(); View2 {} }); View1 { child: view_2 } }); + let view_1 = window.root(cx); let view_2 = view_1.read_with(cx, |view, _| view.child.clone()); cx.update(|cx| { @@ -6138,7 +6263,8 @@ mod tests { impl_actions!(test, [ActionWithArg]); - let (window_id, view) = cx.add_window(|_| View); + let window = cx.add_window(|_| View); + let view = window.root(cx); cx.update(|cx| { cx.add_global_action(|_: &ActionWithArg, _| {}); cx.add_bindings(vec![ @@ -6250,7 +6376,8 @@ mod tests { } } - let (_, view) = cx.add_window(|_| Counter(0)); + let window = cx.add_window(|_| Counter(0)); + let view = window.root(cx); let condition1 = view.condition(cx, |view, _| view.0 == 2); let condition2 = view.condition(cx, |view, _| view.0 == 3); @@ -6272,15 +6399,15 @@ mod tests { #[crate::test(self)] #[should_panic] async fn test_view_condition_timeout(cx: &mut TestAppContext) { - let (_, view) = cx.add_window(|_| TestView::default()); - view.condition(cx, |_, _| false).await; + let window = cx.add_window(|_| TestView::default()); + window.root(cx).condition(cx, |_, _| false).await; } #[crate::test(self)] #[should_panic(expected = "view dropped with pending condition")] async fn test_view_condition_panic_on_drop(cx: &mut TestAppContext) { - let (window_id, _root_view) = cx.add_window(|_| TestView::default()); - let view = cx.add_view(window_id, |_| TestView::default()); + let window = cx.add_window(|_| TestView::default()); + let view = window.add_view(cx, |_| TestView::default()); let condition = view.condition(cx, |_, _| false); cx.update(|_| drop(view)); @@ -6305,22 +6432,21 @@ mod tests { } } - let (window_id, root_view) = cx.add_window(Default::default(), |_| View(0)); - cx.update_window(window_id, |cx| { + let window = cx.add_window(Default::default(), |_| View(0)); + let root_view = window.root(cx); + window.update(cx, |cx| { assert_eq!( cx.window.rendered_views[&root_view.id()].name(), Some("render count: 0") ); }); - let view = cx - .update_window(window_id, |cx| { - cx.refresh_windows(); - cx.add_view(|_| View(0)) - }) - .unwrap(); + let view = window.update(cx, |cx| { + cx.refresh_windows(); + cx.add_view(|_| View(0)) + }); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { assert_eq!( cx.window.rendered_views[&root_view.id()].name(), Some("render count: 1") @@ -6333,7 +6459,7 @@ mod tests { cx.update(|cx| cx.refresh_windows()); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { assert_eq!( cx.window.rendered_views[&root_view.id()].name(), Some("render count: 2") @@ -6349,7 +6475,7 @@ mod tests { drop(view); }); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { assert_eq!( cx.window.rendered_views[&root_view.id()].name(), Some("render count: 3") @@ -6397,7 +6523,7 @@ mod tests { } let events = Rc::new(RefCell::new(Vec::new())); - let (window_1, _) = cx.add_window(|cx: &mut ViewContext| { + let window_1 = cx.add_window(|cx: &mut ViewContext| { cx.observe_window_activation({ let events = events.clone(); move |this, active, _| events.borrow_mut().push((this.0, active)) @@ -6407,7 +6533,7 @@ mod tests { }); assert_eq!(mem::take(&mut *events.borrow_mut()), [("window 1", true)]); - let (window_2, _) = cx.add_window(|cx: &mut ViewContext| { + let window_2 = cx.add_window(|cx: &mut ViewContext| { cx.observe_window_activation({ let events = events.clone(); move |this, active, _| events.borrow_mut().push((this.0, active)) @@ -6420,7 +6546,7 @@ mod tests { [("window 1", false), ("window 2", true)] ); - let (window_3, _) = cx.add_window(|cx: &mut ViewContext| { + let window_3 = cx.add_window(|cx: &mut ViewContext| { cx.observe_window_activation({ let events = events.clone(); move |this, active, _| events.borrow_mut().push((this.0, active)) diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs index f0c1699f165..c076a8a476f 100644 --- a/crates/gpui/src/app/ref_counts.rs +++ b/crates/gpui/src/app/ref_counts.rs @@ -23,8 +23,10 @@ struct ElementStateRefCount { #[derive(Default)] pub struct RefCounts { + window_counts: HashMap, entity_counts: HashMap, element_state_counts: HashMap, + dropped_windows: HashSet, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, dropped_element_states: HashSet, @@ -43,6 +45,18 @@ impl RefCounts { } } + pub fn inc_window(&mut self, window_id: usize) { + match self.window_counts.entry(window_id) { + Entry::Occupied(mut entry) => { + *entry.get_mut() += 1; + } + Entry::Vacant(entry) => { + entry.insert(1); + self.dropped_windows.remove(&window_id); + } + } + } + pub fn inc_model(&mut self, model_id: usize) { match self.entity_counts.entry(model_id) { Entry::Occupied(mut entry) => { @@ -85,6 +99,15 @@ impl RefCounts { } } + pub fn dec_window(&mut self, window_id: usize) { + let count = self.window_counts.get_mut(&window_id).unwrap(); + *count -= 1; + if *count == 0 { + self.entity_counts.remove(&window_id); + self.dropped_windows.insert(window_id); + } + } + pub fn dec_model(&mut self, model_id: usize) { let count = self.entity_counts.get_mut(&model_id).unwrap(); *count -= 1; diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 2fa86998837..0fa64f531e1 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -6,7 +6,7 @@ use crate::{ platform::{Event, InputHandler, KeyDownEvent, Platform}, Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle, - WindowContext, + WindowContext, WindowHandle, }; use collections::BTreeMap; use futures::Future; @@ -148,17 +148,18 @@ impl TestAppContext { self.cx.borrow_mut().add_model(build_model) } - pub fn add_window(&mut self, build_root_view: F) -> (usize, ViewHandle) + pub fn add_window(&mut self, build_root_view: F) -> WindowHandle where T: View, F: FnOnce(&mut ViewContext) -> T, { - let (window_id, view) = self + let window = self .cx .borrow_mut() .add_window(Default::default(), build_root_view); - self.simulate_window_activation(Some(window_id)); - (window_id, view) + self.simulate_window_activation(Some(window.id())); + + WindowHandle::new(window.id(), self.cx.borrow_mut().ref_counts.clone()) } pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle From 300ce61bd0d6bf77153669eb3be044a870e76676 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2023 08:25:40 -0600 Subject: [PATCH 026/105] WIP --- crates/gpui/src/app.rs | 76 ++++++++++++++----------- crates/gpui/src/app/ref_counts.rs | 5 +- crates/gpui/src/app/test_app_context.rs | 2 +- crates/gpui/src/app/window.rs | 2 +- 4 files changed, 47 insertions(+), 38 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b2d732d1700..adabcf0a8b9 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -498,8 +498,8 @@ pub struct AppContext { // Action Types -> Action Handlers global_actions: HashMap>, keystroke_matcher: KeymapMatcher, - next_entity_id: usize, - next_window_id: usize, + next_id: usize, + // next_window_id: usize, next_subscription_id: usize, frame_count: usize, @@ -558,8 +558,7 @@ impl AppContext { actions: Default::default(), global_actions: Default::default(), keystroke_matcher: KeymapMatcher::default(), - next_entity_id: 0, - next_window_id: 0, + next_id: 0, next_subscription_id: 0, frame_count: 0, subscriptions: Default::default(), @@ -1230,7 +1229,7 @@ impl AppContext { F: FnOnce(&mut ModelContext) -> T, { self.update(|this| { - let model_id = post_inc(&mut this.next_entity_id); + let model_id = post_inc(&mut this.next_id); let handle = ModelHandle::new(model_id, &this.ref_counts); let mut cx = ModelContext::new(this, model_id); let model = build_model(&mut cx); @@ -1310,7 +1309,7 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_window_id); + let window_id = post_inc(&mut this.next_id); let platform_window = this.platform .open_window(window_id, window_options, this.foreground.clone()); @@ -1326,7 +1325,7 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_window_id); + let window_id = post_inc(&mut this.next_id); let platform_window = this.platform.add_status_item(window_id); let window = this.build_window(window_id, platform_window, build_root_view); let root_view = window.root_view().clone().downcast::().unwrap(); @@ -3810,6 +3809,7 @@ pub struct WindowHandle { view_type: PhantomData, } +#[allow(dead_code)] impl WindowHandle { fn id(&self) -> usize { self.any_handle.id() @@ -3922,6 +3922,17 @@ impl AnyWindowHandle { pub fn id(&self) -> usize { self.window_id } + + pub fn downcast(self) -> Option> { + if TypeId::of::() == self.root_view_type { + Some(WindowHandle { + any_handle: self, + view_type: PhantomData, + }) + } else { + None + } + } } impl Drop for AnyWindowHandle { @@ -5613,20 +5624,18 @@ mod tests { child: None, }); let view_1 = window.root(cx); - let view_2 = window - .update(cx, |cx| { - let view_2 = cx.add_view(|_| View { - events: view_events.clone(), - name: "view 2".to_string(), - child: None, - }); - view_1.update(cx, |view_1, cx| { - view_1.child = Some(view_2.clone().into_any()); - cx.notify(); - }); - view_2 - }) - .unwrap(); + let view_2 = window.update(cx, |cx| { + let view_2 = cx.add_view(|_| View { + events: view_events.clone(), + name: "view 2".to_string(), + child: None, + }); + view_1.update(cx, |view_1, cx| { + view_1.child = Some(view_2.clone().into_any()); + cx.notify(); + }); + view_2 + }); let observed_events: Arc>> = Default::default(); view_1.update(cx, |_, cx| { @@ -6071,26 +6080,26 @@ mod tests { } }); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_keystroke(&Keystroke::parse("a").unwrap()) }); assert_eq!(&*actions.borrow(), &["2 a"]); actions.borrow_mut().clear(); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_keystroke(&Keystroke::parse("b").unwrap()); }); assert_eq!(&*actions.borrow(), &["3 b", "2 b", "1 b", "global b"]); actions.borrow_mut().clear(); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_keystroke(&Keystroke::parse("c").unwrap()); }); assert_eq!(&*actions.borrow(), &["3 c"]); actions.borrow_mut().clear(); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { cx.dispatch_keystroke(&Keystroke::parse("d").unwrap()); }); assert_eq!(&*actions.borrow(), &["2 d"]); @@ -6201,7 +6210,7 @@ mod tests { // Check that global actions do not have a binding, even if a binding does exist in another view assert_eq!( - &available_actions(window_id, view_1.id(), cx), + &available_actions(window.id(), view_1.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::GlobalAction", vec![]) @@ -6210,7 +6219,7 @@ mod tests { // Check that view 1 actions and bindings are available even when called from view 2 assert_eq!( - &available_actions(window_id, view_2.id(), cx), + &available_actions(window.id(), view_2.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::Action2", vec![Keystroke::parse("b").unwrap()]), @@ -6273,7 +6282,7 @@ mod tests { ]); }); - let actions = cx.available_actions(window_id, view.id()); + let actions = cx.available_actions(window.id(), view.id()); assert_eq!( actions[0].1.as_any().downcast_ref::(), Some(&ActionWithArg { arg: false }) @@ -6559,25 +6568,25 @@ mod tests { [("window 2", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_2)); + cx.simulate_window_activation(Some(window_2.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 3", false), ("window 2", true)] ); - cx.simulate_window_activation(Some(window_1)); + cx.simulate_window_activation(Some(window_1.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 2", false), ("window 1", true)] ); - cx.simulate_window_activation(Some(window_3)); + cx.simulate_window_activation(Some(window_3.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 1", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_3)); + cx.simulate_window_activation(Some(window_3.id())); assert_eq!(mem::take(&mut *events.borrow_mut()), []); } @@ -6633,12 +6642,13 @@ mod tests { let child_rendered = Rc::new(Cell::new(false)); let child_dropped = Rc::new(Cell::new(false)); - let (_, root_view) = cx.add_window(|cx| Parent { + let window = cx.add_window(|cx| Parent { child: Some(cx.add_view(|_| Child { rendered: child_rendered.clone(), dropped: child_dropped.clone(), })), }); + let root_view = window.root(cx); assert!(child_rendered.take()); assert!(!child_dropped.take()); diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs index c076a8a476f..74563d05bc0 100644 --- a/crates/gpui/src/app/ref_counts.rs +++ b/crates/gpui/src/app/ref_counts.rs @@ -23,7 +23,6 @@ struct ElementStateRefCount { #[derive(Default)] pub struct RefCounts { - window_counts: HashMap, entity_counts: HashMap, element_state_counts: HashMap, dropped_windows: HashSet, @@ -46,7 +45,7 @@ impl RefCounts { } pub fn inc_window(&mut self, window_id: usize) { - match self.window_counts.entry(window_id) { + match self.entity_counts.entry(window_id) { Entry::Occupied(mut entry) => { *entry.get_mut() += 1; } @@ -100,7 +99,7 @@ impl RefCounts { } pub fn dec_window(&mut self, window_id: usize) { - let count = self.window_counts.get_mut(&window_id).unwrap(); + let count = self.entity_counts.get_mut(&window_id).unwrap(); *count -= 1; if *count == 0 { self.entity_counts.remove(&window_id); diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 0fa64f531e1..80f10374665 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -60,7 +60,7 @@ impl TestAppContext { RefCounts::new(leak_detector), (), ); - cx.next_entity_id = first_entity_id; + cx.next_id = first_entity_id; let cx = TestAppContext { cx: Rc::new(RefCell::new(cx)), foreground_platform, diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index e4beb58873c..9dc5d99bc5e 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -1176,7 +1176,7 @@ impl<'a> WindowContext<'a> { F: FnOnce(&mut ViewContext) -> Option, { let window_id = self.window_id; - let view_id = post_inc(&mut self.next_entity_id); + let view_id = post_inc(&mut self.next_id); let mut cx = ViewContext::mutable(self, view_id); let handle = if let Some(view) = build_view(&mut cx) { let mut keymap_context = KeymapContext::default(); From 4c7d60ed131f563edc5c937e5bf3cf654f1c9178 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 09:07:35 -0700 Subject: [PATCH 027/105] Upgrade to rust 1.71 --- Dockerfile | 2 +- rust-toolchain.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2a78d37cbbc..77d011490e5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax = docker/dockerfile:1.2 -FROM rust:1.70-bullseye as builder +FROM rust:1.71-bullseye as builder WORKDIR app COPY . . diff --git a/rust-toolchain.toml b/rust-toolchain.toml index f78a67ddb34..50003020e9b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,4 +1,4 @@ [toolchain] -channel = "1.70" +channel = "1.71" components = [ "rustfmt" ] targets = [ "x86_64-apple-darwin", "aarch64-apple-darwin", "wasm32-wasi" ] From a127b0d3e6168de644267541b5bd29b6adfec55c Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 09:19:23 -0700 Subject: [PATCH 028/105] Fix warnings surfaced in Rust 1.71 --- crates/collab/src/tests/randomized_integration_tests.rs | 2 +- crates/editor/src/display_map/inlay_map.rs | 4 +--- crates/editor/src/inlay_hint_cache.rs | 1 - crates/project/src/worktree.rs | 2 +- crates/sum_tree/src/cursor.rs | 2 +- crates/vim/src/normal/search.rs | 2 +- crates/zed/src/zed.rs | 1 - 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index 8062a12b832..ae3e609b939 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -183,7 +183,7 @@ async fn apply_server_operation( let username; { let mut plan = plan.lock(); - let mut user = plan.user(user_id); + let user = plan.user(user_id); if user.online { return false; } diff --git a/crates/editor/src/display_map/inlay_map.rs b/crates/editor/src/display_map/inlay_map.rs index 6a59cecae8e..9794ac45c11 100644 --- a/crates/editor/src/display_map/inlay_map.rs +++ b/crates/editor/src/display_map/inlay_map.rs @@ -397,7 +397,7 @@ impl InlayMap { buffer_snapshot: MultiBufferSnapshot, mut buffer_edits: Vec>, ) -> (InlaySnapshot, Vec) { - let mut snapshot = &mut self.snapshot; + let snapshot = &mut self.snapshot; if buffer_edits.is_empty() { if snapshot.buffer.trailing_excerpt_update_count() @@ -572,7 +572,6 @@ impl InlayMap { }) .collect(); let buffer_snapshot = snapshot.buffer.clone(); - drop(snapshot); let (snapshot, edits) = self.sync(buffer_snapshot, buffer_edits); (snapshot, edits) } @@ -635,7 +634,6 @@ impl InlayMap { } log::info!("removing inlays: {:?}", to_remove); - drop(snapshot); let (snapshot, edits) = self.splice(to_remove, to_insert); (snapshot, edits) } diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 63076ba234d..3d4ea3d6f28 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -571,7 +571,6 @@ fn new_update_task( if let Some(buffer) = refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) { - drop(refresh_multi_buffer); editor.inlay_hint_cache.update_tasks.insert( pending_refresh_query.excerpt_id, UpdateTask { diff --git a/crates/project/src/worktree.rs b/crates/project/src/worktree.rs index b0795818b8d..9e30796bbc5 100644 --- a/crates/project/src/worktree.rs +++ b/crates/project/src/worktree.rs @@ -2369,7 +2369,7 @@ impl BackgroundScannerState { } // Remove any git repositories whose .git entry no longer exists. - let mut snapshot = &mut self.snapshot; + let snapshot = &mut self.snapshot; let mut repositories = mem::take(&mut snapshot.git_repositories); let mut repository_entries = mem::take(&mut snapshot.repository_entries); repositories.retain(|work_directory_id, _| { diff --git a/crates/sum_tree/src/cursor.rs b/crates/sum_tree/src/cursor.rs index efd6ac145e0..12ab12dc27b 100644 --- a/crates/sum_tree/src/cursor.rs +++ b/crates/sum_tree/src/cursor.rs @@ -202,7 +202,7 @@ where self.position = D::default(); } - let mut entry = self.stack.last_mut().unwrap(); + let entry = self.stack.last_mut().unwrap(); if !descending { if entry.index == 0 { self.stack.pop(); diff --git a/crates/vim/src/normal/search.rs b/crates/vim/src/normal/search.rs index 614866d9c9c..9375c4e78da 100644 --- a/crates/vim/src/normal/search.rs +++ b/crates/vim/src/normal/search.rs @@ -93,7 +93,7 @@ fn search_submit(workspace: &mut Workspace, _: &SearchSubmit, cx: &mut ViewConte pane.update(cx, |pane, cx| { if let Some(search_bar) = pane.toolbar().read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { - let mut state = &mut vim.state.search; + let state = &mut vim.state.search; let mut count = state.count; // in the case that the query has changed, the search bar diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4b0bf1cd4c6..b67bf0bab48 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -545,7 +545,6 @@ pub fn handle_keymap_file_changes( reload_keymaps(cx, &keymap_content); } }) - .detach(); })); } } From b0ec05a73242bc131d1e788fa278a057e4dfb569 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 2 Aug 2023 13:50:30 -0400 Subject: [PATCH 029/105] v0.99.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 a00b0735866..fbf4e750c62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9815,7 +9815,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.98.0" +version = "0.99.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index a5877aaccb4..95d6445d176 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.98.0" +version = "0.99.0" publish = false [lib] From 60e190e5001553e4c3c0cd472e6feb45ea07aca2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2023 12:08:56 -0600 Subject: [PATCH 030/105] WIP --- crates/copilot/src/sign_in.rs | 13 ++++++------- crates/gpui/src/app.rs | 10 +++++----- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 803cb5cc856..fec8f27c973 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -4,7 +4,7 @@ use gpui::{ geometry::rect::RectF, platform::{WindowBounds, WindowKind, WindowOptions}, AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, - ViewHandle, + WindowHandle, }; use theme::ui::modal; @@ -18,14 +18,14 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { - let mut code_verification: Option> = None; + let mut code_verification: Option> = None; cx.observe(&copilot, move |copilot, cx| { let status = copilot.read(cx).status(); match &status { crate::Status::SigningIn { prompt } => { if let Some(code_verification_handle) = code_verification.as_mut() { - let window_id = code_verification_handle.window_id(); + let window_id = code_verification_handle.id(); let updated = cx.update_window(window_id, |cx| { code_verification_handle.update(cx, |code_verification, cx| { code_verification.set_status(status.clone(), cx) @@ -66,7 +66,7 @@ pub fn init(cx: &mut AppContext) { fn create_copilot_auth_window( cx: &mut AppContext, status: &Status, -) -> ViewHandle { +) -> WindowHandle { let window_size = theme::current(cx).copilot.modal.dimensions(); let window_options = WindowOptions { bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), @@ -78,10 +78,9 @@ fn create_copilot_auth_window( is_movable: true, screen: None, }; - let (_, view) = cx.add_window(window_options, |_cx| { + cx.add_window(window_options, |_cx| { CopilotCodeVerification::new(status.clone()) - }); - view + }) } pub struct CopilotCodeVerification { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index adabcf0a8b9..9c0e50647c6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3811,10 +3811,6 @@ pub struct WindowHandle { #[allow(dead_code)] impl WindowHandle { - fn id(&self) -> usize { - self.any_handle.id() - } - fn new(window_id: usize, ref_counts: Arc>) -> Self { WindowHandle { any_handle: AnyWindowHandle::new::(window_id, ref_counts), @@ -3822,7 +3818,11 @@ impl WindowHandle { } } - fn root(&self, cx: &impl BorrowAppContext) -> ViewHandle { + pub fn id(&self) -> usize { + self.any_handle.id() + } + + pub fn root(&self, cx: &impl BorrowAppContext) -> ViewHandle { self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap()) } From 9e755bb85582f2cbe953492e9a14ce0a166f24c1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 2 Aug 2023 12:15:39 -0700 Subject: [PATCH 031/105] Revert "Extract syntax highlighting properties from tree-sitter highlight queries (#2797)" This reverts commit 45c635872b5ef7bb8994e16d593aa25edf8e94bf, reversing changes made to f2b82369f27b79fdcaa6a4276bb047abddf7171c. --- crates/zed/src/languages/bash/highlights.scm | 2 +- crates/zed/src/languages/c/highlights.scm | 3 +- crates/zed/src/languages/c/injections.scm | 4 +- crates/zed/src/languages/cpp/highlights.scm | 4 +- crates/zed/src/languages/cpp/injections.scm | 4 +- crates/zed/src/languages/css/highlights.scm | 2 +- crates/zed/src/languages/elixir/embedding.scm | 6 +- .../zed/src/languages/elixir/highlights.scm | 18 +- .../zed/src/languages/elixir/injections.scm | 4 +- crates/zed/src/languages/elixir/outline.scm | 4 +- crates/zed/src/languages/elm/injections.scm | 2 +- crates/zed/src/languages/erb/injections.scm | 8 +- crates/zed/src/languages/glsl/highlights.scm | 4 +- crates/zed/src/languages/heex/injections.scm | 6 +- crates/zed/src/languages/html/injections.scm | 4 +- .../src/languages/javascript/highlights.scm | 6 +- crates/zed/src/languages/lua/highlights.scm | 10 +- crates/zed/src/languages/php/highlights.scm | 10 +- crates/zed/src/languages/php/injections.scm | 4 +- .../zed/src/languages/python/highlights.scm | 8 +- .../zed/src/languages/racket/highlights.scm | 7 +- crates/zed/src/languages/racket/outline.scm | 4 +- crates/zed/src/languages/ruby/brackets.scm | 2 +- crates/zed/src/languages/ruby/highlights.scm | 8 +- crates/zed/src/languages/rust/highlights.scm | 4 +- crates/zed/src/languages/rust/injections.scm | 4 +- .../zed/src/languages/scheme/highlights.scm | 4 +- crates/zed/src/languages/scheme/outline.scm | 4 +- .../zed/src/languages/svelte/injections.scm | 14 +- .../src/languages/typescript/highlights.scm | 10 +- styles/package.json | 1 - styles/src/build_themes.ts | 9 +- styles/src/build_tokens.ts | 4 +- styles/src/component/icon_button.ts | 5 +- styles/src/component/tab_bar_button.ts | 67 ++- styles/src/component/text_button.ts | 5 +- styles/src/style_tree/app.ts | 2 +- styles/src/style_tree/assistant.ts | 69 ++-- styles/src/style_tree/editor.ts | 32 +- styles/src/style_tree/feedback.ts | 2 +- styles/src/style_tree/picker.ts | 2 +- styles/src/style_tree/project_panel.ts | 16 +- styles/src/style_tree/status_bar.ts | 10 +- styles/src/style_tree/titlebar.ts | 4 +- styles/src/theme/create_theme.ts | 30 +- styles/src/theme/syntax.ts | 387 ++++++++++++++---- styles/src/theme/theme_config.ts | 6 +- styles/src/theme/tokens/theme.ts | 10 +- styles/src/themes/atelier/common.ts | 9 +- styles/src/themes/ayu/common.ts | 6 +- styles/src/themes/gruvbox/gruvbox-common.ts | 6 +- styles/src/themes/one/one-dark.ts | 4 +- styles/src/themes/one/one-light.ts | 2 + styles/src/themes/rose-pine/common.ts | 4 +- styles/src/types/extract_syntax_types.ts | 111 ----- styles/src/types/syntax.ts | 202 --------- styles/tsconfig.json | 4 +- 57 files changed, 553 insertions(+), 630 deletions(-) delete mode 100644 styles/src/types/extract_syntax_types.ts delete mode 100644 styles/src/types/syntax.ts diff --git a/crates/zed/src/languages/bash/highlights.scm b/crates/zed/src/languages/bash/highlights.scm index f3e0c9529a1..a72c5468edd 100644 --- a/crates/zed/src/languages/bash/highlights.scm +++ b/crates/zed/src/languages/bash/highlights.scm @@ -54,5 +54,5 @@ ( (command (_) @constant) - (.match? @constant "^-") + (#match? @constant "^-") ) diff --git a/crates/zed/src/languages/c/highlights.scm b/crates/zed/src/languages/c/highlights.scm index 5245e53a052..064ec61a378 100644 --- a/crates/zed/src/languages/c/highlights.scm +++ b/crates/zed/src/languages/c/highlights.scm @@ -86,7 +86,7 @@ (identifier) @variable ((identifier) @constant - (.match? @constant "^_*[A-Z][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) (call_expression function: (identifier) @function) @@ -106,3 +106,4 @@ (primitive_type) (sized_type_specifier) ] @type + diff --git a/crates/zed/src/languages/c/injections.scm b/crates/zed/src/languages/c/injections.scm index fbc7d83f821..845a63bd1bd 100644 --- a/crates/zed/src/languages/c/injections.scm +++ b/crates/zed/src/languages/c/injections.scm @@ -1,7 +1,7 @@ (preproc_def value: (preproc_arg) @content - (.set! "language" "c")) + (#set! "language" "c")) (preproc_function_def value: (preproc_arg) @content - (.set! "language" "c")) + (#set! "language" "c")) \ No newline at end of file diff --git a/crates/zed/src/languages/cpp/highlights.scm b/crates/zed/src/languages/cpp/highlights.scm index a040b1d053c..bcfa01ca5c6 100644 --- a/crates/zed/src/languages/cpp/highlights.scm +++ b/crates/zed/src/languages/cpp/highlights.scm @@ -31,13 +31,13 @@ declarator: (field_identifier) @function) ((namespace_identifier) @type - (.match? @type "^[A-Z]")) + (#match? @type "^[A-Z]")) (auto) @type (type_identifier) @type ((identifier) @constant - (.match? @constant "^_*[A-Z][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) (field_identifier) @property (statement_identifier) @label diff --git a/crates/zed/src/languages/cpp/injections.scm b/crates/zed/src/languages/cpp/injections.scm index 3c94ba4061f..eca372d577b 100644 --- a/crates/zed/src/languages/cpp/injections.scm +++ b/crates/zed/src/languages/cpp/injections.scm @@ -1,7 +1,7 @@ (preproc_def value: (preproc_arg) @content - (.set! "language" "c++")) + (#set! "language" "c++")) (preproc_function_def value: (preproc_arg) @content - (.set! "language" "c++")) + (#set! "language" "c++")) \ No newline at end of file diff --git a/crates/zed/src/languages/css/highlights.scm b/crates/zed/src/languages/css/highlights.scm index 83f99861c59..e271d8583c6 100644 --- a/crates/zed/src/languages/css/highlights.scm +++ b/crates/zed/src/languages/css/highlights.scm @@ -46,7 +46,7 @@ (property_name) (plain_value) ] @variable.special - (.match? @variable.special "^--") + (#match? @variable.special "^--") ) [ diff --git a/crates/zed/src/languages/elixir/embedding.scm b/crates/zed/src/languages/elixir/embedding.scm index 3c523c24876..16ad20746d4 100644 --- a/crates/zed/src/languages/elixir/embedding.scm +++ b/crates/zed/src/languages/elixir/embedding.scm @@ -3,7 +3,7 @@ operator: "@" operand: (call target: (identifier) @unary - (.match? @unary "^(doc)$")) + (#match? @unary "^(doc)$")) ) @context . (call @@ -18,10 +18,10 @@ target: (identifier) @name) operator: "when") ]) - (.match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#match? @name "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item ) (call target: (identifier) @name (arguments (alias) @name) - (.match? @name "^(defmodule|defprotocol)$")) @item + (#match? @name "^(defmodule|defprotocol)$")) @item diff --git a/crates/zed/src/languages/elixir/highlights.scm b/crates/zed/src/languages/elixir/highlights.scm index a8fd7eb45a6..0e779d195c5 100644 --- a/crates/zed/src/languages/elixir/highlights.scm +++ b/crates/zed/src/languages/elixir/highlights.scm @@ -54,13 +54,13 @@ (sigil_name) @__name__ quoted_start: _ @string quoted_end: _ @string - (.match? @__name__ "^[sS]$")) @string + (#match? @__name__ "^[sS]$")) @string (sigil (sigil_name) @__name__ quoted_start: _ @string.regex quoted_end: _ @string.regex - (.match? @__name__ "^[rR]$")) @string.regex + (#match? @__name__ "^[rR]$")) @string.regex (sigil (sigil_name) @__name__ @@ -69,7 +69,7 @@ ( (identifier) @comment.unused - (.match? @comment.unused "^_") + (#match? @comment.unused "^_") ) (call @@ -91,7 +91,7 @@ operator: "|>" right: (identifier)) ]) - (.match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) (binary_operator operator: "|>" @@ -99,15 +99,15 @@ (call target: (identifier) @keyword - (.match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$")) + (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$")) (call target: (identifier) @keyword - (.match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$")) + (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$")) ( (identifier) @constant.builtin - (.match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") + (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") ) (unary_operator @@ -121,7 +121,7 @@ (sigil) (boolean) ] @comment.doc)) - (.match? @__attribute__ "^(moduledoc|typedoc|doc)$")) + (#match? @__attribute__ "^(moduledoc|typedoc|doc)$")) (comment) @comment @@ -150,4 +150,4 @@ ((sigil (sigil_name) @_sigil_name (quoted_content) @embedded) - (.eq? @_sigil_name "H")) + (#eq? @_sigil_name "H")) diff --git a/crates/zed/src/languages/elixir/injections.scm b/crates/zed/src/languages/elixir/injections.scm index 5d445a7b820..4de229f1046 100644 --- a/crates/zed/src/languages/elixir/injections.scm +++ b/crates/zed/src/languages/elixir/injections.scm @@ -3,5 +3,5 @@ ((sigil (sigil_name) @_sigil_name (quoted_content) @content) - (.eq? @_sigil_name "H") - (.set! language "heex")) + (#eq? @_sigil_name "H") + (#set! language "heex")) diff --git a/crates/zed/src/languages/elixir/outline.scm b/crates/zed/src/languages/elixir/outline.scm index 756d3965103..a3311fb6d46 100644 --- a/crates/zed/src/languages/elixir/outline.scm +++ b/crates/zed/src/languages/elixir/outline.scm @@ -1,7 +1,7 @@ (call target: (identifier) @context (arguments (alias) @name) - (.match? @context "^(defmodule|defprotocol)$")) @item + (#match? @context "^(defmodule|defprotocol)$")) @item (call target: (identifier) @context @@ -23,4 +23,4 @@ ")" @context.extra)) operator: "when") ]) - (.match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item + (#match? @context "^(def|defp|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp)$")) @item diff --git a/crates/zed/src/languages/elm/injections.scm b/crates/zed/src/languages/elm/injections.scm index 3456f59a04a..0567320675a 100644 --- a/crates/zed/src/languages/elm/injections.scm +++ b/crates/zed/src/languages/elm/injections.scm @@ -1,2 +1,2 @@ ((glsl_content) @content - (.set! "language" "glsl")) + (#set! "language" "glsl")) diff --git a/crates/zed/src/languages/erb/injections.scm b/crates/zed/src/languages/erb/injections.scm index d9801015b7c..7a69a818ef3 100644 --- a/crates/zed/src/languages/erb/injections.scm +++ b/crates/zed/src/languages/erb/injections.scm @@ -1,7 +1,7 @@ ((code) @content - (.set! "language" "ruby") - (.set! "combined")) + (#set! "language" "ruby") + (#set! "combined")) ((content) @content - (.set! "language" "html") - (.set! "combined")) + (#set! "language" "html") + (#set! "combined")) diff --git a/crates/zed/src/languages/glsl/highlights.scm b/crates/zed/src/languages/glsl/highlights.scm index 2378b8449b5..e4503c6fbba 100644 --- a/crates/zed/src/languages/glsl/highlights.scm +++ b/crates/zed/src/languages/glsl/highlights.scm @@ -74,7 +74,7 @@ (sized_type_specifier) @type ((identifier) @constant - (.match? @constant "^[A-Z][A-Z\\d_]*$")) + (#match? @constant "^[A-Z][A-Z\\d_]*$")) (identifier) @variable @@ -114,5 +114,5 @@ ( (identifier) @variable.builtin - (.match? @variable.builtin "^gl_") + (#match? @variable.builtin "^gl_") ) diff --git a/crates/zed/src/languages/heex/injections.scm b/crates/zed/src/languages/heex/injections.scm index 1b63005cbfa..b503bcb28dc 100644 --- a/crates/zed/src/languages/heex/injections.scm +++ b/crates/zed/src/languages/heex/injections.scm @@ -5,9 +5,9 @@ (expression_value) (ending_expression_value) ] @content) - (.set! language "elixir") - (.set! combined) + (#set! language "elixir") + (#set! combined) ) ((expression (expression_value) @content) - (.set! language "elixir")) + (#set! language "elixir")) diff --git a/crates/zed/src/languages/html/injections.scm b/crates/zed/src/languages/html/injections.scm index 7d2ed0a225e..9084e373f21 100644 --- a/crates/zed/src/languages/html/injections.scm +++ b/crates/zed/src/languages/html/injections.scm @@ -1,7 +1,7 @@ (script_element (raw_text) @content - (.set! "language" "javascript")) + (#set! "language" "javascript")) (style_element (raw_text) @content - (.set! "language" "css")) + (#set! "language" "css")) diff --git a/crates/zed/src/languages/javascript/highlights.scm b/crates/zed/src/languages/javascript/highlights.scm index 7761bbb3a2d..36ab21ca1ec 100644 --- a/crates/zed/src/languages/javascript/highlights.scm +++ b/crates/zed/src/languages/javascript/highlights.scm @@ -44,7 +44,7 @@ ; Special identifiers ((identifier) @type - (.match? @type "^[A-Z]")) + (#match? @type "^[A-Z]")) (type_identifier) @type (predefined_type) @type.builtin @@ -53,7 +53,7 @@ (shorthand_property_identifier) (shorthand_property_identifier_pattern) ] @constant -(.match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) ; Literals @@ -214,4 +214,4 @@ "type" "readonly" "override" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/lua/highlights.scm b/crates/zed/src/languages/lua/highlights.scm index e00d0b9557c..f061bbf8f91 100644 --- a/crates/zed/src/languages/lua/highlights.scm +++ b/crates/zed/src/languages/lua/highlights.scm @@ -127,7 +127,7 @@ (identifier) @variable ((identifier) @variable.special - (.eq? @variable.special "self")) + (#eq? @variable.special "self")) (variable_list attribute: (attribute @@ -137,7 +137,7 @@ ;; Constants ((identifier) @constant - (.match? @constant "^[A-Z][A-Z_0-9]*$")) + (#match? @constant "^[A-Z][A-Z_0-9]*$")) (vararg_expression) @constant @@ -158,7 +158,7 @@ [ "{" "}" -] @method.constructor) +] @constructor) ;; Functions @@ -180,7 +180,7 @@ (function_call (identifier) @function.builtin - (.any-of? @function.builtin + (#any-of? @function.builtin ;; built-in functions in Lua 5.1 "assert" "collectgarbage" "dofile" "error" "getfenv" "getmetatable" "ipairs" "load" "loadfile" "loadstring" "module" "next" "pairs" "pcall" "print" @@ -195,4 +195,4 @@ (number) @number -(string) @string +(string) @string \ No newline at end of file diff --git a/crates/zed/src/languages/php/highlights.scm b/crates/zed/src/languages/php/highlights.scm index fb85d997fad..fcb087c47d1 100644 --- a/crates/zed/src/languages/php/highlights.scm +++ b/crates/zed/src/languages/php/highlights.scm @@ -43,15 +43,15 @@ (relative_scope) @variable.builtin ((name) @constant - (.match? @constant "^_?[A-Z][A-Z\\d_]+$")) + (#match? @constant "^_?[A-Z][A-Z\\d_]+$")) ((name) @constant.builtin - (.match? @constant.builtin "^__[A-Z][A-Z\d_]+__$")) + (#match? @constant.builtin "^__[A-Z][A-Z\d_]+__$")) -((name) @method.constructor -(.match? @method.constructor "^[A-Z]")) +((name) @constructor + (#match? @constructor "^[A-Z]")) ((name) @variable.builtin - (.eq? @variable.builtin "this")) + (#eq? @variable.builtin "this")) (variable_name) @variable diff --git a/crates/zed/src/languages/php/injections.scm b/crates/zed/src/languages/php/injections.scm index 725729337b2..57abd8ea2b0 100644 --- a/crates/zed/src/languages/php/injections.scm +++ b/crates/zed/src/languages/php/injections.scm @@ -1,3 +1,3 @@ ((text) @content - (.set! "language" "html") - (.set! "combined")) + (#set! "language" "html") + (#set! "combined")) diff --git a/crates/zed/src/languages/python/highlights.scm b/crates/zed/src/languages/python/highlights.scm index b31bddaeb50..71ab963d826 100644 --- a/crates/zed/src/languages/python/highlights.scm +++ b/crates/zed/src/languages/python/highlights.scm @@ -18,16 +18,16 @@ ; Identifier naming conventions ((identifier) @type - (.match? @type "^[A-Z]")) + (#match? @type "^[A-Z]")) ((identifier) @constant - (.match? @constant "^_*[A-Z][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) ; Builtin functions ((call function: (identifier) @function.builtin) - (.match? + (#match? @function.builtin "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$")) @@ -122,4 +122,4 @@ "yield" "match" "case" -] @keyword +] @keyword \ No newline at end of file diff --git a/crates/zed/src/languages/racket/highlights.scm b/crates/zed/src/languages/racket/highlights.scm index 304b10a018b..2c0caf89357 100644 --- a/crates/zed/src/languages/racket/highlights.scm +++ b/crates/zed/src/languages/racket/highlights.scm @@ -22,7 +22,7 @@ (lang_name) @variable.special ((symbol) @operator - (.match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) + (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) (list . @@ -31,9 +31,10 @@ (list . (symbol) @keyword - (.match? @keyword + (#match? @keyword "^(unit-from-context|for/last|syntax-case|match-let\\*-values|define-for-syntax|define/subexpression-pos-prop|set-field!|class-field-accessor|invoke-unit|#%stratified-body|for\\*/and|for\\*/weak-set|flat-rec-contract|for\\*/stream|planet|for/mutable-seteqv|log-error|delay|#%declare|prop:dict/contract|->d|lib|override\\*|define-local-member-name|send-generic|for\\*/hasheq|define-syntax|submod|except|include-at/relative-to/reader|public\\*|define-member-name|define/public|let\\*|for/and|for\\*/first|for|delay/strict|define-values-for-export|==|match-define-values|for/weak-seteq|for\\*/async|for/stream|for/weak-seteqv|set!-values|lambda|for\\*/product|augment-final\\*|pubment\\*|command-line|contract|case|struct-field-index|contract-struct|unless|for/hasheq|for/seteqv|with-method|define-values-for-syntax|for-template|pubment|for\\*/list|syntax-case\\*|init-field|define-serializable-class|=>|for/foldr/derived|letrec-syntaxes|overment\\*|unquote-splicing|_|inherit-field|for\\*|stream-lazy|match-lambda\\*|contract-pos/neg-doubling|unit/c|match-define|for\\*/set|unit/s|nor|#%expression|class/c|this%|place/context|super-make-object|when|set!|parametric->/c|syntax-id-rules|include/reader|compound-unit|override-final|get-field|gen:dict|for\\*/seteqv|for\\*/hash|#%provide|combine-out|link|with-contract-continuation-mark|define-struct/derived|stream\\*|λ|rename-out|define-serializable-class\\*|augment|define/augment|let|define-signature-form|letrec-syntax|abstract|define-namespace-anchor|#%module-begin|#%top-interaction|for\\*/weak-seteqv|do|define/subexpression-pos-prop/name|absent|send/apply|with-handlers\\*|all-from-out|provide-signature-elements|gen:stream|define/override-final|for\\*/mutable-seteqv|rename|quasisyntax/loc|instantiate|for/list|extends|include-at/relative-to|mixin|define/pubment|#%plain-lambda|except-out|#%plain-module-begin|init|for\\*/last|relative-in|define-unit/new-import-export|->dm|member-name-key|nand|interface\\*|struct|define/override|else|define/augment-final|failure-cont|open|log-info|define/final-prop|all-defined-out|for/sum|for\\*/sum|recursive-contract|define|define-logger|match\\*|log-debug|rename-inner|->|struct/derived|unit|class\\*|prefix-out|any|define/overment|define-signature|match-letrec-values|let-syntaxes|for/mutable-set|define/match|cond|super-instantiate|define-contract-struct|import|hash/dc|define-custom-set-types|public-final|for/vector|for-label|prefix-in|for\\*/foldr/derived|define-unit-binding|object-contract|syntax-rules|augride|for\\*/mutable-seteq|quasisyntax|inner|for-syntax|overment|send/keyword-apply|generic|let\\*-values|->m|define-values|struct-copy|init-depend|struct/ctc|match-lambda|#%printing-module-begin|match\\*/derived|case->m|this|file|stream-cons|inspect|field|for/weak-set|struct\\*|gen:custom-write|thunk\\*|combine-in|unquote|for/lists|define/private|for\\*/foldr|define-unit/s|with-continuation-mark|begin|prefix|quote-syntax/prune|object/c|interface|match/derived|for/hasheqv|current-contract-region|define-compound-unit|override|define/public-final|recontract-out|let/cc|augride\\*|inherit|send|define-values/invoke-unit|for/mutable-seteq|#%datum|for/first|match-let\\*|invoke-unit/infer|define/contract|syntax/loc|for\\*/hasheqv|define-sequence-syntax|let/ec|for/product|for\\*/fold/derived|define-syntax-rule|lazy|unconstrained-domain->|augment-final|private|class|define-splicing-for-clause-syntax|for\\*/fold|prompt-tag/c|contract-out|match/values|public-final\\*|case-lambda|for/fold|unsyntax|for/set|begin0|#%require|time|public|define-struct|include|define-values/invoke-unit/infer|only-space-in|struct/c|only-meta-in|unit/new-import-export|place|begin-for-syntax|shared|inherit/super|quote|for/or|struct/contract|export|inherit/inner|struct-out|let-syntax|augment\\*|for\\*/vector|rename-in|match-let|define-unit|:do-in|~@|for\\*/weak-seteq|private\\*|and|except-in|log-fatal|gen:equal\\+hash|provide|require|thunk|invariant-assertion|define-match-expander|init-rest|->\\*|class/derived|super-new|for/fold/derived|for\\*/mutable-set|match-lambda\\*\\*|only|with-contract|~\\?|opt/c|let-values|delay/thread|->i|for/foldr|for-meta|only-in|send\\+|\\.\\.\\.|struct-guard/c|->\\*m|gen:set|struct/dc|define-syntaxes|if|parameterize|module\\*|module|send\\*|#%variable-reference|compound-unit/infer|#%plain-app|for/hash|contracted|case->|match|for\\*/lists|#%app|letrec-values|log-warning|super|define/augride|local-require|provide/contract|define-struct/contract|match-let-values|quote-syntax|for\\*/seteq|define-compound-unit/infer|parameterize\\*|values/drop|for/seteq|tag|stream|delay/idle|module\\+|define-custom-hash-types|cons/dc|define-module-boundary-contract|or|protect-out|define-opt/c|implies|letrec-syntaxes\\+values|for\\*/or|unsyntax-splicing|override-final\\*|for/async|parameterize-break|syntax|place\\*|for-space|quasiquote|with-handlers|delay/sync|define-unit-from-context|match-letrec|#%top|define-unit/contract|delay/name|new|field-bound\\?|letrec|class-field-mutator|with-syntax|flat-murec-contract|rename-super|local)$" )) ((symbol) @comment - (.match? @comment "^#[cC][iIsS]$")) + (#match? @comment "^#[cC][iIsS]$")) + diff --git a/crates/zed/src/languages/racket/outline.scm b/crates/zed/src/languages/racket/outline.scm index 188067078de..604e052a63f 100644 --- a/crates/zed/src/languages/racket/outline.scm +++ b/crates/zed/src/languages/racket/outline.scm @@ -6,5 +6,5 @@ (symbol) @name (list . (symbol) @name) ] - (.match? @start-symbol "^define") -) @item + (#match? @start-symbol "^define") +) @item \ No newline at end of file diff --git a/crates/zed/src/languages/ruby/brackets.scm b/crates/zed/src/languages/ruby/brackets.scm index f5129f8f310..957b20ecdb4 100644 --- a/crates/zed/src/languages/ruby/brackets.scm +++ b/crates/zed/src/languages/ruby/brackets.scm @@ -11,4 +11,4 @@ (begin "begin" @open "end" @close) (module "module" @open "end" @close) (_ . "def" @open "end" @close) -(_ . "class" @open "end" @close) +(_ . "class" @open "end" @close) \ No newline at end of file diff --git a/crates/zed/src/languages/ruby/highlights.scm b/crates/zed/src/languages/ruby/highlights.scm index 93cf2608f4e..2610cfa1ccf 100644 --- a/crates/zed/src/languages/ruby/highlights.scm +++ b/crates/zed/src/languages/ruby/highlights.scm @@ -33,12 +33,12 @@ (identifier) @variable ((identifier) @keyword - (.match? @keyword "^(private|protected|public)$")) + (#match? @keyword "^(private|protected|public)$")) ; Function calls ((identifier) @function.method.builtin - (.eq? @function.method.builtin "require")) + (#eq? @function.method.builtin "require")) "defined?" @function.method.builtin @@ -60,7 +60,7 @@ ] @property ((identifier) @constant.builtin - (.match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) + (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) (file) @constant.builtin (line) @constant.builtin @@ -71,7 +71,7 @@ ) @constant.builtin ((constant) @constant - (.match? @constant "^[A-Z\\d_]+$")) + (#match? @constant "^[A-Z\\d_]+$")) (constant) @type diff --git a/crates/zed/src/languages/rust/highlights.scm b/crates/zed/src/languages/rust/highlights.scm index 54dbfa00bd3..7240173a892 100644 --- a/crates/zed/src/languages/rust/highlights.scm +++ b/crates/zed/src/languages/rust/highlights.scm @@ -38,11 +38,11 @@ ; Assume uppercase names are types/enum-constructors ((identifier) @type - (.match? @type "^[A-Z]")) + (#match? @type "^[A-Z]")) ; Assume all-caps names are constants ((identifier) @constant - (.match? @constant "^_*[A-Z][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z][A-Z\\d_]*$")) [ "(" diff --git a/crates/zed/src/languages/rust/injections.scm b/crates/zed/src/languages/rust/injections.scm index 78fde3752fd..57ebea85393 100644 --- a/crates/zed/src/languages/rust/injections.scm +++ b/crates/zed/src/languages/rust/injections.scm @@ -1,7 +1,7 @@ (macro_invocation (token_tree) @content - (.set! "language" "rust")) + (#set! "language" "rust")) (macro_rule (token_tree) @content - (.set! "language" "rust")) + (#set! "language" "rust")) \ No newline at end of file diff --git a/crates/zed/src/languages/scheme/highlights.scm b/crates/zed/src/languages/scheme/highlights.scm index 201b0e92768..40ba61cd055 100644 --- a/crates/zed/src/languages/scheme/highlights.scm +++ b/crates/zed/src/languages/scheme/highlights.scm @@ -14,7 +14,7 @@ (directive)] @comment ((symbol) @operator - (.match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) + (#match? @operator "^(\\+|-|\\*|/|=|>|<|>=|<=)$")) (list . @@ -23,6 +23,6 @@ (list . (symbol) @keyword - (.match? @keyword + (#match? @keyword "^(define-syntax|let\\*|lambda|λ|case|=>|quote-splicing|unquote-splicing|set!|let|letrec|letrec-syntax|let-values|let\\*-values|do|else|define|cond|syntax-rules|unquote|begin|quote|let-syntax|and|if|quasiquote|letrec|delay|or|when|unless|identifier-syntax|assert|library|export|import|rename|only|except|prefix)$" )) diff --git a/crates/zed/src/languages/scheme/outline.scm b/crates/zed/src/languages/scheme/outline.scm index 188067078de..604e052a63f 100644 --- a/crates/zed/src/languages/scheme/outline.scm +++ b/crates/zed/src/languages/scheme/outline.scm @@ -6,5 +6,5 @@ (symbol) @name (list . (symbol) @name) ] - (.match? @start-symbol "^define") -) @item + (#match? @start-symbol "^define") +) @item \ No newline at end of file diff --git a/crates/zed/src/languages/svelte/injections.scm b/crates/zed/src/languages/svelte/injections.scm index 17719b325c0..8c1ac9fcd0b 100755 --- a/crates/zed/src/languages/svelte/injections.scm +++ b/crates/zed/src/languages/svelte/injections.scm @@ -2,27 +2,27 @@ ; -------------- (script_element (raw_text) @content - (.set! "language" "javascript")) + (#set! "language" "javascript")) ((script_element (start_tag (attribute (quoted_attribute_value (attribute_value) @_language))) (raw_text) @content) - (.eq? @_language "ts") - (.set! "language" "typescript")) + (#eq? @_language "ts") + (#set! "language" "typescript")) ((script_element (start_tag (attribute (quoted_attribute_value (attribute_value) @_language))) (raw_text) @content) - (.eq? @_language "typescript") - (.set! "language" "typescript")) + (#eq? @_language "typescript") + (#set! "language" "typescript")) (style_element (raw_text) @content - (.set! "language" "css")) + (#set! "language" "css")) ((raw_text_expr) @content - (.set! "language" "javascript")) + (#set! "language" "javascript")) diff --git a/crates/zed/src/languages/typescript/highlights.scm b/crates/zed/src/languages/typescript/highlights.scm index 9272108670a..bf086ea156f 100644 --- a/crates/zed/src/languages/typescript/highlights.scm +++ b/crates/zed/src/languages/typescript/highlights.scm @@ -43,11 +43,11 @@ ; Special identifiers -((identifier) @method.constructor - (.match? @method.constructor "^[A-Z]")) +((identifier) @constructor + (#match? @constructor "^[A-Z]")) ((identifier) @type - (.match? @type "^[A-Z]")) + (#match? @type "^[A-Z]")) (type_identifier) @type (predefined_type) @type.builtin @@ -56,7 +56,7 @@ (shorthand_property_identifier) (shorthand_property_identifier_pattern) ] @constant -(.match? @constant "^_*[A-Z_][A-Z\\d_]*$")) + (#match? @constant "^_*[A-Z_][A-Z\\d_]*$")) ; Literals @@ -218,4 +218,4 @@ "type" "readonly" "override" -] @keyword +] @keyword \ No newline at end of file diff --git a/styles/package.json b/styles/package.json index 3a50ac53716..16e95d90d5b 100644 --- a/styles/package.json +++ b/styles/package.json @@ -8,7 +8,6 @@ "build-licenses": "ts-node ./src/build_licenses.ts", "build-tokens": "ts-node ./src/build_tokens.ts", "build-types": "ts-node ./src/build_types.ts", - "generate-syntax": "ts-node ./src/types/extract_syntax_types.ts", "test": "vitest" }, "author": "Zed Industries (https://github.com/zed-industries/)", diff --git a/styles/src/build_themes.ts b/styles/src/build_themes.ts index 4d262f8146e..17575663a1f 100644 --- a/styles/src/build_themes.ts +++ b/styles/src/build_themes.ts @@ -21,7 +21,9 @@ function clear_themes(theme_directory: string) { } } -const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) +) function write_themes(themes: Theme[], output_directory: string) { clear_themes(output_directory) @@ -32,7 +34,10 @@ function write_themes(themes: Theme[], output_directory: string) { const style_tree = app() const style_tree_json = JSON.stringify(style_tree, null, 2) const temp_path = path.join(temp_directory, `${theme.name}.json`) - const out_path = path.join(output_directory, `${theme.name}.json`) + const out_path = path.join( + output_directory, + `${theme.name}.json` + ) fs.writeFileSync(temp_path, style_tree_json) fs.renameSync(temp_path, out_path) console.log(`- ${out_path} created`) diff --git a/styles/src/build_tokens.ts b/styles/src/build_tokens.ts index 3c52b6d9896..fd6aa18ced5 100644 --- a/styles/src/build_tokens.ts +++ b/styles/src/build_tokens.ts @@ -83,6 +83,8 @@ function write_tokens(themes: Theme[], tokens_directory: string) { console.log(`- ${METADATA_FILE} created`) } -const all_themes: Theme[] = themes.map((theme) => create_theme(theme)) +const all_themes: Theme[] = themes.map((theme) => + create_theme(theme) +) write_tokens(all_themes, TOKENS_DIRECTORY) diff --git a/styles/src/component/icon_button.ts b/styles/src/component/icon_button.ts index 13dfce6d776..6887fc7c30e 100644 --- a/styles/src/component/icon_button.ts +++ b/styles/src/component/icon_button.ts @@ -10,7 +10,10 @@ export type Margin = { } interface IconButtonOptions { - layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] + layer?: + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial } diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts index 9e7f9acfc31..0c43e7010e5 100644 --- a/styles/src/component/tab_bar_button.ts +++ b/styles/src/component/tab_bar_button.ts @@ -12,47 +12,44 @@ type TabBarButtonProps = TabBarButtonOptions & { state?: Partial>> } -export function tab_bar_button( - theme: Theme, - { icon, color = "base" }: TabBarButtonProps -) { +export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) { const button_spacing = 8 - return interactive({ - base: { - icon: { - color: foreground(theme.middle, color), - asset: icon, - dimensions: { - width: 15, - height: 15, + return ( + interactive({ + base: { + icon: { + color: foreground(theme.middle, color), + asset: icon, + dimensions: { + width: 15, + height: 15, + }, }, - }, - container: { - corner_radius: 4, - padding: { - top: 4, - bottom: 4, - left: 4, - right: 4, - }, - margin: { - left: button_spacing / 2, - right: button_spacing / 2, - }, - }, - }, - state: { - hovered: { container: { - background: background(theme.middle, color, "hovered"), + corner_radius: 4, + padding: { + top: 4, bottom: 4, left: 4, right: 4 + }, + margin: { + left: button_spacing / 2, + right: button_spacing / 2, + }, }, }, - clicked: { - container: { - background: background(theme.middle, color, "pressed"), + state: { + hovered: { + container: { + background: background(theme.middle, color, "hovered"), + + } + }, + clicked: { + container: { + background: background(theme.middle, color, "pressed"), + } }, }, - }, - }) + }) + ) } diff --git a/styles/src/component/text_button.ts b/styles/src/component/text_button.ts index 68ec01c92bf..58b2a1cbf2f 100644 --- a/styles/src/component/text_button.ts +++ b/styles/src/component/text_button.ts @@ -9,7 +9,10 @@ import { useTheme, Theme } from "../theme" import { Margin } from "./icon_button" interface TextButtonOptions { - layer?: Theme["lowest"] | Theme["middle"] | Theme["highest"] + layer?: + | Theme["lowest"] + | Theme["middle"] + | Theme["highest"] color?: keyof Theme["lowest"] margin?: Partial text_properties?: TextProperties diff --git a/styles/src/style_tree/app.ts b/styles/src/style_tree/app.ts index ee0aa133a04..ccfdd60a981 100644 --- a/styles/src/style_tree/app.ts +++ b/styles/src/style_tree/app.ts @@ -57,6 +57,6 @@ export default function app(): any { tooltip: tooltip(), terminal: terminal(), assistant: assistant(), - feedback: feedback(), + feedback: feedback() } } diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 7df5434f910..cfc1f8d8136 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -8,48 +8,50 @@ type RoleCycleButton = TextStyle & { } // TODO: Replace these with zed types type RemainingTokens = TextStyle & { - background: string - margin: { top: number; right: number } + background: string, + margin: { top: number, right: number }, padding: { - right: number - left: number - top: number - bottom: number - } - corner_radius: number + right: number, + left: number, + top: number, + bottom: number, + }, + corner_radius: number, } export default function assistant(): any { const theme = useTheme() - const interactive_role = ( - color: StyleSets - ): Interactive => { - return interactive({ - base: { - ...text(theme.highest, "sans", color, { size: "sm" }), - }, - state: { - hovered: { + const interactive_role = (color: StyleSets): Interactive => { + return ( + interactive({ + base: { ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "hovered"), }, - clicked: { - ...text(theme.highest, "sans", color, { size: "sm" }), - background: background(theme.highest, color, "pressed"), + state: { + hovered: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "hovered"), + }, + clicked: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "pressed"), + } }, - }, - }) + }) + ) } const tokens_remaining = (color: StyleSets): RemainingTokens => { - return { - ...text(theme.highest, "mono", color, { size: "xs" }), - background: background(theme.highest, "on", "default"), - margin: { top: 12, right: 20 }, - padding: { right: 4, left: 4, top: 1, bottom: 1 }, - corner_radius: 6, - } + return ( + { + ...text(theme.highest, "mono", color, { size: "xs" }), + background: background(theme.highest, "on", "default"), + margin: { top: 12, right: 20 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, + } + ) } return { @@ -91,10 +93,7 @@ export default function assistant(): any { base: { background: background(theme.middle), padding: { top: 4, bottom: 4 }, - border: border(theme.middle, "default", { - top: true, - overlay: true, - }), + border: border(theme.middle, "default", { top: true, overlay: true }), }, state: { hovered: { @@ -102,7 +101,7 @@ export default function assistant(): any { }, clicked: { background: background(theme.middle, "pressed"), - }, + } }, }), saved_at: { diff --git a/styles/src/style_tree/editor.ts b/styles/src/style_tree/editor.ts index deab45d4b21..9ad008f38d4 100644 --- a/styles/src/style_tree/editor.ts +++ b/styles/src/style_tree/editor.ts @@ -9,9 +9,9 @@ import { } from "./components" import hover_popover from "./hover_popover" +import { build_syntax } from "../theme/syntax" import { interactive, toggleable } from "../element" import { useTheme } from "../theme" -import chroma from "chroma-js" export default function editor(): any { const theme = useTheme() @@ -48,28 +48,16 @@ export default function editor(): any { } } + const syntax = build_syntax() + return { - text_color: theme.syntax.primary.color, + text_color: syntax.primary.color, background: background(layer), active_line_background: with_opacity(background(layer, "on"), 0.75), highlighted_line_background: background(layer, "on"), // Inline autocomplete suggestions, Co-pilot suggestions, etc. - hint: chroma - .mix( - theme.ramps.neutral(0.6).hex(), - theme.ramps.blue(0.4).hex(), - 0.45, - "lch" - ) - .hex(), - suggestion: chroma - .mix( - theme.ramps.neutral(0.4).hex(), - theme.ramps.blue(0.4).hex(), - 0.45, - "lch" - ) - .hex(), + hint: syntax.hint, + suggestion: syntax.predictive, code_actions: { indicator: toggleable({ base: interactive({ @@ -267,8 +255,8 @@ export default function editor(): any { invalid_warning_diagnostic: diagnostic(theme.middle, "base"), hover_popover: hover_popover(), link_definition: { - color: theme.syntax.link_uri.color, - underline: theme.syntax.link_uri.underline, + color: syntax.link_uri.color, + underline: syntax.link_uri.underline, }, jump_icon: interactive({ base: { @@ -318,7 +306,7 @@ export default function editor(): any { ? with_opacity(theme.ramps.green(0.5).hex(), 0.8) : with_opacity(theme.ramps.green(0.4).hex(), 0.8), }, - selections: foreground(layer, "accent"), + selections: foreground(layer, "accent") }, composition_mark: { underline: { @@ -326,6 +314,6 @@ export default function editor(): any { color: border_color(layer), }, }, - syntax: theme.syntax, + syntax, } } diff --git a/styles/src/style_tree/feedback.ts b/styles/src/style_tree/feedback.ts index 03493595330..b1bd96e1654 100644 --- a/styles/src/style_tree/feedback.ts +++ b/styles/src/style_tree/feedback.ts @@ -37,7 +37,7 @@ export default function feedback(): any { ...text(theme.highest, "mono", "on", "disabled"), background: background(theme.highest, "on", "disabled"), border: border(theme.highest, "on", "disabled"), - }, + } }, }), button_margin: 8, diff --git a/styles/src/style_tree/picker.ts b/styles/src/style_tree/picker.ts index 317f600b1e2..28ae8547879 100644 --- a/styles/src/style_tree/picker.ts +++ b/styles/src/style_tree/picker.ts @@ -152,7 +152,7 @@ export default function picker(): any { 0.5 ), }, - }, + } }), } } diff --git a/styles/src/style_tree/project_panel.ts b/styles/src/style_tree/project_panel.ts index 51958af145b..e239f9a8402 100644 --- a/styles/src/style_tree/project_panel.ts +++ b/styles/src/style_tree/project_panel.ts @@ -64,17 +64,17 @@ export default function project_panel(): any { const unselected_default_style = merge( base_properties, unselected?.default ?? {}, - {} + {}, ) const unselected_hovered_style = merge( base_properties, { background: background(theme.middle, "hovered") }, - unselected?.hovered ?? {} + unselected?.hovered ?? {}, ) const unselected_clicked_style = merge( base_properties, { background: background(theme.middle, "pressed") }, - unselected?.clicked ?? {} + unselected?.clicked ?? {}, ) const selected_default_style = merge( base_properties, @@ -82,7 +82,7 @@ export default function project_panel(): any { background: background(theme.lowest), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.default ?? {} + selected_style?.default ?? {}, ) const selected_hovered_style = merge( base_properties, @@ -90,7 +90,7 @@ export default function project_panel(): any { background: background(theme.lowest, "hovered"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.hovered ?? {} + selected_style?.hovered ?? {}, ) const selected_clicked_style = merge( base_properties, @@ -98,7 +98,7 @@ export default function project_panel(): any { background: background(theme.lowest, "pressed"), text: text(theme.lowest, "sans", { size: "sm" }), }, - selected_style?.clicked ?? {} + selected_style?.clicked ?? {}, ) return toggleable({ @@ -175,7 +175,7 @@ export default function project_panel(): any { default: { icon_color: foreground(theme.middle, "variant"), }, - } + }, ), cut_entry: entry( { @@ -190,7 +190,7 @@ export default function project_panel(): any { size: "sm", }), }, - } + }, ), filename_editor: { background: background(theme.middle, "on"), diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 45baf4e8712..6261939994a 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -34,14 +34,10 @@ export default function status_bar(): any { ...text(layer, "mono", "variant", { size: "xs" }), }, active_language: text_button({ - color: "variant", - }), - auto_update_progress_message: text(layer, "sans", "variant", { - size: "xs", - }), - auto_update_done_message: text(layer, "sans", "variant", { - size: "xs", + color: "variant" }), + auto_update_progress_message: text(layer, "sans", "variant", { size: "xs" }), + auto_update_done_message: text(layer, "sans", "variant", { size: "xs" }), lsp_status: interactive({ base: { ...diagnostic_status_container, diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index fe0c53e87da..177a8c5bd8d 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -183,10 +183,10 @@ export function titlebar(): any { project_name_divider: text(theme.lowest, "sans", "variant"), project_menu_button: toggleable_text_button(theme, { - color: "base", + color: 'base', }), git_menu_button: toggleable_text_button(theme, { - color: "variant", + color: 'variant', }), // Collaborators diff --git a/styles/src/theme/create_theme.ts b/styles/src/theme/create_theme.ts index 6df36d7077b..d2701f8341a 100644 --- a/styles/src/theme/create_theme.ts +++ b/styles/src/theme/create_theme.ts @@ -1,28 +1,28 @@ import { Scale, Color } from "chroma-js" +import { Syntax, ThemeSyntax, SyntaxHighlightStyle } from "./syntax" +export { Syntax, ThemeSyntax, SyntaxHighlightStyle } import { ThemeConfig, ThemeAppearance, ThemeConfigInputColors, } from "./theme_config" import { get_ramps } from "./ramps" -import { syntaxStyle } from "./syntax" -import { Syntax } from "../types/syntax" export interface Theme { name: string is_light: boolean /** - * App background, other elements that should sit directly on top of the background. - */ + * App background, other elements that should sit directly on top of the background. + */ lowest: Layer /** - * Panels, tabs, other UI surfaces that sit on top of the background. - */ + * Panels, tabs, other UI surfaces that sit on top of the background. + */ middle: Layer /** - * Editors like code buffers, conversation editors, etc. - */ + * Editors like code buffers, conversation editors, etc. + */ highest: Layer ramps: RampSet @@ -31,7 +31,7 @@ export interface Theme { modal_shadow: Shadow players: Players - syntax: Syntax + syntax?: Partial } export interface Meta { @@ -115,7 +115,12 @@ export interface Style { } export function create_theme(theme: ThemeConfig): Theme { - const { name, appearance, input_color } = theme + const { + name, + appearance, + input_color, + override: { syntax }, + } = theme const is_light = appearance === ThemeAppearance.Light const color_ramps: ThemeConfigInputColors = input_color @@ -157,11 +162,6 @@ export function create_theme(theme: ThemeConfig): Theme { "7": player(ramps.yellow), } - const syntax = syntaxStyle( - ramps, - theme.override.syntax ? theme.override.syntax : {} - ) - return { name, is_light, diff --git a/styles/src/theme/syntax.ts b/styles/src/theme/syntax.ts index db8f98de663..540a1d0ff98 100644 --- a/styles/src/theme/syntax.ts +++ b/styles/src/theme/syntax.ts @@ -1,45 +1,325 @@ import deepmerge from "deepmerge" -import { font_weights, ThemeConfigInputSyntax, RampSet } from "../common" -import { Syntax, SyntaxHighlightStyle, allSyntaxKeys } from "../types/syntax" +import { FontWeight, font_weights, useTheme } from "../common" +import chroma from "chroma-js" -// Apply defaults to any missing syntax properties that are not defined manually -function apply_defaults( - ramps: RampSet, - syntax_highlights: Partial -): Syntax { - const restKeys: (keyof Syntax)[] = allSyntaxKeys.filter( - (key) => !syntax_highlights[key] - ) +export interface SyntaxHighlightStyle { + color?: string + weight?: FontWeight + underline?: boolean + italic?: boolean +} - const completeSyntax: Syntax = {} as Syntax +export interface Syntax { + // == Text Styles ====== / + comment: SyntaxHighlightStyle + // elixir: doc comment + "comment.doc": SyntaxHighlightStyle + primary: SyntaxHighlightStyle + predictive: SyntaxHighlightStyle + hint: SyntaxHighlightStyle - const defaults: SyntaxHighlightStyle = { - color: ramps.neutral(1).hex(), - } + // === Formatted Text ====== / + emphasis: SyntaxHighlightStyle + "emphasis.strong": SyntaxHighlightStyle + title: SyntaxHighlightStyle + link_uri: SyntaxHighlightStyle + link_text: SyntaxHighlightStyle + /** md: indented_code_block, fenced_code_block, code_span */ + "text.literal": SyntaxHighlightStyle - for (const key of restKeys) { - { - completeSyntax[key] = { - ...defaults, - } + // == Punctuation ====== / + punctuation: SyntaxHighlightStyle + /** Example: `(`, `[`, `{`...*/ + "punctuation.bracket": SyntaxHighlightStyle + /**., ;*/ + "punctuation.delimiter": SyntaxHighlightStyle + // js, ts: ${, } in a template literal + // yaml: *, &, ---, ... + "punctuation.special": SyntaxHighlightStyle + // md: list_marker_plus, list_marker_dot, etc + "punctuation.list_marker": SyntaxHighlightStyle + + // == Strings ====== / + + string: SyntaxHighlightStyle + // css: color_value + // js: this, super + // toml: offset_date_time, local_date_time... + "string.special": SyntaxHighlightStyle + // elixir: atom, quoted_atom, keyword, quoted_keyword + // ruby: simple_symbol, delimited_symbol... + "string.special.symbol"?: SyntaxHighlightStyle + // elixir, python, yaml...: escape_sequence + "string.escape"?: SyntaxHighlightStyle + // Regular expressions + "string.regex"?: SyntaxHighlightStyle + + // == Types ====== / + // We allow Function here because all JS objects literals have this property + constructor: SyntaxHighlightStyle | Function // eslint-disable-line @typescript-eslint/ban-types + variant: SyntaxHighlightStyle + type: SyntaxHighlightStyle + // js: predefined_type + "type.builtin"?: SyntaxHighlightStyle + + // == Values + variable: SyntaxHighlightStyle + // this, ... + // css: -- (var(--foo)) + // lua: self + "variable.special"?: SyntaxHighlightStyle + // c: statement_identifier, + label: SyntaxHighlightStyle + // css: tag_name, nesting_selector, universal_selector... + tag: SyntaxHighlightStyle + // css: attribute, pseudo_element_selector (tag_name), + attribute: SyntaxHighlightStyle + // css: class_name, property_name, namespace_name... + property: SyntaxHighlightStyle + // true, false, null, nullptr + constant: SyntaxHighlightStyle + // css: @media, @import, @supports... + // js: declare, implements, interface, keyof, public... + keyword: SyntaxHighlightStyle + // note: js enum is currently defined as a keyword + enum: SyntaxHighlightStyle + // -, --, ->, !=, &&, ||, <=... + operator: SyntaxHighlightStyle + number: SyntaxHighlightStyle + boolean: SyntaxHighlightStyle + // elixir: __MODULE__, __DIR__, __ENV__, etc + // go: nil, iota + "constant.builtin"?: SyntaxHighlightStyle + + // == Functions ====== / + + function: SyntaxHighlightStyle + // lua: assert, error, loadfile, tostring, unpack... + "function.builtin"?: SyntaxHighlightStyle + // go: call_expression, method_declaration + // js: call_expression, method_definition, pair (key, arrow function) + // rust: function_item name: (identifier) + "function.definition"?: SyntaxHighlightStyle + // rust: macro_definition name: (identifier) + "function.special.definition"?: SyntaxHighlightStyle + "function.method"?: SyntaxHighlightStyle + // ruby: identifier/"defined?" // Nate note: I don't fully understand this one. + "function.method.builtin"?: SyntaxHighlightStyle + + // == Unsorted ====== / + // lua: hash_bang_line + preproc: SyntaxHighlightStyle + // elixir, python: interpolation (ex: foo in ${foo}) + // js: template_substitution + embedded: SyntaxHighlightStyle +} + +export type ThemeSyntax = Partial + +const default_syntax_highlight_style: Omit = { + weight: "normal", + underline: false, + italic: false, +} + +function build_default_syntax(): Syntax { + const theme = useTheme() + + // Make a temporary object that is allowed to be missing + // the "color" property for each style + const syntax: { + [key: string]: Omit + } = {} + + // then spread the default to each style + for (const key of Object.keys({} as Syntax)) { + syntax[key as keyof Syntax] = { + ...default_syntax_highlight_style, } } - const mergedBaseSyntax = Object.assign(completeSyntax, syntax_highlights) + // Mix the neutral and blue colors to get a + // predictive color distinct from any other color in the theme + const predictive = chroma + .mix( + theme.ramps.neutral(0.4).hex(), + theme.ramps.blue(0.4).hex(), + 0.45, + "lch" + ) + .hex() + // Mix the neutral and green colors to get a + // hint color distinct from any other color in the theme + const hint = chroma + .mix( + theme.ramps.neutral(0.6).hex(), + theme.ramps.blue(0.4).hex(), + 0.45, + "lch" + ) + .hex() - return mergedBaseSyntax + const color = { + primary: theme.ramps.neutral(1).hex(), + comment: theme.ramps.neutral(0.71).hex(), + punctuation: theme.ramps.neutral(0.86).hex(), + predictive: predictive, + hint: hint, + emphasis: theme.ramps.blue(0.5).hex(), + string: theme.ramps.orange(0.5).hex(), + function: theme.ramps.yellow(0.5).hex(), + type: theme.ramps.cyan(0.5).hex(), + constructor: theme.ramps.blue(0.5).hex(), + variant: theme.ramps.blue(0.5).hex(), + property: theme.ramps.blue(0.5).hex(), + enum: theme.ramps.orange(0.5).hex(), + operator: theme.ramps.orange(0.5).hex(), + number: theme.ramps.green(0.5).hex(), + boolean: theme.ramps.green(0.5).hex(), + constant: theme.ramps.green(0.5).hex(), + keyword: theme.ramps.blue(0.5).hex(), + } + + // Then assign colors and use Syntax to enforce each style getting it's own color + const default_syntax: Syntax = { + ...syntax, + comment: { + color: color.comment, + }, + "comment.doc": { + color: color.comment, + }, + primary: { + color: color.primary, + }, + predictive: { + color: color.predictive, + italic: true, + }, + hint: { + color: color.hint, + weight: font_weights.bold, + }, + emphasis: { + color: color.emphasis, + }, + "emphasis.strong": { + color: color.emphasis, + weight: font_weights.bold, + }, + title: { + color: color.primary, + weight: font_weights.bold, + }, + link_uri: { + color: theme.ramps.green(0.5).hex(), + underline: true, + }, + link_text: { + color: theme.ramps.orange(0.5).hex(), + italic: true, + }, + "text.literal": { + color: color.string, + }, + punctuation: { + color: color.punctuation, + }, + "punctuation.bracket": { + color: color.punctuation, + }, + "punctuation.delimiter": { + color: color.punctuation, + }, + "punctuation.special": { + color: theme.ramps.neutral(0.86).hex(), + }, + "punctuation.list_marker": { + color: color.punctuation, + }, + string: { + color: color.string, + }, + "string.special": { + color: color.string, + }, + "string.special.symbol": { + color: color.string, + }, + "string.escape": { + color: color.comment, + }, + "string.regex": { + color: color.string, + }, + constructor: { + color: theme.ramps.blue(0.5).hex(), + }, + variant: { + color: theme.ramps.blue(0.5).hex(), + }, + type: { + color: color.type, + }, + variable: { + color: color.primary, + }, + label: { + color: theme.ramps.blue(0.5).hex(), + }, + tag: { + color: theme.ramps.blue(0.5).hex(), + }, + attribute: { + color: theme.ramps.blue(0.5).hex(), + }, + property: { + color: theme.ramps.blue(0.5).hex(), + }, + constant: { + color: color.constant, + }, + keyword: { + color: color.keyword, + }, + enum: { + color: color.enum, + }, + operator: { + color: color.operator, + }, + number: { + color: color.number, + }, + boolean: { + color: color.boolean, + }, + function: { + color: color.function, + }, + preproc: { + color: color.primary, + }, + embedded: { + color: color.primary, + }, + } + + return default_syntax } -// Merge the base syntax with the theme syntax overrides -// This is a deep merge, so any nested properties will be merged as well -// This allows for a theme to only override a single property of a syntax highlight style -const merge_syntax = ( - baseSyntax: Syntax, - theme_syntax_overrides: ThemeConfigInputSyntax -): Syntax => { - return deepmerge( - baseSyntax, - theme_syntax_overrides, +export function build_syntax(): Syntax { + const theme = useTheme() + + const default_syntax: Syntax = build_default_syntax() + + if (!theme.syntax) { + return default_syntax + } + + const syntax = deepmerge>( + default_syntax, + theme.syntax, { arrayMerge: (destinationArray, sourceArray) => [ ...destinationArray, @@ -47,49 +327,6 @@ const merge_syntax = ( ], } ) -} -/** Returns a complete Syntax object of the combined styles of a theme's syntax overrides and the default syntax styles */ -export const syntaxStyle = ( - ramps: RampSet, - theme_syntax_overrides: ThemeConfigInputSyntax -): Syntax => { - const syntax_highlights: Partial = { - comment: { color: ramps.neutral(0.71).hex() }, - "comment.doc": { color: ramps.neutral(0.71).hex() }, - primary: { color: ramps.neutral(1).hex() }, - emphasis: { color: ramps.blue(0.5).hex() }, - "emphasis.strong": { - color: ramps.blue(0.5).hex(), - weight: font_weights.bold, - }, - link_uri: { color: ramps.green(0.5).hex(), underline: true }, - link_text: { color: ramps.orange(0.5).hex(), italic: true }, - "text.literal": { color: ramps.orange(0.5).hex() }, - punctuation: { color: ramps.neutral(0.86).hex() }, - "punctuation.bracket": { color: ramps.neutral(0.86).hex() }, - "punctuation.special": { color: ramps.neutral(0.86).hex() }, - "punctuation.delimiter": { color: ramps.neutral(0.86).hex() }, - "punctuation.list_marker": { color: ramps.neutral(0.86).hex() }, - string: { color: ramps.orange(0.5).hex() }, - "string.special": { color: ramps.orange(0.5).hex() }, - "string.special.symbol": { color: ramps.orange(0.5).hex() }, - "string.escape": { color: ramps.neutral(0.71).hex() }, - "string.regex": { color: ramps.orange(0.5).hex() }, - "method.constructor": { color: ramps.blue(0.5).hex() }, - type: { color: ramps.cyan(0.5).hex() }, - label: { color: ramps.blue(0.5).hex() }, - attribute: { color: ramps.blue(0.5).hex() }, - property: { color: ramps.blue(0.5).hex() }, - constant: { color: ramps.green(0.5).hex() }, - keyword: { color: ramps.blue(0.5).hex() }, - operator: { color: ramps.orange(0.5).hex() }, - number: { color: ramps.green(0.5).hex() }, - boolean: { color: ramps.green(0.5).hex() }, - function: { color: ramps.yellow(0.5).hex() }, - } - - const baseSyntax = apply_defaults(ramps, syntax_highlights) - const mergedSyntax = merge_syntax(baseSyntax, theme_syntax_overrides) - return mergedSyntax + return syntax } diff --git a/styles/src/theme/theme_config.ts b/styles/src/theme/theme_config.ts index f5f83590743..bc8f07425f9 100644 --- a/styles/src/theme/theme_config.ts +++ b/styles/src/theme/theme_config.ts @@ -1,5 +1,5 @@ import { Scale, Color } from "chroma-js" -import { SyntaxHighlightStyle, SyntaxProperty } from "../types/syntax" +import { Syntax } from "./syntax" interface ThemeMeta { /** The name of the theme */ @@ -55,9 +55,7 @@ export type ThemeConfigInputColorsKeys = keyof ThemeConfigInputColors * } * ``` */ -export type ThemeConfigInputSyntax = Partial< - Record> -> +export type ThemeConfigInputSyntax = Partial interface ThemeConfigOverrides { syntax: ThemeConfigInputSyntax diff --git a/styles/src/theme/tokens/theme.ts b/styles/src/theme/tokens/theme.ts index d9307936696..f759bc81391 100644 --- a/styles/src/theme/tokens/theme.ts +++ b/styles/src/theme/tokens/theme.ts @@ -4,13 +4,17 @@ import { SingleOtherToken, TokenTypes, } from "@tokens-studio/types" -import { Shadow } from "../create_theme" +import { + Shadow, + SyntaxHighlightStyle, + ThemeSyntax, +} from "../create_theme" import { LayerToken, layer_token } from "./layer" import { PlayersToken, players_token } from "./players" import { color_token } from "./token" +import { Syntax } from "../syntax" import editor from "../../style_tree/editor" import { useTheme } from "../../../src/common" -import { Syntax, SyntaxHighlightStyle } from "../../types/syntax" interface ThemeTokens { name: SingleOtherToken @@ -47,7 +51,7 @@ const modal_shadow_token = (): SingleBoxShadowToken => { return create_shadow_token(shadow, "modal_shadow") } -type ThemeSyntaxColorTokens = Record +type ThemeSyntaxColorTokens = Record function syntax_highlight_style_color_tokens( syntax: Syntax diff --git a/styles/src/themes/atelier/common.ts b/styles/src/themes/atelier/common.ts index 9a0029581c5..b76ccc5b607 100644 --- a/styles/src/themes/atelier/common.ts +++ b/styles/src/themes/atelier/common.ts @@ -1,8 +1,4 @@ -import { - ThemeLicenseType, - ThemeFamilyMeta, - ThemeConfigInputSyntax, -} from "../../common" +import { ThemeLicenseType, ThemeSyntax, ThemeFamilyMeta } from "../../common" export interface Variant { colors: { @@ -33,7 +29,7 @@ export const meta: ThemeFamilyMeta = { "https://atelierbram.github.io/syntax-highlighting/atelier-schemes/cave/", } -export const build_syntax = (variant: Variant): ThemeConfigInputSyntax => { +export const build_syntax = (variant: Variant): ThemeSyntax => { const { colors } = variant return { primary: { color: colors.base06 }, @@ -54,6 +50,7 @@ export const build_syntax = (variant: Variant): ThemeConfigInputSyntax => { property: { color: colors.base08 }, variable: { color: colors.base06 }, "variable.special": { color: colors.base0E }, + variant: { color: colors.base0A }, keyword: { color: colors.base0E }, } } diff --git a/styles/src/themes/ayu/common.ts b/styles/src/themes/ayu/common.ts index 87044558864..2bd0bbf259a 100644 --- a/styles/src/themes/ayu/common.ts +++ b/styles/src/themes/ayu/common.ts @@ -3,8 +3,8 @@ import { chroma, color_ramp, ThemeLicenseType, + ThemeSyntax, ThemeFamilyMeta, - ThemeConfigInputSyntax, } from "../../common" export const ayu = { @@ -27,7 +27,7 @@ export const build_theme = (t: typeof dark, light: boolean) => { purple: t.syntax.constant.hex(), } - const syntax: ThemeConfigInputSyntax = { + const syntax: ThemeSyntax = { constant: { color: t.syntax.constant.hex() }, "string.regex": { color: t.syntax.regexp.hex() }, string: { color: t.syntax.string.hex() }, @@ -61,7 +61,7 @@ export const build_theme = (t: typeof dark, light: boolean) => { } } -export const build_syntax = (t: typeof dark): ThemeConfigInputSyntax => { +export const build_syntax = (t: typeof dark): ThemeSyntax => { return { constant: { color: t.syntax.constant.hex() }, "string.regex": { color: t.syntax.regexp.hex() }, diff --git a/styles/src/themes/gruvbox/gruvbox-common.ts b/styles/src/themes/gruvbox/gruvbox-common.ts index 95e45efa951..2fa6b58faad 100644 --- a/styles/src/themes/gruvbox/gruvbox-common.ts +++ b/styles/src/themes/gruvbox/gruvbox-common.ts @@ -4,8 +4,8 @@ import { ThemeAppearance, ThemeLicenseType, ThemeConfig, + ThemeSyntax, ThemeFamilyMeta, - ThemeConfigInputSyntax, } from "../../common" const meta: ThemeFamilyMeta = { @@ -214,7 +214,7 @@ const build_variant = (variant: Variant): ThemeConfig => { magenta: color_ramp(chroma(variant.colors.gray)), } - const syntax: ThemeConfigInputSyntax = { + const syntax: ThemeSyntax = { primary: { color: neutral[is_light ? 0 : 8] }, "text.literal": { color: colors.blue }, comment: { color: colors.gray }, @@ -229,7 +229,7 @@ const build_variant = (variant: Variant): ThemeConfig => { "string.special.symbol": { color: colors.aqua }, "string.regex": { color: colors.orange }, type: { color: colors.yellow }, - // enum: { color: colors.orange }, + enum: { color: colors.orange }, tag: { color: colors.aqua }, constant: { color: colors.yellow }, keyword: { color: colors.red }, diff --git a/styles/src/themes/one/one-dark.ts b/styles/src/themes/one/one-dark.ts index 97f3922f36f..f672b892ee0 100644 --- a/styles/src/themes/one/one-dark.ts +++ b/styles/src/themes/one/one-dark.ts @@ -54,6 +54,7 @@ export const theme: ThemeConfig = { syntax: { boolean: { color: color.orange }, comment: { color: color.grey }, + enum: { color: color.red }, "emphasis.strong": { color: color.orange }, function: { color: color.blue }, keyword: { color: color.purple }, @@ -72,7 +73,8 @@ export const theme: ThemeConfig = { "text.literal": { color: color.green }, type: { color: color.teal }, "variable.special": { color: color.orange }, - "method.constructor": { color: color.blue }, + variant: { color: color.blue }, + constructor: { color: color.blue }, }, }, } diff --git a/styles/src/themes/one/one-light.ts b/styles/src/themes/one/one-light.ts index 65542875789..c3de7826c96 100644 --- a/styles/src/themes/one/one-light.ts +++ b/styles/src/themes/one/one-light.ts @@ -55,6 +55,7 @@ export const theme: ThemeConfig = { syntax: { boolean: { color: color.orange }, comment: { color: color.grey }, + enum: { color: color.red }, "emphasis.strong": { color: color.orange }, function: { color: color.blue }, keyword: { color: color.purple }, @@ -72,6 +73,7 @@ export const theme: ThemeConfig = { "text.literal": { color: color.green }, type: { color: color.teal }, "variable.special": { color: color.orange }, + variant: { color: color.blue }, }, }, } diff --git a/styles/src/themes/rose-pine/common.ts b/styles/src/themes/rose-pine/common.ts index decccc0a6dc..5c5482a754a 100644 --- a/styles/src/themes/rose-pine/common.ts +++ b/styles/src/themes/rose-pine/common.ts @@ -1,4 +1,4 @@ -import { ThemeConfigInputSyntax } from "../../common" +import { ThemeSyntax } from "../../common" export const color = { default: { @@ -54,7 +54,7 @@ export const color = { }, } -export const syntax = (c: typeof color.default): ThemeConfigInputSyntax => { +export const syntax = (c: typeof color.default): Partial => { return { comment: { color: c.muted }, operator: { color: c.pine }, diff --git a/styles/src/types/extract_syntax_types.ts b/styles/src/types/extract_syntax_types.ts deleted file mode 100644 index eb21d2418b9..00000000000 --- a/styles/src/types/extract_syntax_types.ts +++ /dev/null @@ -1,111 +0,0 @@ -import fs from "fs" -import path from "path" -import readline from "readline" - -function escapeTypeName(name: string): string { - return `'${name.replace("@", "").toLowerCase()}'` -} - -const generatedNote = `// This file is generated by extract_syntax_types.ts -// Do not edit this file directly -// It is generated from the highlight.scm files in the zed crate - -// To regenerate this file manually: -// 'npm run extract-syntax-types' from ./styles` - -const defaultTextProperty = ` /** Default text color */ - | 'primary'` - -const main = async () => { - const pathFromRoot = "crates/zed/src/languages" - const directoryPath = path.join(__dirname, "../../../", pathFromRoot) - const stylesMap: Record> = {} - const propertyLanguageMap: Record> = {} - - const processFile = async (filePath: string, language: string) => { - const fileStream = fs.createReadStream(filePath) - const rl = readline.createInterface({ - input: fileStream, - crlfDelay: Infinity, - }) - - for await (const line of rl) { - const cleanedLine = line.replace(/"@[a-zA-Z0-9_.]*"/g, "") - const match = cleanedLine.match(/@(\w+\.*)*/g) - if (match) { - match.forEach((property) => { - const formattedProperty = escapeTypeName(property) - // Only add non-empty properties - if (formattedProperty !== "''") { - if (!propertyLanguageMap[formattedProperty]) { - propertyLanguageMap[formattedProperty] = new Set() - } - propertyLanguageMap[formattedProperty].add(language) - } - }) - } - } - } - - const directories = fs - .readdirSync(directoryPath, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name) - - for (const dir of directories) { - const highlightsFilePath = path.join( - directoryPath, - dir, - "highlights.scm" - ) - if (fs.existsSync(highlightsFilePath)) { - await processFile(highlightsFilePath, dir) - } - } - - for (const [language, properties] of Object.entries(stylesMap)) { - console.log(`${language}: ${Array.from(properties).join(", ")}`) - } - - const sortedProperties = Object.entries(propertyLanguageMap).sort( - ([propA], [propB]) => propA.localeCompare(propB) - ) - - const outStream = fs.createWriteStream(path.join(__dirname, "syntax.ts")) - let allProperties = "" - const syntaxKeys = [] - for (const [property, languages] of sortedProperties) { - let languagesArray = Array.from(languages) - const moreThanSeven = languagesArray.length > 7 - // Limit to the first 7 languages, append "..." if more than 7 - languagesArray = languagesArray.slice(0, 7) - if (moreThanSeven) { - languagesArray.push("...") - } - const languagesString = languagesArray.join(", ") - const comment = `/** ${languagesString} */` - allProperties += ` ${comment}\n | ${property} \n` - syntaxKeys.push(property) - } - outStream.write(`${generatedNote} - -export type SyntaxHighlightStyle = { - color: string, - fade_out?: number, - italic?: boolean, - underline?: boolean, - weight?: string, -} - -export type Syntax = Record -export type SyntaxOverride = Partial - -export type SyntaxProperty = \n${defaultTextProperty}\n\n${allProperties} - -export const allSyntaxKeys: SyntaxProperty[] = [\n ${syntaxKeys.join( - ",\n " - )}\n]`) - outStream.end() -} - -main().catch(console.error) diff --git a/styles/src/types/syntax.ts b/styles/src/types/syntax.ts deleted file mode 100644 index 9b23dbde3cc..00000000000 --- a/styles/src/types/syntax.ts +++ /dev/null @@ -1,202 +0,0 @@ -// This file is generated by extract_syntax_types.ts -// Do not edit this file directly -// It is generated from the highlight.scm files in the zed crate - -// To regenerate this file manually: -// 'npm run extract-syntax-types' from ./styles - -export type SyntaxHighlightStyle = { - color: string - fade_out?: number - italic?: boolean - underline?: boolean - weight?: string -} - -export type Syntax = Record -export type SyntaxOverride = Partial - -export type SyntaxProperty = - /** Default text color */ - | "primary" - - /** elixir */ - | "__attribute__" - /** elixir */ - | "__name__" - /** elixir */ - | "_sigil_name" - /** css, heex, lua */ - | "attribute" - /** javascript, lua, tsx, typescript, yaml */ - | "boolean" - /** elixir */ - | "comment.doc" - /** elixir */ - | "comment.unused" - /** bash, c, cpp, css, elixir, elm, erb, ... */ - | "comment" - /** elixir, go, javascript, lua, php, python, racket, ... */ - | "constant.builtin" - /** bash, c, cpp, elixir, elm, glsl, heex, ... */ - | "constant" - /** glsl */ - | "delimiter" - /** bash, elixir, javascript, python, ruby, tsx, typescript */ - | "embedded" - /** markdown */ - | "emphasis.strong" - /** markdown */ - | "emphasis" - /** go, python, racket, ruby, scheme */ - | "escape" - /** lua */ - | "field" - /** lua, php, python */ - | "function.builtin" - /** elm, lua, rust */ - | "function.definition" - /** ruby */ - | "function.method.builtin" - /** go, javascript, php, python, ruby, rust, tsx, ... */ - | "function.method" - /** rust */ - | "function.special.definition" - /** c, cpp, glsl, rust */ - | "function.special" - /** bash, c, cpp, css, elixir, elm, glsl, ... */ - | "function" - /** elm */ - | "identifier" - /** glsl */ - | "keyword.function" - /** bash, c, cpp, css, elixir, elm, erb, ... */ - | "keyword" - /** c, cpp, glsl */ - | "label" - /** markdown */ - | "link_text" - /** markdown */ - | "link_uri" - /** lua, php, tsx, typescript */ - | "method.constructor" - /** lua */ - | "method" - /** heex */ - | "module" - /** svelte */ - | "none" - /** bash, c, cpp, css, elixir, glsl, go, ... */ - | "number" - /** bash, c, cpp, css, elixir, elm, glsl, ... */ - | "operator" - /** lua */ - | "parameter" - /** lua */ - | "preproc" - /** bash, c, cpp, css, glsl, go, html, ... */ - | "property" - /** c, cpp, elixir, elm, heex, html, javascript, ... */ - | "punctuation.bracket" - /** c, cpp, css, elixir, elm, heex, javascript, ... */ - | "punctuation.delimiter" - /** markdown */ - | "punctuation.list_marker" - /** elixir, javascript, python, ruby, tsx, typescript, yaml */ - | "punctuation.special" - /** elixir */ - | "punctuation" - /** glsl */ - | "storageclass" - /** elixir, elm, yaml */ - | "string.escape" - /** elixir, javascript, racket, ruby, tsx, typescript */ - | "string.regex" - /** elixir, ruby */ - | "string.special.symbol" - /** css, elixir, toml */ - | "string.special" - /** bash, c, cpp, css, elixir, elm, glsl, ... */ - | "string" - /** svelte */ - | "tag.delimiter" - /** css, heex, php, svelte */ - | "tag" - /** markdown */ - | "text.literal" - /** markdown */ - | "title" - /** javascript, php, rust, tsx, typescript */ - | "type.builtin" - /** glsl */ - | "type.qualifier" - /** c, cpp, css, elixir, elm, glsl, go, ... */ - | "type" - /** glsl, php */ - | "variable.builtin" - /** cpp, css, javascript, lua, racket, ruby, rust, ... */ - | "variable.special" - /** c, cpp, elm, glsl, go, javascript, lua, ... */ - | "variable" - -export const allSyntaxKeys: SyntaxProperty[] = [ - "__attribute__", - "__name__", - "_sigil_name", - "attribute", - "boolean", - "comment.doc", - "comment.unused", - "comment", - "constant.builtin", - "constant", - "delimiter", - "embedded", - "emphasis.strong", - "emphasis", - "escape", - "field", - "function.builtin", - "function.definition", - "function.method.builtin", - "function.method", - "function.special.definition", - "function.special", - "function", - "identifier", - "keyword.function", - "keyword", - "label", - "link_text", - "link_uri", - "method.constructor", - "method", - "module", - "none", - "number", - "operator", - "parameter", - "preproc", - "property", - "punctuation.bracket", - "punctuation.delimiter", - "punctuation.list_marker", - "punctuation.special", - "punctuation", - "storageclass", - "string.escape", - "string.regex", - "string.special.symbol", - "string.special", - "string", - "tag.delimiter", - "tag", - "text.literal", - "title", - "type.builtin", - "type.qualifier", - "type", - "variable.builtin", - "variable.special", - "variable", -] diff --git a/styles/tsconfig.json b/styles/tsconfig.json index a1913027b70..281bd74b215 100644 --- a/styles/tsconfig.json +++ b/styles/tsconfig.json @@ -24,5 +24,7 @@ "useUnknownInCatchVariables": false, "baseUrl": "." }, - "exclude": ["node_modules"] + "exclude": [ + "node_modules" + ] } From 884cee6dfda9f4b887976cb14f87e82ac6b87fc0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2023 14:05:03 -0600 Subject: [PATCH 032/105] Get tests compiling returning WindowHandle from add_window --- crates/collab/src/tests.rs | 5 +- crates/collab/src/tests/integration_tests.rs | 52 +- .../src/incoming_call_notification.rs | 4 +- .../src/project_shared_notification.rs | 4 +- crates/command_palette/src/command_palette.rs | 4 +- crates/copilot/src/sign_in.rs | 8 +- crates/diagnostics/src/diagnostics.rs | 8 +- crates/editor/src/editor_tests.rs | 627 ++++++++++-------- crates/editor/src/element.rs | 30 +- crates/editor/src/inlay_hint_cache.rs | 30 +- .../src/test/editor_lsp_test_context.rs | 5 +- crates/editor/src/test/editor_test_context.rs | 12 +- crates/file_finder/src/file_finder.rs | 230 +++---- crates/gpui/src/app.rs | 26 +- crates/gpui/src/app/window.rs | 8 +- crates/language_tools/src/lsp_log_tests.rs | 4 +- crates/project_panel/src/project_panel.rs | 32 +- crates/project_symbols/src/project_symbols.rs | 4 +- crates/search/src/buffer_search.rs | 19 +- crates/search/src/project_search.rs | 16 +- crates/terminal_view/src/terminal_view.rs | 4 +- crates/workspace/src/pane.rs | 30 +- crates/workspace/src/workspace.rs | 108 +-- crates/zed/src/zed.rs | 34 +- 24 files changed, 726 insertions(+), 578 deletions(-) diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index b1d0bedb2cf..4804f5b0f1f 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -495,8 +495,9 @@ impl TestClient { // We use a workspace container so that we don't need to remove the window in order to // drop the workspace and we can use a ViewHandle instead. - let (window_id, container) = cx.add_window(|_| WorkspaceContainer { workspace: None }); - let workspace = cx.add_view(window_id, |cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|_| WorkspaceContainer { workspace: None }); + let container = window.root(cx); + let workspace = window.add_view(cx, |cx| Workspace::test_new(project.clone(), cx)); container.update(cx, |container, cx| { container.workspace = Some(workspace.downgrade()); cx.notify(); diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ab94f16a07f..1a8e6d938d6 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7,8 +7,7 @@ use client::{User, RECEIVE_TIMEOUT}; use collections::HashSet; use editor::{ test::editor_test_context::EditorTestContext, ConfirmCodeAction, ConfirmCompletion, - ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToOffset, ToggleCodeActions, - Undo, + ConfirmRename, Editor, ExcerptRange, MultiBuffer, Redo, Rename, ToggleCodeActions, Undo, }; use fs::{repository::GitFileStatus, FakeFs, Fs as _, LineEnding, RemoveOptions}; use futures::StreamExt as _; @@ -1208,7 +1207,7 @@ async fn test_share_project( cx_c: &mut TestAppContext, ) { deterministic.forbid_parking(); - let (window_b, _) = cx_b.add_window(|_| EmptyView); + let window_b = cx_b.add_window(|_| EmptyView); 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; @@ -1316,7 +1315,7 @@ async fn test_share_project( .await .unwrap(); - let editor_b = cx_b.add_view(window_b, |cx| Editor::for_buffer(buffer_b, None, cx)); + let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, None, cx)); // Client A sees client B's selection deterministic.run_until_parked(); @@ -1499,8 +1498,8 @@ async fn test_host_disconnect( deterministic.run_until_parked(); assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared())); - let (window_id_b, workspace_b) = - cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let workspace_b = window_b.root(cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "b.txt"), None, true, cx) @@ -1509,9 +1508,7 @@ async fn test_host_disconnect( .unwrap() .downcast::() .unwrap(); - assert!(cx_b - .read_window(window_id_b, |cx| editor_b.is_focused(cx)) - .unwrap()); + assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); assert!(cx_b.is_window_edited(workspace_b.window_id())); @@ -1525,7 +1522,7 @@ async fn test_host_disconnect( assert!(worktree_a.read_with(cx_a, |tree, _| !tree.as_local().unwrap().is_shared())); // Ensure client B's edited state is reset and that the whole window is blurred. - cx_b.read_window(window_id_b, |cx| { + window_b.read_with(cx_b, |cx| { assert_eq!(cx.focused_view_id(), None); }); assert!(!cx_b.is_window_edited(workspace_b.window_id())); @@ -3445,13 +3442,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let (window_a, _) = cx_a.add_window(|_| EmptyView); - let editor_a = cx_a.add_view(window_a, |cx| { - Editor::for_buffer(buffer_a, Some(project_a), cx) - }); + let window_a = cx_a.add_window(|_| EmptyView); + let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a, - window_id: window_a, + window_id: window_a.id(), editor: editor_a, }; @@ -3460,13 +3455,11 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.txt"), cx)) .await .unwrap(); - let (window_b, _) = cx_b.add_window(|_| EmptyView); - let editor_b = cx_b.add_view(window_b, |cx| { - Editor::for_buffer(buffer_b, Some(project_b), cx) - }); + let window_b = cx_b.add_window(|_| EmptyView); + let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b, - window_id: window_b, + window_id: window_b.id(), editor: editor_b, }; @@ -4205,8 +4198,8 @@ async fn test_collaborating_with_completion( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let (window_b, _) = cx_b.add_window(|_| EmptyView); - let editor_b = cx_b.add_view(window_b, |cx| { + let window_b = cx_b.add_window(|_| EmptyView); + let editor_b = window_b.add_view(cx_b, |cx| { Editor::for_buffer(buffer_b.clone(), Some(project_b.clone()), cx) }); @@ -5316,7 +5309,8 @@ async fn test_collaborating_with_code_actions( // Join the project as client B. let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let workspace_b = window_b.root(cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -5540,7 +5534,8 @@ async fn test_collaborating_with_renames( .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let window_b = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx)); + let workspace_b = window_b.root(cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "one.rs"), None, true, cx) @@ -5571,6 +5566,7 @@ async fn test_collaborating_with_renames( .unwrap(); prepare_rename.await.unwrap(); editor_b.update(cx_b, |editor, cx| { + use editor::ToOffset; let rename = editor.pending_rename().unwrap(); let buffer = editor.buffer().read(cx).snapshot(cx); assert_eq!( @@ -7601,8 +7597,8 @@ async fn test_on_input_format_from_host_to_guest( .update(cx_a, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let (window_a, _) = cx_a.add_window(|_| EmptyView); - let editor_a = cx_a.add_view(window_a, |cx| { + let window_a = cx_a.add_window(|_| EmptyView); + let editor_a = window_a.add_view(cx_a, |cx| { Editor::for_buffer(buffer_a, Some(project_a.clone()), cx) }); @@ -7730,8 +7726,8 @@ async fn test_on_input_format_from_guest_to_host( .update(cx_b, |p, cx| p.open_buffer((worktree_id, "main.rs"), cx)) .await .unwrap(); - let (window_b, _) = cx_b.add_window(|_| EmptyView); - let editor_b = cx_b.add_view(window_b, |cx| { + let window_b = cx_b.add_window(|_| EmptyView); + let editor_b = window_b.add_view(cx_b, |cx| { Editor::for_buffer(buffer_b, Some(project_b.clone()), cx) }); diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 4066b5b229a..a9c5e697a5f 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -31,7 +31,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for screen in cx.platform().screens() { let screen_bounds = screen.bounds(); - let (window_id, _) = cx.add_window( + let window = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( screen_bounds.upper_right() @@ -49,7 +49,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), ); - notification_windows.push(window_id); + notification_windows.push(window.id()); } } } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index fea6118bdf4..03ab91623b4 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -26,7 +26,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { for screen in cx.platform().screens() { let screen_bounds = screen.bounds(); - let (window_id, _) = cx.add_window( + let window = cx.add_window( WindowOptions { bounds: WindowBounds::Fixed(RectF::new( screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING), @@ -52,7 +52,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_windows .entry(*project_id) .or_insert(Vec::new()) - .push(window_id); + .push(window.id()); } } room::Event::RemoteProjectUnshared { project_id } => { diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 7461fb28c73..7d4b4126b70 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -295,7 +295,9 @@ mod tests { let app_state = init_test(cx); let project = Project::test(app_state.fs.clone(), [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let editor = cx.add_view(window_id, |cx| { let mut editor = Editor::single_line(None, cx); editor.set_text("abc", cx); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index fec8f27c973..659bee7445f 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -27,7 +27,7 @@ pub fn init(cx: &mut AppContext) { if let Some(code_verification_handle) = code_verification.as_mut() { let window_id = code_verification_handle.id(); let updated = cx.update_window(window_id, |cx| { - code_verification_handle.update(cx, |code_verification, cx| { + code_verification_handle.update_root(cx, |code_verification, cx| { code_verification.set_status(status.clone(), cx) }); cx.activate_window(); @@ -41,9 +41,9 @@ pub fn init(cx: &mut AppContext) { } Status::Authorized | Status::Unauthorized => { if let Some(code_verification) = code_verification.as_ref() { - let window_id = code_verification.window_id(); + let window_id = code_verification.id(); cx.update_window(window_id, |cx| { - code_verification.update(cx, |code_verification, cx| { + code_verification.update_root(cx, |code_verification, cx| { code_verification.set_status(status, cx) }); @@ -54,7 +54,7 @@ pub fn init(cx: &mut AppContext) { } _ => { if let Some(code_verification) = code_verification.take() { - cx.update_window(code_verification.window_id(), |cx| cx.remove_window()); + code_verification.update(cx, |cx| cx.remove_window()); } } } diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index d0cd437946a..2444465be66 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -855,7 +855,9 @@ mod tests { let language_server_id = LanguageServerId(0); let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); // Create some diagnostics project.update(cx, |project, cx| { @@ -1248,7 +1250,9 @@ mod tests { let server_id_1 = LanguageServerId(100); let server_id_2 = LanguageServerId(101); let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let view = cx.add_view(window_id, |cx| { ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index eb03d2bdc0f..96921643d45 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -48,36 +48,40 @@ fn test_edit_events(cx: &mut TestAppContext) { }); let events = Rc::new(RefCell::new(Vec::new())); - let (_, editor1) = cx.add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!( - event, - Event::Edited | Event::BufferEdited | Event::DirtyChanged - ) { - events.borrow_mut().push(("editor1", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); - let (_, editor2) = cx.add_window({ - let events = events.clone(); - |cx| { - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!( - event, - Event::Edited | Event::BufferEdited | Event::DirtyChanged - ) { - events.borrow_mut().push(("editor2", event.clone())); - } - }) - .detach(); - Editor::for_buffer(buffer.clone(), None, cx) - } - }); + let editor1 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor1", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .detach(cx); + let editor2 = cx + .add_window({ + let events = events.clone(); + |cx| { + cx.subscribe(&cx.handle(), move |_, _, event, _| { + if matches!( + event, + Event::Edited | Event::BufferEdited | Event::DirtyChanged + ) { + events.borrow_mut().push(("editor2", event.clone())); + } + }) + .detach(); + Editor::for_buffer(buffer.clone(), None, cx) + } + }) + .detach(cx); assert_eq!(mem::take(&mut *events.borrow_mut()), []); // Mutating editor 1 will emit an `Edited` event only for that editor. @@ -173,7 +177,9 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { let buffer = cx.add_model(|cx| language::Buffer::new(0, "123456", cx)); 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.add_window(|cx| build_editor(buffer.clone(), cx)); + let editor = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .detach(cx); editor.update(cx, |editor, cx| { editor.start_transaction_at(now, cx); @@ -343,10 +349,12 @@ fn test_ime_composition(cx: &mut TestAppContext) { fn test_selection_with_mouse(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); - build_editor(buffer, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); }); @@ -410,10 +418,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { fn test_canceling_pending_selection(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); @@ -456,10 +466,12 @@ fn test_clone(cx: &mut TestAppContext) { true, ); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&text, cx); - build_editor(buffer, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&text, cx); + build_editor(buffer, cx) + }) + .detach(cx); editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); @@ -473,9 +485,11 @@ fn test_clone(cx: &mut TestAppContext) { ); }); - let (_, cloned_editor) = editor.update(cx, |editor, cx| { - cx.add_window(Default::default(), |cx| editor.clone(cx)) - }); + let cloned_editor = editor + .update(cx, |editor, cx| { + cx.add_window(Default::default(), |cx| editor.clone(cx)) + }) + .detach(cx); let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); @@ -509,7 +523,9 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); cx.add_view(window_id, |cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); @@ -618,10 +634,12 @@ async fn test_navigation_history(cx: &mut TestAppContext) { fn test_cancel(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); @@ -661,9 +679,10 @@ fn test_cancel(cx: &mut TestAppContext) { fn test_fold_action(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple( - &" + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + &" impl Foo { // Hello! @@ -680,11 +699,12 @@ fn test_fold_action(cx: &mut TestAppContext) { } } " - .unindent(), - cx, - ); - build_editor(buffer.clone(), cx) - }); + .unindent(), + cx, + ); + build_editor(buffer.clone(), cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -752,7 +772,9 @@ fn test_move_cursor(cx: &mut TestAppContext) { init_test(cx, |_| {}); let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); - let (_, view) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); + let view = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .detach(cx); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -827,10 +849,12 @@ fn test_move_cursor(cx: &mut TestAppContext) { fn test_move_cursor_multibyte(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); - build_editor(buffer.clone(), cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); + build_editor(buffer.clone(), cx) + }) + .detach(cx); assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); @@ -932,10 +956,12 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); - build_editor(buffer.clone(), cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); + build_editor(buffer.clone(), cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); @@ -982,10 +1008,12 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { fn test_beginning_end_of_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\n def", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\n def", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1145,10 +1173,12 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { fn test_prev_next_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1197,10 +1227,13 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = + MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.set_wrap_width(Some(140.), cx); @@ -1530,10 +1563,12 @@ async fn test_delete_to_beginning_of_line(cx: &mut gpui::TestAppContext) { fn test_delete_to_word_boundary(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("one two three four", cx); - build_editor(buffer.clone(), cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("one two three four", cx); + build_editor(buffer.clone(), cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1566,10 +1601,12 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { fn test_newline(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); - build_editor(buffer.clone(), cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); + build_editor(buffer.clone(), cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1589,9 +1626,10 @@ fn test_newline(cx: &mut TestAppContext) { fn test_newline_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple( - " + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple( + " a b( X @@ -1600,19 +1638,20 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { X ) " - .unindent() - .as_str(), - cx, - ); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([ - Point::new(2, 4)..Point::new(2, 5), - Point::new(5, 4)..Point::new(5, 5), - ]) - }); - editor - }); + .unindent() + .as_str(), + cx, + ); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([ + Point::new(2, 4)..Point::new(2, 5), + Point::new(5, 4)..Point::new(5, 5), + ]) + }); + editor + }) + .detach(cx); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -1817,12 +1856,14 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { fn test_insert_with_old_selections(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); - let mut editor = build_editor(buffer.clone(), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); - editor - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx); + let mut editor = build_editor(buffer.clone(), cx); + editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); + editor + }) + .detach(cx); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -2329,10 +2370,12 @@ async fn test_delete(cx: &mut gpui::TestAppContext) { fn test_delete_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2352,10 +2395,12 @@ fn test_delete_line(cx: &mut TestAppContext) { ); }); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) @@ -2654,10 +2699,12 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2680,10 +2727,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) { ); }); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2707,10 +2756,12 @@ fn test_duplicate_line(cx: &mut TestAppContext) { fn test_move_line_up_down(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -2806,10 +2857,12 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); - build_editor(buffer, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .detach(cx); editor.update(cx, |editor, cx| { let snapshot = editor.buffer.read(cx).snapshot(cx); editor.insert_blocks( @@ -2834,102 +2887,94 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { fn test_transpose(cx: &mut TestAppContext) { init_test(cx, |_| {}); - _ = cx - .add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [2..2]); + editor.change_selections(None, cx, |s| s.select_ranges([1..1])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [2..2]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bca"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bca"); + assert_eq!(editor.selections.ranges(cx), [3..3]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bac"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bac"); + assert_eq!(editor.selections.ranges(cx), [3..3]); - editor - }) - .1; + editor + }); - _ = cx - .add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.change_selections(None, cx, |s| s.select_ranges([3..3])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acb\nde"); - assert_eq!(editor.selections.ranges(cx), [3..3]); + editor.change_selections(None, cx, |s| s.select_ranges([3..3])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acb\nde"); + assert_eq!(editor.selections.ranges(cx), [3..3]); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [5..5]); + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [5..5]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbde\n"); - assert_eq!(editor.selections.ranges(cx), [6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbde\n"); + assert_eq!(editor.selections.ranges(cx), [6..6]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "acbd\ne"); - assert_eq!(editor.selections.ranges(cx), [6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "acbd\ne"); + assert_eq!(editor.selections.ranges(cx), [6..6]); - editor - }) - .1; + editor + }); - _ = cx - .add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), cx); - editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bacd\ne"); - assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); + editor.change_selections(None, cx, |s| s.select_ranges([1..1, 2..2, 4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bacd\ne"); + assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcda\ne"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcda\ne"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcade\n"); - assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcade\n"); + assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "bcaed\n"); - assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "bcaed\n"); + assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]); - editor - }) - .1; + editor + }); - _ = cx - .add_window(|cx| { - let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); + _ = cx.add_window(|cx| { + let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), cx); - editor.change_selections(None, cx, |s| s.select_ranges([4..4])); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [8..8]); + editor.change_selections(None, cx, |s| s.select_ranges([4..4])); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [8..8]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀✋🍐"); - assert_eq!(editor.selections.ranges(cx), [11..11]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀✋🍐"); + assert_eq!(editor.selections.ranges(cx), [11..11]); - editor.transpose(&Default::default(), cx); - assert_eq!(editor.text(cx), "🏀🍐✋"); - assert_eq!(editor.selections.ranges(cx), [11..11]); + editor.transpose(&Default::default(), cx); + assert_eq!(editor.text(cx), "🏀🍐✋"); + assert_eq!(editor.selections.ranges(cx), [11..11]); - editor - }) - .1; + editor + }); } #[gpui::test] @@ -3132,10 +3177,12 @@ async fn test_paste_multiline(cx: &mut gpui::TestAppContext) { fn test_select_all(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.select_all(&SelectAll, cx); assert_eq!( @@ -3149,10 +3196,12 @@ fn test_select_all(cx: &mut TestAppContext) { fn test_select_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -3196,10 +3245,12 @@ fn test_select_line(cx: &mut TestAppContext) { fn test_split_selection_into_lines(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3267,10 +3318,12 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { fn test_add_selection_above_below(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, view) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); - build_editor(buffer, cx) - }); + let view = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); + build_editor(buffer, cx) + }) + .detach(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -3555,7 +3608,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -3718,7 +3771,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; @@ -4281,7 +4334,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4429,7 +4482,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4519,7 +4572,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); @@ -4649,7 +4702,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4761,7 +4814,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4875,7 +4928,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, editor) = cx.add_window(|cx| build_editor(buffer, cx)); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); let format = editor.update(cx, |editor, cx| { @@ -5653,7 +5706,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx)); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { @@ -5723,7 +5776,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { multibuffer }); - let (_, view) = cx.add_window(|cx| build_editor(multibuffer, cx)); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" @@ -5799,22 +5852,24 @@ fn test_refresh_selections(cx: &mut TestAppContext) { multibuffer }); - let (_, editor) = cx.add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.change_selections(None, cx, |s| { - s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) - }); - editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [ - Point::new(1, 3)..Point::new(1, 3), - Point::new(2, 1)..Point::new(2, 1), - ] - ); - editor - }); + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(1, 3)..Point::new(1, 3)]) + }); + editor.begin_selection(Point::new(2, 1).to_display_point(&snapshot), true, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [ + Point::new(1, 3)..Point::new(1, 3), + Point::new(2, 1)..Point::new(2, 1), + ] + ); + editor + }) + .detach(cx); // Refreshing selections is a no-op when excerpts haven't changed. editor.update(cx, |editor, cx| { @@ -5884,16 +5939,18 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { multibuffer }); - let (_, editor) = cx.add_window(|cx| { - let mut editor = build_editor(multibuffer.clone(), cx); - let snapshot = editor.snapshot(cx); - editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); - assert_eq!( - editor.selections.ranges(cx), - [Point::new(1, 3)..Point::new(1, 3)] - ); - editor - }); + let editor = cx + .add_window(|cx| { + let mut editor = build_editor(multibuffer.clone(), cx); + let snapshot = editor.snapshot(cx); + editor.begin_selection(Point::new(1, 3).to_display_point(&snapshot), false, 1, cx); + assert_eq!( + editor.selections.ranges(cx), + [Point::new(1, 3)..Point::new(1, 3)] + ); + editor + }) + .detach(cx); multibuffer.update(cx, |multibuffer, cx| { multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); @@ -5956,7 +6013,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let (_, view) = cx.add_window(|cx| build_editor(buffer, cx)); + let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); view.condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -5992,10 +6049,12 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { fn test_highlighted_ranges(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); - build_editor(buffer.clone(), cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); + build_editor(buffer.clone(), cx) + }) + .detach(cx); editor.update(cx, |editor, cx| { struct Type1; @@ -6084,16 +6143,20 @@ async fn test_following(cx: &mut gpui::TestAppContext) { .unwrap(); cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)) }); - let (_, leader) = cx.add_window(|cx| build_editor(buffer.clone(), cx)); - let (_, follower) = cx.update(|cx| { - cx.add_window( - WindowOptions { - bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), - ..Default::default() - }, - |cx| build_editor(buffer.clone(), cx), - ) - }); + let leader = cx + .add_window(|cx| build_editor(buffer.clone(), cx)) + .detach(cx); + let follower = cx + .update(|cx| { + cx.add_window( + WindowOptions { + bounds: WindowBounds::Fixed(RectF::from_points(vec2f(0., 0.), vec2f(10., 80.))), + ..Default::default() + }, + |cx| build_editor(buffer.clone(), cx), + ) + }) + .detach(cx); let is_still_following = Rc::new(RefCell::new(true)); let follower_edit_event_count = Rc::new(RefCell::new(0)); @@ -6224,7 +6287,9 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let leader = pane.update(cx, |_, cx| { @@ -6968,7 +7033,7 @@ async fn test_copilot_multibuffer( ); multibuffer }); - let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); handle_copilot_completion_request( &copilot_lsp, @@ -7098,7 +7163,7 @@ async fn test_copilot_disabled_globs( ); multibuffer }); - let (_, editor) = cx.add_window(|cx| build_editor(multibuffer, cx)); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); let mut copilot_requests = copilot_lsp .handle_request::(move |_params, _cx| async move { @@ -7177,7 +7242,9 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -7282,7 +7349,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::Test .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let _window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let _buffer = project .update(cx, |project, cx| { project.open_local_buffer("/a/main.rs", cx) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 750beaea138..dc40e7fb856 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3002,10 +3002,12 @@ mod tests { 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); - Editor::new(EditorMode::Full, buffer, None, None, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .detach(cx); let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let layouts = editor.update(cx, |editor, cx| { @@ -3021,10 +3023,12 @@ mod tests { fn test_layout_with_placeholder_text_and_blocks(cx: &mut TestAppContext) { init_test(cx, |_| {}); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple("", cx); - Editor::new(EditorMode::Full, buffer, None, None, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple("", cx); + Editor::new(EditorMode::Full, buffer, None, None, cx) + }) + .detach(cx); editor.update(cx, |editor, cx| { editor.set_placeholder_text("hello", cx); @@ -3231,10 +3235,12 @@ mod tests { info!( "Creating editor with mode {editor_mode:?}, width {editor_width} and text '{input_text}'" ); - let (_, editor) = cx.add_window(|cx| { - let buffer = MultiBuffer::build_simple(&input_text, cx); - Editor::new(editor_mode, buffer, None, None, cx) - }); + let editor = cx + .add_window(|cx| { + let buffer = MultiBuffer::build_simple(&input_text, cx); + Editor::new(editor_mode, buffer, None, None, cx) + }) + .detach(cx); let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let (_, layout_state) = editor.update(cx, |editor, cx| { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 63076ba234d..089cbb29958 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1136,7 +1136,9 @@ mod tests { ) .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1836,7 +1838,9 @@ mod tests { .await; let project = Project::test(fs, ["/a".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1989,7 +1993,9 @@ mod tests { project.update(cx, |project, _| { project.languages().add(Arc::clone(&language)) }); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -2075,8 +2081,9 @@ mod tests { deterministic.run_until_parked(); cx.foreground().run_until_parked(); - let (_, editor) = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) + .detach(cx); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); let closure_editor_edited = Arc::clone(&editor_edited); @@ -2328,7 +2335,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" project.update(cx, |project, _| { project.languages().add(Arc::clone(&language)) }); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -2373,8 +2382,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" deterministic.run_until_parked(); cx.foreground().run_until_parked(); - let (_, editor) = - cx.add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)); + let editor = cx + .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) + .detach(cx); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); let closure_editor_edited = Arc::clone(&editor_edited); @@ -2562,7 +2572,9 @@ all hints should be invalidated and requeried for all of its visible excerpts" let project = Project::test(fs, ["/a".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(Arc::new(language))); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 0fe49d4d046..f53115f224a 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -69,7 +69,8 @@ impl<'a> EditorLspTestContext<'a> { .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }})) .await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); project .update(cx, |project, cx| { project.find_or_create_local_worktree("/root", true, cx) @@ -98,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> { Self { cx: EditorTestContext { cx, - window_id, + window_id: window.id(), editor, }, lsp, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index bac70f139a6..c7ea1b4f385 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -32,16 +32,14 @@ impl<'a> EditorTestContext<'a> { let buffer = project .update(cx, |project, cx| project.create_buffer("", None, cx)) .unwrap(); - let (window_id, editor) = cx.update(|cx| { - cx.add_window(Default::default(), |cx| { - cx.focus_self(); - build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx) - }) + let window = cx.add_window(|cx| { + cx.focus_self(); + build_editor(MultiBuffer::build_from_buffer(buffer, cx), cx) }); - + let editor = window.root(cx); Self { cx, - window_id, + window_id: window.id(), editor, } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index b6701f12d6e..2c9d9c0c71e 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -617,8 +617,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(window_id, Toggle); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder @@ -631,8 +632,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -671,8 +672,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(window_id, Toggle); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -704,8 +706,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -754,8 +756,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - cx.dispatch_action(window_id, Toggle); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -787,8 +790,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -837,19 +840,23 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), cx, - ), - cx, - ) - }); + ) + }) + .detach(cx); let query = test_path_like("hi"); finder @@ -931,19 +938,23 @@ mod tests { cx, ) .await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), cx, - ), - cx, - ) - }); + ) + }) + .detach(cx); finder .update(cx, |f, cx| { f.delegate_mut().spawn_search(test_path_like("hi"), cx) @@ -967,19 +978,23 @@ mod tests { cx, ) .await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), cx, - ), - cx, - ) - }); + ) + }) + .detach(cx); // Even though there is only one worktree, that worktree's filename // is included in the matching, because the worktree is a single file. @@ -1015,61 +1030,6 @@ mod tests { finder.read_with(cx, |f, _| assert_eq!(f.delegate().matches.len(), 0)); } - #[gpui::test] - async fn test_multiple_matches_with_same_relative_path(cx: &mut TestAppContext) { - let app_state = init_test(cx); - app_state - .fs - .as_fake() - .insert_tree( - "/root", - json!({ - "dir1": { "a.txt": "" }, - "dir2": { "a.txt": "" } - }), - ) - .await; - - let project = Project::test( - app_state.fs.clone(), - ["/root/dir1".as_ref(), "/root/dir2".as_ref()], - cx, - ) - .await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), - cx, - ), - cx, - ) - }); - - // Run a search that matches two files with the same relative path. - finder - .update(cx, |f, cx| { - f.delegate_mut().spawn_search(test_path_like("a.t"), cx) - }) - .await; - - // Can switch between different matches with the same relative path. - finder.update(cx, |finder, cx| { - let delegate = finder.delegate_mut(); - assert_eq!(delegate.matches.len(), 2); - assert_eq!(delegate.selected_index(), 0); - delegate.set_selected_index(1, cx); - assert_eq!(delegate.selected_index(), 1); - delegate.set_selected_index(0, cx); - assert_eq!(delegate.selected_index(), 0); - }); - } - #[gpui::test] async fn test_path_distance_ordering(cx: &mut TestAppContext) { let app_state = init_test(cx); @@ -1089,7 +1049,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1103,18 +1065,20 @@ mod tests { worktree_id, path: Arc::from(Path::new("/root/dir2/b.txt")), })); - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - b_path, - Vec::new(), + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + b_path, + Vec::new(), + cx, + ), cx, - ), - cx, - ) - }); + ) + }) + .detach(cx); finder .update(cx, |f, cx| { @@ -1151,19 +1115,23 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); - let (_, finder) = cx.add_window(|cx| { - Picker::new( - FileFinderDelegate::new( - workspace.downgrade(), - workspace.read(cx).project().clone(), - None, - Vec::new(), + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); + let finder = cx + .add_window(|cx| { + Picker::new( + FileFinderDelegate::new( + workspace.downgrade(), + workspace.read(cx).project().clone(), + None, + Vec::new(), + cx, + ), cx, - ), - cx, - ) - }); + ) + }) + .detach(cx); finder .update(cx, |f, cx| { f.delegate_mut().spawn_search(test_path_like("dir"), cx) @@ -1198,7 +1166,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1404,7 +1374,9 @@ mod tests { .detach(); deterministic.run_until_parked(); - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9c0e50647c6..45169ed3af4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1424,7 +1424,7 @@ impl AppContext { &mut self, window_id: usize, build_root_view: F, - ) -> Option> + ) -> Option> where V: View, F: FnOnce(&mut ViewContext) -> V, @@ -3826,6 +3826,15 @@ impl WindowHandle { self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap()) } + /// Keep this window open until it's explicitly closed. + // + // TODO: Implement window dropping behavior when we don't call this. + pub fn detach(mut self, cx: &impl BorrowAppContext) -> ViewHandle { + let root = self.root(cx); + self.any_handle.ref_counts.take(); + root + } + pub fn read_with(&self, cx: &C, read: F) -> R where C: BorrowAppContext, @@ -3893,7 +3902,7 @@ impl WindowHandle { pub struct AnyWindowHandle { window_id: usize, root_view_type: TypeId, - ref_counts: Arc>, + ref_counts: Option>>, #[cfg(any(test, feature = "test-support"))] handle_id: usize, @@ -3913,7 +3922,7 @@ impl AnyWindowHandle { Self { window_id, root_view_type: TypeId::of::(), - ref_counts, + ref_counts: Some(ref_counts), #[cfg(any(test, feature = "test-support"))] handle_id, } @@ -3937,7 +3946,16 @@ impl AnyWindowHandle { impl Drop for AnyWindowHandle { fn drop(&mut self) { - self.ref_counts.lock().dec_window(self.window_id) + if let Some(ref_counts) = self.ref_counts.as_ref() { + ref_counts.lock().dec_window(self.window_id); + + #[cfg(any(test, feature = "test-support"))] + ref_counts + .lock() + .leak_detector + .lock() + .handle_dropped(self.window_id, self.handle_id); + } } } diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 9dc5d99bc5e..7cdcbc2c8fc 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -15,7 +15,7 @@ use crate::{ util::post_inc, Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, - View, ViewContext, ViewHandle, WindowInvalidation, + View, ViewContext, ViewHandle, WindowHandle, WindowInvalidation, }; use anyhow::{anyhow, bail, Result}; use collections::{HashMap, HashSet}; @@ -1151,15 +1151,15 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn replace_root_view(&mut self, build_root_view: F) -> ViewHandle + pub fn replace_root_view(&mut self, build_root_view: F) -> WindowHandle where V: View, F: FnOnce(&mut ViewContext) -> V, { let root_view = self.add_view(|cx| build_root_view(cx)); - self.window.root_view = Some(root_view.clone().into_any()); self.window.focused_view_id = Some(root_view.id()); - root_view + self.window.root_view = Some(root_view.into_any()); + WindowHandle::new(self.window_id, self.ref_counts.clone()) } pub fn add_view(&mut self, build_view: F) -> ViewHandle diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index d4a16b5758a..ce05a417ad8 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -61,7 +61,9 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { .receive_notification::() .await; - let (_, log_view) = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); + let log_view = cx + .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)) + .detach(cx); language_server.notify::(lsp::LogMessageParams { message: "hello from the server".into(), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 0be52646e63..fdc5ea108a8 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1780,7 +1780,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -1868,7 +1870,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2219,7 +2223,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2319,7 +2325,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); panel.update(cx, |panel, cx| { @@ -2392,7 +2400,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); toggle_expand_dir(&panel, "src/test", cx); @@ -2481,7 +2491,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "src/", cx); @@ -2627,7 +2639,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let new_search_events_count = Arc::new(AtomicUsize::new(0)); @@ -2714,7 +2728,9 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); panel.update(cx, |panel, cx| { diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index cbf914230d3..8471f3a3a70 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -326,7 +326,9 @@ mod tests { }, ); - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); + let window_id = window.id(); // Create the project symbols view. let symbols = cx.add_view(window_id, |cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 45842aa5617..1e635432bd5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -849,11 +849,13 @@ mod tests { cx, ) }); - let (window_id, _root_view) = cx.add_window(|_| EmptyView); + let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let editor = cx.add_view(window.id(), |cx| { + Editor::for_buffer(buffer.clone(), None, cx) + }); - let search_bar = cx.add_view(window_id, |cx| { + let search_bar = cx.add_view(window.id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1229,7 +1231,8 @@ mod tests { "Should pick a query with multiple results" ); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); - let (window_id, _root_view) = cx.add_window(|_| EmptyView); + let window = cx.add_window(|_| EmptyView); + let window_id = window.id(); let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1416,11 +1419,13 @@ mod tests { "# .unindent(); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); - let (window_id, _root_view) = cx.add_window(|_| EmptyView); + let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let editor = cx.add_view(window.id(), |cx| { + Editor::for_buffer(buffer.clone(), None, cx) + }); - let search_bar = cx.add_view(window_id, |cx| { + let search_bar = cx.add_view(window.id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 1b4e32f4b83..e57edd3b147 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1447,7 +1447,9 @@ pub mod tests { .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); - let (_, search_view) = cx.add_window(|cx| ProjectSearchView::new(search.clone(), cx)); + let search_view = cx + .add_window(|cx| ProjectSearchView::new(search.clone(), cx)) + .detach(cx); search_view.update(cx, |search_view, cx| { search_view @@ -1564,7 +1566,9 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let active_item = cx.read(|cx| { workspace @@ -1748,7 +1752,9 @@ pub mod tests { let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); let active_item = cx.read(|cx| { workspace @@ -1866,7 +1872,9 @@ pub mod tests { ) .await; let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); workspace.update(cx, |workspace, cx| { ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) }); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index e108a05ccc0..874978b4fc5 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1070,7 +1070,9 @@ mod tests { }); let project = Project::test(params.fs.clone(), [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); (project, workspace) } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index ee658c9cc92..98883fac337 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1972,7 +1972,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); pane.update(cx, |pane, cx| { @@ -1987,7 +1988,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // 1. Add with a destination index @@ -2065,7 +2067,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // 1. Add with a destination index @@ -2141,7 +2144,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); // singleton view @@ -2209,7 +2213,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); add_labeled_item(&pane, "A", false, cx); @@ -2256,7 +2261,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); @@ -2276,7 +2282,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); add_labeled_item(&pane, "A", true, cx); @@ -2299,7 +2306,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); @@ -2319,7 +2327,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); set_labeled_items(&pane, ["A", "B", "C*", "D", "E"], cx); @@ -2339,7 +2348,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); add_labeled_item(&pane, "A", false, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 434975216ad..3222ea2eb8c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -793,7 +793,7 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; - let workspace = requesting_window_id + let window = requesting_window_id .and_then(|window_id| { cx.update(|cx| { cx.replace_root_view(window_id, |cx| { @@ -852,9 +852,9 @@ impl Workspace { ) }, ) - .1 }); + let workspace = window.root(&cx); (app_state.initialize_workspace)( workspace.downgrade(), serialized_workspace.is_some(), @@ -864,7 +864,7 @@ impl Workspace { .await .log_err(); - cx.update_window(workspace.window_id(), |cx| cx.activate_window()); + window.update(&mut cx, |cx| cx.activate_window()); let workspace = workspace.downgrade(); notify_if_database_failed(&workspace, &mut cx); @@ -3977,7 +3977,7 @@ pub fn join_remote_project( .await?; let window_bounds_override = window_bounds_env_override(&cx); - let (_, workspace) = cx.add_window( + let window = cx.add_window( (app_state.build_window_options)( window_bounds_override, None, @@ -3985,6 +3985,7 @@ pub fn join_remote_project( ), |cx| Workspace::new(0, project, app_state.clone(), cx), ); + let workspace = window.root(&cx); (app_state.initialize_workspace)( workspace.downgrade(), false, @@ -4113,10 +4114,11 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); // Adding an item with no ambiguity renders the tab without detail. - let item1 = cx.add_view(window_id, |_| { + let item1 = window.add_view(cx, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b1/c", "a/b1/c"]); item @@ -4128,7 +4130,7 @@ mod tests { // Adding an item that creates ambiguity increases the level of detail on // both tabs. - let item2 = cx.add_view(window_id, |_| { + let item2 = window.add_view(cx, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); item @@ -4142,7 +4144,7 @@ mod tests { // Adding an item that creates ambiguity increases the level of detail only // on the ambiguous tabs. In this case, the ambiguity can't be resolved so // we stop at the highest detail available. - let item3 = cx.add_view(window_id, |_| { + let item3 = window.add_view(cx, |_| { let mut item = TestItem::new(); item.tab_descriptions = Some(vec!["c", "b2/c", "a/b2/c"]); item @@ -4177,16 +4179,17 @@ mod tests { .await; let project = Project::test(fs, ["root1".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); - let item1 = cx.add_view(window_id, |cx| { + let item1 = window.add_view(cx, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(1, "one.txt", cx)]) }); - let item2 = cx.add_view(window_id, |cx| { + let item2 = window.add_view(cx, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(2, "two.txt", cx)]) }); @@ -4201,14 +4204,14 @@ mod tests { ); }); assert_eq!( - cx.current_window_title(window_id).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1") ); // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); assert_eq!( - cx.current_window_title(window_id).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("two.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4227,7 +4230,7 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window_id).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4247,14 +4250,14 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window_id).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1, root2") ); // Remove a project folder project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); assert_eq!( - cx.current_window_title(window_id).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root2") ); } @@ -4267,18 +4270,19 @@ mod tests { fs.insert_tree("/root", json!({ "one": "" })).await; let project = Project::test(fs, ["root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = window.root(cx); // When there are no dirty items, there's nothing to do. - let item1 = cx.add_view(window_id, |_| TestItem::new()); + let item1 = window.add_view(cx, |_| TestItem::new()); workspace.update(cx, |w, cx| w.add_item(Box::new(item1.clone()), cx)); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); assert!(task.await.unwrap()); // When there are dirty untitled items, prompt to save each one. If the user // cancels any prompt, then abort. - let item2 = cx.add_view(window_id, |_| TestItem::new().with_dirty(true)); - let item3 = cx.add_view(window_id, |cx| { + let item2 = window.add_view(cx, |_| TestItem::new().with_dirty(true)); + let item3 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) @@ -4289,9 +4293,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - cx.simulate_prompt_answer(window_id, 2 /* cancel */); + cx.simulate_prompt_answer(window.id(), 2 /* cancel */); cx.foreground().run_until_parked(); - assert!(!cx.has_pending_prompt(window_id)); + assert!(!cx.has_pending_prompt(window.id())); assert!(!task.await.unwrap()); } @@ -4302,26 +4306,27 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, None, cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); - let item1 = cx.add_view(window_id, |cx| { + let item1 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); - let item2 = cx.add_view(window_id, |cx| { + let item2 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_conflict(true) .with_project_items(&[TestProjectItem::new(2, "2.txt", cx)]) }); - let item3 = cx.add_view(window_id, |cx| { + let item3 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_conflict(true) .with_project_items(&[TestProjectItem::new(3, "3.txt", cx)]) }); - let item4 = cx.add_view(window_id, |cx| { + let item4 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_project_items(&[TestProjectItem::new_untitled(cx)]) @@ -4349,10 +4354,10 @@ mod tests { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(cx.has_pending_prompt(window_id)); + assert!(cx.has_pending_prompt(window.id())); // Confirm saving item 1. - cx.simulate_prompt_answer(window_id, 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); // Item 1 is saved. There's a prompt to save item 3. @@ -4363,10 +4368,10 @@ mod tests { assert_eq!(pane.items_len(), 3); assert_eq!(pane.active_item().unwrap().id(), item3.id()); }); - assert!(cx.has_pending_prompt(window_id)); + assert!(cx.has_pending_prompt(window.id())); // Cancel saving item 3. - cx.simulate_prompt_answer(window_id, 1); + cx.simulate_prompt_answer(window.id(), 1); cx.foreground().run_until_parked(); // Item 3 is reloaded. There's a prompt to save item 4. @@ -4377,10 +4382,10 @@ mod tests { assert_eq!(pane.items_len(), 2); assert_eq!(pane.active_item().unwrap().id(), item4.id()); }); - assert!(cx.has_pending_prompt(window_id)); + assert!(cx.has_pending_prompt(window.id())); // Confirm saving item 4. - cx.simulate_prompt_answer(window_id, 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); // There's a prompt for a path for item 4. @@ -4404,13 +4409,14 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); // Create several workspace items with single project entries, and two // workspace items with multiple project entries. let single_entry_items = (0..=4) .map(|project_entry_id| { - cx.add_view(window_id, |cx| { + window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_project_items(&[TestProjectItem::new( @@ -4421,7 +4427,7 @@ mod tests { }) }) .collect::>(); - let item_2_3 = cx.add_view(window_id, |cx| { + let item_2_3 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_singleton(false) @@ -4430,7 +4436,7 @@ mod tests { single_entry_items[3].read(cx).project_items[0].clone(), ]) }); - let item_3_4 = cx.add_view(window_id, |cx| { + let item_3_4 = window.add_view(cx, |cx| { TestItem::new() .with_dirty(true) .with_singleton(false) @@ -4482,7 +4488,7 @@ mod tests { &[ProjectEntryId::from_proto(0)] ); }); - cx.simulate_prompt_answer(window_id, 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { @@ -4491,7 +4497,7 @@ mod tests { &[ProjectEntryId::from_proto(2)] ); }); - cx.simulate_prompt_answer(window_id, 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); close.await.unwrap(); @@ -4507,10 +4513,11 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - let item = cx.add_view(window_id, |cx| { + let item = window.add_view(cx, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); let item_id = item.id(); @@ -4550,7 +4557,7 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - cx.simulate_window_activation(Some(window_id)); + cx.simulate_window_activation(Some(window.id())); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; @@ -4592,7 +4599,7 @@ mod tests { pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); - assert!(!cx.has_pending_prompt(window_id)); + assert!(!cx.has_pending_prompt(window.id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. @@ -4613,7 +4620,7 @@ mod tests { let _close_items = pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); - assert!(cx.has_pending_prompt(window_id)); + assert!(cx.has_pending_prompt(window.id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); } @@ -4624,9 +4631,10 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); - let item = cx.add_view(window_id, |cx| { + let item = window.add_view(cx, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) }); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); @@ -4677,7 +4685,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); let panel = workspace.update(cx, |workspace, cx| { let panel = cx.add_view(|_| TestPanel::new(DockPosition::Right)); @@ -4824,7 +4833,8 @@ mod tests { let fs = FakeFs::new(cx.background()); let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { // Add panel_1 on the left, panel_2 on the right. @@ -4979,7 +4989,7 @@ mod tests { // If focus is transferred to another view that's not a panel or another pane, we still show // the panel as zoomed. - let focus_receiver = cx.add_view(window_id, |_| EmptyView); + let focus_receiver = window.add_view(cx, |_| EmptyView); focus_receiver.update(cx, |_, cx| cx.focus_self()); workspace.read_with(cx, |workspace, _| { assert_eq!(workspace.zoomed, Some(panel_1.downgrade().into_any())); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 4b0bf1cd4c6..1770c5648e1 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -983,7 +983,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1295,7 +1297,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); // Open a file within an existing worktree. workspace @@ -1336,7 +1340,9 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer @@ -1429,7 +1435,9 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; project.update(cx, |project, _| project.languages().add(rust_lang())); - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); // Create a new untitled buffer cx.dispatch_action(window_id, NewFile); @@ -1480,7 +1488,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); + let window_id = window.id(); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1554,7 +1564,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .detach(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); @@ -1831,7 +1843,9 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = cx + .add_window(|cx| Workspace::test_new(project, cx)) + .detach(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); @@ -2073,7 +2087,8 @@ mod tests { cx.foreground().run_until_parked(); - let (window_id, _view) = cx.add_window(|_| TestView); + let window = cx.add_window(|_| TestView); + let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( @@ -2243,7 +2258,8 @@ mod tests { cx.foreground().run_until_parked(); - let (window_id, _view) = cx.add_window(|_| TestView); + let window = cx.add_window(|_| TestView); + let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( From 8e36da1382e84c87c5d3576a848eb06645bb21ab Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 2 Aug 2023 15:02:55 -0600 Subject: [PATCH 033/105] Get tests passing --- crates/gpui/src/app.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 45169ed3af4..bd615522c26 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -3831,7 +3831,14 @@ impl WindowHandle { // TODO: Implement window dropping behavior when we don't call this. pub fn detach(mut self, cx: &impl BorrowAppContext) -> ViewHandle { let root = self.root(cx); - self.any_handle.ref_counts.take(); + let ref_counts = self.any_handle.ref_counts.take(); + #[cfg(any(test, feature = "test-support"))] + ref_counts + .unwrap() + .lock() + .leak_detector + .lock() + .handle_dropped(self.id(), self.any_handle.handle_id); root } From df4480ba52aede20b416afb87a0b0e17eb025682 Mon Sep 17 00:00:00 2001 From: Julia Date: Wed, 2 Aug 2023 17:33:56 -0400 Subject: [PATCH 034/105] Use the same font size for hovered state of LSP status This element is used for the update state as well for some reason so while we don't normally ever see this state, it is used when the status is acting as the restart to update button --- styles/src/style_tree/status_bar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/style_tree/status_bar.ts b/styles/src/style_tree/status_bar.ts index 6261939994a..d35b721c6ce 100644 --- a/styles/src/style_tree/status_bar.ts +++ b/styles/src/style_tree/status_bar.ts @@ -49,7 +49,7 @@ export default function status_bar(): any { }, state: { hovered: { - message: text(layer, "sans"), + message: text(layer, "sans", { size: "xs" }), icon_color: foreground(layer), background: background(layer, "hovered"), }, From 3c938a7377bbc2a3147f71da53ef05a1f45eaa5a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 3 Aug 2023 08:10:16 -0600 Subject: [PATCH 035/105] WIP --- crates/gpui/src/app.rs | 16 ++++++++++++---- crates/gpui/src/app/test_app_context.rs | 2 ++ crates/gpui/src/app/window.rs | 2 ++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index bd615522c26..9b847e9c0cf 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -130,10 +130,12 @@ pub trait BorrowAppContext { } pub trait BorrowWindowContext { - fn read_with(&self, window_id: usize, f: F) -> T + type Return; + + fn read_with(&self, window_id: usize, f: F) -> Self::Return where F: FnOnce(&WindowContext) -> T; - fn update(&mut self, window_id: usize, f: F) -> T + fn update(&mut self, window_id: usize, f: F) -> Self::Return where F: FnOnce(&mut WindowContext) -> T; } @@ -3358,6 +3360,8 @@ impl BorrowAppContext for ViewContext<'_, '_, V> { } impl BorrowWindowContext for ViewContext<'_, '_, V> { + type Return = T; + fn read_with T>(&self, window_id: usize, f: F) -> T { BorrowWindowContext::read_with(&*self.window_context, window_id, f) } @@ -3463,6 +3467,8 @@ impl BorrowAppContext for LayoutContext<'_, '_, '_, V> { } impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { + type Return = T; + fn read_with T>(&self, window_id: usize, f: F) -> T { BorrowWindowContext::read_with(&*self.view_context, window_id, f) } @@ -3515,6 +3521,8 @@ impl BorrowAppContext for EventContext<'_, '_, '_, V> { } impl BorrowWindowContext for EventContext<'_, '_, '_, V> { + type Return = T; + fn read_with T>(&self, window_id: usize, f: F) -> T { BorrowWindowContext::read_with(&*self.view_context, window_id, f) } @@ -4013,7 +4021,7 @@ impl ViewHandle { cx.read_view(self) } - pub fn read_with(&self, cx: &C, read: F) -> S + pub fn read_with(&self, cx: &C, read: F) -> C::Return where C: BorrowWindowContext, F: FnOnce(&T, &ViewContext) -> S, @@ -4024,7 +4032,7 @@ impl ViewHandle { }) } - pub fn update(&self, cx: &mut C, update: F) -> S + pub fn update(&self, cx: &mut C, update: F) -> C::Return where C: BorrowWindowContext, F: FnOnce(&mut T, &mut ViewContext) -> S, diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 80f10374665..5c7947a4482 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -406,6 +406,8 @@ impl BorrowAppContext for TestAppContext { } impl BorrowWindowContext for TestAppContext { + type Return = T; + fn read_with T>(&self, window_id: usize, f: F) -> T { self.cx .borrow() diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 7cdcbc2c8fc..671d2b38c77 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -142,6 +142,8 @@ impl BorrowAppContext for WindowContext<'_> { } impl BorrowWindowContext for WindowContext<'_> { + type Return = T; + fn read_with T>(&self, window_id: usize, f: F) -> T { if self.window_id == window_id { f(self) From ee1b4a52cc41cb7c331ad9936f1c1cbfb220057e Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Thu, 3 Aug 2023 18:57:43 -0400 Subject: [PATCH 036/105] Add `PathExt` trait (#2823) This PR adds a `PathExt` trait. It pulls in our existing `compact()` function, as a method, and then adds a method, and testing, for `icon_suffix()`. A test was added to fix: - https://github.com/zed-industries/community/issues/1877 Release Notes: - Fixed a bug where file icons would not be registered for files with with `.` characters in their name ([#1877](https://github.com/zed-industries/community/issues/1877)). --- crates/editor/src/items.rs | 9 +- crates/project_panel/src/file_associations.rs | 11 +- .../src/highlighted_workspace_location.rs | 3 +- crates/recent_projects/src/recent_projects.rs | 3 +- crates/util/src/paths.rs | 118 ++++++++++++------ 5 files changed, 88 insertions(+), 56 deletions(-) diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 7c8fe12aa06..b99977a60eb 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -28,7 +28,10 @@ use std::{ path::{Path, PathBuf}, }; use text::Selection; -use util::{paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; +use util::{ + paths::{PathExt, FILE_ROW_COLUMN_DELIMITER}, + ResultExt, TryFutureExt, +}; use workspace::item::{BreadcrumbText, FollowableItemHandle}; use workspace::{ item::{FollowableItem, Item, ItemEvent, ItemHandle, ProjectItem}, @@ -546,9 +549,7 @@ impl Item for Editor { .and_then(|f| f.as_local())? .abs_path(cx); - let file_path = util::paths::compact(&file_path) - .to_string_lossy() - .to_string(); + let file_path = file_path.compact().to_string_lossy().to_string(); Some(file_path.into()) } diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 2694fa1697f..f2692b96db2 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -4,7 +4,7 @@ use collections::HashMap; use gpui::{AppContext, AssetSource}; use serde_derive::Deserialize; -use util::iife; +use util::{iife, paths::PathExt}; #[derive(Deserialize, Debug)] struct TypeConfig { @@ -48,14 +48,7 @@ impl FileAssociations { // FIXME: Associate a type with the languages and have the file's langauge // override these associations iife!({ - let suffix = path - .file_name() - .and_then(|os_str| os_str.to_str()) - .and_then(|file_name| { - file_name - .find('.') - .and_then(|dot_index| file_name.get(dot_index + 1..)) - })?; + let suffix = path.icon_suffix()?; this.suffixes .get(suffix) diff --git a/crates/recent_projects/src/highlighted_workspace_location.rs b/crates/recent_projects/src/highlighted_workspace_location.rs index d3ecfb74fb6..f915cb24edf 100644 --- a/crates/recent_projects/src/highlighted_workspace_location.rs +++ b/crates/recent_projects/src/highlighted_workspace_location.rs @@ -5,6 +5,7 @@ use gpui::{ elements::{Label, LabelStyle}, AnyElement, Element, View, }; +use util::paths::PathExt; use workspace::WorkspaceLocation; pub struct HighlightedText { @@ -61,7 +62,7 @@ impl HighlightedWorkspaceLocation { .paths() .iter() .map(|path| { - let path = util::paths::compact(&path); + let path = path.compact(); let highlighted_text = Self::highlights_for_path( path.as_ref(), &string_match.positions, diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 5bf9ba6ccfe..7a09ac259f5 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -11,6 +11,7 @@ use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; use picker::{Picker, PickerDelegate, PickerEvent}; use std::sync::Arc; +use util::paths::PathExt; use workspace::{ notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, WORKSPACE_DB, @@ -134,7 +135,7 @@ impl PickerDelegate for RecentProjectsDelegate { let combined_string = location .paths() .iter() - .map(|path| util::paths::compact(&path).to_string_lossy().into_owned()) + .map(|path| path.compact().to_string_lossy().into_owned()) .collect::>() .join(""); StringMatchCandidate::new(id, combined_string) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 5df0ed12e96..7e0b240570f 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -30,49 +30,47 @@ pub mod legacy { } } -/// Compacts a given file path by replacing the user's home directory -/// prefix with a tilde (`~`). -/// -/// # Arguments -/// -/// * `path` - A reference to a `Path` representing the file path to compact. -/// -/// # Examples -/// -/// ``` -/// use std::path::{Path, PathBuf}; -/// use util::paths::compact; -/// let path: PathBuf = [ -/// util::paths::HOME.to_string_lossy().to_string(), -/// "some_file.txt".to_string(), -/// ] -/// .iter() -/// .collect(); -/// if cfg!(target_os = "linux") || cfg!(target_os = "macos") { -/// assert_eq!(compact(&path).to_str(), Some("~/some_file.txt")); -/// } else { -/// assert_eq!(compact(&path).to_str(), path.to_str()); -/// } -/// ``` -/// -/// # Returns -/// -/// * A `PathBuf` containing the compacted file path. If the input path -/// does not have the user's home directory prefix, or if we are not on -/// Linux or macOS, the original path is returned unchanged. -pub fn compact(path: &Path) -> PathBuf { - if cfg!(target_os = "linux") || cfg!(target_os = "macos") { - match path.strip_prefix(HOME.as_path()) { - Ok(relative_path) => { - let mut shortened_path = PathBuf::new(); - shortened_path.push("~"); - shortened_path.push(relative_path); - shortened_path +pub trait PathExt { + fn compact(&self) -> PathBuf; + fn icon_suffix(&self) -> Option<&str>; +} + +impl> PathExt for T { + /// Compacts a given file path by replacing the user's home directory + /// prefix with a tilde (`~`). + /// + /// # Returns + /// + /// * A `PathBuf` containing the compacted file path. If the input path + /// does not have the user's home directory prefix, or if we are not on + /// Linux or macOS, the original path is returned unchanged. + fn compact(&self) -> PathBuf { + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + match self.as_ref().strip_prefix(HOME.as_path()) { + Ok(relative_path) => { + let mut shortened_path = PathBuf::new(); + shortened_path.push("~"); + shortened_path.push(relative_path); + shortened_path + } + Err(_) => self.as_ref().to_path_buf(), } - Err(_) => path.to_path_buf(), + } else { + self.as_ref().to_path_buf() } - } else { - path.to_path_buf() + } + + fn icon_suffix(&self) -> Option<&str> { + let file_name = self.as_ref().file_name()?.to_str()?; + + if file_name.starts_with(".") { + return file_name.strip_prefix("."); + } + + self.as_ref() + .extension() + .map(|extension| extension.to_str()) + .flatten() } } @@ -279,4 +277,42 @@ mod tests { ); } } + + #[test] + fn test_path_compact() { + let path: PathBuf = [ + HOME.to_string_lossy().to_string(), + "some_file.txt".to_string(), + ] + .iter() + .collect(); + if cfg!(target_os = "linux") || cfg!(target_os = "macos") { + assert_eq!(path.compact().to_str(), Some("~/some_file.txt")); + } else { + assert_eq!(path.compact().to_str(), path.to_str()); + } + } + + #[test] + fn test_path_suffix() { + // No dots in name + let path = Path::new("/a/b/c/file_name.rs"); + assert_eq!(path.icon_suffix(), Some("rs")); + + // Single dot in name + let path = Path::new("/a/b/c/file.name.rs"); + assert_eq!(path.icon_suffix(), Some("rs")); + + // Multiple dots in name + let path = Path::new("/a/b/c/long.file.name.rs"); + assert_eq!(path.icon_suffix(), Some("rs")); + + // Hidden file, no extension + let path = Path::new("/a/b/c/.gitignore"); + assert_eq!(path.icon_suffix(), Some("gitignore")); + + // Hidden file, with extension + let path = Path::new("/a/b/c/.eslintrc.js"); + assert_eq!(path.icon_suffix(), Some("eslintrc.js")); + } } From afcc0d621b8524d4b3cfa9a5ed19b00c11666348 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 3 Aug 2023 17:03:39 -0600 Subject: [PATCH 037/105] WIP --- crates/editor/src/editor_tests.rs | 98 ++++---- crates/editor/src/element.rs | 6 +- crates/editor/src/inlay_hint_cache.rs | 14 +- crates/file_finder/src/file_finder.rs | 20 +- crates/gpui/src/app.rs | 278 ++++++++++++--------- crates/gpui/src/app/test_app_context.rs | 10 +- crates/gpui/src/app/window.rs | 10 +- crates/language_tools/src/lsp_log_tests.rs | 2 +- crates/project_panel/src/project_panel.rs | 8 +- crates/search/src/project_search.rs | 4 +- crates/terminal_view/src/terminal_view.rs | 2 +- crates/workspace/src/workspace.rs | 104 ++++---- crates/zed/src/zed.rs | 6 +- 13 files changed, 300 insertions(+), 262 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 96921643d45..a114cd437b1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -64,7 +64,7 @@ fn test_edit_events(cx: &mut TestAppContext) { Editor::for_buffer(buffer.clone(), None, cx) } }) - .detach(cx); + .root(cx); let editor2 = cx .add_window({ let events = events.clone(); @@ -81,7 +81,7 @@ fn test_edit_events(cx: &mut TestAppContext) { Editor::for_buffer(buffer.clone(), None, cx) } }) - .detach(cx); + .root(cx); assert_eq!(mem::take(&mut *events.borrow_mut()), []); // Mutating editor 1 will emit an `Edited` event only for that editor. @@ -179,7 +179,7 @@ fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) { let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); let editor = cx .add_window(|cx| build_editor(buffer.clone(), cx)) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { editor.start_transaction_at(now, cx); @@ -354,7 +354,7 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); }); @@ -423,7 +423,7 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(2, 2), false, 1, cx); @@ -471,7 +471,7 @@ fn test_clone(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&text, cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { editor.change_selections(None, cx, |s| s.select_ranges(selection_ranges.clone())); @@ -489,7 +489,7 @@ fn test_clone(cx: &mut TestAppContext) { .update(cx, |editor, cx| { cx.add_window(Default::default(), |cx| editor.clone(cx)) }) - .detach(cx); + .root(cx); let snapshot = editor.update(cx, |e, cx| e.snapshot(cx)); let cloned_snapshot = cloned_editor.update(cx, |e, cx| e.snapshot(cx)); @@ -639,7 +639,7 @@ fn test_cancel(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); @@ -704,7 +704,7 @@ fn test_fold_action(cx: &mut TestAppContext) { ); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -774,7 +774,7 @@ fn test_move_cursor(cx: &mut TestAppContext) { let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx)); let view = cx .add_window(|cx| build_editor(buffer.clone(), cx)) - .detach(cx); + .root(cx); buffer.update(cx, |buffer, cx| { buffer.edit( @@ -854,7 +854,7 @@ fn test_move_cursor_multibyte(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcde\nαβγδε\n", cx); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); assert_eq!('ⓐ'.len_utf8(), 3); assert_eq!('α'.len_utf8(), 2); @@ -961,7 +961,7 @@ fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]); @@ -1013,7 +1013,7 @@ fn test_beginning_end_of_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\n def", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1178,7 +1178,7 @@ fn test_prev_next_word_boundary(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -1233,7 +1233,7 @@ fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) { MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.set_wrap_width(Some(140.), cx); @@ -1568,7 +1568,7 @@ fn test_delete_to_word_boundary(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("one two three four", cx); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1606,7 +1606,7 @@ fn test_newline(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -1651,7 +1651,7 @@ fn test_newline_with_old_selections(cx: &mut TestAppContext) { }); editor }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -1863,7 +1863,7 @@ fn test_insert_with_old_selections(cx: &mut TestAppContext) { editor.change_selections(None, cx, |s| s.select_ranges([3..4, 11..12, 19..20])); editor }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { // Edit the buffer directly, deleting ranges surrounding the editor's selections @@ -2375,7 +2375,7 @@ fn test_delete_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2400,7 +2400,7 @@ fn test_delete_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)]) @@ -2704,7 +2704,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2732,7 +2732,7 @@ fn test_duplicate_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -2761,7 +2761,7 @@ fn test_move_line_up_down(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -2862,7 +2862,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { let snapshot = editor.buffer.read(cx).snapshot(cx); editor.insert_blocks( @@ -3182,7 +3182,7 @@ fn test_select_all(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.select_all(&SelectAll, cx); assert_eq!( @@ -3201,7 +3201,7 @@ fn test_select_line(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { s.select_display_ranges([ @@ -3250,7 +3250,7 @@ fn test_split_selection_into_lines(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.fold_ranges( vec![ @@ -3323,7 +3323,7 @@ fn test_add_selection_above_below(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple("abc\ndefghi\n\njk\nlmno\n", cx); build_editor(buffer, cx) }) - .detach(cx); + .root(cx); view.update(cx, |view, cx| { view.change_selections(None, cx, |s| { @@ -3608,7 +3608,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(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)) .await; @@ -3771,7 +3771,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor .condition(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; @@ -4334,7 +4334,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(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)) .await; @@ -4482,7 +4482,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor .condition(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; @@ -4572,7 +4572,7 @@ async fn test_snippets(cx: &mut gpui::TestAppContext) { ); let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor.update(cx, |editor, cx| { let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap(); @@ -4702,7 +4702,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4814,7 +4814,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); assert!(cx.read(|cx| editor.is_dirty(cx))); @@ -4928,7 +4928,7 @@ async fn test_document_format_manual_trigger(cx: &mut gpui::TestAppContext) { let fake_server = fake_servers.next().await.unwrap(); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let editor = cx.add_window(|cx| build_editor(buffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(buffer, cx)).root(cx); editor.update(cx, |editor, cx| editor.set_text("one\ntwo\nthree\n", cx)); let format = editor.update(cx, |editor, cx| { @@ -5706,7 +5706,7 @@ fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); view.update(cx, |view, cx| { assert_eq!(view.text(cx), "aaaa\nbbbb"); view.change_selections(None, cx, |s| { @@ -5776,7 +5776,7 @@ fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) { multibuffer }); - let view = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); + let view = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); view.update(cx, |view, cx| { let (expected_text, selection_ranges) = marked_text_ranges( indoc! {" @@ -5869,7 +5869,7 @@ fn test_refresh_selections(cx: &mut TestAppContext) { ); editor }) - .detach(cx); + .root(cx); // Refreshing selections is a no-op when excerpts haven't changed. editor.update(cx, |editor, cx| { @@ -5950,7 +5950,7 @@ fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) { ); editor }) - .detach(cx); + .root(cx); multibuffer.update(cx, |multibuffer, cx| { multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx); @@ -6013,7 +6013,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { let buffer = cx.add_model(|cx| Buffer::new(0, text, cx).with_language(language, cx)); let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); - let view = cx.add_window(|cx| build_editor(buffer, cx)).detach(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)) .await; @@ -6054,7 +6054,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) { let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx); build_editor(buffer.clone(), cx) }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { struct Type1; @@ -6145,7 +6145,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { }); let leader = cx .add_window(|cx| build_editor(buffer.clone(), cx)) - .detach(cx); + .root(cx); let follower = cx .update(|cx| { cx.add_window( @@ -6156,7 +6156,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { |cx| build_editor(buffer.clone(), cx), ) }) - .detach(cx); + .root(cx); let is_still_following = Rc::new(RefCell::new(true)); let follower_edit_event_count = Rc::new(RefCell::new(0)); @@ -6289,7 +6289,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) { let project = Project::test(fs, ["/file.rs".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let leader = pane.update(cx, |_, cx| { @@ -7033,7 +7033,7 @@ async fn test_copilot_multibuffer( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); handle_copilot_completion_request( &copilot_lsp, @@ -7163,7 +7163,7 @@ async fn test_copilot_disabled_globs( ); multibuffer }); - let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).detach(cx); + let editor = cx.add_window(|cx| build_editor(multibuffer, cx)).root(cx); let mut copilot_requests = copilot_lsp .handle_request::(move |_params, _cx| async move { @@ -7244,7 +7244,7 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { project.update(cx, |project, _| project.languages().add(Arc::new(language))); let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index dc40e7fb856..2d4b273f5ef 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -3007,7 +3007,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx); Editor::new(EditorMode::Full, buffer, None, None, cx) }) - .detach(cx); + .root(cx); let element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let layouts = editor.update(cx, |editor, cx| { @@ -3028,7 +3028,7 @@ mod tests { let buffer = MultiBuffer::build_simple("", cx); Editor::new(EditorMode::Full, buffer, None, None, cx) }) - .detach(cx); + .root(cx); editor.update(cx, |editor, cx| { editor.set_placeholder_text("hello", cx); @@ -3240,7 +3240,7 @@ mod tests { let buffer = MultiBuffer::build_simple(&input_text, cx); Editor::new(editor_mode, buffer, None, None, cx) }) - .detach(cx); + .root(cx); let mut element = EditorElement::new(editor.read_with(cx, |editor, cx| editor.style(cx))); let (_, layout_state) = editor.update(cx, |editor, cx| { diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 089cbb29958..47a27c049f9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1138,7 +1138,7 @@ mod tests { let project = Project::test(fs, ["/a".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1840,7 +1840,7 @@ mod tests { project.update(cx, |project, _| project.languages().add(Arc::new(language))); let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -1995,7 +1995,7 @@ mod tests { }); let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -2083,7 +2083,7 @@ mod tests { cx.foreground().run_until_parked(); let editor = cx .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) - .detach(cx); + .root(cx); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); let closure_editor_edited = Arc::clone(&editor_edited); @@ -2337,7 +2337,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" }); let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() @@ -2384,7 +2384,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" cx.foreground().run_until_parked(); let editor = cx .add_window(|cx| Editor::for_multibuffer(multibuffer, Some(project.clone()), cx)) - .detach(cx); + .root(cx); let editor_edited = Arc::new(AtomicBool::new(false)); let fake_server = fake_servers.next().await.unwrap(); let closure_editor_edited = Arc::clone(&editor_edited); @@ -2574,7 +2574,7 @@ all hints should be invalidated and requeried for all of its visible excerpts" project.update(cx, |project, _| project.languages().add(Arc::new(language))); let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let worktree_id = workspace.update(cx, |workspace, cx| { workspace.project().read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 2c9d9c0c71e..12bf3242627 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -842,7 +842,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let finder = cx .add_window(|cx| { Picker::new( @@ -856,7 +856,7 @@ mod tests { cx, ) }) - .detach(cx); + .root(cx); let query = test_path_like("hi"); finder @@ -940,7 +940,7 @@ mod tests { .await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let finder = cx .add_window(|cx| { Picker::new( @@ -954,7 +954,7 @@ mod tests { cx, ) }) - .detach(cx); + .root(cx); finder .update(cx, |f, cx| { f.delegate_mut().spawn_search(test_path_like("hi"), cx) @@ -980,7 +980,7 @@ mod tests { .await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let finder = cx .add_window(|cx| { Picker::new( @@ -994,7 +994,7 @@ mod tests { cx, ) }) - .detach(cx); + .root(cx); // Even though there is only one worktree, that worktree's filename // is included in the matching, because the worktree is a single file. @@ -1051,7 +1051,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1078,7 +1078,7 @@ mod tests { cx, ) }) - .detach(cx); + .root(cx); finder .update(cx, |f, cx| { @@ -1117,7 +1117,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let finder = cx .add_window(|cx| { Picker::new( @@ -1131,7 +1131,7 @@ mod tests { cx, ) }) - .detach(cx); + .root(cx); finder .update(cx, |f, cx| { f.delegate_mut().spawn_search(test_path_like("dir"), cx) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 9b847e9c0cf..dce0b0e5f0c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -130,12 +130,12 @@ pub trait BorrowAppContext { } pub trait BorrowWindowContext { - type Return; + type Result; - fn read_with(&self, window_id: usize, f: F) -> Self::Return + fn read_window_with(&self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T; - fn update(&mut self, window_id: usize, f: F) -> Self::Return + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T; } @@ -458,6 +458,26 @@ impl BorrowAppContext for AsyncAppContext { } } +impl BorrowWindowContext for AsyncAppContext { + type Result = Option; + + fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + where + F: FnOnce(&WindowContext) -> T, + { + self.0.borrow().read_with(|cx| cx.read_window(window_id, f)) + } + + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + where + F: FnOnce(&mut WindowContext) -> T, + { + self.0 + .borrow_mut() + .update(|cx| cx.update_window(window_id, f)) + } +} + type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut WindowContext, usize); type GlobalActionCallback = dyn FnMut(&dyn Action, &mut AppContext); @@ -2162,6 +2182,24 @@ impl BorrowAppContext for AppContext { } } +impl BorrowWindowContext for AppContext { + type Result = Option; + + fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + where + F: FnOnce(&WindowContext) -> T, + { + AppContext::read_window(self, window_id, f) + } + + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + where + F: FnOnce(&mut WindowContext) -> T, + { + AppContext::update_window(self, window_id, f) + } +} + #[derive(Debug)] pub enum ParentId { View(usize), @@ -3360,14 +3398,18 @@ impl BorrowAppContext for ViewContext<'_, '_, V> { } impl BorrowWindowContext for ViewContext<'_, '_, V> { - type Return = T; + type Result = T; - fn read_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_with(&*self.window_context, window_id, f) + fn read_window_with T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window_with(&*self.window_context, window_id, f) } - fn update T>(&mut self, window_id: usize, f: F) -> T { - BorrowWindowContext::update(&mut *self.window_context, window_id, f) + fn update_window T>( + &mut self, + window_id: usize, + f: F, + ) -> T { + BorrowWindowContext::update_window(&mut *self.window_context, window_id, f) } } @@ -3467,14 +3509,18 @@ impl BorrowAppContext for LayoutContext<'_, '_, '_, V> { } impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { - type Return = T; + type Result = T; - fn read_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_with(&*self.view_context, window_id, f) + fn read_window_with T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window_with(&*self.view_context, window_id, f) } - fn update T>(&mut self, window_id: usize, f: F) -> T { - BorrowWindowContext::update(&mut *self.view_context, window_id, f) + fn update_window T>( + &mut self, + window_id: usize, + f: F, + ) -> T { + BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) } } @@ -3521,14 +3567,18 @@ impl BorrowAppContext for EventContext<'_, '_, '_, V> { } impl BorrowWindowContext for EventContext<'_, '_, '_, V> { - type Return = T; + type Result = T; - fn read_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_with(&*self.view_context, window_id, f) + fn read_window_with T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window_with(&*self.view_context, window_id, f) } - fn update T>(&mut self, window_id: usize, f: F) -> T { - BorrowWindowContext::update(&mut *self.view_context, window_id, f) + fn update_window T>( + &mut self, + window_id: usize, + f: F, + ) -> T { + BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) } } @@ -3830,32 +3880,16 @@ impl WindowHandle { self.any_handle.id() } - pub fn root(&self, cx: &impl BorrowAppContext) -> ViewHandle { + pub fn root(&self, cx: &C) -> C::Result> { self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap()) } - /// Keep this window open until it's explicitly closed. - // - // TODO: Implement window dropping behavior when we don't call this. - pub fn detach(mut self, cx: &impl BorrowAppContext) -> ViewHandle { - let root = self.root(cx); - let ref_counts = self.any_handle.ref_counts.take(); - #[cfg(any(test, feature = "test-support"))] - ref_counts - .unwrap() - .lock() - .leak_detector - .lock() - .handle_dropped(self.id(), self.any_handle.handle_id); - root - } - - pub fn read_with(&self, cx: &C, read: F) -> R + pub fn read_with(&self, cx: &C, read: F) -> C::Result where - C: BorrowAppContext, + C: BorrowWindowContext, F: FnOnce(&WindowContext) -> R, { - cx.read_with(|cx| cx.read_window(self.id(), read).unwrap()) + cx.read_window_with(self.id(), |cx| read(cx)) } pub fn update(&self, cx: &mut C, update: F) -> R @@ -3891,9 +3925,9 @@ impl WindowHandle { root_view.read(cx) } - pub fn read_root_with(&self, cx: &C, read: F) -> R + pub fn read_root_with(&self, cx: &C, read: F) -> C::Result where - C: BorrowAppContext, + C: BorrowWindowContext, F: FnOnce(&V, &ViewContext) -> R, { self.read_with(cx, |cx| { @@ -4021,25 +4055,25 @@ impl ViewHandle { cx.read_view(self) } - pub fn read_with(&self, cx: &C, read: F) -> C::Return + pub fn read_with(&self, cx: &C, read: F) -> C::Result where C: BorrowWindowContext, F: FnOnce(&T, &ViewContext) -> S, { - cx.read_with(self.window_id, |cx| { + cx.read_window_with(self.window_id, |cx| { let cx = ViewContext::immutable(cx, self.view_id); read(cx.read_view(self), &cx) }) } - pub fn update(&self, cx: &mut C, update: F) -> C::Return + pub fn update(&self, cx: &mut C, update: F) -> C::Result where C: BorrowWindowContext, F: FnOnce(&mut T, &mut ViewContext) -> S, { let mut update = Some(update); - cx.update(self.window_id, |cx| { + cx.update_window(self.window_id, |cx| { cx.update_view(self, &mut |view, cx| { let update = update.take().unwrap(); update(view, cx) @@ -5005,7 +5039,7 @@ mod tests { } #[crate::test(self)] - fn test_entity_release_hooks(cx: &mut AppContext) { + fn test_entity_release_hooks(cx: &mut TestAppContext) { struct Model { released: Rc>, } @@ -5048,7 +5082,7 @@ mod tests { let model = cx.add_model(|_| Model { released: model_released.clone(), }); - let window = cx.add_window(Default::default(), |_| View { + let window = cx.add_window(|_| View { released: view_released.clone(), }); let view = window.root(cx); @@ -5056,16 +5090,18 @@ mod tests { assert!(!model_released.get()); assert!(!view_released.get()); - cx.observe_release(&model, { - let model_release_observed = model_release_observed.clone(); - move |_, _| model_release_observed.set(true) - }) - .detach(); - cx.observe_release(&view, { - let view_release_observed = view_release_observed.clone(); - move |_, _| view_release_observed.set(true) - }) - .detach(); + cx.update(|cx| { + cx.observe_release(&model, { + let model_release_observed = model_release_observed.clone(); + move |_, _| model_release_observed.set(true) + }) + .detach(); + cx.observe_release(&view, { + let view_release_observed = view_release_observed.clone(); + move |_, _| view_release_observed.set(true) + }) + .detach(); + }); cx.update(move |_| { drop(model); @@ -5795,7 +5831,7 @@ mod tests { } #[crate::test(self)] - fn test_dispatch_action(cx: &mut AppContext) { + fn test_dispatch_action(cx: &mut TestAppContext) { struct ViewA { id: usize, child: Option, @@ -5846,68 +5882,70 @@ mod tests { impl_actions!(test, [Action]); let actions = Rc::new(RefCell::new(Vec::new())); - - cx.add_global_action({ - let actions = actions.clone(); - move |_: &Action, _: &mut AppContext| { - actions.borrow_mut().push("global".to_string()); - } - }); - - cx.add_action({ - let actions = actions.clone(); - move |view: &mut ViewA, action: &Action, cx| { - assert_eq!(action.0, "bar"); - cx.propagate_action(); - actions.borrow_mut().push(format!("{} a", view.id)); - } - }); - - cx.add_action({ - let actions = actions.clone(); - move |view: &mut ViewA, _: &Action, cx| { - if view.id != 1 { - cx.add_view(|cx| { - cx.propagate_action(); // Still works on a nested ViewContext - ViewB { id: 5, child: None } - }); - } - actions.borrow_mut().push(format!("{} b", view.id)); - } - }); - - cx.add_action({ - let actions = actions.clone(); - move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions.borrow_mut().push(format!("{} c", view.id)); - } - }); - - cx.add_action({ - let actions = actions.clone(); - move |view: &mut ViewB, _: &Action, cx| { - cx.propagate_action(); - actions.borrow_mut().push(format!("{} d", view.id)); - } - }); - - cx.capture_action({ - let actions = actions.clone(); - move |view: &mut ViewA, _: &Action, cx| { - cx.propagate_action(); - actions.borrow_mut().push(format!("{} capture", view.id)); - } - }); - let observed_actions = Rc::new(RefCell::new(Vec::new())); - cx.observe_actions({ - let observed_actions = observed_actions.clone(); - move |action_id, _| observed_actions.borrow_mut().push(action_id) - }) - .detach(); - let window = cx.add_window(Default::default(), |_| ViewA { id: 1, child: None }); + cx.update(|cx| { + cx.add_global_action({ + let actions = actions.clone(); + move |_: &Action, _: &mut AppContext| { + actions.borrow_mut().push("global".to_string()); + } + }); + + cx.add_action({ + let actions = actions.clone(); + move |view: &mut ViewA, action: &Action, cx| { + assert_eq!(action.0, "bar"); + cx.propagate_action(); + actions.borrow_mut().push(format!("{} a", view.id)); + } + }); + + cx.add_action({ + let actions = actions.clone(); + move |view: &mut ViewA, _: &Action, cx| { + if view.id != 1 { + cx.add_view(|cx| { + cx.propagate_action(); // Still works on a nested ViewContext + ViewB { id: 5, child: None } + }); + } + actions.borrow_mut().push(format!("{} b", view.id)); + } + }); + + cx.add_action({ + let actions = actions.clone(); + move |view: &mut ViewB, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} c", view.id)); + } + }); + + cx.add_action({ + let actions = actions.clone(); + move |view: &mut ViewB, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} d", view.id)); + } + }); + + cx.capture_action({ + let actions = actions.clone(); + move |view: &mut ViewA, _: &Action, cx| { + cx.propagate_action(); + actions.borrow_mut().push(format!("{} capture", view.id)); + } + }); + + cx.observe_actions({ + let observed_actions = observed_actions.clone(); + move |action_id, _| observed_actions.borrow_mut().push(action_id) + }) + .detach(); + }); + + let window = cx.add_window(|_| ViewA { id: 1, child: None }); let view_1 = window.root(cx); let view_2 = window.update(cx, |cx| { let child = cx.add_view(|_| ViewB { id: 2, child: None }); @@ -5956,7 +5994,7 @@ mod tests { // Remove view_1, which doesn't propagate the action - let window = cx.add_window(Default::default(), |_| ViewB { id: 2, child: None }); + let window = cx.add_window(|_| ViewB { id: 2, child: None }); let view_2 = window.root(cx); let view_3 = window.update(cx, |cx| { let child = cx.add_view(|_| ViewA { id: 3, child: None }); @@ -6457,7 +6495,7 @@ mod tests { } #[crate::test(self)] - fn test_refresh_windows(cx: &mut AppContext) { + fn test_refresh_windows(cx: &mut TestAppContext) { struct View(usize); impl super::Entity for View { @@ -6474,7 +6512,7 @@ mod tests { } } - let window = cx.add_window(Default::default(), |_| View(0)); + let window = cx.add_window(|_| View(0)); let root_view = window.root(cx); window.update(cx, |cx| { assert_eq!( diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 5c7947a4482..0165c52e9f3 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -406,16 +406,20 @@ impl BorrowAppContext for TestAppContext { } impl BorrowWindowContext for TestAppContext { - type Return = T; + type Result = T; - fn read_with T>(&self, window_id: usize, f: F) -> T { + fn read_window_with T>(&self, window_id: usize, f: F) -> T { self.cx .borrow() .read_window(window_id, f) .expect("window was closed") } - fn update T>(&mut self, window_id: usize, f: F) -> T { + fn update_window T>( + &mut self, + window_id: usize, + f: F, + ) -> T { self.cx .borrow_mut() .update_window(window_id, f) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 671d2b38c77..0149c310daa 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -142,9 +142,9 @@ impl BorrowAppContext for WindowContext<'_> { } impl BorrowWindowContext for WindowContext<'_> { - type Return = T; + type Result = T; - fn read_with T>(&self, window_id: usize, f: F) -> T { + fn read_window_with T>(&self, window_id: usize, f: F) -> T { if self.window_id == window_id { f(self) } else { @@ -152,7 +152,11 @@ impl BorrowWindowContext for WindowContext<'_> { } } - fn update T>(&mut self, window_id: usize, f: F) -> T { + fn update_window T>( + &mut self, + window_id: usize, + f: F, + ) -> T { if self.window_id == window_id { f(self) } else { diff --git a/crates/language_tools/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs index ce05a417ad8..d26000ebc73 100644 --- a/crates/language_tools/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -63,7 +63,7 @@ async fn test_lsp_logs(cx: &mut TestAppContext) { let log_view = cx .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)) - .detach(cx); + .root(cx); language_server.notify::(lsp::LogMessageParams { message: "hello from the server".into(), diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index fdc5ea108a8..021ea2d3bc3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1782,7 +1782,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -2327,7 +2327,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); panel.update(cx, |panel, cx| { @@ -2641,7 +2641,7 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); let new_search_events_count = Arc::new(AtomicUsize::new(0)); @@ -2730,7 +2730,7 @@ mod tests { let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); panel.update(cx, |panel, cx| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index e57edd3b147..0db66b4e378 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1449,7 +1449,7 @@ pub mod tests { let search = cx.add_model(|cx| ProjectSearch::new(project, cx)); let search_view = cx .add_window(|cx| ProjectSearchView::new(search.clone(), cx)) - .detach(cx); + .root(cx); search_view.update(cx, |search_view, cx| { search_view @@ -1754,7 +1754,7 @@ pub mod tests { }); let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let active_item = cx.read(|cx| { workspace diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 874978b4fc5..a600046ac28 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1072,7 +1072,7 @@ mod tests { let project = Project::test(params.fs.clone(), [], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); (project, workspace) } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 3222ea2eb8c..2efa9f8daac 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -793,68 +793,60 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; - let window = requesting_window_id - .and_then(|window_id| { - cx.update(|cx| { - cx.replace_root_view(window_id, |cx| { - Workspace::new( - workspace_id, - project_handle.clone(), - app_state.clone(), - cx, - ) - }) + let window = requesting_window_id.and_then(|window_id| { + cx.update(|cx| { + cx.replace_root_view(window_id, |cx| { + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) }) }) - .unwrap_or_else(|| { - let window_bounds_override = window_bounds_env_override(&cx); - let (bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(bounds), None) - } else { - serialized_workspace - .as_ref() - .and_then(|serialized_workspace| { - let display = serialized_workspace.display?; - let mut bounds = serialized_workspace.bounds?; + }); + let window = window.unwrap_or_else(|| { + let window_bounds_override = window_bounds_env_override(&cx); + let (bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(bounds), None) + } else { + serialized_workspace + .as_ref() + .and_then(|serialized_workspace| { + let display = serialized_workspace.display?; + let mut bounds = serialized_workspace.bounds?; - // Stored bounds are relative to the containing display. - // So convert back to global coordinates if that screen still exists - if let WindowBounds::Fixed(mut window_bounds) = bounds { - if let Some(screen) = cx.platform().screen_by_id(display) { - let screen_bounds = screen.bounds(); - window_bounds.set_origin_x( - window_bounds.origin_x() + screen_bounds.origin_x(), - ); - window_bounds.set_origin_y( - window_bounds.origin_y() + screen_bounds.origin_y(), - ); - bounds = WindowBounds::Fixed(window_bounds); - } else { - // Screen no longer exists. Return none here. - return None; - } + // Stored bounds are relative to the containing display. + // So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds.set_origin_x( + window_bounds.origin_x() + screen_bounds.origin_x(), + ); + window_bounds.set_origin_y( + window_bounds.origin_y() + screen_bounds.origin_y(), + ); + bounds = WindowBounds::Fixed(window_bounds); + } else { + // Screen no longer exists. Return none here. + return None; } + } - Some((bounds, display)) - }) - .unzip() - }; + Some((bounds, display)) + }) + .unzip() + }; - // Use the serialized workspace to construct the new window - cx.add_window( - (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| { - Workspace::new( - workspace_id, - project_handle.clone(), - app_state.clone(), - cx, - ) - }, - ) - }); + // Use the serialized workspace to construct the new window + cx.add_window( + (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), + |cx| { + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) + }, + ) + }); + + // We haven't yielded the main thread since obtaining the window handle, + // so the window exists. + let workspace = window.root(&cx).unwrap(); - let workspace = window.root(&cx); (app_state.initialize_workspace)( workspace.downgrade(), serialized_workspace.is_some(), @@ -3985,7 +3977,7 @@ pub fn join_remote_project( ), |cx| Workspace::new(0, project, app_state.clone(), cx), ); - let workspace = window.root(&cx); + let workspace = window.root(&cx).unwrap(); (app_state.initialize_workspace)( workspace.downgrade(), false, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1770c5648e1..a459122cfc9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -985,7 +985,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1566,7 +1566,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .detach(cx); + .root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); @@ -1845,7 +1845,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let workspace = cx .add_window(|cx| Workspace::test_new(project, cx)) - .detach(cx); + .root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); From 485c0a482ee8e7b2a2014f1129ca060d4554af0c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 3 Aug 2023 17:11:47 -0600 Subject: [PATCH 038/105] Don't refcount window handles --- crates/collab/src/tests/integration_tests.rs | 4 +- .../src/incoming_call_notification.rs | 2 +- .../src/project_shared_notification.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/copilot/src/sign_in.rs | 4 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/editor_tests.rs | 2 +- .../src/test/editor_lsp_test_context.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 2 +- crates/file_finder/src/file_finder.rs | 22 ++-- crates/gpui/src/app.rs | 100 ++++-------------- crates/gpui/src/app/ref_counts.rs | 22 ---- crates/gpui/src/app/test_app_context.rs | 4 +- crates/gpui/src/app/window.rs | 2 +- crates/project_panel/src/project_panel.rs | 8 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 10 +- crates/search/src/project_search.rs | 4 +- crates/workspace/src/workspace.rs | 36 +++---- crates/zed/src/zed.rs | 12 +-- 20 files changed, 83 insertions(+), 163 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 1a8e6d938d6..037f97f71ab 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3446,7 +3446,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a, - window_id: window_a.id(), + window_id: window_a.window_id(), editor: editor_a, }; @@ -3459,7 +3459,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b, - window_id: window_b.id(), + window_id: window_b.window_id(), editor: editor_b, }; diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index a9c5e697a5f..770f5d5795c 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -49,7 +49,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), ); - notification_windows.push(window.id()); + notification_windows.push(window.window_id()); } } } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 03ab91623b4..5be7e33bf3c 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -52,7 +52,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_windows .entry(*project_id) .or_insert(Vec::new()) - .push(window.id()); + .push(window.window_id()); } } room::Event::RemoteProjectUnshared { project_id } => { diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 7d4b4126b70..935358c2a12 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -297,7 +297,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let editor = cx.add_view(window_id, |cx| { let mut editor = Editor::single_line(None, cx); editor.set_text("abc", cx); diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 659bee7445f..0d5bb28ed68 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -25,7 +25,7 @@ pub fn init(cx: &mut AppContext) { match &status { crate::Status::SigningIn { prompt } => { if let Some(code_verification_handle) = code_verification.as_mut() { - let window_id = code_verification_handle.id(); + let window_id = code_verification_handle.window_id(); let updated = cx.update_window(window_id, |cx| { code_verification_handle.update_root(cx, |code_verification, cx| { code_verification.set_status(status.clone(), cx) @@ -41,7 +41,7 @@ pub fn init(cx: &mut AppContext) { } Status::Authorized | Status::Unauthorized => { if let Some(code_verification) = code_verification.as_ref() { - let window_id = code_verification.id(); + let window_id = code_verification.window_id(); cx.update_window(window_id, |cx| { code_verification.update_root(cx, |code_verification, cx| { code_verification.set_status(status, cx) diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2444465be66..f2db0a77630 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -857,7 +857,7 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); // Create some diagnostics project.update(cx, |project, cx| { @@ -1252,7 +1252,7 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let view = cx.add_view(window_id, |cx| { ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a114cd437b1..e8913505cab 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -525,7 +525,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let project = Project::test(fs, [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); cx.add_view(window_id, |cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index f53115f224a..d25ca7bb88a 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -99,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> { Self { cx: EditorTestContext { cx, - window_id: window.id(), + window_id: window.window_id(), editor, }, lsp, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index c7ea1b4f385..ac519764fdd 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -39,7 +39,7 @@ impl<'a> EditorTestContext<'a> { let editor = window.root(cx); Self { cx, - window_id: window.id(), + window_id: window.window_id(), editor, } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 12bf3242627..84a45b083e6 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -619,7 +619,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.window_id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder @@ -632,8 +632,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.window_id(), SelectNext); + cx.dispatch_action(window.window_id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -674,7 +674,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.window_id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -706,8 +706,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.window_id(), SelectNext); + cx.dispatch_action(window.window_id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -758,7 +758,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.window_id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -790,8 +790,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.window_id(), SelectNext); + cx.dispatch_action(window.window_id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -1168,7 +1168,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1376,7 +1376,7 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index dce0b0e5f0c..90f910d255f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1337,7 +1337,7 @@ impl AppContext { .open_window(window_id, window_options, this.foreground.clone()); let window = this.build_window(window_id, platform_window, build_root_view); this.windows.insert(window_id, window); - WindowHandle::new(window_id, this.ref_counts.clone()) + WindowHandle::new(window_id) }) } @@ -3863,21 +3863,21 @@ impl Clone for WeakModelHandle { impl Copy for WeakModelHandle {} pub struct WindowHandle { - any_handle: AnyWindowHandle, - view_type: PhantomData, + window_id: usize, + root_view_type: PhantomData, } #[allow(dead_code)] impl WindowHandle { - fn new(window_id: usize, ref_counts: Arc>) -> Self { + fn new(window_id: usize) -> Self { WindowHandle { - any_handle: AnyWindowHandle::new::(window_id, ref_counts), - view_type: PhantomData, + window_id, + root_view_type: PhantomData, } } - pub fn id(&self) -> usize { - self.any_handle.id() + pub fn window_id(&self) -> usize { + self.window_id } pub fn root(&self, cx: &C) -> C::Result> { @@ -3889,7 +3889,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&WindowContext) -> R, { - cx.read_window_with(self.id(), |cx| read(cx)) + cx.read_window_with(self.window_id(), |cx| read(cx)) } pub fn update(&self, cx: &mut C, update: F) -> R @@ -3897,7 +3897,7 @@ impl WindowHandle { C: BorrowAppContext, F: FnOnce(&mut WindowContext) -> R, { - cx.update(|cx| cx.update_window(self.id(), update).unwrap()) + cx.update(|cx| cx.update_window(self.window_id(), update).unwrap()) } pub fn update_root(&self, cx: &mut C, update: F) -> R @@ -3905,7 +3905,7 @@ impl WindowHandle { C: BorrowAppContext, F: FnOnce(&mut V, &mut ViewContext) -> R, { - let window_id = self.id(); + let window_id = self.window_id(); cx.update(|cx| { cx.update_window(window_id, |cx| { cx.root_view() @@ -3920,7 +3920,9 @@ impl WindowHandle { pub fn read_root<'a>(&self, cx: &'a AppContext) -> &'a V { let root_view = cx - .read_window(self.id(), |cx| cx.root_view().clone().downcast().unwrap()) + .read_window(self.window_id(), |cx| { + cx.root_view().clone().downcast().unwrap() + }) .unwrap(); root_view.read(cx) } @@ -3948,66 +3950,6 @@ impl WindowHandle { } } -pub struct AnyWindowHandle { - window_id: usize, - root_view_type: TypeId, - ref_counts: Option>>, - - #[cfg(any(test, feature = "test-support"))] - handle_id: usize, -} - -impl AnyWindowHandle { - fn new(window_id: usize, ref_counts: Arc>) -> Self { - ref_counts.lock().inc_window(window_id); - - #[cfg(any(test, feature = "test-support"))] - let handle_id = ref_counts - .lock() - .leak_detector - .lock() - .handle_created(None, window_id); - - Self { - window_id, - root_view_type: TypeId::of::(), - ref_counts: Some(ref_counts), - #[cfg(any(test, feature = "test-support"))] - handle_id, - } - } - - pub fn id(&self) -> usize { - self.window_id - } - - pub fn downcast(self) -> Option> { - if TypeId::of::() == self.root_view_type { - Some(WindowHandle { - any_handle: self, - view_type: PhantomData, - }) - } else { - None - } - } -} - -impl Drop for AnyWindowHandle { - fn drop(&mut self) { - if let Some(ref_counts) = self.ref_counts.as_ref() { - ref_counts.lock().dec_window(self.window_id); - - #[cfg(any(test, feature = "test-support"))] - ref_counts - .lock() - .leak_detector - .lock() - .handle_dropped(self.window_id, self.handle_id); - } - } -} - #[repr(transparent)] pub struct ViewHandle { any_handle: AnyViewHandle, @@ -6281,7 +6223,7 @@ mod tests { // Check that global actions do not have a binding, even if a binding does exist in another view assert_eq!( - &available_actions(window.id(), view_1.id(), cx), + &available_actions(window.window_id(), view_1.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::GlobalAction", vec![]) @@ -6290,7 +6232,7 @@ mod tests { // Check that view 1 actions and bindings are available even when called from view 2 assert_eq!( - &available_actions(window.id(), view_2.id(), cx), + &available_actions(window.window_id(), view_2.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::Action2", vec![Keystroke::parse("b").unwrap()]), @@ -6353,7 +6295,7 @@ mod tests { ]); }); - let actions = cx.available_actions(window.id(), view.id()); + let actions = cx.available_actions(window.window_id(), view.id()); assert_eq!( actions[0].1.as_any().downcast_ref::(), Some(&ActionWithArg { arg: false }) @@ -6639,25 +6581,25 @@ mod tests { [("window 2", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_2.id())); + cx.simulate_window_activation(Some(window_2.window_id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 3", false), ("window 2", true)] ); - cx.simulate_window_activation(Some(window_1.id())); + cx.simulate_window_activation(Some(window_1.window_id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 2", false), ("window 1", true)] ); - cx.simulate_window_activation(Some(window_3.id())); + cx.simulate_window_activation(Some(window_3.window_id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 1", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_3.id())); + cx.simulate_window_activation(Some(window_3.window_id())); assert_eq!(mem::take(&mut *events.borrow_mut()), []); } diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs index 74563d05bc0..f0c1699f165 100644 --- a/crates/gpui/src/app/ref_counts.rs +++ b/crates/gpui/src/app/ref_counts.rs @@ -25,7 +25,6 @@ struct ElementStateRefCount { pub struct RefCounts { entity_counts: HashMap, element_state_counts: HashMap, - dropped_windows: HashSet, dropped_models: HashSet, dropped_views: HashSet<(usize, usize)>, dropped_element_states: HashSet, @@ -44,18 +43,6 @@ impl RefCounts { } } - pub fn inc_window(&mut self, window_id: usize) { - match self.entity_counts.entry(window_id) { - Entry::Occupied(mut entry) => { - *entry.get_mut() += 1; - } - Entry::Vacant(entry) => { - entry.insert(1); - self.dropped_windows.remove(&window_id); - } - } - } - pub fn inc_model(&mut self, model_id: usize) { match self.entity_counts.entry(model_id) { Entry::Occupied(mut entry) => { @@ -98,15 +85,6 @@ impl RefCounts { } } - pub fn dec_window(&mut self, window_id: usize) { - let count = self.entity_counts.get_mut(&window_id).unwrap(); - *count -= 1; - if *count == 0 { - self.entity_counts.remove(&window_id); - self.dropped_windows.insert(window_id); - } - } - pub fn dec_model(&mut self, model_id: usize) { let count = self.entity_counts.get_mut(&model_id).unwrap(); *count -= 1; diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 0165c52e9f3..3b574eb03ee 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -157,9 +157,9 @@ impl TestAppContext { .cx .borrow_mut() .add_window(Default::default(), build_root_view); - self.simulate_window_activation(Some(window.id())); + self.simulate_window_activation(Some(window.window_id())); - WindowHandle::new(window.id(), self.cx.borrow_mut().ref_counts.clone()) + WindowHandle::new(window.window_id()) } pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 0149c310daa..789341d46fa 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -1165,7 +1165,7 @@ impl<'a> WindowContext<'a> { let root_view = self.add_view(|cx| build_root_view(cx)); self.window.focused_view_id = Some(root_view.id()); self.window.root_view = Some(root_view.into_any()); - WindowHandle::new(self.window_id, self.ref_counts.clone()) + WindowHandle::new(self.window_id) } pub fn add_view(&mut self, build_view: F) -> ViewHandle diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 021ea2d3bc3..80847f9f4b9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1872,7 +1872,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2225,7 +2225,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2402,7 +2402,7 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); toggle_expand_dir(&panel, "src/test", cx); @@ -2493,7 +2493,7 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "src/", cx); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 8471f3a3a70..4bd186fc98b 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,7 +328,7 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); // Create the project symbols view. let symbols = cx.add_view(window_id, |cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 1e635432bd5..265e4f02061 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -851,11 +851,11 @@ mod tests { }); let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window.id(), |cx| { + let editor = cx.add_view(window.window_id(), |cx| { Editor::for_buffer(buffer.clone(), None, cx) }); - let search_bar = cx.add_view(window.id(), |cx| { + let search_bar = cx.add_view(window.window_id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1232,7 +1232,7 @@ mod tests { ); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let window_id = window.id(); + let window_id = window.window_id(); let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1421,11 +1421,11 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window.id(), |cx| { + let editor = cx.add_view(window.window_id(), |cx| { Editor::for_buffer(buffer.clone(), None, cx) }); - let search_bar = cx.add_view(window.id(), |cx| { + let search_bar = cx.add_view(window.window_id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0db66b4e378..febd564050d 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1568,7 +1568,7 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let active_item = cx.read(|cx| { workspace @@ -1874,7 +1874,7 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); workspace.update(cx, |workspace, cx| { ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2efa9f8daac..09e4c4c2193 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4196,14 +4196,14 @@ mod tests { ); }); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + cx.current_window_title(window.window_id()).as_deref(), Some("one.txt — root1") ); // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + cx.current_window_title(window.window_id()).as_deref(), Some("two.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4222,7 +4222,7 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + cx.current_window_title(window.window_id()).as_deref(), Some("one.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4242,14 +4242,14 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + cx.current_window_title(window.window_id()).as_deref(), Some("one.txt — root1, root2") ); // Remove a project folder project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + cx.current_window_title(window.window_id()).as_deref(), Some("one.txt — root2") ); } @@ -4285,9 +4285,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - cx.simulate_prompt_answer(window.id(), 2 /* cancel */); + cx.simulate_prompt_answer(window.window_id(), 2 /* cancel */); cx.foreground().run_until_parked(); - assert!(!cx.has_pending_prompt(window.id())); + assert!(!cx.has_pending_prompt(window.window_id())); assert!(!task.await.unwrap()); } @@ -4346,10 +4346,10 @@ mod tests { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(cx.has_pending_prompt(window.window_id())); // Confirm saving item 1. - cx.simulate_prompt_answer(window.id(), 0); + cx.simulate_prompt_answer(window.window_id(), 0); cx.foreground().run_until_parked(); // Item 1 is saved. There's a prompt to save item 3. @@ -4360,10 +4360,10 @@ mod tests { assert_eq!(pane.items_len(), 3); assert_eq!(pane.active_item().unwrap().id(), item3.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(cx.has_pending_prompt(window.window_id())); // Cancel saving item 3. - cx.simulate_prompt_answer(window.id(), 1); + cx.simulate_prompt_answer(window.window_id(), 1); cx.foreground().run_until_parked(); // Item 3 is reloaded. There's a prompt to save item 4. @@ -4374,10 +4374,10 @@ mod tests { assert_eq!(pane.items_len(), 2); assert_eq!(pane.active_item().unwrap().id(), item4.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(cx.has_pending_prompt(window.window_id())); // Confirm saving item 4. - cx.simulate_prompt_answer(window.id(), 0); + cx.simulate_prompt_answer(window.window_id(), 0); cx.foreground().run_until_parked(); // There's a prompt for a path for item 4. @@ -4480,7 +4480,7 @@ mod tests { &[ProjectEntryId::from_proto(0)] ); }); - cx.simulate_prompt_answer(window.id(), 0); + cx.simulate_prompt_answer(window.window_id(), 0); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { @@ -4489,7 +4489,7 @@ mod tests { &[ProjectEntryId::from_proto(2)] ); }); - cx.simulate_prompt_answer(window.id(), 0); + cx.simulate_prompt_answer(window.window_id(), 0); cx.foreground().run_until_parked(); close.await.unwrap(); @@ -4549,7 +4549,7 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - cx.simulate_window_activation(Some(window.id())); + cx.simulate_window_activation(Some(window.window_id())); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; @@ -4591,7 +4591,7 @@ mod tests { pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); - assert!(!cx.has_pending_prompt(window.id())); + assert!(!cx.has_pending_prompt(window.window_id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. @@ -4612,7 +4612,7 @@ mod tests { let _close_items = pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); - assert!(cx.has_pending_prompt(window.id())); + assert!(cx.has_pending_prompt(window.window_id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a459122cfc9..1c65317c413 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1299,7 +1299,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); // Open a file within an existing worktree. workspace @@ -1342,7 +1342,7 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer @@ -1437,7 +1437,7 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); // Create a new untitled buffer cx.dispatch_action(window_id, NewFile); @@ -1490,7 +1490,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); + let window_id = window.window_id(); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -2088,7 +2088,7 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.id(); + let window_id = window.window_id(); // Test loading the keymap base at all assert_key_bindings_for( @@ -2259,7 +2259,7 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.id(); + let window_id = window.window_id(); // Test loading the keymap base at all assert_key_bindings_for( From 2d96388be369259c5e8b6006bfe81cfe979889a3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Thu, 3 Aug 2023 17:46:34 -0600 Subject: [PATCH 039/105] Use WindowHandles in a couple places --- crates/copilot/src/sign_in.rs | 44 +++++++++++++++++------------------ crates/gpui/src/app.rs | 43 +++++++++++++++------------------- 2 files changed, 41 insertions(+), 46 deletions(-) diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 0d5bb28ed68..d03a2d393bf 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -18,42 +18,42 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { - let mut code_verification: Option> = None; + let mut verification_window: Option> = None; cx.observe(&copilot, move |copilot, cx| { let status = copilot.read(cx).status(); match &status { crate::Status::SigningIn { prompt } => { - if let Some(code_verification_handle) = code_verification.as_mut() { - let window_id = code_verification_handle.window_id(); - let updated = cx.update_window(window_id, |cx| { - code_verification_handle.update_root(cx, |code_verification, cx| { - code_verification.set_status(status.clone(), cx) - }); - cx.activate_window(); - }); - if updated.is_none() { - code_verification = Some(create_copilot_auth_window(cx, &status)); + if let Some(window) = verification_window.as_mut() { + let updated = window + .root(cx) + .map(|root| { + root.update(cx, |verification, cx| { + verification.set_status(status.clone(), cx); + cx.activate_window(); + }) + }) + .is_some(); + if !updated { + verification_window = Some(create_copilot_auth_window(cx, &status)); } } else if let Some(_prompt) = prompt { - code_verification = Some(create_copilot_auth_window(cx, &status)); + verification_window = Some(create_copilot_auth_window(cx, &status)); } } Status::Authorized | Status::Unauthorized => { - if let Some(code_verification) = code_verification.as_ref() { - let window_id = code_verification.window_id(); - cx.update_window(window_id, |cx| { - code_verification.update_root(cx, |code_verification, cx| { - code_verification.set_status(status, cx) + if let Some(window) = verification_window.as_ref() { + if let Some(verification) = window.root(cx) { + verification.update(cx, |verification, cx| { + verification.set_status(status, cx); + cx.platform().activate(true); + cx.activate_window(); }); - - cx.platform().activate(true); - cx.activate_window(); - }); + } } } _ => { - if let Some(code_verification) = code_verification.take() { + if let Some(code_verification) = verification_window.take() { code_verification.update(cx, |cx| cx.remove_window()); } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 90f910d255f..d98033820bb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -808,7 +808,7 @@ impl AppContext { result } - pub fn read_window T>( + fn read_window T>( &self, window_id: usize, callback: F, @@ -3892,31 +3892,26 @@ impl WindowHandle { cx.read_window_with(self.window_id(), |cx| read(cx)) } - pub fn update(&self, cx: &mut C, update: F) -> R + pub fn update(&self, cx: &mut C, update: F) -> C::Result where - C: BorrowAppContext, + C: BorrowWindowContext, F: FnOnce(&mut WindowContext) -> R, { - cx.update(|cx| cx.update_window(self.window_id(), update).unwrap()) + cx.update_window(self.window_id(), update) } - pub fn update_root(&self, cx: &mut C, update: F) -> R - where - C: BorrowAppContext, - F: FnOnce(&mut V, &mut ViewContext) -> R, - { - let window_id = self.window_id(); - cx.update(|cx| { - cx.update_window(window_id, |cx| { - cx.root_view() - .clone() - .downcast::() - .unwrap() - .update(cx, update) - }) - .unwrap() - }) - } + // pub fn update_root(&self, cx: &mut C, update: F) -> C::Result> + // where + // C: BorrowWindowContext, + // F: FnOnce(&mut V, &mut ViewContext) -> R, + // { + // cx.update_window(self.window_id, |cx| { + // cx.root_view() + // .clone() + // .downcast::() + // .map(|v| v.update(cx, update)) + // }) + // } pub fn read_root<'a>(&self, cx: &'a AppContext) -> &'a V { let root_view = cx @@ -3940,9 +3935,9 @@ impl WindowHandle { }) } - pub fn add_view(&self, cx: &mut C, build_view: F) -> ViewHandle + pub fn add_view(&self, cx: &mut C, build_view: F) -> C::Result> where - C: BorrowAppContext, + C: BorrowWindowContext, U: View, F: FnOnce(&mut ViewContext) -> U, { @@ -4836,7 +4831,7 @@ mod tests { let called_defer = Rc::new(AtomicBool::new(false)); let called_after_window_update = Rc::new(AtomicBool::new(false)); - window.update_root(cx, |this, cx| { + window.root(cx).update(cx, |this, cx| { assert_eq!(this.render_count, 1); cx.defer({ let called_defer = called_defer.clone(); From 8c98b02e457ffb2fb0f457e9e2834ebbc9bebdd7 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 4 Aug 2023 15:09:08 -0400 Subject: [PATCH 040/105] Add `convert to {upper,lower} case` commands Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/editor/src/editor.rs | 66 +++++++++++++++++++++++++++++++ crates/editor/src/editor_tests.rs | 59 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index a4d9259a6d7..ab952ba6e72 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -231,6 +231,8 @@ actions!( SortLinesCaseInsensitive, ReverseLines, ShuffleLines, + ConvertToUpperCase, + ConvertToLowerCase, Transpose, Cut, Copy, @@ -353,6 +355,8 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::sort_lines_case_insensitive); cx.add_action(Editor::reverse_lines); cx.add_action(Editor::shuffle_lines); + cx.add_action(Editor::convert_to_upper_case); + cx.add_action(Editor::convert_to_lower_case); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -4306,6 +4310,68 @@ impl Editor { }); } + pub fn convert_to_upper_case(&mut self, _: &ConvertToUpperCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_uppercase()) + } + + pub fn convert_to_lower_case(&mut self, _: &ConvertToLowerCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_lowercase()) + } + + fn manipulate_text(&mut self, cx: &mut ViewContext, mut callback: Fn) + where + Fn: FnMut(&str) -> String, + { + let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); + let buffer = self.buffer.read(cx).snapshot(cx); + + let mut new_selections = Vec::new(); + let mut edits = Vec::new(); + for selection in self.selections.all::(cx) { + let selection_is_empty = selection.is_empty(); + + let (start, end) = if selection_is_empty { + let word_range = movement::surrounding_word( + &display_map, + selection.start.to_display_point(&display_map), + ); + let start = word_range.start.to_offset(&display_map, Bias::Left); + let end = word_range.end.to_offset(&display_map, Bias::Left); + (start, end) + } else { + (selection.start, selection.end) + }; + + let text = buffer.text_for_range(start..end).collect::(); + let text = callback(&text); + + if selection_is_empty { + new_selections.push(selection); + } else { + new_selections.push(Selection { + start, + end: start + text.len(), + goal: SelectionGoal::None, + ..selection + }); + } + + edits.push((start..end, text)); + } + + self.transact(cx, |this, cx| { + this.buffer.update(cx, |buffer, cx| { + buffer.edit(edits, None, cx); + }); + + this.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select(new_selections); + }); + + this.request_autoscroll(Autoscroll::fit(), cx); + }); + } + pub fn duplicate_line(&mut self, _: &DuplicateLine, cx: &mut ViewContext) { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); let buffer = &display_map.buffer_snapshot; diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e8913505cab..11d64f085cd 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2695,6 +2695,65 @@ async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) { "}); } +#[gpui::test] +async fn test_manipulate_text(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + let mut cx = EditorTestContext::new(cx).await; + + // Test convert_to_upper_case() + cx.set_state(indoc! {" + «hello worldˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «HELLO WORLDˇ» + "}); + + // Test convert_to_lower_case() + cx.set_state(indoc! {" + «HELLO WORLDˇ» + "}); + cx.update_editor(|e, cx| e.convert_to_lower_case(&ConvertToLowerCase, cx)); + cx.assert_editor_state(indoc! {" + «hello worldˇ» + "}); + + // From here on out, test more complex cases of manipulate_text() with a single driver method: convert_to_upper_case() + + // Test no selection case - should affect words cursors are in + // Cursor at beginning, middle, and end of word + cx.set_state(indoc! {" + ˇhello big beauˇtiful worldˇ + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + ˇHELLO big BEAUˇTIFUL WORLDˇ + "}); + + // Test multiple selections on a single line and across multiple lines + cx.set_state(indoc! {" + «Theˇ» quick «brown + foxˇ» jumps «overˇ» + the «lazyˇ» dog + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «THEˇ» quick «BROWN + FOXˇ» jumps «OVERˇ» + the «LAZYˇ» dog + "}); + + // Test case where text length grows + cx.set_state(indoc! {" + «tschüߡ» + "}); + cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); + cx.assert_editor_state(indoc! {" + «TSCHÜSSˇ» + "}); +} + #[gpui::test] fn test_duplicate_line(cx: &mut TestAppContext) { init_test(cx, |_| {}); From 12e8f417e4ca3ff1d7aa019825b40c3b3d779483 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Fri, 4 Aug 2023 22:37:29 -0400 Subject: [PATCH 041/105] Add more convert to case commands ConvertToTitleCase ConvertToSnakeCase ConvertToKebabCase ConvertToUpperCamelCase ConvertToLowerCamelCase --- Cargo.lock | 10 ++++++++++ crates/editor/Cargo.toml | 3 ++- crates/editor/src/editor.rs | 39 +++++++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fbf4e750c62..5afa1ebe625 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,15 @@ dependencies = [ "theme", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "copilot" version = "0.1.0" @@ -2314,6 +2323,7 @@ dependencies = [ "clock", "collections", "context_menu", + "convert_case", "copilot", "ctor", "db", diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index bc1c9044049..2fdeee56c18 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -47,6 +47,7 @@ workspace = { path = "../workspace" } aho-corasick = "0.7" anyhow.workspace = true +convert_case = "0.6.0" futures.workspace = true indoc = "1.0.4" itertools = "0.10" @@ -56,12 +57,12 @@ ordered-float.workspace = true parking_lot.workspace = true postage.workspace = true pulldown-cmark = { version = "0.9.2", default-features = false } +rand.workspace = true schemars.workspace = true serde.workspace = true serde_derive.workspace = true smallvec.workspace = true smol.workspace = true -rand.workspace = true tree-sitter-rust = { workspace = true, optional = true } tree-sitter-html = { workspace = true, optional = true } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index ab952ba6e72..c70a1a8e048 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,6 +28,7 @@ use blink_manager::BlinkManager; use client::{ClickhouseEvent, TelemetrySettings}; use clock::{Global, ReplicaId}; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; +use convert_case::{Case, Casing}; use copilot::Copilot; pub use display_map::DisplayPoint; use display_map::*; @@ -233,6 +234,11 @@ actions!( ShuffleLines, ConvertToUpperCase, ConvertToLowerCase, + ConvertToTitleCase, + ConvertToSnakeCase, + ConvertToKebabCase, + ConvertToUpperCamelCase, + ConvertToLowerCamelCase, Transpose, Cut, Copy, @@ -357,6 +363,11 @@ pub fn init(cx: &mut AppContext) { cx.add_action(Editor::shuffle_lines); cx.add_action(Editor::convert_to_upper_case); cx.add_action(Editor::convert_to_lower_case); + cx.add_action(Editor::convert_to_title_case); + cx.add_action(Editor::convert_to_snake_case); + cx.add_action(Editor::convert_to_kebab_case); + cx.add_action(Editor::convert_to_upper_camel_case); + cx.add_action(Editor::convert_to_lower_camel_case); cx.add_action(Editor::delete_to_previous_word_start); cx.add_action(Editor::delete_to_previous_subword_start); cx.add_action(Editor::delete_to_next_word_end); @@ -4318,6 +4329,34 @@ impl Editor { self.manipulate_text(cx, |text| text.to_lowercase()) } + pub fn convert_to_title_case(&mut self, _: &ConvertToTitleCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_case(Case::Title)) + } + + pub fn convert_to_snake_case(&mut self, _: &ConvertToSnakeCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_case(Case::Snake)) + } + + pub fn convert_to_kebab_case(&mut self, _: &ConvertToKebabCase, cx: &mut ViewContext) { + self.manipulate_text(cx, |text| text.to_case(Case::Kebab)) + } + + pub fn convert_to_upper_camel_case( + &mut self, + _: &ConvertToUpperCamelCase, + cx: &mut ViewContext, + ) { + self.manipulate_text(cx, |text| text.to_case(Case::UpperCamel)) + } + + pub fn convert_to_lower_camel_case( + &mut self, + _: &ConvertToLowerCamelCase, + cx: &mut ViewContext, + ) { + self.manipulate_text(cx, |text| text.to_case(Case::Camel)) + } + fn manipulate_text(&mut self, cx: &mut ViewContext, mut callback: Fn) where Fn: FnMut(&str) -> String, From 1abb6a0176feb0b1fe78553a08a2f6675654a8cf Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 5 Aug 2023 11:31:21 -0400 Subject: [PATCH 042/105] Expand empty selections to cover full word and fix bugs --- crates/editor/src/editor.rs | 21 +++++++++++---------- crates/editor/src/editor_tests.rs | 23 +++++++++++++++++++++-- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c70a1a8e048..cd5e86b9109 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -4366,6 +4366,8 @@ impl Editor { let mut new_selections = Vec::new(); let mut edits = Vec::new(); + let mut selection_adjustment = 0i32; + for selection in self.selections.all::(cx) { let selection_is_empty = selection.is_empty(); @@ -4382,18 +4384,17 @@ impl Editor { }; let text = buffer.text_for_range(start..end).collect::(); + let old_length = text.len() as i32; let text = callback(&text); - if selection_is_empty { - new_selections.push(selection); - } else { - new_selections.push(Selection { - start, - end: start + text.len(), - goal: SelectionGoal::None, - ..selection - }); - } + new_selections.push(Selection { + start: (start as i32 - selection_adjustment) as usize, + end: ((start + text.len()) as i32 - selection_adjustment) as usize, + goal: SelectionGoal::None, + ..selection + }); + + selection_adjustment += old_length - text.len() as i32; edits.push((start..end, text)); } diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 11d64f085cd..7e39647ac63 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -2719,7 +2719,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { «hello worldˇ» "}); - // From here on out, test more complex cases of manipulate_text() with a single driver method: convert_to_upper_case() + // From here on out, test more complex cases of manipulate_text() // Test no selection case - should affect words cursors are in // Cursor at beginning, middle, and end of word @@ -2728,7 +2728,7 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { "}); cx.update_editor(|e, cx| e.convert_to_upper_case(&ConvertToUpperCase, cx)); cx.assert_editor_state(indoc! {" - ˇHELLO big BEAUˇTIFUL WORLDˇ + «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ» "}); // Test multiple selections on a single line and across multiple lines @@ -2752,6 +2752,25 @@ async fn test_manipulate_text(cx: &mut TestAppContext) { cx.assert_editor_state(indoc! {" «TSCHÜSSˇ» "}); + + // Test to make sure we don't crash when text shrinks + cx.set_state(indoc! {" + aaa_bbbˇ + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» + "}); + + // Test to make sure we all aware of the fact that each word can grow and shrink + // Final selections should be aware of this fact + cx.set_state(indoc! {" + aaa_bˇbb bbˇb_ccc ˇccc_ddd + "}); + cx.update_editor(|e, cx| e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, cx)); + cx.assert_editor_state(indoc! {" + «aaaBbbˇ» «bbbCccˇ» «cccDddˇ» + "}); } #[gpui::test] From dcf8b00656eab32299338c2a63b69dc128fff19d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 5 Aug 2023 18:00:44 -0600 Subject: [PATCH 043/105] WIP --- crates/gpui/src/app.rs | 59 ++++++++++++++++++++--------------- crates/gpui/src/app/window.rs | 2 +- 2 files changed, 35 insertions(+), 26 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d98033820bb..62d7d1d48ae 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1442,7 +1442,7 @@ impl AppContext { window } - pub fn replace_root_view( + pub(crate) fn replace_root_view( &mut self, window_id: usize, build_root_view: F, @@ -3900,28 +3900,6 @@ impl WindowHandle { cx.update_window(self.window_id(), update) } - // pub fn update_root(&self, cx: &mut C, update: F) -> C::Result> - // where - // C: BorrowWindowContext, - // F: FnOnce(&mut V, &mut ViewContext) -> R, - // { - // cx.update_window(self.window_id, |cx| { - // cx.root_view() - // .clone() - // .downcast::() - // .map(|v| v.update(cx, update)) - // }) - // } - - pub fn read_root<'a>(&self, cx: &'a AppContext) -> &'a V { - let root_view = cx - .read_window(self.window_id(), |cx| { - cx.root_view().clone().downcast().unwrap() - }) - .unwrap(); - root_view.read(cx) - } - pub fn read_root_with(&self, cx: &C, read: F) -> C::Result where C: BorrowWindowContext, @@ -3935,6 +3913,20 @@ impl WindowHandle { }) } + pub fn update_root(&self, cx: &mut C, update: F) -> C::Result + where + C: BorrowWindowContext, + F: FnOnce(&mut V, &mut ViewContext) -> R, + { + cx.update_window(self.window_id, |cx| { + cx.root_view() + .clone() + .downcast::() + .unwrap() + .update(cx, update) + }) + } + pub fn add_view(&self, cx: &mut C, build_view: F) -> C::Result> where C: BorrowWindowContext, @@ -3943,6 +3935,23 @@ impl WindowHandle { { self.update(cx, |cx| cx.add_view(build_view)) } + + pub(crate) fn replace_root_view( + &self, + cx: &mut C, + build_root_view: F, + ) -> C::Result> + where + C: BorrowWindowContext, + F: FnOnce(&mut ViewContext) -> V, + { + cx.update_window(self.window_id, |cx| { + let root_view = self.add_view(cx, |cx| build_root_view(cx)); + cx.window.root_view = Some(root_view.clone().into_any()); + cx.window.focused_view_id = Some(root_view.id()); + root_view + }) + } } #[repr(transparent)] @@ -5329,7 +5338,7 @@ mod tests { TestView { events: Vec::new() } }); - assert_eq!(window.read_root(cx).events, ["before emit"]); + window.read_root_with(cx, |view, _| assert_eq!(view.events, ["before emit"])); } #[crate::test(self)] @@ -5381,7 +5390,7 @@ mod tests { TestView { events: Vec::new() } }); - assert_eq!(window.read_root(cx).events, ["before notify"]); + assert_eq!(window.read_root_with(cx, |view, _| assert_eq!(view.events, ["before notify"]))); } #[crate::test(self)] diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 789341d46fa..20992bfbaad 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -1157,7 +1157,7 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub fn replace_root_view(&mut self, build_root_view: F) -> WindowHandle + pub(crate) fn replace_root_view(&mut self, build_root_view: F) -> WindowHandle where V: View, F: FnOnce(&mut ViewContext) -> V, From ef5b982ea5901f9f5ef78854e1bcba93b77dd2b6 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sun, 6 Aug 2023 02:20:31 -0400 Subject: [PATCH 044/105] Fix bash path_suffixes and add line_comment --- crates/zed/src/languages/bash/config.toml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml index 80b8753d801..d03897a071d 100644 --- a/crates/zed/src/languages/bash/config.toml +++ b/crates/zed/src/languages/bash/config.toml @@ -1,5 +1,6 @@ name = "Shell Script" -path_suffixes = [".sh", ".bash", ".bashrc", ".bash_profile", ".bash_aliases", ".bash_logout", ".profile", ".zsh", ".zshrc", ".zshenv", ".zsh_profile", ".zsh_aliases", ".zsh_histfile", ".zlogin"] +path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin"] +line_comment = "# " first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" brackets = [ { start = "[", end = "]", close = true, newline = false }, From adc50469ff0c938415c4995ce8ed7fb761021a68 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 6 Aug 2023 12:45:31 -0600 Subject: [PATCH 045/105] WIP --- Cargo.lock | 39 ++- Cargo.toml | 1 + crates/collab/src/tests/integration_tests.rs | 4 +- .../src/incoming_call_notification.rs | 2 +- .../src/project_shared_notification.rs | 2 +- crates/command_palette/src/command_palette.rs | 2 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/editor/src/editor_tests.rs | 2 +- .../src/test/editor_lsp_test_context.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 2 +- crates/file_finder/src/file_finder.rs | 22 +- crates/gpui/Cargo.toml | 1 + crates/gpui/src/app.rs | 264 +++++++++--------- crates/gpui/src/app/test_app_context.rs | 33 ++- crates/gpui/src/app/window.rs | 21 +- crates/project_panel/src/project_panel.rs | 8 +- crates/project_symbols/src/project_symbols.rs | 2 +- crates/search/src/buffer_search.rs | 10 +- crates/search/src/project_search.rs | 4 +- crates/workspace/src/workspace.rs | 161 +++++------ crates/zed/src/zed.rs | 20 +- 21 files changed, 329 insertions(+), 277 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fbf4e750c62..6cd36252d1b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1649,6 +1649,12 @@ dependencies = [ "theme", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + [[package]] name = "copilot" version = "0.1.0" @@ -2133,6 +2139,19 @@ dependencies = [ "serde", ] +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.0", + "syn 1.0.109", +] + [[package]] name = "dhat" version = "0.3.2" @@ -3096,6 +3115,7 @@ dependencies = [ "core-graphics", "core-text", "ctor", + "derive_more", "dhat", "env_logger 0.9.3", "etagere", @@ -5106,7 +5126,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39fe46acc5503595e5949c17b818714d26fdf9b4920eacf3b2947f0199f4a6ff" dependencies = [ - "rustc_version", + "rustc_version 0.3.3", ] [[package]] @@ -6276,7 +6296,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" dependencies = [ - "semver", + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver 1.0.18", ] [[package]] @@ -6716,6 +6745,12 @@ dependencies = [ "semver-parser", ] +[[package]] +name = "semver" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" + [[package]] name = "semver-parser" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index 6e79c6b6576..1938e832e9c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ resolver = "2" anyhow = { version = "1.0.57" } async-trait = { version = "0.1" } ctor = { version = "0.1" } +derive_more = { version = "0.99.17" } env_logger = { version = "0.9" } futures = { version = "0.3" } globset = { version = "0.4" } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 037f97f71ab..1a8e6d938d6 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3446,7 +3446,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a, - window_id: window_a.window_id(), + window_id: window_a.id(), editor: editor_a, }; @@ -3459,7 +3459,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b, - window_id: window_b.window_id(), + window_id: window_b.id(), editor: editor_b, }; diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 770f5d5795c..a9c5e697a5f 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -49,7 +49,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), ); - notification_windows.push(window.window_id()); + notification_windows.push(window.id()); } } } diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 5be7e33bf3c..03ab91623b4 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -52,7 +52,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_windows .entry(*project_id) .or_insert(Vec::new()) - .push(window.window_id()); + .push(window.id()); } } room::Event::RemoteProjectUnshared { project_id } => { diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 935358c2a12..7d4b4126b70 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -297,7 +297,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let editor = cx.add_view(window_id, |cx| { let mut editor = Editor::single_line(None, cx); editor.set_text("abc", cx); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index f2db0a77630..2444465be66 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -857,7 +857,7 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); // Create some diagnostics project.update(cx, |project, cx| { @@ -1252,7 +1252,7 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let view = cx.add_view(window_id, |cx| { ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index e8913505cab..a114cd437b1 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -525,7 +525,7 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let project = Project::test(fs, [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); cx.add_view(window_id, |cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index d25ca7bb88a..f53115f224a 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -99,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> { Self { cx: EditorTestContext { cx, - window_id: window.window_id(), + window_id: window.id(), editor, }, lsp, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index ac519764fdd..c7ea1b4f385 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -39,7 +39,7 @@ impl<'a> EditorTestContext<'a> { let editor = window.root(cx); Self { cx, - window_id: window.window_id(), + window_id: window.id(), editor, } } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 84a45b083e6..12bf3242627 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -619,7 +619,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.window_id(), Toggle); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder @@ -632,8 +632,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.window_id(), SelectNext); - cx.dispatch_action(window.window_id(), Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -674,7 +674,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.window_id(), Toggle); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -706,8 +706,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.window_id(), SelectNext); - cx.dispatch_action(window.window_id(), Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -758,7 +758,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.window_id(), Toggle); + cx.dispatch_action(window.id(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -790,8 +790,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.window_id(), SelectNext); - cx.dispatch_action(window.window_id(), Confirm); + cx.dispatch_action(window.id(), SelectNext); + cx.dispatch_action(window.id(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -1168,7 +1168,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1376,7 +1376,7 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); diff --git a/crates/gpui/Cargo.toml b/crates/gpui/Cargo.toml index 31b7db008d2..5bd7d031843 100644 --- a/crates/gpui/Cargo.toml +++ b/crates/gpui/Cargo.toml @@ -22,6 +22,7 @@ sqlez = { path = "../sqlez" } async-task = "4.0.3" backtrace = { version = "0.3", optional = true } ctor.workspace = true +derive_more.workspace = true dhat = { version = "0.3", optional = true } env_logger = { version = "0.9", optional = true } etagere = "0.2" diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 62d7d1d48ae..f967f134039 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -23,6 +23,7 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; +use derive_more::Deref; use parking_lot::Mutex; use postage::oneshot; use smallvec::SmallVec; @@ -132,11 +133,13 @@ pub trait BorrowAppContext { pub trait BorrowWindowContext { type Result; - fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + fn read_window_with(&self, window_handle: H, f: F) -> Self::Result where + H: Into, F: FnOnce(&WindowContext) -> T; - fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + fn update_window(&mut self, window_handle: H, f: F) -> Self::Result where + H: Into, F: FnOnce(&mut WindowContext) -> T; } @@ -300,13 +303,13 @@ impl App { result } - fn update_window T>( - &mut self, - window_id: usize, - callback: F, - ) -> Option { + fn update_window(&mut self, handle: H, callback: F) -> Option + where + H: Into, + F: FnOnce(&mut WindowContext) -> T, + { let mut state = self.0.borrow_mut(); - let result = state.update_window(window_id, callback); + let result = state.update_window(handle, callback); state.pending_notifications.clear(); result } @@ -333,6 +336,10 @@ impl AsyncAppContext { self.0.borrow_mut().update(callback) } + pub fn windows(&self) -> Vec { + self.0.borrow().windows().collect() + } + pub fn read_window T>( &self, window_id: usize, @@ -343,10 +350,10 @@ impl AsyncAppContext { pub fn update_window T>( &mut self, - window_id: usize, + handle: AnyWindowHandle, callback: F, ) -> Option { - self.0.borrow_mut().update_window(window_id, callback) + self.0.borrow_mut().update_window(handle, callback) } pub fn debug_elements(&self, window_id: usize) -> Option { @@ -359,14 +366,14 @@ impl AsyncAppContext { pub fn dispatch_action( &mut self, - window_id: usize, + window: impl Into, view_id: usize, action: &dyn Action, ) -> Result<()> { self.0 .borrow_mut() - .update_window(window_id, |window| { - window.dispatch_action(Some(view_id), action); + .update_window(window, |cx| { + cx.dispatch_action(Some(view_id), action); }) .ok_or_else(|| anyhow!("window not found")) } @@ -380,22 +387,6 @@ impl AsyncAppContext { .unwrap_or_default() } - pub fn has_window(&self, window_id: usize) -> bool { - self.read(|cx| cx.windows.contains_key(&window_id)) - } - - pub fn window_is_active(&self, window_id: usize) -> bool { - self.read(|cx| cx.windows.get(&window_id).map_or(false, |w| w.is_active)) - } - - pub fn root_view(&self, window_id: usize) -> Option { - self.read(|cx| cx.windows.get(&window_id).map(|w| w.root_view().clone())) - } - - pub fn window_ids(&self) -> Vec { - self.read(|cx| cx.windows.keys().copied().collect()) - } - pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, @@ -501,7 +492,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, views_metadata: HashMap<(usize, usize), ViewMetadata>, - windows: HashMap, + windows: HashMap, globals: HashMap>, element_states: HashMap>, background: Arc, @@ -818,22 +809,6 @@ impl AppContext { Some(callback(&window_context)) } - pub fn update_window T>( - &mut self, - window_id: usize, - callback: F, - ) -> Option { - self.update(|app_context| { - let mut window = app_context.windows.remove(&window_id)?; - let mut window_context = WindowContext::mutable(app_context, &mut window, window_id); - let result = callback(&mut window_context); - if !window_context.removed { - app_context.windows.insert(window_id, window); - } - Some(result) - }) - } - pub fn update_active_window T>( &mut self, callback: F, @@ -1331,43 +1306,40 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_id); + let window = WindowHandle::::new(post_inc(&mut this.next_id)); let platform_window = this.platform - .open_window(window_id, window_options, this.foreground.clone()); - let window = this.build_window(window_id, platform_window, build_root_view); - this.windows.insert(window_id, window); - WindowHandle::new(window_id) + .open_window(window, window_options, this.foreground.clone()); + let window = this.build_window(window, platform_window, build_root_view); + this.windows.insert(window.into(), window); + window }) } - pub fn add_status_bar_item(&mut self, build_root_view: F) -> (usize, ViewHandle) + pub fn add_status_bar_item(&mut self, build_root_view: F) -> WindowHandle where V: View, F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_id); - let platform_window = this.platform.add_status_item(window_id); - let window = this.build_window(window_id, platform_window, build_root_view); - let root_view = window.root_view().clone().downcast::().unwrap(); + let handle = WindowHandle::::new(post_inc(&mut this.next_id)); + let platform_window = this.platform.add_status_item(handle.id()); + let window = this.build_window(handle, platform_window, build_root_view); - this.windows.insert(window_id, window); - this.update_window(window_id, |cx| { - root_view.update(cx, |view, cx| view.focus_in(cx.handle().into_any(), cx)) - }); - - (window_id, root_view) + this.windows.insert(handle.into(), window); + handle.update_root(this, |view, cx| view.focus_in(cx.handle().into_any(), cx)); + handle }) } - pub fn build_window( + pub fn build_window( &mut self, - window_id: usize, + handle: H, mut platform_window: Box, build_root_view: F, ) -> Window where + H: Into, V: View, F: FnOnce(&mut ViewContext) -> V, { @@ -1375,7 +1347,7 @@ impl AppContext { let mut app = self.upgrade(); platform_window.on_event(Box::new(move |event| { - app.update_window(window_id, |cx| { + app.update_window(handle, |cx| { if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event { if cx.dispatch_keystroke(keystroke) { return true; @@ -1391,35 +1363,35 @@ impl AppContext { { let mut app = self.upgrade(); platform_window.on_active_status_change(Box::new(move |is_active| { - app.update(|cx| cx.window_changed_active_status(window_id, is_active)) + app.update(|cx| cx.window_changed_active_status(handle, is_active)) })); } { let mut app = self.upgrade(); platform_window.on_resize(Box::new(move || { - app.update(|cx| cx.window_was_resized(window_id)) + app.update(|cx| cx.window_was_resized(handle)) })); } { let mut app = self.upgrade(); platform_window.on_moved(Box::new(move || { - app.update(|cx| cx.window_was_moved(window_id)) + app.update(|cx| cx.window_was_moved(handle)) })); } { let mut app = self.upgrade(); platform_window.on_fullscreen(Box::new(move |is_fullscreen| { - app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen)) + app.update(|cx| cx.window_was_fullscreen_changed(handle, is_fullscreen)) })); } { let mut app = self.upgrade(); platform_window.on_close(Box::new(move || { - app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window())); + app.update(|cx| cx.update_window(handle, |cx| cx.remove_window())); })); } @@ -1431,27 +1403,20 @@ impl AppContext { platform_window.set_input_handler(Box::new(WindowInputHandler { app: self.upgrade().0, - window_id, + window_id: handle, })); - let mut window = Window::new(window_id, platform_window, self, build_root_view); - let mut cx = WindowContext::mutable(self, &mut window, window_id); + let mut window = Window::new(handle, platform_window, self, build_root_view); + let mut cx = WindowContext::mutable(self, &mut window, handle); cx.layout(false).expect("initial layout should not error"); let scene = cx.paint().expect("initial paint should not error"); window.platform_window.present_scene(scene); window } - pub(crate) fn replace_root_view( - &mut self, - window_id: usize, - build_root_view: F, - ) -> Option> - where - V: View, - F: FnOnce(&mut ViewContext) -> V, - { - self.update_window(window_id, |cx| cx.replace_root_view(build_root_view)) + pub fn windows(&self) -> impl Iterator { + todo!(); + None.into_iter() } pub fn read_view(&self, handle: &ViewHandle) -> &T { @@ -2192,11 +2157,20 @@ impl BorrowWindowContext for AppContext { AppContext::read_window(self, window_id, f) } - fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + fn update_window(&mut self, window: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, { - AppContext::update_window(self, window_id, f) + self.update(|app_context| { + let mut window = app_context.windows.remove(&window_handle)?; + let mut window_context = + WindowContext::mutable(app_context, &mut window, window_handle); + let result = callback(&mut window_context); + if !window_context.removed { + app_context.windows.insert(window_handle, window); + } + Some(result) + }) } } @@ -3862,44 +3836,28 @@ impl Clone for WeakModelHandle { impl Copy for WeakModelHandle {} +#[derive(Deref, Copy, Clone)] pub struct WindowHandle { - window_id: usize, + #[deref] + any_handle: AnyWindowHandle, root_view_type: PhantomData, } -#[allow(dead_code)] impl WindowHandle { fn new(window_id: usize) -> Self { WindowHandle { - window_id, + any_handle: AnyWindowHandle { + window_id, + root_view_type: TypeId::of::(), + }, root_view_type: PhantomData, } } - pub fn window_id(&self) -> usize { - self.window_id - } - pub fn root(&self, cx: &C) -> C::Result> { self.read_with(cx, |cx| cx.root_view().clone().downcast().unwrap()) } - pub fn read_with(&self, cx: &C, read: F) -> C::Result - where - C: BorrowWindowContext, - F: FnOnce(&WindowContext) -> R, - { - cx.read_window_with(self.window_id(), |cx| read(cx)) - } - - pub fn update(&self, cx: &mut C, update: F) -> C::Result - where - C: BorrowWindowContext, - F: FnOnce(&mut WindowContext) -> R, - { - cx.update_window(self.window_id(), update) - } - pub fn read_root_with(&self, cx: &C, read: F) -> C::Result where C: BorrowWindowContext, @@ -3918,7 +3876,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&mut V, &mut ViewContext) -> R, { - cx.update_window(self.window_id, |cx| { + cx.update_window(*self, |cx| { cx.root_view() .clone() .downcast::() @@ -3927,6 +3885,53 @@ impl WindowHandle { }) } + pub fn replace_root(&self, cx: &mut C, build_root: F) -> C::Result> + where + C: BorrowWindowContext, + F: FnOnce(&mut ViewContext) -> V, + { + cx.update_window(self.into_any(), |cx| { + let root_view = self.add_view(cx, |cx| build_root(cx)); + cx.window.root_view = Some(root_view.clone().into_any()); + cx.window.focused_view_id = Some(root_view.id()); + root_view + }) + } +} + +impl Into for WindowHandle { + fn into(self) -> AnyWindowHandle { + self.any_handle + } +} + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct AnyWindowHandle { + window_id: usize, + root_view_type: TypeId, +} + +impl AnyWindowHandle { + pub fn id(&self) -> usize { + self.window_id + } + + pub fn read_with(&self, cx: &C, read: F) -> C::Result + where + C: BorrowWindowContext, + F: FnOnce(&WindowContext) -> R, + { + cx.read_window_with(self.window_id, |cx| read(cx)) + } + + pub fn update(&self, cx: &mut C, update: F) -> C::Result + where + C: BorrowWindowContext, + F: FnOnce(&mut WindowContext) -> R, + { + cx.update_window(self.window_id, update) + } + pub fn add_view(&self, cx: &mut C, build_view: F) -> C::Result> where C: BorrowWindowContext, @@ -3936,21 +3941,16 @@ impl WindowHandle { self.update(cx, |cx| cx.add_view(build_view)) } - pub(crate) fn replace_root_view( - &self, - cx: &mut C, - build_root_view: F, - ) -> C::Result> - where - C: BorrowWindowContext, - F: FnOnce(&mut ViewContext) -> V, - { - cx.update_window(self.window_id, |cx| { - let root_view = self.add_view(cx, |cx| build_root_view(cx)); - cx.window.root_view = Some(root_view.clone().into_any()); - cx.window.focused_view_id = Some(root_view.id()); - root_view - }) + pub fn root_is(&self) -> bool { + self.root_view_type == TypeId::of::() + } + + pub fn is_active(&self, cx: &C) -> C::Result { + self.read_with(cx, |cx| cx.window.is_active) + } + + pub fn remove(&self, cx: &mut C) -> C::Result<()> { + self.update(cx, |cx| cx.remove_window()) } } @@ -5390,7 +5390,7 @@ mod tests { TestView { events: Vec::new() } }); - assert_eq!(window.read_root_with(cx, |view, _| assert_eq!(view.events, ["before notify"]))); + window.read_root_with(cx, |view, _| assert_eq!(view.events, ["before notify"])); } #[crate::test(self)] @@ -6227,7 +6227,7 @@ mod tests { // Check that global actions do not have a binding, even if a binding does exist in another view assert_eq!( - &available_actions(window.window_id(), view_1.id(), cx), + &available_actions(window.id(), view_1.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::GlobalAction", vec![]) @@ -6236,7 +6236,7 @@ mod tests { // Check that view 1 actions and bindings are available even when called from view 2 assert_eq!( - &available_actions(window.window_id(), view_2.id(), cx), + &available_actions(window.id(), view_2.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::Action2", vec![Keystroke::parse("b").unwrap()]), @@ -6299,7 +6299,7 @@ mod tests { ]); }); - let actions = cx.available_actions(window.window_id(), view.id()); + let actions = cx.available_actions(window.id(), view.id()); assert_eq!( actions[0].1.as_any().downcast_ref::(), Some(&ActionWithArg { arg: false }) @@ -6585,25 +6585,25 @@ mod tests { [("window 2", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_2.window_id())); + cx.simulate_window_activation(Some(window_2.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 3", false), ("window 2", true)] ); - cx.simulate_window_activation(Some(window_1.window_id())); + cx.simulate_window_activation(Some(window_1.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 2", false), ("window 1", true)] ); - cx.simulate_window_activation(Some(window_3.window_id())); + cx.simulate_window_activation(Some(window_3.id())); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 1", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_3.window_id())); + cx.simulate_window_activation(Some(window_3.id())); assert_eq!(mem::take(&mut *events.borrow_mut()), []); } diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 3b574eb03ee..5b005d7d6f2 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -4,9 +4,9 @@ use crate::{ keymap_matcher::{Binding, Keystroke}, platform, platform::{Event, InputHandler, KeyDownEvent, Platform}, - Action, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, Handle, - ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, WeakHandle, - WindowContext, WindowHandle, + Action, AnyWindowHandle, AppContext, BorrowAppContext, BorrowWindowContext, Entity, FontCache, + Handle, ModelContext, ModelHandle, Subscription, Task, View, ViewContext, ViewHandle, + WeakHandle, WindowContext, WindowHandle, }; use collections::BTreeMap; use futures::Future; @@ -124,6 +124,13 @@ impl TestAppContext { } } + pub fn window(&self, window_id: usize) -> Option> { + self.cx + .borrow() + .read_window(window_id, |cx| cx.window()) + .flatten() + } + pub fn read_window T>( &self, window_id: usize, @@ -157,9 +164,9 @@ impl TestAppContext { .cx .borrow_mut() .add_window(Default::default(), build_root_view); - self.simulate_window_activation(Some(window.window_id())); + self.simulate_window_activation(Some(window.id())); - WindowHandle::new(window.window_id()) + WindowHandle::new(window.id()) } pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle @@ -191,10 +198,14 @@ impl TestAppContext { self.cx.borrow_mut().subscribe_global(callback) } - pub fn window_ids(&self) -> Vec { - self.cx.borrow().windows.keys().copied().collect() + pub fn windows(&self) -> impl Iterator { + self.cx.borrow().windows() } + // pub fn window_ids(&self) -> Vec { + // self.cx.borrow().windows.keys().copied().collect() + // } + pub fn remove_all_windows(&mut self) { self.update(|cx| cx.windows.clear()); } @@ -311,15 +322,15 @@ impl TestAppContext { pub fn simulate_window_activation(&self, to_activate: Option) { self.cx.borrow_mut().update(|cx| { - let other_window_ids = cx + let other_windows = cx .windows .keys() - .filter(|window_id| Some(**window_id) != to_activate) + .filter(|window| Some(window.id()) != to_activate) .copied() .collect::>(); - for window_id in other_window_ids { - cx.window_changed_active_status(window_id, false) + for window in other_windows { + cx.window_changed_active_status(window.id(), false) } if let Some(to_activate) = to_activate { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 20992bfbaad..cc8468edbd7 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -196,6 +196,16 @@ impl<'a> WindowContext<'a> { self.window_id } + pub fn window(&self) -> Option> { + self.window.root_view.as_ref().and_then(|root_view| { + if root_view.is::() { + Some(WindowHandle::new(self.window_id)) + } else { + None + } + }) + } + pub fn app_context(&mut self) -> &mut AppContext { &mut self.app_context } @@ -1157,17 +1167,6 @@ impl<'a> WindowContext<'a> { self.window.platform_window.prompt(level, msg, answers) } - pub(crate) fn replace_root_view(&mut self, build_root_view: F) -> WindowHandle - where - V: View, - F: FnOnce(&mut ViewContext) -> V, - { - let root_view = self.add_view(|cx| build_root_view(cx)); - self.window.focused_view_id = Some(root_view.id()); - self.window.root_view = Some(root_view.into_any()); - WindowHandle::new(self.window_id) - } - pub fn add_view(&mut self, build_view: F) -> ViewHandle where T: View, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 80847f9f4b9..021ea2d3bc3 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1872,7 +1872,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2225,7 +2225,7 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2402,7 +2402,7 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); toggle_expand_dir(&panel, "src/test", cx); @@ -2493,7 +2493,7 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "src/", cx); diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 4bd186fc98b..8471f3a3a70 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,7 +328,7 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); // Create the project symbols view. let symbols = cx.add_view(window_id, |cx| { diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 265e4f02061..1e635432bd5 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -851,11 +851,11 @@ mod tests { }); let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window.window_id(), |cx| { + let editor = cx.add_view(window.id(), |cx| { Editor::for_buffer(buffer.clone(), None, cx) }); - let search_bar = cx.add_view(window.window_id(), |cx| { + let search_bar = cx.add_view(window.id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1232,7 +1232,7 @@ mod tests { ); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let window_id = window.window_id(); + let window_id = window.id(); let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); @@ -1421,11 +1421,11 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window.window_id(), |cx| { + let editor = cx.add_view(window.id(), |cx| { Editor::for_buffer(buffer.clone(), None, cx) }); - let search_bar = cx.add_view(window.window_id(), |cx| { + let search_bar = cx.add_view(window.id(), |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index febd564050d..0db66b4e378 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1568,7 +1568,7 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let active_item = cx.read(|cx| { workspace @@ -1874,7 +1874,7 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); workspace.update(cx, |workspace, cx| { ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) }); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 09e4c4c2193..37fab1ee462 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -37,7 +37,7 @@ use gpui::{ }, AnyModelHandle, AnyViewHandle, AnyWeakViewHandle, AppContext, AsyncAppContext, Entity, ModelContext, ModelHandle, SizeConstraint, Subscription, Task, View, ViewContext, ViewHandle, - WeakViewHandle, WindowContext, + WeakViewHandle, WindowContext, WindowHandle, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ProjectItem}; use itertools::Itertools; @@ -749,7 +749,7 @@ impl Workspace { fn new_local( abs_paths: Vec, app_state: Arc, - requesting_window_id: Option, + requesting_window: Option>, cx: &mut AppContext, ) -> Task<( WeakViewHandle, @@ -793,55 +793,60 @@ impl Workspace { DB.next_id().await.unwrap_or(0) }; - let window = requesting_window_id.and_then(|window_id| { - cx.update(|cx| { - cx.replace_root_view(window_id, |cx| { - Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) - }) - }) - }); - let window = window.unwrap_or_else(|| { - let window_bounds_override = window_bounds_env_override(&cx); - let (bounds, display) = if let Some(bounds) = window_bounds_override { - (Some(bounds), None) - } else { - serialized_workspace - .as_ref() - .and_then(|serialized_workspace| { - let display = serialized_workspace.display?; - let mut bounds = serialized_workspace.bounds?; + let window = if let Some(window) = requesting_window { + window.replace_root(&mut cx, |cx| { + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) + }); + window + } else { + { + let window_bounds_override = window_bounds_env_override(&cx); + let (bounds, display) = if let Some(bounds) = window_bounds_override { + (Some(bounds), None) + } else { + serialized_workspace + .as_ref() + .and_then(|serialized_workspace| { + let display = serialized_workspace.display?; + let mut bounds = serialized_workspace.bounds?; - // Stored bounds are relative to the containing display. - // So convert back to global coordinates if that screen still exists - if let WindowBounds::Fixed(mut window_bounds) = bounds { - if let Some(screen) = cx.platform().screen_by_id(display) { - let screen_bounds = screen.bounds(); - window_bounds.set_origin_x( - window_bounds.origin_x() + screen_bounds.origin_x(), - ); - window_bounds.set_origin_y( - window_bounds.origin_y() + screen_bounds.origin_y(), - ); - bounds = WindowBounds::Fixed(window_bounds); - } else { - // Screen no longer exists. Return none here. - return None; + // Stored bounds are relative to the containing display. + // So convert back to global coordinates if that screen still exists + if let WindowBounds::Fixed(mut window_bounds) = bounds { + if let Some(screen) = cx.platform().screen_by_id(display) { + let screen_bounds = screen.bounds(); + window_bounds.set_origin_x( + window_bounds.origin_x() + screen_bounds.origin_x(), + ); + window_bounds.set_origin_y( + window_bounds.origin_y() + screen_bounds.origin_y(), + ); + bounds = WindowBounds::Fixed(window_bounds); + } else { + // Screen no longer exists. Return none here. + return None; + } } - } - Some((bounds, display)) - }) - .unzip() - }; + Some((bounds, display)) + }) + .unzip() + }; - // Use the serialized workspace to construct the new window - cx.add_window( - (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), - |cx| { - Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) - }, - ) - }); + // Use the serialized workspace to construct the new window + cx.add_window( + (app_state.build_window_options)(bounds, display, cx.platform().as_ref()), + |cx| { + Workspace::new( + workspace_id, + project_handle.clone(), + app_state.clone(), + cx, + ) + }, + ) + } + }; // We haven't yielded the main thread since obtaining the window handle, // so the window exists. @@ -1227,14 +1232,14 @@ impl Workspace { pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { cx.spawn(|mut cx| async move { - let id = cx - .window_ids() + let window = cx + .windows() .into_iter() - .find(|&id| cx.window_is_active(id)); - if let Some(id) = id { + .find(|window| window.is_active(&cx).unwrap_or(false)); + if let Some(window) = window { //This can only get called when the window's project connection has been lost //so we don't need to prompt the user for anything and instead just close the window - cx.remove_window(id); + window.remove(&mut cx); } }) .detach(); @@ -1265,9 +1270,9 @@ impl Workspace { cx.spawn(|this, mut cx| async move { let workspace_count = cx - .window_ids() + .windows() .into_iter() - .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::()) + .filter(|window| window.root_is::()) .count(); if let Some(active_call) = active_call { @@ -1385,7 +1390,7 @@ impl Workspace { paths: Vec, cx: &mut ViewContext, ) -> Task> { - let window_id = cx.window_id(); + let window = cx.window::(); let is_remote = self.project.read(cx).is_remote(); let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); @@ -1397,15 +1402,15 @@ impl Workspace { let app_state = self.app_state.clone(); cx.spawn(|_, mut cx| async move { - let window_id_to_replace = if let Some(close_task) = close_task { + let window_to_replace = if let Some(close_task) = close_task { if !close_task.await? { return Ok(()); } - Some(window_id) + window } else { None }; - cx.update(|cx| open_paths(&paths, &app_state, window_id_to_replace, cx)) + cx.update(|cx| open_paths(&paths, &app_state, window_to_replace, cx)) .await?; Ok(()) }) @@ -3851,7 +3856,7 @@ pub async fn last_opened_workspace_paths() -> Option { pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, - requesting_window_id: Option, + requesting_window: Option>, cx: &mut AppContext, ) -> Task< Result<( @@ -3879,7 +3884,7 @@ pub fn open_paths( } else { Ok(cx .update(|cx| { - Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx) + Workspace::new_local(abs_paths, app_state.clone(), requesting_window, cx) }) .await) } @@ -4196,14 +4201,14 @@ mod tests { ); }); assert_eq!( - cx.current_window_title(window.window_id()).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1") ); // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); assert_eq!( - cx.current_window_title(window.window_id()).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("two.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4222,7 +4227,7 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window.window_id()).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1") ); project.read_with(cx, |project, cx| { @@ -4242,14 +4247,14 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window.window_id()).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root1, root2") ); // Remove a project folder project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); assert_eq!( - cx.current_window_title(window.window_id()).as_deref(), + cx.current_window_title(window.id()).as_deref(), Some("one.txt — root2") ); } @@ -4285,9 +4290,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - cx.simulate_prompt_answer(window.window_id(), 2 /* cancel */); + cx.simulate_prompt_answer(window.id(), 2 /* cancel */); cx.foreground().run_until_parked(); - assert!(!cx.has_pending_prompt(window.window_id())); + assert!(!cx.has_pending_prompt(window.id())); assert!(!task.await.unwrap()); } @@ -4346,10 +4351,10 @@ mod tests { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(cx.has_pending_prompt(window.window_id())); + assert!(cx.has_pending_prompt(window.id())); // Confirm saving item 1. - cx.simulate_prompt_answer(window.window_id(), 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); // Item 1 is saved. There's a prompt to save item 3. @@ -4360,10 +4365,10 @@ mod tests { assert_eq!(pane.items_len(), 3); assert_eq!(pane.active_item().unwrap().id(), item3.id()); }); - assert!(cx.has_pending_prompt(window.window_id())); + assert!(cx.has_pending_prompt(window.id())); // Cancel saving item 3. - cx.simulate_prompt_answer(window.window_id(), 1); + cx.simulate_prompt_answer(window.id(), 1); cx.foreground().run_until_parked(); // Item 3 is reloaded. There's a prompt to save item 4. @@ -4374,10 +4379,10 @@ mod tests { assert_eq!(pane.items_len(), 2); assert_eq!(pane.active_item().unwrap().id(), item4.id()); }); - assert!(cx.has_pending_prompt(window.window_id())); + assert!(cx.has_pending_prompt(window.id())); // Confirm saving item 4. - cx.simulate_prompt_answer(window.window_id(), 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); // There's a prompt for a path for item 4. @@ -4480,7 +4485,7 @@ mod tests { &[ProjectEntryId::from_proto(0)] ); }); - cx.simulate_prompt_answer(window.window_id(), 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { @@ -4489,7 +4494,7 @@ mod tests { &[ProjectEntryId::from_proto(2)] ); }); - cx.simulate_prompt_answer(window.window_id(), 0); + cx.simulate_prompt_answer(window.id(), 0); cx.foreground().run_until_parked(); close.await.unwrap(); @@ -4549,7 +4554,7 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - cx.simulate_window_activation(Some(window.window_id())); + cx.simulate_window_activation(Some(window.id())); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; @@ -4591,7 +4596,7 @@ mod tests { pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); - assert!(!cx.has_pending_prompt(window.window_id())); + assert!(!cx.has_pending_prompt(window.id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. @@ -4612,7 +4617,7 @@ mod tests { let _close_items = pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); - assert!(cx.has_pending_prompt(window.window_id())); + assert!(cx.has_pending_prompt(window.id())); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 46fcc014a13..a8a287fb4e6 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -813,11 +813,12 @@ mod tests { // Replace existing windows let window_id = cx.window_ids()[0]; + let window = cx.read_window(window_id, |cx| cx.window()).flatten(); cx.update(|cx| { open_paths( &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], &app_state, - Some(window_id), + window, cx, ) }) @@ -982,9 +983,8 @@ mod tests { .await; let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project, cx)) - .root(cx); + let window = cx.add_window(|cx| Workspace::test_new(project, cx)); + let workspace = window.root(cx); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1298,7 +1298,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); // Open a file within an existing worktree. workspace @@ -1341,7 +1341,7 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer @@ -1436,7 +1436,7 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); // Create a new untitled buffer cx.dispatch_action(window_id, NewFile); @@ -1489,7 +1489,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.window_id(); + let window_id = window.id(); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -2087,7 +2087,7 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.window_id(); + let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( @@ -2258,7 +2258,7 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.window_id(); + let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( From d4d32611fe3422e9dbb03d432fb8e746f7b8198c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sun, 6 Aug 2023 18:57:02 -0600 Subject: [PATCH 046/105] WIP --- crates/gpui/src/app.rs | 58 +++++++++++++++---------- crates/gpui/src/app/menu.rs | 6 +-- crates/gpui/src/app/test_app_context.rs | 12 ++--- 3 files changed, 44 insertions(+), 32 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f967f134039..c2d2397d9c4 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -133,13 +133,11 @@ pub trait BorrowAppContext { pub trait BorrowWindowContext { type Result; - fn read_window_with(&self, window_handle: H, f: F) -> Self::Result + fn read_window_with(&self, window_id: usize, f: F) -> Self::Result where - H: Into, F: FnOnce(&WindowContext) -> T; - fn update_window(&mut self, window_handle: H, f: F) -> Self::Result + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where - H: Into, F: FnOnce(&mut WindowContext) -> T; } @@ -303,13 +301,12 @@ impl App { result } - fn update_window(&mut self, handle: H, callback: F) -> Option + fn update_window(&mut self, window_id: usize, callback: F) -> Option where - H: Into, F: FnOnce(&mut WindowContext) -> T, { let mut state = self.0.borrow_mut(); - let result = state.update_window(handle, callback); + let result = state.update_window(window_id, callback); state.pending_notifications.clear(); result } @@ -350,10 +347,10 @@ impl AsyncAppContext { pub fn update_window T>( &mut self, - handle: AnyWindowHandle, + window_id: usize, callback: F, ) -> Option { - self.0.borrow_mut().update_window(handle, callback) + self.0.borrow_mut().update_window(window_id, callback) } pub fn debug_elements(&self, window_id: usize) -> Option { @@ -366,13 +363,13 @@ impl AsyncAppContext { pub fn dispatch_action( &mut self, - window: impl Into, + window_id: usize, view_id: usize, action: &dyn Action, ) -> Result<()> { self.0 .borrow_mut() - .update_window(window, |cx| { + .update_window(window_id, |cx| { cx.dispatch_action(Some(view_id), action); }) .ok_or_else(|| anyhow!("window not found")) @@ -492,7 +489,7 @@ pub struct AppContext { models: HashMap>, views: HashMap<(usize, usize), Box>, views_metadata: HashMap<(usize, usize), ViewMetadata>, - windows: HashMap, + windows: HashMap, globals: HashMap>, element_states: HashMap>, background: Arc, @@ -1414,9 +1411,18 @@ impl AppContext { window } - pub fn windows(&self) -> impl Iterator { - todo!(); - None.into_iter() + pub fn main_window(&self) -> Option { + self.platform.main_window_id().and_then(|main_window_id| { + self.windows + .get(&main_window_id) + .map(|window| AnyWindowHandle::new(main_window_id, window.root_view().type_id())) + }) + } + + pub fn windows(&self) -> impl '_ + Iterator { + self.windows.iter().map(|(window_id, window)| { + AnyWindowHandle::new(*window_id, window.root_view().type_id()) + }) } pub fn read_view(&self, handle: &ViewHandle) -> &T { @@ -2157,17 +2163,16 @@ impl BorrowWindowContext for AppContext { AppContext::read_window(self, window_id, f) } - fn update_window(&mut self, window: usize, f: F) -> Self::Result + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, { self.update(|app_context| { - let mut window = app_context.windows.remove(&window_handle)?; - let mut window_context = - WindowContext::mutable(app_context, &mut window, window_handle); - let result = callback(&mut window_context); + let mut window = app_context.windows.remove(&window_id)?; + let mut window_context = WindowContext::mutable(app_context, &mut window, window_id); + let result = f(&mut window_context); if !window_context.removed { - app_context.windows.insert(window_handle, window); + app_context.windows.insert(window_id, window); } Some(result) }) @@ -3876,7 +3881,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&mut V, &mut ViewContext) -> R, { - cx.update_window(*self, |cx| { + cx.update_window(self.id(), |cx| { cx.root_view() .clone() .downcast::() @@ -3890,7 +3895,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&mut ViewContext) -> V, { - cx.update_window(self.into_any(), |cx| { + cx.update_window(self.id(), |cx| { let root_view = self.add_view(cx, |cx| build_root(cx)); cx.window.root_view = Some(root_view.clone().into_any()); cx.window.focused_view_id = Some(root_view.id()); @@ -3912,6 +3917,13 @@ pub struct AnyWindowHandle { } impl AnyWindowHandle { + fn new(window_id: usize, root_view_type: TypeId) -> Self { + Self { + window_id, + root_view_type, + } + } + pub fn id(&self) -> usize { self.window_id } diff --git a/crates/gpui/src/app/menu.rs b/crates/gpui/src/app/menu.rs index a2ac13984be..1d8908b8fdd 100644 --- a/crates/gpui/src/app/menu.rs +++ b/crates/gpui/src/app/menu.rs @@ -77,9 +77,9 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, let cx = app.0.clone(); move |action| { let mut cx = cx.borrow_mut(); - if let Some(main_window_id) = cx.platform.main_window_id() { - let dispatched = cx - .update_window(main_window_id, |cx| { + if let Some(main_window) = cx.main_window() { + let dispatched = main_window + .update(&mut *cx, |cx| { if let Some(view_id) = cx.focused_view_id() { cx.dispatch_action(Some(view_id), action); true diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 5b005d7d6f2..dcbe810804b 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -198,8 +198,8 @@ impl TestAppContext { self.cx.borrow_mut().subscribe_global(callback) } - pub fn windows(&self) -> impl Iterator { - self.cx.borrow().windows() + pub fn windows(&self) -> Vec { + self.cx.borrow().windows().collect() } // pub fn window_ids(&self) -> Vec { @@ -322,15 +322,15 @@ impl TestAppContext { pub fn simulate_window_activation(&self, to_activate: Option) { self.cx.borrow_mut().update(|cx| { - let other_windows = cx + let other_window_ids = cx .windows .keys() - .filter(|window| Some(window.id()) != to_activate) + .filter(|window_id| Some(**window_id) != to_activate) .copied() .collect::>(); - for window in other_windows { - cx.window_changed_active_status(window.id(), false) + for window_id in other_window_ids { + cx.window_changed_active_status(window_id, false) } if let Some(to_activate) = to_activate { From 3e0d0e5c010f3f5f0c8e964af5fcfce621705b14 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 13:54:47 -0600 Subject: [PATCH 047/105] WIP --- crates/gpui/src/app.rs | 186 ++++++++++++++++---- crates/gpui/src/app/test_app_context.rs | 59 ++++--- crates/gpui/src/app/window.rs | 16 +- crates/gpui/src/app/window_input_handler.rs | 27 ++- crates/workspace/src/workspace.rs | 11 +- 5 files changed, 221 insertions(+), 78 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index c2d2397d9c4..730d0da5a05 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -133,12 +133,18 @@ pub trait BorrowAppContext { pub trait BorrowWindowContext { type Result; - fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T; + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option; fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T; + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option; } #[derive(Clone)] @@ -449,13 +455,22 @@ impl BorrowAppContext for AsyncAppContext { impl BorrowWindowContext for AsyncAppContext { type Result = Option; - fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T, { self.0.borrow().read_with(|cx| cx.read_window(window_id, f)) } + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + self.0 + .borrow_mut() + .update(|cx| cx.read_window_optional(window_id, f)) + } + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, @@ -464,6 +479,15 @@ impl BorrowWindowContext for AsyncAppContext { .borrow_mut() .update(|cx| cx.update_window(window_id, f)) } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + self.0 + .borrow_mut() + .update(|cx| cx.update_window_optional(window_id, f)) + } } type ActionCallback = dyn FnMut(&mut dyn AnyView, &dyn Action, &mut WindowContext, usize); @@ -1303,13 +1327,14 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window = WindowHandle::::new(post_inc(&mut this.next_id)); + let window_id = post_inc(&mut this.next_id); let platform_window = this.platform - .open_window(window, window_options, this.foreground.clone()); - let window = this.build_window(window, platform_window, build_root_view); - this.windows.insert(window.into(), window); - window + .open_window(window_id, window_options, this.foreground.clone()); + let handle = WindowHandle::::new(window_id); + let window = this.build_window(handle, platform_window, build_root_view); + this.windows.insert(window_id, window); + handle }) } @@ -1319,11 +1344,11 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let handle = WindowHandle::::new(post_inc(&mut this.next_id)); - let platform_window = this.platform.add_status_item(handle.id()); + let window_id = post_inc(&mut this.next_id); + let platform_window = this.platform.add_status_item(window_id); + let handle = WindowHandle::::new(window_id); let window = this.build_window(handle, platform_window, build_root_view); - - this.windows.insert(handle.into(), window); + this.windows.insert(window_id, window); handle.update_root(this, |view, cx| view.focus_in(cx.handle().into_any(), cx)); handle }) @@ -1340,11 +1365,14 @@ impl AppContext { V: View, F: FnOnce(&mut ViewContext) -> V, { + let handle: AnyWindowHandle = handle.into(); + let window_id = handle.id(); + { let mut app = self.upgrade(); platform_window.on_event(Box::new(move |event| { - app.update_window(handle, |cx| { + app.update_window(window_id, |cx| { if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event { if cx.dispatch_keystroke(keystroke) { return true; @@ -1360,35 +1388,35 @@ impl AppContext { { let mut app = self.upgrade(); platform_window.on_active_status_change(Box::new(move |is_active| { - app.update(|cx| cx.window_changed_active_status(handle, is_active)) + app.update(|cx| cx.window_changed_active_status(window_id, is_active)) })); } { let mut app = self.upgrade(); platform_window.on_resize(Box::new(move || { - app.update(|cx| cx.window_was_resized(handle)) + app.update(|cx| cx.window_was_resized(window_id)) })); } { let mut app = self.upgrade(); platform_window.on_moved(Box::new(move || { - app.update(|cx| cx.window_was_moved(handle)) + app.update(|cx| cx.window_was_moved(window_id)) })); } { let mut app = self.upgrade(); platform_window.on_fullscreen(Box::new(move |is_fullscreen| { - app.update(|cx| cx.window_was_fullscreen_changed(handle, is_fullscreen)) + app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen)) })); } { let mut app = self.upgrade(); platform_window.on_close(Box::new(move || { - app.update(|cx| cx.update_window(handle, |cx| cx.remove_window())); + app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window())); })); } @@ -1400,11 +1428,11 @@ impl AppContext { platform_window.set_input_handler(Box::new(WindowInputHandler { app: self.upgrade().0, - window_id: handle, + window: handle, })); - let mut window = Window::new(handle, platform_window, self, build_root_view); - let mut cx = WindowContext::mutable(self, &mut window, handle); + let mut window = Window::new(window_id, platform_window, self, build_root_view); + let mut cx = WindowContext::mutable(self, &mut window, window_id); cx.layout(false).expect("initial layout should not error"); let scene = cx.paint().expect("initial paint should not error"); window.platform_window.present_scene(scene); @@ -2156,13 +2184,20 @@ impl BorrowAppContext for AppContext { impl BorrowWindowContext for AppContext { type Result = Option; - fn read_window_with(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T, { AppContext::read_window(self, window_id, f) } + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + AppContext::read_window(self, window_id, f).flatten() + } + fn update_window(&mut self, window_id: usize, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, @@ -2177,6 +2212,13 @@ impl BorrowWindowContext for AppContext { Some(result) }) } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + AppContext::update_window(self, window_id, f).flatten() + } } #[derive(Debug)] @@ -3379,8 +3421,15 @@ impl BorrowAppContext for ViewContext<'_, '_, V> { impl BorrowWindowContext for ViewContext<'_, '_, V> { type Result = T; - fn read_window_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window_with(&*self.window_context, window_id, f) + fn read_window T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window(&*self.window_context, window_id, f) + } + + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + BorrowWindowContext::read_window_optional(&*self.window_context, window_id, f) } fn update_window T>( @@ -3390,6 +3439,13 @@ impl BorrowWindowContext for ViewContext<'_, '_, V> { ) -> T { BorrowWindowContext::update_window(&mut *self.window_context, window_id, f) } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + BorrowWindowContext::update_window_optional(&mut *self.window_context, window_id, f) + } } pub struct LayoutContext<'a, 'b, 'c, V: View> { @@ -3490,8 +3546,15 @@ impl BorrowAppContext for LayoutContext<'_, '_, '_, V> { impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { type Result = T; - fn read_window_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window_with(&*self.view_context, window_id, f) + fn read_window T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window(&*self.view_context, window_id, f) + } + + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + BorrowWindowContext::read_window_optional(&*self.view_context, window_id, f) } fn update_window T>( @@ -3501,6 +3564,13 @@ impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { ) -> T { BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + BorrowWindowContext::update_window_optional(&mut *self.view_context, window_id, f) + } } pub struct EventContext<'a, 'b, 'c, V: View> { @@ -3548,8 +3618,15 @@ impl BorrowAppContext for EventContext<'_, '_, '_, V> { impl BorrowWindowContext for EventContext<'_, '_, '_, V> { type Result = T; - fn read_window_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window_with(&*self.view_context, window_id, f) + fn read_window T>(&self, window_id: usize, f: F) -> T { + BorrowWindowContext::read_window(&*self.view_context, window_id, f) + } + + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + BorrowWindowContext::read_window_optional(&*self.view_context, window_id, f) } fn update_window T>( @@ -3559,6 +3636,13 @@ impl BorrowWindowContext for EventContext<'_, '_, '_, V> { ) -> T { BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + BorrowWindowContext::update_window_optional(&mut *self.view_context, window_id, f) + } } pub(crate) enum Reference<'a, T> { @@ -3841,13 +3925,24 @@ impl Clone for WeakModelHandle { impl Copy for WeakModelHandle {} -#[derive(Deref, Copy, Clone)] -pub struct WindowHandle { +#[derive(Deref)] +pub struct WindowHandle { #[deref] any_handle: AnyWindowHandle, - root_view_type: PhantomData, + root_view_type: PhantomData, } +impl Clone for WindowHandle { + fn clone(&self) -> Self { + Self { + any_handle: self.any_handle.clone(), + root_view_type: PhantomData, + } + } +} + +impl Copy for WindowHandle {} + impl WindowHandle { fn new(window_id: usize) -> Self { WindowHandle { @@ -3933,7 +4028,15 @@ impl AnyWindowHandle { C: BorrowWindowContext, F: FnOnce(&WindowContext) -> R, { - cx.read_window_with(self.window_id, |cx| read(cx)) + cx.read_window(self.window_id, |cx| read(cx)) + } + + pub fn read_optional_with(&self, cx: &C, read: F) -> Option + where + C: BorrowWindowContext, + F: FnOnce(&WindowContext) -> Option, + { + cx.read_window_optional(self.window_id, |cx| read(cx)) } pub fn update(&self, cx: &mut C, update: F) -> C::Result @@ -3944,6 +4047,14 @@ impl AnyWindowHandle { cx.update_window(self.window_id, update) } + pub fn update_optional(&self, cx: &mut C, update: F) -> Option + where + C: BorrowWindowContext, + F: FnOnce(&mut WindowContext) -> Option, + { + cx.update_window_optional(self.window_id, update) + } + pub fn add_view(&self, cx: &mut C, build_view: F) -> C::Result> where C: BorrowWindowContext, @@ -3953,6 +4064,17 @@ impl AnyWindowHandle { self.update(cx, |cx| cx.add_view(build_view)) } + pub fn downcast(self) -> Option> { + if self.root_view_type == TypeId::of::() { + Some(WindowHandle { + any_handle: self, + root_view_type: PhantomData, + }) + } else { + None + } + } + pub fn root_is(&self) -> bool { self.root_view_type == TypeId::of::() } @@ -4018,7 +4140,7 @@ impl ViewHandle { C: BorrowWindowContext, F: FnOnce(&T, &ViewContext) -> S, { - cx.read_window_with(self.window_id, |cx| { + cx.read_window(self.window_id, |cx| { let cx = ViewContext::immutable(cx, self.view_id); read(cx.read_view(self), &cx) }) diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index dcbe810804b..0331c7922e6 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -92,33 +92,34 @@ impl TestAppContext { self.update(|cx| cx.dispatch_global_action_any(&action)); } - pub fn dispatch_keystroke(&mut self, window_id: usize, keystroke: Keystroke, is_held: bool) { - let handled = self - .cx - .borrow_mut() - .update_window(window_id, |cx| { - if cx.dispatch_keystroke(&keystroke) { - return true; - } + pub fn dispatch_keystroke( + &mut self, + window: AnyWindowHandle, + keystroke: Keystroke, + is_held: bool, + ) { + let handled = window.update(self, |cx| { + if cx.dispatch_keystroke(&keystroke) { + return true; + } - if cx.dispatch_event( - Event::KeyDown(KeyDownEvent { - keystroke: keystroke.clone(), - is_held, - }), - false, - ) { - return true; - } + if cx.dispatch_event( + Event::KeyDown(KeyDownEvent { + keystroke: keystroke.clone(), + is_held, + }), + false, + ) { + return true; + } - false - }) - .unwrap_or(false); + false + }); if !handled && !keystroke.cmd && !keystroke.ctrl { WindowInputHandler { app: self.cx.clone(), - window_id, + window, } .replace_text_in_range(None, &keystroke.key) } @@ -419,13 +420,20 @@ impl BorrowAppContext for TestAppContext { impl BorrowWindowContext for TestAppContext { type Result = T; - fn read_window_with T>(&self, window_id: usize, f: F) -> T { + fn read_window T>(&self, window_id: usize, f: F) -> T { self.cx .borrow() .read_window(window_id, f) .expect("window was closed") } + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + BorrowWindowContext::read_window(self, window_id, f) + } + fn update_window T>( &mut self, window_id: usize, @@ -436,6 +444,13 @@ impl BorrowWindowContext for TestAppContext { .update_window(window_id, f) .expect("window was closed") } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + BorrowWindowContext::update_window(self, window_id, f) + } } impl ModelHandle { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index cc8468edbd7..d960b9da16a 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -144,7 +144,7 @@ impl BorrowAppContext for WindowContext<'_> { impl BorrowWindowContext for WindowContext<'_> { type Result = T; - fn read_window_with T>(&self, window_id: usize, f: F) -> T { + fn read_window T>(&self, window_id: usize, f: F) -> T { if self.window_id == window_id { f(self) } else { @@ -152,6 +152,13 @@ impl BorrowWindowContext for WindowContext<'_> { } } + fn read_window_optional(&self, window_id: usize, f: F) -> Option + where + F: FnOnce(&WindowContext) -> Option, + { + BorrowWindowContext::read_window(self, window_id, f) + } + fn update_window T>( &mut self, window_id: usize, @@ -163,6 +170,13 @@ impl BorrowWindowContext for WindowContext<'_> { panic!("update called with id of window that does not belong to this context") } } + + fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + where + F: FnOnce(&mut WindowContext) -> Option, + { + BorrowWindowContext::update_window_optional(self, window_id, f) + } } impl<'a> WindowContext<'a> { diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs index 8ee9f7eeff5..d7c65b11fae 100644 --- a/crates/gpui/src/app/window_input_handler.rs +++ b/crates/gpui/src/app/window_input_handler.rs @@ -2,11 +2,11 @@ use std::{cell::RefCell, ops::Range, rc::Rc}; use pathfinder_geometry::rect::RectF; -use crate::{platform::InputHandler, window::WindowContext, AnyView, AppContext}; +use crate::{platform::InputHandler, window::WindowContext, AnyView, AnyWindowHandle, AppContext}; pub struct WindowInputHandler { pub app: Rc>, - pub window_id: usize, + pub window: AnyWindowHandle, } impl WindowInputHandler { @@ -21,13 +21,12 @@ impl WindowInputHandler { // // See https://github.com/zed-industries/community/issues/444 let mut app = self.app.try_borrow_mut().ok()?; - app.update_window(self.window_id, |cx| { + self.window.update_optional(&mut *app, |cx| { let view_id = cx.window.focused_view_id?; - let view = cx.views.get(&(self.window_id, view_id))?; + let view = cx.views.get(&(self.window.id(), view_id))?; let result = f(view.as_ref(), &cx); Some(result) }) - .flatten() } fn update_focused_view(&mut self, f: F) -> Option @@ -35,11 +34,12 @@ impl WindowInputHandler { F: FnOnce(&mut dyn AnyView, &mut WindowContext, usize) -> T, { let mut app = self.app.try_borrow_mut().ok()?; - app.update_window(self.window_id, |cx| { - let view_id = cx.window.focused_view_id?; - cx.update_any_view(view_id, |view, cx| f(view, cx, view_id)) - }) - .flatten() + self.window + .update(&mut *app, |cx| { + let view_id = cx.window.focused_view_id?; + cx.update_any_view(view_id, |view, cx| f(view, cx, view_id)) + }) + .flatten() } } @@ -83,9 +83,8 @@ impl InputHandler for WindowInputHandler { } fn rect_for_range(&self, range_utf16: Range) -> Option { - self.app - .borrow() - .read_window(self.window_id, |cx| cx.rect_for_text_range(range_utf16)) - .flatten() + self.window.read_optional_with(&*self.app.borrow(), |cx| { + cx.rect_for_text_range(range_utf16) + }) } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 37fab1ee462..c5e0e7e1ad6 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4035,16 +4035,9 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { let should_confirm = settings::get::(cx).confirm_quit; cx.spawn(|mut cx| async move { let mut workspaces = cx - .window_ids() + .windows() .into_iter() - .filter_map(|window_id| { - Some( - cx.root_view(window_id)? - .clone() - .downcast::()? - .downgrade(), - ) - }) + .filter_map(|window| Some(window.downcast::()?.root(&cx)?.downgrade())) .collect::>(); // If multiple windows have unsaved changes, and need a save prompt, From 4e33654abaafd22de5140cc6e40f342351047a2a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 7 Aug 2023 13:53:41 -0700 Subject: [PATCH 048/105] Make LspAdapter::process_diagnostics synchronous Co-authored-by: Nathan --- crates/language/src/language.rs | 14 ++++---------- crates/project/src/project.rs | 29 +++++++++++++---------------- crates/zed/src/languages/rust.rs | 4 ++-- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 125e14d445f..100ab275717 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -182,8 +182,8 @@ impl CachedLspAdapter { self.adapter.workspace_configuration(cx) } - pub async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { - self.adapter.process_diagnostics(params).await + pub fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { + self.adapter.process_diagnostics(params) } pub async fn process_completion(&self, completion_item: &mut lsp::CompletionItem) { @@ -262,7 +262,7 @@ pub trait LspAdapter: 'static + Send + Sync { container_dir: PathBuf, ) -> Option; - async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} async fn process_completion(&self, _: &mut lsp::CompletionItem) {} @@ -1487,12 +1487,6 @@ impl Language { None } - pub async fn process_diagnostics(&self, diagnostics: &mut lsp::PublishDiagnosticsParams) { - for adapter in &self.adapters { - adapter.process_diagnostics(diagnostics).await; - } - } - pub async fn process_completion(self: &Arc, completion: &mut lsp::CompletionItem) { for adapter in &self.adapters { adapter.process_completion(completion).await; @@ -1756,7 +1750,7 @@ impl LspAdapter for Arc { unreachable!(); } - async fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} + fn process_diagnostics(&self, _: &mut lsp::PublishDiagnosticsParams) {} async fn disk_based_diagnostic_sources(&self) -> Vec { self.disk_based_diagnostics_sources.clone() diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 6b905a1faa8..1aa2a2dd40c 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -2769,24 +2769,21 @@ impl Project { language_server .on_notification::({ let adapter = adapter.clone(); - move |mut params, cx| { + move |mut params, mut cx| { let this = this; let adapter = adapter.clone(); - cx.spawn(|mut cx| async move { - adapter.process_diagnostics(&mut params).await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.update_diagnostics( - server_id, - params, - &adapter.disk_based_diagnostic_sources, - cx, - ) - .log_err(); - }); - } - }) - .detach(); + adapter.process_diagnostics(&mut params); + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.update_diagnostics( + server_id, + params, + &adapter.disk_based_diagnostic_sources, + cx, + ) + .log_err(); + }); + } } }) .detach(); diff --git a/crates/zed/src/languages/rust.rs b/crates/zed/src/languages/rust.rs index 97549b00583..3c7f84fec7d 100644 --- a/crates/zed/src/languages/rust.rs +++ b/crates/zed/src/languages/rust.rs @@ -102,7 +102,7 @@ impl LspAdapter for RustLspAdapter { Some("rust-analyzer/flycheck".into()) } - async fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { + fn process_diagnostics(&self, params: &mut lsp::PublishDiagnosticsParams) { lazy_static! { static ref REGEX: Regex = Regex::new("(?m)`([^`]+)\n`$").unwrap(); } @@ -310,7 +310,7 @@ mod tests { }, ], }; - RustLspAdapter.process_diagnostics(&mut params).await; + RustLspAdapter.process_diagnostics(&mut params); assert_eq!(params.diagnostics[0].message, "use of moved value `a`"); From 580c2ea8eb7ec308ba0dc1d003b8addb212bc371 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 7 Aug 2023 17:07:01 -0400 Subject: [PATCH 049/105] Fix test name --- crates/util/src/paths.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index 7e0b240570f..f231669197f 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -294,7 +294,7 @@ mod tests { } #[test] - fn test_path_suffix() { + fn test_icon_suffix() { // No dots in name let path = Path::new("/a/b/c/file_name.rs"); assert_eq!(path.icon_suffix(), Some("rs")); From dbf25ea2ffbb77c8ad2276860c4e24a1da4fd70d Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Mon, 7 Aug 2023 17:24:22 -0400 Subject: [PATCH 050/105] Add syntax highlighting for Cargo.toml files --- crates/zed/src/languages/toml/config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/zed/src/languages/toml/config.toml b/crates/zed/src/languages/toml/config.toml index 4e89f5cabd9..188239a8e0d 100644 --- a/crates/zed/src/languages/toml/config.toml +++ b/crates/zed/src/languages/toml/config.toml @@ -1,5 +1,5 @@ name = "TOML" -path_suffixes = ["toml"] +path_suffixes = ["Cargo.lock", "toml"] line_comment = "# " autoclose_before = ",]}" brackets = [ From ca21626064d078867c7241d7eb39b09f763fa894 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 7 Aug 2023 23:32:27 +0200 Subject: [PATCH 051/105] Baseline: Improve selection rendering for large quantities from 270ms to 90ms --- crates/editor/src/editor.rs | 64 +++++++++++++++++++++++++++++++++- crates/editor/src/element.rs | 67 +++++++++++++----------------------- 2 files changed, 86 insertions(+), 45 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cd5e86b9109..8f8511deb69 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -90,7 +90,7 @@ use std::{ cmp::{self, Ordering, Reverse}, mem, num::NonZeroU32, - ops::{ControlFlow, Deref, DerefMut, Range}, + ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::Path, sync::Arc, time::{Duration, Instant}, @@ -7549,6 +7549,68 @@ impl Editor { results } + pub fn selected_rows( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &Theme, + ) -> Vec> { + 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, + }; + let mut push_region = |start, end| { + if let (Some(start_display), Some(end_display)) = (start, end) { + results.push(start_display..=end_display); + } + }; + let mut start_row = None; + let mut end_row = None; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, buffer).is_ge() { + break; + } + let start = range.start.to_point(buffer).row; + let end = range.end.to_point(buffer).row; + if start_row.is_none() { + assert_eq!(end_row, None); + start_row = Some(start); + end_row = Some(end); + continue; + } + if let Some(current_end) = end_row.as_mut() { + if start > *current_end + 1 { + push_region(start_row, end_row); + start_row = Some(start); + end_row = Some(end); + } else { + // Merge two hunks. + *current_end = end; + } + } else { + unreachable!(); + } + } + // We might still have a hunk that was not rendered (if there was a search hit on the last line) + push_region(start_row, end_row); + + results + } + pub fn highlight_text( &mut self, ranges: Vec>, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2d4b273f5ef..9ef95ec929a 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1107,8 +1107,6 @@ impl EditorElement { if layout.is_singleton && scrollbar_settings.selections { let start_anchor = Anchor::min(); let end_anchor = Anchor::max(); - let mut start_row = None; - let mut end_row = None; let color = scrollbar_theme.selections; let border = Border { width: 1., @@ -1119,54 +1117,35 @@ impl EditorElement { bottom: false, left: true, }; - let mut push_region = |start, end| { - if let (Some(start_display), Some(end_display)) = (start, end) { - let start_y = y_for_row(start_display as f32); - let mut end_y = y_for_row(end_display as f32); - if end_y - start_y < 1. { - end_y = start_y + 1.; - } - let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); - - scene.push_quad(Quad { - bounds, - background: Some(color), - border, - corner_radius: style.thumb.corner_radius, - }) + let mut push_region = |start: u32, end: u32| { + let start_y = y_for_row(start as f32); + let mut end_y = y_for_row(end as f32); + if end_y - start_y < 1. { + end_y = start_y + 1.; } + let bounds = RectF::from_points(vec2f(left, start_y), vec2f(right, end_y)); + + scene.push_quad(Quad { + bounds, + background: Some(color), + border, + corner_radius: style.thumb.corner_radius, + }) }; - for (row, _) in &editor - .background_highlights_in_range_for::( + let start = std::time::Instant::now(); + + let background_ranges = editor + .selected_rows::( start_anchor..end_anchor, &layout.position_map.snapshot, &theme, - ) - { - let start_display = row.start; - let end_display = row.end; - - if start_row.is_none() { - assert_eq!(end_row, None); - start_row = Some(start_display.row()); - end_row = Some(end_display.row()); - continue; - } - if let Some(current_end) = end_row.as_mut() { - if start_display.row() > *current_end + 1 { - push_region(start_row, end_row); - start_row = Some(start_display.row()); - end_row = Some(end_display.row()); - } else { - // Merge two hunks. - *current_end = end_display.row(); - } - } else { - unreachable!(); - } + ); + dbg!(start.elapsed().as_millis()); + for row in background_ranges { + let start = row.start(); + let end = row.end(); + push_region(*start, *end); } - // We might still have a hunk that was not rendered (if there was a search hit on the last line) - push_region(start_row, end_row); } if layout.is_singleton && scrollbar_settings.git_diff { From fa168959764defa98ed2c7ce5173f38afb6135aa Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 00:27:38 +0200 Subject: [PATCH 052/105] Do not query start of range if it's end is the same as the previous hunk's --- crates/editor/src/editor.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f8511deb69..c12bdc9c55c 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7557,12 +7557,11 @@ impl Editor { ) -> Vec> { let mut results = Vec::new(); let buffer = &display_snapshot.buffer_snapshot; - let Some((color_fetcher, ranges)) = self.background_highlights + let Some((_, 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() { @@ -7584,8 +7583,14 @@ impl Editor { if range.start.cmp(&search_range.end, buffer).is_ge() { break; } - let start = range.start.to_point(buffer).row; let end = range.end.to_point(buffer).row; + if let Some(current_row) = &end_row { + if end == *current_row { + continue; + } + } + let start = range.start.to_point(buffer).row; + if start_row.is_none() { assert_eq!(end_row, None); start_row = Some(start); From 42e12213571d475b5019113b49c1a88b3f0f0a30 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:17:11 +0200 Subject: [PATCH 053/105] Add upper bound limit. Remove dbg! statements --- crates/editor/src/editor.rs | 4 +++- crates/editor/src/element.rs | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index c12bdc9c55c..682974d61fb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7553,6 +7553,7 @@ impl Editor { &self, search_range: Range, display_snapshot: &DisplaySnapshot, + count: usize, theme: &Theme, ) -> Vec> { let mut results = Vec::new(); @@ -7572,6 +7573,7 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; + let end_ix = count.min(ranges.len()); let mut push_region = |start, end| { if let (Some(start_display), Some(end_display)) = (start, end) { results.push(start_display..=end_display); @@ -7579,7 +7581,7 @@ impl Editor { }; let mut start_row = None; let mut end_row = None; - for range in &ranges[start_ix..] { + for range in &ranges[start_ix..end_ix] { if range.start.cmp(&search_range.end, buffer).is_ge() { break; } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 9ef95ec929a..735abbbd13f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1132,15 +1132,13 @@ impl EditorElement { corner_radius: style.thumb.corner_radius, }) }; - let start = std::time::Instant::now(); - let background_ranges = editor .selected_rows::( start_anchor..end_anchor, &layout.position_map.snapshot, + 50000, &theme, ); - dbg!(start.elapsed().as_millis()); for row in background_ranges { let start = row.start(); let end = row.end(); From 241d3951b8193d2ee1ba8fd177815531334da33b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:25:30 +0200 Subject: [PATCH 054/105] Remove redundant argument --- crates/editor/src/editor.rs | 1 - crates/editor/src/element.rs | 1 - 2 files changed, 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 682974d61fb..0711e5eb024 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7554,7 +7554,6 @@ impl Editor { search_range: Range, display_snapshot: &DisplaySnapshot, count: usize, - theme: &Theme, ) -> Vec> { let mut results = Vec::new(); let buffer = &display_snapshot.buffer_snapshot; diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 735abbbd13f..c3c8681bcf0 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1137,7 +1137,6 @@ impl EditorElement { start_anchor..end_anchor, &layout.position_map.snapshot, 50000, - &theme, ); for row in background_ranges { let start = row.start(); From b0fc6da55b495dc5f43a934ee8b2d67eb1fbe62e Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:37:27 +0200 Subject: [PATCH 055/105] Use display maps --- crates/editor/src/editor.rs | 21 ++++++++++++--------- crates/editor/src/element.rs | 6 +++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 0711e5eb024..8f4f97ad604 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7554,7 +7554,7 @@ impl Editor { search_range: Range, display_snapshot: &DisplaySnapshot, count: usize, - ) -> Vec> { + ) -> Vec> { let mut results = Vec::new(); let buffer = &display_snapshot.buffer_snapshot; let Some((_, ranges)) = self.background_highlights @@ -7573,24 +7573,27 @@ impl Editor { Ok(i) | Err(i) => i, }; let end_ix = count.min(ranges.len()); - let mut push_region = |start, end| { + 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 = None; - let mut end_row = None; + let mut start_row: Option = None; + let mut end_row: Option = None; for range in &ranges[start_ix..end_ix] { if range.start.cmp(&search_range.end, buffer).is_ge() { break; } - let end = range.end.to_point(buffer).row; + let end = range.end.to_point(buffer); if let Some(current_row) = &end_row { - if end == *current_row { + if end.row == current_row.row { continue; } } - let start = range.start.to_point(buffer).row; + let start = range.start.to_point(buffer); if start_row.is_none() { assert_eq!(end_row, None); @@ -7599,7 +7602,7 @@ impl Editor { continue; } if let Some(current_end) = end_row.as_mut() { - if start > *current_end + 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/editor/src/element.rs b/crates/editor/src/element.rs index c3c8681bcf0..30a9cba85c3 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1117,9 +1117,9 @@ impl EditorElement { bottom: false, left: true, }; - let mut push_region = |start: u32, end: u32| { - let start_y = y_for_row(start as f32); - let mut end_y = y_for_row(end as f32); + let mut push_region = |start: DisplayPoint, end: DisplayPoint| { + let start_y = y_for_row(start.row() as f32); + let mut end_y = y_for_row(end.row() as f32); if end_y - start_y < 1. { end_y = start_y + 1.; } From 371c669e003f8082896706261779231b3c2dce11 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 02:45:15 +0200 Subject: [PATCH 056/105] Address review feedback. Rename selected_rows to background_highlight_row_ranges. Do not return any ranges if there are more than 50k results --- crates/editor/src/editor.rs | 6 ++++-- crates/editor/src/element.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8f4f97ad604..17195cb22b5 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7549,7 +7549,7 @@ impl Editor { results } - pub fn selected_rows( + pub fn background_highlight_row_ranges( &self, search_range: Range, display_snapshot: &DisplaySnapshot, @@ -7616,7 +7616,9 @@ impl Editor { } // We might still have a hunk that was not rendered (if there was a search hit on the last line) push_region(start_row, end_row); - + if results.len() > count { + return vec![]; + } results } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 30a9cba85c3..33ebd4c7bd6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1133,7 +1133,7 @@ impl EditorElement { }) }; let background_ranges = editor - .selected_rows::( + .background_highlight_row_ranges::( start_anchor..end_anchor, &layout.position_map.snapshot, 50000, From 486f5bc6ca186b07c415aea5794631c5f6cbcf80 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 19:08:58 -0600 Subject: [PATCH 057/105] Get compiling --- crates/collab/src/tests/integration_tests.rs | 4 +- .../src/project_shared_notification.rs | 2 +- .../collab_ui/src/sharing_status_indicator.rs | 8 +- crates/editor/src/editor_tests.rs | 6 +- .../src/test/editor_lsp_test_context.rs | 2 +- crates/editor/src/test/editor_test_context.rs | 10 ++- crates/go_to_line/src/go_to_line.rs | 18 ++-- crates/gpui/src/app.rs | 7 ++ crates/gpui/src/app/window.rs | 2 +- crates/vim/src/editor_events.rs | 2 +- crates/vim/src/mode_indicator.rs | 2 +- crates/vim/src/test/vim_test_context.rs | 2 +- crates/workspace/src/workspace.rs | 52 ++++++----- crates/zed/src/zed.rs | 87 +++++++------------ 14 files changed, 97 insertions(+), 107 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 1a8e6d938d6..92ccee91c0e 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -3446,7 +3446,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_a = window_a.add_view(cx_a, |cx| Editor::for_buffer(buffer_a, Some(project_a), cx)); let mut editor_cx_a = EditorTestContext { cx: cx_a, - window_id: window_a.id(), + window: window_a.into(), editor: editor_a, }; @@ -3459,7 +3459,7 @@ async fn test_newline_above_or_below_does_not_move_guest_cursor( let editor_b = window_b.add_view(cx_b, |cx| Editor::for_buffer(buffer_b, Some(project_b), cx)); let mut editor_cx_b = EditorTestContext { cx: cx_b, - window_id: window_b.id(), + window: window_b.into(), editor: editor_b, }; diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 03ab91623b4..d5e7c877f70 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -5,7 +5,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, - AppContext, Entity, View, ViewContext, + AppContext, BorrowWindowContext, Entity, View, ViewContext, }; use std::sync::{Arc, Weak}; use workspace::AppState; diff --git a/crates/collab_ui/src/sharing_status_indicator.rs b/crates/collab_ui/src/sharing_status_indicator.rs index 3a1dde072f8..a39ffc457a0 100644 --- a/crates/collab_ui/src/sharing_status_indicator.rs +++ b/crates/collab_ui/src/sharing_status_indicator.rs @@ -20,11 +20,11 @@ pub fn init(cx: &mut AppContext) { { status_indicator = Some(cx.add_status_bar_item(|_| SharingStatusIndicator)); } - } else if let Some((window_id, _)) = status_indicator.take() { - cx.update_window(window_id, |cx| cx.remove_window()); + } else if let Some(window) = status_indicator.take() { + window.update(cx, |cx| cx.remove_window()); } - } else if let Some((window_id, _)) = status_indicator.take() { - cx.update_window(window_id, |cx| cx.remove_window()); + } else if let Some(window) = status_indicator.take() { + window.update(cx, |cx| cx.remove_window()); } }) .detach(); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index a114cd437b1..9a65e2e9532 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1290,7 +1290,7 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height)); + cx.simulate_window_resize(cx.window.id(), vec2f(100., 4. * line_height)); cx.set_state( &r#"ˇone @@ -1401,7 +1401,7 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window_id, vec2f(1000., 4. * line_height + 0.5)); + cx.simulate_window_resize(cx.window.id(), vec2f(1000., 4. * line_height + 0.5)); cx.set_state( &r#"ˇone @@ -1439,7 +1439,7 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window_id, vec2f(100., 4. * line_height)); + cx.simulate_window_resize(cx.window.id(), vec2f(100., 4. * line_height)); cx.set_state( &r#" diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index f53115f224a..83aaa3b7034 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/crates/editor/src/test/editor_lsp_test_context.rs @@ -99,7 +99,7 @@ impl<'a> EditorLspTestContext<'a> { Self { cx: EditorTestContext { cx, - window_id: window.id(), + window: window.into(), editor, }, lsp, diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index c7ea1b4f385..118cddaa922 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -3,7 +3,8 @@ use crate::{ }; use futures::Future; use gpui::{ - keymap_matcher::Keystroke, AppContext, ContextHandle, ModelContext, ViewContext, ViewHandle, + keymap_matcher::Keystroke, AnyWindowHandle, AppContext, ContextHandle, ModelContext, + ViewContext, ViewHandle, }; use indoc::indoc; use language::{Buffer, BufferSnapshot}; @@ -21,7 +22,7 @@ use super::build_editor; pub struct EditorTestContext<'a> { pub cx: &'a mut gpui::TestAppContext, - pub window_id: usize, + pub window: AnyWindowHandle, pub editor: ViewHandle, } @@ -39,7 +40,7 @@ impl<'a> EditorTestContext<'a> { let editor = window.root(cx); Self { cx, - window_id: window.id(), + window: window.into(), editor, } } @@ -111,7 +112,8 @@ impl<'a> EditorTestContext<'a> { let keystroke_under_test_handle = self.add_assertion_context(format!("Simulated Keystroke: {:?}", keystroke_text)); let keystroke = Keystroke::parse(keystroke_text).unwrap(); - self.cx.dispatch_keystroke(self.window_id, keystroke, false); + + self.cx.dispatch_keystroke(self.window, keystroke, false); keystroke_under_test_handle } diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index 769f2eda55b..fa42a523741 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -135,14 +135,16 @@ impl Entity for GoToLine { fn release(&mut self, cx: &mut AppContext) { let scroll_position = self.prev_scroll_position.take(); - cx.update_window(self.active_editor.window_id(), |cx| { - self.active_editor.update(cx, |editor, cx| { - editor.highlight_rows(None); - if let Some(scroll_position) = scroll_position { - editor.set_scroll_position(scroll_position, cx); - } - }) - }); + if let Some(window) = self.active_editor.window(cx) { + window.update(cx, |cx| { + self.active_editor.update(cx, |editor, cx| { + editor.highlight_rows(None); + if let Some(scroll_position) = scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + }) + }); + } } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 730d0da5a05..b95cb0179a8 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -23,6 +23,7 @@ use std::{ }; use anyhow::{anyhow, Context, Result}; + use derive_more::Deref; use parking_lot::Mutex; use postage::oneshot; @@ -4127,6 +4128,12 @@ impl ViewHandle { self.window_id } + pub fn window(&self, cx: &C) -> C::Result { + cx.read_window(self.window_id, |cx| { + AnyWindowHandle::new(self.window_id, cx.window.root_view.type_id()) + }) + } + pub fn id(&self) -> usize { self.view_id } diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index d960b9da16a..70d02c1fa12 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -175,7 +175,7 @@ impl BorrowWindowContext for WindowContext<'_> { where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window_optional(self, window_id, f) + BorrowWindowContext::update_window(self, window_id, f) } } diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 60e63f98234..4fef6bd715e 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,6 +1,6 @@ use crate::Vim; use editor::{EditorBlurred, EditorFocused, EditorReleased}; -use gpui::AppContext; +use gpui::{AppContext, BorrowWindowContext}; pub fn init(cx: &mut AppContext) { cx.subscribe_global(focused).detach(); diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index 639a7594f11..afd60af848b 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,6 +1,6 @@ use gpui::{ elements::{Empty, Label}, - AnyElement, Element, Entity, Subscription, View, ViewContext, + AnyElement, Element, Entity, Subscription, View, ViewContext, BorrowWindowContext }; use settings::SettingsStore; use workspace::{item::ItemHandle, StatusItemView}; diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index ea09e550916..839ab3aafc8 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -85,7 +85,7 @@ impl<'a> VimTestContext<'a> { } pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { - let window_id = self.window_id; + let window_id = self.window.id(); self.update_window(window_id, |cx| { Vim::update(cx, |vim, cx| { vim.switch_mode(mode, false, cx); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index c5e0e7e1ad6..679c34611b3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3827,9 +3827,9 @@ pub fn activate_workspace_for_project( cx: &mut AsyncAppContext, predicate: impl Fn(&mut Project, &mut ModelContext) -> bool, ) -> Option> { - for window_id in cx.window_ids() { - let handle = cx - .update_window(window_id, |cx| { + for window in cx.windows() { + let handle = window + .update(cx, |cx| { if let Some(workspace_handle) = cx.root_view().clone().downcast::() { let project = workspace_handle.read(cx).project.clone(); if project.update(cx, &predicate) { @@ -3945,18 +3945,23 @@ pub fn join_remote_project( ) -> Task> { cx.spawn(|mut cx| async move { let existing_workspace = cx - .window_ids() + .windows() .into_iter() - .filter_map(|window_id| cx.root_view(window_id)?.clone().downcast::()) - .find(|workspace| { - cx.read_window(workspace.window_id(), |cx| { - workspace.read(cx).project().read(cx).remote_id() == Some(project_id) + .find_map(|window| { + window.downcast::().and_then(|window| { + window.read_root_with(&cx, |workspace, cx| { + if workspace.project().read(cx).remote_id() == Some(project_id) { + Some(cx.handle().downgrade()) + } else { + None + } + }) }) - .unwrap_or(false) - }); + }) + .flatten(); let workspace = if let Some(existing_workspace) = existing_workspace { - existing_workspace.downgrade() + existing_workspace } else { let active_call = cx.read(ActiveCall::global); let room = active_call @@ -4034,19 +4039,19 @@ pub fn join_remote_project( pub fn restart(_: &Restart, cx: &mut AppContext) { let should_confirm = settings::get::(cx).confirm_quit; cx.spawn(|mut cx| async move { - let mut workspaces = cx + let mut workspace_windows = cx .windows() .into_iter() - .filter_map(|window| Some(window.downcast::()?.root(&cx)?.downgrade())) + .filter_map(|window| window.downcast::()) .collect::>(); // If multiple windows have unsaved changes, and need a save prompt, // prompt in the active window before switching to a different window. - workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id())); + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - if let (true, Some(workspace)) = (should_confirm, workspaces.first()) { + if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { let answer = cx.prompt( - workspace.window_id(), + window.id(), PromptLevel::Info, "Are you sure you want to restart?", &["Restart", "Cancel"], @@ -4061,14 +4066,13 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { } // If the user cancels any save prompt, then keep the app open. - for workspace in workspaces { - if !workspace - .update(&mut cx, |workspace, cx| { - workspace.prepare_to_close(true, cx) - })? - .await? - { - return Ok(()); + for window in workspace_windows { + if let Some(close) = window.update_root(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) { + if !close.await? { + return Ok(()); + } } } cx.platform().restart(); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index a8a287fb4e6..2b9321b303e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -406,26 +406,19 @@ pub fn build_window_options( fn quit(_: &Quit, cx: &mut gpui::AppContext) { let should_confirm = settings::get::(cx).confirm_quit; cx.spawn(|mut cx| async move { - let mut workspaces = cx - .window_ids() + let mut workspace_windows = cx + .windows() .into_iter() - .filter_map(|window_id| { - Some( - cx.root_view(window_id)? - .clone() - .downcast::()? - .downgrade(), - ) - }) + .filter_map(|window| window.downcast::()) .collect::>(); // If multiple windows have unsaved changes, and need a save prompt, // prompt in the active window before switching to a different window. - workspaces.sort_by_key(|workspace| !cx.window_is_active(workspace.window_id())); + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - if let (true, Some(workspace)) = (should_confirm, workspaces.first()) { + if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { let answer = cx.prompt( - workspace.window_id(), + window.id(), PromptLevel::Info, "Are you sure you want to quit?", &["Quit", "Cancel"], @@ -440,14 +433,13 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) { } // If the user cancels any save prompt, then keep the app open. - for workspace in workspaces { - if !workspace - .update(&mut cx, |workspace, cx| { - workspace.prepare_to_close(true, cx) - })? - .await? - { - return Ok(()); + for window in workspace_windows { + if let Some(close) = window.update_root(&mut cx, |workspace, cx| { + workspace.prepare_to_close(false, cx) + }) { + if close.await? { + return Ok(()); + } } } cx.platform().quit(); @@ -782,17 +774,13 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 1); + assert_eq!(cx.windows().len(), 1); cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 1); - let workspace_1 = cx - .read_window(cx.window_ids()[0], |cx| cx.root_view().clone()) - .unwrap() - .downcast::() - .unwrap(); + assert_eq!(cx.windows().len(), 1); + let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); workspace_1.update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2); assert!(workspace.left_dock().read(cx).is_open()); @@ -809,28 +797,22 @@ mod tests { }) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 2); + assert_eq!(cx.windows().len(), 2); // Replace existing windows - let window_id = cx.window_ids()[0]; - let window = cx.read_window(window_id, |cx| cx.window()).flatten(); + let window = cx.windows()[0].downcast::().unwrap(); cx.update(|cx| { open_paths( &[PathBuf::from("/root/c"), PathBuf::from("/root/d")], &app_state, - window, + Some(window), cx, ) }) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 2); - let workspace_1 = cx - .read_window(cx.window_ids()[0], |cx| cx.root_view().clone()) - .unwrap() - .clone() - .downcast::() - .unwrap(); + assert_eq!(cx.windows().len(), 2); + let workspace_1 = cx.windows()[0].downcast::().unwrap().root(cx); workspace_1.update(cx, |workspace, cx| { assert_eq!( workspace @@ -856,14 +838,10 @@ mod tests { cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 1); + assert_eq!(cx.windows().len(), 1); // When opening the workspace, the window is not in a edited state. - let workspace = cx - .read_window(cx.window_ids()[0], |cx| cx.root_view().clone()) - .unwrap() - .downcast::() - .unwrap(); + let workspace = cx.windows()[0].downcast::().unwrap().root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let editor = workspace.read_with(cx, |workspace, cx| { workspace @@ -917,12 +895,12 @@ mod tests { // buffer having unsaved changes. assert!(!cx.simulate_window_close(workspace.window_id())); executor.run_until_parked(); - assert_eq!(cx.window_ids().len(), 1); + assert_eq!(cx.windows().len(), 1); // The window is successfully closed after the user dismisses the prompt. cx.simulate_prompt_answer(workspace.window_id(), 1); executor.run_until_parked(); - assert_eq!(cx.window_ids().len(), 0); + assert_eq!(cx.windows().len(), 0); } #[gpui::test] @@ -935,12 +913,13 @@ mod tests { }) .await; - let window_id = *cx.window_ids().first().unwrap(); - let workspace = cx - .read_window(window_id, |cx| cx.root_view().clone()) + let window = cx + .windows() + .first() .unwrap() .downcast::() .unwrap(); + let workspace = window.root(cx); let editor = workspace.update(cx, |workspace, cx| { workspace @@ -1105,12 +1084,8 @@ mod tests { cx.update(|cx| open_paths(&[PathBuf::from("/dir1/")], &app_state, None, cx)) .await .unwrap(); - assert_eq!(cx.window_ids().len(), 1); - let workspace = cx - .read_window(cx.window_ids()[0], |cx| cx.root_view().clone()) - .unwrap() - .downcast::() - .unwrap(); + assert_eq!(cx.windows().len(), 1); + let workspace = cx.windows()[0].downcast::().unwrap().root(cx); #[track_caller] fn assert_project_panel_selection( From 0197d49230832118cf78f4b5c94d47d390b27d44 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 19:45:43 -0600 Subject: [PATCH 058/105] Move activation simulation to AnyWindowHandle --- crates/gpui/src/app.rs | 31 +++++++++++++++++++++---- crates/gpui/src/app/test_app_context.rs | 28 ++++------------------ crates/workspace/src/workspace.rs | 6 ++--- 3 files changed, 34 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index b95cb0179a8..bd3fbb8149f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -4087,6 +4087,29 @@ impl AnyWindowHandle { pub fn remove(&self, cx: &mut C) -> C::Result<()> { self.update(cx, |cx| cx.remove_window()) } + + pub fn simulate_activation(&self, cx: &mut TestAppContext) { + self.update(cx, |cx| { + let other_window_ids = cx + .windows + .keys() + .filter(|window_id| **window_id != self.window_id) + .copied() + .collect::>(); + + for window_id in other_window_ids { + cx.window_changed_active_status(window_id, false) + } + + cx.window_changed_active_status(self.window_id, true) + }); + } + + pub fn simulate_deactivation(&self, cx: &mut TestAppContext) { + self.update(cx, |cx| { + cx.window_changed_active_status(self.window_id, false); + }) + } } #[repr(transparent)] @@ -6726,25 +6749,25 @@ mod tests { [("window 2", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_2.id())); + window_2.simulate_activation(cx); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 3", false), ("window 2", true)] ); - cx.simulate_window_activation(Some(window_1.id())); + window_1.simulate_activation(cx); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 2", false), ("window 1", true)] ); - cx.simulate_window_activation(Some(window_3.id())); + window_3.simulate_activation(cx); assert_eq!( mem::take(&mut *events.borrow_mut()), [("window 1", false), ("window 3", true)] ); - cx.simulate_window_activation(Some(window_3.id())); + window_3.simulate_activation(cx); assert_eq!(mem::take(&mut *events.borrow_mut()), []); } diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 0331c7922e6..675d8b8528d 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -156,17 +156,16 @@ impl TestAppContext { self.cx.borrow_mut().add_model(build_model) } - pub fn add_window(&mut self, build_root_view: F) -> WindowHandle + pub fn add_window(&mut self, build_root_view: F) -> WindowHandle where - T: View, - F: FnOnce(&mut ViewContext) -> T, + V: View, + F: FnOnce(&mut ViewContext) -> V, { let window = self .cx .borrow_mut() .add_window(Default::default(), build_root_view); - self.simulate_window_activation(Some(window.id())); - + window.simulate_activation(self); WindowHandle::new(window.id()) } @@ -321,25 +320,6 @@ impl TestAppContext { self.platform_window_mut(window_id).resize_handlers = handlers; } - pub fn simulate_window_activation(&self, to_activate: Option) { - self.cx.borrow_mut().update(|cx| { - let other_window_ids = cx - .windows - .keys() - .filter(|window_id| Some(**window_id) != to_activate) - .copied() - .collect::>(); - - for window_id in other_window_ids { - cx.window_changed_active_status(window_id, false) - } - - if let Some(to_activate) = to_activate { - cx.window_changed_active_status(to_activate, true) - } - }); - } - pub fn is_window_edited(&self, window_id: usize) -> bool { self.platform_window_mut(window_id).edited } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 679c34611b3..dc52614ecf4 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4530,7 +4530,7 @@ mod tests { }); // Deactivating the window saves the file. - cx.simulate_window_activation(None); + window.simulate_deactivation(cx); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 1)); @@ -4551,12 +4551,12 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 2)); // Deactivating the window still saves the file. - cx.simulate_window_activation(Some(window.id())); + window.simulate_activation(cx); item.update(cx, |item, cx| { cx.focus_self(); item.is_dirty = true; }); - cx.simulate_window_activation(None); + window.simulate_deactivation(cx); deterministic.run_until_parked(); item.read_with(cx, |item, _| assert_eq!(item.save_count, 3)); From f2be3181a98aab8c711d6c4f92b43dc891dc6ce0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 20:23:04 -0600 Subject: [PATCH 059/105] Move window-related methods from TestAppContext to AnyWindowHandle --- crates/collab/src/tests/integration_tests.rs | 4 +- crates/editor/src/editor_tests.rs | 9 +- crates/gpui/src/app/test_app_context.rs | 132 +++++++++---------- crates/project_panel/src/project_panel.rs | 26 ++-- crates/workspace/src/workspace.rs | 46 +++---- crates/zed/src/zed.rs | 29 ++-- 6 files changed, 118 insertions(+), 128 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 92ccee91c0e..29dcd95eae0 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -1510,7 +1510,7 @@ async fn test_host_disconnect( .unwrap(); assert!(window_b.read_with(cx_b, |cx| editor_b.is_focused(cx))); editor_b.update(cx_b, |editor, cx| editor.insert("X", cx)); - assert!(cx_b.is_window_edited(workspace_b.window_id())); + assert!(window_b.is_edited(cx_b)); // Drop client A's connection. Collaborators should disappear and the project should not be shown as shared. server.forbid_connections(); @@ -1525,7 +1525,7 @@ async fn test_host_disconnect( window_b.read_with(cx_b, |cx| { assert_eq!(cx.focused_view_id(), None); }); - assert!(!cx_b.is_window_edited(workspace_b.window_id())); + assert!(!window_b.is_edited(cx_b)); // Ensure client B is not prompted to save edits when closing window after disconnecting. let can_close = workspace_b diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9a65e2e9532..61fa952f944 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -1290,7 +1290,8 @@ async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut gpui::TestAppCon let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window.id(), vec2f(100., 4. * line_height)); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); cx.set_state( &r#"ˇone @@ -1401,7 +1402,8 @@ async fn test_scroll_page_up_page_down(cx: &mut gpui::TestAppContext) { init_test(cx, |_| {}); let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window.id(), vec2f(1000., 4. * line_height + 0.5)); + let window = cx.window; + window.simulate_resize(vec2f(1000., 4. * line_height + 0.5), &mut cx); cx.set_state( &r#"ˇone @@ -1439,7 +1441,8 @@ async fn test_move_page_up_page_down(cx: &mut gpui::TestAppContext) { let mut cx = EditorTestContext::new(cx).await; let line_height = cx.editor(|editor, cx| editor.style(cx).text.line_height(cx.font_cache())); - cx.simulate_window_resize(cx.window.id(), vec2f(100., 4. * line_height)); + let window = cx.window; + window.simulate_resize(vec2f(100., 4. * line_height), &mut cx); cx.set_state( &r#" diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index 675d8b8528d..e298224caa2 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -202,10 +202,6 @@ impl TestAppContext { self.cx.borrow().windows().collect() } - // pub fn window_ids(&self) -> Vec { - // self.cx.borrow().windows.keys().copied().collect() - // } - pub fn remove_all_windows(&mut self) { self.update(|cx| cx.windows.clear()); } @@ -273,57 +269,6 @@ impl TestAppContext { self.foreground_platform.as_ref().did_prompt_for_new_path() } - pub fn simulate_prompt_answer(&self, window_id: usize, answer: usize) { - use postage::prelude::Sink as _; - - let mut done_tx = self - .platform_window_mut(window_id) - .pending_prompts - .borrow_mut() - .pop_front() - .expect("prompt was not called"); - done_tx.try_send(answer).ok(); - } - - pub fn has_pending_prompt(&self, window_id: usize) -> bool { - let window = self.platform_window_mut(window_id); - let prompts = window.pending_prompts.borrow_mut(); - !prompts.is_empty() - } - - pub fn current_window_title(&self, window_id: usize) -> Option { - self.platform_window_mut(window_id).title.clone() - } - - pub fn simulate_window_close(&self, window_id: usize) -> bool { - let handler = self - .platform_window_mut(window_id) - .should_close_handler - .take(); - if let Some(mut handler) = handler { - let should_close = handler(); - self.platform_window_mut(window_id).should_close_handler = Some(handler); - should_close - } else { - false - } - } - - pub fn simulate_window_resize(&self, window_id: usize, size: Vector2F) { - let mut window = self.platform_window_mut(window_id); - window.size = size; - let mut handlers = mem::take(&mut window.resize_handlers); - drop(window); - for handler in &mut handlers { - handler(); - } - self.platform_window_mut(window_id).resize_handlers = handlers; - } - - pub fn is_window_edited(&self, window_id: usize) -> bool { - self.platform_window_mut(window_id).edited - } - pub fn leak_detector(&self) -> Arc> { self.cx.borrow().leak_detector() } @@ -344,18 +289,6 @@ impl TestAppContext { self.assert_dropped(weak); } - fn platform_window_mut(&self, window_id: usize) -> std::cell::RefMut { - std::cell::RefMut::map(self.cx.borrow_mut(), |state| { - let window = state.windows.get_mut(&window_id).unwrap(); - let test_window = window - .platform_window - .as_any_mut() - .downcast_mut::() - .unwrap(); - test_window - }) - } - pub fn set_condition_duration(&mut self, duration: Option) { self.condition_duration = duration; } @@ -545,6 +478,71 @@ impl ModelHandle { } } +impl AnyWindowHandle { + pub fn has_pending_prompt(&self, cx: &mut TestAppContext) -> bool { + let window = self.platform_window_mut(cx); + let prompts = window.pending_prompts.borrow_mut(); + !prompts.is_empty() + } + + pub fn current_title(&self, cx: &mut TestAppContext) -> Option { + self.platform_window_mut(cx).title.clone() + } + + pub fn simulate_close(&self, cx: &mut TestAppContext) -> bool { + let handler = self.platform_window_mut(cx).should_close_handler.take(); + if let Some(mut handler) = handler { + let should_close = handler(); + self.platform_window_mut(cx).should_close_handler = Some(handler); + should_close + } else { + false + } + } + + pub fn simulate_resize(&self, size: Vector2F, cx: &mut TestAppContext) { + let mut window = self.platform_window_mut(cx); + window.size = size; + let mut handlers = mem::take(&mut window.resize_handlers); + drop(window); + for handler in &mut handlers { + handler(); + } + self.platform_window_mut(cx).resize_handlers = handlers; + } + + pub fn is_edited(&self, cx: &mut TestAppContext) -> bool { + self.platform_window_mut(cx).edited + } + + pub fn simulate_prompt_answer(&self, answer: usize, cx: &mut TestAppContext) { + use postage::prelude::Sink as _; + + let mut done_tx = self + .platform_window_mut(cx) + .pending_prompts + .borrow_mut() + .pop_front() + .expect("prompt was not called"); + done_tx.try_send(answer).ok(); + } + + fn platform_window_mut<'a>( + &self, + cx: &'a mut TestAppContext, + ) -> std::cell::RefMut<'a, platform::test::Window> { + std::cell::RefMut::map(cx.cx.borrow_mut(), |state| { + let window = state.windows.get_mut(&self.window_id).unwrap(); + let test_window = window + .platform_window + .as_any_mut() + .downcast_mut::() + .unwrap(); + test_window + }) + } +} + impl ViewHandle { pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { use postage::prelude::{Sink as _, Stream as _}; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 021ea2d3bc3..07aaea812a9 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1726,7 +1726,7 @@ impl ClipboardEntry { #[cfg(test)] mod tests { use super::*; - use gpui::{TestAppContext, ViewHandle}; + use gpui::{AnyWindowHandle, TestAppContext, ViewHandle}; use pretty_assertions::assert_eq; use project::FakeFs; use serde_json::json; @@ -2421,7 +2421,7 @@ mod tests { ); ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx); - submit_deletion(window_id, &panel, cx); + submit_deletion(window.into(), &panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2432,7 +2432,7 @@ mod tests { ], "Project panel should have no deleted file, no other file is selected in it" ); - ensure_no_open_items_and_panes(window_id, &workspace, cx); + ensure_no_open_items_and_panes(window.into(), &workspace, cx); select_path(&panel, "src/test/second.rs", cx); panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); @@ -2464,13 +2464,13 @@ mod tests { .expect("Open item should be an editor"); open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); }); - submit_deletion(window_id, &panel, cx); + submit_deletion(window.into(), &panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &["v src", " v test", " third.rs"], "Project panel should have no deleted file, with one last file remaining" ); - ensure_no_open_items_and_panes(window_id, &workspace, cx); + ensure_no_open_items_and_panes(window.into(), &workspace, cx); } #[gpui::test] @@ -2910,12 +2910,12 @@ mod tests { } fn submit_deletion( - window_id: usize, + window: AnyWindowHandle, panel: &ViewHandle, cx: &mut TestAppContext, ) { assert!( - !cx.has_pending_prompt(window_id), + !window.has_pending_prompt(cx), "Should have no prompts before the deletion" ); panel.update(cx, |panel, cx| { @@ -2925,27 +2925,27 @@ mod tests { .detach_and_log_err(cx); }); assert!( - cx.has_pending_prompt(window_id), + window.has_pending_prompt(cx), "Should have a prompt after the deletion" ); - cx.simulate_prompt_answer(window_id, 0); + window.simulate_prompt_answer(0, cx); assert!( - !cx.has_pending_prompt(window_id), + !window.has_pending_prompt(cx), "Should have no prompts after prompt was replied to" ); cx.foreground().run_until_parked(); } fn ensure_no_open_items_and_panes( - window_id: usize, + window: AnyWindowHandle, workspace: &ViewHandle, cx: &mut TestAppContext, ) { assert!( - !cx.has_pending_prompt(window_id), + !window.has_pending_prompt(cx), "Should have no prompts after deletion operation closes the file" ); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let open_project_paths = workspace .read(cx) .panes() diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index dc52614ecf4..da708c6ea72 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4197,17 +4197,11 @@ mod tests { .map(|e| e.id) ); }); - assert_eq!( - cx.current_window_title(window.id()).as_deref(), - Some("one.txt — root1") - ); + assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); // Add a second item to a non-empty pane workspace.update(cx, |workspace, cx| workspace.add_item(Box::new(item2), cx)); - assert_eq!( - cx.current_window_title(window.id()).as_deref(), - Some("two.txt — root1") - ); + assert_eq!(window.current_title(cx).as_deref(), Some("two.txt — root1")); project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), @@ -4223,10 +4217,7 @@ mod tests { }) .await .unwrap(); - assert_eq!( - cx.current_window_title(window.id()).as_deref(), - Some("one.txt — root1") - ); + assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root1")); project.read_with(cx, |project, cx| { assert_eq!( project.active_entry(), @@ -4244,16 +4235,13 @@ mod tests { .await .unwrap(); assert_eq!( - cx.current_window_title(window.id()).as_deref(), + window.current_title(cx).as_deref(), Some("one.txt — root1, root2") ); // Remove a project folder project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - assert_eq!( - cx.current_window_title(window.id()).as_deref(), - Some("one.txt — root2") - ); + assert_eq!(window.current_title(cx).as_deref(), Some("one.txt — root2")); } #[gpui::test] @@ -4287,9 +4275,9 @@ mod tests { }); let task = workspace.update(cx, |w, cx| w.prepare_to_close(false, cx)); cx.foreground().run_until_parked(); - cx.simulate_prompt_answer(window.id(), 2 /* cancel */); + window.simulate_prompt_answer(2, cx); // cancel cx.foreground().run_until_parked(); - assert!(!cx.has_pending_prompt(window.id())); + assert!(!window.has_pending_prompt(cx)); assert!(!task.await.unwrap()); } @@ -4348,10 +4336,10 @@ mod tests { assert_eq!(pane.items_len(), 4); assert_eq!(pane.active_item().unwrap().id(), item1.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(window.has_pending_prompt(cx)); // Confirm saving item 1. - cx.simulate_prompt_answer(window.id(), 0); + window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); // Item 1 is saved. There's a prompt to save item 3. @@ -4362,10 +4350,10 @@ mod tests { assert_eq!(pane.items_len(), 3); assert_eq!(pane.active_item().unwrap().id(), item3.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(window.has_pending_prompt(cx)); // Cancel saving item 3. - cx.simulate_prompt_answer(window.id(), 1); + window.simulate_prompt_answer(1, cx); cx.foreground().run_until_parked(); // Item 3 is reloaded. There's a prompt to save item 4. @@ -4376,10 +4364,10 @@ mod tests { assert_eq!(pane.items_len(), 2); assert_eq!(pane.active_item().unwrap().id(), item4.id()); }); - assert!(cx.has_pending_prompt(window.id())); + assert!(window.has_pending_prompt(cx)); // Confirm saving item 4. - cx.simulate_prompt_answer(window.id(), 0); + window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); // There's a prompt for a path for item 4. @@ -4482,7 +4470,7 @@ mod tests { &[ProjectEntryId::from_proto(0)] ); }); - cx.simulate_prompt_answer(window.id(), 0); + window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { @@ -4491,7 +4479,7 @@ mod tests { &[ProjectEntryId::from_proto(2)] ); }); - cx.simulate_prompt_answer(window.id(), 0); + window.simulate_prompt_answer(0, cx); cx.foreground().run_until_parked(); close.await.unwrap(); @@ -4593,7 +4581,7 @@ mod tests { pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); - assert!(!cx.has_pending_prompt(window.id())); + assert!(!window.has_pending_prompt(cx)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Add the item again, ensuring autosave is prevented if the underlying file has been deleted. @@ -4614,7 +4602,7 @@ mod tests { let _close_items = pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); - assert!(cx.has_pending_prompt(window.id())); + assert!(window.has_pending_prompt(cx)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 2b9321b303e..3435727b1e0 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -841,7 +841,8 @@ mod tests { assert_eq!(cx.windows().len(), 1); // When opening the workspace, the window is not in a edited state. - let workspace = cx.windows()[0].downcast::().unwrap().root(cx); + let window = cx.windows()[0].downcast::().unwrap(); + let workspace = window.root(cx); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let editor = workspace.read_with(cx, |workspace, cx| { workspace @@ -850,19 +851,19 @@ mod tests { .downcast::() .unwrap() }); - assert!(!cx.is_window_edited(workspace.window_id())); + assert!(!window.is_edited(cx)); // Editing a buffer marks the window as edited. editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); - assert!(cx.is_window_edited(workspace.window_id())); + assert!(window.is_edited(cx)); // Undoing the edit restores the window's edited state. editor.update(cx, |editor, cx| editor.undo(&Default::default(), cx)); - assert!(!cx.is_window_edited(workspace.window_id())); + assert!(!window.is_edited(cx)); // Redoing the edit marks the window as edited again. editor.update(cx, |editor, cx| editor.redo(&Default::default(), cx)); - assert!(cx.is_window_edited(workspace.window_id())); + assert!(window.is_edited(cx)); // Closing the item restores the window's edited state. let close = pane.update(cx, |pane, cx| { @@ -870,9 +871,10 @@ mod tests { pane.close_active_item(&Default::default(), cx).unwrap() }); executor.run_until_parked(); - cx.simulate_prompt_answer(workspace.window_id(), 1); + + window.simulate_prompt_answer(1, cx); close.await.unwrap(); - assert!(!cx.is_window_edited(workspace.window_id())); + assert!(!window.is_edited(cx)); // Opening the buffer again doesn't impact the window's edited state. cx.update(|cx| open_paths(&[PathBuf::from("/root/a")], &app_state, None, cx)) @@ -885,20 +887,20 @@ mod tests { .downcast::() .unwrap() }); - assert!(!cx.is_window_edited(workspace.window_id())); + assert!(!window.is_edited(cx)); // Editing the buffer marks the window as edited. editor.update(cx, |editor, cx| editor.insert("EDIT", cx)); - assert!(cx.is_window_edited(workspace.window_id())); + assert!(window.is_edited(cx)); // Ensure closing the window via the mouse gets preempted due to the // buffer having unsaved changes. - assert!(!cx.simulate_window_close(workspace.window_id())); + assert!(!window.simulate_close(cx)); executor.run_until_parked(); assert_eq!(cx.windows().len(), 1); // The window is successfully closed after the user dismisses the prompt. - cx.simulate_prompt_answer(workspace.window_id(), 1); + window.simulate_prompt_answer(1, cx); executor.run_until_parked(); assert_eq!(cx.windows().len(), 0); } @@ -1273,7 +1275,6 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); // Open a file within an existing worktree. workspace @@ -1299,7 +1300,7 @@ mod tests { cx.read(|cx| assert!(editor.is_dirty(cx))); let save_task = workspace.update(cx, |workspace, cx| workspace.save_active_item(false, cx)); - cx.simulate_prompt_answer(window_id, 0); + window.simulate_prompt_answer(0, cx); save_task.await.unwrap(); editor.read_with(cx, |editor, cx| { assert!(!editor.is_dirty(cx)); @@ -1506,7 +1507,7 @@ mod tests { cx.dispatch_action(window_id, workspace::CloseActiveItem); cx.foreground().run_until_parked(); - cx.simulate_prompt_answer(window_id, 1); + window.simulate_prompt_answer(1, cx); cx.foreground().run_until_parked(); workspace.read_with(cx, |workspace, cx| { From 0f332238b3076d17fbaf2ff920cf9eaeda6ac2ac Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 22:08:44 -0600 Subject: [PATCH 060/105] Remove unused method --- crates/gpui/src/app/test_app_context.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index e298224caa2..e0fd0cb8e9f 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -125,13 +125,6 @@ impl TestAppContext { } } - pub fn window(&self, window_id: usize) -> Option> { - self.cx - .borrow() - .read_window(window_id, |cx| cx.window()) - .flatten() - } - pub fn read_window T>( &self, window_id: usize, From f0da6b05fdfec04e4583be1231c7d755dad24646 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 22:15:53 -0600 Subject: [PATCH 061/105] Remove TestAppContext::add_view Instead, we now call this on window handles. --- crates/collab/src/tests.rs | 44 ++----------------- crates/collab/src/tests/integration_tests.rs | 40 ++++++++--------- crates/command_palette/src/command_palette.rs | 3 +- crates/diagnostics/src/diagnostics.rs | 6 +-- crates/editor/src/editor_tests.rs | 3 +- crates/gpui/src/app/test_app_context.rs | 9 ---- crates/project_symbols/src/project_symbols.rs | 3 +- crates/search/src/buffer_search.rs | 17 +++---- crates/search/src/project_search.rs | 3 +- 9 files changed, 35 insertions(+), 93 deletions(-) diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 4804f5b0f1f..febe43ce5e1 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -12,10 +12,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{ - elements::*, executor::Deterministic, AnyElement, Entity, ModelHandle, TestAppContext, View, - ViewContext, ViewHandle, WeakViewHandle, -}; +use gpui::{executor::Deterministic, ModelHandle, TestAppContext, WindowHandle}; use language::LanguageRegistry; use parking_lot::Mutex; use project::{Project, WorktreeId}; @@ -466,43 +463,8 @@ impl TestClient { &self, project: &ModelHandle, cx: &mut TestAppContext, - ) -> ViewHandle { - struct WorkspaceContainer { - workspace: Option>, - } - - impl Entity for WorkspaceContainer { - type Event = (); - } - - impl View for WorkspaceContainer { - fn ui_name() -> &'static str { - "WorkspaceContainer" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(workspace) = self - .workspace - .as_ref() - .and_then(|workspace| workspace.upgrade(cx)) - { - ChildView::new(&workspace, cx).into_any() - } else { - Empty::new().into_any() - } - } - } - - // We use a workspace container so that we don't need to remove the window in order to - // drop the workspace and we can use a ViewHandle instead. - let window = cx.add_window(|_| WorkspaceContainer { workspace: None }); - let container = window.root(cx); - let workspace = window.add_view(cx, |cx| Workspace::test_new(project.clone(), cx)); - container.update(cx, |container, cx| { - container.workspace = Some(workspace.downgrade()); - cx.notify(); - }); - workspace + ) -> WindowHandle { + cx.add_window(|cx| Workspace::test_new(project.clone(), cx)) } } diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 29dcd95eae0..8ad7864adfc 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6441,8 +6441,10 @@ async fn test_basic_following( .await .unwrap(); - let workspace_a = client_a.build_workspace(&project_a, cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let window_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = window_a.root(cx_a); + let window_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = window_b.root(cx_b); // Client A opens some editors. let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); @@ -6525,7 +6527,7 @@ async fn test_basic_following( cx_c.foreground().run_until_parked(); let active_call_c = cx_c.read(ActiveCall::global); let project_c = client_c.build_remote_project(project_id, cx_c).await; - let workspace_c = client_c.build_workspace(&project_c, cx_c); + let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); active_call_c .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .await @@ -6543,7 +6545,7 @@ async fn test_basic_following( cx_d.foreground().run_until_parked(); let active_call_d = cx_d.read(ActiveCall::global); let project_d = client_d.build_remote_project(project_id, cx_d).await; - let workspace_d = client_d.build_workspace(&project_d, cx_d); + let workspace_d = client_d.build_workspace(&project_d, cx_d).root(cx_d); active_call_d .update(cx_d, |call, cx| call.set_location(Some(&project_d), cx)) .await @@ -6870,9 +6872,7 @@ async fn test_basic_following( }); // Client B activates a panel, and the previously-opened screen-sharing item gets activated. - let panel = cx_b.add_view(workspace_b.window_id(), |_| { - TestPanel::new(DockPosition::Left) - }); + let panel = window_b.add_view(cx_b, |_| TestPanel::new(DockPosition::Left)); workspace_b.update(cx_b, |workspace, cx| { workspace.add_panel(panel, cx); workspace.toggle_panel_focus::(cx); @@ -6900,7 +6900,7 @@ async fn test_basic_following( // Client B activates an item that doesn't implement following, // so the previously-opened screen-sharing item gets activated. - let unfollowable_item = cx_b.add_view(workspace_b.window_id(), |_| TestItem::new()); + let unfollowable_item = window_b.add_view(cx_b, |_| TestItem::new()); workspace_b.update(cx_b, |workspace, cx| { workspace.active_pane().update(cx, |pane, cx| { pane.add_item(Box::new(unfollowable_item), true, true, None, cx) @@ -7062,10 +7062,10 @@ async fn test_following_tab_order( .await .unwrap(); - let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let client_b_id = project_a.read_with(cx_a, |project, _| { @@ -7188,7 +7188,7 @@ async fn test_peers_following_each_other( .unwrap(); // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let pane_a1 = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone()); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { @@ -7200,7 +7200,7 @@ async fn test_peers_following_each_other( .unwrap(); // Client B opens an editor. - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); let pane_b1 = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let _editor_b1 = workspace_b .update(cx_b, |workspace, cx| { @@ -7359,7 +7359,7 @@ async fn test_auto_unfollowing( .unwrap(); // Client A opens some editors. - let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let _editor_a1 = workspace_a .update(cx_a, |workspace, cx| { workspace.open_path((worktree_id, "1.txt"), None, true, cx) @@ -7370,7 +7370,7 @@ async fn test_auto_unfollowing( .unwrap(); // Client B starts following client A. - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); let pane_b = workspace_b.read_with(cx_b, |workspace, _| workspace.active_pane().clone()); let leader_id = project_b.read_with(cx_b, |project, _| { project.collaborators().values().next().unwrap().peer_id @@ -7498,14 +7498,14 @@ async fn test_peers_simultaneously_following_each_other( client_a.fs.insert_tree("/a", json!({})).await; let (project_a, _) = client_a.build_local_project("/a", cx_a).await; - let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); let project_id = active_call_a .update(cx_a, |call, cx| call.share_project(project_a.clone(), cx)) .await .unwrap(); let project_b = client_b.build_remote_project(project_id, cx_b).await; - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); deterministic.run_until_parked(); let client_a_id = project_b.read_with(cx_b, |project, _| { @@ -7887,7 +7887,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( .await .unwrap(); - let workspace_a = client_a.build_workspace(&project_a, cx_a); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); cx_a.foreground().start_waiting(); let _buffer_a = project_a @@ -7955,7 +7955,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Host editor update the cache version after every cache/view change", ); }); - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); let editor_b = workspace_b .update(cx_b, |workspace, cx| { workspace.open_path((worktree_id, "main.rs"), None, true, cx) @@ -8194,8 +8194,8 @@ async fn test_inlay_hint_refresh_is_forwarded( .await .unwrap(); - let workspace_a = client_a.build_workspace(&project_a, cx_a); - let workspace_b = client_b.build_workspace(&project_b, cx_b); + let workspace_a = client_a.build_workspace(&project_a, cx_a).root(cx_a); + let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); cx_a.foreground().start_waiting(); cx_b.foreground().start_waiting(); diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 7d4b4126b70..f51a6c92380 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -297,8 +297,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); - let editor = cx.add_view(window_id, |cx| { + let editor = window.add_view(cx, |cx| { let mut editor = Editor::single_line(None, cx); editor.set_text("abc", cx); editor diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 2444465be66..16a7340fae4 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -857,7 +857,6 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); // Create some diagnostics project.update(cx, |project, cx| { @@ -944,7 +943,7 @@ mod tests { }); // Open the project diagnostics view while there are already diagnostics. - let view = cx.add_view(window_id, |cx| { + let view = window.add_view(cx, |cx| { ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) }); @@ -1252,9 +1251,8 @@ mod tests { let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); - let view = cx.add_view(window_id, |cx| { + let view = window.add_view(cx, |cx| { ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) }); diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index c9688011a44..ec1cc124989 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -525,9 +525,8 @@ async fn test_navigation_history(cx: &mut TestAppContext) { let project = Project::test(fs, [], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - cx.add_view(window_id, |cx| { + window.add_view(cx, |cx| { let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx); let mut editor = build_editor(buffer.clone(), cx); let handle = cx.handle(); diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index e0fd0cb8e9f..ff988607b09 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -162,15 +162,6 @@ impl TestAppContext { WindowHandle::new(window.id()) } - pub fn add_view(&mut self, window_id: usize, build_view: F) -> ViewHandle - where - T: View, - F: FnOnce(&mut ViewContext) -> T, - { - self.update_window(window_id, |cx| cx.add_view(build_view)) - .expect("window not found") - } - pub fn observe_global(&mut self, callback: F) -> Subscription where E: Any, diff --git a/crates/project_symbols/src/project_symbols.rs b/crates/project_symbols/src/project_symbols.rs index 8471f3a3a70..e88aee5dcfb 100644 --- a/crates/project_symbols/src/project_symbols.rs +++ b/crates/project_symbols/src/project_symbols.rs @@ -328,10 +328,9 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); // Create the project symbols view. - let symbols = cx.add_view(window_id, |cx| { + let symbols = window.add_view(cx, |cx| { ProjectSymbols::new( ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()), cx, diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index 1e635432bd5..b20ffed6d7e 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -850,12 +850,9 @@ mod tests { ) }); let window = cx.add_window(|_| EmptyView); + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); - let editor = cx.add_view(window.id(), |cx| { - Editor::for_buffer(buffer.clone(), None, cx) - }); - - let search_bar = cx.add_view(window.id(), |cx| { + let search_bar = window.add_view(cx, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1234,9 +1231,9 @@ mod tests { let window = cx.add_window(|_| EmptyView); let window_id = window.id(); - let editor = cx.add_view(window_id, |cx| Editor::for_buffer(buffer.clone(), None, cx)); + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = cx.add_view(window_id, |cx| { + let search_bar = window.add_view(cx, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); @@ -1421,11 +1418,9 @@ mod tests { let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let editor = cx.add_view(window.id(), |cx| { - Editor::for_buffer(buffer.clone(), None, cx) - }); + let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); - let search_bar = cx.add_view(window.id(), |cx| { + let search_bar = window.add_view(cx, |cx| { let mut search_bar = BufferSearchBar::new(cx); search_bar.set_active_pane_item(Some(&editor), cx); search_bar.show(cx); diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 0db66b4e378..dffd88db142 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1874,7 +1874,6 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); workspace.update(cx, |workspace, cx| { ProjectSearchView::deploy(workspace, &workspace::NewSearch, cx) }); @@ -1889,7 +1888,7 @@ pub mod tests { .expect("Search view expected to appear after new search event trigger") }); - let search_bar = cx.add_view(window_id, |cx| { + let search_bar = window.add_view(cx, |cx| { let mut search_bar = ProjectSearchBar::new(); search_bar.set_active_pane_item(Some(&search_view), cx); // search_bar.show(cx); From dba2facd23d94ebb79aab8a32b4318ab28320c83 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 7 Aug 2023 22:58:01 -0600 Subject: [PATCH 062/105] Remove window via handles --- crates/collab_ui/src/incoming_call_notification.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index a9c5e697a5f..6f86a74300a 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -7,7 +7,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, - AnyElement, AppContext, Entity, View, ViewContext, + AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, }; use util::ResultExt; use workspace::AppState; @@ -16,10 +16,10 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { let app_state = Arc::downgrade(app_state); let mut incoming_call = ActiveCall::global(cx).read(cx).incoming(); cx.spawn(|mut cx| async move { - let mut notification_windows = Vec::new(); + let mut notification_windows: Vec> = Vec::new(); while let Some(incoming_call) = incoming_call.next().await { - for window_id in notification_windows.drain(..) { - cx.remove_window(window_id); + for window in notification_windows.drain(..) { + window.remove(&mut cx); } if let Some(incoming_call) = incoming_call { @@ -49,7 +49,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { |_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()), ); - notification_windows.push(window.id()); + notification_windows.push(window); } } } From 1aff642981fd8b6fd9f4a0b58d7ca43758485edb Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 8 Aug 2023 13:09:27 +0200 Subject: [PATCH 063/105] Do not highlgiht selections at all over the threshold --- crates/editor/src/editor.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 17195cb22b5..02cd58524bb 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7572,7 +7572,6 @@ impl Editor { }) { Ok(i) | Err(i) => i, }; - let end_ix = count.min(ranges.len()); let mut push_region = |start: Option, end: Option| { if let (Some(start_display), Some(end_display)) = (start, end) { results.push( @@ -7583,7 +7582,10 @@ impl Editor { }; let mut start_row: Option = None; let mut end_row: Option = None; - for range in &ranges[start_ix..end_ix] { + if ranges.len() > count { + return vec![]; + } + for range in &ranges[start_ix..] { if range.start.cmp(&search_range.end, buffer).is_ge() { break; } @@ -7616,9 +7618,6 @@ impl Editor { } // We might still have a hunk that was not rendered (if there was a search hit on the last line) push_region(start_row, end_row); - if results.len() > count { - return vec![]; - } results } From 49f1f1c6c263e1346f0534660c8ce2c8c70859be Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 09:13:17 -0600 Subject: [PATCH 064/105] Remove window when closing workspace in test --- crates/collab/src/tests/integration_tests.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 8ad7864adfc..ce7fd8a094a 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -6527,7 +6527,8 @@ async fn test_basic_following( cx_c.foreground().run_until_parked(); let active_call_c = cx_c.read(ActiveCall::global); let project_c = client_c.build_remote_project(project_id, cx_c).await; - let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); + let window_c = client_c.build_workspace(&project_c, cx_c); + let workspace_c = window_c.root(cx_c); active_call_c .update(cx_c, |call, cx| call.set_location(Some(&project_c), cx)) .await @@ -6643,6 +6644,7 @@ async fn test_basic_following( } // Client C closes the project. + window_c.remove(cx_c); cx_c.drop_last(workspace_c); // Clients A and B see that client B is following A, and client C is not present in the followers. From d896d89842ed11021482aaa477177d1203f56347 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:08:37 -0600 Subject: [PATCH 065/105] Store an AnyWindowHandle in WindowContext --- crates/collab_ui/src/contact_list.rs | 4 +- .../src/project_shared_notification.rs | 16 +- crates/command_palette/src/command_palette.rs | 8 +- crates/context_menu/src/context_menu.rs | 12 +- crates/file_finder/src/file_finder.rs | 42 +- crates/go_to_line/src/go_to_line.rs | 18 +- crates/gpui/src/app.rs | 621 +++++++++--------- crates/gpui/src/app/test_app_context.rs | 32 +- crates/gpui/src/app/window.rs | 149 ++--- crates/project_panel/src/project_panel.rs | 28 +- crates/search/src/buffer_search.rs | 15 +- crates/search/src/project_search.rs | 25 +- crates/vim/src/editor_events.rs | 10 +- crates/vim/src/mode_indicator.rs | 4 +- crates/vim/src/test/vim_test_context.rs | 4 +- crates/workspace/src/dock.rs | 12 +- crates/workspace/src/pane.rs | 4 +- crates/workspace/src/workspace.rs | 22 +- crates/zed/src/zed.rs | 59 +- 19 files changed, 526 insertions(+), 559 deletions(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 428f2156d11..a2b281856d9 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -305,7 +305,7 @@ impl ContactList { github_login ); let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); - let window_id = cx.window_id(); + let window = cx.window(); cx.spawn(|_, mut cx| async move { if answer.next().await == Some(0) { if let Err(e) = user_store @@ -313,7 +313,7 @@ impl ContactList { .await { cx.prompt( - window_id, + window, PromptLevel::Info, &format!("Failed to remove contact: {}", e), &["Ok"], diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index d5e7c877f70..63922f2b65c 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -5,7 +5,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions}, - AppContext, BorrowWindowContext, Entity, View, ViewContext, + AppContext, Entity, View, ViewContext, }; use std::sync::{Arc, Weak}; use workspace::AppState; @@ -52,20 +52,20 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { notification_windows .entry(*project_id) .or_insert(Vec::new()) - .push(window.id()); + .push(window); } } room::Event::RemoteProjectUnshared { project_id } => { - if let Some(window_ids) = notification_windows.remove(&project_id) { - for window_id in window_ids { - cx.update_window(window_id, |cx| cx.remove_window()); + if let Some(windows) = notification_windows.remove(&project_id) { + for window in windows { + window.remove(cx); } } } room::Event::Left => { - for (_, window_ids) in notification_windows.drain() { - for window_id in window_ids { - cx.update_window(window_id, |cx| cx.remove_window()); + for (_, windows) in notification_windows.drain() { + for window in windows { + window.remove(cx); } } } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index f51a6c92380..b3703aa64a4 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -80,11 +80,11 @@ impl PickerDelegate for CommandPaletteDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let window_id = cx.window_id(); let view_id = self.focused_view_id; + let window = cx.window(); cx.spawn(move |picker, mut cx| async move { let actions = cx - .available_actions(window_id, view_id) + .available_actions(window, view_id) .into_iter() .filter_map(|(name, action, bindings)| { let filtered = cx.read(|cx| { @@ -162,13 +162,13 @@ impl PickerDelegate for CommandPaletteDelegate { fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if !self.matches.is_empty() { - let window_id = cx.window_id(); + let window = cx.window(); let focused_view_id = self.focused_view_id; let action_ix = self.matches[self.selected_ix].candidate_id; let action = self.actions.remove(action_ix).action; cx.app_context() .spawn(move |mut cx| async move { - cx.dispatch_action(window_id, focused_view_id, action.as_ref()) + cx.dispatch_action(window, focused_view_id, action.as_ref()) }) .detach_and_log_err(cx); } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index f58afab361f..75cfd872b29 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -218,12 +218,12 @@ impl ContextMenu { if let Some(ContextMenuItem::Item { action, .. }) = self.items.get(ix) { match action { ContextMenuItemAction::Action(action) => { - let window_id = cx.window_id(); + let window = cx.window(); let view_id = self.parent_view_id; let action = action.boxed_clone(); cx.app_context() .spawn(|mut cx| async move { - cx.dispatch_action(window_id, view_id, action.as_ref()) + cx.dispatch_action(window, view_id, action.as_ref()) }) .detach_and_log_err(cx); } @@ -480,17 +480,13 @@ impl ContextMenu { .on_down(MouseButton::Left, |_, _, _| {}) // Capture these events .on_click(MouseButton::Left, move |_, menu, cx| { menu.cancel(&Default::default(), cx); - let window_id = cx.window_id(); + let window = cx.window(); match &action { ContextMenuItemAction::Action(action) => { let action = action.boxed_clone(); cx.app_context() .spawn(|mut cx| async move { - cx.dispatch_action( - window_id, - view_id, - action.as_ref(), - ) + cx.dispatch_action(window, view_id, action.as_ref()) }) .detach_and_log_err(cx); } diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 12bf3242627..523d6e8a5ca 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -619,7 +619,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.into(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder @@ -632,8 +632,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -674,7 +674,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.into(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -706,8 +706,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -758,7 +758,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - cx.dispatch_action(window.id(), Toggle); + cx.dispatch_action(window.into(), Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); let file_query = &first_file_name[..3]; @@ -790,8 +790,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window.id(), SelectNext); - cx.dispatch_action(window.id(), Confirm); + cx.dispatch_action(window.into(), SelectNext); + cx.dispatch_action(window.into(), Confirm); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) .await; @@ -1168,7 +1168,6 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); @@ -1186,7 +1185,7 @@ mod tests { "fir", 1, "first.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1201,7 +1200,7 @@ mod tests { "sec", 1, "second.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1223,7 +1222,7 @@ mod tests { "thi", 1, "third.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1255,7 +1254,7 @@ mod tests { "sec", 1, "second.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1294,7 +1293,7 @@ mod tests { "thi", 1, "third.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1376,7 +1375,6 @@ mod tests { let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let worktree_id = cx.read(|cx| { let worktrees = workspace.read(cx).worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1,); @@ -1411,7 +1409,7 @@ mod tests { "sec", 1, "second.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1433,7 +1431,7 @@ mod tests { "fir", 1, "first.rs", - window_id, + window.into(), &workspace, &deterministic, cx, @@ -1465,12 +1463,12 @@ mod tests { input: &str, expected_matches: usize, expected_editor_title: &str, - window_id: usize, + window: gpui::AnyWindowHandle, workspace: &ViewHandle, deterministic: &gpui::executor::Deterministic, cx: &mut gpui::TestAppContext, ) -> Vec { - cx.dispatch_action(window_id, Toggle); + cx.dispatch_action(window, Toggle); let finder = cx.read(|cx| workspace.read(cx).modal::().unwrap()); finder .update(cx, |finder, cx| { @@ -1487,8 +1485,8 @@ mod tests { }); let active_pane = cx.read(|cx| workspace.read(cx).active_pane().clone()); - cx.dispatch_action(window_id, SelectNext); - cx.dispatch_action(window_id, Confirm); + cx.dispatch_action(window, SelectNext); + cx.dispatch_action(window, Confirm); deterministic.run_until_parked(); active_pane .condition(cx, |pane, _| pane.active_item().is_some()) diff --git a/crates/go_to_line/src/go_to_line.rs b/crates/go_to_line/src/go_to_line.rs index fa42a523741..1d3b44fa435 100644 --- a/crates/go_to_line/src/go_to_line.rs +++ b/crates/go_to_line/src/go_to_line.rs @@ -135,16 +135,14 @@ impl Entity for GoToLine { fn release(&mut self, cx: &mut AppContext) { let scroll_position = self.prev_scroll_position.take(); - if let Some(window) = self.active_editor.window(cx) { - window.update(cx, |cx| { - self.active_editor.update(cx, |editor, cx| { - editor.highlight_rows(None); - if let Some(scroll_position) = scroll_position { - editor.set_scroll_position(scroll_position, cx); - } - }) - }); - } + self.active_editor.window().update(cx, |cx| { + self.active_editor.update(cx, |editor, cx| { + editor.highlight_rows(None); + if let Some(scroll_position) = scroll_position { + editor.set_scroll_position(scroll_position, cx); + } + }) + }); } } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ff8e343c2ca..e0f46b036a0 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -134,16 +134,16 @@ pub trait BorrowAppContext { pub trait BorrowWindowContext { type Result; - fn read_window(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T; - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option; - fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T; - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option; } @@ -308,12 +308,12 @@ impl App { result } - fn update_window(&mut self, window_id: usize, callback: F) -> Option + fn update_window(&mut self, window: AnyWindowHandle, callback: F) -> Option where F: FnOnce(&mut WindowContext) -> T, { let mut state = self.0.borrow_mut(); - let result = state.update_window(window_id, callback); + let result = state.update_window(window, callback); state.pending_notifications.clear(); result } @@ -346,22 +346,22 @@ impl AsyncAppContext { pub fn read_window T>( &self, - window_id: usize, + window: AnyWindowHandle, callback: F, ) -> Option { - self.0.borrow_mut().read_window(window_id, callback) + self.0.borrow_mut().read_window(window, callback) } pub fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, callback: F, ) -> Option { - self.0.borrow_mut().update_window(window_id, callback) + self.0.borrow_mut().update_window(window, callback) } - pub fn debug_elements(&self, window_id: usize) -> Option { - self.0.borrow().read_window(window_id, |cx| { + pub fn debug_elements(&self, window: AnyWindowHandle) -> Option { + self.0.borrow().read_window(window, |cx| { let root_view = cx.window.root_view(); let root_element = cx.window.rendered_views.get(&root_view.id())?; root_element.debug(cx).log_err() @@ -370,13 +370,13 @@ impl AsyncAppContext { pub fn dispatch_action( &mut self, - window_id: usize, + window: AnyWindowHandle, view_id: usize, action: &dyn Action, ) -> Result<()> { self.0 .borrow_mut() - .update_window(window_id, |cx| { + .update_window(window, |cx| { cx.dispatch_action(Some(view_id), action); }) .ok_or_else(|| anyhow!("window not found")) @@ -384,10 +384,10 @@ impl AsyncAppContext { pub fn available_actions( &self, - window_id: usize, + window: AnyWindowHandle, view_id: usize, ) -> Vec<(&'static str, Box, SmallVec<[Binding; 1]>)> { - self.read_window(window_id, |cx| cx.available_actions(view_id)) + self.read_window(window, |cx| cx.available_actions(view_id)) .unwrap_or_default() } @@ -411,23 +411,23 @@ impl AsyncAppContext { self.update(|cx| cx.add_window(window_options, build_root_view)) } - pub fn remove_window(&mut self, window_id: usize) { - self.update_window(window_id, |cx| cx.remove_window()); + pub fn remove_window(&mut self, window: AnyWindowHandle) { + self.update_window(window, |cx| cx.remove_window()); } - pub fn activate_window(&mut self, window_id: usize) { - self.update_window(window_id, |cx| cx.activate_window()); + pub fn activate_window(&mut self, window: AnyWindowHandle) { + self.update_window(window, |cx| cx.activate_window()); } // TODO: Can we eliminate this method and move it to WindowContext then call it with update_window?s pub fn prompt( &mut self, - window_id: usize, + window: AnyWindowHandle, level: PromptLevel, msg: &str, answers: &[&str], ) -> Option> { - self.update_window(window_id, |cx| cx.prompt(level, msg, answers)) + self.update_window(window, |cx| cx.prompt(level, msg, answers)) } pub fn platform(&self) -> Arc { @@ -456,38 +456,36 @@ impl BorrowAppContext for AsyncAppContext { impl BorrowWindowContext for AsyncAppContext { type Result = Option; - fn read_window(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T, { - self.0.borrow().read_with(|cx| cx.read_window(window_id, f)) + self.0.borrow().read_with(|cx| cx.read_window(window, f)) } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { self.0 .borrow_mut() - .update(|cx| cx.read_window_optional(window_id, f)) + .update(|cx| cx.read_window_optional(window, f)) } - fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + fn update_window(&mut self, window: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, { - self.0 - .borrow_mut() - .update(|cx| cx.update_window(window_id, f)) + self.0.borrow_mut().update(|cx| cx.update_window(window, f)) } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { self.0 .borrow_mut() - .update(|cx| cx.update_window_optional(window_id, f)) + .update(|cx| cx.update_window_optional(window, f)) } } @@ -534,7 +532,7 @@ pub struct AppContext { global_actions: HashMap>, keystroke_matcher: KeymapMatcher, next_id: usize, - // next_window_id: usize, + // next_window: AnyWindowHandle, next_subscription_id: usize, frame_count: usize, @@ -794,13 +792,13 @@ impl AppContext { } } - pub fn view_ui_name(&self, window_id: usize, view_id: usize) -> Option<&'static str> { - Some(self.views.get(&(window_id, view_id))?.ui_name()) + pub fn view_ui_name(&self, window: AnyWindowHandle, view_id: usize) -> Option<&'static str> { + Some(self.views.get(&(window.id(), view_id))?.ui_name()) } - pub fn view_type_id(&self, window_id: usize, view_id: usize) -> Option { + pub fn view_type_id(&self, window: AnyWindowHandle, view_id: usize) -> Option { self.views_metadata - .get(&(window_id, view_id)) + .get(&(window.id(), view_id)) .map(|metadata| metadata.type_id) } @@ -823,11 +821,11 @@ impl AppContext { fn read_window T>( &self, - window_id: usize, + handle: AnyWindowHandle, callback: F, ) -> Option { - let window = self.windows.get(&window_id)?; - let window_context = WindowContext::immutable(self, &window, window_id); + let window = self.windows.get(&handle.id())?; + let window_context = WindowContext::immutable(self, &window, handle); Some(callback(&window_context)) } @@ -835,9 +833,8 @@ impl AppContext { &mut self, callback: F, ) -> Option { - self.platform - .main_window_id() - .and_then(|id| self.update_window(id, callback)) + self.main_window() + .and_then(|window| window.update(self, callback)) } pub fn prompt_for_paths( @@ -1075,10 +1072,10 @@ impl AppContext { } } - fn notify_view(&mut self, window_id: usize, view_id: usize) { + fn notify_view(&mut self, window: AnyWindowHandle, view_id: usize) { if self.pending_notifications.insert(view_id) { self.pending_effects - .push_back(Effect::ViewNotification { window_id, view_id }); + .push_back(Effect::ViewNotification { window, view_id }); } } @@ -1096,13 +1093,13 @@ impl AppContext { pub fn is_action_available(&self, action: &dyn Action) -> bool { let mut available_in_window = false; let action_id = action.id(); - if let Some(window_id) = self.platform.main_window_id() { + if let Some(window) = self.main_window() { available_in_window = self - .read_window(window_id, |cx| { + .read_window(window, |cx| { if let Some(focused_view_id) = cx.focused_view_id() { for view_id in cx.ancestors(focused_view_id) { if let Some(view_metadata) = - cx.views_metadata.get(&(window_id, view_id)) + cx.views_metadata.get(&(cx.window_handle.id(), view_id)) { if let Some(actions) = cx.actions.get(&view_metadata.type_id) { if actions.contains_key(&action_id) { @@ -1367,13 +1364,12 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { let handle: AnyWindowHandle = handle.into(); - let window_id = handle.id(); { let mut app = self.upgrade(); platform_window.on_event(Box::new(move |event| { - app.update_window(window_id, |cx| { + app.update_window(handle, |cx| { if let Event::KeyDown(KeyDownEvent { keystroke, .. }) = &event { if cx.dispatch_keystroke(keystroke) { return true; @@ -1389,35 +1385,35 @@ impl AppContext { { let mut app = self.upgrade(); platform_window.on_active_status_change(Box::new(move |is_active| { - app.update(|cx| cx.window_changed_active_status(window_id, is_active)) + app.update(|cx| cx.window_changed_active_status(handle, is_active)) })); } { let mut app = self.upgrade(); platform_window.on_resize(Box::new(move || { - app.update(|cx| cx.window_was_resized(window_id)) + app.update(|cx| cx.window_was_resized(handle)) })); } { let mut app = self.upgrade(); platform_window.on_moved(Box::new(move || { - app.update(|cx| cx.window_was_moved(window_id)) + app.update(|cx| cx.window_was_moved(handle)) })); } { let mut app = self.upgrade(); platform_window.on_fullscreen(Box::new(move |is_fullscreen| { - app.update(|cx| cx.window_was_fullscreen_changed(window_id, is_fullscreen)) + app.update(|cx| cx.window_was_fullscreen_changed(handle, is_fullscreen)) })); } { let mut app = self.upgrade(); platform_window.on_close(Box::new(move || { - app.update(|cx| cx.update_window(window_id, |cx| cx.remove_window())); + app.update(|cx| cx.update_window(handle, |cx| cx.remove_window())); })); } @@ -1432,8 +1428,8 @@ impl AppContext { window: handle, })); - let mut window = Window::new(window_id, platform_window, self, build_root_view); - let mut cx = WindowContext::mutable(self, &mut window, window_id); + let mut window = Window::new(handle, platform_window, self, build_root_view); + let mut cx = WindowContext::mutable(self, &mut window, handle); cx.layout(false).expect("initial layout should not error"); let scene = cx.paint().expect("initial paint should not error"); window.platform_window.present_scene(scene); @@ -1455,7 +1451,7 @@ impl AppContext { } pub fn read_view(&self, handle: &ViewHandle) -> &T { - if let Some(view) = self.views.get(&(handle.window_id, handle.view_id)) { + if let Some(view) = self.views.get(&(handle.window.id(), handle.view_id)) { view.as_any().downcast_ref().expect("downcast is type safe") } else { panic!("circular view reference for type {}", type_name::()); @@ -1465,7 +1461,7 @@ impl AppContext { fn upgrade_view_handle(&self, handle: &WeakViewHandle) -> Option> { if self.ref_counts.lock().is_entity_alive(handle.view_id) { Some(ViewHandle::new( - handle.window_id, + handle.window, handle.view_id, &self.ref_counts, )) @@ -1477,7 +1473,7 @@ impl AppContext { fn upgrade_any_view_handle(&self, handle: &AnyWeakViewHandle) -> Option { if self.ref_counts.lock().is_entity_alive(handle.view_id) { Some(AnyViewHandle::new( - handle.window_id, + handle.window, handle.view_id, handle.view_type, self.ref_counts.clone(), @@ -1585,9 +1581,10 @@ impl AppContext { observations.emit(model_id, |callback| callback(self)); } - Effect::ViewNotification { window_id, view_id } => { - self.handle_view_notification_effect(window_id, view_id) - } + Effect::ViewNotification { + window: window_id, + view_id, + } => self.handle_view_notification_effect(window_id, view_id), Effect::GlobalNotification { type_id } => { let mut subscriptions = self.global_observations.clone(); @@ -1618,13 +1615,13 @@ impl AppContext { Effect::Focus(mut effect) => { if focus_effects - .get(&effect.window_id()) + .get(&effect.window().id()) .map_or(false, |prev_effect| prev_effect.is_forced()) { effect.force(); } - focus_effects.insert(effect.window_id(), effect); + focus_effects.insert(effect.window().id(), effect); } Effect::FocusObservation { @@ -1639,42 +1636,38 @@ impl AppContext { ); } - Effect::ResizeWindow { window_id } => { - if let Some(window) = self.windows.get_mut(&window_id) { + Effect::ResizeWindow { window } => { + if let Some(window) = self.windows.get_mut(&window.id()) { window .invalidation .get_or_insert(WindowInvalidation::default()); } - self.handle_window_moved(window_id); + self.handle_window_moved(window); } - Effect::MoveWindow { window_id } => { - self.handle_window_moved(window_id); + Effect::MoveWindow { window } => { + self.handle_window_moved(window); } Effect::WindowActivationObservation { - window_id, + window, subscription_id, callback, } => self.window_activation_observations.add_callback( - window_id, + window.id(), subscription_id, callback, ), - Effect::ActivateWindow { - window_id, - is_active, - } => { - if self.handle_window_activation_effect(window_id, is_active) - && is_active + Effect::ActivateWindow { window, is_active } => { + if self.handle_window_activation_effect(window, is_active) && is_active { focus_effects - .entry(window_id) + .entry(window.id()) .or_insert_with(|| FocusEffect::View { - window_id, + window, view_id: self - .read_window(window_id, |cx| cx.focused_view_id()) + .read_window(window, |cx| cx.focused_view_id()) .flatten(), is_forced: true, }) @@ -1683,26 +1676,26 @@ impl AppContext { } Effect::WindowFullscreenObservation { - window_id, + window, subscription_id, callback, } => self.window_fullscreen_observations.add_callback( - window_id, + window.id(), subscription_id, callback, ), Effect::FullscreenWindow { - window_id, + window, is_fullscreen, - } => self.handle_fullscreen_effect(window_id, is_fullscreen), + } => self.handle_fullscreen_effect(window, is_fullscreen), Effect::WindowBoundsObservation { - window_id, + window, subscription_id, callback, } => self.window_bounds_observations.add_callback( - window_id, + window.id(), subscription_id, callback, ), @@ -1714,18 +1707,15 @@ impl AppContext { Effect::ActionDispatchNotification { action_id } => { self.handle_action_dispatch_notification_effect(action_id) } - Effect::WindowShouldCloseSubscription { - window_id, - callback, - } => { - self.handle_window_should_close_subscription_effect(window_id, callback) + Effect::WindowShouldCloseSubscription { window, callback } => { + self.handle_window_should_close_subscription_effect(window, callback) } Effect::Keystroke { - window_id, + window, keystroke, handled_by, result, - } => self.handle_keystroke_effect(window_id, keystroke, handled_by, result), + } => self.handle_keystroke_effect(window, keystroke, handled_by, result), Effect::ActiveLabeledTasksChanged => { self.handle_active_labeled_tasks_changed_effect() } @@ -1740,8 +1730,8 @@ impl AppContext { } self.pending_notifications.clear(); } else { - for window_id in self.windows.keys().cloned().collect::>() { - self.update_window(window_id, |cx| { + for window in self.windows().collect::>() { + self.update_window(window, |cx| { let invalidation = if refreshing { let mut invalidation = cx.window.invalidation.take().unwrap_or_default(); @@ -1757,7 +1747,7 @@ impl AppContext { let appearance = cx.window.platform_window.appearance(); cx.invalidate(invalidation, appearance); if let Some(old_parents) = cx.layout(refreshing).log_err() { - updated_windows.insert(window_id); + updated_windows.insert(window); if let Some(focused_view_id) = cx.focused_view_id() { let old_ancestors = std::iter::successors( @@ -1772,7 +1762,7 @@ impl AppContext { for old_ancestor in old_ancestors.iter().copied() { if !new_ancestors.contains(&old_ancestor) { if let Some(mut view) = - cx.views.remove(&(window_id, old_ancestor)) + cx.views.remove(&(window.id(), old_ancestor)) { view.focus_out( focused_view_id, @@ -1780,7 +1770,7 @@ impl AppContext { old_ancestor, ); cx.views - .insert((window_id, old_ancestor), view); + .insert((window.id(), old_ancestor), view); } } } @@ -1789,7 +1779,7 @@ impl AppContext { for new_ancestor in new_ancestors.iter().copied() { if !old_ancestors.contains(&new_ancestor) { if let Some(mut view) = - cx.views.remove(&(window_id, new_ancestor)) + cx.views.remove(&(window.id(), new_ancestor)) { view.focus_in( focused_view_id, @@ -1797,7 +1787,7 @@ impl AppContext { new_ancestor, ); cx.views - .insert((window_id, new_ancestor), view); + .insert((window.id(), new_ancestor), view); } } } @@ -1806,13 +1796,15 @@ impl AppContext { // there isn't any pending focus, focus the root view. let root_view_id = cx.window.root_view().id(); if focused_view_id != root_view_id - && !cx.views.contains_key(&(window_id, focused_view_id)) - && !focus_effects.contains_key(&window_id) + && !cx + .views + .contains_key(&(window.id(), focused_view_id)) + && !focus_effects.contains_key(&window.id()) { focus_effects.insert( - window_id, + window.id(), FocusEffect::View { - window_id, + window, view_id: Some(root_view_id), is_forced: false, }, @@ -1833,8 +1825,8 @@ impl AppContext { callback(self); } - for window_id in updated_windows.drain() { - self.update_window(window_id, |cx| { + for window in updated_windows.drain() { + self.update_window(window, |cx| { if let Some(scene) = cx.paint().log_err() { cx.window.platform_window.present_scene(scene); } @@ -1855,39 +1847,37 @@ impl AppContext { } } - fn window_was_resized(&mut self, window_id: usize) { + fn window_was_resized(&mut self, window: AnyWindowHandle) { self.pending_effects - .push_back(Effect::ResizeWindow { window_id }); + .push_back(Effect::ResizeWindow { window }); } - fn window_was_moved(&mut self, window_id: usize) { + fn window_was_moved(&mut self, window: AnyWindowHandle) { self.pending_effects - .push_back(Effect::MoveWindow { window_id }); + .push_back(Effect::MoveWindow { window }); } - fn window_was_fullscreen_changed(&mut self, window_id: usize, is_fullscreen: bool) { + fn window_was_fullscreen_changed(&mut self, window: AnyWindowHandle, is_fullscreen: bool) { self.pending_effects.push_back(Effect::FullscreenWindow { - window_id, + window, is_fullscreen, }); } - fn window_changed_active_status(&mut self, window_id: usize, is_active: bool) { - self.pending_effects.push_back(Effect::ActivateWindow { - window_id, - is_active, - }); + fn window_changed_active_status(&mut self, window: AnyWindowHandle, is_active: bool) { + self.pending_effects + .push_back(Effect::ActivateWindow { window, is_active }); } fn keystroke( &mut self, - window_id: usize, + window: AnyWindowHandle, keystroke: Keystroke, handled_by: Option>, result: MatchResult, ) { self.pending_effects.push_back(Effect::Keystroke { - window_id, + window, keystroke, handled_by, result, @@ -1910,16 +1900,16 @@ impl AppContext { fn handle_view_notification_effect( &mut self, - observed_window_id: usize, + observed_window: AnyWindowHandle, observed_view_id: usize, ) { - let view_key = (observed_window_id, observed_view_id); + let view_key = (observed_window.id(), observed_view_id); if let Some((view, mut view_metadata)) = self .views .remove(&view_key) .zip(self.views_metadata.remove(&view_key)) { - if let Some(window) = self.windows.get_mut(&observed_window_id) { + if let Some(window) = self.windows.get_mut(&observed_window.id()) { window .invalidation .get_or_insert_with(Default::default) @@ -1946,17 +1936,17 @@ impl AppContext { }) } - fn handle_fullscreen_effect(&mut self, window_id: usize, is_fullscreen: bool) { - self.update_window(window_id, |cx| { + fn handle_fullscreen_effect(&mut self, window: AnyWindowHandle, is_fullscreen: bool) { + self.update_window(window, |cx| { cx.window.is_fullscreen = is_fullscreen; let mut fullscreen_observations = cx.window_fullscreen_observations.clone(); - fullscreen_observations.emit(window_id, |callback| callback(is_fullscreen, cx)); + fullscreen_observations.emit(window.id(), |callback| callback(is_fullscreen, cx)); if let Some(uuid) = cx.window_display_uuid() { let bounds = cx.window_bounds(); let mut bounds_observations = cx.window_bounds_observations.clone(); - bounds_observations.emit(window_id, |callback| callback(bounds, uuid, cx)); + bounds_observations.emit(window.id(), |callback| callback(bounds, uuid, cx)); } Some(()) @@ -1965,42 +1955,42 @@ impl AppContext { fn handle_keystroke_effect( &mut self, - window_id: usize, + window: AnyWindowHandle, keystroke: Keystroke, handled_by: Option>, result: MatchResult, ) { - self.update_window(window_id, |cx| { + self.update_window(window, |cx| { let mut observations = cx.keystroke_observations.clone(); - observations.emit(window_id, move |callback| { + observations.emit(window.id(), move |callback| { callback(&keystroke, &result, handled_by.as_ref(), cx) }); }); } - fn handle_window_activation_effect(&mut self, window_id: usize, active: bool) -> bool { - self.update_window(window_id, |cx| { + fn handle_window_activation_effect(&mut self, window: AnyWindowHandle, active: bool) -> bool { + self.update_window(window, |cx| { if cx.window.is_active == active { return false; } cx.window.is_active = active; let mut observations = cx.window_activation_observations.clone(); - observations.emit(window_id, |callback| callback(active, cx)); + observations.emit(window.id(), |callback| callback(active, cx)); true }) .unwrap_or(false) } fn handle_focus_effect(&mut self, effect: FocusEffect) { - let window_id = effect.window_id(); - self.update_window(window_id, |cx| { + let window = effect.window(); + self.update_window(window, |cx| { // Ensure the newly-focused view still exists, otherwise focus // the root view instead. let focused_id = match effect { FocusEffect::View { view_id, .. } => { if let Some(view_id) = view_id { - if cx.views.contains_key(&(window_id, view_id)) { + if cx.views.contains_key(&(window.id(), view_id)) { Some(view_id) } else { Some(cx.root_view().id()) @@ -2025,9 +2015,9 @@ impl AppContext { if focus_changed { if let Some(blurred_id) = blurred_id { for view_id in cx.ancestors(blurred_id).collect::>() { - if let Some(mut view) = cx.views.remove(&(window_id, view_id)) { + if let Some(mut view) = cx.views.remove(&(window.id(), view_id)) { view.focus_out(blurred_id, cx, view_id); - cx.views.insert((window_id, view_id), view); + cx.views.insert((window.id(), view_id), view); } } @@ -2039,9 +2029,9 @@ impl AppContext { if focus_changed || effect.is_forced() { if let Some(focused_id) = focused_id { for view_id in cx.ancestors(focused_id).collect::>() { - if let Some(mut view) = cx.views.remove(&(window_id, view_id)) { + if let Some(mut view) = cx.views.remove(&(window.id(), view_id)) { view.focus_in(focused_id, cx, view_id); - cx.views.insert((window_id, view_id), view); + cx.views.insert((window.id(), view_id), view); } } @@ -2063,24 +2053,24 @@ impl AppContext { fn handle_window_should_close_subscription_effect( &mut self, - window_id: usize, + window: AnyWindowHandle, mut callback: WindowShouldCloseSubscriptionCallback, ) { let mut app = self.upgrade(); - if let Some(window) = self.windows.get_mut(&window_id) { + if let Some(window) = self.windows.get_mut(&window.id()) { window .platform_window .on_should_close(Box::new(move || app.update(|cx| callback(cx)))) } } - fn handle_window_moved(&mut self, window_id: usize) { - self.update_window(window_id, |cx| { + fn handle_window_moved(&mut self, window: AnyWindowHandle) { + self.update_window(window, |cx| { if let Some(display) = cx.window_display_uuid() { let bounds = cx.window_bounds(); cx.window_bounds_observations .clone() - .emit(window_id, move |callback| { + .emit(window.id(), move |callback| { callback(bounds, display, cx); true }); @@ -2097,10 +2087,10 @@ impl AppContext { }); } - pub fn focus(&mut self, window_id: usize, view_id: Option) { + pub fn focus(&mut self, window: AnyWindowHandle, view_id: Option) { self.pending_effects .push_back(Effect::Focus(FocusEffect::View { - window_id, + window, view_id, is_forced: false, })); @@ -2185,40 +2175,40 @@ impl BorrowAppContext for AppContext { impl BorrowWindowContext for AppContext { type Result = Option; - fn read_window(&self, window_id: usize, f: F) -> Self::Result + fn read_window(&self, window: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&WindowContext) -> T, { - AppContext::read_window(self, window_id, f) + AppContext::read_window(self, window, f) } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - AppContext::read_window(self, window_id, f).flatten() + AppContext::read_window(self, window, f).flatten() } - fn update_window(&mut self, window_id: usize, f: F) -> Self::Result + fn update_window(&mut self, handle: AnyWindowHandle, f: F) -> Self::Result where F: FnOnce(&mut WindowContext) -> T, { self.update(|app_context| { - let mut window = app_context.windows.remove(&window_id)?; - let mut window_context = WindowContext::mutable(app_context, &mut window, window_id); + let mut window = app_context.windows.remove(&handle.id())?; + let mut window_context = WindowContext::mutable(app_context, &mut window, handle); let result = f(&mut window_context); if !window_context.removed { - app_context.windows.insert(window_id, window); + app_context.windows.insert(handle.id(), window); } Some(result) }) } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, handle: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - AppContext::update_window(self, window_id, f).flatten() + AppContext::update_window(self, handle, f).flatten() } } @@ -2242,22 +2232,22 @@ pub struct WindowInvalidation { #[derive(Debug)] pub enum FocusEffect { View { - window_id: usize, + window: AnyWindowHandle, view_id: Option, is_forced: bool, }, ViewParent { - window_id: usize, + window: AnyWindowHandle, view_id: usize, is_forced: bool, }, } impl FocusEffect { - fn window_id(&self) -> usize { + fn window(&self) -> AnyWindowHandle { match self { - FocusEffect::View { window_id, .. } => *window_id, - FocusEffect::ViewParent { window_id, .. } => *window_id, + FocusEffect::View { window, .. } => *window, + FocusEffect::ViewParent { window, .. } => *window, } } @@ -2303,7 +2293,7 @@ pub enum Effect { model_id: usize, }, ViewNotification { - window_id: usize, + window: AnyWindowHandle, view_id: usize, }, Deferred { @@ -2328,36 +2318,36 @@ pub enum Effect { callback: FocusObservationCallback, }, ResizeWindow { - window_id: usize, + window: AnyWindowHandle, }, MoveWindow { - window_id: usize, + window: AnyWindowHandle, }, ActivateWindow { - window_id: usize, + window: AnyWindowHandle, is_active: bool, }, WindowActivationObservation { - window_id: usize, + window: AnyWindowHandle, subscription_id: usize, callback: WindowActivationCallback, }, FullscreenWindow { - window_id: usize, + window: AnyWindowHandle, is_fullscreen: bool, }, WindowFullscreenObservation { - window_id: usize, + window: AnyWindowHandle, subscription_id: usize, callback: WindowFullscreenCallback, }, WindowBoundsObservation { - window_id: usize, + window: AnyWindowHandle, subscription_id: usize, callback: WindowBoundsCallback, }, Keystroke { - window_id: usize, + window: AnyWindowHandle, keystroke: Keystroke, handled_by: Option>, result: MatchResult, @@ -2367,7 +2357,7 @@ pub enum Effect { action_id: TypeId, }, WindowShouldCloseSubscription { - window_id: usize, + window: AnyWindowHandle, callback: WindowShouldCloseSubscriptionCallback, }, ActiveLabeledTasksChanged, @@ -2419,9 +2409,9 @@ impl Debug for Effect { .debug_struct("Effect::ModelNotification") .field("model_id", model_id) .finish(), - Effect::ViewNotification { window_id, view_id } => f + Effect::ViewNotification { window, view_id } => f .debug_struct("Effect::ViewNotification") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("view_id", view_id) .finish(), Effect::GlobalNotification { type_id } => f @@ -2451,71 +2441,68 @@ impl Debug for Effect { .debug_struct("Effect::ActionDispatchNotification") .field("action_id", action_id) .finish(), - Effect::ResizeWindow { window_id } => f + Effect::ResizeWindow { window } => f .debug_struct("Effect::RefreshWindow") - .field("window_id", window_id) + .field("window_id", &window.id()) .finish(), - Effect::MoveWindow { window_id } => f + Effect::MoveWindow { window } => f .debug_struct("Effect::MoveWindow") - .field("window_id", window_id) + .field("window_id", &window.id()) .finish(), Effect::WindowActivationObservation { - window_id, + window, subscription_id, .. } => f .debug_struct("Effect::WindowActivationObservation") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("subscription_id", subscription_id) .finish(), - Effect::ActivateWindow { - window_id, - is_active, - } => f + Effect::ActivateWindow { window, is_active } => f .debug_struct("Effect::ActivateWindow") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("is_active", is_active) .finish(), Effect::FullscreenWindow { - window_id, + window, is_fullscreen, } => f .debug_struct("Effect::FullscreenWindow") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("is_fullscreen", is_fullscreen) .finish(), Effect::WindowFullscreenObservation { - window_id, + window, subscription_id, callback: _, } => f .debug_struct("Effect::WindowFullscreenObservation") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("subscription_id", subscription_id) .finish(), Effect::WindowBoundsObservation { - window_id, + window, subscription_id, callback: _, } => f .debug_struct("Effect::WindowBoundsObservation") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("subscription_id", subscription_id) .finish(), Effect::RefreshWindows => f.debug_struct("Effect::FullViewRefresh").finish(), - Effect::WindowShouldCloseSubscription { window_id, .. } => f + Effect::WindowShouldCloseSubscription { window, .. } => f .debug_struct("Effect::WindowShouldCloseSubscription") - .field("window_id", window_id) + .field("window_id", &window.id()) .finish(), Effect::Keystroke { - window_id, + window, keystroke, handled_by, result, } => f .debug_struct("Effect::Keystroke") - .field("window_id", window_id) + .field("window_id", &window.id()) .field("keystroke", keystroke) .field( "keystroke", @@ -2613,9 +2600,14 @@ pub trait AnyView { cx: &mut WindowContext, view_id: usize, ); - fn any_handle(&self, window_id: usize, view_id: usize, cx: &AppContext) -> AnyViewHandle { + fn any_handle( + &self, + window: AnyWindowHandle, + view_id: usize, + cx: &AppContext, + ) -> AnyViewHandle { AnyViewHandle::new( - window_id, + window, view_id, self.as_any().type_id(), cx.ref_counts.clone(), @@ -2653,7 +2645,7 @@ where fn render(&mut self, cx: &mut WindowContext, view_id: usize) -> Box { let mut view_context = ViewContext::mutable(cx, view_id); let element = V::render(self, &mut view_context); - let view = WeakViewHandle::new(cx.window_id, view_id); + let view = WeakViewHandle::new(cx.window_handle, view_id); Box::new(RootElement::new(element, view)) } @@ -2664,11 +2656,11 @@ where } else { let focused_type = cx .views_metadata - .get(&(cx.window_id, focused_id)) + .get(&(cx.window_handle.id(), focused_id)) .unwrap() .type_id; AnyViewHandle::new( - cx.window_id, + cx.window_handle, focused_id, focused_type, cx.ref_counts.clone(), @@ -2684,11 +2676,11 @@ where } else { let blurred_type = cx .views_metadata - .get(&(cx.window_id, blurred_id)) + .get(&(cx.window_handle.id(), blurred_id)) .unwrap() .type_id; AnyViewHandle::new( - cx.window_id, + cx.window_handle, blurred_id, blurred_type, cx.ref_counts.clone(), @@ -2995,18 +2987,22 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { pub fn handle(&self) -> ViewHandle { ViewHandle::new( - self.window_id, + self.window_handle, self.view_id, &self.window_context.ref_counts, ) } pub fn weak_handle(&self) -> WeakViewHandle { - WeakViewHandle::new(self.window_id, self.view_id) + WeakViewHandle::new(self.window_handle, self.view_id) } pub fn window_id(&self) -> usize { - self.window_id + self.window_handle.id() + } + + pub fn window(&self) -> AnyWindowHandle { + self.window_handle } pub fn view_id(&self) -> usize { @@ -3054,11 +3050,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn focus_parent(&mut self) { - let window_id = self.window_id; + let window = self.window_handle; let view_id = self.view_id; self.pending_effects .push_back(Effect::Focus(FocusEffect::ViewParent { - window_id, + window, view_id, is_forced: false, })); @@ -3072,13 +3068,13 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { where F: 'static + FnMut(&mut V, &mut ViewContext) -> bool, { - let window_id = self.window_id; + let window = self.window_handle; let view = self.weak_handle(); self.pending_effects .push_back(Effect::WindowShouldCloseSubscription { - window_id, + window, callback: Box::new(move |cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(view) = view.upgrade(cx) { view.update(cx, |view, cx| callback(view, cx)) } else { @@ -3117,11 +3113,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { H: Handle, F: 'static + FnMut(&mut V, H, &mut ViewContext), { - let window_id = self.window_id; + let window = self.window_handle; let observer = self.weak_handle(); self.window_context .observe_internal(handle, move |observed, cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { callback(observer, observed, cx); @@ -3140,10 +3136,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { G: Any, F: 'static + FnMut(&mut V, &mut ViewContext), { - let window_id = self.window_id; + let window = self.window_handle; let observer = self.weak_handle(); self.window_context.observe_global::(move |cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| callback(observer, cx)); } @@ -3176,11 +3172,11 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { H: Handle, F: 'static + FnMut(&mut V, &E, &mut ViewContext), { - let window_id = self.window_id; + let window = self.window_handle; let observer = self.weak_handle(); self.window_context .observe_release(handle, move |released, cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { callback(observer, released, cx); @@ -3194,10 +3190,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { where F: 'static + FnMut(&mut V, TypeId, &mut ViewContext), { - let window_id = self.window_id; + let window = self.window_handle; let observer = self.weak_handle(); self.window_context.observe_actions(move |action_id, cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { callback(observer, action_id, cx); @@ -3289,10 +3285,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { where F: 'static + FnMut(&mut V, &mut ViewContext), { - let window_id = self.window_id; + let window = self.window_handle; let observer = self.weak_handle(); self.window_context.observe_active_labeled_tasks(move |cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { if let Some(observer) = observer.upgrade(cx) { observer.update(cx, |observer, cx| { callback(observer, cx); @@ -3316,9 +3312,9 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { } pub fn notify(&mut self) { - let window_id = self.window_id; + let window = self.window_handle; let view_id = self.view_id; - self.window_context.notify_view(window_id, view_id); + self.window_context.notify_view(window, view_id); } pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext)) { @@ -3331,10 +3327,10 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { &mut self, callback: impl 'static + FnOnce(&mut V, &mut ViewContext), ) { - let window_id = self.window_id; + let window = self.window_handle; let handle = self.handle(); self.window_context.after_window_update(move |cx| { - cx.update_window(window_id, |cx| { + cx.update_window(window, |cx| { handle.update(cx, |view, cx| { callback(view, cx); }) @@ -3422,30 +3418,30 @@ impl BorrowAppContext for ViewContext<'_, '_, V> { impl BorrowWindowContext for ViewContext<'_, '_, V> { type Result = T; - fn read_window T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window(&*self.window_context, window_id, f) + fn read_window T>(&self, window: AnyWindowHandle, f: F) -> T { + BorrowWindowContext::read_window(&*self.window_context, window, f) } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - BorrowWindowContext::read_window_optional(&*self.window_context, window_id, f) + BorrowWindowContext::read_window_optional(&*self.window_context, window, f) } fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, f: F, ) -> T { - BorrowWindowContext::update_window(&mut *self.window_context, window_id, f) + BorrowWindowContext::update_window(&mut *self.window_context, window, f) } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window_optional(&mut *self.window_context, window_id, f) + BorrowWindowContext::update_window_optional(&mut *self.window_context, window, f) } } @@ -3483,11 +3479,11 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { ) -> Option> { self.notify_if_view_ancestors_change(view_id); - let window_id = self.window_id; + let window = self.window_handle; let mut contexts = Vec::new(); let mut handler_depth = None; for (i, view_id) in self.ancestors(view_id).enumerate() { - if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) { + if let Some(view_metadata) = self.views_metadata.get(&(window.id(), view_id)) { if let Some(actions) = self.actions.get(&view_metadata.type_id) { if actions.contains_key(&action.id()) { handler_depth = Some(i); @@ -3547,30 +3543,30 @@ impl BorrowAppContext for LayoutContext<'_, '_, '_, V> { impl BorrowWindowContext for LayoutContext<'_, '_, '_, V> { type Result = T; - fn read_window T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window(&*self.view_context, window_id, f) + fn read_window T>(&self, window: AnyWindowHandle, f: F) -> T { + BorrowWindowContext::read_window(&*self.view_context, window, f) } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - BorrowWindowContext::read_window_optional(&*self.view_context, window_id, f) + BorrowWindowContext::read_window_optional(&*self.view_context, window, f) } fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, f: F, ) -> T { - BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) + BorrowWindowContext::update_window(&mut *self.view_context, window, f) } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window_optional(&mut *self.view_context, window_id, f) + BorrowWindowContext::update_window_optional(&mut *self.view_context, window, f) } } @@ -3619,30 +3615,30 @@ impl BorrowAppContext for EventContext<'_, '_, '_, V> { impl BorrowWindowContext for EventContext<'_, '_, '_, V> { type Result = T; - fn read_window T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_window(&*self.view_context, window_id, f) + fn read_window T>(&self, window: AnyWindowHandle, f: F) -> T { + BorrowWindowContext::read_window(&*self.view_context, window, f) } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - BorrowWindowContext::read_window_optional(&*self.view_context, window_id, f) + BorrowWindowContext::read_window_optional(&*self.view_context, window, f) } fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, f: F, ) -> T { - BorrowWindowContext::update_window(&mut *self.view_context, window_id, f) + BorrowWindowContext::update_window(&mut *self.view_context, window, f) } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window_optional(&mut *self.view_context, window_id, f) + BorrowWindowContext::update_window_optional(&mut *self.view_context, window, f) } } @@ -3977,7 +3973,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&mut V, &mut ViewContext) -> R, { - cx.update_window(self.id(), |cx| { + cx.update_window(self.any_handle, |cx| { cx.root_view() .clone() .downcast::() @@ -3991,7 +3987,7 @@ impl WindowHandle { C: BorrowWindowContext, F: FnOnce(&mut ViewContext) -> V, { - cx.update_window(self.id(), |cx| { + cx.update_window(self.any_handle, |cx| { let root_view = self.add_view(cx, |cx| build_root(cx)); cx.window.root_view = Some(root_view.clone().into_any()); cx.window.focused_view_id = Some(root_view.id()); @@ -4006,7 +4002,7 @@ impl Into for WindowHandle { } } -#[derive(Copy, Clone, Eq, PartialEq, Hash)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] pub struct AnyWindowHandle { window_id: usize, root_view_type: TypeId, @@ -4029,7 +4025,7 @@ impl AnyWindowHandle { C: BorrowWindowContext, F: FnOnce(&WindowContext) -> R, { - cx.read_window(self.window_id, |cx| read(cx)) + cx.read_window(*self, |cx| read(cx)) } pub fn read_optional_with(&self, cx: &C, read: F) -> Option @@ -4037,7 +4033,7 @@ impl AnyWindowHandle { C: BorrowWindowContext, F: FnOnce(&WindowContext) -> Option, { - cx.read_window_optional(self.window_id, |cx| read(cx)) + cx.read_window_optional(*self, |cx| read(cx)) } pub fn update(&self, cx: &mut C, update: F) -> C::Result @@ -4045,7 +4041,7 @@ impl AnyWindowHandle { C: BorrowWindowContext, F: FnOnce(&mut WindowContext) -> R, { - cx.update_window(self.window_id, update) + cx.update_window(*self, update) } pub fn update_optional(&self, cx: &mut C, update: F) -> Option @@ -4053,7 +4049,7 @@ impl AnyWindowHandle { C: BorrowWindowContext, F: FnOnce(&mut WindowContext) -> Option, { - cx.update_window_optional(self.window_id, update) + cx.update_window_optional(*self, update) } pub fn add_view(&self, cx: &mut C, build_view: F) -> C::Result> @@ -4092,24 +4088,22 @@ impl AnyWindowHandle { pub fn simulate_activation(&self, cx: &mut TestAppContext) { self.update(cx, |cx| { let other_window_ids = cx - .windows - .keys() - .filter(|window_id| **window_id != self.window_id) - .copied() + .windows() + .filter(|window| *window != *self) .collect::>(); - for window_id in other_window_ids { - cx.window_changed_active_status(window_id, false) + for window in other_window_ids { + cx.window_changed_active_status(window, false) } - cx.window_changed_active_status(self.window_id, true) + cx.window_changed_active_status(*self, true) }); } #[cfg(any(test, feature = "test-support"))] pub fn simulate_deactivation(&self, cx: &mut TestAppContext) { self.update(cx, |cx| { - cx.window_changed_active_status(self.window_id, false); + cx.window_changed_active_status(*self, false); }) } } @@ -4129,20 +4123,15 @@ impl Deref for ViewHandle { } impl ViewHandle { - fn new(window_id: usize, view_id: usize, ref_counts: &Arc>) -> Self { + fn new(window: AnyWindowHandle, view_id: usize, ref_counts: &Arc>) -> Self { Self { - any_handle: AnyViewHandle::new( - window_id, - view_id, - TypeId::of::(), - ref_counts.clone(), - ), + any_handle: AnyViewHandle::new(window, view_id, TypeId::of::(), ref_counts.clone()), view_type: PhantomData, } } pub fn downgrade(&self) -> WeakViewHandle { - WeakViewHandle::new(self.window_id, self.view_id) + WeakViewHandle::new(self.window, self.view_id) } pub fn into_any(self) -> AnyViewHandle { @@ -4150,13 +4139,11 @@ impl ViewHandle { } pub fn window_id(&self) -> usize { - self.window_id + self.window.id() } - pub fn window(&self, cx: &C) -> C::Result { - cx.read_window(self.window_id, |cx| { - AnyWindowHandle::new(self.window_id, cx.window.root_view.type_id()) - }) + pub fn window(&self) -> AnyWindowHandle { + self.window } pub fn id(&self) -> usize { @@ -4172,7 +4159,7 @@ impl ViewHandle { C: BorrowWindowContext, F: FnOnce(&T, &ViewContext) -> S, { - cx.read_window(self.window_id, |cx| { + cx.read_window(self.window, |cx| { let cx = ViewContext::immutable(cx, self.view_id); read(cx.read_view(self), &cx) }) @@ -4185,7 +4172,7 @@ impl ViewHandle { { let mut update = Some(update); - cx.update_window(self.window_id, |cx| { + cx.update_window(self.window, |cx| { cx.update_view(self, &mut |view, cx| { let update = update.take().unwrap(); update(view, cx) @@ -4200,31 +4187,31 @@ impl ViewHandle { impl Clone for ViewHandle { fn clone(&self) -> Self { - ViewHandle::new(self.window_id, self.view_id, &self.ref_counts) + ViewHandle::new(self.window, self.view_id, &self.ref_counts) } } impl PartialEq for ViewHandle { fn eq(&self, other: &Self) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } impl PartialEq for ViewHandle { fn eq(&self, other: &AnyViewHandle) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } impl PartialEq> for ViewHandle { fn eq(&self, other: &WeakViewHandle) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } impl PartialEq> for WeakViewHandle { fn eq(&self, other: &ViewHandle) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } @@ -4232,7 +4219,7 @@ impl Eq for ViewHandle {} impl Hash for ViewHandle { fn hash(&self, state: &mut H) { - self.window_id.hash(state); + self.window.hash(state); self.view_id.hash(state); } } @@ -4240,7 +4227,7 @@ impl Hash for ViewHandle { impl Debug for ViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct(&format!("ViewHandle<{}>", type_name::())) - .field("window_id", &self.window_id) + .field("window_id", &self.window) .field("view_id", &self.view_id) .finish() } @@ -4254,7 +4241,7 @@ impl Handle for ViewHandle { } fn location(&self) -> EntityLocation { - EntityLocation::View(self.window_id, self.view_id) + EntityLocation::View(self.window.id(), self.view_id) } fn downgrade(&self) -> Self::Weak { @@ -4270,7 +4257,7 @@ impl Handle for ViewHandle { } pub struct AnyViewHandle { - window_id: usize, + window: AnyWindowHandle, view_id: usize, view_type: TypeId, ref_counts: Arc>, @@ -4281,12 +4268,12 @@ pub struct AnyViewHandle { impl AnyViewHandle { fn new( - window_id: usize, + window: AnyWindowHandle, view_id: usize, view_type: TypeId, ref_counts: Arc>, ) -> Self { - ref_counts.lock().inc_view(window_id, view_id); + ref_counts.lock().inc_view(window.id(), view_id); #[cfg(any(test, feature = "test-support"))] let handle_id = ref_counts @@ -4296,7 +4283,7 @@ impl AnyViewHandle { .handle_created(None, view_id); Self { - window_id, + window, view_id, view_type, ref_counts, @@ -4305,8 +4292,12 @@ impl AnyViewHandle { } } + pub fn window(&self) -> AnyWindowHandle { + self.window + } + pub fn window_id(&self) -> usize { - self.window_id + self.window.id() } pub fn id(&self) -> usize { @@ -4338,7 +4329,7 @@ impl AnyViewHandle { pub fn downgrade(&self) -> AnyWeakViewHandle { AnyWeakViewHandle { - window_id: self.window_id, + window: self.window, view_id: self.view_id, view_type: self.view_type, } @@ -4350,7 +4341,7 @@ impl AnyViewHandle { pub fn debug_json<'a, 'b>(&self, cx: &'b WindowContext<'a>) -> serde_json::Value { cx.views - .get(&(self.window_id, self.view_id)) + .get(&(self.window.id(), self.view_id)) .map_or_else(|| serde_json::Value::Null, |view| view.debug_json(cx)) } } @@ -4358,7 +4349,7 @@ impl AnyViewHandle { impl Clone for AnyViewHandle { fn clone(&self) -> Self { Self::new( - self.window_id, + self.window, self.view_id, self.view_type, self.ref_counts.clone(), @@ -4368,13 +4359,13 @@ impl Clone for AnyViewHandle { impl PartialEq for AnyViewHandle { fn eq(&self, other: &Self) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } impl PartialEq> for AnyViewHandle { fn eq(&self, other: &ViewHandle) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } @@ -4382,7 +4373,7 @@ impl Drop for AnyViewHandle { fn drop(&mut self) { self.ref_counts .lock() - .dec_view(self.window_id, self.view_id); + .dec_view(self.window.id(), self.view_id); #[cfg(any(test, feature = "test-support"))] self.ref_counts .lock() @@ -4395,7 +4386,7 @@ impl Drop for AnyViewHandle { impl Debug for AnyViewHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("AnyViewHandle") - .field("window_id", &self.window_id) + .field("window_id", &self.window.id()) .field("view_id", &self.view_id) .finish() } @@ -4531,10 +4522,10 @@ impl WeakHandle for WeakViewHandle { } impl WeakViewHandle { - fn new(window_id: usize, view_id: usize) -> Self { + fn new(window: AnyWindowHandle, view_id: usize) -> Self { Self { any_handle: AnyWeakViewHandle { - window_id, + window, view_id, view_type: TypeId::of::(), }, @@ -4546,8 +4537,12 @@ impl WeakViewHandle { self.view_id } + pub fn window(&self) -> AnyWindowHandle { + self.window + } + pub fn window_id(&self) -> usize { - self.window_id + self.window.id() } pub fn into_any(self) -> AnyWeakViewHandle { @@ -4567,7 +4562,7 @@ impl WeakViewHandle { let handle = cx .upgrade_view_handle(self) .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?; - cx.read_window(self.window_id, |cx| handle.read_with(cx, read)) + cx.read_window(self.window, |cx| handle.read_with(cx, read)) .ok_or_else(|| anyhow!("window was removed")) }) } @@ -4581,7 +4576,7 @@ impl WeakViewHandle { let handle = cx .upgrade_view_handle(self) .ok_or_else(|| anyhow!("view {} was dropped", V::ui_name()))?; - cx.update_window(self.window_id, |cx| handle.update(cx, update)) + cx.update_window(self.window, |cx| handle.update(cx, update)) .ok_or_else(|| anyhow!("window was removed")) }) } @@ -4606,7 +4601,7 @@ impl Clone for WeakViewHandle { impl PartialEq for WeakViewHandle { fn eq(&self, other: &Self) -> bool { - self.window_id == other.window_id && self.view_id == other.view_id + self.window == other.window && self.view_id == other.view_id } } @@ -4620,7 +4615,7 @@ impl Hash for WeakViewHandle { #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct AnyWeakViewHandle { - window_id: usize, + window: AnyWindowHandle, view_id: usize, view_type: TypeId, } @@ -4652,7 +4647,7 @@ impl AnyWeakViewHandle { impl Hash for AnyWeakViewHandle { fn hash(&self, state: &mut H) { - self.window_id.hash(state); + self.window.hash(state); self.view_id.hash(state); self.view_type.hash(state); } @@ -6393,7 +6388,7 @@ mod tests { // Check that global actions do not have a binding, even if a binding does exist in another view assert_eq!( - &available_actions(window.id(), view_1.id(), cx), + &available_actions(window.into(), view_1.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::GlobalAction", vec![]) @@ -6402,7 +6397,7 @@ mod tests { // Check that view 1 actions and bindings are available even when called from view 2 assert_eq!( - &available_actions(window.id(), view_2.id(), cx), + &available_actions(window.into(), view_2.id(), cx), &[ ("test::Action1", vec![Keystroke::parse("a").unwrap()]), ("test::Action2", vec![Keystroke::parse("b").unwrap()]), @@ -6412,11 +6407,11 @@ mod tests { // Produces a list of actions and key bindings fn available_actions( - window_id: usize, + window: AnyWindowHandle, view_id: usize, cx: &TestAppContext, ) -> Vec<(&'static str, Vec)> { - cx.available_actions(window_id, view_id) + cx.available_actions(window.into(), view_id) .into_iter() .map(|(action_name, _, bindings)| { ( @@ -6465,7 +6460,7 @@ mod tests { ]); }); - let actions = cx.available_actions(window.id(), view.id()); + let actions = cx.available_actions(window.into(), view.id()); assert_eq!( actions[0].1.as_any().downcast_ref::(), Some(&ActionWithArg { arg: false }) diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index ff988607b09..b1729d89b85 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -72,8 +72,8 @@ impl TestAppContext { cx } - pub fn dispatch_action(&mut self, window_id: usize, action: A) { - self.update_window(window_id, |window| { + pub fn dispatch_action(&mut self, window: AnyWindowHandle, action: A) { + self.update_window(window, |window| { window.dispatch_action(window.focused_view_id(), &action); }) .expect("window not found"); @@ -81,10 +81,10 @@ impl TestAppContext { pub fn available_actions( &self, - window_id: usize, + window: AnyWindowHandle, view_id: usize, ) -> Vec<(&'static str, Box, SmallVec<[Binding; 1]>)> { - self.read_window(window_id, |cx| cx.available_actions(view_id)) + self.read_window(window, |cx| cx.available_actions(view_id)) .unwrap_or_default() } @@ -127,18 +127,18 @@ impl TestAppContext { pub fn read_window T>( &self, - window_id: usize, + window: AnyWindowHandle, callback: F, ) -> Option { - self.cx.borrow().read_window(window_id, callback) + self.cx.borrow().read_window(window, callback) } pub fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, callback: F, ) -> Option { - self.cx.borrow_mut().update_window(window_id, callback) + self.cx.borrow_mut().update_window(window, callback) } pub fn add_model(&mut self, build_model: F) -> ModelHandle @@ -317,36 +317,36 @@ impl BorrowAppContext for TestAppContext { impl BorrowWindowContext for TestAppContext { type Result = T; - fn read_window T>(&self, window_id: usize, f: F) -> T { + fn read_window T>(&self, window: AnyWindowHandle, f: F) -> T { self.cx .borrow() - .read_window(window_id, f) + .read_window(window, f) .expect("window was closed") } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - BorrowWindowContext::read_window(self, window_id, f) + BorrowWindowContext::read_window(self, window, f) } fn update_window T>( &mut self, - window_id: usize, + window: AnyWindowHandle, f: F, ) -> T { self.cx .borrow_mut() - .update_window(window_id, f) + .update_window(window, f) .expect("window was closed") } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window(self, window_id, f) + BorrowWindowContext::update_window(self, window, f) } } diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 70d02c1fa12..44a3c5cfdc8 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -13,9 +13,9 @@ use crate::{ }, text_layout::TextLayoutCache, util::post_inc, - Action, AnyView, AnyViewHandle, AppContext, BorrowAppContext, BorrowWindowContext, Effect, - Element, Entity, Handle, LayoutContext, MouseRegion, MouseRegionId, SceneBuilder, Subscription, - View, ViewContext, ViewHandle, WindowHandle, WindowInvalidation, + Action, AnyView, AnyViewHandle, AnyWindowHandle, AppContext, BorrowAppContext, + BorrowWindowContext, Effect, Element, Entity, Handle, LayoutContext, MouseRegion, + MouseRegionId, SceneBuilder, Subscription, View, ViewContext, ViewHandle, WindowInvalidation, }; use anyhow::{anyhow, bail, Result}; use collections::{HashMap, HashSet}; @@ -60,7 +60,7 @@ pub struct Window { impl Window { pub fn new( - window_id: usize, + handle: AnyWindowHandle, platform_window: Box, cx: &mut AppContext, build_view: F, @@ -92,7 +92,7 @@ impl Window { appearance, }; - let mut window_context = WindowContext::mutable(cx, &mut window, window_id); + let mut window_context = WindowContext::mutable(cx, &mut window, handle); let root_view = window_context.add_view(|cx| build_view(cx)); if let Some(invalidation) = window_context.window.invalidation.take() { window_context.invalidate(invalidation, appearance); @@ -113,7 +113,7 @@ impl Window { pub struct WindowContext<'a> { pub(crate) app_context: Reference<'a, AppContext>, pub(crate) window: Reference<'a, Window>, - pub(crate) window_id: usize, + pub(crate) window_handle: AnyWindowHandle, pub(crate) removed: bool, } @@ -144,15 +144,15 @@ impl BorrowAppContext for WindowContext<'_> { impl BorrowWindowContext for WindowContext<'_> { type Result = T; - fn read_window T>(&self, window_id: usize, f: F) -> T { - if self.window_id == window_id { + fn read_window T>(&self, handle: AnyWindowHandle, f: F) -> T { + if self.window_handle == handle { f(self) } else { panic!("read_with called with id of window that does not belong to this context") } } - fn read_window_optional(&self, window_id: usize, f: F) -> Option + fn read_window_optional(&self, window_id: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { @@ -161,21 +161,21 @@ impl BorrowWindowContext for WindowContext<'_> { fn update_window T>( &mut self, - window_id: usize, + handle: AnyWindowHandle, f: F, ) -> T { - if self.window_id == window_id { + if self.window_handle == handle { f(self) } else { panic!("update called with id of window that does not belong to this context") } } - fn update_window_optional(&mut self, window_id: usize, f: F) -> Option + fn update_window_optional(&mut self, handle: AnyWindowHandle, f: F) -> Option where F: FnOnce(&mut WindowContext) -> Option, { - BorrowWindowContext::update_window(self, window_id, f) + BorrowWindowContext::update_window(self, handle, f) } } @@ -183,21 +183,25 @@ impl<'a> WindowContext<'a> { pub fn mutable( app_context: &'a mut AppContext, window: &'a mut Window, - window_id: usize, + handle: AnyWindowHandle, ) -> Self { Self { app_context: Reference::Mutable(app_context), window: Reference::Mutable(window), - window_id, + window_handle: handle, removed: false, } } - pub fn immutable(app_context: &'a AppContext, window: &'a Window, window_id: usize) -> Self { + pub fn immutable( + app_context: &'a AppContext, + window: &'a Window, + handle: AnyWindowHandle, + ) -> Self { Self { app_context: Reference::Immutable(app_context), window: Reference::Immutable(window), - window_id, + window_handle: handle, removed: false, } } @@ -207,17 +211,11 @@ impl<'a> WindowContext<'a> { } pub fn window_id(&self) -> usize { - self.window_id + self.window_handle.id() } - pub fn window(&self) -> Option> { - self.window.root_view.as_ref().and_then(|root_view| { - if root_view.is::() { - Some(WindowHandle::new(self.window_id)) - } else { - None - } - }) + pub fn window(&self) -> AnyWindowHandle { + self.window_handle } pub fn app_context(&mut self) -> &mut AppContext { @@ -240,10 +238,10 @@ impl<'a> WindowContext<'a> { where F: FnOnce(&mut dyn AnyView, &mut Self) -> T, { - let window_id = self.window_id; - let mut view = self.views.remove(&(window_id, view_id))?; + let handle = self.window_handle; + let mut view = self.views.remove(&(handle.id(), view_id))?; let result = f(view.as_mut(), self); - self.views.insert((window_id, view_id), view); + self.views.insert((handle.id(), view_id), view); Some(result) } @@ -268,9 +266,9 @@ impl<'a> WindowContext<'a> { } pub fn defer(&mut self, callback: impl 'static + FnOnce(&mut WindowContext)) { - let window_id = self.window_id; + let handle = self.window_handle; self.app_context.defer(move |cx| { - cx.update_window(window_id, |cx| callback(cx)); + cx.update_window(handle, |cx| callback(cx)); }) } @@ -310,10 +308,10 @@ impl<'a> WindowContext<'a> { H: Handle, F: 'static + FnMut(H, &E::Event, &mut WindowContext) -> bool, { - let window_id = self.window_id; + let window_handle = self.window_handle; self.app_context .subscribe_internal(handle, move |emitter, event, cx| { - cx.update_window(window_id, |cx| callback(emitter, event, cx)) + cx.update_window(window_handle, |cx| callback(emitter, event, cx)) .unwrap_or(false) }) } @@ -322,17 +320,17 @@ impl<'a> WindowContext<'a> { where F: 'static + FnMut(bool, &mut WindowContext) -> bool, { - let window_id = self.window_id; + let handle = self.window_handle; let subscription_id = post_inc(&mut self.next_subscription_id); self.pending_effects .push_back(Effect::WindowActivationObservation { - window_id, + window: handle, subscription_id, callback: Box::new(callback), }); Subscription::WindowActivationObservation( self.window_activation_observations - .subscribe(window_id, subscription_id), + .subscribe(handle.id(), subscription_id), ) } @@ -340,17 +338,17 @@ impl<'a> WindowContext<'a> { where F: 'static + FnMut(bool, &mut WindowContext) -> bool, { - let window_id = self.window_id; + let window = self.window_handle; let subscription_id = post_inc(&mut self.next_subscription_id); self.pending_effects .push_back(Effect::WindowFullscreenObservation { - window_id, + window, subscription_id, callback: Box::new(callback), }); Subscription::WindowActivationObservation( self.window_activation_observations - .subscribe(window_id, subscription_id), + .subscribe(window.id(), subscription_id), ) } @@ -358,17 +356,17 @@ impl<'a> WindowContext<'a> { where F: 'static + FnMut(WindowBounds, Uuid, &mut WindowContext) -> bool, { - let window_id = self.window_id; + let window = self.window_handle; let subscription_id = post_inc(&mut self.next_subscription_id); self.pending_effects .push_back(Effect::WindowBoundsObservation { - window_id, + window, subscription_id, callback: Box::new(callback), }); Subscription::WindowBoundsObservation( self.window_bounds_observations - .subscribe(window_id, subscription_id), + .subscribe(window.id(), subscription_id), ) } @@ -377,13 +375,13 @@ impl<'a> WindowContext<'a> { F: 'static + FnMut(&Keystroke, &MatchResult, Option<&Box>, &mut WindowContext) -> bool, { - let window_id = self.window_id; + let window = self.window_handle; let subscription_id = post_inc(&mut self.next_subscription_id); self.keystroke_observations - .add_callback(window_id, subscription_id, Box::new(callback)); + .add_callback(window.id(), subscription_id, Box::new(callback)); Subscription::KeystrokeObservation( self.keystroke_observations - .subscribe(window_id, subscription_id), + .subscribe(window.id(), subscription_id), ) } @@ -391,11 +389,11 @@ impl<'a> WindowContext<'a> { &self, view_id: usize, ) -> Vec<(&'static str, Box, SmallVec<[Binding; 1]>)> { - let window_id = self.window_id; + let handle = self.window_handle; let mut contexts = Vec::new(); let mut handler_depths_by_action_id = HashMap::::default(); for (depth, view_id) in self.ancestors(view_id).enumerate() { - if let Some(view_metadata) = self.views_metadata.get(&(window_id, view_id)) { + if let Some(view_metadata) = self.views_metadata.get(&(handle.id(), view_id)) { contexts.push(view_metadata.keymap_context.clone()); if let Some(actions) = self.actions.get(&view_metadata.type_id) { handler_depths_by_action_id @@ -440,13 +438,13 @@ impl<'a> WindowContext<'a> { } pub(crate) fn dispatch_keystroke(&mut self, keystroke: &Keystroke) -> bool { - let window_id = self.window_id; + let handle = self.window_handle; if let Some(focused_view_id) = self.focused_view_id() { let dispatch_path = self .ancestors(focused_view_id) .filter_map(|view_id| { self.views_metadata - .get(&(window_id, view_id)) + .get(&(handle.id(), view_id)) .map(|view| (view_id, view.keymap_context.clone())) }) .collect(); @@ -471,15 +469,10 @@ impl<'a> WindowContext<'a> { } }; - self.keystroke( - window_id, - keystroke.clone(), - handled_by, - match_result.clone(), - ); + self.keystroke(handle, keystroke.clone(), handled_by, match_result.clone()); keystroke_handled } else { - self.keystroke(window_id, keystroke.clone(), None, MatchResult::None); + self.keystroke(handle, keystroke.clone(), None, MatchResult::None); false } } @@ -487,7 +480,7 @@ impl<'a> WindowContext<'a> { pub(crate) fn dispatch_event(&mut self, event: Event, event_reused: bool) -> bool { let mut mouse_events = SmallVec::<[_; 2]>::new(); let mut notified_views: HashSet = Default::default(); - let window_id = self.window_id; + let handle = self.window_handle; // 1. Handle platform event. Keyboard events get dispatched immediately, while mouse events // get mapped into the mouse-specific MouseEvent type. @@ -851,19 +844,19 @@ impl<'a> WindowContext<'a> { } for view_id in notified_views { - self.notify_view(window_id, view_id); + self.notify_view(handle, view_id); } any_event_handled } pub(crate) fn dispatch_key_down(&mut self, event: &KeyDownEvent) -> bool { - let window_id = self.window_id; + let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(window_id, view_id)) { + if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { let handled = view.key_down(event, self, view_id); - self.views.insert((window_id, view_id), view); + self.views.insert((handle.id(), view_id), view); if handled { return true; } @@ -877,12 +870,12 @@ impl<'a> WindowContext<'a> { } pub(crate) fn dispatch_key_up(&mut self, event: &KeyUpEvent) -> bool { - let window_id = self.window_id; + let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(window_id, view_id)) { + if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { let handled = view.key_up(event, self, view_id); - self.views.insert((window_id, view_id), view); + self.views.insert((handle.id(), view_id), view); if handled { return true; } @@ -896,12 +889,12 @@ impl<'a> WindowContext<'a> { } pub(crate) fn dispatch_modifiers_changed(&mut self, event: &ModifiersChangedEvent) -> bool { - let window_id = self.window_id; + let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(window_id, view_id)) { + if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { let handled = view.modifiers_changed(event, self, view_id); - self.views.insert((window_id, view_id), view); + self.views.insert((handle.id(), view_id), view); if handled { return true; } @@ -936,14 +929,14 @@ impl<'a> WindowContext<'a> { } pub fn render_view(&mut self, params: RenderParams) -> Result> { - let window_id = self.window_id; + let handle = self.window_handle; let view_id = params.view_id; let mut view = self .views - .remove(&(window_id, view_id)) + .remove(&(handle.id(), view_id)) .ok_or_else(|| anyhow!("view not found"))?; let element = view.render(self, view_id); - self.views.insert((window_id, view_id), view); + self.views.insert((handle.id(), view_id), view); Ok(element) } @@ -971,9 +964,9 @@ impl<'a> WindowContext<'a> { } else if old_parent_id == new_parent_id { current_view_id = *old_parent_id.unwrap(); } else { - let window_id = self.window_id; + let handle = self.window_handle; for view_id_to_notify in view_ids_to_notify { - self.notify_view(window_id, view_id_to_notify); + self.notify_view(handle, view_id_to_notify); } break; } @@ -1141,7 +1134,7 @@ impl<'a> WindowContext<'a> { } pub fn focus(&mut self, view_id: Option) { - self.app_context.focus(self.window_id, view_id); + self.app_context.focus(self.window_handle, view_id); } pub fn window_bounds(&self) -> WindowBounds { @@ -1194,26 +1187,26 @@ impl<'a> WindowContext<'a> { T: View, F: FnOnce(&mut ViewContext) -> Option, { - let window_id = self.window_id; + let handle = self.window_handle; let view_id = post_inc(&mut self.next_id); let mut cx = ViewContext::mutable(self, view_id); let handle = if let Some(view) = build_view(&mut cx) { let mut keymap_context = KeymapContext::default(); view.update_keymap_context(&mut keymap_context, cx.app_context()); self.views_metadata.insert( - (window_id, view_id), + (handle.id(), view_id), ViewMetadata { type_id: TypeId::of::(), keymap_context, }, ); - self.views.insert((window_id, view_id), Box::new(view)); + self.views.insert((handle.id(), view_id), Box::new(view)); self.window .invalidation .get_or_insert_with(Default::default) .updated .insert(view_id); - Some(ViewHandle::new(window_id, view_id, &self.ref_counts)) + Some(ViewHandle::new(handle, view_id, &self.ref_counts)) } else { None }; @@ -1390,7 +1383,7 @@ pub struct ChildView { impl ChildView { pub fn new(view: &AnyViewHandle, cx: &AppContext) -> Self { - let view_name = cx.view_ui_name(view.window_id(), view.id()).unwrap(); + let view_name = cx.view_ui_name(view.window, view.id()).unwrap(); Self { view_id: view.id(), view_name, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 07aaea812a9..df4334a19fc 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1726,7 +1726,7 @@ impl ClipboardEntry { #[cfg(test)] mod tests { use super::*; - use gpui::{AnyWindowHandle, TestAppContext, ViewHandle}; + use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle}; use pretty_assertions::assert_eq; use project::FakeFs; use serde_json::json; @@ -1872,7 +1872,6 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -1894,7 +1893,7 @@ mod tests { // Add a file with the root folder selected. The filename editor is placed // before the first file in the root folder. panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let panel = panel.read(cx); assert!(panel.filename_editor.is_focused(cx)); }); @@ -2225,7 +2224,6 @@ mod tests { let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "root1", cx); @@ -2247,7 +2245,7 @@ mod tests { // Add a file with the root folder selected. The filename editor is placed // before the first file in the root folder. panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let panel = panel.read(cx); assert!(panel.filename_editor.is_focused(cx)); }); @@ -2402,7 +2400,6 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); toggle_expand_dir(&panel, "src/test", cx); @@ -2419,7 +2416,7 @@ mod tests { " third.rs" ] ); - ensure_single_file_is_opened(window_id, &workspace, "test/first.rs", cx); + ensure_single_file_is_opened(window, "test/first.rs", cx); submit_deletion(window.into(), &panel, cx); assert_eq!( @@ -2446,9 +2443,9 @@ mod tests { " third.rs" ] ); - ensure_single_file_is_opened(window_id, &workspace, "test/second.rs", cx); + ensure_single_file_is_opened(window, "test/second.rs", cx); - cx.update_window(window_id, |cx| { + window.update(cx, |cx| { let active_items = workspace .read(cx) .panes() @@ -2493,7 +2490,6 @@ mod tests { let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); let workspace = window.root(cx); - let window_id = window.id(); let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); select_path(&panel, "src/", cx); @@ -2504,7 +2500,7 @@ mod tests { &["v src <== selected", " > test"] ); panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let panel = panel.read(cx); assert!(panel.filename_editor.is_focused(cx)); }); @@ -2535,7 +2531,7 @@ mod tests { &["v src", " > test <== selected"] ); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let panel = panel.read(cx); assert!(panel.filename_editor.is_focused(cx)); }); @@ -2585,7 +2581,7 @@ mod tests { ], ); panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { let panel = panel.read(cx); assert!(panel.filename_editor.is_focused(cx)); }); @@ -2882,13 +2878,11 @@ mod tests { } fn ensure_single_file_is_opened( - window_id: usize, - workspace: &ViewHandle, + window: WindowHandle, expected_path: &str, cx: &mut TestAppContext, ) { - cx.read_window(window_id, |cx| { - let workspace = workspace.read(cx); + window.update_root(cx, |workspace, cx| { let worktrees = workspace.worktrees(cx).collect::>(); assert_eq!(worktrees.len(), 1); let worktree_id = WorktreeId::from_usize(worktrees[0].id()); diff --git a/crates/search/src/buffer_search.rs b/crates/search/src/buffer_search.rs index b20ffed6d7e..c3b4f5caa66 100644 --- a/crates/search/src/buffer_search.rs +++ b/crates/search/src/buffer_search.rs @@ -1229,8 +1229,6 @@ mod tests { ); let buffer = cx.add_model(|cx| Buffer::new(0, buffer_text, cx)); let window = cx.add_window(|_| EmptyView); - let window_id = window.id(); - let editor = window.add_view(cx, |cx| Editor::for_buffer(buffer.clone(), None, cx)); let search_bar = window.add_view(cx, |cx| { @@ -1249,12 +1247,13 @@ mod tests { search_bar.activate_current_match(cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( !editor.is_focused(cx), "Initially, the editor should not be focused" ); }); + let initial_selections = editor.update(cx, |editor, cx| { let initial_selections = editor.selections.display_ranges(cx); assert_eq!( @@ -1271,7 +1270,7 @@ mod tests { cx.focus(search_bar.query_editor.as_any()); search_bar.select_all_matches(&SelectAllMatches, cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( editor.is_focused(cx), "Should focus editor after successful SelectAllMatches" @@ -1295,7 +1294,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.select_next_match(&SelectNextMatch, cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( editor.is_focused(cx), "Should still have editor focused after SelectNextMatch" @@ -1324,7 +1323,7 @@ mod tests { cx.focus(search_bar.query_editor.as_any()); search_bar.select_all_matches(&SelectAllMatches, cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( editor.is_focused(cx), "Should focus editor after successful SelectAllMatches" @@ -1348,7 +1347,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.select_prev_match(&SelectPrevMatch, cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( editor.is_focused(cx), "Should still have editor focused after SelectPrevMatch" @@ -1384,7 +1383,7 @@ mod tests { search_bar.update(cx, |search_bar, cx| { search_bar.select_all_matches(&SelectAllMatches, cx); }); - cx.read_window(window_id, |cx| { + window.read_with(cx, |cx| { assert!( !editor.is_focused(cx), "Should not switch focus to editor if SelectAllMatches does not find any matches" diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index dffd88db142..202d494560e 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1568,7 +1568,6 @@ pub mod tests { let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let active_item = cx.read(|cx| { workspace @@ -1599,9 +1598,9 @@ pub mod tests { }; let search_view_id = search_view.id(); - cx.spawn( - |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, - ) + cx.spawn(|mut cx| async move { + cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + }) .detach(); deterministic.run_until_parked(); search_view.update(cx, |search_view, cx| { @@ -1651,9 +1650,9 @@ pub mod tests { "Search view should be focused after mismatching query had been used in search", ); }); - cx.spawn( - |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, - ) + cx.spawn(|mut cx| async move { + cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + }) .detach(); deterministic.run_until_parked(); search_view.update(cx, |search_view, cx| { @@ -1683,9 +1682,9 @@ pub mod tests { "Search view with mismatching query should be focused after search results are available", ); }); - cx.spawn( - |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, - ) + cx.spawn(|mut cx| async move { + cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + }) .detach(); deterministic.run_until_parked(); search_view.update(cx, |search_view, cx| { @@ -1713,9 +1712,9 @@ pub mod tests { ); }); - cx.spawn( - |mut cx| async move { cx.dispatch_action(window_id, search_view_id, &ToggleFocus) }, - ) + cx.spawn(|mut cx| async move { + cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + }) .detach(); deterministic.run_until_parked(); search_view.update(cx, |search_view, cx| { diff --git a/crates/vim/src/editor_events.rs b/crates/vim/src/editor_events.rs index 4fef6bd715e..893f5e8a854 100644 --- a/crates/vim/src/editor_events.rs +++ b/crates/vim/src/editor_events.rs @@ -1,6 +1,6 @@ use crate::Vim; use editor::{EditorBlurred, EditorFocused, EditorReleased}; -use gpui::{AppContext, BorrowWindowContext}; +use gpui::AppContext; pub fn init(cx: &mut AppContext) { cx.subscribe_global(focused).detach(); @@ -10,7 +10,7 @@ pub fn init(cx: &mut AppContext) { fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { if let Some(previously_active_editor) = Vim::read(cx).active_editor.clone() { - cx.update_window(previously_active_editor.window_id(), |cx| { + previously_active_editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { vim.update_active_editor(cx, |previously_active_editor, cx| { vim.unhook_vim_settings(previously_active_editor, cx) @@ -19,7 +19,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { }); } - cx.update_window(editor.window_id(), |cx| { + editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { vim.set_active_editor(editor.clone(), cx); }); @@ -27,7 +27,7 @@ fn focused(EditorFocused(editor): &EditorFocused, cx: &mut AppContext) { } fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) { - cx.update_window(editor.window_id(), |cx| { + editor.window().update(cx, |cx| { Vim::update(cx, |vim, cx| { if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { @@ -41,7 +41,7 @@ fn blurred(EditorBlurred(editor): &EditorBlurred, cx: &mut AppContext) { } fn released(EditorReleased(editor): &EditorReleased, cx: &mut AppContext) { - cx.update_window(editor.window_id(), |cx| { + editor.window().update(cx, |cx| { cx.update_default_global(|vim: &mut Vim, _| { if let Some(previous_editor) = vim.active_editor.clone() { if previous_editor == editor.clone() { diff --git a/crates/vim/src/mode_indicator.rs b/crates/vim/src/mode_indicator.rs index afd60af848b..5d54a66723d 100644 --- a/crates/vim/src/mode_indicator.rs +++ b/crates/vim/src/mode_indicator.rs @@ -1,6 +1,6 @@ use gpui::{ elements::{Empty, Label}, - AnyElement, Element, Entity, Subscription, View, ViewContext, BorrowWindowContext + AnyElement, Element, Entity, Subscription, View, ViewContext, }; use settings::SettingsStore; use workspace::{item::ItemHandle, StatusItemView}; @@ -20,7 +20,7 @@ impl ModeIndicator { if let Some(mode_indicator) = handle.upgrade(cx) { match event { VimEvent::ModeChanged { mode } => { - cx.update_window(mode_indicator.window_id(), |cx| { + mode_indicator.window().update(cx, |cx| { mode_indicator.update(cx, move |mode_indicator, cx| { mode_indicator.set_mode(mode, cx); }) diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 839ab3aafc8..5eaaef900e4 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -85,8 +85,8 @@ impl<'a> VimTestContext<'a> { } pub fn set_state(&mut self, text: &str, mode: Mode) -> ContextHandle { - let window_id = self.window.id(); - self.update_window(window_id, |cx| { + let window = self.window; + window.update(self.cx.cx.cx, |cx| { Vim::update(cx, |vim, cx| { vim.switch_mode(mode, false, cx); }) diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index ebaf399e22b..5bb0fd93f8c 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -203,7 +203,7 @@ impl Dock { pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option { self.panel_entries.iter().position(|entry| { let panel = entry.panel.as_any(); - cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name) + cx.view_ui_name(panel.window(), panel.id()) == Some(ui_name) }) } @@ -530,16 +530,12 @@ impl View for PanelButtons { tooltip_action.as_ref().map(|action| action.boxed_clone()); move |_, this, cx| { if let Some(tooltip_action) = &tooltip_action { - let window_id = cx.window_id(); + let window = cx.window(); let view_id = this.workspace.id(); let tooltip_action = tooltip_action.boxed_clone(); cx.spawn(|_, mut cx| async move { - cx.dispatch_action( - window_id, - view_id, - &*tooltip_action, - ) - .ok(); + cx.dispatch_action(window, view_id, &*tooltip_action) + .ok(); }) .detach(); } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 98883fac337..ecbe25accad 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1917,8 +1917,8 @@ impl Element for PaneBackdrop { MouseRegion::new::(child_view_id, 0, visible_bounds).on_down( gpui::platform::MouseButton::Left, move |_, _: &mut V, cx| { - let window_id = cx.window_id(); - cx.app_context().focus(window_id, Some(child_view_id)) + let window = cx.window(); + cx.app_context().focus(window, Some(child_view_id)) }, ), ); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index da708c6ea72..f0990322653 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1250,11 +1250,11 @@ impl Workspace { _: &CloseWindow, cx: &mut ViewContext, ) -> Option>> { - let window_id = cx.window_id(); + let window = cx.window(); let prepare = self.prepare_to_close(false, cx); Some(cx.spawn(|_, mut cx| async move { if prepare.await? { - cx.remove_window(window_id); + cx.remove_window(window); } Ok(()) })) @@ -1266,7 +1266,7 @@ impl Workspace { cx: &mut ViewContext, ) -> Task> { let active_call = self.active_call().cloned(); - let window_id = cx.window_id(); + let window = cx.window(); cx.spawn(|this, mut cx| async move { let workspace_count = cx @@ -1281,7 +1281,7 @@ impl Workspace { && active_call.read_with(&cx, |call, _| call.room().is_some()) { let answer = cx.prompt( - window_id, + window, PromptLevel::Warning, "Do you want to leave the current call?", &["Close window and hang up", "Cancel"], @@ -1390,7 +1390,7 @@ impl Workspace { paths: Vec, cx: &mut ViewContext, ) -> Task> { - let window = cx.window::(); + let window = cx.window().downcast::(); let is_remote = self.project.read(cx).is_remote(); let has_worktree = self.project.read(cx).worktrees(cx).next().is_some(); let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx)); @@ -3181,7 +3181,7 @@ impl Workspace { let left_visible = left_dock.is_open(); let left_active_panel = left_dock.visible_panel().and_then(|panel| { Some( - cx.view_ui_name(panel.as_any().window_id(), panel.id())? + cx.view_ui_name(panel.as_any().window(), panel.id())? .to_string(), ) }); @@ -3194,7 +3194,7 @@ impl Workspace { let right_visible = right_dock.is_open(); let right_active_panel = right_dock.visible_panel().and_then(|panel| { Some( - cx.view_ui_name(panel.as_any().window_id(), panel.id())? + cx.view_ui_name(panel.as_any().window(), panel.id())? .to_string(), ) }); @@ -3207,7 +3207,7 @@ impl Workspace { let bottom_visible = bottom_dock.is_open(); let bottom_active_panel = bottom_dock.visible_panel().and_then(|panel| { Some( - cx.view_ui_name(panel.as_any().window_id(), panel.id())? + cx.view_ui_name(panel.as_any().window(), panel.id())? .to_string(), ) }); @@ -4000,7 +4000,7 @@ pub fn join_remote_project( workspace.downgrade() }; - cx.activate_window(workspace.window_id()); + cx.activate_window(workspace.window()); cx.platform().activate(true); workspace.update(&mut cx, |workspace, cx| { @@ -4049,9 +4049,9 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { // prompt in the active window before switching to a different window. workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { + if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { let answer = cx.prompt( - window.id(), + window.into(), PromptLevel::Info, "Are you sure you want to restart?", &["Restart", "Cancel"], diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3435727b1e0..6397a1e6b2a 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -179,13 +179,12 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { let app_state = workspace.app_state().clone(); let markdown = app_state.languages.language_for_name("JSON"); - let window_id = cx.window_id(); + let window = cx.window(); cx.spawn(|workspace, mut cx| async move { let markdown = markdown.await.log_err(); - let content = to_string_pretty( - &cx.debug_elements(window_id) - .ok_or_else(|| anyhow!("could not debug elements for {window_id}"))?, - ) + let content = to_string_pretty(&cx.debug_elements(window).ok_or_else(|| { + anyhow!("could not debug elements for window {}", window.id()) + })?) .unwrap(); workspace .update(&mut cx, |workspace, cx| { @@ -416,9 +415,9 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) { // prompt in the active window before switching to a different window. workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { + if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { let answer = cx.prompt( - window.id(), + window.into(), PromptLevel::Info, "Are you sure you want to quit?", &["Quit", "Cancel"], @@ -716,8 +715,8 @@ mod tests { use editor::{scroll::autoscroll::Autoscroll, DisplayPoint, Editor}; use fs::{FakeFs, Fs}; use gpui::{ - actions, elements::Empty, executor::Deterministic, Action, AnyElement, AppContext, - AssetSource, Element, Entity, TestAppContext, View, ViewHandle, + actions, elements::Empty, executor::Deterministic, Action, AnyElement, AnyWindowHandle, + AppContext, AssetSource, Element, Entity, TestAppContext, View, ViewHandle, }; use language::LanguageRegistry; use node_runtime::NodeRuntime; @@ -1317,11 +1316,10 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap()); // Create a new untitled buffer - cx.dispatch_action(window_id, NewFile); + cx.dispatch_action(window.into(), NewFile); let editor = workspace.read_with(cx, |workspace, cx| { workspace .active_item(cx) @@ -1376,7 +1374,7 @@ mod tests { // Open the same newly-created file in another pane item. The new editor should reuse // the same buffer. - cx.dispatch_action(window_id, NewFile); + cx.dispatch_action(window.into(), NewFile); workspace .update(cx, |workspace, cx| { workspace.split_and_clone( @@ -1412,10 +1410,9 @@ mod tests { project.update(cx, |project, _| project.languages().add(rust_lang())); let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); // Create a new untitled buffer - cx.dispatch_action(window_id, NewFile); + cx.dispatch_action(window.into(), NewFile); let editor = workspace.read_with(cx, |workspace, cx| { workspace .active_item(cx) @@ -1465,7 +1462,6 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let window = cx.add_window(|cx| Workspace::test_new(project, cx)); let workspace = window.root(cx); - let window_id = window.id(); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1487,7 +1483,7 @@ mod tests { (editor.downgrade(), buffer) }); - cx.dispatch_action(window_id, pane::SplitRight); + cx.dispatch_action(window.into(), pane::SplitRight); let editor_2 = cx.update(|cx| { let pane_2 = workspace.read(cx).active_pane().clone(); assert_ne!(pane_1, pane_2); @@ -1497,7 +1493,7 @@ mod tests { pane2_item.downcast::().unwrap().downgrade() }); - cx.dispatch_action(window_id, workspace::CloseActiveItem); + cx.dispatch_action(window.into(), workspace::CloseActiveItem); cx.foreground().run_until_parked(); workspace.read_with(cx, |workspace, _| { @@ -1505,7 +1501,7 @@ mod tests { assert_eq!(workspace.active_pane(), &pane_1); }); - cx.dispatch_action(window_id, workspace::CloseActiveItem); + cx.dispatch_action(window.into(), workspace::CloseActiveItem); cx.foreground().run_until_parked(); window.simulate_prompt_answer(1, cx); cx.foreground().run_until_parked(); @@ -2063,11 +2059,10 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( - window_id, + window.into(), cx, vec![("backspace", &A), ("k", &ActivatePreviousPane)], line!(), @@ -2094,7 +2089,7 @@ mod tests { cx.foreground().run_until_parked(); assert_key_bindings_for( - window_id, + window.into(), cx, vec![("backspace", &B), ("k", &ActivatePreviousPane)], line!(), @@ -2117,7 +2112,7 @@ mod tests { cx.foreground().run_until_parked(); assert_key_bindings_for( - window_id, + window.into(), cx, vec![("backspace", &B), ("[", &ActivatePrevItem)], line!(), @@ -2125,7 +2120,7 @@ mod tests { #[track_caller] fn assert_key_bindings_for<'a>( - window_id: usize, + window: AnyWindowHandle, cx: &TestAppContext, actions: Vec<(&'static str, &'a dyn Action)>, line: u32, @@ -2133,7 +2128,7 @@ mod tests { for (key, action) in actions { // assert that... assert!( - cx.available_actions(window_id, 0) + cx.available_actions(window, 0) .into_iter() .any(|(_, bound_action, b)| { // action names match... @@ -2234,11 +2229,10 @@ mod tests { cx.foreground().run_until_parked(); let window = cx.add_window(|_| TestView); - let window_id = window.id(); // Test loading the keymap base at all assert_key_bindings_for( - window_id, + window.into(), cx, vec![("backspace", &A), ("k", &ActivatePreviousPane)], line!(), @@ -2264,7 +2258,12 @@ mod tests { cx.foreground().run_until_parked(); - assert_key_bindings_for(window_id, cx, vec![("k", &ActivatePreviousPane)], line!()); + assert_key_bindings_for( + window.into(), + cx, + vec![("k", &ActivatePreviousPane)], + line!(), + ); // Test modifying the base, while retaining the users keymap fs.save( @@ -2282,11 +2281,11 @@ mod tests { cx.foreground().run_until_parked(); - assert_key_bindings_for(window_id, cx, vec![("[", &ActivatePrevItem)], line!()); + assert_key_bindings_for(window.into(), cx, vec![("[", &ActivatePrevItem)], line!()); #[track_caller] fn assert_key_bindings_for<'a>( - window_id: usize, + window: AnyWindowHandle, cx: &TestAppContext, actions: Vec<(&'static str, &'a dyn Action)>, line: u32, @@ -2294,7 +2293,7 @@ mod tests { for (key, action) in actions { // assert that... assert!( - cx.available_actions(window_id, 0) + cx.available_actions(window, 0) .into_iter() .any(|(_, bound_action, b)| { // action names match... From da7dc9c880b06cf465aafacd8756abd9a6f60f86 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:12:32 -0600 Subject: [PATCH 066/105] Work with window handles instead of ids in drag code --- crates/command_palette/src/command_palette.rs | 6 +-- crates/drag_and_drop/src/drag_and_drop.rs | 40 +++++++++---------- crates/project_panel/src/project_panel.rs | 6 +-- crates/terminal_view/src/terminal_panel.rs | 4 +- .../src/pane/dragged_item_receiver.rs | 12 +++--- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index b3703aa64a4..0d398fcce90 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,8 +1,8 @@ use collections::CommandPaletteFilter; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, keymap_matcher::Keystroke, Action, AppContext, Element, MouseState, - ViewContext, + actions, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle, AppContext, Element, + MouseState, ViewContext, }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::cmp; @@ -28,7 +28,7 @@ pub struct CommandPaletteDelegate { pub enum Event { Dismissed, Confirmed { - window_id: usize, + window: AnyWindowHandle, focused_view_id: usize, action: Box, }, diff --git a/crates/drag_and_drop/src/drag_and_drop.rs b/crates/drag_and_drop/src/drag_and_drop.rs index 828e7304038..ddfed0c8582 100644 --- a/crates/drag_and_drop/src/drag_and_drop.rs +++ b/crates/drag_and_drop/src/drag_and_drop.rs @@ -6,7 +6,7 @@ use gpui::{ geometry::{rect::RectF, vector::Vector2F}, platform::{CursorStyle, MouseButton}, scene::{MouseDown, MouseDrag}, - AnyElement, Element, View, ViewContext, WeakViewHandle, WindowContext, + AnyElement, AnyWindowHandle, Element, View, ViewContext, WeakViewHandle, WindowContext, }; const DEAD_ZONE: f32 = 4.; @@ -21,7 +21,7 @@ enum State { region: RectF, }, Dragging { - window_id: usize, + window: AnyWindowHandle, position: Vector2F, region_offset: Vector2F, region: RectF, @@ -49,14 +49,14 @@ impl Clone for State { region, }, State::Dragging { - window_id, + window, position, region_offset, region, payload, render, } => Self::Dragging { - window_id: window_id.clone(), + window: window.clone(), position: position.clone(), region_offset: region_offset.clone(), region: region.clone(), @@ -87,16 +87,16 @@ impl DragAndDrop { self.containers.insert(handle); } - pub fn currently_dragged(&self, window_id: usize) -> Option<(Vector2F, Rc)> { + pub fn currently_dragged(&self, window: AnyWindowHandle) -> Option<(Vector2F, Rc)> { self.currently_dragged.as_ref().and_then(|state| { if let State::Dragging { position, payload, - window_id: window_dragged_from, + window: window_dragged_from, .. } = state { - if &window_id != window_dragged_from { + if &window != window_dragged_from { return None; } @@ -126,9 +126,9 @@ impl DragAndDrop { cx: &mut WindowContext, render: Rc) -> AnyElement>, ) { - let window_id = cx.window_id(); + let window = cx.window(); cx.update_global(|this: &mut Self, cx| { - this.notify_containers_for_window(window_id, cx); + this.notify_containers_for_window(window, cx); match this.currently_dragged.as_ref() { Some(&State::Down { @@ -141,7 +141,7 @@ impl DragAndDrop { }) => { if (event.position - (region.origin() + region_offset)).length() > DEAD_ZONE { this.currently_dragged = Some(State::Dragging { - window_id, + window, region_offset, region, position: event.position, @@ -163,7 +163,7 @@ impl DragAndDrop { .. }) => { this.currently_dragged = Some(State::Dragging { - window_id, + window, region_offset, region, position: event.position, @@ -188,14 +188,14 @@ impl DragAndDrop { State::Down { .. } => None, State::DeadZone { .. } => None, State::Dragging { - window_id, + window, region_offset, position, region, payload, render, } => { - if cx.window_id() != window_id { + if cx.window() != window { return None; } @@ -260,27 +260,27 @@ impl DragAndDrop { pub fn cancel_dragging(&mut self, cx: &mut WindowContext) { if let Some(State::Dragging { - payload, window_id, .. + payload, window, .. }) = &self.currently_dragged { if payload.is::

() { - let window_id = *window_id; + let window = *window; self.currently_dragged = Some(State::Canceled); - self.notify_containers_for_window(window_id, cx); + self.notify_containers_for_window(window, cx); } } } fn finish_dragging(&mut self, cx: &mut WindowContext) { - if let Some(State::Dragging { window_id, .. }) = self.currently_dragged.take() { - self.notify_containers_for_window(window_id, cx); + if let Some(State::Dragging { window, .. }) = self.currently_dragged.take() { + self.notify_containers_for_window(window, cx); } } - fn notify_containers_for_window(&mut self, window_id: usize, cx: &mut WindowContext) { + fn notify_containers_for_window(&mut self, window: AnyWindowHandle, cx: &mut WindowContext) { self.containers.retain(|container| { if let Some(container) = container.upgrade(cx) { - if container.window_id() == window_id { + if container.window() == window { container.update(cx, |_, cx| cx.notify()); } true diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index df4334a19fc..f7582f1764c 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1415,7 +1415,7 @@ impl ProjectPanel { if cx .global::>() - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .is_some() && dragged_entry_destination .as_ref() @@ -1459,7 +1459,7 @@ impl ProjectPanel { .on_up(MouseButton::Left, move |_, this, cx| { if let Some((_, dragged_entry)) = cx .global::>() - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) { this.move_entry( *dragged_entry, @@ -1472,7 +1472,7 @@ impl ProjectPanel { .on_move(move |_, this, cx| { if cx .global::>() - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .is_some() { this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6ad321c735d..d00324cf166 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -48,7 +48,7 @@ impl TerminalPanel { fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { let weak_self = cx.weak_handle(); let pane = cx.add_view(|cx| { - let window_id = cx.window_id(); + let window = cx.window(); let mut pane = Pane::new( workspace.weak_handle(), workspace.project().clone(), @@ -60,7 +60,7 @@ impl TerminalPanel { pane.set_can_navigate(false, cx); pane.on_can_drop(move |drag_and_drop, cx| { drag_and_drop - .currently_dragged::(window_id) + .currently_dragged::(window) .map_or(false, |(_, item)| { item.handle.act_as::(cx).is_some() }) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 7146ab7b85b..2d3fe8ea832 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -28,11 +28,11 @@ where let drag_and_drop = cx.global::>(); let drag_position = if (pane.can_drop)(drag_and_drop, cx) { drag_and_drop - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .map(|(drag_position, _)| drag_position) .or_else(|| { drag_and_drop - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .map(|(drag_position, _)| drag_position) }) } else { @@ -91,10 +91,10 @@ where let drag_and_drop = cx.global::>(); if drag_and_drop - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .is_some() || drag_and_drop - .currently_dragged::(cx.window_id()) + .currently_dragged::(cx.window()) .is_some() { cx.notify(); @@ -122,11 +122,11 @@ pub fn handle_dropped_item( } let drag_and_drop = cx.global::>(); let action = if let Some((_, dragged_item)) = - drag_and_drop.currently_dragged::(cx.window_id()) + drag_and_drop.currently_dragged::(cx.window()) { Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) } else if let Some((_, project_entry)) = - drag_and_drop.currently_dragged::(cx.window_id()) + drag_and_drop.currently_dragged::(cx.window()) { Action::Open(*project_entry) } else { From 0a4633f88f2166818ab57859783181a0972c0e77 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:20:09 -0600 Subject: [PATCH 067/105] Remove more window id usage --- crates/gpui/src/app.rs | 14 +++----------- crates/gpui/src/app/menu.rs | 2 +- crates/gpui/src/app/window.rs | 8 ++------ crates/project/src/terminals.rs | 6 +++--- crates/terminal/src/terminal.rs | 6 +++--- crates/terminal_view/src/terminal_panel.rs | 4 ++-- crates/terminal_view/src/terminal_view.rs | 8 ++++---- crates/workspace/src/item.rs | 7 ++++--- crates/workspace/src/searchable.rs | 2 +- crates/zed/src/main.rs | 4 ++-- 10 files changed, 25 insertions(+), 36 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e0f46b036a0..e44ac93818f 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -833,7 +833,7 @@ impl AppContext { &mut self, callback: F, ) -> Option { - self.main_window() + self.active_window() .and_then(|window| window.update(self, callback)) } @@ -1093,7 +1093,7 @@ impl AppContext { pub fn is_action_available(&self, action: &dyn Action) -> bool { let mut available_in_window = false; let action_id = action.id(); - if let Some(window) = self.main_window() { + if let Some(window) = self.active_window() { available_in_window = self .read_window(window, |cx| { if let Some(focused_view_id) = cx.focused_view_id() { @@ -1436,7 +1436,7 @@ impl AppContext { window } - pub fn main_window(&self) -> Option { + pub fn active_window(&self) -> Option { self.platform.main_window_id().and_then(|main_window_id| { self.windows .get(&main_window_id) @@ -2997,10 +2997,6 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> { WeakViewHandle::new(self.window_handle, self.view_id) } - pub fn window_id(&self) -> usize { - self.window_handle.id() - } - pub fn window(&self) -> AnyWindowHandle { self.window_handle } @@ -4138,10 +4134,6 @@ impl ViewHandle { self.any_handle } - pub fn window_id(&self) -> usize { - self.window.id() - } - pub fn window(&self) -> AnyWindowHandle { self.window } diff --git a/crates/gpui/src/app/menu.rs b/crates/gpui/src/app/menu.rs index 1d8908b8fdd..67531a82975 100644 --- a/crates/gpui/src/app/menu.rs +++ b/crates/gpui/src/app/menu.rs @@ -77,7 +77,7 @@ pub(crate) fn setup_menu_handlers(foreground_platform: &dyn ForegroundPlatform, let cx = app.0.clone(); move |action| { let mut cx = cx.borrow_mut(); - if let Some(main_window) = cx.main_window() { + if let Some(main_window) = cx.active_window() { let dispatched = main_window .update(&mut *cx, |cx| { if let Some(view_id) = cx.focused_view_id() { diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 44a3c5cfdc8..df3f8207556 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -152,11 +152,11 @@ impl BorrowWindowContext for WindowContext<'_> { } } - fn read_window_optional(&self, window_id: AnyWindowHandle, f: F) -> Option + fn read_window_optional(&self, window: AnyWindowHandle, f: F) -> Option where F: FnOnce(&WindowContext) -> Option, { - BorrowWindowContext::read_window(self, window_id, f) + BorrowWindowContext::read_window(self, window, f) } fn update_window T>( @@ -210,10 +210,6 @@ impl<'a> WindowContext<'a> { self.removed = true; } - pub fn window_id(&self) -> usize { - self.window_handle.id() - } - pub fn window(&self) -> AnyWindowHandle { self.window_handle } diff --git a/crates/project/src/terminals.rs b/crates/project/src/terminals.rs index 7bd9ce8aecb..db5996829fa 100644 --- a/crates/project/src/terminals.rs +++ b/crates/project/src/terminals.rs @@ -1,5 +1,5 @@ use crate::Project; -use gpui::{ModelContext, ModelHandle, WeakModelHandle}; +use gpui::{AnyWindowHandle, ModelContext, ModelHandle, WeakModelHandle}; use std::path::PathBuf; use terminal::{Terminal, TerminalBuilder, TerminalSettings}; @@ -11,7 +11,7 @@ impl Project { pub fn create_terminal( &mut self, working_directory: Option, - window_id: usize, + window: AnyWindowHandle, cx: &mut ModelContext, ) -> anyhow::Result> { if self.is_remote() { @@ -27,7 +27,7 @@ impl Project { settings.env.clone(), Some(settings.blinking.clone()), settings.alternate_scroll, - window_id, + window, ) .map(|builder| { let terminal_handle = cx.add_model(|cx| builder.subscribe(cx)); diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 06befd5f4ef..f6cfe5ae309 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -53,7 +53,7 @@ use gpui::{ keymap_matcher::Keystroke, platform::{Modifiers, MouseButton, MouseMovedEvent, TouchPhase}, scene::{MouseDown, MouseDrag, MouseScrollWheel, MouseUp}, - AppContext, ClipboardItem, Entity, ModelContext, Task, + AnyWindowHandle, AppContext, ClipboardItem, Entity, ModelContext, Task, }; use crate::mappings::{ @@ -404,7 +404,7 @@ impl TerminalBuilder { mut env: HashMap, blink_settings: Option, alternate_scroll: AlternateScroll, - window_id: usize, + window: AnyWindowHandle, ) -> Result { let pty_config = { let alac_shell = match shell.clone() { @@ -462,7 +462,7 @@ impl TerminalBuilder { let pty = match tty::new( &pty_config, TerminalSize::default().into(), - window_id as u64, + window.id() as u64, ) { Ok(pty) => pty, Err(error) => { diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index d00324cf166..6be8a547cd3 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -255,10 +255,10 @@ impl TerminalPanel { .clone(); let working_directory = crate::get_working_directory(workspace, cx, working_directory_strategy); - let window_id = cx.window_id(); + let window = cx.window(); if let Some(terminal) = workspace.project().update(cx, |project, cx| { project - .create_terminal(working_directory, window_id, cx) + .create_terminal(working_directory, window, cx) .log_err() }) { let terminal = Box::new(cx.add_view(|cx| { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index a600046ac28..970e0115df2 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -112,11 +112,11 @@ impl TerminalView { let working_directory = get_working_directory(workspace, cx, strategy.working_directory.clone()); - let window_id = cx.window_id(); + let window = cx.window(); let terminal = workspace .project() .update(cx, |project, cx| { - project.create_terminal(working_directory, window_id, cx) + project.create_terminal(working_directory, window, cx) }) .notify_err(workspace, cx); @@ -741,7 +741,7 @@ impl Item for TerminalView { item_id: workspace::ItemId, cx: &mut ViewContext, ) -> Task>> { - let window_id = cx.window_id(); + let window = cx.window(); cx.spawn(|pane, mut cx| async move { let cwd = TERMINAL_DB .get_working_directory(item_id, workspace_id) @@ -762,7 +762,7 @@ impl Item for TerminalView { }); let terminal = project.update(&mut cx, |project, cx| { - project.create_terminal(cwd, window_id, cx) + project.create_terminal(cwd, window, cx) })?; Ok(pane.update(&mut cx, |_, cx| { cx.add_view(|cx| TerminalView::new(terminal, workspace, workspace_id, cx)) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index f0af080d4a6..67827b78038 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -6,6 +6,7 @@ use crate::{AutosaveSetting, DelayedDebouncedEditAction, WorkspaceSettings}; use anyhow::Result; use client::{proto, Client}; use gpui::geometry::vector::Vector2F; +use gpui::AnyWindowHandle; use gpui::{ fonts::HighlightStyle, AnyElement, AnyViewHandle, AppContext, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -250,7 +251,7 @@ pub trait ItemHandle: 'static + fmt::Debug { fn workspace_deactivated(&self, cx: &mut WindowContext); fn navigate(&self, data: Box, cx: &mut WindowContext) -> bool; fn id(&self) -> usize; - fn window_id(&self) -> usize; + fn window(&self) -> AnyWindowHandle; fn as_any(&self) -> &AnyViewHandle; fn is_dirty(&self, cx: &AppContext) -> bool; fn has_conflict(&self, cx: &AppContext) -> bool; @@ -542,8 +543,8 @@ impl ItemHandle for ViewHandle { self.id() } - fn window_id(&self) -> usize { - self.window_id() + fn window(&self) -> AnyWindowHandle { + AnyViewHandle::window(self) } fn as_any(&self) -> &AnyViewHandle { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index ae95838a742..72117e1cabb 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -235,7 +235,7 @@ impl From<&Box> for AnyViewHandle { impl PartialEq for Box { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() && self.window_id() == other.window_id() + self.id() == other.id() && self.window() == other.window() } } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2a1fef6a56e..bb30cac6d30 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -931,11 +931,11 @@ pub fn dock_default_item_factory( .clone(); let working_directory = get_working_directory(workspace, cx, strategy); - let window_id = cx.window_id(); + let window = cx.window(); let terminal = workspace .project() .update(cx, |project, cx| { - project.create_terminal(working_directory, window_id, cx) + project.create_terminal(working_directory, window, cx) }) .notify_err(workspace, cx)?; From fe6a1886c1e26e821bc05cfc106ff3bffe322299 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:20:42 -0600 Subject: [PATCH 068/105] Remove unused dock code --- crates/zed/src/main.rs | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index bb30cac6d30..795c83aba65 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -14,7 +14,7 @@ use futures::{ channel::{mpsc, oneshot}, FutureExt, SinkExt, StreamExt, }; -use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task, ViewContext}; +use gpui::{Action, App, AppContext, AssetSource, AsyncAppContext, Task}; use isahc::{config::Configurable, Request}; use language::{LanguageRegistry, Point}; use log::LevelFilter; @@ -43,7 +43,6 @@ use std::{ time::{Duration, SystemTime, UNIX_EPOCH}, }; use sum_tree::Bias; -use terminal_view::{get_working_directory, TerminalSettings, TerminalView}; use util::{ channel::ReleaseChannel, http::{self, HttpClient}, @@ -56,7 +55,7 @@ use fs::RealFs; #[cfg(debug_assertions)] use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; -use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; +use workspace::AppState; use zed::{ assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, @@ -922,35 +921,6 @@ async fn handle_cli_connection( } } -pub fn dock_default_item_factory( - workspace: &mut Workspace, - cx: &mut ViewContext, -) -> Option> { - let strategy = settings::get::(cx) - .working_directory - .clone(); - let working_directory = get_working_directory(workspace, cx, strategy); - - let window = cx.window(); - let terminal = workspace - .project() - .update(cx, |project, cx| { - project.create_terminal(working_directory, window, cx) - }) - .notify_err(workspace, cx)?; - - let terminal_view = cx.add_view(|cx| { - TerminalView::new( - terminal, - workspace.weak_handle(), - workspace.database_id(), - cx, - ) - }); - - Some(Box::new(terminal_view)) -} - pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { &[ ("Go to file", &file_finder::Toggle), From 1fd80ba8bd12e6dcfb26da3090ab876563422dee Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:22:43 -0600 Subject: [PATCH 069/105] Remove AsyncAppContext::remove_window --- crates/gpui/src/app.rs | 4 ---- crates/workspace/src/workspace.rs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e44ac93818f..2b3a3f3bf4a 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -411,10 +411,6 @@ impl AsyncAppContext { self.update(|cx| cx.add_window(window_options, build_root_view)) } - pub fn remove_window(&mut self, window: AnyWindowHandle) { - self.update_window(window, |cx| cx.remove_window()); - } - pub fn activate_window(&mut self, window: AnyWindowHandle) { self.update_window(window, |cx| cx.activate_window()); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f0990322653..f2a7a442f44 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1254,7 +1254,7 @@ impl Workspace { let prepare = self.prepare_to_close(false, cx); Some(cx.spawn(|_, mut cx| async move { if prepare.await? { - cx.remove_window(window); + window.remove(&mut cx); } Ok(()) })) From 4f10f0ee865b92f6853cef9f9cd046d0c01eb8c0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:23:49 -0600 Subject: [PATCH 070/105] Remove window methods from AsyncAppContext --- crates/gpui/src/app.rs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 2b3a3f3bf4a..ddec0e10a3c 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -344,22 +344,6 @@ impl AsyncAppContext { self.0.borrow().windows().collect() } - pub fn read_window T>( - &self, - window: AnyWindowHandle, - callback: F, - ) -> Option { - self.0.borrow_mut().read_window(window, callback) - } - - pub fn update_window T>( - &mut self, - window: AnyWindowHandle, - callback: F, - ) -> Option { - self.0.borrow_mut().update_window(window, callback) - } - pub fn debug_elements(&self, window: AnyWindowHandle) -> Option { self.0.borrow().read_window(window, |cx| { let root_view = cx.window.root_view(); From 95cd96e4becbf8e1404effb90893e5a1f6f0b3ce Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:27:19 -0600 Subject: [PATCH 071/105] Move debug_elements to AnyWindowHandle --- crates/gpui/src/app.rs | 16 ++++++++-------- crates/zed/src/zed.rs | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index ddec0e10a3c..5eb4ed744cb 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -344,14 +344,6 @@ impl AsyncAppContext { self.0.borrow().windows().collect() } - pub fn debug_elements(&self, window: AnyWindowHandle) -> Option { - self.0.borrow().read_window(window, |cx| { - let root_view = cx.window.root_view(); - let root_element = cx.window.rendered_views.get(&root_view.id())?; - root_element.debug(cx).log_err() - })? - } - pub fn dispatch_action( &mut self, window: AnyWindowHandle, @@ -4060,6 +4052,14 @@ impl AnyWindowHandle { self.update(cx, |cx| cx.remove_window()) } + pub fn debug_elements(&self, cx: &C) -> Option { + self.read_optional_with(cx, |cx| { + let root_view = cx.window.root_view(); + let root_element = cx.window.rendered_views.get(&root_view.id())?; + root_element.debug(cx).log_err() + }) + } + #[cfg(any(test, feature = "test-support"))] pub fn simulate_activation(&self, cx: &mut TestAppContext) { self.update(cx, |cx| { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 6397a1e6b2a..1cde8d5b9a5 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -182,7 +182,7 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { let window = cx.window(); cx.spawn(|workspace, mut cx| async move { let markdown = markdown.await.log_err(); - let content = to_string_pretty(&cx.debug_elements(window).ok_or_else(|| { + let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { anyhow!("could not debug elements for window {}", window.id()) })?) .unwrap(); From b2d9ccc0a2f97a8ad4cde4fa15aa9bd500bfc63e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:38:07 -0600 Subject: [PATCH 072/105] Move more window methods off AsyncAppContext --- crates/collab_ui/src/contact_list.rs | 4 +- crates/command_palette/src/command_palette.rs | 13 ++-- crates/context_menu/src/context_menu.rs | 14 +++- crates/gpui/src/app.rs | 71 +++++++++---------- crates/search/src/project_search.rs | 12 ++-- crates/workspace/src/dock.rs | 7 +- crates/workspace/src/workspace.rs | 12 ++-- crates/zed/src/zed.rs | 4 +- 8 files changed, 73 insertions(+), 64 deletions(-) diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index a2b281856d9..49784523845 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -312,11 +312,11 @@ impl ContactList { .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) .await { - cx.prompt( - window, + window.prompt( PromptLevel::Info, &format!("Failed to remove contact: {}", e), &["Ok"], + &mut cx, ); } } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 0d398fcce90..101d4dc545c 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -1,8 +1,8 @@ use collections::CommandPaletteFilter; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle, AppContext, Element, - MouseState, ViewContext, + actions, anyhow::anyhow, elements::*, keymap_matcher::Keystroke, Action, AnyWindowHandle, + AppContext, Element, MouseState, ViewContext, }; use picker::{Picker, PickerDelegate, PickerEvent}; use std::cmp; @@ -83,9 +83,10 @@ impl PickerDelegate for CommandPaletteDelegate { let view_id = self.focused_view_id; let window = cx.window(); cx.spawn(move |picker, mut cx| async move { - let actions = cx - .available_actions(window, view_id) + let actions = window + .available_actions(view_id, &cx) .into_iter() + .flatten() .filter_map(|(name, action, bindings)| { let filtered = cx.read(|cx| { if cx.has_global::() { @@ -168,7 +169,9 @@ impl PickerDelegate for CommandPaletteDelegate { let action = self.actions.remove(action_ix).action; cx.app_context() .spawn(move |mut cx| async move { - cx.dispatch_action(window, focused_view_id, action.as_ref()) + window + .dispatch_action(focused_view_id, action.as_ref(), &mut cx) + .ok_or_else(|| anyhow!("window was closed")) }) .detach_and_log_err(cx); } diff --git a/crates/context_menu/src/context_menu.rs b/crates/context_menu/src/context_menu.rs index 75cfd872b29..a5534b6262c 100644 --- a/crates/context_menu/src/context_menu.rs +++ b/crates/context_menu/src/context_menu.rs @@ -1,5 +1,5 @@ use gpui::{ - anyhow, + anyhow::{self, anyhow}, elements::*, geometry::vector::Vector2F, keymap_matcher::KeymapContext, @@ -223,7 +223,9 @@ impl ContextMenu { let action = action.boxed_clone(); cx.app_context() .spawn(|mut cx| async move { - cx.dispatch_action(window, view_id, action.as_ref()) + window + .dispatch_action(view_id, action.as_ref(), &mut cx) + .ok_or_else(|| anyhow!("window was closed")) }) .detach_and_log_err(cx); } @@ -486,7 +488,13 @@ impl ContextMenu { let action = action.boxed_clone(); cx.app_context() .spawn(|mut cx| async move { - cx.dispatch_action(window, view_id, action.as_ref()) + window + .dispatch_action( + view_id, + action.as_ref(), + &mut cx, + ) + .ok_or_else(|| anyhow!("window was closed")) }) .detach_and_log_err(cx); } diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5eb4ed744cb..e6e11ca321b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -344,29 +344,6 @@ impl AsyncAppContext { self.0.borrow().windows().collect() } - pub fn dispatch_action( - &mut self, - window: AnyWindowHandle, - view_id: usize, - action: &dyn Action, - ) -> Result<()> { - self.0 - .borrow_mut() - .update_window(window, |cx| { - cx.dispatch_action(Some(view_id), action); - }) - .ok_or_else(|| anyhow!("window not found")) - } - - pub fn available_actions( - &self, - window: AnyWindowHandle, - view_id: usize, - ) -> Vec<(&'static str, Box, SmallVec<[Binding; 1]>)> { - self.read_window(window, |cx| cx.available_actions(view_id)) - .unwrap_or_default() - } - pub fn add_model(&mut self, build_model: F) -> ModelHandle where T: Entity, @@ -387,21 +364,6 @@ impl AsyncAppContext { self.update(|cx| cx.add_window(window_options, build_root_view)) } - pub fn activate_window(&mut self, window: AnyWindowHandle) { - self.update_window(window, |cx| cx.activate_window()); - } - - // TODO: Can we eliminate this method and move it to WindowContext then call it with update_window?s - pub fn prompt( - &mut self, - window: AnyWindowHandle, - level: PromptLevel, - msg: &str, - answers: &[&str], - ) -> Option> { - self.update_window(window, |cx| cx.prompt(level, msg, answers)) - } - pub fn platform(&self) -> Arc { self.0.borrow().platform().clone() } @@ -4060,6 +4022,39 @@ impl AnyWindowHandle { }) } + pub fn activate(&mut self, cx: &mut C) -> C::Result<()> { + self.update(cx, |cx| cx.activate_window()) + } + + pub fn prompt( + &self, + level: PromptLevel, + msg: &str, + answers: &[&str], + cx: &mut C, + ) -> C::Result> { + self.update(cx, |cx| cx.prompt(level, msg, answers)) + } + + pub fn dispatch_action( + &self, + view_id: usize, + action: &dyn Action, + cx: &mut C, + ) -> C::Result<()> { + self.update(cx, |cx| { + cx.dispatch_action(Some(view_id), action); + }) + } + + pub fn available_actions( + &self, + view_id: usize, + cx: &C, + ) -> C::Result, SmallVec<[Binding; 1]>)>> { + self.read_with(cx, |cx| cx.available_actions(view_id)) + } + #[cfg(any(test, feature = "test-support"))] pub fn simulate_activation(&self, cx: &mut TestAppContext) { self.update(cx, |cx| { diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 202d494560e..018ab9cb115 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -1599,7 +1599,7 @@ pub mod tests { let search_view_id = search_view.id(); cx.spawn(|mut cx| async move { - cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); }) .detach(); deterministic.run_until_parked(); @@ -1650,9 +1650,9 @@ pub mod tests { "Search view should be focused after mismatching query had been used in search", ); }); - cx.spawn(|mut cx| async move { - cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) - }) + cx.spawn( + |mut cx| async move { window.dispatch_action(search_view_id, &ToggleFocus, &mut cx) }, + ) .detach(); deterministic.run_until_parked(); search_view.update(cx, |search_view, cx| { @@ -1683,7 +1683,7 @@ pub mod tests { ); }); cx.spawn(|mut cx| async move { - cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); }) .detach(); deterministic.run_until_parked(); @@ -1713,7 +1713,7 @@ pub mod tests { }); cx.spawn(|mut cx| async move { - cx.dispatch_action(window.into(), search_view_id, &ToggleFocus) + window.dispatch_action(search_view_id, &ToggleFocus, &mut cx); }) .detach(); deterministic.run_until_parked(); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 5bb0fd93f8c..e33c0a53917 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -534,8 +534,11 @@ impl View for PanelButtons { let view_id = this.workspace.id(); let tooltip_action = tooltip_action.boxed_clone(); cx.spawn(|_, mut cx| async move { - cx.dispatch_action(window, view_id, &*tooltip_action) - .ok(); + window.dispatch_action( + view_id, + &*tooltip_action, + &mut cx, + ); }) .detach(); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f2a7a442f44..2f884b0ceb3 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1280,11 +1280,11 @@ impl Workspace { && workspace_count == 1 && active_call.read_with(&cx, |call, _| call.room().is_some()) { - let answer = cx.prompt( - window, + let answer = window.prompt( PromptLevel::Warning, "Do you want to leave the current call?", &["Close window and hang up", "Cancel"], + &mut cx, ); if let Some(mut answer) = answer { @@ -4000,7 +4000,7 @@ pub fn join_remote_project( workspace.downgrade() }; - cx.activate_window(workspace.window()); + workspace.window().activate(&mut cx); cx.platform().activate(true); workspace.update(&mut cx, |workspace, cx| { @@ -4049,12 +4049,12 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { // prompt in the active window before switching to a different window. workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - let answer = cx.prompt( - window.into(), + if let (true, Some(window)) = (should_confirm, workspace_windows.first()) { + let answer = window.prompt( PromptLevel::Info, "Are you sure you want to restart?", &["Restart", "Cancel"], + &mut cx, ); if let Some(mut answer) = answer { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1cde8d5b9a5..8ad8080375e 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -416,11 +416,11 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) { workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - let answer = cx.prompt( - window.into(), + let answer = window.prompt( PromptLevel::Info, "Are you sure you want to quit?", &["Quit", "Cancel"], + &mut cx, ); if let Some(mut answer) = answer { From b77c336a3d719cc1a483e58319bcdf310739e774 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 11:39:56 -0600 Subject: [PATCH 073/105] Return window handles from WeakItemHandle --- crates/workspace/src/item.rs | 6 +++--- crates/workspace/src/searchable.rs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 67827b78038..21956be4468 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -281,7 +281,7 @@ pub trait ItemHandle: 'static + fmt::Debug { pub trait WeakItemHandle { fn id(&self) -> usize; - fn window_id(&self) -> usize; + fn window(&self) -> AnyWindowHandle; fn upgrade(&self, cx: &AppContext) -> Option>; } @@ -650,8 +650,8 @@ impl WeakItemHandle for WeakViewHandle { self.id() } - fn window_id(&self) -> usize { - self.window_id() + fn window(&self) -> AnyWindowHandle { + self.window() } fn upgrade(&self, cx: &AppContext) -> Option> { diff --git a/crates/workspace/src/searchable.rs b/crates/workspace/src/searchable.rs index 72117e1cabb..7a470db7c92 100644 --- a/crates/workspace/src/searchable.rs +++ b/crates/workspace/src/searchable.rs @@ -259,7 +259,7 @@ impl WeakSearchableItemHandle for WeakViewHandle { impl PartialEq for Box { fn eq(&self, other: &Self) -> bool { - self.id() == other.id() && self.window_id() == other.window_id() + self.id() == other.id() && self.window() == other.window() } } @@ -267,6 +267,6 @@ impl Eq for Box {} impl std::hash::Hash for Box { fn hash(&self, state: &mut H) { - (self.id(), self.window_id()).hash(state) + (self.id(), self.window().id()).hash(state) } } From afd89b256abb4e5519446de40c6c52067d1b6587 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 16:06:53 -0600 Subject: [PATCH 074/105] Store AnyWindowHandles instead of usizes --- crates/gpui/src/app.rs | 126 ++++++++------------ crates/gpui/src/app/ref_counts.rs | 14 +-- crates/gpui/src/app/test_app_context.rs | 4 +- crates/gpui/src/app/window.rs | 28 ++--- crates/gpui/src/app/window_input_handler.rs | 2 +- crates/gpui/src/platform.rs | 8 +- crates/gpui/src/platform/mac/platform.rs | 12 +- crates/gpui/src/platform/mac/window.rs | 17 +-- crates/gpui/src/platform/test.rs | 40 ++++--- 9 files changed, 117 insertions(+), 134 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e6e11ca321b..507cbffad27 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -444,9 +444,9 @@ type WindowShouldCloseSubscriptionCallback = Box b pub struct AppContext { models: HashMap>, - views: HashMap<(usize, usize), Box>, - views_metadata: HashMap<(usize, usize), ViewMetadata>, - windows: HashMap, + views: HashMap<(AnyWindowHandle, usize), Box>, + views_metadata: HashMap<(AnyWindowHandle, usize), ViewMetadata>, + windows: HashMap, globals: HashMap>, element_states: HashMap>, background: Arc, @@ -727,12 +727,12 @@ impl AppContext { } pub fn view_ui_name(&self, window: AnyWindowHandle, view_id: usize) -> Option<&'static str> { - Some(self.views.get(&(window.id(), view_id))?.ui_name()) + Some(self.views.get(&(window, view_id))?.ui_name()) } pub fn view_type_id(&self, window: AnyWindowHandle, view_id: usize) -> Option { self.views_metadata - .get(&(window.id(), view_id)) + .get(&(window, view_id)) .map(|metadata| metadata.type_id) } @@ -758,7 +758,7 @@ impl AppContext { handle: AnyWindowHandle, callback: F, ) -> Option { - let window = self.windows.get(&handle.id())?; + let window = self.windows.get(&handle)?; let window_context = WindowContext::immutable(self, &window, handle); Some(callback(&window_context)) } @@ -1033,7 +1033,7 @@ impl AppContext { if let Some(focused_view_id) = cx.focused_view_id() { for view_id in cx.ancestors(focused_view_id) { if let Some(view_metadata) = - cx.views_metadata.get(&(cx.window_handle.id(), view_id)) + cx.views_metadata.get(&(cx.window_handle, view_id)) { if let Some(actions) = cx.actions.get(&view_metadata.type_id) { if actions.contains_key(&action_id) { @@ -1259,13 +1259,12 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_id); + let handle = WindowHandle::::new(post_inc(&mut this.next_id)); let platform_window = this.platform - .open_window(window_id, window_options, this.foreground.clone()); - let handle = WindowHandle::::new(window_id); - let window = this.build_window(handle, platform_window, build_root_view); - this.windows.insert(window_id, window); + .open_window(handle.into(), window_options, this.foreground.clone()); + let window = this.build_window(handle.into(), platform_window, build_root_view); + this.windows.insert(handle.into(), window); handle }) } @@ -1276,29 +1275,25 @@ impl AppContext { F: FnOnce(&mut ViewContext) -> V, { self.update(|this| { - let window_id = post_inc(&mut this.next_id); - let platform_window = this.platform.add_status_item(window_id); - let handle = WindowHandle::::new(window_id); - let window = this.build_window(handle, platform_window, build_root_view); - this.windows.insert(window_id, window); + let handle = WindowHandle::::new(post_inc(&mut this.next_id)); + let platform_window = this.platform.add_status_item(handle.into()); + let window = this.build_window(handle.into(), platform_window, build_root_view); + this.windows.insert(handle.into(), window); handle.update_root(this, |view, cx| view.focus_in(cx.handle().into_any(), cx)); handle }) } - pub fn build_window( + pub fn build_window( &mut self, - handle: H, + handle: AnyWindowHandle, mut platform_window: Box, build_root_view: F, ) -> Window where - H: Into, V: View, F: FnOnce(&mut ViewContext) -> V, { - let handle: AnyWindowHandle = handle.into(); - { let mut app = self.upgrade(); @@ -1371,21 +1366,15 @@ impl AppContext { } pub fn active_window(&self) -> Option { - self.platform.main_window_id().and_then(|main_window_id| { - self.windows - .get(&main_window_id) - .map(|window| AnyWindowHandle::new(main_window_id, window.root_view().type_id())) - }) + self.platform.main_window() } pub fn windows(&self) -> impl '_ + Iterator { - self.windows.iter().map(|(window_id, window)| { - AnyWindowHandle::new(*window_id, window.root_view().type_id()) - }) + self.windows.keys().copied() } pub fn read_view(&self, handle: &ViewHandle) -> &T { - if let Some(view) = self.views.get(&(handle.window.id(), handle.view_id)) { + if let Some(view) = self.views.get(&(handle.window, handle.view_id)) { view.as_any().downcast_ref().expect("downcast is type safe") } else { panic!("circular view reference for type {}", type_name::()); @@ -1437,13 +1426,13 @@ impl AppContext { .push_back(Effect::ModelRelease { model_id, model }); } - for (window_id, view_id) in dropped_views { + for (window, view_id) in dropped_views { self.subscriptions.remove(view_id); self.observations.remove(view_id); - self.views_metadata.remove(&(window_id, view_id)); - let mut view = self.views.remove(&(window_id, view_id)).unwrap(); + self.views_metadata.remove(&(window, view_id)); + let mut view = self.views.remove(&(window, view_id)).unwrap(); view.release(self); - if let Some(window) = self.windows.get_mut(&window_id) { + if let Some(window) = self.windows.get_mut(&window) { window.parents.remove(&view_id); window .invalidation @@ -1571,7 +1560,7 @@ impl AppContext { } Effect::ResizeWindow { window } => { - if let Some(window) = self.windows.get_mut(&window.id()) { + if let Some(window) = self.windows.get_mut(&window) { window .invalidation .get_or_insert(WindowInvalidation::default()); @@ -1696,15 +1685,14 @@ impl AppContext { for old_ancestor in old_ancestors.iter().copied() { if !new_ancestors.contains(&old_ancestor) { if let Some(mut view) = - cx.views.remove(&(window.id(), old_ancestor)) + cx.views.remove(&(window, old_ancestor)) { view.focus_out( focused_view_id, cx, old_ancestor, ); - cx.views - .insert((window.id(), old_ancestor), view); + cx.views.insert((window, old_ancestor), view); } } } @@ -1713,15 +1701,14 @@ impl AppContext { for new_ancestor in new_ancestors.iter().copied() { if !old_ancestors.contains(&new_ancestor) { if let Some(mut view) = - cx.views.remove(&(window.id(), new_ancestor)) + cx.views.remove(&(window, new_ancestor)) { view.focus_in( focused_view_id, cx, new_ancestor, ); - cx.views - .insert((window.id(), new_ancestor), view); + cx.views.insert((window, new_ancestor), view); } } } @@ -1730,9 +1717,7 @@ impl AppContext { // there isn't any pending focus, focus the root view. let root_view_id = cx.window.root_view().id(); if focused_view_id != root_view_id - && !cx - .views - .contains_key(&(window.id(), focused_view_id)) + && !cx.views.contains_key(&(window, focused_view_id)) && !focus_effects.contains_key(&window.id()) { focus_effects.insert( @@ -1837,13 +1822,13 @@ impl AppContext { observed_window: AnyWindowHandle, observed_view_id: usize, ) { - let view_key = (observed_window.id(), observed_view_id); + let view_key = (observed_window, observed_view_id); if let Some((view, mut view_metadata)) = self .views .remove(&view_key) .zip(self.views_metadata.remove(&view_key)) { - if let Some(window) = self.windows.get_mut(&observed_window.id()) { + if let Some(window) = self.windows.get_mut(&observed_window) { window .invalidation .get_or_insert_with(Default::default) @@ -1924,7 +1909,7 @@ impl AppContext { let focused_id = match effect { FocusEffect::View { view_id, .. } => { if let Some(view_id) = view_id { - if cx.views.contains_key(&(window.id(), view_id)) { + if cx.views.contains_key(&(window, view_id)) { Some(view_id) } else { Some(cx.root_view().id()) @@ -1949,9 +1934,9 @@ impl AppContext { if focus_changed { if let Some(blurred_id) = blurred_id { for view_id in cx.ancestors(blurred_id).collect::>() { - if let Some(mut view) = cx.views.remove(&(window.id(), view_id)) { + if let Some(mut view) = cx.views.remove(&(window, view_id)) { view.focus_out(blurred_id, cx, view_id); - cx.views.insert((window.id(), view_id), view); + cx.views.insert((window, view_id), view); } } @@ -1963,9 +1948,9 @@ impl AppContext { if focus_changed || effect.is_forced() { if let Some(focused_id) = focused_id { for view_id in cx.ancestors(focused_id).collect::>() { - if let Some(mut view) = cx.views.remove(&(window.id(), view_id)) { + if let Some(mut view) = cx.views.remove(&(window, view_id)) { view.focus_in(focused_id, cx, view_id); - cx.views.insert((window.id(), view_id), view); + cx.views.insert((window, view_id), view); } } @@ -1991,7 +1976,7 @@ impl AppContext { mut callback: WindowShouldCloseSubscriptionCallback, ) { let mut app = self.upgrade(); - if let Some(window) = self.windows.get_mut(&window.id()) { + if let Some(window) = self.windows.get_mut(&window) { window .platform_window .on_should_close(Box::new(move || app.update(|cx| callback(cx)))) @@ -2127,15 +2112,13 @@ impl BorrowWindowContext for AppContext { where F: FnOnce(&mut WindowContext) -> T, { - self.update(|app_context| { - let mut window = app_context.windows.remove(&handle.id())?; - let mut window_context = WindowContext::mutable(app_context, &mut window, handle); - let result = f(&mut window_context); - if !window_context.removed { - app_context.windows.insert(handle.id(), window); - } - Some(result) - }) + let mut window = self.windows.remove(&handle)?; + let mut window_context = WindowContext::mutable(self, &mut window, handle); + let result = f(&mut window_context); + if !window_context.removed { + self.windows.insert(handle, window); + } + Some(result) } fn update_window_optional(&mut self, handle: AnyWindowHandle, f: F) -> Option @@ -2590,7 +2573,7 @@ where } else { let focused_type = cx .views_metadata - .get(&(cx.window_handle.id(), focused_id)) + .get(&(cx.window_handle, focused_id)) .unwrap() .type_id; AnyViewHandle::new( @@ -2610,7 +2593,7 @@ where } else { let blurred_type = cx .views_metadata - .get(&(cx.window_handle.id(), blurred_id)) + .get(&(cx.window_handle, blurred_id)) .unwrap() .type_id; AnyViewHandle::new( @@ -3413,7 +3396,7 @@ impl<'a, 'b, 'c, V: View> LayoutContext<'a, 'b, 'c, V> { let mut contexts = Vec::new(); let mut handler_depth = None; for (i, view_id) in self.ancestors(view_id).enumerate() { - if let Some(view_metadata) = self.views_metadata.get(&(window.id(), view_id)) { + if let Some(view_metadata) = self.views_metadata.get(&(window, view_id)) { if let Some(actions) = self.actions.get(&view_metadata.type_id) { if actions.contains_key(&action.id()) { handler_depth = Some(i); @@ -3873,10 +3856,7 @@ impl Copy for WindowHandle {} impl WindowHandle { fn new(window_id: usize) -> Self { WindowHandle { - any_handle: AnyWindowHandle { - window_id, - root_view_type: TypeId::of::(), - }, + any_handle: AnyWindowHandle::new(window_id, TypeId::of::()), root_view_type: PhantomData, } } @@ -4240,7 +4220,7 @@ impl AnyViewHandle { view_type: TypeId, ref_counts: Arc>, ) -> Self { - ref_counts.lock().inc_view(window.id(), view_id); + ref_counts.lock().inc_view(window, view_id); #[cfg(any(test, feature = "test-support"))] let handle_id = ref_counts @@ -4308,7 +4288,7 @@ impl AnyViewHandle { pub fn debug_json<'a, 'b>(&self, cx: &'b WindowContext<'a>) -> serde_json::Value { cx.views - .get(&(self.window.id(), self.view_id)) + .get(&(self.window, self.view_id)) .map_or_else(|| serde_json::Value::Null, |view| view.debug_json(cx)) } } @@ -4338,9 +4318,7 @@ impl PartialEq> for AnyViewHandle { impl Drop for AnyViewHandle { fn drop(&mut self) { - self.ref_counts - .lock() - .dec_view(self.window.id(), self.view_id); + self.ref_counts.lock().dec_view(self.window, self.view_id); #[cfg(any(test, feature = "test-support"))] self.ref_counts .lock() diff --git a/crates/gpui/src/app/ref_counts.rs b/crates/gpui/src/app/ref_counts.rs index f0c1699f165..63905326fe2 100644 --- a/crates/gpui/src/app/ref_counts.rs +++ b/crates/gpui/src/app/ref_counts.rs @@ -9,7 +9,7 @@ use collections::{hash_map::Entry, HashMap, HashSet}; #[cfg(any(test, feature = "test-support"))] use crate::util::post_inc; -use crate::ElementStateId; +use crate::{AnyWindowHandle, ElementStateId}; lazy_static! { static ref LEAK_BACKTRACE: bool = @@ -26,7 +26,7 @@ pub struct RefCounts { entity_counts: HashMap, element_state_counts: HashMap, dropped_models: HashSet, - dropped_views: HashSet<(usize, usize)>, + dropped_views: HashSet<(AnyWindowHandle, usize)>, dropped_element_states: HashSet, #[cfg(any(test, feature = "test-support"))] @@ -55,12 +55,12 @@ impl RefCounts { } } - pub fn inc_view(&mut self, window_id: usize, view_id: usize) { + pub fn inc_view(&mut self, window: AnyWindowHandle, view_id: usize) { match self.entity_counts.entry(view_id) { Entry::Occupied(mut entry) => *entry.get_mut() += 1, Entry::Vacant(entry) => { entry.insert(1); - self.dropped_views.remove(&(window_id, view_id)); + self.dropped_views.remove(&(window, view_id)); } } } @@ -94,12 +94,12 @@ impl RefCounts { } } - pub fn dec_view(&mut self, window_id: usize, view_id: usize) { + pub fn dec_view(&mut self, window: AnyWindowHandle, view_id: usize) { let count = self.entity_counts.get_mut(&view_id).unwrap(); *count -= 1; if *count == 0 { self.entity_counts.remove(&view_id); - self.dropped_views.insert((window_id, view_id)); + self.dropped_views.insert((window, view_id)); } } @@ -120,7 +120,7 @@ impl RefCounts { &mut self, ) -> ( HashSet, - HashSet<(usize, usize)>, + HashSet<(AnyWindowHandle, usize)>, HashSet, ) { ( diff --git a/crates/gpui/src/app/test_app_context.rs b/crates/gpui/src/app/test_app_context.rs index b1729d89b85..6d593c2e724 100644 --- a/crates/gpui/src/app/test_app_context.rs +++ b/crates/gpui/src/app/test_app_context.rs @@ -159,7 +159,7 @@ impl TestAppContext { .borrow_mut() .add_window(Default::default(), build_root_view); window.simulate_activation(self); - WindowHandle::new(window.id()) + window } pub fn observe_global(&mut self, callback: F) -> Subscription @@ -516,7 +516,7 @@ impl AnyWindowHandle { cx: &'a mut TestAppContext, ) -> std::cell::RefMut<'a, platform::test::Window> { std::cell::RefMut::map(cx.cx.borrow_mut(), |state| { - let window = state.windows.get_mut(&self.window_id).unwrap(); + let window = state.windows.get_mut(&self).unwrap(); let test_window = window .platform_window .as_any_mut() diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index df3f8207556..d39219e65d4 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -235,9 +235,9 @@ impl<'a> WindowContext<'a> { F: FnOnce(&mut dyn AnyView, &mut Self) -> T, { let handle = self.window_handle; - let mut view = self.views.remove(&(handle.id(), view_id))?; + let mut view = self.views.remove(&(handle, view_id))?; let result = f(view.as_mut(), self); - self.views.insert((handle.id(), view_id), view); + self.views.insert((handle, view_id), view); Some(result) } @@ -389,7 +389,7 @@ impl<'a> WindowContext<'a> { let mut contexts = Vec::new(); let mut handler_depths_by_action_id = HashMap::::default(); for (depth, view_id) in self.ancestors(view_id).enumerate() { - if let Some(view_metadata) = self.views_metadata.get(&(handle.id(), view_id)) { + if let Some(view_metadata) = self.views_metadata.get(&(handle, view_id)) { contexts.push(view_metadata.keymap_context.clone()); if let Some(actions) = self.actions.get(&view_metadata.type_id) { handler_depths_by_action_id @@ -440,7 +440,7 @@ impl<'a> WindowContext<'a> { .ancestors(focused_view_id) .filter_map(|view_id| { self.views_metadata - .get(&(handle.id(), view_id)) + .get(&(handle, view_id)) .map(|view| (view_id, view.keymap_context.clone())) }) .collect(); @@ -850,9 +850,9 @@ impl<'a> WindowContext<'a> { let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { + if let Some(mut view) = self.views.remove(&(handle, view_id)) { let handled = view.key_down(event, self, view_id); - self.views.insert((handle.id(), view_id), view); + self.views.insert((handle, view_id), view); if handled { return true; } @@ -869,9 +869,9 @@ impl<'a> WindowContext<'a> { let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { + if let Some(mut view) = self.views.remove(&(handle, view_id)) { let handled = view.key_up(event, self, view_id); - self.views.insert((handle.id(), view_id), view); + self.views.insert((handle, view_id), view); if handled { return true; } @@ -888,9 +888,9 @@ impl<'a> WindowContext<'a> { let handle = self.window_handle; if let Some(focused_view_id) = self.window.focused_view_id { for view_id in self.ancestors(focused_view_id).collect::>() { - if let Some(mut view) = self.views.remove(&(handle.id(), view_id)) { + if let Some(mut view) = self.views.remove(&(handle, view_id)) { let handled = view.modifiers_changed(event, self, view_id); - self.views.insert((handle.id(), view_id), view); + self.views.insert((handle, view_id), view); if handled { return true; } @@ -929,10 +929,10 @@ impl<'a> WindowContext<'a> { let view_id = params.view_id; let mut view = self .views - .remove(&(handle.id(), view_id)) + .remove(&(handle, view_id)) .ok_or_else(|| anyhow!("view not found"))?; let element = view.render(self, view_id); - self.views.insert((handle.id(), view_id), view); + self.views.insert((handle, view_id), view); Ok(element) } @@ -1190,13 +1190,13 @@ impl<'a> WindowContext<'a> { let mut keymap_context = KeymapContext::default(); view.update_keymap_context(&mut keymap_context, cx.app_context()); self.views_metadata.insert( - (handle.id(), view_id), + (handle, view_id), ViewMetadata { type_id: TypeId::of::(), keymap_context, }, ); - self.views.insert((handle.id(), view_id), Box::new(view)); + self.views.insert((handle, view_id), Box::new(view)); self.window .invalidation .get_or_insert_with(Default::default) diff --git a/crates/gpui/src/app/window_input_handler.rs b/crates/gpui/src/app/window_input_handler.rs index d7c65b11fae..bdc871a8027 100644 --- a/crates/gpui/src/app/window_input_handler.rs +++ b/crates/gpui/src/app/window_input_handler.rs @@ -23,7 +23,7 @@ impl WindowInputHandler { let mut app = self.app.try_borrow_mut().ok()?; self.window.update_optional(&mut *app, |cx| { let view_id = cx.window.focused_view_id?; - let view = cx.views.get(&(self.window.id(), view_id))?; + let view = cx.views.get(&(self.window, view_id))?; let result = f(view.as_ref(), &cx); Some(result) }) diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 67f8e52c04f..1d93a45fc7b 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -19,7 +19,7 @@ use crate::{ }, keymap_matcher::KeymapMatcher, text_layout::{LineLayout, RunStyle}, - Action, ClipboardItem, Menu, Scene, + Action, AnyWindowHandle, ClipboardItem, Menu, Scene, }; use anyhow::{anyhow, bail, Result}; use async_task::Runnable; @@ -58,13 +58,13 @@ pub trait Platform: Send + Sync { fn open_window( &self, - id: usize, + handle: AnyWindowHandle, options: WindowOptions, executor: Rc, ) -> Box; - fn main_window_id(&self) -> Option; + fn main_window(&self) -> Option; - fn add_status_item(&self, id: usize) -> Box; + fn add_status_item(&self, handle: AnyWindowHandle) -> Box; fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 509c979b853..277ff8403f8 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -6,7 +6,7 @@ use crate::{ executor, keymap_matcher::KeymapMatcher, platform::{self, AppVersion, CursorStyle, Event}, - Action, ClipboardItem, Menu, MenuItem, + Action, AnyWindowHandle, ClipboardItem, Menu, MenuItem, }; use anyhow::{anyhow, Result}; use block::ConcreteBlock; @@ -590,18 +590,18 @@ impl platform::Platform for MacPlatform { fn open_window( &self, - id: usize, + handle: AnyWindowHandle, options: platform::WindowOptions, executor: Rc, ) -> Box { - Box::new(Window::open(id, options, executor, self.fonts())) + Box::new(Window::open(handle, options, executor, self.fonts())) } - fn main_window_id(&self) -> Option { - Window::main_window_id() + fn main_window(&self) -> Option { + Window::main_window() } - fn add_status_item(&self, _id: usize) -> Box { + fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { Box::new(StatusItem::add(self.fonts())) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c0f0ade7b92..59324f473a9 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -13,6 +13,7 @@ use crate::{ Event, InputHandler, KeyDownEvent, Modifiers, ModifiersChangedEvent, MouseButton, MouseButtonEvent, MouseMovedEvent, Scene, WindowBounds, WindowKind, }, + AnyWindowHandle, }; use block::ConcreteBlock; use cocoa::{ @@ -282,7 +283,7 @@ struct InsertText { } struct WindowState { - id: usize, + handle: AnyWindowHandle, native_window: id, kind: WindowKind, event_callback: Option bool>>, @@ -426,7 +427,7 @@ pub struct Window(Rc>); impl Window { pub fn open( - id: usize, + handle: AnyWindowHandle, options: platform::WindowOptions, executor: Rc, fonts: Arc, @@ -504,7 +505,7 @@ impl Window { assert!(!native_view.is_null()); let window = Self(Rc::new(RefCell::new(WindowState { - id, + handle, native_window, kind: options.kind, event_callback: None, @@ -621,13 +622,13 @@ impl Window { } } - pub fn main_window_id() -> Option { + pub fn main_window() -> Option { unsafe { let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; if msg_send![main_window, isKindOfClass: WINDOW_CLASS] { - let id = get_window_state(&*main_window).borrow().id; - Some(id) + let handle = get_window_state(&*main_window).borrow().handle; + Some(handle) } else { None } @@ -881,7 +882,7 @@ impl platform::Window for Window { fn is_topmost_for_position(&self, position: Vector2F) -> bool { let self_borrow = self.0.borrow(); - let self_id = self_borrow.id; + let self_id = self_borrow.handle; unsafe { let app = NSApplication::sharedApplication(nil); @@ -898,7 +899,7 @@ impl platform::Window for Window { let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS]; let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS]; if is_panel == YES || is_window == YES { - let topmost_window_id = get_window_state(&*top_most_window).borrow().id; + let topmost_window_id = get_window_state(&*top_most_window).borrow().handle; topmost_window_id == self_id } else { // Someone else's window is on top diff --git a/crates/gpui/src/platform/test.rs b/crates/gpui/src/platform/test.rs index f949a6cd78e..6c11817b5c1 100644 --- a/crates/gpui/src/platform/test.rs +++ b/crates/gpui/src/platform/test.rs @@ -5,7 +5,7 @@ use crate::{ vector::{vec2f, Vector2F}, }, keymap_matcher::KeymapMatcher, - Action, ClipboardItem, Menu, + Action, AnyWindowHandle, ClipboardItem, Menu, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -102,7 +102,7 @@ pub struct Platform { fonts: Arc, current_clipboard_item: Mutex>, cursor: Mutex, - active_window_id: Arc>>, + active_window: Arc>>, } impl Platform { @@ -112,7 +112,7 @@ impl Platform { fonts: Arc::new(super::current::FontSystem::new()), current_clipboard_item: Default::default(), cursor: Mutex::new(CursorStyle::Arrow), - active_window_id: Default::default(), + active_window: Default::default(), } } } @@ -146,30 +146,30 @@ impl super::Platform for Platform { fn open_window( &self, - id: usize, + handle: AnyWindowHandle, options: super::WindowOptions, _executor: Rc, ) -> Box { - *self.active_window_id.lock() = Some(id); + *self.active_window.lock() = Some(handle); Box::new(Window::new( - id, + handle, match options.bounds { WindowBounds::Maximized | WindowBounds::Fullscreen => vec2f(1024., 768.), WindowBounds::Fixed(rect) => rect.size(), }, - self.active_window_id.clone(), + self.active_window.clone(), )) } - fn main_window_id(&self) -> Option { - self.active_window_id.lock().clone() + fn main_window(&self) -> Option { + self.active_window.lock().clone() } - fn add_status_item(&self, id: usize) -> Box { + fn add_status_item(&self, handle: AnyWindowHandle) -> Box { Box::new(Window::new( - id, + handle, vec2f(24., 24.), - self.active_window_id.clone(), + self.active_window.clone(), )) } @@ -256,7 +256,7 @@ impl super::Screen for Screen { } pub struct Window { - id: usize, + handle: AnyWindowHandle, pub(crate) size: Vector2F, scale_factor: f32, current_scene: Option, @@ -270,13 +270,17 @@ pub struct Window { pub(crate) title: Option, pub(crate) edited: bool, pub(crate) pending_prompts: RefCell>>, - active_window_id: Arc>>, + active_window: Arc>>, } impl Window { - pub fn new(id: usize, size: Vector2F, active_window_id: Arc>>) -> Self { + pub fn new( + handle: AnyWindowHandle, + size: Vector2F, + active_window: Arc>>, + ) -> Self { Self { - id, + handle, size, event_handlers: Default::default(), resize_handlers: Default::default(), @@ -290,7 +294,7 @@ impl Window { title: None, edited: false, pending_prompts: Default::default(), - active_window_id, + active_window, } } @@ -342,7 +346,7 @@ impl super::Window for Window { } fn activate(&self) { - *self.active_window_id.lock() = Some(self.id); + *self.active_window.lock() = Some(self.handle); } fn set_title(&mut self, title: &str) { From 8e49d1419a2ec3e832fc6716f2795305f35320df Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 16:38:46 -0600 Subject: [PATCH 075/105] Minimize window id usage --- crates/gpui/src/app.rs | 58 +++++++++++++------------- crates/gpui/src/app/window.rs | 10 ++--- crates/gpui/src/platform/mac/window.rs | 6 +-- crates/theme/src/ui.rs | 1 - 4 files changed, 38 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 507cbffad27..8020147844d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -477,10 +477,10 @@ pub struct AppContext { focus_observations: CallbackCollection, release_observations: CallbackCollection, action_dispatch_observations: CallbackCollection<(), ActionObservationCallback>, - window_activation_observations: CallbackCollection, - window_fullscreen_observations: CallbackCollection, - window_bounds_observations: CallbackCollection, - keystroke_observations: CallbackCollection, + window_activation_observations: CallbackCollection, + window_fullscreen_observations: CallbackCollection, + window_bounds_observations: CallbackCollection, + keystroke_observations: CallbackCollection, active_labeled_task_observations: CallbackCollection<(), ActiveLabeledTasksCallback>, foreground: Rc, @@ -1460,7 +1460,7 @@ impl AppContext { let mut refreshing = false; let mut updated_windows = HashSet::default(); - let mut focus_effects = HashMap::::default(); + let mut focus_effects = HashMap::::default(); loop { self.remove_dropped_entities(); if let Some(effect) = self.pending_effects.pop_front() { @@ -1538,13 +1538,13 @@ impl AppContext { Effect::Focus(mut effect) => { if focus_effects - .get(&effect.window().id()) + .get(&effect.window()) .map_or(false, |prev_effect| prev_effect.is_forced()) { effect.force(); } - focus_effects.insert(effect.window().id(), effect); + focus_effects.insert(effect.window(), effect); } Effect::FocusObservation { @@ -1577,7 +1577,7 @@ impl AppContext { subscription_id, callback, } => self.window_activation_observations.add_callback( - window.id(), + window, subscription_id, callback, ), @@ -1586,7 +1586,7 @@ impl AppContext { if self.handle_window_activation_effect(window, is_active) && is_active { focus_effects - .entry(window.id()) + .entry(window) .or_insert_with(|| FocusEffect::View { window, view_id: self @@ -1603,7 +1603,7 @@ impl AppContext { subscription_id, callback, } => self.window_fullscreen_observations.add_callback( - window.id(), + window, subscription_id, callback, ), @@ -1618,7 +1618,7 @@ impl AppContext { subscription_id, callback, } => self.window_bounds_observations.add_callback( - window.id(), + window, subscription_id, callback, ), @@ -1718,10 +1718,10 @@ impl AppContext { let root_view_id = cx.window.root_view().id(); if focused_view_id != root_view_id && !cx.views.contains_key(&(window, focused_view_id)) - && !focus_effects.contains_key(&window.id()) + && !focus_effects.contains_key(&window) { focus_effects.insert( - window.id(), + window, FocusEffect::View { window, view_id: Some(root_view_id), @@ -1860,12 +1860,12 @@ impl AppContext { cx.window.is_fullscreen = is_fullscreen; let mut fullscreen_observations = cx.window_fullscreen_observations.clone(); - fullscreen_observations.emit(window.id(), |callback| callback(is_fullscreen, cx)); + fullscreen_observations.emit(window, |callback| callback(is_fullscreen, cx)); if let Some(uuid) = cx.window_display_uuid() { let bounds = cx.window_bounds(); let mut bounds_observations = cx.window_bounds_observations.clone(); - bounds_observations.emit(window.id(), |callback| callback(bounds, uuid, cx)); + bounds_observations.emit(window, |callback| callback(bounds, uuid, cx)); } Some(()) @@ -1881,7 +1881,7 @@ impl AppContext { ) { self.update_window(window, |cx| { let mut observations = cx.keystroke_observations.clone(); - observations.emit(window.id(), move |callback| { + observations.emit(window, move |callback| { callback(&keystroke, &result, handled_by.as_ref(), cx) }); }); @@ -1895,7 +1895,7 @@ impl AppContext { cx.window.is_active = active; let mut observations = cx.window_activation_observations.clone(); - observations.emit(window.id(), |callback| callback(active, cx)); + observations.emit(window, |callback| callback(active, cx)); true }) .unwrap_or(false) @@ -1989,7 +1989,7 @@ impl AppContext { let bounds = cx.window_bounds(); cx.window_bounds_observations .clone() - .emit(window.id(), move |callback| { + .emit(window, move |callback| { callback(bounds, display, cx); true }); @@ -4038,12 +4038,12 @@ impl AnyWindowHandle { #[cfg(any(test, feature = "test-support"))] pub fn simulate_activation(&self, cx: &mut TestAppContext) { self.update(cx, |cx| { - let other_window_ids = cx + let other_windows = cx .windows() .filter(|window| *window != *self) .collect::>(); - for window in other_window_ids { + for window in other_windows { cx.window_changed_active_status(window, false) } @@ -4243,10 +4243,6 @@ impl AnyViewHandle { self.window } - pub fn window_id(&self) -> usize { - self.window.id() - } - pub fn id(&self) -> usize { self.view_id } @@ -4660,10 +4656,16 @@ pub enum Subscription { GlobalSubscription(callback_collection::Subscription), GlobalObservation(callback_collection::Subscription), FocusObservation(callback_collection::Subscription), - WindowActivationObservation(callback_collection::Subscription), - WindowFullscreenObservation(callback_collection::Subscription), - WindowBoundsObservation(callback_collection::Subscription), - KeystrokeObservation(callback_collection::Subscription), + WindowActivationObservation( + callback_collection::Subscription, + ), + WindowFullscreenObservation( + callback_collection::Subscription, + ), + WindowBoundsObservation( + callback_collection::Subscription, + ), + KeystrokeObservation(callback_collection::Subscription), ReleaseObservation(callback_collection::Subscription), ActionObservation(callback_collection::Subscription<(), ActionObservationCallback>), ActiveLabeledTasksObservation( diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index d39219e65d4..1012d7e77ec 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -326,7 +326,7 @@ impl<'a> WindowContext<'a> { }); Subscription::WindowActivationObservation( self.window_activation_observations - .subscribe(handle.id(), subscription_id), + .subscribe(handle, subscription_id), ) } @@ -344,7 +344,7 @@ impl<'a> WindowContext<'a> { }); Subscription::WindowActivationObservation( self.window_activation_observations - .subscribe(window.id(), subscription_id), + .subscribe(window, subscription_id), ) } @@ -362,7 +362,7 @@ impl<'a> WindowContext<'a> { }); Subscription::WindowBoundsObservation( self.window_bounds_observations - .subscribe(window.id(), subscription_id), + .subscribe(window, subscription_id), ) } @@ -374,10 +374,10 @@ impl<'a> WindowContext<'a> { let window = self.window_handle; let subscription_id = post_inc(&mut self.next_subscription_id); self.keystroke_observations - .add_callback(window.id(), subscription_id, Box::new(callback)); + .add_callback(window, subscription_id, Box::new(callback)); Subscription::KeystrokeObservation( self.keystroke_observations - .subscribe(window.id(), subscription_id), + .subscribe(window, subscription_id), ) } diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 59324f473a9..2a722cad829 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -882,7 +882,7 @@ impl platform::Window for Window { fn is_topmost_for_position(&self, position: Vector2F) -> bool { let self_borrow = self.0.borrow(); - let self_id = self_borrow.handle; + let self_handle = self_borrow.handle; unsafe { let app = NSApplication::sharedApplication(nil); @@ -899,8 +899,8 @@ impl platform::Window for Window { let is_panel: BOOL = msg_send![top_most_window, isKindOfClass: PANEL_CLASS]; let is_window: BOOL = msg_send![top_most_window, isKindOfClass: WINDOW_CLASS]; if is_panel == YES || is_window == YES { - let topmost_window_id = get_window_state(&*top_most_window).borrow().handle; - topmost_window_id == self_id + let topmost_window = get_window_state(&*top_most_window).borrow().handle; + topmost_window == self_handle } else { // Someone else's window is on top false diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs index 308ea6f2d7b..a16c3cb21e7 100644 --- a/crates/theme/src/ui.rs +++ b/crates/theme/src/ui.rs @@ -192,7 +192,6 @@ where F: FnOnce(&mut gpui::ViewContext) -> D, { const TITLEBAR_HEIGHT: f32 = 28.; - // let active = cx.window_is_active(cx.window_id()); Flex::column() .with_child( From fc96676662f1dc9a06d08cdcf19993fc40c6c08e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 17:20:46 -0600 Subject: [PATCH 076/105] Use AppContext::update when updating windows so we handle effects --- crates/gpui/src/app.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 8020147844d..8682e6c9dd6 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2112,13 +2112,15 @@ impl BorrowWindowContext for AppContext { where F: FnOnce(&mut WindowContext) -> T, { - let mut window = self.windows.remove(&handle)?; - let mut window_context = WindowContext::mutable(self, &mut window, handle); - let result = f(&mut window_context); - if !window_context.removed { - self.windows.insert(handle, window); - } - Some(result) + self.update(|cx| { + let mut window = cx.windows.remove(&handle)?; + let mut window_context = WindowContext::mutable(cx, &mut window, handle); + let result = f(&mut window_context); + if !window_context.removed { + cx.windows.insert(handle, window); + } + Some(result) + }) } fn update_window_optional(&mut self, handle: AnyWindowHandle, f: F) -> Option From 0dc70e6cbf07e70c00c99aeab775410626c6a05a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 8 Aug 2023 17:21:06 -0600 Subject: [PATCH 077/105] Rename mac platform Window to MacWindow for clarity --- crates/gpui/src/platform/mac.rs | 2 +- crates/gpui/src/platform/mac/platform.rs | 6 +++--- crates/gpui/src/platform/mac/window.rs | 8 ++++---- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/platform/mac.rs b/crates/gpui/src/platform/mac.rs index 342c1c66d0c..92ab53f15e3 100644 --- a/crates/gpui/src/platform/mac.rs +++ b/crates/gpui/src/platform/mac.rs @@ -21,7 +21,7 @@ pub use fonts::FontSystem; use platform::{MacForegroundPlatform, MacPlatform}; pub use renderer::Surface; use std::{ops::Range, rc::Rc, sync::Arc}; -use window::Window; +use window::MacWindow; use crate::executor; diff --git a/crates/gpui/src/platform/mac/platform.rs b/crates/gpui/src/platform/mac/platform.rs index 277ff8403f8..9a799c3a3a1 100644 --- a/crates/gpui/src/platform/mac/platform.rs +++ b/crates/gpui/src/platform/mac/platform.rs @@ -1,6 +1,6 @@ use super::{ event::key_to_native, screen::Screen, status_item::StatusItem, BoolExt as _, Dispatcher, - FontSystem, Window, + FontSystem, MacWindow, }; use crate::{ executor, @@ -594,11 +594,11 @@ impl platform::Platform for MacPlatform { options: platform::WindowOptions, executor: Rc, ) -> Box { - Box::new(Window::open(handle, options, executor, self.fonts())) + Box::new(MacWindow::open(handle, options, executor, self.fonts())) } fn main_window(&self) -> Option { - Window::main_window() + MacWindow::main_window() } fn add_status_item(&self, _handle: AnyWindowHandle) -> Box { diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 2a722cad829..022346ea67b 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -423,9 +423,9 @@ impl WindowState { } } -pub struct Window(Rc>); +pub struct MacWindow(Rc>); -impl Window { +impl MacWindow { pub fn open( handle: AnyWindowHandle, options: platform::WindowOptions, @@ -636,7 +636,7 @@ impl Window { } } -impl Drop for Window { +impl Drop for MacWindow { fn drop(&mut self) { let this = self.0.borrow(); let window = this.native_window; @@ -650,7 +650,7 @@ impl Drop for Window { } } -impl platform::Window for Window { +impl platform::Window for MacWindow { fn bounds(&self) -> WindowBounds { self.0.as_ref().borrow().bounds() } From e3bb5e5103ca7dcbdda1b327414c4b50cd82291f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Aug 2023 17:39:05 -0700 Subject: [PATCH 078/105] Fix failure to remove hovered region_ids on element removal Co-authored-by: Mikayla --- crates/gpui/src/app/window.rs | 13 +++++++++---- crates/gpui/src/scene/mouse_region.rs | 2 +- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index 789341d46fa..889dd2510eb 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -51,8 +51,8 @@ pub struct Window { cursor_regions: Vec, mouse_regions: Vec<(MouseRegion, usize)>, last_mouse_moved_event: Option, - pub(crate) hovered_region_ids: HashSet, - pub(crate) clicked_region_ids: HashSet, + pub(crate) hovered_region_ids: Vec, + pub(crate) clicked_region_ids: Vec, pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>, mouse_position: Vector2F, text_layout_cache: TextLayoutCache, @@ -664,6 +664,7 @@ impl<'a> WindowContext<'a> { let mut highest_z_index = None; let mouse_position = self.window.mouse_position.clone(); let window = &mut *self.window; + let prev_hovered_regions = mem::take(&mut window.hovered_region_ids); for (region, z_index) in window.mouse_regions.iter().rev() { // Allow mouse regions to appear transparent to hovers if !region.hoverable { @@ -682,7 +683,11 @@ impl<'a> WindowContext<'a> { // highest_z_index is set. if contains_mouse && z_index == highest_z_index.unwrap() { //Ensure that hover entrance events aren't sent twice - if window.hovered_region_ids.insert(region.id()) { + if let Err(ix) = window.hovered_region_ids.binary_search(®ion.id()) { + window.hovered_region_ids.insert(ix, region.id()); + } + // window.hovered_region_ids.insert(region.id()); + if !prev_hovered_regions.contains(®ion.id()) { valid_regions.push(region.clone()); if region.notify_on_hover { notified_views.insert(region.id().view_id()); @@ -690,7 +695,7 @@ impl<'a> WindowContext<'a> { } } else { // Ensure that hover exit events aren't sent twice - if window.hovered_region_ids.remove(®ion.id()) { + if prev_hovered_regions.contains(®ion.id()) { valid_regions.push(region.clone()); if region.notify_on_hover { notified_views.insert(region.id().view_id()); diff --git a/crates/gpui/src/scene/mouse_region.rs b/crates/gpui/src/scene/mouse_region.rs index ca2cc04b9d1..cf39ac782fc 100644 --- a/crates/gpui/src/scene/mouse_region.rs +++ b/crates/gpui/src/scene/mouse_region.rs @@ -177,7 +177,7 @@ impl MouseRegion { } } -#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)] +#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, PartialOrd, Ord)] pub struct MouseRegionId { view_id: usize, tag: TypeId, From 0b93a30821fa6103da0ce8db6b8ebb40e76696ec Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 8 Aug 2023 17:39:45 -0700 Subject: [PATCH 079/105] Terminate synthetic drag state on mouse up w/ ctrl held Co-authored-by: Mikayla --- crates/gpui/src/platform/mac/window.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index c0f0ade7b92..83469b82954 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -1086,7 +1086,10 @@ extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { button: MouseButton::Left, modifiers: Modifiers { ctrl: true, .. }, .. - }) => return, + }) => { + window_state_borrow.synthetic_drag_counter += 1; + return; + } _ => None, }; From c523ccc4c7cd5739e465a194a4b621840a1cd021 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 8 Aug 2023 18:42:38 -0400 Subject: [PATCH 080/105] Fix code that identifies language via extension --- crates/language/src/language.rs | 4 +-- crates/util/src/paths.rs | 37 +++++++++++++++++++++-- crates/zed/src/languages/bash/config.toml | 2 +- 3 files changed, 38 insertions(+), 5 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 100ab275717..223f5679aed 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -45,7 +45,7 @@ use syntax_map::SyntaxSnapshot; use theme::{SyntaxTheme, Theme}; use tree_sitter::{self, Query}; use unicase::UniCase; -use util::http::HttpClient; +use util::{http::HttpClient, paths::PathExt}; use util::{merge_json_value_into, post_inc, ResultExt, TryFutureExt as _, UnwrapFuture}; #[cfg(any(test, feature = "test-support"))] @@ -777,7 +777,7 @@ impl LanguageRegistry { ) -> UnwrapFuture>>> { let path = path.as_ref(); let filename = path.file_name().and_then(|name| name.to_str()); - let extension = path.extension().and_then(|name| name.to_str()); + let extension = path.extension_or_hidden_file_name(); let path_suffixes = [extension, filename]; self.get_or_load_language(|config| { let path_matches = config diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index f231669197f..e7e6e0ac727 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -33,6 +33,7 @@ pub mod legacy { pub trait PathExt { fn compact(&self) -> PathBuf; fn icon_suffix(&self) -> Option<&str>; + fn extension_or_hidden_file_name(&self) -> Option<&str>; } impl> PathExt for T { @@ -60,6 +61,7 @@ impl> PathExt for T { } } + /// Returns a suffix of the path that is used to determine which file icon to use fn icon_suffix(&self) -> Option<&str> { let file_name = self.as_ref().file_name()?.to_str()?; @@ -69,8 +71,16 @@ impl> PathExt for T { self.as_ref() .extension() - .map(|extension| extension.to_str()) - .flatten() + .and_then(|extension| extension.to_str()) + } + + /// Returns a file's extension or, if the file is hidden, its name without the leading dot + fn extension_or_hidden_file_name(&self) -> Option<&str> { + if let Some(extension) = self.as_ref().extension() { + return extension.to_str(); + } + + self.as_ref().file_name()?.to_str()?.split('.').last() } } @@ -315,4 +325,27 @@ mod tests { let path = Path::new("/a/b/c/.eslintrc.js"); assert_eq!(path.icon_suffix(), Some("eslintrc.js")); } + + #[test] + fn test_extension_or_hidden_file_name() { + // No dots in name + let path = Path::new("/a/b/c/file_name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Single dot in name + let path = Path::new("/a/b/c/file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Multiple dots in name + let path = Path::new("/a/b/c/long.file.name.rs"); + assert_eq!(path.extension_or_hidden_file_name(), Some("rs")); + + // Hidden file, no extension + let path = Path::new("/a/b/c/.gitignore"); + assert_eq!(path.extension_or_hidden_file_name(), Some("gitignore")); + + // Hidden file, with extension + let path = Path::new("/a/b/c/.eslintrc.js"); + assert_eq!(path.extension_or_hidden_file_name(), Some("js")); + } } diff --git a/crates/zed/src/languages/bash/config.toml b/crates/zed/src/languages/bash/config.toml index d03897a071d..8c4513b2509 100644 --- a/crates/zed/src/languages/bash/config.toml +++ b/crates/zed/src/languages/bash/config.toml @@ -1,5 +1,5 @@ name = "Shell Script" -path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin"] +path_suffixes = ["sh", "bash", "bashrc", "bash_profile", "bash_aliases", "bash_logout", "profile", "zsh", "zshrc", "zshenv", "zsh_profile", "zsh_aliases", "zsh_histfile", "zlogin", "zprofile"] line_comment = "# " first_line_pattern = "^#!.*\\b(?:ba|z)?sh\\b" brackets = [ From b23f1c809a4438f463d431de5d92dad7a53d224a Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 9 Aug 2023 11:01:20 -0400 Subject: [PATCH 081/105] WIP add IBM Plex Sans (base) natebutler@Nate16 zed % cargo run Compiling zed v0.99.0 (/Users/natebutler/Code/zed/zed/crates/zed) Finished dev [unoptimized + debuginfo] target(s) in 9.15s Running `target/debug/Zed` Thread "main" panicked with "called `Result::unwrap()` on an `Err` value: parse error" at crates/zed/src/main.rs:667:10 0: backtrace::backtrace::libunwind::trace at /Users/natebutler/.cargo/registry/src/index.crates.io-6f17d22bba15001f/backtrace-0.3.68/src/backtrace/libunwind.rs:93:5 backtrace::backtrace::trace_unsynchronized at /Users/natebutler/.cargo/registry/src/index.crates.io-6f17d22bba15001f/backtrace-0.3.68/src/backtrace/mod.rs:66:5 1: backtrace::backtrace::trace at /Users/natebutler/.cargo/registry/src/index.crates.io-6f17d22bba15001f/backtrace-0.3.68/src/backtrace/mod.rs:53:14 2: backtrace::capture::Backtrace::create at /Users/natebutler/.cargo/registry/src/index.crates.io-6f17d22bba15001f/backtrace-0.3.68/src/capture.rs:176:9 3: backtrace::capture::Backtrace::new at /Users/natebutler/.cargo/registry/src/index.crates.io-6f17d22bba15001f/backtrace-0.3.68/src/capture.rs:140:22 4: Zed::init_panic_hook::{{closure}} at crates/zed/src/main.rs:436:29 5: std::panicking::rust_panic_with_hook 6: std::panicking::begin_panic_handler::{{closure}} 7: std::sys_common::backtrace::__rust_end_short_backtrace 8: _rust_begin_unwind 9: core::panicking::panic_fmt 10: core::result::unwrap_failed 11: core::result::Result::unwrap at /private/tmp/rust-20230613-7622-103lepv/rustc-1.70.0-src/library/core/src/result.rs:1089:23 12: Zed::load_embedded_fonts at crates/zed/src/main.rs:664:5 13: Zed::main at crates/zed/src/main.rs:80:5 14: core::ops::function::FnOnce::call_once at /private/tmp/rust-20230613-7622-103lepv/rustc-1.70.0-src/library/core/src/ops/function.rs:250:5 15: std::sys_common::backtrace::__rust_begin_short_backtrace at /private/tmp/rust-20230613-7622-103lepv/rustc-1.70.0-src/library/std/src/sys_common/backtrace.rs:134:18 16: std::rt::lang_start::{{closure}} at /private/tmp/rust-20230613-7622-103lepv/rustc-1.70.0-src/library/std/src/rt.rs:166:18 17: std::panicking::try 18: std::rt::lang_start_internal 19: std::rt::lang_start at /private/tmp/rust-20230613-7622-103lepv/rustc-1.70.0-src/library/std/src/rt.rs:165:17 20: _mai --- assets/fonts/plex/IBMPlexSans-Bold.ttf | Bin 0 -> 193120 bytes assets/fonts/plex/IBMPlexSans-Italic.ttf | Bin 0 -> 202128 bytes assets/fonts/plex/IBMPlexSans-Regular.ttf | Bin 0 -> 192980 bytes assets/fonts/plex/LICENSE.txt | 93 ++++++++++++++++++++++ crates/zed/src/zed.rs | 5 ++ styles/src/common.ts | 1 + 6 files changed, 99 insertions(+) create mode 100644 assets/fonts/plex/IBMPlexSans-Bold.ttf create mode 100644 assets/fonts/plex/IBMPlexSans-Italic.ttf create mode 100644 assets/fonts/plex/IBMPlexSans-Regular.ttf create mode 100644 assets/fonts/plex/LICENSE.txt diff --git a/assets/fonts/plex/IBMPlexSans-Bold.ttf b/assets/fonts/plex/IBMPlexSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..72b190a849c9c275b44de7466c5bbd101e4e0403 GIT binary patch literal 193120 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 literal 0 HcmV?d00001 diff --git a/assets/fonts/plex/IBMPlexSans-Italic.ttf b/assets/fonts/plex/IBMPlexSans-Italic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..a997a20562517421ed0b19885bca198a86f9597c GIT binary patch literal 202128 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

9TLSh;J=v5J5YtI_%z-TEL}?!jkpVnj95 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 Q*dYhwGq(Ls8MpKQ0qZt9BLDyZ literal 0 HcmV?d00001 diff --git a/assets/fonts/plex/LICENSE.txt b/assets/fonts/plex/LICENSE.txt new file mode 100644 index 00000000000..c35c4c618fa --- /dev/null +++ b/assets/fonts/plex/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 8ad8080375e..099d837ba4c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -2327,6 +2327,11 @@ mod tests { .unwrap() .to_vec() .into(), + Assets + .load("fonts/plex/IBMPlexSans-Regular.ttf") + .unwrap() + .to_vec() + .into(), ]) .unwrap(); let themes = ThemeRegistry::new(Assets, cx.font_cache().clone()); diff --git a/styles/src/common.ts b/styles/src/common.ts index 054b2837914..f6437d90078 100644 --- a/styles/src/common.ts +++ b/styles/src/common.ts @@ -3,6 +3,7 @@ export * from "./theme" export { chroma } export const font_families = { + ui_sans: "IBM Plex Sans", sans: "Zed Sans", mono: "Zed Mono", } From 183c292a5ca15e869dfdc2f8f3427ad9c2c47c54 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 9 Aug 2023 11:11:57 -0400 Subject: [PATCH 082/105] Remove license causing unwrap error Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- assets/fonts/plex/LICENSE.txt | 93 ----------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 assets/fonts/plex/LICENSE.txt diff --git a/assets/fonts/plex/LICENSE.txt b/assets/fonts/plex/LICENSE.txt deleted file mode 100644 index c35c4c618fa..00000000000 --- a/assets/fonts/plex/LICENSE.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. From af388e7f9cc15d864d7f3c5f6babd9b3fd08d2ff Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 9 Aug 2023 11:38:02 -0400 Subject: [PATCH 083/105] Only load TTF fonts for now, additional font types will need to be manually added Co-Authored-By: Julia <30666851+ForLoveOfCats@users.noreply.github.com> --- crates/zed/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 795c83aba65..50630fb02ad 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -654,6 +654,10 @@ fn load_embedded_fonts(app: &App) { let embedded_fonts = Mutex::new(Vec::new()); smol::block_on(app.background().scoped(|scope| { for font_path in &font_paths { + if !font_path.ends_with(".ttf") { + continue; + } + scope.spawn(async { let font_path = &*font_path; let font_bytes = Assets.load(font_path).unwrap().to_vec(); From 85af025d8291a689b9dbc1809b0700857bfdb39f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Wed, 9 Aug 2023 11:39:15 -0400 Subject: [PATCH 084/105] Add IBM Plex license --- assets/fonts/plex/LICENSE.txt | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 assets/fonts/plex/LICENSE.txt diff --git a/assets/fonts/plex/LICENSE.txt b/assets/fonts/plex/LICENSE.txt new file mode 100644 index 00000000000..c35c4c618fa --- /dev/null +++ b/assets/fonts/plex/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. From 230b8948710c15d83e01081229660f36a14b74e9 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 9 Aug 2023 12:30:39 -0400 Subject: [PATCH 085/105] v0.100.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 467798ebf52..a0be9756bfd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9860,7 +9860,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.99.0" +version = "0.100.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 95d6445d176..1a2575dd5f6 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.99.0" +version = "0.100.0" publish = false [lib] From 704ab33f7266e7190c5ccc4b19ea16340bffbca8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 9 Aug 2023 23:37:01 +0300 Subject: [PATCH 086/105] Restore shutdown behavior --- crates/workspace/src/workspace.rs | 4 ++-- crates/zed/src/zed.rs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 2f884b0ceb3..1612aadc2f5 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4067,10 +4067,10 @@ pub fn restart(_: &Restart, cx: &mut AppContext) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { - if let Some(close) = window.update_root(&mut cx, |workspace, cx| { + if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { workspace.prepare_to_close(true, cx) }) { - if !close.await? { + if !should_close.await? { return Ok(()); } } diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 099d837ba4c..f27a6f075d2 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -433,10 +433,10 @@ fn quit(_: &Quit, cx: &mut gpui::AppContext) { // If the user cancels any save prompt, then keep the app open. for window in workspace_windows { - if let Some(close) = window.update_root(&mut cx, |workspace, cx| { - workspace.prepare_to_close(false, cx) + if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) }) { - if close.await? { + if !should_close.await? { return Ok(()); } } From ffffbbea1f3a6f3679acbdff2106db4d900b8518 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:29:55 +0200 Subject: [PATCH 087/105] chore: use Cow instead of String for tooltips (#2838) A QoL change to align `Tooltip` with other elements like `Label` Release Notes: - N/A --- crates/ai/src/assistant.rs | 22 +++++-------------- crates/collab_ui/src/collab_titlebar_item.rs | 14 ++++++------ crates/collab_ui/src/contact_list.rs | 2 +- crates/copilot_button/src/copilot_button.rs | 2 +- crates/diagnostics/src/items.rs | 2 +- crates/editor/src/editor.rs | 2 +- crates/feedback/src/deploy_feedback_button.rs | 2 +- crates/feedback/src/submit_feedback_button.rs | 2 +- crates/gpui/src/elements.rs | 4 ++-- crates/gpui/src/elements/tooltip.rs | 9 +++++--- crates/terminal_view/src/terminal_panel.rs | 5 +---- crates/workspace/src/pane.rs | 6 ++--- crates/workspace/src/toolbar.rs | 4 ++-- 13 files changed, 33 insertions(+), 43 deletions(-) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 957c5e1c063..0a18266d2af 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -362,7 +362,7 @@ impl AssistantPanel { this.set_active_editor_index(this.prev_active_editor_index, cx); } }) - .with_tooltip::(1, "History".into(), None, tooltip_style, cx) + .with_tooltip::(1, "History", None, tooltip_style, cx) } fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec> { @@ -394,7 +394,7 @@ impl AssistantPanel { }) .with_tooltip::( 1, - "Split Message".into(), + "Split Message", Some(Box::new(Split)), tooltip_style, cx, @@ -416,13 +416,7 @@ impl AssistantPanel { active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); } }) - .with_tooltip::( - 1, - "Assist".into(), - Some(Box::new(Assist)), - tooltip_style, - cx, - ) + .with_tooltip::(1, "Assist", Some(Box::new(Assist)), tooltip_style, cx) } fn render_quote_button(cx: &mut ViewContext) -> impl Element { @@ -446,7 +440,7 @@ impl AssistantPanel { }) .with_tooltip::( 1, - "Quote Selection".into(), + "Quote Selection", Some(Box::new(QuoteSelection)), tooltip_style, cx, @@ -468,7 +462,7 @@ impl AssistantPanel { }) .with_tooltip::( 1, - "New Conversation".into(), + "New Conversation", Some(Box::new(NewConversation)), tooltip_style, cx, @@ -498,11 +492,7 @@ impl AssistantPanel { }) .with_tooltip::( 0, - if self.zoomed { - "Zoom Out".into() - } else { - "Zoom In".into() - }, + if self.zoomed { "Zoom Out" } else { "Zoom In" }, Some(Box::new(ToggleZoom)), tooltip_style, cx, diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index dbcbaf60724..fefb1c608f2 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -238,7 +238,7 @@ impl CollabTitlebarItem { .left() .with_tooltip::( 0, - "Recent projects".into(), + "Recent projects", Some(Box::new(recent_projects::OpenRecent)), theme.tooltip.clone(), cx, @@ -282,7 +282,7 @@ impl CollabTitlebarItem { .left() .with_tooltip::( 0, - "Recent branches".into(), + "Recent branches", Some(Box::new(ToggleVcsMenu)), theme.tooltip.clone(), cx, @@ -582,7 +582,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( 0, - "Show contacts menu".into(), + "Show contacts menu", Some(Box::new(ToggleContactsMenu)), theme.tooltip.clone(), cx, @@ -633,7 +633,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( 0, - tooltip.into(), + tooltip, Some(Box::new(ToggleScreenSharing)), theme.tooltip.clone(), cx, @@ -686,7 +686,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( 0, - tooltip.into(), + tooltip, Some(Box::new(ToggleMute)), theme.tooltip.clone(), cx, @@ -734,7 +734,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( 0, - tooltip.into(), + tooltip, Some(Box::new(ToggleDeafen)), theme.tooltip.clone(), cx, @@ -768,7 +768,7 @@ impl CollabTitlebarItem { }) .with_tooltip::( 0, - tooltip.into(), + tooltip, Some(Box::new(LeaveCall)), theme.tooltip.clone(), cx, diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 49784523845..24ecec08e70 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -1345,7 +1345,7 @@ impl View for ContactList { }) .with_tooltip::( 0, - "Search for new contact".into(), + "Search for new contact", None, theme.tooltip.clone(), cx, diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index 5576451b1b0..eae1746a01a 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -140,7 +140,7 @@ impl View for CopilotButton { }) .with_tooltip::( 0, - "GitHub Copilot".into(), + "GitHub Copilot", None, theme.tooltip.clone(), cx, diff --git a/crates/diagnostics/src/items.rs b/crates/diagnostics/src/items.rs index c106f042b5c..0ae55e99d95 100644 --- a/crates/diagnostics/src/items.rs +++ b/crates/diagnostics/src/items.rs @@ -173,7 +173,7 @@ impl View for DiagnosticIndicator { }) .with_tooltip::

( 0, - "Project Diagnostics".to_string(), + "Project Diagnostics", Some(Box::new(crate::Deploy)), tooltip_style, cx, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 02cd58524bb..8d8b77ea950 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -8685,7 +8685,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend // We really need to rethink this ID system... .with_tooltip::( cx.block_id, - "Copy diagnostic message".to_string(), + "Copy diagnostic message", None, tooltip_style, cx, diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index beb5284031f..d197f57fa52 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -66,7 +66,7 @@ impl View for DeployFeedbackButton { }) .with_tooltip::( 0, - "Send Feedback".into(), + "Send Feedback", Some(Box::new(GiveFeedback)), theme.tooltip.clone(), cx, diff --git a/crates/feedback/src/submit_feedback_button.rs b/crates/feedback/src/submit_feedback_button.rs index 03a2cd51edd..2133296e254 100644 --- a/crates/feedback/src/submit_feedback_button.rs +++ b/crates/feedback/src/submit_feedback_button.rs @@ -80,7 +80,7 @@ impl View for SubmitFeedbackButton { .with_margin_left(theme.feedback.button_margin) .with_tooltip::( 0, - "cmd-s".into(), + "cmd-s", Some(Box::new(SubmitFeedback)), theme.tooltip.clone(), cx, diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 5bed935319f..bc903fe74ce 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -170,7 +170,7 @@ pub trait Element: 'static { fn with_tooltip( self, id: usize, - text: String, + text: impl Into>, action: Option>, style: TooltipStyle, cx: &mut ViewContext, @@ -178,7 +178,7 @@ pub trait Element: 'static { where Self: 'static + Sized, { - Tooltip::new::(id, text, action, style, self.into_any(), cx) + Tooltip::new::(id, text, action, style, self.into_any(), cx) } fn resizable( diff --git a/crates/gpui/src/elements/tooltip.rs b/crates/gpui/src/elements/tooltip.rs index 0510baa9e45..14f3809e67b 100644 --- a/crates/gpui/src/elements/tooltip.rs +++ b/crates/gpui/src/elements/tooltip.rs @@ -12,6 +12,7 @@ use crate::{ use schemars::JsonSchema; use serde::Deserialize; use std::{ + borrow::Cow, cell::{Cell, RefCell}, ops::Range, rc::Rc, @@ -52,9 +53,9 @@ pub struct KeystrokeStyle { } impl Tooltip { - pub fn new( + pub fn new( id: usize, - text: String, + text: impl Into>, action: Option>, style: TooltipStyle, child: AnyElement, @@ -66,6 +67,8 @@ impl Tooltip { let state_handle = cx.default_element_state::, Rc>(id); let state = state_handle.read(cx).clone(); + let text = text.into(); + let tooltip = if state.visible.get() { let mut collapsed_tooltip = Self::render_tooltip( focused_view_id, @@ -127,7 +130,7 @@ impl Tooltip { pub fn render_tooltip( focused_view_id: Option, - text: String, + text: impl Into>, style: TooltipStyle, action: Option>, measure: bool, diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 6be8a547cd3..770cae907c1 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -72,10 +72,7 @@ impl TerminalPanel { 0, "icons/plus_12.svg", false, - Some(( - "New Terminal".into(), - Some(Box::new(workspace::NewTerminal)), - )), + Some(("New Terminal", Some(Box::new(workspace::NewTerminal)))), cx, move |_, cx| { let this = this.clone(); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 80644789643..1463ac21559 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -303,10 +303,10 @@ impl Pane { let tooltip_label; if pane.is_zoomed() { icon_path = "icons/minimize_8.svg"; - tooltip_label = "Zoom In".into(); + tooltip_label = "Zoom In"; } else { icon_path = "icons/maximize_8.svg"; - tooltip_label = "Zoom In".into(); + tooltip_label = "Zoom In"; } Pane::render_tab_bar_button( @@ -1477,7 +1477,7 @@ impl Pane { index: usize, icon: &'static str, is_active: bool, - tooltip: Option<(String, Option>)>, + tooltip: Option<(&'static str, Option>)>, cx: &mut ViewContext, on_click: F1, on_down: F2, diff --git a/crates/workspace/src/toolbar.rs b/crates/workspace/src/toolbar.rs index 69394b84213..945ac7b0f51 100644 --- a/crates/workspace/src/toolbar.rs +++ b/crates/workspace/src/toolbar.rs @@ -220,7 +220,7 @@ fn nav_button spacing: f32, on_click: F, tooltip_action: A, - action_name: &str, + action_name: &'static str, cx: &mut ViewContext, ) -> AnyElement { MouseEventHandler::::new(0, cx, |state, _| { @@ -252,7 +252,7 @@ fn nav_button }) .with_tooltip::( 0, - action_name.to_string(), + action_name, Some(Box::new(tooltip_action)), tooltip_style, cx, From 7970406694bb1e523fb582e8dbd00662887fcb33 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 11 Aug 2023 17:56:56 -0700 Subject: [PATCH 088/105] Add a compile test for the element derive --- Cargo.lock | 1 + crates/gpui/src/elements.rs | 4 ++++ crates/gpui_macros/Cargo.toml | 2 ++ crates/gpui_macros/src/gpui_macros.rs | 8 ++++++-- crates/gpui_macros/tests/test.rs | 14 ++++++++++++++ 5 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 crates/gpui_macros/tests/test.rs diff --git a/Cargo.lock b/Cargo.lock index a0be9756bfd..1ff9981a6a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3172,6 +3172,7 @@ dependencies = [ name = "gpui_macros" version = "0.1.0" dependencies = [ + "gpui", "proc-macro2", "quote", "syn 1.0.109", diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index bc903fe74ce..56a712802b6 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -201,6 +201,10 @@ pub trait Element: 'static { } } +pub trait RenderElement { + fn render(&mut self, view: &mut V, cx: &mut ViewContext) -> AnyElement; +} + trait AnyElementState { fn layout( &mut self, diff --git a/crates/gpui_macros/Cargo.toml b/crates/gpui_macros/Cargo.toml index 76daeae2a84..9ff340299b6 100644 --- a/crates/gpui_macros/Cargo.toml +++ b/crates/gpui_macros/Cargo.toml @@ -14,3 +14,5 @@ syn = "1.0" quote = "1.0" proc-macro2 = "1.0" +[dev-dependencies] +gpui = { path = "../gpui" } diff --git a/crates/gpui_macros/src/gpui_macros.rs b/crates/gpui_macros/src/gpui_macros.rs index dbf57b83e5b..2e6c7258727 100644 --- a/crates/gpui_macros/src/gpui_macros.rs +++ b/crates/gpui_macros/src/gpui_macros.rs @@ -283,8 +283,12 @@ pub fn element_derive(input: TokenStream) -> TokenStream { // The name of the struct/enum let name = input.ident; + let must_implement = format_ident!("{}MustImplementRenderElement", name); let expanded = quote! { + trait #must_implement : gpui::elements::RenderElement {} + impl #must_implement for #name {} + impl gpui::elements::Element for #name { type LayoutState = gpui::elements::AnyElement; type PaintState = (); @@ -307,7 +311,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream { visible_bounds: gpui::geometry::rect::RectF, element: &mut gpui::elements::AnyElement, view: &mut V, - cx: &mut gpui::ViewContext, + cx: &mut gpui::PaintContext, ) { element.paint(scene, bounds.origin(), visible_bounds, view, cx); } @@ -332,7 +336,7 @@ pub fn element_derive(input: TokenStream) -> TokenStream { _: &(), view: &V, cx: &gpui::ViewContext, - ) -> serde_json::Value { + ) -> gpui::serde_json::Value { element.debug(view, cx) } } diff --git a/crates/gpui_macros/tests/test.rs b/crates/gpui_macros/tests/test.rs new file mode 100644 index 00000000000..b5a91a7e980 --- /dev/null +++ b/crates/gpui_macros/tests/test.rs @@ -0,0 +1,14 @@ +use gpui::{elements::RenderElement, View, ViewContext}; +use gpui_macros::Element; + +#[test] +fn test_derive_render_element() { + #[derive(Element)] + struct TestElement {} + + impl RenderElement for TestElement { + fn render(&mut self, _: &mut V, _: &mut ViewContext) -> gpui::AnyElement { + unimplemented!() + } + } +} From 84dc4090bd2c738637022171c530cbb45ee8af7c Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 12 Aug 2023 10:40:23 -0600 Subject: [PATCH 089/105] Wire up per corner radii for quad Still need to expose this in the styling layer and allow images to have per corner radii. --- crates/collab_ui/src/contact_list.rs | 8 +- crates/editor/src/element.rs | 26 ++-- crates/editor/src/hover_popover.rs | 2 +- crates/gpui/examples/quad.rs | 120 ++++++++++++++++++ crates/gpui/src/elements/container.rs | 8 +- crates/gpui/src/platform/mac/renderer.rs | 5 +- .../gpui/src/platform/mac/shaders/shaders.h | 5 +- .../src/platform/mac/shaders/shaders.metal | 36 +++++- crates/gpui/src/scene.rs | 21 ++- crates/terminal_view/src/terminal_element.rs | 4 +- crates/workspace/src/pane.rs | 2 +- .../src/pane/dragged_item_receiver.rs | 2 +- crates/workspace/src/workspace.rs | 2 +- 13 files changed, 205 insertions(+), 36 deletions(-) create mode 100644 crates/gpui/examples/quad.rs diff --git a/crates/collab_ui/src/contact_list.rs b/crates/collab_ui/src/contact_list.rs index 24ecec08e70..b8024e2bfd5 100644 --- a/crates/collab_ui/src/contact_list.rs +++ b/crates/collab_ui/src/contact_list.rs @@ -837,7 +837,7 @@ impl ContactList { ), background: Some(tree_branch.color), border: gpui::Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); scene.push_quad(gpui::Quad { bounds: RectF::from_points( @@ -846,7 +846,7 @@ impl ContactList { ), background: Some(tree_branch.color), border: gpui::Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); })) .constrained() @@ -934,7 +934,7 @@ impl ContactList { ), background: Some(tree_branch.color), border: gpui::Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); scene.push_quad(gpui::Quad { bounds: RectF::from_points( @@ -943,7 +943,7 @@ impl ContactList { ), background: Some(tree_branch.color), border: gpui::Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); })) .constrained() diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 0ea4ff758bd..24b4811b61d 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -488,13 +488,13 @@ impl EditorElement { bounds: gutter_bounds, background: Some(self.style.gutter_background), border: Border::new(0., Color::transparent_black()), - corner_radius: 0., + corner_radii: Default::default(), }); scene.push_quad(Quad { bounds: text_bounds, background: Some(self.style.background), border: Border::new(0., Color::transparent_black()), - corner_radius: 0., + corner_radii: Default::default(), }); if let EditorMode::Full = layout.mode { @@ -522,7 +522,7 @@ impl EditorElement { bounds: RectF::new(origin, size), background: Some(self.style.active_line_background), border: Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); } } @@ -542,7 +542,7 @@ impl EditorElement { bounds: RectF::new(origin, size), background: Some(self.style.highlighted_line_background), border: Border::default(), - corner_radius: 0., + corner_radii: Default::default(), }); } @@ -572,7 +572,7 @@ impl EditorElement { ), background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: 0., + corner_radii: Default::default(), }); } } @@ -673,7 +673,7 @@ impl EditorElement { bounds: highlight_bounds, background: Some(diff_style.modified), border: Border::new(0., Color::transparent_black()), - corner_radius: 1. * line_height, + corner_radii: (1. * line_height).into(), }); continue; @@ -706,7 +706,7 @@ impl EditorElement { bounds: highlight_bounds, background: Some(diff_style.deleted), border: Border::new(0., Color::transparent_black()), - corner_radius: 1. * line_height, + corner_radii: (1. * line_height).into(), }); continue; @@ -728,7 +728,7 @@ impl EditorElement { bounds: highlight_bounds, background: Some(color), border: Border::new(0., Color::transparent_black()), - corner_radius: diff_style.corner_radius * line_height, + corner_radii: (diff_style.corner_radius * line_height).into(), }); } } @@ -1129,7 +1129,7 @@ impl EditorElement { bounds, background: Some(color), border, - corner_radius: style.thumb.corner_radius, + corner_radii: style.thumb.corner_radius.into(), }) }; let background_ranges = editor @@ -1189,7 +1189,7 @@ impl EditorElement { bounds, background: Some(color), border, - corner_radius: style.thumb.corner_radius, + corner_radii: style.thumb.corner_radius.into(), }) } } @@ -1198,7 +1198,7 @@ impl EditorElement { bounds: thumb_bounds, border: style.thumb.border, background: style.thumb.background_color, - corner_radius: style.thumb.corner_radius, + corner_radii: style.thumb.corner_radius.into(), }); } @@ -2725,14 +2725,14 @@ impl Cursor { bounds, background: None, border: Border::all(1., self.color), - corner_radius: 0., + corner_radii: Default::default(), }); } else { scene.push_quad(Quad { bounds, background: Some(self.color), border: Default::default(), - corner_radius: 0., + corner_radii: Default::default(), }); } diff --git a/crates/editor/src/hover_popover.rs b/crates/editor/src/hover_popover.rs index 92ed9ef77d5..afc13f983d3 100644 --- a/crates/editor/src/hover_popover.rs +++ b/crates/editor/src/hover_popover.rs @@ -599,7 +599,7 @@ impl InfoPopover { bounds, background: Some(code_span_background_color), border: Default::default(), - corner_radius: 2.0, + corner_radii: (2.0).into(), }); } }, diff --git a/crates/gpui/examples/quad.rs b/crates/gpui/examples/quad.rs new file mode 100644 index 00000000000..5d796111c68 --- /dev/null +++ b/crates/gpui/examples/quad.rs @@ -0,0 +1,120 @@ +use gpui::{color::Color, geometry::rect::RectF, AnyElement, App, Element, Entity, Quad, View}; +use log::LevelFilter; +use pathfinder_geometry::vector::vec2f; +use simplelog::SimpleLogger; + +fn main() { + SimpleLogger::init(LevelFilter::Info, Default::default()).expect("could not initialize logger"); + + App::new(()).unwrap().run(|cx| { + cx.platform().activate(true); + cx.add_window(Default::default(), |_| QuadView); + }); +} + +struct QuadView; + +impl Entity for QuadView { + type Event = (); +} + +impl View for QuadView { + fn ui_name() -> &'static str { + "QuadView" + } + + fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { + QuadElement.into_any() + } +} + +struct QuadElement; + +impl gpui::Element for QuadElement { + type LayoutState = (); + + type PaintState = (); + + fn layout( + &mut self, + constraint: gpui::SizeConstraint, + _: &mut V, + _: &mut gpui::LayoutContext, + ) -> (gpui::geometry::vector::Vector2F, Self::LayoutState) { + (constraint.max, ()) + } + + fn paint( + &mut self, + scene: &mut gpui::SceneBuilder, + _: pathfinder_geometry::rect::RectF, + _: pathfinder_geometry::rect::RectF, + _: &mut Self::LayoutState, + _: &mut V, + _: &mut gpui::PaintContext, + ) -> Self::PaintState { + scene.push_quad(Quad { + bounds: RectF::new(vec2f(100., 100.), vec2f(100., 100.)), + background: Some(Color::red()), + border: Default::default(), + corner_radii: gpui::scene::CornerRadii { + top_left: 20., + ..Default::default() + }, + }); + + scene.push_quad(Quad { + bounds: RectF::new(vec2f(200., 100.), vec2f(100., 100.)), + background: Some(Color::green()), + border: Default::default(), + corner_radii: gpui::scene::CornerRadii { + top_right: 20., + ..Default::default() + }, + }); + + scene.push_quad(Quad { + bounds: RectF::new(vec2f(100., 200.), vec2f(100., 100.)), + background: Some(Color::blue()), + border: Default::default(), + corner_radii: gpui::scene::CornerRadii { + bottom_left: 20., + ..Default::default() + }, + }); + + scene.push_quad(Quad { + bounds: RectF::new(vec2f(200., 200.), vec2f(100., 100.)), + background: Some(Color::yellow()), + border: Default::default(), + corner_radii: gpui::scene::CornerRadii { + bottom_right: 20., + ..Default::default() + }, + }); + } + + fn rect_for_text_range( + &self, + _: std::ops::Range, + _: pathfinder_geometry::rect::RectF, + _: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &V, + _: &gpui::ViewContext, + ) -> Option { + unimplemented!() + } + + fn debug( + &self, + _: pathfinder_geometry::rect::RectF, + _: &Self::LayoutState, + _: &Self::PaintState, + _: &V, + _: &gpui::ViewContext, + ) -> serde_json::Value { + unimplemented!() + } +} diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 656847980c9..df6f35caaf6 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -248,7 +248,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.background_color, border: Default::default(), - corner_radius: self.style.corner_radius, + corner_radii: self.style.corner_radius.into(), }); self.child @@ -259,7 +259,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.overlay_color, border: self.style.border, - corner_radius: self.style.corner_radius, + corner_radii: self.style.corner_radius.into(), }); scene.pop_layer(); } else { @@ -267,7 +267,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.background_color, border: self.style.border, - corner_radius: self.style.corner_radius, + corner_radii: self.style.corner_radius.into(), }); let child_origin = child_origin @@ -284,7 +284,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.overlay_color, border: Default::default(), - corner_radius: 0., + corner_radii: self.style.corner_radius.into(), }); scene.pop_layer(); } diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 24c44bb6983..c743f00f924 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -586,7 +586,10 @@ impl Renderer { border_bottom: border_width * (quad.border.bottom as usize as f32), border_left: border_width * (quad.border.left as usize as f32), border_color: quad.border.color.to_uchar4(), - corner_radius: quad.corner_radius * scale_factor, + corner_radius_top_left: quad.corner_radii.top_left * scale_factor, + corner_radius_top_right: quad.corner_radii.top_right * scale_factor, + corner_radius_bottom_right: quad.corner_radii.bottom_right * scale_factor, + corner_radius_bottom_left: quad.corner_radii.bottom_left * scale_factor, }; unsafe { *(buffer_contents.add(ix)) = shader_quad; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index 6e0ed1a5f1b..9e8ec538879 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -19,7 +19,10 @@ typedef struct { float border_bottom; float border_left; vector_uchar4 border_color; - float corner_radius; + float corner_radius_top_left; + float corner_radius_top_right; + float corner_radius_bottom_right; + float corner_radius_bottom_left; } GPUIQuad; typedef enum { diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index 397e7647c47..ac7b9ac2083 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -43,7 +43,10 @@ struct QuadFragmentInput { float border_bottom; float border_left; float4 border_color; - float corner_radius; + float corner_radius_top_left; + float corner_radius_top_right; + float corner_radius_bottom_right; + float corner_radius_bottom_left; uchar grayscale; // only used in image shader }; @@ -51,12 +54,27 @@ float4 quad_sdf(QuadFragmentInput input) { float2 half_size = input.size / 2.; float2 center = input.origin + half_size; float2 center_to_point = input.position.xy - center; - float2 rounded_edge_to_point = abs(center_to_point) - half_size + input.corner_radius; - float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - input.corner_radius; + float corner_radius; + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + corner_radius = input.corner_radius_top_left; + } else { + corner_radius = input.corner_radius_bottom_left; + } + } else { + if (center_to_point.y < 0.) { + corner_radius = input.corner_radius_top_right; + } else { + corner_radius = input.corner_radius_bottom_right; + } + } + + float2 rounded_edge_to_point = abs(center_to_point) - half_size + corner_radius; + float distance = length(max(0., rounded_edge_to_point)) + min(0., max(rounded_edge_to_point.x, rounded_edge_to_point.y)) - corner_radius; float vertical_border = center_to_point.x <= 0. ? input.border_left : input.border_right; float horizontal_border = center_to_point.y <= 0. ? input.border_top : input.border_bottom; - float2 inset_size = half_size - input.corner_radius - float2(vertical_border, horizontal_border); + float2 inset_size = half_size - corner_radius - float2(vertical_border, horizontal_border); float2 point_to_inset_corner = abs(center_to_point) - inset_size; float border_width; if (point_to_inset_corner.x < 0. && point_to_inset_corner.y < 0.) { @@ -110,7 +128,10 @@ vertex QuadFragmentInput quad_vertex( quad.border_bottom, quad.border_left, coloru_to_colorf(quad.border_color), - quad.corner_radius, + quad.corner_radius_top_left, + quad.corner_radius_top_right, + quad.corner_radius_bottom_right, + quad.corner_radius_bottom_left, 0, }; } @@ -253,6 +274,9 @@ vertex QuadFragmentInput image_vertex( image.border_left, coloru_to_colorf(image.border_color), image.corner_radius, + image.corner_radius, + image.corner_radius, + image.corner_radius, image.grayscale, }; } @@ -266,7 +290,7 @@ fragment float4 image_fragment( if (input.grayscale) { float grayscale = 0.2126 * input.background_color.r + - 0.7152 * input.background_color.g + + 0.7152 * input.background_color.g + 0.0722 * input.background_color.b; input.background_color = float4(grayscale, grayscale, grayscale, input.background_color.a); } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 7793a28ee0d..ac45e0eb94a 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -65,7 +65,26 @@ pub struct Quad { pub bounds: RectF, pub background: Option, pub border: Border, - pub corner_radius: f32, + pub corner_radii: CornerRadii, +} + +#[derive(Default, Debug)] +pub struct CornerRadii { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, +} + +impl From for CornerRadii { + fn from(radius: f32) -> Self { + Self { + top_left: radius, + top_right: radius, + bottom_right: radius, + bottom_left: radius, + } + } } #[derive(Debug)] diff --git a/crates/terminal_view/src/terminal_element.rs b/crates/terminal_view/src/terminal_element.rs index bf7b6e6aac4..232d3c5535b 100644 --- a/crates/terminal_view/src/terminal_element.rs +++ b/crates/terminal_view/src/terminal_element.rs @@ -153,7 +153,7 @@ impl LayoutRect { bounds: RectF::new(position, size), background: Some(self.color), border: Default::default(), - corner_radius: 0., + corner_radii: Default::default(), }) } } @@ -763,7 +763,7 @@ impl Element for TerminalElement { bounds: RectF::new(bounds.origin(), bounds.size()), background: Some(layout.background_color), border: Default::default(), - corner_radius: 0., + corner_radii: Default::default(), }); for rect in &layout.rects { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 1463ac21559..06f13cd52d1 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -1397,7 +1397,7 @@ impl Pane { bounds: square, background: Some(color), border: Default::default(), - corner_radius: diameter / 2., + corner_radii: (diameter / 2.).into(), }); } }) diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 2d3fe8ea832..165537a1af4 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -61,7 +61,7 @@ where bounds: overlay_region, background: Some(overlay_color(cx)), border: Default::default(), - corner_radius: 0., + corner_radii: Default::default(), }); }); } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 1612aadc2f5..ab4f7286dcc 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -3614,7 +3614,7 @@ fn notify_of_new_dock(workspace: &WeakViewHandle, cx: &mut AsyncAppCo bounds, background: Some(code_span_background_color), border: Default::default(), - corner_radius: 2.0, + corner_radii: (2.0).into(), }) }) .into_any() From 40f478937edee69190771b0ceaeea993bdd10456 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 12 Aug 2023 10:50:04 -0600 Subject: [PATCH 090/105] Allow distinct corner radii for images --- crates/gpui/src/elements/image.rs | 2 +- crates/gpui/src/platform/mac/renderer.rs | 12 +++++++++--- crates/gpui/src/platform/mac/shaders/shaders.h | 5 ++++- crates/gpui/src/platform/mac/shaders/shaders.metal | 8 ++++---- crates/gpui/src/scene.rs | 5 +++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/elements/image.rs b/crates/gpui/src/elements/image.rs index e87c0659175..034494221d8 100644 --- a/crates/gpui/src/elements/image.rs +++ b/crates/gpui/src/elements/image.rs @@ -103,7 +103,7 @@ impl Element for Image { scene.push_image(scene::Image { bounds, border: self.style.border, - corner_radius: self.style.corner_radius, + corner_radii: self.style.corner_radius.into(), grayscale: self.style.grayscale, data: data.clone(), }); diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index c743f00f924..1cc3d33b2aa 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -741,7 +741,7 @@ impl Renderer { for image in images { let origin = image.bounds.origin() * scale_factor; let target_size = image.bounds.size() * scale_factor; - let corner_radius = image.corner_radius * scale_factor; + let corner_radii = image.corner_radii * scale_factor; let border_width = image.border.width * scale_factor; let (alloc_id, atlas_bounds) = self.image_cache.render(&image.data); images_by_atlas @@ -757,7 +757,10 @@ impl Renderer { border_bottom: border_width * (image.border.bottom as usize as f32), border_left: border_width * (image.border.left as usize as f32), border_color: image.border.color.to_uchar4(), - corner_radius, + corner_radius_top_left: corner_radii.top_left, + corner_radius_top_right: corner_radii.top_right, + corner_radius_bottom_right: corner_radii.bottom_right, + corner_radius_bottom_left: corner_radii.bottom_left, grayscale: image.grayscale as u8, }); } @@ -780,7 +783,10 @@ impl Renderer { border_bottom: 0., border_left: 0., border_color: Default::default(), - corner_radius: 0., + corner_radius_top_left: 0., + corner_radius_top_right: 0., + corner_radius_bottom_right: 0., + corner_radius_bottom_left: 0., grayscale: false as u8, }); } else { diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index 9e8ec538879..ac8389e1f61 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -92,7 +92,10 @@ typedef struct { float border_bottom; float border_left; vector_uchar4 border_color; - float corner_radius; + float corner_radius_top_left; + float corner_radius_top_right; + float corner_radius_bottom_right; + float corner_radius_bottom_left; uint8_t grayscale; } GPUIImage; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index ac7b9ac2083..cc38f3df7a5 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -273,10 +273,10 @@ vertex QuadFragmentInput image_vertex( image.border_bottom, image.border_left, coloru_to_colorf(image.border_color), - image.corner_radius, - image.corner_radius, - image.corner_radius, - image.corner_radius, + image.corner_radius_top_left, + image.corner_radius_top_right, + image.corner_radius_bottom_right, + image.corner_radius_bottom_left, image.grayscale, }; } diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ac45e0eb94a..abc763b73b7 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -3,6 +3,7 @@ mod mouse_region; #[cfg(debug_assertions)] use collections::HashSet; +use derive_more::Mul; use schemars::JsonSchema; use serde::Deserialize; use serde_json::json; @@ -68,7 +69,7 @@ pub struct Quad { pub corner_radii: CornerRadii, } -#[derive(Default, Debug)] +#[derive(Default, Debug, Mul, Clone, Copy)] pub struct CornerRadii { pub top_left: f32, pub top_right: f32, @@ -196,7 +197,7 @@ pub struct PathVertex { pub struct Image { pub bounds: RectF, pub border: Border, - pub corner_radius: f32, + pub corner_radii: CornerRadii, pub grayscale: bool, pub data: Arc, } From 65123e6eedb75d780808e4281c48a6810c07d22b Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 12 Aug 2023 10:58:04 -0600 Subject: [PATCH 091/105] Allow individual corner radii on drop shadows --- crates/editor/src/element.rs | 6 ++--- crates/gpui/src/elements/container.rs | 21 ++++++++------- crates/gpui/src/platform/mac/renderer.rs | 6 ++++- .../gpui/src/platform/mac/shaders/shaders.h | 5 +++- .../src/platform/mac/shaders/shaders.metal | 26 ++++++++++++++++--- crates/gpui/src/scene.rs | 5 ++-- 6 files changed, 50 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 24b4811b61d..13856cc8efa 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1129,7 +1129,7 @@ impl EditorElement { bounds, background: Some(color), border, - corner_radii: style.thumb.corner_radius.into(), + corner_radii: style.thumb.corner_radii.into(), }) }; let background_ranges = editor @@ -1189,7 +1189,7 @@ impl EditorElement { bounds, background: Some(color), border, - corner_radii: style.thumb.corner_radius.into(), + corner_radii: style.thumb.corner_radii.into(), }) } } @@ -1198,7 +1198,7 @@ impl EditorElement { bounds: thumb_bounds, border: style.thumb.border, background: style.thumb.background_color, - corner_radii: style.thumb.corner_radius.into(), + corner_radii: style.thumb.corner_radii.into(), }); } diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index df6f35caaf6..984742e818f 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -9,7 +9,7 @@ use crate::{ }, json::ToJson, platform::CursorStyle, - scene::{self, Border, CursorRegion, Quad}, + scene::{self, Border, CornerRadii, CursorRegion, Quad}, AnyElement, Element, LayoutContext, PaintContext, SceneBuilder, SizeConstraint, View, ViewContext, }; @@ -30,7 +30,7 @@ pub struct ContainerStyle { #[serde(default)] pub border: Border, #[serde(default)] - pub corner_radius: f32, + pub corner_radii: CornerRadii, #[serde(default)] pub shadow: Option, #[serde(default)] @@ -133,7 +133,10 @@ impl Container { } pub fn with_corner_radius(mut self, radius: f32) -> Self { - self.style.corner_radius = radius; + self.style.corner_radii.top_left = radius; + self.style.corner_radii.top_right = radius; + self.style.corner_radii.bottom_right = radius; + self.style.corner_radii.bottom_left = radius; self } @@ -225,7 +228,7 @@ impl Element for Container { if let Some(shadow) = self.style.shadow.as_ref() { scene.push_shadow(scene::Shadow { bounds: quad_bounds + shadow.offset, - corner_radius: self.style.corner_radius, + corner_radii: self.style.corner_radii, sigma: shadow.blur, color: shadow.color, }); @@ -248,7 +251,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.background_color, border: Default::default(), - corner_radii: self.style.corner_radius.into(), + corner_radii: self.style.corner_radii.into(), }); self.child @@ -259,7 +262,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.overlay_color, border: self.style.border, - corner_radii: self.style.corner_radius.into(), + corner_radii: self.style.corner_radii.into(), }); scene.pop_layer(); } else { @@ -267,7 +270,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.background_color, border: self.style.border, - corner_radii: self.style.corner_radius.into(), + corner_radii: self.style.corner_radii.into(), }); let child_origin = child_origin @@ -284,7 +287,7 @@ impl Element for Container { bounds: quad_bounds, background: self.style.overlay_color, border: Default::default(), - corner_radii: self.style.corner_radius.into(), + corner_radii: self.style.corner_radii.into(), }); scene.pop_layer(); } @@ -328,7 +331,7 @@ impl ToJson for ContainerStyle { "padding": self.padding.to_json(), "background_color": self.background_color.to_json(), "border": self.border.to_json(), - "corner_radius": self.corner_radius, + "corner_radius": self.corner_radii, "shadow": self.shadow.to_json(), }) } diff --git a/crates/gpui/src/platform/mac/renderer.rs b/crates/gpui/src/platform/mac/renderer.rs index 1cc3d33b2aa..5fe9e34ee48 100644 --- a/crates/gpui/src/platform/mac/renderer.rs +++ b/crates/gpui/src/platform/mac/renderer.rs @@ -509,10 +509,14 @@ impl Renderer { }; for (ix, shadow) in shadows.iter().enumerate() { let shape_bounds = shadow.bounds * scale_factor; + let corner_radii = shadow.corner_radii * scale_factor; let shader_shadow = shaders::GPUIShadow { origin: shape_bounds.origin().to_float2(), size: shape_bounds.size().to_float2(), - corner_radius: shadow.corner_radius * scale_factor, + corner_radius_top_left: corner_radii.top_left, + corner_radius_top_right: corner_radii.top_right, + corner_radius_bottom_right: corner_radii.bottom_right, + corner_radius_bottom_left: corner_radii.bottom_left, sigma: shadow.sigma, color: shadow.color.to_uchar4(), }; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.h b/crates/gpui/src/platform/mac/shaders/shaders.h index ac8389e1f61..b73e5f8e989 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.h +++ b/crates/gpui/src/platform/mac/shaders/shaders.h @@ -34,7 +34,10 @@ typedef enum { typedef struct { vector_float2 origin; vector_float2 size; - float corner_radius; + float corner_radius_top_left; + float corner_radius_top_right; + float corner_radius_bottom_right; + float corner_radius_bottom_left; float sigma; vector_uchar4 color; } GPUIShadow; diff --git a/crates/gpui/src/platform/mac/shaders/shaders.metal b/crates/gpui/src/platform/mac/shaders/shaders.metal index cc38f3df7a5..b803094cf65 100644 --- a/crates/gpui/src/platform/mac/shaders/shaders.metal +++ b/crates/gpui/src/platform/mac/shaders/shaders.metal @@ -146,7 +146,10 @@ struct ShadowFragmentInput { float4 position [[position]]; vector_float2 origin; vector_float2 size; - float corner_radius; + float corner_radius_top_left; + float corner_radius_top_right; + float corner_radius_bottom_right; + float corner_radius_bottom_left; float sigma; vector_uchar4 color; }; @@ -169,7 +172,10 @@ vertex ShadowFragmentInput shadow_vertex( device_position, shadow.origin, shadow.size, - shadow.corner_radius, + shadow.corner_radius_top_left, + shadow.corner_radius_top_right, + shadow.corner_radius_bottom_right, + shadow.corner_radius_bottom_left, shadow.sigma, shadow.color, }; @@ -179,10 +185,24 @@ fragment float4 shadow_fragment( ShadowFragmentInput input [[stage_in]] ) { float sigma = input.sigma; - float corner_radius = input.corner_radius; float2 half_size = input.size / 2.; float2 center = input.origin + half_size; float2 point = input.position.xy - center; + float2 center_to_point = input.position.xy - center; + float corner_radius; + if (center_to_point.x < 0.) { + if (center_to_point.y < 0.) { + corner_radius = input.corner_radius_top_left; + } else { + corner_radius = input.corner_radius_bottom_left; + } + } else { + if (center_to_point.y < 0.) { + corner_radius = input.corner_radius_top_right; + } else { + corner_radius = input.corner_radius_bottom_right; + } + } // The signal is only non-zero in a limited range, so don't waste samples float low = point.y - half_size.y; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index abc763b73b7..48649c57918 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -6,6 +6,7 @@ use collections::HashSet; use derive_more::Mul; use schemars::JsonSchema; use serde::Deserialize; +use serde_derive::Serialize; use serde_json::json; use std::{borrow::Cow, sync::Arc}; @@ -69,7 +70,7 @@ pub struct Quad { pub corner_radii: CornerRadii, } -#[derive(Default, Debug, Mul, Clone, Copy)] +#[derive(Default, Debug, Mul, Clone, Copy, Deserialize, Serialize, JsonSchema)] pub struct CornerRadii { pub top_left: f32, pub top_right: f32, @@ -91,7 +92,7 @@ impl From for CornerRadii { #[derive(Debug)] pub struct Shadow { pub bounds: RectF, - pub corner_radius: f32, + pub corner_radii: CornerRadii, pub sigma: f32, pub color: Color, } From fa7ebd08253379dbfab0ce08009446d80639720a Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Sat, 12 Aug 2023 11:08:27 -0600 Subject: [PATCH 092/105] Include drop shadows with different corner radii in the example --- .../examples/{quad.rs => corner_radii.rs} | 57 +++++++++++++++---- 1 file changed, 46 insertions(+), 11 deletions(-) rename crates/gpui/examples/{quad.rs => corner_radii.rs} (67%) diff --git a/crates/gpui/examples/quad.rs b/crates/gpui/examples/corner_radii.rs similarity index 67% rename from crates/gpui/examples/quad.rs rename to crates/gpui/examples/corner_radii.rs index 5d796111c68..1f33917529e 100644 --- a/crates/gpui/examples/quad.rs +++ b/crates/gpui/examples/corner_radii.rs @@ -1,4 +1,7 @@ -use gpui::{color::Color, geometry::rect::RectF, AnyElement, App, Element, Entity, Quad, View}; +use gpui::{ + color::Color, geometry::rect::RectF, scene::Shadow, AnyElement, App, Element, Entity, Quad, + View, +}; use log::LevelFilter; use pathfinder_geometry::vector::vec2f; use simplelog::SimpleLogger; @@ -8,29 +11,29 @@ fn main() { App::new(()).unwrap().run(|cx| { cx.platform().activate(true); - cx.add_window(Default::default(), |_| QuadView); + cx.add_window(Default::default(), |_| CornersView); }); } -struct QuadView; +struct CornersView; -impl Entity for QuadView { +impl Entity for CornersView { type Event = (); } -impl View for QuadView { +impl View for CornersView { fn ui_name() -> &'static str { - "QuadView" + "CornersView" } - fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { - QuadElement.into_any() + fn render(&mut self, _: &mut gpui::ViewContext) -> AnyElement { + CornersElement.into_any() } } -struct QuadElement; +struct CornersElement; -impl gpui::Element for QuadElement { +impl gpui::Element for CornersElement { type LayoutState = (); type PaintState = (); @@ -47,12 +50,20 @@ impl gpui::Element for QuadElement { fn paint( &mut self, scene: &mut gpui::SceneBuilder, - _: pathfinder_geometry::rect::RectF, + bounds: pathfinder_geometry::rect::RectF, _: pathfinder_geometry::rect::RectF, _: &mut Self::LayoutState, _: &mut V, _: &mut gpui::PaintContext, ) -> Self::PaintState { + scene.push_quad(Quad { + bounds, + background: Some(Color::white()), + ..Default::default() + }); + + scene.push_layer(None); + scene.push_quad(Quad { bounds: RectF::new(vec2f(100., 100.), vec2f(100., 100.)), background: Some(Color::red()), @@ -92,6 +103,30 @@ impl gpui::Element for QuadElement { ..Default::default() }, }); + + scene.push_shadow(Shadow { + bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)), + corner_radii: gpui::scene::CornerRadii { + bottom_right: 20., + ..Default::default() + }, + sigma: 20.0, + color: Color::black(), + }); + + scene.push_layer(None); + scene.push_quad(Quad { + bounds: RectF::new(vec2f(400., 100.), vec2f(100., 100.)), + background: Some(Color::red()), + border: Default::default(), + corner_radii: gpui::scene::CornerRadii { + bottom_right: 20., + ..Default::default() + }, + }); + + scene.pop_layer(); + scene.pop_layer(); } fn rect_for_text_range( From 563b25f26f2311e1294c105bbe66d84ff6a5011f Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 12 Aug 2023 12:21:44 -0700 Subject: [PATCH 093/105] Add deserialization helper --- crates/gpui/src/elements/container.rs | 1 + crates/gpui/src/scene.rs | 43 ++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/container.rs b/crates/gpui/src/elements/container.rs index 984742e818f..698100ab296 100644 --- a/crates/gpui/src/elements/container.rs +++ b/crates/gpui/src/elements/container.rs @@ -30,6 +30,7 @@ pub struct ContainerStyle { #[serde(default)] pub border: Border, #[serde(default)] + #[serde(alias = "corner_radius")] pub corner_radii: CornerRadii, #[serde(default)] pub shadow: Option, diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index 48649c57918..d75a4bceff9 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -70,7 +70,7 @@ pub struct Quad { pub corner_radii: CornerRadii, } -#[derive(Default, Debug, Mul, Clone, Copy, Deserialize, Serialize, JsonSchema)] +#[derive(Default, Debug, Mul, Clone, Copy, Serialize, JsonSchema)] pub struct CornerRadii { pub top_left: f32, pub top_right: f32, @@ -78,6 +78,47 @@ pub struct CornerRadii { pub bottom_left: f32, } +impl<'de> Deserialize<'de> for CornerRadii { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + pub struct CornerRadiiHelper { + pub top_left: f32, + pub top_right: f32, + pub bottom_right: f32, + pub bottom_left: f32, + } + + #[derive(Deserialize)] + #[serde(untagged)] + enum RadiusOrRadii { + Radius(f32), + Radii(CornerRadiiHelper), + } + + let json = RadiusOrRadii::deserialize(deserializer)?; + + let result = match json { + RadiusOrRadii::Radius(radius) => CornerRadii::from(radius), + RadiusOrRadii::Radii(CornerRadiiHelper { + top_left, + top_right, + bottom_right, + bottom_left, + }) => CornerRadii { + top_left, + top_right, + bottom_right, + bottom_left, + }, + }; + + Ok(result) + } +} + impl From for CornerRadii { fn from(radius: f32) -> Self { Self { From 29a85635eafd9eec06e2f6c244e6de5076549158 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sat, 12 Aug 2023 12:23:46 -0700 Subject: [PATCH 094/105] Make each setting optional --- crates/gpui/src/scene.rs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index d75a4bceff9..cbc0a921512 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -85,10 +85,10 @@ impl<'de> Deserialize<'de> for CornerRadii { { #[derive(Deserialize)] pub struct CornerRadiiHelper { - pub top_left: f32, - pub top_right: f32, - pub bottom_right: f32, - pub bottom_left: f32, + pub top_left: Option, + pub top_right: Option, + pub bottom_right: Option, + pub bottom_left: Option, } #[derive(Deserialize)] @@ -108,10 +108,10 @@ impl<'de> Deserialize<'de> for CornerRadii { bottom_right, bottom_left, }) => CornerRadii { - top_left, - top_right, - bottom_right, - bottom_left, + top_left: top_left.unwrap_or(0.0), + top_right: top_right.unwrap_or(0.0), + bottom_right: bottom_right.unwrap_or(0.0), + bottom_left: bottom_left.unwrap_or(0.0), }, }; From 5d2750e0d4e5e9206146cef07c564beb92168cb7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:39:37 +0300 Subject: [PATCH 095/105] Hide inlay cache fields --- crates/collab/src/tests/integration_tests.rs | 42 +++++++++++--------- crates/editor/src/inlay_hint_cache.rs | 23 ++++++++--- 2 files changed, 41 insertions(+), 24 deletions(-) diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index ce7fd8a094a..657457d5928 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -7953,7 +7953,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host editor update the cache version after every cache/view change", ); }); @@ -7976,7 +7977,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -7996,7 +7998,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "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); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8010,7 +8012,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "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); + assert_eq!(inlay_cache.version(), edits_made); }); editor_a.update(cx_a, |editor, cx| { @@ -8035,7 +8037,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( 4th query was made by guest (but not applied) due to cache invalidation logic" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); editor_b.update(cx_b, |editor, _| { assert_eq!( @@ -8051,7 +8053,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( "Guest should get hints from 3rd edit, 6th LSP query" ); let inlay_cache = editor.inlay_hint_cache(); - assert_eq!(inlay_cache.version, edits_made); + assert_eq!(inlay_cache.version(), edits_made); }); fake_language_server @@ -8077,7 +8079,8 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Host should accepted all edits and bump its cache version every time" ); }); @@ -8098,7 +8101,7 @@ async fn test_mutual_editor_inlay_hint_cache_update( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, + inlay_cache.version(), edits_made, "Guest should accepted all edits and bump its cache version every time" ); @@ -8264,7 +8267,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8279,7 +8283,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest editor update the cache version after every cache/view change" ); }); @@ -8296,7 +8301,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, 0, + inlay_cache.version(), + 0, "Host should not increment its cache version due to no changes", ); }); @@ -8311,7 +8317,8 @@ async fn test_inlay_hint_refresh_is_forwarded( ); let inlay_cache = editor.inlay_hint_cache(); assert_eq!( - inlay_cache.version, edits_made, + inlay_cache.version(), + edits_made, "Guest should accepted all edits and bump its cache version every time" ); }); @@ -8343,13 +8350,10 @@ fn room_participants(room: &ModelHandle, cx: &mut TestAppContext) -> RoomP fn extract_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 hint in editor.inlay_hint_cache().hints() { + match hint.label { + project::InlayHintLabel::String(s) => labels.push(s), + _ => unreachable!(), } } labels diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 2d75b4d2cec..44fbbf163d4 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -20,10 +20,10 @@ use language::language_settings::InlayHintSettings; use util::post_inc; pub struct InlayHintCache { - pub hints: HashMap>>, - pub allowed_hint_kinds: HashSet>, - pub version: usize, - pub enabled: bool, + hints: HashMap>>, + allowed_hint_kinds: HashSet>, + version: usize, + enabled: bool, update_tasks: HashMap, } @@ -32,7 +32,7 @@ pub struct CachedExcerptHints { version: usize, buffer_version: Global, buffer_id: u64, - pub hints: Vec<(InlayId, InlayHint)>, + hints: Vec<(InlayId, InlayHint)>, } #[derive(Debug, Clone, Copy)] @@ -368,6 +368,19 @@ impl InlayHintCache { self.update_tasks.clear(); self.hints.clear(); } + + pub fn hints(&self) -> Vec { + let mut hints = Vec::new(); + for excerpt_hints in self.hints.values() { + let excerpt_hints = excerpt_hints.read(); + hints.extend(excerpt_hints.hints.iter().map(|(_, hint)| hint).cloned()); + } + hints + } + + pub fn version(&self) -> usize { + self.version + } } fn spawn_new_update_tasks( From 708409e06d381269a21dedbd02fa86b30c5d4047 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 14:51:38 +0300 Subject: [PATCH 096/105] Query hints on every scroll --- crates/editor/src/inlay_hint_cache.rs | 4 +++- crates/editor/src/scroll.rs | 6 ++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 44fbbf163d4..1dbef165fad 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -24,6 +24,7 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, + // TODO kb track them by excerpt range update_tasks: HashMap, } @@ -100,6 +101,7 @@ impl InvalidationStrategy { } impl ExcerptQuery { + // TODO kb query only visible + one visible below and above fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { let visible_range = self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; @@ -168,7 +170,6 @@ impl InlayHintCache { ); if new_splice.is_some() { self.version += 1; - self.update_tasks.clear(); self.allowed_hint_kinds = new_allowed_hint_kinds; } ControlFlow::Break(new_splice) @@ -464,6 +465,7 @@ fn spawn_new_update_tasks( cx, ) }; + // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { let update_task = o.get_mut(); diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index d595337428d..1f3adaf477f 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -13,7 +13,7 @@ use gpui::{ }; use language::{Bias, Point}; use util::ResultExt; -use workspace::{item::Item, WorkspaceId}; +use workspace::WorkspaceId; use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, @@ -333,9 +333,7 @@ impl Editor { cx, ); - if !self.is_singleton(cx) { - self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); - } + self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { From 0e2a1fc14996c5831c1060fe7cf3ddb3670a6626 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 10 Aug 2023 18:09:09 +0300 Subject: [PATCH 097/105] Query inlay hints for parts of the file --- crates/editor/src/inlay_hint_cache.rs | 396 ++++++++++---------------- 1 file changed, 145 insertions(+), 251 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 1dbef165fad..30f02c17f5c 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -9,7 +9,7 @@ use crate::{ }; use anyhow::Context; use clock::Global; -use gpui::{ModelHandle, Task, ViewContext}; +use gpui::{ModelContext, ModelHandle, Task, ViewContext}; use language::{language_settings::InlayHintKind, Buffer, BufferSnapshot}; use log::error; use parking_lot::RwLock; @@ -24,8 +24,13 @@ pub struct InlayHintCache { allowed_hint_kinds: HashSet>, version: usize, enabled: bool, - // TODO kb track them by excerpt range - update_tasks: HashMap, + update_tasks: HashMap, +} + +#[derive(Debug)] +struct TasksForRanges { + tasks: Vec>, + ranges: Vec>, } #[derive(Debug)] @@ -49,18 +54,6 @@ pub struct InlaySplice { pub to_insert: Vec, } -struct UpdateTask { - invalidate: InvalidationStrategy, - cache_version: usize, - task: RunningTask, - pending_refresh: Option, -} - -struct RunningTask { - _task: Task<()>, - is_running_rx: smol::channel::Receiver<()>, -} - #[derive(Debug)] struct ExcerptHintsUpdate { excerpt_id: ExcerptId, @@ -73,24 +66,10 @@ struct ExcerptHintsUpdate { struct ExcerptQuery { buffer_id: u64, excerpt_id: ExcerptId, - dimensions: ExcerptDimensions, cache_version: usize, invalidate: InvalidationStrategy, } -#[derive(Debug, Clone, Copy)] -struct ExcerptDimensions { - excerpt_range_start: language::Anchor, - excerpt_range_end: language::Anchor, - excerpt_visible_range_start: language::Anchor, - excerpt_visible_range_end: language::Anchor, -} - -struct HintFetchRanges { - visible_range: Range, - other_ranges: Vec>, -} - impl InvalidationStrategy { fn should_invalidate(&self) -> bool { matches!( @@ -100,37 +79,43 @@ impl InvalidationStrategy { } } -impl ExcerptQuery { - // TODO kb query only visible + one visible below and above - fn hints_fetch_ranges(&self, buffer: &BufferSnapshot) -> HintFetchRanges { - let visible_range = - self.dimensions.excerpt_visible_range_start..self.dimensions.excerpt_visible_range_end; - let mut other_ranges = Vec::new(); - if self - .dimensions - .excerpt_range_start - .cmp(&visible_range.start, buffer) - .is_lt() - { - let mut end = visible_range.start; - end.offset -= 1; - other_ranges.push(self.dimensions.excerpt_range_start..end); - } - if self - .dimensions - .excerpt_range_end - .cmp(&visible_range.end, buffer) - .is_gt() - { - let mut start = visible_range.end; - start.offset += 1; - other_ranges.push(start..self.dimensions.excerpt_range_end); +impl TasksForRanges { + fn new(ranges: Vec>, task: Task<()>) -> Self { + Self { + tasks: vec![task], + ranges, } + } - HintFetchRanges { - visible_range, - other_ranges: other_ranges.into_iter().map(|range| range).collect(), - } + fn update_cached_tasks( + &mut self, + buffer_snapshot: &BufferSnapshot, + query_range: Range, + invalidate: InvalidationStrategy, + spawn_task: impl FnOnce(Vec>) -> Task<()>, + ) { + let ranges_to_query = match invalidate { + InvalidationStrategy::None => { + // let mut ranges_to_query = Vec::new(); + + // todo!("TODO kb also remove task ranges on invalidation"); + // if ranges_to_query.is_empty() { + // return; + // } + // ranges_to_query + vec![query_range] + } + InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { + self.tasks.clear(); + self.ranges.clear(); + vec![query_range] + } + }; + + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); } } @@ -198,7 +183,7 @@ impl InlayHintCache { pub fn spawn_hint_refresh( &mut self, - mut excerpts_to_query: HashMap, Global, Range)>, + excerpts_to_query: HashMap, Global, Range)>, invalidate: InvalidationStrategy, cx: &mut ViewContext, ) -> Option { @@ -206,11 +191,10 @@ impl InlayHintCache { return None; } - let update_tasks = &mut self.update_tasks; let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { let mut changed = false; - update_tasks.retain(|task_excerpt_id, _| { + self.update_tasks.retain(|task_excerpt_id, _| { let retain = excerpts_to_query.contains_key(task_excerpt_id); changed |= !retain; retain @@ -232,17 +216,6 @@ impl InlayHintCache { } let cache_version = self.version; - excerpts_to_query.retain(|visible_excerpt_id, _| { - match update_tasks.entry(*visible_excerpt_id) { - hash_map::Entry::Occupied(o) => match o.get().cache_version.cmp(&cache_version) { - cmp::Ordering::Less => true, - cmp::Ordering::Equal => invalidate.should_invalidate(), - cmp::Ordering::Greater => false, - }, - hash_map::Entry::Vacant(_) => true, - } - }); - cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -392,13 +365,14 @@ fn spawn_new_update_tasks( cx: &mut ViewContext<'_, '_, Editor>, ) { let visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - for (excerpt_id, (buffer_handle, new_task_buffer_version, excerpt_visible_range)) in + for (excerpt_id, (excerpt_buffer, new_task_buffer_version, excerpt_visible_range)) in excerpts_to_query { if excerpt_visible_range.is_empty() { continue; } - let buffer = buffer_handle.read(cx); + let buffer = excerpt_buffer.read(cx); + let buffer_id = buffer.remote_id(); let buffer_snapshot = buffer.snapshot(); if buffer_snapshot .version() @@ -416,203 +390,120 @@ fn spawn_new_update_tasks( { continue; } - if !new_task_buffer_version.changed_since(&cached_buffer_version) - && !matches!(invalidate, InvalidationStrategy::RefreshRequested) - { - continue; - } }; - let buffer_id = buffer.remote_id(); - let excerpt_visible_range_start = buffer.anchor_before(excerpt_visible_range.start); - let excerpt_visible_range_end = buffer.anchor_after(excerpt_visible_range.end); - - let (multi_buffer_snapshot, full_excerpt_range) = + let (multi_buffer_snapshot, Some(query_range)) = editor.buffer.update(cx, |multi_buffer, cx| { - let multi_buffer_snapshot = multi_buffer.snapshot(cx); ( - multi_buffer_snapshot, - multi_buffer - .excerpts_for_buffer(&buffer_handle, cx) - .into_iter() - .find(|(id, _)| id == &excerpt_id) - .map(|(_, range)| range.context), + multi_buffer.snapshot(cx), + determine_query_range( + multi_buffer, + excerpt_id, + &excerpt_buffer, + excerpt_visible_range, + cx, + ), ) - }); + }) else { return; }; + let query = ExcerptQuery { + buffer_id, + excerpt_id, + cache_version: update_cache_version, + invalidate, + }; - if let Some(full_excerpt_range) = full_excerpt_range { - let query = ExcerptQuery { - buffer_id, - excerpt_id, - dimensions: ExcerptDimensions { - excerpt_range_start: full_excerpt_range.start, - excerpt_range_end: full_excerpt_range.end, - excerpt_visible_range_start, - excerpt_visible_range_end, - }, - cache_version: update_cache_version, - invalidate, - }; - - let new_update_task = |is_refresh_after_regular_task| { - new_update_task( - query, - multi_buffer_snapshot, - buffer_snapshot, - Arc::clone(&visible_hints), - cached_excerpt_hints, - is_refresh_after_regular_task, - cx, - ) - }; - // TODO kb need to add to update tasks + ensure RefreshRequested cleans other ranges - match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { - hash_map::Entry::Occupied(mut o) => { - let update_task = o.get_mut(); - match (update_task.invalidate, invalidate) { - (_, InvalidationStrategy::None) => {} - ( - InvalidationStrategy::BufferEdited, - InvalidationStrategy::RefreshRequested, - ) if !update_task.task.is_running_rx.is_closed() => { - update_task.pending_refresh = Some(query); - } - _ => { - o.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } - } - } - hash_map::Entry::Vacant(v) => { - v.insert(UpdateTask { - invalidate, - cache_version: query.cache_version, - task: new_update_task(false), - pending_refresh: None, - }); - } + let new_update_task = |fetch_ranges| { + new_update_task( + query, + fetch_ranges, + multi_buffer_snapshot, + buffer_snapshot.clone(), + Arc::clone(&visible_hints), + cached_excerpt_hints, + cx, + ) + }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { + hash_map::Entry::Occupied(mut o) => { + o.get_mut().update_cached_tasks( + &buffer_snapshot, + query_range, + invalidate, + new_update_task, + ); + } + hash_map::Entry::Vacant(v) => { + v.insert(TasksForRanges::new( + vec![query_range.clone()], + new_update_task(vec![query_range]), + )); } } } } +fn determine_query_range( + multi_buffer: &mut MultiBuffer, + excerpt_id: ExcerptId, + excerpt_buffer: &ModelHandle, + excerpt_visible_range: Range, + cx: &mut ModelContext<'_, MultiBuffer>, +) -> 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 excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; + let start = buffer.anchor_before( + excerpt_visible_range + .start + .saturating_sub(excerpt_visible_len) + .max(full_excerpt_range.start.offset), + ); + let end = buffer.anchor_after( + excerpt_visible_range + .end + .saturating_add(excerpt_visible_len) + .min(full_excerpt_range.end.offset) + .min(buffer.len()), + ); + Some(start..end) +} + fn new_update_task( query: ExcerptQuery, + hints_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, cached_excerpt_hints: Option>>, - is_refresh_after_regular_task: bool, cx: &mut ViewContext<'_, '_, Editor>, -) -> RunningTask { - let hints_fetch_ranges = query.hints_fetch_ranges(&buffer_snapshot); - let (is_running_tx, is_running_rx) = smol::channel::bounded(1); - let _task = cx.spawn(|editor, mut cx| async move { - let _is_running_tx = is_running_tx; - let create_update_task = |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(), - ) - }; - - if is_refresh_after_regular_task { - let visible_range_has_updates = - match create_update_task(hints_fetch_ranges.visible_range).await { - Ok(updated) => updated, - Err(e) => { - error!("inlay hint visible range update task failed: {e:#}"); - return; - } - }; - - if visible_range_has_updates { - let other_update_results = futures::future::join_all( - hints_fetch_ranges - .other_ranges - .into_iter() - .map(create_update_task), +) -> Task<()> { + cx.spawn(|editor, cx| async move { + let task_update_results = + futures::future::join_all(hints_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(), ) - .await; - - for result in other_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } - } - } - } else { - let task_update_results = futures::future::join_all( - std::iter::once(hints_fetch_ranges.visible_range) - .chain(hints_fetch_ranges.other_ranges.into_iter()) - .map(create_update_task), - ) + })) .await; - for result in task_update_results { - if let Err(e) = result { - error!("inlay hint update task failed: {e:#}"); - } + for result in task_update_results { + if let Err(e) = result { + error!("inlay hint update task failed: {e:#}"); } } - - editor - .update(&mut cx, |editor, cx| { - let pending_refresh_query = editor - .inlay_hint_cache - .update_tasks - .get_mut(&query.excerpt_id) - .and_then(|task| task.pending_refresh.take()); - - if let Some(pending_refresh_query) = pending_refresh_query { - let refresh_multi_buffer = editor.buffer().read(cx); - let refresh_multi_buffer_snapshot = refresh_multi_buffer.snapshot(cx); - let refresh_visible_hints = Arc::new(editor.visible_inlay_hints(cx)); - let refresh_cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .get(&pending_refresh_query.excerpt_id) - .map(Arc::clone); - if let Some(buffer) = - refresh_multi_buffer.buffer(pending_refresh_query.buffer_id) - { - editor.inlay_hint_cache.update_tasks.insert( - pending_refresh_query.excerpt_id, - UpdateTask { - invalidate: InvalidationStrategy::RefreshRequested, - cache_version: editor.inlay_hint_cache.version, - task: new_update_task( - pending_refresh_query, - refresh_multi_buffer_snapshot, - buffer.read(cx).snapshot(), - refresh_visible_hints, - refresh_cached_excerpt_hints, - true, - cx, - ), - pending_refresh: None, - }, - ); - } - } - }) - .ok(); - }); - - RunningTask { - _task, - is_running_rx, - } + }) } async fn fetch_and_update_hints( @@ -2202,7 +2093,8 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // TODO kb find the range needed + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2227,7 +2119,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2255,7 +2147,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - "main hint #4".to_string(), + // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2284,6 +2176,8 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), + // TODO kb why? + "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), From 56f89739f888c9122f54de48d701a52ced806f91 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 17:36:34 +0300 Subject: [PATCH 098/105] Do not add duplicate hints to the cache --- crates/editor/src/inlay_hint_cache.rs | 106 +++++++++++++------------- crates/editor/src/scroll.rs | 1 + 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 30f02c17f5c..e792064ec78 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -193,29 +193,21 @@ impl InlayHintCache { let mut invalidated_hints = Vec::new(); if invalidate.should_invalidate() { - let mut changed = false; - self.update_tasks.retain(|task_excerpt_id, _| { - let retain = excerpts_to_query.contains_key(task_excerpt_id); - changed |= !retain; - retain - }); + self.update_tasks + .retain(|task_excerpt_id, _| excerpts_to_query.contains_key(task_excerpt_id)); self.hints.retain(|cached_excerpt, cached_hints| { let retain = excerpts_to_query.contains_key(cached_excerpt); - changed |= !retain; if !retain { invalidated_hints.extend(cached_hints.read().hints.iter().map(|&(id, _)| id)); } retain }); - if changed { - self.version += 1; - } } if excerpts_to_query.is_empty() && invalidated_hints.is_empty() { return None; } - let cache_version = self.version; + let cache_version = self.version + 1; cx.spawn(|editor, mut cx| async move { editor .update(&mut cx, |editor, cx| { @@ -475,7 +467,7 @@ fn determine_query_range( fn new_update_task( query: ExcerptQuery, - hints_fetch_ranges: Vec>, + hint_fetch_ranges: Vec>, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, visible_hints: Arc>, @@ -484,7 +476,7 @@ fn new_update_task( ) -> Task<()> { cx.spawn(|editor, cx| async move { let task_update_results = - futures::future::join_all(hints_fetch_ranges.into_iter().map(|range| { + futures::future::join_all(hint_fetch_ranges.into_iter().map(|range| { fetch_and_update_hints( editor.clone(), multi_buffer_snapshot.clone(), @@ -515,7 +507,7 @@ async fn fetch_and_update_hints( query: ExcerptQuery, fetch_range: Range, mut cx: gpui::AsyncAppContext, -) -> anyhow::Result { +) -> anyhow::Result<()> { let inlay_hints_fetch_task = editor .update(&mut cx, |editor, cx| { editor @@ -531,8 +523,7 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let mut update_happened = false; - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(update_happened) }; + let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; let new_hints = inlay_hints_fetch_task .await .context("inlay hint fetch task")?; @@ -555,10 +546,6 @@ async fn fetch_and_update_hints( editor .update(&mut cx, |editor, cx| { if let Some(new_update) = new_update { - update_happened = !new_update.add_to_cache.is_empty() - || !new_update.remove_from_cache.is_empty() - || !new_update.remove_from_visible.is_empty(); - let cached_excerpt_hints = editor .inlay_hint_cache .hints @@ -578,43 +565,51 @@ async fn fetch_and_update_hints( cached_excerpt_hints.version = query.cache_version; } } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); cached_excerpt_hints .hints .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - editor.inlay_hint_cache.version += 1; - let mut splice = InlaySplice { to_remove: new_update.remove_from_visible, to_insert: Vec::new(), }; - for new_hint in new_update.add_to_cache { - let new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints.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) + } + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = multi_buffer_snapshot + .anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice.to_insert.push(Inlay::hint( + new_inlay_id, + new_hint_position, + &new_hint, + )); + } + cached_hints + .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; } - - cached_excerpt_hints - .hints - .push((InlayId::Hint(new_inlay_id), new_hint)); } - - cached_excerpt_hints - .hints - .sort_by(|(_, hint_a), (_, hint_b)| { - hint_a.position.cmp(&hint_b.position, &buffer_snapshot) - }); + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); drop(cached_excerpt_hints); if query.invalidate.should_invalidate() { @@ -633,6 +628,7 @@ async fn fetch_and_update_hints( .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); } } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); editor .inlay_hint_cache .hints @@ -643,14 +639,18 @@ async fn fetch_and_update_hints( to_remove, to_insert, } = splice; - if !to_remove.is_empty() || !to_insert.is_empty() { + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { editor.splice_inlay_hints(to_remove, to_insert, cx) } } }) .ok(); - Ok(update_happened) + Ok(()) } fn calculate_hint_updates( @@ -2093,7 +2093,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed + // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? // "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), @@ -2176,8 +2176,6 @@ mod tests { "main hint(edited) #1".to_string(), "main hint(edited) #2".to_string(), "main hint(edited) #3".to_string(), - // TODO kb why? - "main hint(edited) #3".to_string(), "main hint(edited) #4".to_string(), "main hint(edited) #5".to_string(), "other hint(edited) #0".to_string(), @@ -2192,8 +2190,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( editor.inlay_hint_cache().version, - last_scroll_update_version + expected_layers.len() + 1, - "Due to every excerpt having one hint, cache should update per new excerpt received + 1 for outdated hints removal" + last_scroll_update_version + expected_layers.len(), + "Due to every excerpt having one hint, cache should update per new excerpt received" ); }); } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 1f3adaf477f..9a6748fa7c2 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,6 +333,7 @@ impl Editor { cx, ); + // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 449c009639d07d760a33d8caac8dc80e05dac9c5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 11 Aug 2023 23:06:57 +0300 Subject: [PATCH 099/105] Properly generate ranges to query --- crates/editor/src/inlay_hint_cache.rs | 71 ++++++++++++++++++++++----- crates/editor/src/scroll.rs | 1 - 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index e792064ec78..a3edb651286 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -96,14 +96,55 @@ impl TasksForRanges { ) { let ranges_to_query = match invalidate { InvalidationStrategy::None => { - // let mut ranges_to_query = Vec::new(); + let mut ranges_to_query = Vec::new(); + let mut last_cache_range_stop = None::; + for cached_range in self + .ranges + .iter() + .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 last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) + < cached_range.start.offset + { + ranges_to_query.push(last_cache_range_stop..cached_range.start); + } + } + None => { + if query_range + .start + .cmp(&cached_range.start, buffer_snapshot) + .is_lt() + { + ranges_to_query.push(query_range.start..cached_range.start); + } + } + } + last_cache_range_stop = Some(cached_range.end); + } - // todo!("TODO kb also remove task ranges on invalidation"); - // if ranges_to_query.is_empty() { - // return; - // } - // ranges_to_query - vec![query_range] + match last_cache_range_stop { + Some(last_cache_range_stop) => { + if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { + ranges_to_query.push(last_cache_range_stop..query_range.end); + } + } + None => ranges_to_query.push(query_range), + } + + ranges_to_query } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); @@ -112,10 +153,12 @@ impl TasksForRanges { } }; - self.ranges.extend(ranges_to_query.clone()); - self.ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); - self.tasks.push(spawn_task(ranges_to_query)); + if !ranges_to_query.is_empty() { + self.ranges.extend(ranges_to_query.clone()); + self.ranges + .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); + self.tasks.push(spawn_task(ranges_to_query)); + } } } @@ -462,7 +505,11 @@ fn determine_query_range( .min(full_excerpt_range.end.offset) .min(buffer.len()), ); - Some(start..end) + if start.cmp(&end, buffer).is_eq() { + None + } else { + Some(start..end) + } } fn new_update_task( diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 9a6748fa7c2..1f3adaf477f 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -333,7 +333,6 @@ impl Editor { cx, ); - // TODO kb too many events + too many LSP requests even due to deduplication, why? self.refresh_inlays(InlayRefreshReason::NewLinesShown, cx); } From 87e6651ecb2acbe8f949e786528bcb751ff41def Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 11:24:49 +0300 Subject: [PATCH 100/105] Fix hint tests, add a char boundary bug test --- crates/editor/src/editor.rs | 2 +- crates/editor/src/inlay_hint_cache.rs | 534 ++++++++++++++++++-------- 2 files changed, 377 insertions(+), 159 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d8b77ea950..f65f19cfecc 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2723,7 +2723,7 @@ impl Editor { .collect() } - fn excerpt_visible_offsets( + pub fn excerpt_visible_offsets( &self, restrict_to_languages: Option<&HashSet>>, cx: &mut ViewContext<'_, '_, Editor>, diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index a3edb651286..b06a7200902 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -30,7 +30,7 @@ pub struct InlayHintCache { #[derive(Debug)] struct TasksForRanges { tasks: Vec>, - ranges: Vec>, + sorted_ranges: Vec>, } #[derive(Debug)] @@ -80,10 +80,10 @@ impl InvalidationStrategy { } impl TasksForRanges { - fn new(ranges: Vec>, task: Task<()>) -> Self { + fn new(sorted_ranges: Vec>, task: Task<()>) -> Self { Self { tasks: vec![task], - ranges, + sorted_ranges, } } @@ -99,8 +99,8 @@ impl TasksForRanges { let mut ranges_to_query = Vec::new(); let mut last_cache_range_stop = None::; for cached_range in self - .ranges - .iter() + .sorted_ranges + .iter_mut() .skip_while(|cached_range| { cached_range .end @@ -148,14 +148,14 @@ impl TasksForRanges { } InvalidationStrategy::RefreshRequested | InvalidationStrategy::BufferEdited => { self.tasks.clear(); - self.ranges.clear(); + self.sorted_ranges.clear(); vec![query_range] } }; if !ranges_to_query.is_empty() { - self.ranges.extend(ranges_to_query.clone()); - self.ranges + self.sorted_ranges.extend(ranges_to_query.clone()); + self.sorted_ranges .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } @@ -458,6 +458,7 @@ fn spawn_new_update_tasks( cx, ) }; + match editor.inlay_hint_cache.update_tasks.entry(excerpt_id) { hash_map::Entry::Occupied(mut o) => { o.get_mut().update_cached_tasks( @@ -570,10 +571,10 @@ async fn fetch_and_update_hints( }) .ok() .flatten(); - let Some(inlay_hints_fetch_task) = inlay_hints_fetch_task else { return Ok(()) }; - let new_hints = inlay_hints_fetch_task - .await - .context("inlay hint fetch task")?; + let new_hints = match inlay_hints_fetch_task { + Some(task) => task.await.context("inlay hint fetch task")?, + None => return Ok(()), + }; let background_task_buffer_snapshot = buffer_snapshot.clone(); let backround_fetch_range = fetch_range.clone(); let new_update = cx @@ -589,114 +590,20 @@ async fn fetch_and_update_hints( ) }) .await; - - editor - .update(&mut cx, |editor, cx| { - if let Some(new_update) = new_update { - let cached_excerpt_hints = editor - .inlay_hint_cache - .hints - .entry(new_update.excerpt_id) - .or_insert_with(|| { - Arc::new(RwLock::new(CachedExcerptHints { - version: query.cache_version, - buffer_version: buffer_snapshot.version().clone(), - buffer_id: query.buffer_id, - hints: Vec::new(), - })) - }); - let mut cached_excerpt_hints = cached_excerpt_hints.write(); - match query.cache_version.cmp(&cached_excerpt_hints.version) { - cmp::Ordering::Less => return, - cmp::Ordering::Greater | cmp::Ordering::Equal => { - cached_excerpt_hints.version = query.cache_version; - } - } - - let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); - cached_excerpt_hints - .hints - .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); - let mut splice = InlaySplice { - to_remove: new_update.remove_from_visible, - to_insert: Vec::new(), - }; - for new_hint in new_update.add_to_cache { - let cached_hints = &mut cached_excerpt_hints.hints; - let insert_position = match cached_hints.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) - } - } - Err(i) => Some(i), - }; - - if let Some(insert_position) = insert_position { - let new_inlay_id = post_inc(&mut editor.next_inlay_id); - if editor - .inlay_hint_cache - .allowed_hint_kinds - .contains(&new_hint.kind) - { - let new_hint_position = multi_buffer_snapshot - .anchor_in_excerpt(query.excerpt_id, new_hint.position); - splice.to_insert.push(Inlay::hint( - new_inlay_id, - new_hint_position, - &new_hint, - )); - } - cached_hints - .insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); - cached_inlays_changed = true; - } - } - cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); - drop(cached_excerpt_hints); - - if query.invalidate.should_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(); - if excerpt_hints.buffer_id == query.buffer_id - && excerpt_id != &query.excerpt_id - && buffer_snapshot - .version() - .changed_since(&excerpt_hints.buffer_version) - { - outdated_excerpt_caches.insert(*excerpt_id); - splice - .to_remove - .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); - } - } - cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); - editor - .inlay_hint_cache - .hints - .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); - } - - let InlaySplice { - to_remove, - to_insert, - } = splice; - let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); - if cached_inlays_changed || displayed_inlays_changed { - editor.inlay_hint_cache.version += 1; - } - if displayed_inlays_changed { - editor.splice_inlay_hints(to_remove, to_insert, cx) - } - } - }) - .ok(); - + if let Some(new_update) = new_update { + editor + .update(&mut cx, |editor, cx| { + apply_hint_update( + editor, + new_update, + query, + buffer_snapshot, + multi_buffer_snapshot, + cx, + ); + }) + .ok(); + } Ok(()) } @@ -808,6 +715,113 @@ fn contains_position( && range.end.cmp(&position, buffer_snapshot).is_ge() } +fn apply_hint_update( + editor: &mut Editor, + new_update: ExcerptHintsUpdate, + query: ExcerptQuery, + buffer_snapshot: BufferSnapshot, + multi_buffer_snapshot: MultiBufferSnapshot, + cx: &mut ViewContext<'_, '_, Editor>, +) { + let cached_excerpt_hints = editor + .inlay_hint_cache + .hints + .entry(new_update.excerpt_id) + .or_insert_with(|| { + Arc::new(RwLock::new(CachedExcerptHints { + version: query.cache_version, + buffer_version: buffer_snapshot.version().clone(), + buffer_id: query.buffer_id, + hints: Vec::new(), + })) + }); + let mut cached_excerpt_hints = cached_excerpt_hints.write(); + match query.cache_version.cmp(&cached_excerpt_hints.version) { + cmp::Ordering::Less => return, + cmp::Ordering::Greater | cmp::Ordering::Equal => { + cached_excerpt_hints.version = query.cache_version; + } + } + + let mut cached_inlays_changed = !new_update.remove_from_cache.is_empty(); + cached_excerpt_hints + .hints + .retain(|(hint_id, _)| !new_update.remove_from_cache.contains(hint_id)); + let mut splice = InlaySplice { + to_remove: new_update.remove_from_visible, + to_insert: Vec::new(), + }; + for new_hint in new_update.add_to_cache { + let cached_hints = &mut cached_excerpt_hints.hints; + let insert_position = match cached_hints + .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) + } + } + Err(i) => Some(i), + }; + + if let Some(insert_position) = insert_position { + let new_inlay_id = post_inc(&mut editor.next_inlay_id); + if editor + .inlay_hint_cache + .allowed_hint_kinds + .contains(&new_hint.kind) + { + let new_hint_position = + multi_buffer_snapshot.anchor_in_excerpt(query.excerpt_id, new_hint.position); + splice + .to_insert + .push(Inlay::hint(new_inlay_id, new_hint_position, &new_hint)); + } + cached_hints.insert(insert_position, (InlayId::Hint(new_inlay_id), new_hint)); + cached_inlays_changed = true; + } + } + cached_excerpt_hints.buffer_version = buffer_snapshot.version().clone(); + drop(cached_excerpt_hints); + + if query.invalidate.should_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(); + if excerpt_hints.buffer_id == query.buffer_id + && excerpt_id != &query.excerpt_id + && buffer_snapshot + .version() + .changed_since(&excerpt_hints.buffer_version) + { + outdated_excerpt_caches.insert(*excerpt_id); + splice + .to_remove + .extend(excerpt_hints.hints.iter().map(|(id, _)| id)); + } + } + cached_inlays_changed |= !outdated_excerpt_caches.is_empty(); + editor + .inlay_hint_cache + .hints + .retain(|excerpt_id, _| !outdated_excerpt_caches.contains(excerpt_id)); + } + + let InlaySplice { + to_remove, + to_insert, + } = splice; + let displayed_inlays_changed = !to_remove.is_empty() || !to_insert.is_empty(); + if cached_inlays_changed || displayed_inlays_changed { + editor.inlay_hint_cache.version += 1; + } + if displayed_inlays_changed { + editor.splice_inlay_hints(to_remove, to_insert, cx) + } +} + #[cfg(test)] mod tests { use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; @@ -819,6 +833,7 @@ mod tests { }; use futures::StreamExt; use gpui::{executor::Deterministic, TestAppContext, ViewHandle}; + use itertools::Itertools; use language::{ language_settings::AllLanguageSettingsContent, FakeLspAdapter, Language, LanguageConfig, }; @@ -826,7 +841,7 @@ mod tests { use parking_lot::Mutex; use project::{FakeFs, Project}; use settings::SettingsStore; - use text::Point; + use text::{Point, ToPoint}; use workspace::Workspace; use crate::editor_tests::update_test_language_settings; @@ -1832,7 +1847,7 @@ 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::SeqCst) + 1; + let i = Arc::clone(&task_lsp_request_count).fetch_add(1, Ordering::Release) + 1; Ok(Some(vec![lsp::InlayHint { position: query_start, label: lsp::InlayHintLabel::String(i.to_string()), @@ -1847,18 +1862,44 @@ mod tests { }) .next() .await; + fn editor_visible_range( + editor: &ViewHandle, + cx: &mut gpui::TestAppContext, + ) -> Range { + let ranges = editor.update(cx, |editor, cx| editor.excerpt_visible_offsets(None, cx)); + assert_eq!( + ranges.len(), + 1, + "Single buffer should produce a single excerpt with visible range" + ); + let (_, (excerpt_buffer, _, excerpt_visible_range)) = + ranges.into_iter().next().unwrap(); + excerpt_buffer.update(cx, |buffer, _| { + let snapshot = buffer.snapshot(); + let start = buffer + .anchor_before(excerpt_visible_range.start) + .to_point(&snapshot); + let end = buffer + .anchor_after(excerpt_visible_range.end) + .to_point(&snapshot); + start..end + }) + } + + 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); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 2, "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line, ranges[1].start.line, "Both requests should be on the same line"); - assert_eq!(ranges[0].end.character + 1, ranges[1].start.character, "Both request should be concequent"); + 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!(lsp_request_count.load(Ordering::SeqCst), 2, - "When scroll is at the edge of a big document, its visible part + the rest should be queried for hints"); - let expected_layers = vec!["1".to_string(), "2".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 1); + let expected_layers = vec!["1".to_string()]; assert_eq!( expected_layers, cached_hint_labels(editor), @@ -1866,37 +1907,108 @@ mod tests { ); assert_eq!(expected_layers, visible_hint_labels(editor, cx)); assert_eq!( - editor.inlay_hint_cache().version, 2, - "Both LSP queries should've bumped the cache version" + editor.inlay_hint_cache().version, 1, + "LSP queries should've bumped the cache version" ); }); editor.update(cx, |editor, cx| { editor.scroll_screen(&ScrollAmount::Page(1.0), cx); editor.scroll_screen(&ScrollAmount::Page(1.0), cx); - editor.change_selections(None, cx, |s| s.select_ranges([600..600])); - editor.handle_input("++++more text++++", cx); }); + let visible_range_after_scrolls = editor_visible_range(&editor, cx); + cx.foreground().run_until_parked(); + let selection_in_cached_range = editor.update(cx, |editor, cx| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .sorted_by_key(|r| r.start) + .collect::>(); + assert_eq!( + ranges.len(), + 2, + "Should query 2 ranges after both scrolls, but got: {ranges:?}" + ); + let first_scroll = &ranges[0]; + let second_scroll = &ranges[1]; + assert_eq!( + first_scroll.end, second_scroll.start, + "Should query 2 adjacent ranges after the scrolls, but got: {ranges:?}" + ); + assert_eq!( + first_scroll.start, expected_initial_query_range_end, + "First scroll should start the query right after the end of the original scroll", + ); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + assert_eq!( + second_scroll.end, + lsp::Position::new( + visible_range_after_scrolls.end.row + + expected_increment, + 0 + ), + "Second scroll should query one more screen down after the end of the visible range" + ); + + 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, + 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, + 3, + "Should update the cache for every LSP response with hints added" + ); + + let mut selection_in_cached_range = visible_range_after_scrolls.end; + selection_in_cached_range.row -= expected_increment; + selection_in_cached_range + }); + + editor.update(cx, |editor, cx| { + editor.change_selections(Some(Autoscroll::Next), cx, |s| { + s.select_ranges([selection_in_cached_range..selection_in_cached_range]) + }); + }); + cx.foreground().run_until_parked(); + editor.update(cx, |_, _| { + let ranges = lsp_request_ranges + .lock() + .drain(..) + .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); + }); + + editor.update(cx, |editor, cx| { + editor.handle_input("++++more text++++", cx); + }); cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { - let mut ranges = lsp_request_ranges.lock().drain(..).collect::>(); - ranges.sort_by_key(|range| range.start); - assert_eq!(ranges.len(), 3, "When scroll is at the middle of a big document, its visible part + 2 other inbisible parts should be queried for hints"); - assert_eq!(ranges[0].start, lsp::Position::new(0, 0), "Should query from the beginning of the document"); - assert_eq!(ranges[0].end.line + 1, ranges[1].start.line, "Neighbour requests got on different lines due to the line end"); - assert_ne!(ranges[0].end.character, 0, "First query was in the end of the line, not in the beginning"); - assert_eq!(ranges[1].start.character, 0, "Second query got pushed into a new line and starts from the beginning"); - assert_eq!(ranges[1].end.line, ranges[2].start.line, "Neighbour requests should be on the same line"); - assert_eq!(ranges[1].end.character + 1, ranges[2].start.character, "Neighbour request should be concequent"); + let ranges = lsp_request_ranges.lock().drain(..).collect::>(); + let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; + 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_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); + assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::SeqCst), 5, - "When scroll not at the edge of a big document, visible part + 2 other parts should be queried for hints"); - let expected_layers = vec!["3".to_string(), "4".to_string(), "5".to_string()]; + assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); + let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; assert_eq!(expected_layers, cached_hint_labels(editor), - "Should have hints from the new LSP response after edit"); + "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, 5, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 3, "Should update the cache for every LSP response with hints added"); }); } @@ -2130,7 +2242,7 @@ mod tests { s.select_ranges([Point::new(22, 0)..Point::new(22, 0)]) }); editor.change_selections(Some(Autoscroll::Next), cx, |s| { - s.select_ranges([Point::new(56, 0)..Point::new(56, 0)]) + s.select_ranges([Point::new(50, 0)..Point::new(50, 0)]) }); }); cx.foreground().run_until_parked(); @@ -2140,8 +2252,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // TODO kb find the range needed. Is it due to the hint not fitting any excerpt subranges? - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2166,7 +2277,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2194,7 +2305,7 @@ mod tests { "main hint #1".to_string(), "main hint #2".to_string(), "main hint #3".to_string(), - // "main hint #4".to_string(), + "main hint #4".to_string(), "main hint #5".to_string(), "other hint #0".to_string(), "other hint #1".to_string(), @@ -2442,8 +2553,8 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 3, - "Excerpt removal should trigger cache update" + 2, + "Excerpt removal should trigger a cache update" ); }); @@ -2470,12 +2581,119 @@ all hints should be invalidated and requeried for all of its visible excerpts" ); assert_eq!( editor.inlay_hint_cache().version, - 4, - "Settings change should trigger cache update" + 3, + "Settings change should trigger a cache update" ); }); } + #[gpui::test] + async fn test_inside_char_boundary_range_hints(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 language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + capabilities: lsp::ServerCapabilities { + inlay_hint_provider: Some(lsp::OneOf::Left(true)), + ..Default::default() + }, + ..Default::default() + })) + .await; + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": format!(r#"fn main() {{\n{}\n}}"#, format!("let i = {};\n", "√".repeat(10)).repeat(500)), + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let workspace = cx + .add_window(|cx| Workspace::test_new(project.clone(), cx)) + .root(cx); + let worktree_id = workspace.update(cx, |workspace, cx| { + workspace.project().read_with(cx, |project, cx| { + project.worktrees(cx).next().unwrap().read(cx).id() + }) + }); + + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + cx.foreground().run_until_parked(); + cx.foreground().start_waiting(); + let fake_server = fake_servers.next().await.unwrap(); + let editor = workspace + .update(cx, |workspace, cx| { + workspace.open_path((worktree_id, "main.rs"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let lsp_request_count = Arc::new(AtomicU32::new(0)); + let closure_lsp_request_count = Arc::clone(&lsp_request_count); + fake_server + .handle_request::(move |params, _| { + let task_lsp_request_count = Arc::clone(&closure_lsp_request_count); + async move { + assert_eq!( + params.text_document.uri, + lsp::Url::from_file_path("/a/main.rs").unwrap(), + ); + 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, + label: lsp::InlayHintLabel::String(i.to_string()), + kind: None, + text_edits: None, + tooltip: None, + padding_left: None, + padding_right: None, + data: None, + }])) + } + }) + .next() + .await; + + cx.foreground().run_until_parked(); + editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |s| { + s.select_ranges([Point::new(10, 0)..Point::new(10, 0)]) + }) + }); + 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)); + assert_eq!(editor.inlay_hint_cache().version, 1); + }); + } + pub(crate) fn init_test(cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); From 558367dc8babf93484db741ad1235014501d58f0 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 16:19:44 +0300 Subject: [PATCH 101/105] Optimize query ranges tracking --- crates/editor/src/inlay_hint_cache.rs | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index b06a7200902..d014e714880 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -97,7 +97,7 @@ impl TasksForRanges { let ranges_to_query = match invalidate { InvalidationStrategy::None => { let mut ranges_to_query = Vec::new(); - let mut last_cache_range_stop = None::; + let mut latest_cached_range = None::<&mut Range>; for cached_range in self .sorted_ranges .iter_mut() @@ -114,12 +114,13 @@ impl TasksForRanges { .is_le() }) { - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) + 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(last_cache_range_stop..cached_range.start); + ranges_to_query.push(latest_cached_range.end..cached_range.start); + cached_range.start = latest_cached_range.end; } } None => { @@ -129,19 +130,28 @@ impl TasksForRanges { .is_lt() { ranges_to_query.push(query_range.start..cached_range.start); + cached_range.start = query_range.start; } } } - last_cache_range_stop = Some(cached_range.end); + latest_cached_range = Some(cached_range); } - match last_cache_range_stop { - Some(last_cache_range_stop) => { - if last_cache_range_stop.offset.saturating_add(1) < query_range.end.offset { - ranges_to_query.push(last_cache_range_stop..query_range.end); + 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), + 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 @@ -154,9 +164,6 @@ impl TasksForRanges { }; if !ranges_to_query.is_empty() { - self.sorted_ranges.extend(ranges_to_query.clone()); - self.sorted_ranges - .sort_by(|range_a, range_b| range_a.start.cmp(&range_b.start, buffer_snapshot)); self.tasks.push(spawn_task(ranges_to_query)); } } From 336fbb3392ece0db7d78e778257b6e652bf81dab Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 18:37:50 +0300 Subject: [PATCH 102/105] Clip offsets in inlay hint queries --- crates/editor/src/inlay_hint_cache.rs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d014e714880..24aa84ee88b 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -17,6 +17,7 @@ use project::InlayHint; use collections::{hash_map, HashMap, HashSet}; use language::language_settings::InlayHintSettings; +use sum_tree::Bias; use util::post_inc; pub struct InlayHintCache { @@ -500,19 +501,17 @@ fn determine_query_range( let buffer = excerpt_buffer.read(cx); let excerpt_visible_len = excerpt_visible_range.end - excerpt_visible_range.start; - let start = buffer.anchor_before( - excerpt_visible_range - .start - .saturating_sub(excerpt_visible_len) - .max(full_excerpt_range.start.offset), - ); - let end = buffer.anchor_after( - excerpt_visible_range - .end - .saturating_add(excerpt_visible_len) - .min(full_excerpt_range.end.offset) - .min(buffer.len()), - ); + 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 } else { From 4b3273182ac90b4d2df9c5e7cd13b03665a1dfe6 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 19:20:20 +0300 Subject: [PATCH 103/105] Do not filter out hints to be removed --- crates/editor/src/inlay_hint_cache.rs | 29 +++------------------------ 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 24aa84ee88b..d6b45629a48 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -659,19 +659,6 @@ fn calculate_hint_updates( visible_hints .iter() .filter(|hint| hint.position.excerpt_id == query.excerpt_id) - .filter(|hint| { - contains_position(&fetch_range, hint.position.text_anchor, buffer_snapshot) - }) - .filter(|hint| { - fetch_range - .start - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&hint.position.text_anchor, buffer_snapshot) - .is_ge() - }) .map(|inlay_hint| inlay_hint.id) .filter(|hint_id| !excerpt_hints_to_persist.contains_key(hint_id)), ); @@ -685,16 +672,6 @@ fn calculate_hint_updates( .filter(|(cached_inlay_id, _)| { !excerpt_hints_to_persist.contains_key(cached_inlay_id) }) - .filter(|(_, cached_hint)| { - fetch_range - .start - .cmp(&cached_hint.position, buffer_snapshot) - .is_le() - && fetch_range - .end - .cmp(&cached_hint.position, buffer_snapshot) - .is_ge() - }) .map(|(cached_inlay_id, _)| *cached_inlay_id), ); } @@ -2009,12 +1986,12 @@ mod tests { assert_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); - assert_eq!(lsp_request_count.load(Ordering::Acquire), 3, "Should query for hints after the scroll and again after the edit"); - let expected_layers = vec!["1".to_string(), "2".to_string(), "3".to_string()]; + 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), "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, 3, "Should update the cache for every LSP response with hints added"); + assert_eq!(editor.inlay_hint_cache().version, 4, "Should update the cache for every LSP response with hints added"); }); } From e0d011e35475b84bb2af1457bb0c6fdc841f6d52 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 20:12:35 +0300 Subject: [PATCH 104/105] Better assert multibuffer edit test results --- crates/editor/src/inlay_hint_cache.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index d6b45629a48..8be72aec46d 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -1901,6 +1901,8 @@ mod tests { }); 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 @@ -1923,12 +1925,11 @@ mod tests { first_scroll.start, expected_initial_query_range_end, "First scroll should start the query right after the end of the original scroll", ); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; assert_eq!( second_scroll.end, lsp::Position::new( visible_range_after_scrolls.end.row - + expected_increment, + + visible_line_count.ceil() as u32, 0 ), "Second scroll should query one more screen down after the end of the visible range" @@ -1953,12 +1954,12 @@ mod tests { ); let mut selection_in_cached_range = visible_range_after_scrolls.end; - selection_in_cached_range.row -= expected_increment; + selection_in_cached_range.row -= visible_line_count.ceil() as u32; selection_in_cached_range }); editor.update(cx, |editor, cx| { - editor.change_selections(Some(Autoscroll::Next), cx, |s| { + editor.change_selections(Some(Autoscroll::center()), cx, |s| { s.select_ranges([selection_in_cached_range..selection_in_cached_range]) }); }); @@ -1979,12 +1980,17 @@ mod tests { cx.foreground().run_until_parked(); editor.update(cx, |editor, cx| { let ranges = lsp_request_ranges.lock().drain(..).collect::>(); - let expected_increment = editor.visible_line_count().unwrap().ceil() as u32; 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_eq!(query_range.start, lsp::Position::new(selection_in_cached_range.row - expected_increment, 0)); - assert_eq!(query_range.end, lsp::Position::new(selection_in_cached_range.row + expected_increment, 0)); + assert!(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, + "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, + "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, + "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()]; From 27bf01c3a82efe050d43358c6c35e7f1e038595f Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 14 Aug 2023 22:42:22 +0300 Subject: [PATCH 105/105] Strip off inlay hints data that should be resolved --- crates/lsp/src/lsp.rs | 4 ++- crates/project/src/lsp_command.rs | 56 ++++++------------------------- 2 files changed, 14 insertions(+), 46 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index 78c858a90c4..e0ae64d8069 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() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f17..a8692257d80 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, - cx: &mut AppContext, + _: &mut AppContext, ) -> proto::InlayHintsResponse { proto::InlayHintsResponse { hints: response @@ -1963,51 +1963,17 @@ 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, - 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), - } + // 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(), + }, + )), }), }) .collect(),